Linux 18 进程控制
🔥个人主页:Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
一.进程创建
1.1再识fork
关于fork的基础使用,在前面的文章已经讲过,此处不再赘述
进程调⽤ fork ,当控制转移到内核中的 fork 代码后,内核做:分配新的内存块和内核数据结构给⼦进程将⽗进程部分数据结构内容拷⻉⾄⼦进程添加⼦进程到系统进程列表当中fork 返回,开始调度器调度
1.2 写时拷贝
当父子数据未被修改写入时,共用同一份资源,但其中一方数据进行了写入,便会写时拷贝一份

可以看到修改前,数据段都为r,修改后,不只为r,原理是对权限为r的数据段进行写入,OS会检查,发现错误,就会自动修改权限,进行拷贝
同时我们发现没被修改的代码段依旧共用同一份资源
因此,写时拷贝的好处
1.减少创建时间
2.减少内存浪费
1.3 fork调用失败原因
• 系统中有太多的进程• 实际⽤⼾的进程数超过了限制
二.进程终止
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1 进程退出场景
代码运⾏完毕,结果正确代码运⾏完毕,结果不正确代码异常终⽌
子进程是由父进程创建的,是要让完成某些事情的,子进程要反馈父进程某些信息
查看最近一个进程的退出码,可以通过 echo $ ?
代码正常运行,结果正确
退出码:
代码运行完毕,结果不正确
退出码
注:
再查看退出码,又会为0,因为是查看最近一个进程的退出码
常见退出方式
正常终⽌(可以通过 echo $? 查看进程退出码):1. 从main返回2. 调⽤exit3. _exit异常退出:• ctrl + c,信号终⽌

2.2exit && _exit
#include <unistd.h>void _exit(int status);参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值#include <unistd.h>void exit(int status);
前面我们学过,当有sleep时,会写入缓存区,当\n可以刷新缓存区,接下来让我们看这两个,会发现无\n时,_exit不会刷新缓存区,而exit会
原因在于
1. 执⾏⽤⼾通过 atexit或on_exit定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写⼊3. 调⽤_exit(库调用系统调用,封装了_exit)

因此我们可以得到一个结论
我们前面所说的缓存区不在操作系统内部的缓存区,而是库的缓存区
三.进程等待
• 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存泄漏。• 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也没有办法杀死⼀个已经死去的进程。• 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。• ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息
3.1等待方式
wait
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULLwaitpid
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。3.2获取status和信号
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。•如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。•否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。•status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位):

include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 #include<string.h> 5 6 int main() 7 { 8 pid_t id =fork(); 9 if(id ==0) 10 { 11 //子进程 12 int cnt =5; 13 while(cnt--) 14 { 15 printf("子进程 pid:%d ppid%d\n",getpid(),getppid()); 16 sleep(1); 17 18 } 19 //退出的是子进程的程序,父进程不受影响 20 exit(0); 21 22 } 23 //procid_t ret = wait(NULL); 24 int st=0; E> 25 pid_t ret = waitpid(id,&st,0); 26 27 if(ret >0&& (st>>8)&0XFF==0)//id匹配上,且正常退出 28 { 29 printf("wait success rid:%d,exit code :%d\n",ret,(st>>8&0XFF)); 30 } 31 else if(ret>0)//异常退出,信号code不为0 W> 32 printf("wait failed rid:%d\n exit code:%d\n",ret,st&0X7F); 33 sleep(100); 34 35 36 return 0; 37 } 四.阻塞非阻塞等待
阻塞等待即父进程会一直等待子进程完成再继续执行,非阻塞等待则是父进程在等待子进程返回结果时,也会执行自己的程序
当然,上面太过生涩,接下来我们讲两个故事理解
非阻塞等待
为了应对OS期末考试,小明前往并打电话找学霸小李进行复习指导,小李回复,他正在干自己的事,让小明等个10多分钟,后挂断了电话.
在这等待的过程中,小明一边干自己的事(看书等),一边隔一会给小李打个电话,得到小李的回复还未干好,立马挂掉电话
在这期中,小明在等待小李的时候,还自己进行了看错,效率提高
#include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 #include<string.h> 5 typedef void (*func_t)(); 6 #define NUM 5 7 func_t handlers[NUM+1]; 8 void download() 9 { 10 printf("下载任务\n"); 11 } 12 void up() 13 { 14 printf("更新任务\n"); 15 } 16 void flush() 17 { 18 printf("更新任务\n"); 19 } 20 void regist(func_t h[],func_t f) 21 { 22 int i=0; 23 for(;i<NUM;++i) 24 { 25 if(h[i]==NULL)break; 26 } 27 if(i==NUM) return; 28 h[i]=f; 29 h[i++]=NULL; 30 } 31 int main() 32 { 33 regist(handlers,download); 34 regist(handlers,up); 35 regist(handlers,flush); 36 pid_t id =fork(); 37 if(id ==0) 38 { 39 //子进程 40 int cnt =5; 41 while(1) 42 { 43 printf("子进程 pid:%d ppid%d\n",getpid(),getppid()); 44 sleep(1); 45 cnt--; 46 } 47 //退出的是子进程的程序,父进程不受影响 48 exit(10); 49 50 } 51 //procid_t ret = wait(NULL); 52 while(1) 53 { 54 55 56 int st=0; E> 57 pid_t ret = waitpid(id,&st,WNOHANG); 58 if(ret >0)//id匹配上,且正常退出 60 { 61 printf("wait success rid:%d,exit code :%d\n",ret,(st>>8&0XFF)); 62 break; 63 } 64 else if(ret==0) 65 { 66 //函数指针回调哦 67 int i=0; 68 for(;handlers[i];++i) 69 handlers[i](); 70 printf("本轮调用结束\n"); 71 sleep(1); 72 } 73 else 74 { 75 76 printf("调用失败\n"); 77 break; 78 } 79 80 } 81 return 0; 82 } ~ 在上面中,父进程在等待子进程调用时,也在干自己的事,如下载,刷新等,效率得到提高
阻塞等待
在上面的通话中,小明觉得每次打过于麻烦,于是下次找小李复习时,让小李没挂电话了,一直通电话,直到小李干完自己的事
上面这例子就如我们代码运行到cin scanf ,必须我们进行输入,才会完成
五.进程程序替换
⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀ 种 exec 函数以执⾏另⼀个程序。当进程调⽤⼀种 exec 函数时,该进程的⽤⼾空间代码和数据完全被 新程序替换,从新程序的启动例程开始执⾏。调⽤ exec 并不创建新进程,所以调⽤ exec 前后该进程的 id 并未改变。
注,进程程序替换后,会进行覆盖代码于与数据段,后面的程序将不再执行(只要是可执行程序,就可以替换,即使是其他语言的)
5.1 验证id不变与覆盖式写入
#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } 13 return 0; 14 } 发现确实如此

5.2 替换函数
其实有六种以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• 所以 exec 函数只有出错的返回值⽽没有成功的返回值,所有不用对返回值进行判断,有就一定出错了
5.3 命名理解
命名理解这些函数原型看起来很容易混,但只要掌握了规律就很好记。• l(list) : 表⽰参数采⽤列表• v(vector) : 参数⽤数组• p(path) : 有 p ⾃动搜索环境变量 PATH• e(env) : 表⽰⾃⼰维护环境变量
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 | 典型使用场景 |
|---|---|---|---|---|
| execl | 列表 | 不是 | 是 | 明确知道程序路径,参数较少的场景 |
| execlp | 列表 | 是 | 是 | 执行系统命令(如ls),依赖环境变量找程序 |
| execele | 列表 | 不是 | 不是,须自己组装环境变量 | 需要自定义环境变量的场景 |
| execv | 数组 | 不是 | 是 | 参数较多(需存数组),知道程序路径的场景 |
| execvp | 数组 | 是 | 是 | 执行系统命令且参数较多的场景 |
| execve | 数组 | 不是 | 不是,须自己组装环境变量 | 参数较多且需要自定义环境变量的场景 |
5.4 替换函数使用
也可以替换自己写的程序,前面已经验证了
5.4.1 execl
int execl(const char *path, const char *arg, ...);
第一个参数写路径,第二个到第n-1个参数上面是参数包,你就是写你正常写的命令符,最后一个
如果不用NULL结尾,execl会无限制地读取后续内存中的
#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 // execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } 13 execl("user/bin/ls","ls","-a","-l",NULL); 14 return 0; 15 } ~ 5.4.2 execlp
#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 // execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } 13 execlp("ls","-a","-l",NULL); 14 return 0; 15 } ~ 5.4.3 execv
#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 // execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } char *const args[] ={"ls","-a","-l","NULL"}; 13 execv("user/bin/ls",args); 14 return 0; 15 } ~ 5.4.4 execvp
#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 // execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } char *const args[] ={"ls","-a","-l","NULL"}; 13 execv(args); 14 return 0; 15 } ~ 5.4.5 execele
#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 // execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } char *const env[]={(char*const) "MYLE=55555" NULL; } 13 execl("user/bin/ls","ls","-a","-l",NULL,env); 14 return 0; 15 } ~ 我们前面了解到OS会自带环境变量,但此外我们输出,只要我们定义的环境变量env
解决办法有两个,一不传,OS会自己调用自己的
二:使用putenv(新增环境变量)

#include<stdio.h> 2 #include <unistd.h> 3 #include<sys/types.h> 4 int main() 5 { 6 if(fork()==0) 7 { 8 printf("id:%d\n",getpid()); 9 printf("子进程开始\n"); 10 // execl("./test","test",NULL); 11 printf("子进程结束\n"); 12 } char *const env[]={(char*const) "MYLE=55555" NULL; } 13 execl("user/bin/ls","ls","-a","-l",NULL); execl("user/bin/ls","ls","-a","-l",putenv(env)); 14 return 0; 15 } ~ 5.5 execve

仔细阅读man手册发现,execve是系统调用的,上面的替换函数其实是库函数,最后都会系统调用execve,这也是为什么 有e时,不传环境变量,OS会使用自己默认环境变量的原因