目录
一.进程创建
1.初始fork函数
在Linux中fork函数是一个非常重要的函数,它从一个已存在的进程中创建一个新的进程。新进程为子进程,原进程为父进程。
#include<unistd.h>//头文件
pid_t fork(void);
2.执行fork系统做了什么
fork是一个系统调用,由操作系统的内核执行来创建一个进程。那内核创建进程做了些什么事呢?
- 分配新的内存块和内核数据结构给子进程。分配内存和数据结构PCB,进程地址空间和页表等给子进程。
- 将父进程部分数据结构内容拷贝到子进程。子进程一父进程为"模板"的,但是并不是完全一样的,例如pid等。
- 添加子进程到系统进程列表中
- fork返回,开始由调度器调度
3.fork返回值
fork返回值。
- fork给子进程返回0
- fork给父进程返回子进程pid
- fork出错时返回-1
为什么fork由两个返回值?
内核执行fork函数,申请内存,构建数据结构PCB,进程地址空间和页表等,放到调度列表中。
fork是一个函数,里面含有创建进程的代码,例如:
pid_t fork{
//创建进程代码//最后return
return pid;
}
在返回前已经将子进程创建好了,子进程也要执行return这条语句。原来进程父进程本来就要执行return语句。所以会有两个返回值。
为什么fork给子进程返回0,给父进程返回子进程的pid?
因为父进程只有一个,子进程可以有多个,为了让父进程找到子进程(为后面等待做准备),所以给父进程返回子进程pid。
当fork被调用后,就有两个二进制代码相同的进程。而且他们运行到相同的地方。但是每个进程都将可以开始他们自己的旅程,看下面的代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main(){
6 printf("Now pid :%d\n",getpid());
7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 //child
14 printf("child pid :%d return :%d\n",getpid(),id);
15 sleep(2);
16
17 }
18 else{
19 //father
20 printf("father pid :%d return :%d\n",getpid(),id);
21 sleep(2);
22 }
23
24 return 0;
25 }
这里看到了三行输出,一行是fork前的语句,两行是fork后的语句。
为什么会出现这样的现象?
父进程先先执行自己的代码,打印了fork前的语句,但执行到fork时,创建了一个新进程。此时有两个进程,父进程和子进程。而父子进程代码是共享的,但是数据是私有的。父进程返回值为子进程pid大于0,执行father pid...这条语句,子进程返回0,执行child pid...语句。
为什么子进程没有打印Now pid...这条语句?
当父进程运行到fork语句时,进程控制块PCB里的程序计数器记录的下一次代码执行位置。而子进程的数据结构是以父进程为模板的,PCB里的程序计数器记录的也是同样的位置。所以不会从头开始执行。
画个图帮助理解:
注意:fork后父子进程谁先执行是不确定的,由调度器决定。
4.写时拷贝
通常父进程创建子进程时,父子进程代码是共享的,数据在都不写入时也是共享的,但是当一方试图写入时,数据就变成私有的了。
代码共享,子进程代码不仅只含有父进程fork后的代码,父进程fork前的代码也有。只是因为PCB程序计数器的原因,子进程从fork后开始执行。
为什么代码共享?
代码是不可以修改的,是只读的(页表权限限制)。反正父子进程都不能修改代码,如果各自私有一份浪费空间。父子进程分流后代码仍然是共享的(一样),只是执不执行的问题。
共享的意思是:两进程PCB中保存代码的地址相同,也就是父子进程"代码指针"指向同一个地址空间。不共享并不一定是虚拟地址变了,可能是实际物理地址变了,重新在实际内存中开辟一空间,改变页表中的映射关系。
为什么数据私有?
因为进程之间具有独立性。
4.1 写时拷贝概念
一开始父进程创建子进程代码和数据都是共享的,但是当父子进程一方要写入数据时,系统会在内存开辟另外一空间,将该数据拷贝过去,然后在新开辟空间写入数据。就是拷贝不是立马做的,而是需要写的时候再开辟空间,拷贝数据,写入数据。
4.2 为什么要有写时拷贝
创建进程数据是有很多的,但是并不是所有的数据都立马要使用,并且不是所有的数据都要使用。但是如果立马将数据私有,就要将数据立马全部拷贝。把本来可以后面拷贝的,甚至不需要拷贝的数据都拷贝了。这样创建一个进程就比较浪费时间和空间。
写时拷贝拷贝数据时,是将数据全部拷贝还是只将写的部分数据拷贝?
只将写的数据拷贝。子进程也有一个新的进程地址空间(mm_struct)和页表。写时拷贝只将写的数据放到重新在内存开辟的空间,改变页表映射关系对应物理地址,虚拟地址没变。
5. fork常规用法
- 一个父进程希望复制自己,使父子进程执行不同的代码段。上面的代码就是这样的。
- 一个进程要执行不同的程序。后面讲进程替换的时候讲解。
6. fork失败的原因
- 系统已经有太多的进程
- 每个用户的进程数都有一个限制,进程数超过限制也会失败
7. 总结
如何理解fork子进程的创建?
本质上是系统多了一个进程。子进程以父进程为"模板"(并不是全部一样,例如pid)。创建对应数据结构(PCB,mm_struct,页表等)和代码数据。一开始复制进程代码和数据共享。当有一方写入数据时,给进程发生写时拷贝,将要写的数据重新开辟空间,修改页表的映射关系。
二.进程终止
1. 进程退出的场景
- 代码正常运行完毕,结果也正确
- 代码正常运行完毕,结果不正确
- 代码运行异常
代码运行是以进程为载体的。
怎样才能知道代码运行退出情况呢?
代码运行完毕后会有相应的退出码和退出信号。
2. 进程常见的退出方法
正常终止:(可以通过echo $? 来查看进程退出码)
- 从main函数return返回。
return n//n就代表了退出码
- 调用exit
#include<unistd.h>//头文件
void exit(int status)//status 退出码
- 调用_exit
#include<unistd.h>//头文件
void _exit(int status)//status 退出码
说明:_exit(int status),虽然status是int,但是只有低8位可以被父进程所用。所以_exit(-1),执行echo $?时返回码时255.
异常退出:
- ctrl + c 信号终止
3.exit和return的区别
exit是终止整个进程,任何地方调用都会终止整个进程。
return是终止函数,只是在main函数里return n,等同于exit(n)。因为调用main函数的函数将main函数的返回值当作了exit的参数。
4. _exit和exit区别
4.1 发现问题
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main(){
6 printf("hello world");
7 return 0;
8 }
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main(){
6 printf("hello world");
7 exit(0);
8 }
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main(){
6 printf("hello world");
7 _exit(0);
8 }
从上面现象,我们发现用return和exit终止进程执行了语句,用_exit终止进程没有执行语句。有return和exit的区别可知,return结束进程也是调用了exit。
4.2 结论
_exit只是退出了进程。exit在调用时不仅仅终止了进程,还做了一些其它的事:
- 执行用户通过atexit和on_exit定义的清理函数
- 关闭所有打开的流,刷新缓存
- 调用_exit,退出进程
4.3 解决问题
上面的问题,打印的字符串保存在缓冲区里。用_exit直接就退出了进程,没有刷新缓冲区。return退出进程实际上也是调用exit,exit退出进程会执行清理函数和刷新缓冲区,所以打印了语句。
三.进程等待
1. 进程等待的必要性
- 子进程退出,父进程没有退出,子进程会变成僵尸进程。如果父进程不管子进程,会导致子进程一直在那里,也无法杀死该进程,因为这个进程已经退出了。这样会导致内存泄漏。
- 父进程派给子进程的任务完成的怎么样,父进程需要知道。即父进程需要知道子进程退出的状态。
- 父进程通过进程等待的方式,回收子进程的资源,获取子进程退出信息。
2. 进程等待方法
2.1 wait()
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status)
返回值:成功,返回被等待进程pid,失败,返回-1
参数:输出型参数,获取子进程退出状态,不关心状态可设置为NULL
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main(){
E> 7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 int count=0;
14 while(count<3){
15 printf("child...,count:%d\n",count);
16 count++;
E> 17 sleep(1);
18 }
19 exit(0);
20 }
21 else{
22 int count=0;
23 while(1){
24 printf("father...,count:%d\n",count);
25 if(count==5){
26 wait(NULL);
27 }
28 count++;
E> 29 sleep(1);
30
31 }
32 }
33
34 return 0;
35 }
代码:设置子进程3秒后退出变成僵尸进程,父进程5秒后执行wait命令。查看子进程状态。
上面验证了wait命令可以回收子进程的资源,防止内存泄漏。
注意:wait和waitpid是系统调用,所以资源回收是系统做的,父进程只是调用了系统接口。
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main(){
E> 7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 int count=0;
14 while(count<3){
15 printf("child...,count:%d\n",count);
16 count++;
E> 17 sleep(1);
18 }
19 exit(0);
20 }
21 printf("father before....\n");
22 wait(NULL);
23 printf("father after...\n");
24 return 0;
25 }
这个代码证明父进程执行wait,父进程处于阻塞等待,一直在wait处等待子进程退出,再执行后面的代码。
2.2 waitpid()
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)
返回值:
正常返回时,返回子进程pid
调用错误,返回-1.
参数:
pid:
pid=-1,等待任意一个子进程,与wait等效
pid>0,等待进程id与pid相等的子进程。
status:
返回状态,为一个整形指针,用来接收返回值状态,后面介绍
options:等待方式
WNOHANG:如果pid指定进程没有结束,则waitpid函数返回0,不再等待。如果子进程正常结束,返回子进程id。
0:以默认阻塞方式等待
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main(){
E> 7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 int count=0;
14 while(count<3){
15 printf("child...,count:%d\n",count);
16 count++;
E> 17 sleep(1);
18 }
19 exit(0);
20 }
21 printf("father before....\n");
22 //wait(NULL);
23 //和wait效果一样
24 waitpid(-1,NULL,0);
25 printf("father after...\n");
26 return 0;
27 }
3. 获取子进程状态,int *status
- wait和waitpid,都有一个参数status参数(是一个整形指针),该参数是一个输出型参数,有参数系统填充(赋值)。
输出型参数时用来获取结果的,
输入性参数是用来提供参数的,已知值。
- 如果传递NULL,表示不关心子进程的退出状态
- 不为NULL,操作系统会根据该参数,将子进程退出信息填充到该参数,返回给父进程。
- status不能简单当作int看待,需要当作位图看待。具体细节如下图(只研究status的低16位)
#include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main(){
E> 7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 printf("child...\n");
14
E> 15 sleep(2);
W> 16 int x=1/0;//异常退出
17 //exit(0);//正常退出,结果正确
18 exit(10);//正常退出,结果不正确
19 }
20 int st=0;//接收退出状态
21 pid_t res=waitpid(id,&st,0);//等待子进程,阻塞方式等待
22 if(res>0){
23 int tmp=st&0x7f;
24 if(tmp){
25 //异常退出
26 printf("error exit,exit signal is %d\n",tmp);
27 }
28 else{
29 //正常退出
30 int code=(st>>8)&0xff;
31 if(code){
32 printf("result is error,code id %d\n",code);
33 }
34 else{
35 printf("result id right,code is %d\n",code);
36 }
37 }
38 }
39 else{
40 //调用错误
41 perror("waitpid error");
42 return 1;
43 }
44 return 0;
45 }
两种可以直接获取退出信息的宏:
- WIFEXITED(status):若为正常终止子进程返回真。(查看进程是否正常退出)
- WEXITSTATUS(status):若 WIFEXITED(status)为正(正常退出),获取退出码。(查看进程退出码)
进程阻塞式等待实现
#include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main(){
E> 7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 printf("child...\n");
14
E> 15 sleep(2);
W> 16 int x=1/0;//异常退出
17 //exit(0);//正常退出,结果正确
18 exit(10);//正常退出,结果不正确
19 }
20 int st=0;//接收退出状态
21 pid_t res=waitpid(id,&st,0);//等待子进程,阻塞方式等待
22 if(res>0){
23 int tmp=st&0x7f;
24 if(!WIFEXITED(st)){
25 //异常退出
26 printf("error exit,exit signal is %d\n",tmp);
27 }
28 else{
29 //正常退出
30 //int code=(st>>8)&0xff;
31 int code=WEXITSTATUS(st);
32
33 if(code){
34 printf("result is error,code id %d\n",code);
35 }
36 else{
37 printf("result id right,code is %d\n",code);
38 }
39 }
40 }
41 else{
42 //调用错误
43 perror("waitpid error");
44 return 1;
45 }
46 return 0;
47 }
进程非阻塞式等待实现
#include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main(){
E> 7 pid_t id=fork();
8 if(id<0){
9 perror("fork error");
10 return 1;
11 }
12 else if(id==0){
13 printf("child...\n");
14
E> 15 sleep(5);
W> 16 int x=1/0;//异常退出
17 //exit(0);//正常退出,结果正确
18 exit(10);//正常退出,结果不正确
19 }
20 int st=0;//接收退出状态
21 pid_t res=0;
22 //一直循环,等子进程退出
23 do{
24 res=waitpid(id,&st,WNOHANG);//等待子进程,非阻塞方式等待
25 if(res==0)
26 printf("child is run\n");
E> 27 sleep(1);
28 }while(res==0);
29 if(res>0){
30 int tmp=st&0x7f;
31 if(!WIFEXITED(st)){
32 //异常退出
33 printf("error exit,exit signal is %d\n",tmp);
34 }
35 else{
36 //正常退出
37 //int code=(st>>8)&0xff;
38 int code=WEXITSTATUS(st);
39
40 if(code){
41 printf("result is error,code id %d\n",code);
42 }
43 else{
44 printf("result id right,code is %d\n",code);
45 }
46 }
47 }
48 else{
49 //调用错误
50 perror("waitpid error");
51 return 1;
52 }
53 return 0;
54 }
4. 总结
进程等待是什么?
父进程通过wait/waitpid系统调用,等待子进程状态的一种现象。
为什么要进程等待?
1.防止子进程僵尸状态,导致内存泄漏
2.获得子进程退出结果
怎么进程等待?
wait/waitpid
四.进程程序替换
fork创建一个子进程作用:
- 子进程执行父进程的部分代码,分流。
- 子进程与父进程执行完全不同的代码。程序替换。
1. 替换原理
磁盘中保存一程序的代码和数据,程序替换就是,将磁盘中保存的新程序的代码和数据替换进程中的程序和代码。从新程序代码开始执行。
注意:程序替换没有创建新进程,所以该进程的pid并没有改变。
2.替换函数
#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[])
3.函数解释
- 这些函数如果调用成功则加载到新的程序从启动代码开始执行,不再返回。
原因:执行第一条语句时,还没有替换。程序替换后(execl),将当前进程代码全部替换,也就是执行的是ls程序,后面的不执行了。
- 如果调用出错返回-1。
- 所以exec函数只有出错返回值,没有成功返回值。
exec函数错误时返回-1,不会发生替换,会继续执行下面的代码。
4. 命名理解
exec函数看起来不叫容易搞混,但是掌握规律就很好记了。
- l(list):表示采用列表形式。参数是一个一个传的。
- v(vector):参数用数组传入
- p(path):有p的exec函数会自动搜索环境遍历PATH。
- e(env):表示自己维护环境变量。要自己用数组定义环境变量。
对照下面举列,就很清楚了:
注意:含有l的exec函数最后或者envp前要以NULL结尾
用自己编写的代码替换当前进程
环境变量:
总结函数:
函数名 | 参数格式 | 是否要写路径 | 是否使用当前环境变量 |
execl | 列表 | 要写绝对路径 | 使用系统环境变量 |
execlp | 列表 | 不要写绝对路径 | 使用系统环境变量 |
execle | 列表 | 要写绝对路径 | 使用自定义环境变量 |
execv | 数组 | 要写绝对路径 | 使用系统环境变量 |
execp | 数组 | 不要写绝对路径 | 使用系统环境变量 |
execv | 数组 | 要写绝对路径 | 使用自定义环境变量 |
事实上,只有execve是真正的系统调用,其余5个函数最终都调用execve。
ps说明:
- 一般exec函数我们不会自己调用,一般我们会fork一个子程序,让子程序调用(去完成其它的任务),父进程只要wait就好了。
- 父进程创建子进程后子进程执行替换函数后,父进程不受影响,因为父子进程之间具有独立性。