【Linux】进程控制(2):进程等待进程替换

【Linux】进程控制(2):进程等待进程替换 目录一 进程等待1 为什么2 是什么3 结论4 进程等待的方式1wait方法2waitpid方法3获取子进程status4阻塞等待和非阻塞等待非阻塞等待李四打电话催张三那种2. 阻塞等待李四攥着电话不挂那种一句话总结二 进程程序替换1 理解2 替换原理3 父子进程版本4 替换函数一 进程等待1 为什么父进程提供wait/waitpid这样的系统调用来等待子进程2 是什么status是输出型参数可以通过传递一个整数的形式来获得子进程的退出信息3 结论结论1、原则上一般都是要保证父进程最后退出2、父进程要通过wait等待子进程3、如果子进程不退出父进程就会阻塞在wait这里等待子进程死亡。4 进程等待的方式1wait方法#includesys/types.h #includesys/wait.h pid_t wait(int* status); 返回值 成功返回被等待进程pid失败返回-1。 参数 输出型参数获取⼦进程退出状态,不关⼼则可以设置成为NULL2waitpid方法pid_ t waitpid(pid_t pid, int *status, int options); 返回值 当正常返回的时候waitpid返回收集到的⼦进程的进程ID 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在 参数 pid Pid-1,等待任⼀个⼦进程。与wait等效。 Pid0.等待其进程ID与pid相等的⼦进程。 status: 输出型参数 WIFEXITED(status): 若为正常终⽌⼦进程返回的状态则为真。查看进程 是否是正常退出 WEXITSTATUS(status): 若WIFEXITED⾮零提取⼦进程退出码。查看进程 的退出码 options:默认为0表⽰阻塞等待 WNOHANG: 若pid指定的⼦进程没有结束则waitpid()函数返回0不予以等 待。若正常结束则返回该⼦进程的ID。如果子进程已经退出调用 wait/waitpid 时wait/waitpid 会立即返回并且释放资源获得子进程退出信息。如果在任意时刻调用 wait/waitpid子进程存在且正常运行则进程可能阻塞。如果不存在该子进程则立即出错返回。3获取子进程status当前进程的退出信息叫做status• wait和waitpid都有一个status参数该参数是一个输出型参数由操作系统填充。• 如果传递NULL表示不关心子进程的退出状态信息。• 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。• status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16比特位status是一个int类型所以它有32位比特位但其实它的高16位是不用的只使用低16位。在低16位中低8位和次低8位代表的内容是不一样的如下图所示上述这对应一个正常退出的场景终止信号 0 → 不是被信号杀死是正常 exit()/return 退出退出码 1 → 子进程调用了 exit(1) 或 return 1core dump 标志 0 → 没有 core dump正常退出本来就不会有这个时候有些uu可能就会疑问了退出信号不是没有0吗为什么这里的退出信号是0因为退出码占据8位而2^8256所以退出码的范围是【0255】退出信号没有0因为0表示正常退出非0表示不正常退出父进程获取子进程的退出信息退出码退出信号问题wait/waitpid 是怎样获取子进程的退出信息的1子进程退出退出信息是维护在PCB中的包括将自己设置为僵尸2父进程wait子进程本质就是去读取子进程PCB内部的退出信息结论通过检查子进程Z状态获取子进程task_struct 内部记录的子进程退出信息数据的我们在上面看到waitpid的第三个参数为options--0表示阻塞等待那什么是阻塞等待什么是非阻塞等待4阻塞等待和非阻塞等待WNOHANG宏非阻塞等待字面意思是 “No Hang”不挂起 / 不阻塞我们使用wait/waitpid的时候用一个故事解释阻塞等待和非阻塞等待非阻塞等待李四打电话催张三那种你点了一份外卖没选 “准时宝”你不会一直盯着手机等该追剧追剧、该看书看书、该打游戏打游戏每隔一会儿你就打开外卖 App 看一眼「到哪了取餐了吗快到了吗」要是外卖还没到你立刻切回自己的事继续做自己的直到看到「已送达」你才起身去拿餐。对应到计算机里你 父进程李四外卖小哥 子进程张三打开 App 看一眼 调用waitpidWNOHANG非阻塞轮询特点你不傻等该干嘛干嘛只是定期 “查岗”。2. 阻塞等待李四攥着电话不挂那种你点了一份外卖选了 “必须当面签收”你放下手里所有事坐在门口 / 电话旁眼睛盯着手机、耳朵听着门铃期间你什么都干不了既不追剧也不看书就死等外卖敲门直到外卖小哥按门铃你开门拿餐这段时间才算结束。对应到计算机里你 父进程李四外卖小哥 子进程张三坐在门口死等 调用wait/waitpid不带WNOHANG特点你完全卡住啥也干不了直到子进程完事。一句话总结非阻塞等待我不傻等我该干嘛干嘛抽空看看你完事没阻塞等待我啥也不干了就盯着你等你完事我再动。waitpid的返回值成功返回值0返回的是子进程的pid失败返回值0 返回-1状态没有改变返回0非阻塞轮循最明显的特点父进程不会卡在那结论最佳实践---目前阻塞等待是最佳实践进程的阻塞等待方式int main() { pid_t pid; pid fork(); // 创建子进程父进程返回子进程PID子进程返回0 if(pid 0){ // fork失败 printf(%s fork error\n,__FUNCTION__); return 1; } else if( pid 0 ){ // 子进程分支 printf(child is run, pid is : %d\n,getpid()); sleep(5); // 子进程睡眠5秒模拟业务逻辑 exit(257); // 子进程退出传入257超出0-255范围 } else{ // 父进程分支 int status 0; // 阻塞等待任意子进程-1表示等待所有子进程退出等待5秒 pid_t ret waitpid(-1, status, 0); printf(this is test for wait\n); // WIFEXITED(status)判断子进程是否正常退出ret pid确认是目标子进程 if( WIFEXITED(status) ret pid ){ printf(wait child 5s success, child return code is :%d.\n,WEXITSTATUS(status)); }else{ printf(wait child failed, return.\n); return 1; } } return 0; } 运⾏结果: [rootlocalhost linux]# ./a.out child is run, pid is : 45110 this is test for wait wait child 5s success, child return code is :1.传入 exit(257) 时系统会对数值做 模 256 运算即取低 8 位257 % 256 1所以 WEXITSTATUS(status) 解析出的结果就是 1进程的非阻塞等待方式#include stdio.h #include stdlib.h #include sys/wait.h #include unistd.h #include vector // 函数指针类型定义 typedef void (*handler_t)(); // 函数指针数组使用std命名空间避免编译错误 std::vectorhandler_t handlers; // 临时任务1 void fun_one() { printf(这是⼀个临时任务1\n); } // 临时任务2 void fun_two() { printf(这是⼀个临时任务2\n); } // 注册任务到数组中 void Load() { handlers.push_back(fun_one); handlers.push_back(fun_two); } // 执行所有注册的临时任务 void handler() { // 首次调用时加载任务后续无需重复加载 if (handlers.empty()) { Load(); } // 遍历执行所有任务 for (auto iter : handlers) { iter(); } } int main() { pid_t pid; pid fork(); if (pid 0) { printf(%s fork error\n, __FUNCTION__); return 1; } else if (pid 0) { // 子进程逻辑 printf(child is run, pid is : %d\n, getpid()); sleep(5); // 子进程睡眠5秒模拟业务 exit(1); // 子进程退出退出码1 } else { // 父进程逻辑 int status 0; pid_t ret 0; // 非阻塞轮询等待子进程退出 do { // WNOHANG非阻塞子进程运行时返回0退出时返回子进程PID ret waitpid(-1, status, WNOHANG); if (ret 0) { printf(child is running\n); // 执行临时任务 handler(); // 关键添加1秒休眠避免CPU空转 sleep(1); } } while (ret 0); // 子进程未退出则继续轮询 // 检查子进程是否正常退出且等待的是目标子进程 if (WIFEXITED(status) ret pid) { printf(wait child 5s success, child return code is :%d.\n, WEXITSTATUS(status)); } else { printf(wait child failed, return.\n); return 1; } } return 0; }二 进程程序替换fork() 之后父子进程各自执行父进程代码的一部分。如果子进程想执行一个全新的程序就需要通过进程的程序替换来完成这个功能程序替换是通过特定的接口加载磁盘上的一个全新的程序代码和数据并将其加载到调用进程的地址空间中1 理解进程程序替换就是让程序执行一个全新的程序把当前程序的代码和数据段用新程序的代码和数据段替换掉。修改页表改页表的映射关系之后让原来进程的PCB从新进程的main函数从0开始运行相当于用了原来进程的壳换了代码和数据这个工作就叫做程序替换程序替换不会创建新进程它的目标是让进程执行一个全新的代码我们通过一段代码来理解一下1 #includestdio.h 2 #includeunistd.h 3 4 int main() 5 { 6 printf(我是父进程: pid: %d,ppid: %d\n,getpid(),getppid()); 7 execl(/usr/bin/ls,ls,-a,-l,NULL); 8 printf(我正常退出了...\n); 9 return 0; 10 }运行结果为execl是替换函数如果execl使用成功那么就不会执行execl之后的所有代码我们上面的运行结果也证明了这一点创建一个进程是先创建内核数据结构还是先加载具体程序的代码和数据先创建内核数据结构可以把进程理解为 “运行中的程序”创建进程的过程就像 “开一家新店”先建壳创建内核数据结构先去工商局注册创建进程内核结构体、租店面分配 PID、内存空间、CPU 调度资源等—— 这一步完成后“进程” 这个 “空架子” 已经存在但还没有具体的业务程序代码再填内容加载代码和数据等店面 / 资质都搞定了再把商品、设备、员工程序的代码和数据搬进去 —— 这一步完成后进程才真正具备执行逻辑。用子进程进行程序替换不会影响父进程一旦程序替换成功就会执行新程序的代码原来的代码就会被直接覆盖不会被执行程序替换成功没有返回值只有失败才会有返回值2 替换原理用fork创建子进程后子进程执行的是和父进程相同的程序但有可能执行不同的代码分支子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时该进程的用户空间代码和数据完全被新程序替换从新程序的启动例程开始执行。调用exec并不创建新进程所以调用exec前后该进程的 ID 并未改变3 父子进程版本4 替换函数其实有六种以exec开头的函数,统称exec函数:#include unistd.h 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[]);要执行一个程序需要做什么1 找到对应二进制文件的磁盘位置2 执行程序需要什么选项要怎么执行对execl函数参数的解释几个细节1 省略输入有时候也能运行但是不建议这么做2 execl的path参数不建议省略路径3 程序替换的本质如果当前进程不fork子进程execl不就是加载程序到内存的过程所以execl是加载器的底层接口程序替换能替换系统命令也能替换自己写的程序4 命令行参数通过父进程或者操作系统传递给子进程的通过exec传递给子进程5 c程序替换能把C调用起来那能调用shell脚本pythonJava吗任何程序只要能跑起来就是进程只要是进程c/c就能把它跑起来和是什么语言无关命名解释函数名参数传递方式是否自动搜索 PATH环境变量处理方式核心特点execl列表l❌ 不搜索必须传完整路径✅ 继承当前环境变量固定参数列表、需完整路径execlp列表l✅ 搜索 PATH可传文件名✅ 继承当前环境变量固定参数列表、自动找路径execle列表l❌ 不搜索必须传完整路径❌ 自定义环境变量需自己传固定参数列表、自定义环境execv数组v❌ 不搜索必须传完整路径✅ 继承当前环境变量动态参数数组、需完整路径execvp数组v✅ 搜索 PATH可传文件名✅ 继承当前环境变量动态参数数组、自动找路径execve数组v❌ 不搜索必须传完整路径❌ 自定义环境变量需自己传系统调用底层实现动态参数数组、自定义环境v表示vectorp表示PATHl表示列表表示参数用列表传递如果不想使用默认的环境变量想搭建全新的变量可以使用带e的环境变量进行程序替换的时候即便我们没有显式传递环境变量表信息但是子进程依旧能够获得对应的环境变量信息。环境变量表也以NULL结尾exec调用举例如下#include unistd.h int main() { char *const argv[] {ps, -ef, NULL}; char *const envp[] {PATH/bin:/usr/bin, TERMconsole, NULL}; execl(/bin/ps, ps, -ef, NULL); // 带p的可以使⽤环境变量PATH⽆需写全路径 execlp(ps, ps, -ef, NULL); // 带e的需要⾃⼰组装环境变量 execle(ps, ps, -ef, NULL, envp); execv(/bin/ps, argv); // 带p的可以使⽤环境变量PATH⽆需写全路径 execvp(ps, argv); // 带e的需要⾃⼰组装环境变量 execve(/bin/ps, argv, envp); exit(0); }事实上只有 execve 是真正的系统调用其它五个函数最终都调用 execve所以 execve 在 man 手册第 2 节其它函数在 man 手册第 3 节。这些函数之间的关系如下图所示。下图为 exec 函数簇一个完整的例子