> 技术文档 > 【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程

【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程


1.进程创建

fork函数

pid_t fork()函数就从已存在进程中创建一个进程,新进程为子进程,而原进程就为父进程。
头文件:#include #include
返回值:子进程就返回0,父进程返回当前子进程id,出错返回-1

进程调用 fork ,当控制块转移到内核中 fork 代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程的部分数据结构内容拷贝给子进程
  • 添加子进程到进程的系统进程
  • fork返回,父子进程分别从 fork() 之后的代码开始执行,调度器开始调度进程
    【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程
    【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程

写时拷贝

通常,父子代码是共享的,父子不再写入时候,数据也是共享的,进程共享父进程的内存空间,但内核会标记为 只读 ,当任意一方试图写入,内核会为进程复制一份私有副本,防止进程之间相互影响
【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程
因为有了写时拷贝技术的存在,父子进程就可以数据就可以·分离开来,保证了进程之间的独立性
写时拷贝,是一种延时申请内存的技术,可以提高内存的利用率

fork常规用法

  • 父进程创建子进程,父子进程执行不同的代码段,例如:父进程等待客户端请求,子进程来处理请求或者用子进程来执行另一个不同的程序,子进程通过exec()进程替换来执行另一个任务

fork调用失败原因

  • 系统中有太多进程,而进程条目的数量是有限的,就会导致创建子进程失败
  • 实际用户的进程数受到了限制

fork() 行为总结

行为 父进程 子进程 fork() 返回值 子进程 PID(>0) 0 执行起点 fork() 后的下一行代码 fork() 后的下一行代码 内存修改 触发写时拷贝(COW) 触发写时拷贝(COW) 典型用途 管理子进程 执行新任务

(注意:父子进程两个执行流谁先执行完取决于调度器)

2.进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核结构和对应的代码和数据

进程退出场景

  1. 代码运行完毕,正确退出(从main返回,return 0)
  2. 代码运行完毕,异常退出 (exit(1))
  3. 代码异常终止·(ctrl + c)

退出码

我们通过 退出码 得到最近一次执行程序的退出状态,来了解程序是否正常退出
Linux 退出码:
【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程
我们可以通过strerror函数来查看对应退出码的描述

进程退出码查看

Linux 进程退出我们可以通过echo $?来查看进程退出码。
比如我正确执行一条指令,它的退出码就为0(Linux中执行指令都是通过bash创建子进程来执行的)
【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程

进程退出方法

  1. _exit函数
    _exit 函数是系统调用函数
#include void _exit(int status);

参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

  1. exit函数
    exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他工作:
  • 执行用户通过atexiton_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
    【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程
    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]#
  1. 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
  1. 函数方法:
    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
  1. wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。

  2. 如果传递NULL,表⽰不关心⼦进程的退出状态信息。

  3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给传递父进程

status 可以当做一个位图来看,其具体实现细节如下图:
【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程

退出状态和终止信号以及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 数组 是 唯一直接调用内核的系统调用(其他函数均封装它)

使用方法:

  1. execl
    特点:参数列表形式传参、需完整路径、继承环境变量
#include #include int main() { printf(\"execl调用示例\\n\"); // 执行/bin/ls,参数列表需以NULL结尾 if (execl(\"/bin/ls\", \"ls\", \"-l\", NULL) == -1) { perror(\"execl失败\"); } return 0;}
  1. execlp
    特点:参数列表传参、自动搜索PATH环境变量
#include #include int main() { printf(\"execlp调用示例\\n\"); // 自动搜索PATH中的\"ls\"可执行文件 if (execlp(\"ls\", \"ls\", \"-l\", NULL) == -1) { perror(\"execlp失败\"); } return 0;}
  1. 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;}
  1. execv
    特点:参数数组传参、需完整路径、继承环境变量
#include #include int main() { char *argv[] = {\"ls\", \"-l\", NULL}; printf(\"execv调用示例\\n\"); if (execv(\"/bin/ls\", argv) == -1) { perror(\"execv失败\"); } return 0;}
  1. execvp
    特点:参数数组传参、自动搜索PATH环境变量
#include #include int main() { char *argv[] = {\"ls\", \"-l\", NULL}; printf(\"execvp调用示例\\n\"); if (execvp(\"ls\", argv) == -1) { perror(\"execvp失败\"); } return 0;}
  1. 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;}
总结

总结下来就是 是否需要完整路径、自定义环境变量,以及参数是列表还是数组

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  1. l(list) : 表示参数采用列表
  2. v(vector) : 参数用数组
  3. p(path) : 有p⾃动搜索环境变量PATH
  4. e(env) : 表示自己维护环境变量
    【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程

4,简单shell的实现

**前言:**我们在命令行执行命令时都是由 bash 创建子进程,然后由子进程 exec进程替换 执行对应命令。
【Linux】进程控制和Shell的简易实现_linxu shell 启动子进程
shell脚本的流程:

  1. 获取命令行
  2. 解析命令行
  3. fork()创建子进程
  4. 替换子进程
  5. 父进程等待子进程退出

源码实现:

#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;}