🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
[1.2 写时拷贝](#1.2 写时拷贝)
[1.3 fork调用失败原因](#1.3 fork调用失败原因)
[2.1 进程退出场景](#2.1 进程退出场景)
[2.2exit && _exit](#2.2exit && _exit)
[5.1 验证id不变与覆盖式写入](#5.1 验证id不变与覆盖式写入)
[5.2 替换函数](#5.2 替换函数)
[5.3 命名理解](#5.3 命名理解)
[5.4 替换函数使用](#5.4 替换函数使用)
[5.4.1 execl](#5.4.1 execl)
[5.4.3 execv](#5.4.3 execv)
[5.4.4 execvp](#5.4.4 execvp)
[5.4.5 execele](#5.4.5 execele)
[5.5 execve](#5.5 execve)
一.进程创建
1.1再识fork
关于fork的基础使用,在前面的文章已经讲过,此处不再赘述
进程调⽤ fork ,当控制转移到内核中的 fork 代码后,内核做:
分配新的内存块和内核数据结构给⼦进程
将⽗进程部分数据结构内容拷⻉⾄⼦进程
添加⼦进程到系统进程列表当中
fork 返回,开始调度器调度
1.2 写时拷贝
当父子数据未被修改写入时,共用同一份资源,但其中一方数据进行了写入,便会写时拷贝一份

可以看到修改前,数据段都为r,修改后,不只为r,原理是对权限为r的数据段进行写入,OS会检查,发现错误,就会自动修改权限,进行拷贝
同时我们发现没被修改的代码段依旧共用同一份资源
因此,写时拷贝的好处
1.减少创建时间
2.减少内存浪费
1.3 fork调用失败原因
• 系统中有太多的进程
• 实际⽤⼾的进程数超过了限制
二.进程终止
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1 进程退出场景
代码运⾏完毕,结果正确
代码运⾏完毕,结果不正确
代码异常终⽌
子进程是由父进程创建的,是要让完成某些事情的,子进程要反馈父进程某些信息
查看最近一个进程的退出码,可以通过 echo $ ?
代码正常运行,结果正确
bashtest1.c ⮀ ⮂⮂ buffers 1 #include<stdio.h> 2 int main() 3 { W> 4 int a = 1; 5 return 0; 6 }退出码:
bash[lcb@hcss-ecs-1cde 4]$ echo $? 0代码运行完毕,结果不正确
bash1 #include<stdio.h> 2 int main() 3 { 4 int a = 1; W> 5 a =a/0; 6 return 0; 7 8 }退出码
bash[lcb@hcss-ecs-1cde 4]$ echo $? 136注:
再查看退出码,又会为0,因为是查看最近一个进程的退出码
bash[lcb@hcss-ecs-1cde 4]$ echo $? 0
常见退出方式
正常终⽌(可以通过 echo $? 查看进程退出码):
- 从main返回
- 调⽤exit
- _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会
原因在于
- 执⾏⽤⼾通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写⼊
- 调⽤_exit(库调用系统调用,封装了_exit)

因此我们可以得到一个结论
我们前面所说的缓存区不在操作系统内部的缓存区,而是库的缓存区
三.进程等待
• 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存
泄漏。
• 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的kill -9 也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程。
• 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
不对,或者是否正常退出。
• ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息
3.1等待方式
wait
bash
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
waitpid
bash
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
⽐特位):

bash
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多分钟,后挂断了电话.
在这等待的过程中,小明一边干自己的事(看书等),一边隔一会给小李打个电话,得到小李的回复还未干好,立马挂掉电话
在这期中,小明在等待小李的时候,还自己进行了看错,效率提高
bash
#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不变与覆盖式写入
bash
#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函数:
bash
#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会无限制地读取后续内存中的
bash
#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
bash
#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
bash
#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
bash
#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
bash
#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(新增环境变量)

bash
#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会使用自己默认环境变量的原因