【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程
1.进程创建
fork函数
pid_t fork()
函数就从已存在进程中创建一个进程,新进程为子进程,而原进程就为父进程。
头文件:#include
#include
返回值:子进程就返回0
,父进程返回当前子进程id
,出错返回-1
进程调用 fork ,当控制块转移到内核中 fork 代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程的部分数据结构内容拷贝给子进程
- 添加子进程到进程的系统进程
- fork返回,父子进程分别从 fork() 之后的代码开始执行,调度器开始调度进程
写时拷贝
通常,父子代码是共享的,父子不再写入时候,数据也是共享的,进程共享父进程的内存空间,但内核会标记为 只读 ,当任意一方试图写入,内核会为进程复制一份私有副本,防止进程之间相互影响因为有了写时拷贝技术的存在,父子进程就可以数据就可以·分离开来,保证了进程之间的独立性
写时拷贝,是一种延时申请内存的技术,可以提高内存的利用率
fork常规用法
- 父进程创建子进程,父子进程执行不同的代码段,例如:父进程等待客户端请求,子进程来处理请求或者用子进程来执行另一个不同的程序,子进程通过exec()进程替换来执行另一个任务
fork调用失败原因
- 系统中有太多进程,而进程条目的数量是有限的,就会导致创建子进程失败
- 实际用户的进程数受到了限制
fork()
行为总结
fork()
返回值fork()
后的下一行代码fork()
后的下一行代码(注意:父子进程两个执行流谁先执行完取决于调度器)
2.进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核结构和对应的代码和数据
进程退出场景
- 代码运行完毕,正确退出(从main返回,return 0)
- 代码运行完毕,异常退出 (exit(1))
- 代码异常终止·(ctrl + c)
退出码
我们通过 退出码 得到最近一次执行程序的退出状态,来了解程序是否正常退出
Linux 退出码:
我们可以通过strerror
函数来查看对应退出码的描述
进程退出码查看
Linux 进程退出我们可以通过echo $?
来查看进程退出码。
比如我正确执行一条指令,它的退出码就为0(Linux中执行指令都是通过bash创建子进程来执行的)
进程退出方法
- _exit函数
_exit 函数是系统调用函数
#include void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
- exit函数
exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他工作:
- 执行用户通过
atexit
或on_exit
定义的清理函数。
#include int on_exit(void (*function)(int , void *), void *arg);
#include #include void cleanup(int status, void *msg) { printf(\"Exit status: %d, Message: %s\\n\", status, (char *)msg);}int main() { on_exit(cleanup, (void *)\"Bye!\"); // 注册带参数的清理函数 printf(\"Main function\\n\"); exit(42); // 退出状态 42}
输出:
Main functionExit status: 42, Message: Bye!
- 关闭所有打开的流,所有缓存数据均被写入
- 调用_exit
exit和_exit函数的区别:exit是对_exit系统调用封装的库函数,它就可以刷新用户缓冲区数据,也就是fflush函数刷新用户缓冲区数据到内核缓冲区中,并且还会调用atexit和on_exit清理函数
int main(){printf(\"hello\");exit(0);}运⾏结果:[root@localhost linux]# ./a.outhello[root@localhost linux]#int main(){printf(\"hello\");_exit(0);}运⾏结果:[root@localhost linux]# ./a.out[root@localhost linux]#
- return返回
return 是一种常见的退出进程方法,return n 等同于执行 exit(n),因为调用 main 的运行函数会将 main 的返回值当 exit 的参数。
3.进程等待
僵尸进程
僵尸进程就是已经终止但未被父进程回收的进程。
进程等待
如果想要对僵尸进程进行处理,就需要父进程父进程回收子进程的方式来解决子进程的僵尸状态。
进程等待方法
wait
- 函数方法:
#include
#include
pid_t wait(int* status);
参数: status:保存子进程退出状态(需用宏解析,如 WEXITSTATUS)。
返回值: 成功:返回终止的子进程 PID。
失败:返回 -1(如无子进程)。
#include #include #include int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 printf(\"Child PID: %d\\n\", getpid()); _exit(42); // 子进程退出码 42 } else { // 父进程 int status; pid_t child_pid = wait(&status); // 阻塞等待 printf(\"Child %d exited with status: %d\\n\", child_pid, WEXITSTATUS(status)); } return 0;}
输出:
Child PID: 1234Child 1234 exited with status: 42
waitpid
- 函数方法:
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1: 等待任⼀个⼦进程。与wait等效。
Pid>0: 等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
options: 默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。
等待子进程:
pid_t child_pid = fork();if (child_pid == 0) { // 子进程 _exit(10);} else { int status; // 只等待 child_pid 的子进程 waitpid(child_pid, &status, 0); printf(\"Child %d exited: %d\\n\", child_pid, WEXITSTATUS(status));}
非阻塞轮询:
int status;pid_t pid = waitpid(-1, &status, WNOHANG);if (pid > 0){ printf(\"Child %d exited.\\n\", pid);}else if (pid == 0){ printf(\"No child exited yet.\\n\");} else{ perror(\"waitpid failed\");}
获取子进程status
-
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
-
如果传递NULL,表⽰不关心⼦进程的退出状态信息。
-
否则,操作系统会根据该参数,将子进程的退出信息反馈给传递父进程
status 可以当做一个位图来看,其具体实现细节如下图:
退出状态和终止信号以及core dump表示方式
1.正常终止
退出状态:(status >> 8)&0xFF
2. 被信号所杀
退出信号:status&0x7F
core dump(核心转储):(status>>7)&1
进程替换
替换原理
用 fork 创建的子进程和父进程执行的是同一个程序,如果子进程想要执行另一个程序,往往需要调用 exec 函数来替换当前程序。当进程调用 exec 程序后,进程代码和数据就完全被新进程替换,从新程序的启动例程开始执行,并且执行完新进程后并不会返回之前子进程执行的代码继续执行,而是直接退出,并且调用exec并不会创建新进程,所以调用exec前后该进程的id并没有改变
返回值处理: 所有函数调用成功时不返回,失败时返回-1并设置errno。
替换函数
其中有六个exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg, ...,char *const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execve(const char *path, char *const argv[], char *const envp[]);
execl
/bin/ls
)NULL
结尾(如 \"ls\", \"-l\", NULL
execlp
PATH
搜索文件名execlp(\"ls\", ...)
)execle
envp
数组envp
(如 execle(..., envp)
))execv
argv[]
NULL
结尾(如 char *argv[] = {\"ls\", \"-l\", NULL}
))execvp
argv[]
PATH
搜索文件名execvp(\"ls\", argv)
)execve
argv[]
envp
数组使用方法:
- execl
特点:参数列表形式传参、需完整路径、继承环境变量
#include #include int main() { printf(\"execl调用示例\\n\"); // 执行/bin/ls,参数列表需以NULL结尾 if (execl(\"/bin/ls\", \"ls\", \"-l\", NULL) == -1) { perror(\"execl失败\"); } return 0;}
- execlp
特点:参数列表传参、自动搜索PATH环境变量
#include #include int main() { printf(\"execlp调用示例\\n\"); // 自动搜索PATH中的\"ls\"可执行文件 if (execlp(\"ls\", \"ls\", \"-l\", NULL) == -1) { perror(\"execlp失败\"); } return 0;}
- execle
特点:参数列表传参、需完整路径、自定义环境变量
#include #include int main() { char *envp[] = {\"CUSTOM_ENV=test\", \"PATH=/bin\", NULL}; printf(\"execle调用示例\\n\"); // 传递自定义环境变量envp if (execle(\"/bin/ls\", \"ls\", \"-l\", NULL, envp) == -1) { perror(\"execle失败\"); } return 0;}
- execv
特点:参数数组传参、需完整路径、继承环境变量
#include #include int main() { char *argv[] = {\"ls\", \"-l\", NULL}; printf(\"execv调用示例\\n\"); if (execv(\"/bin/ls\", argv) == -1) { perror(\"execv失败\"); } return 0;}
- execvp
特点:参数数组传参、自动搜索PATH环境变量
#include #include int main() { char *argv[] = {\"ls\", \"-l\", NULL}; printf(\"execvp调用示例\\n\"); if (execvp(\"ls\", argv) == -1) { perror(\"execvp失败\"); } return 0;}
- execve
特点:参数数组传参、需完整路径、自定义环境变量、唯一系统调用
#include #include int main() { char *argv[] = {\"ls\", \"-l\", NULL}; char *envp[] = {\"CUSTOM_ENV=test\", \"PATH=/bin\", NULL}; printf(\"execve调用示例\\n\"); if (execve(\"/bin/ls\", argv, envp) == -1) { perror(\"execve失败\"); } return 0;}
总结
总结下来就是 是否需要完整路径、自定义环境变量,以及参数是列表还是数组
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p⾃动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
4,简单shell的实现
**前言:**我们在命令行执行命令时都是由 bash 创建子进程,然后由子进程 exec进程替换 执行对应命令。
shell脚本的流程:
- 获取命令行
- 解析命令行
- fork()创建子进程
- 替换子进程
- 父进程等待子进程退出
源码实现:
#include #include #include #include #include #include #include #include using namespace std;const int basesize = 1024;const int gnum = 64;//环境变量表和命令行参数表的大小char* genv[gnum];char* gargv[gnum];char buff[basesize];//辅助数组,存储输入的命令char pwd[basesize];char pwdenv[basesize];int gargc = 0;int lastcode = 0;string getUsername(){ string username = getenv(\"USER\"); return username == \"\" ? \"None\" : username;}string getHostname(){ char hostname[20]; int n = gethostname(hostname,sizeof(hostname)); if(n < 0) { perror(\"gethostname\"); exit(1); } return hostname;}string GetPwd(){ if(getcwd(pwd,sizeof(pwd)) == nullptr) { return \"Node\"; } snprintf(pwdenv,sizeof(pwdenv),\"PWD=%s\",pwd); for(int i = 0;genv[i];i++) { string str = genv[i]; char* before = (char*)str.substr(0,3).c_str(); if(strcmp(before,\"PWD\") == 0) { strncpy(genv[i],pwdenv,strlen(pwdenv)); genv[i][strlen(pwdenv)] = 0; return pwd; } }}string Lastdir(){ string cur = GetPwd(); if(cur == \"/\" || cur == \"None\") return cur; size_t pos = cur.rfind(\'/\'); if(pos == std::string::npos) return cur; return cur.substr(pos + 1);}void PrintCommand(){ string username = getUsername(); string hostname = getHostname(); string pwd = Lastdir(); if(username != \"None\" && hostname != \"None\" && pwd != \"None\") { cout << username << \"@\" << hostname << \":\" << pwd << \"$\"; fflush(stdout); } else exit(1);}bool GetCommand(){ memset(buff,0,sizeof buff); char* result = fgets(buff,basesize,stdin); if(result == nullptr) { return false; } //cout << result << endl; buff[strlen(buff) - 1] = 0; if(strlen(buff) == 0) return false; return true;}void ParseCommand(){ const char* sep = \" \"; gargc = 0; gargv[gargc++] = strtok(buff,sep); while((bool)(gargv[gargc++] = strtok(nullptr,sep))); //cout << gargc << endl; gargv[gargc] = nullptr; gargc--;}bool ExecuteCommand(){ pid_t id = fork(); if(id < 0) return false; if(id == 0) { //子进程 // 1. 执行命令 cout << \"gragvp[0] = \" << gargv[0] << endl; int ret = execvpe(gargv[0], gargv, genv); //char* argv[] = { // \"ls\", // \"-l\", // \"-a\", // nullptr // }; //int ret = execvpe(\"ls\",gargv,genv); cout << errno << endl; lastcode = 1; // 2. 退出 exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { lastcode = WEXITSTATUS(status); return true; } lastcode = 100; return false; //pid_t id = fork(); //if(id == 0) //{ // cout << gargv[0] << endl; // int ret = execvpe(gargv[0],gargv,genv); // if(ret == -1) // { // cout << errno << endl; // return false; // } // return true; //} //int status; //pid_t wid = waitpid(id,&status,0); //if(wid > 0) //{ // cout << \"等待子进程成功\" << endl; // return true; //} //return false;}void AddEnv(){ int index = 0; while(genv[index]) { index++; } genv[index] = (char*)malloc(strlen(gargv[1] + 1)); strncpy(genv[index],gargv[1],strlen(gargv[1]) + 1); genv[++index] = nullptr;}bool CheckCommand(){ if(strcmp(gargv[0],\"cd\") == 0) { if(gargc == 2) { chdir(gargv[1]); GetPwd(); lastcode = 0; } else { lastcode = 2; } return true; } else if(strcmp(gargv[0],\"echo\") == 0) { if(gargc == 2) { if(gargv[1][0] == \'$\') { if(gargv[1][1] == \'?\') { printf(\"%d\\n\",lastcode); lastcode = 0; } } else { printf(\"%s\\n\",gargv[1]); lastcode = 0; } } else lastcode = 3; return true; } else if(strcmp(gargv[0],\"env\") == 0) { for(int i = 0;genv[i];i++) cout << genv[i] << endl; lastcode = 0; return true; } else if(strcmp(gargv[0],\"export\") == 0) { if(gargc == 2) { AddEnv(); lastcode = 0; return true; } else lastcode = 4; return true; } return false;}void debug(){ for(int i = 0;genv[i];i++) { cout << genv[i] << endl; } cout << \"//////////////\" << endl; for(int i = 0;gargv[i];i++) { cout << gargv[i] << endl; }}void Initenv(){ extern char** environ; int index = 0; while(environ[index]) { genv[index] = (char*)malloc(strlen(environ[index] + 1)); strncpy(genv[index],environ[index],strlen(environ[index])); index++; } genv[index] = nullptr;}int main(){ Initenv(); while(1) { PrintCommand();//打印命令提示符 //sleep(10); bool ret = GetCommand();//从标准输入获取命令 if(ret = true) ParseCommand();//分析命令 else continue; // cout << endl; // debug(); if(CheckCommand()) { continue; } ExecuteCommand();//处理命令 } return 0;}