目录
1. 进程创建
第一次认识fork是为了验证父子进程,今天在来深入理解他。
1.1 回忆fork
fork创建子进程是以父进程为模板,继承父进程的信息。
为何给父进程返回pid,给子进程返回0?
在之前的博客有深入讨论,点这里
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 using namespace std;
5 int main()
6 {
7 cout<< "i am a process:"<<getpid()<<endl;
8 pid_t id=fork();
9 if(id<0)
10 {
11 cerr<<"fork err"<<endl;
12 }
13 else if(id==0)
14 {
15 cout<<"i am son:"<<getpid()<<endl;
16 sleep(2);
17 }
18 else{
19 cout <<"i am father: "<<getpid()<<" myson pid :"<<id<<endl;
20 sleep(5);
21 }
22 return 0;
23 }
执行程序,输出没有问题
在我们./proc运行的时候进程创建,fork之后子进程被创建。
在程序中我们使用的getpid(),以及getppid(),是系统调用接口。
那么内核在这中间又干了什么呢?
- 分配心得内存块和内核数据结构给子进程。
- 将父进程部分数据结构拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
虽然子进程复制了全部代码,但是同时程序计数器也被复制,所以fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器调度。
1.2 写时拷贝
我们知道父子进程共享代码,数据各自私有。
代码共享:全部代码共享(代码是只读的,没有必要私有节省空间),不过由于程序计数器,从fork之后开始运行。
数据私有:由于进程的独立性,一个进程修改数据,是不能修改另一个进程的。
独立性:体现在拥有两个不同的地址空间,数据私有
假如数据很多,不是所有数据都要立马使用,不是所有的数据都需要修改。假如有一个数据要修改,我难道要直接全部拷贝吗,os肯定不会做这样的事情,遇见哪个数据拷贝在拷贝那个对应的数据,就叫做写时拷贝。
那不写呢,只进行读取操作,那就没必要将数据私有,这时候数据也是共享的。
直接对内存进行操作的
1.3 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4 fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
2. 进程终止
2.1 进程退出的三个场景
注意是代码退出的情况:
- 代码运行完,返回正确结果。
- 代码运行完,返回错误结果
- 代码异常终止
在main函数我们在最后总是要写return 0;
这个返回的数字叫做进程的退出码。
使用echo $?
可以查看最近一次进程的退出码。
首先看代码运行完,返回错误情况,我们ls并不存在-xyz,所以肯定是再其中执行了差错处理的代码,退出码为2.
然后我们在运行一次echo $?,这次查看的是上一次echo命令成功
这就是执行成功的情况。
那么异常终止的情况呢,我们可以故意在代码里进行除0操作,结果运行到那一行就会报错,直接终止进程
操作系统使用的是8号信号,来终止他,我们也可以另起一个ssh渠道,使用信号来模拟终止
代码运行完,退出码才是有意义的。
当代码异常终止,退出码无意义,需要获取退出原因。
2.2 exit与return
exit:终止整个进程,任何地方调用,都会终止,参数就是退出码
return:会终止函数,返回。只有在main函数才会终止。在main函数里return 的也是退出码
假如执行到exit(argc)这条语句,里面的参数就是退出码.
1 #include<iostream>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5
6 using namespace std;
7
8 int exe()
9 {
10 exit(12);
11 }
12 int main()
13 {
14 exe();
15 cout<<"hello"<<endl;
16
17 return 0;
18 }
2.3 _exit与exit
1 #include<iostream>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5
6 using namespace std;
7
8 int exe()
9 {
10 exit(12);
11 }
12 int main()
13 {
14
15
16 cout<<"hello";
17 sleep(2);
18 exe();
19 return 0;
20 }
~
~
~
会打出hello,而且退出码也是12
当exit换成_exit时,虽然退出码也是12但是什么都不打印
总结:
_exit是系统调用,不会刷新缓冲区的数据。直接关掉进程。
exit是库函数,他会做一些清理工作关闭执行流,刷新缓冲区。
一般都是在多进程中,不想让进程继续执行下去,使用exit进行退出。
一般来说都是子进程先退出,假如父进程先退出,那子进程就成了孤儿进程,由操作系统回收了,而且父进程很容易的对子进程进行管理。
3. 进程等待
子进程是为了给父进程处理一些子业务,需要让子进程给父进程一些结果。
所以父进程是需要等待子进程的。假如父进程对子进程不管不顾,子进程就成了僵尸进程,内存泄漏。
子进程运行完,有三种情况。父进程需要知道你的情况。
总的来说那么进程等待有两个作用:
- 回收系统资源
- 获取子进程退出信息
模拟一个僵尸进程。
#include<iostream>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6 using namespace std;
7 int main()
8 {
9 pid_t id=fork();
10 if(id==0)
11 {
12 int count=0;
13 while(1)
14 {
15 sleep(1);
16
17 cout<<"i am child"<<endl;
18 count++;
19 if(count>=15)
20 {
21 break;
22 }
23 }
24 exit(0);
25 }
26 else{
27
28
29 while(1)
30 {
31 sleep(1);
32 cout<<"i am father"<<endl;
33 }
}
return 0;
}
使用while :; do ps axj | head -1 &&ps axj |grep proc |grep -v grep; sleep 1;echo "##############"; done
脚本来监测他们两个进程的状态
当子进程退出后,父进程还在运行
子进程就成了僵尸状态
3.1 wait
在父进程的代码中加入wait,让他在20s的时候wait,NULL表示不关心状态
else{
27 int count=0;
28
29 while(1)
30 {
31 sleep(1);
32 cout<<"i am father"<<endl;
33 if(count==20)
34 {
35 wait(NULL);
36 }
37 count++;
38 }
39 }
40 return 0;
我们可以使用wait进行阻塞等待(一直看着子进程,等待他死亡),将它回收(父进程触发回收动作,操作系统回收资源)。
假如不关心他的状态可以将参数设置为NULL
3.2 waitpid方法
这份代码表示,子进程执行10次退出,父进程回收他,当waitpid方法,参数为id,NULL,0时和wait方法无差异。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6 int main()
7 {
8 pid_t id =fork();
9 if(id<0)
10 {
11 perror("fork err\n");
12 return 1;
13 }
14 else if(id==0)
15 {
16 int count=0;
17 while(count<10)
18 {
19 printf("child:%d is running\n",getpid());
20 sleep(1);
21 count++;
22 }
23 exit(0);
24 }
25 else{
26 printf("father wait before\n");
27 int st=0;
28 pid_t ret= waitpid(id,NULL,0);
29 if(ret>0)
30 {
31 printf("wait successed\n");
32 }
33 else{
34 printf("wait failed\n");
35 }
36 printf("father wait after\n");
37 }
38 return 0;
39 }
3.2.1 第二个参数 int *status
我们重点来研究第二个参数。这个参数是一个输出型参数,作为形参我们要传他的地址,因为函数里面会改变它。将退出信息保存在st中。
若参数为NULL,则表示我们不关心。
若有参数,则需要关注两方面,进程终止无非三种情况,执行完的两种情况属于第一方面,异常退出属于第二方面。
- 退出两种情况
结果正确:
结果错误: - 异常退出
返回之后,st中就保存的是我们进程退出的信息,正常运行,结果不同的两种情况退出码是多少,异常退出的一种情况退出信号是多少?
int 是32位,在这里只用了低16位就够了。
第一种正常终止,所以终止信号是0,去后8位查看他的退出状态。来查看他结果是否正确
第二种被信号所杀,所以就是异常退出。我们需要查看退出信号。
将参数放到waitpid里,ret>0,代表waitpid成功,ret就是pid,输出st看一看。
//父进程
else{
26 printf("father wait before\n");
27 int st=0;
28 pid_t ret= waitpid(id,&st,0);
//wait成功
29 if(ret>0)
30 {
31 printf("%d \n",st);
32 printf("wait successed\n");
33 }
st为0。说明低8位和高八位都为0,也就说明这个是正常退出,而且结果正确。
我们看看正常退出,结果不正确的时候(exit(1)
)
其实也不难想,1在第9位,就是28,当然就是256了。
那么我们想要看看它的退出码,即检测他的高八位。
int st=0;
pid_t ret= waitpid(id,&st,0);
if(ret>0)
{
printf("st:%d \n",st);
int exit_code=(st>>8)&(0xff);
printf("exit_code:%d\n",exit_code);
printf("wait successed\n");
}
这样就检测到了,还在位操作这一步愣了一会。由于你是高8位存储退出码,那我就右移8位,然后和0xff进行按位与操作,就能检测他哪一位是1,然后将它输出就得到了退出码。
再验证一下
然后向右移8位,和0xff,按位与,就得到了高8位也就是,23+22=12
其实理论我们要先检测他的低七位(第8位core dump先不用管),来查看进程是否异常终止,不是异常终止,才能看他是正常终止两种情况的哪一种。
我们只需要让st按位于0x7f,因为退出信号在低七位存储,所以我们只需要低七位按位与7个1即0x7f就可以拿到1的个数。从而打印出退出信号。
int st=0;
pid_t ret= waitpid(id,&st,0);
if(ret>0)
{
printf("st:%d \n",st);
printf("exit_singal:%d\n",st&(0x7f));
int exit_code=(st>>8)&(0xff);
printf("exit_code:%d\n",exit_code);
printf("wait successed\n");
}
这个就表示,正常退出,但结果不正确。
我们在子进程执行的模块加上一个显著的错误除0,很显然没执行到exit他就会异常退出,随后父进程等待会获取到退出信号,放在低7位当中
int main()
{
pid_t id =fork();
if(id<0)
{
perror("fork err\n");
return 1;
}
else if(id==0)
{
int count=0;
while(count<3)
{
printf("child:%d is running\n",getpid());
sleep(1);
count++;
}
int i=1/0;
exit(12);
}
可以看到退出信号为8,因为8代表的就是精度异常
对程序做最终修改,所以我们就可以用if进行检测,来输出。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6 int main()
7 {
8 pid_t id =fork();
9 if(id<0)
10 {
11 perror("fork err\n");
12 return 1;
13 }
14 else if(id==0)
15 {
//子进程
16 int count=0;
17 while(count<3)
18 {
19 printf("child:%d is running\n",getpid());
20 sleep(1);
21 count++;
22 }
23
24 exit(0);
25 }
26 else{
//父进程
27 printf("father wait before\n");
28 int st=0;
29 pid_t ret= waitpid(id,&st,0);
30 if(ret>0)
31 {
//waitpid成功
32 printf("st:%d \n",st);
33 int exit_singal=st & 0x7f;
34 int exit_code=(st>>8)&(0xff);
35
36 if(exit_singal)
37 {
//进程异常终止
38 printf("child error terminal");
39 }
40 else{
41 if(exit_code)
42 { //进程正常执行,结果异常
43 printf("child successed but result is not right, exit_code: %d \n",exit_code);
44 }
45 else{
//进程正常执行,结果正常
46 printf("child successed result is right ,exit_code: %d\n ",exit_code);
47 }
48 }
49 }
50 else{
//waitpid失败
51 printf("wait failed\n");
52 }
53 printf("father wait after\n");
54 }
55 return 0;
56 }
进程异常退出
进程正常,结果正常
进程正常,结果错误
其实在这里我们只是为了深入了解他的原理,其实已经有两个宏已经帮我们实现了,不需要我们自己做。
第一个就对应我们刚才实现的 status&ox7f
只不过要和这个宏一样就得这样表达!(status&0x7f)
,因为st&ox7f
为真表示进程异常退出。表示正常就需要取反。
第二个对应,若进程没有异常终止,(status>>8) & 0xff
取到并查看退出码,来判断结果是否正确。
3.2.2 第三个参数WNOHANG
假如设置了第三个参数 wonohang,现在没有退出的子进程时,waipid就会返回0,即非阻塞式等待,一会再来看看有没有需要等待的。
3.3 waitpid总结
灵魂三问:
- 什么是进程等待:
父进程通过wait/waitpid等系统调用,用来等待子进程,这是必须的。 - 为什么需要?
防止子进程发生僵尸问题,会产生内存泄漏。读取子进程的退出状态。 - 怎么使用?
wait/waitpid,使用waitpid时,可以通过第二个参数status获取退出状态,异常信号。通过第三个参数实现阻塞和非阻塞式等待
4. 进程替换
fork创建子进程的目的:
- 想让子进程执行父进程代码的一部分
- 想让子进程执行与父进程无关的事情
用fork创建子进程之后,子进程会执行与父进程相同的代码(可能是不同的模块),但是当我们想要让子进程执行与子进程完全无关的代码的时候,我们就需要进程替换。
需要注意的是,在我们之前讲到,数据是写时拷贝,代码是共享的,但是在这里代码也是需要写时拷贝的。进程替换是不会产生新进程的,它相当于狸猫换太子,进程的id不会改变。
进程替换是使用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[]);
命名看起来很混乱,其实可以通过函数名字带有的参数来理解
- l:函数形参采用列表进行输入
- p:自动搜索环境变量
- v:传递数组
- e:表示自己维护环境变量
4.1 execl
需要自己传递路径,然后后面参数argc依次代表,执行程序和他对应的命令。
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main()
5 {
6 printf("i can execute");
7 execl("/usr/bin/ps","ps","-e","-l","-f",NULL);
8 printf("i also can execute ");
9 return 0;
10 }
于是这个我们写的./myexe程序就调用了ps这条系统命令,而且就在execl执行之后第二条打印语句是不会被打印的因为进程已经全部被替换,之前的代码也就不在了
可以看到执行结果。换句话说,像exec这一系列函数是没有返回值的(不会再返回来执行第二个printf语句,假如返回了,那就代表进程替换失败)
4.2 execlp
p代表自动搜索环境变量,但是需要我们传程序的名字,l代表执行命令的列表
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("i can execute\n");
//execl("/usr/bin/ls","ls","-a","-l",NULL);
execlp("ls","ls","-a","-l",NULL);
printf("i also cam execute\n");
return 0;
}
~
可以执行
4.3 execv
v代表vector,以数组形式传参
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("i can execute\n");
6 //execl("/usr/bin/ls","ls","-a","-l",NULL);
7 char* const arg[]={
8 "ls",
9 "-i",
10 "-a",
11 "-l",
12 NULL,
13 };
14 //execlp("ls","ls","-a","-l",NULL);
15 execv("/usr/bin/ls",arg);
16 printf("i also cam execute\n");
17 return 0;
18 }
4.4 execvp
v代表以数组形式传参,p代表自动搜索环境变量只需要传程序名称。
execvp("ls",arg);
4.5 另外两个函数之前的补充
在演示之前我们先makefile创建两个可执行文件
1 .PHONY:all
2 all:myexe mycmd
3
4 myexe:myexe.c
5 gcc $^ -o $@
6 mycmd:mycmd.c
7 gcc $^ -o $@
8 .PHONY:clean
9 clean:
10 rm -f myexe mycmd
~
~
~
~
假如不在生成可执行文件的依赖关系中添加伪目标,默认生成第一个可执行。
进程是可以使用exec系列函数替换自己的,只不过会成为死循环。
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("myexe can execute\n");
6 execl("./myexe","./myexe",NULL);
7 //execlp("ls","ls","-a","-l",NULL);
8 //execv("/usr/bin/ls",arg);
9 // execvp("ls",arg);
10 printf("i also cam execute\n");
11 return 0;
12 }
我们再写一个本目录下的进程替换。直接带上他的相对路径,传参形式无差别
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("myexe can execute\n");
6 execl("./mycmd","./mycmd",NULL);
7 //execlp("ls","ls","-a","-l",NULL);
8 //execv("/usr/bin/ls",arg);
9 // execvp("ls",arg);
10 printf("i also cam execute\n");
11 return 0;
12 }
4.6 execle与execve
没有带p,说明都需要传递路径,l和v说明传参形式不同,e说明自定义环境变量。
单独运行mycmd,没有环境变量
使用myexe调用mycmd就可以将我们的env传入进去
还记得main函数里是有环境变量env的,当我们什么都不干,两者环境变量都为NULL
当我们在外export env时,调用myexe环境变量就会作为参数传递下去
那么execve就是传数组。
其实系统调用只有一个就是execve,其他都是为了适用各种场景的再一次封装,调用的都是execve。
而且这也说明了c/c++是可以调用其他语言所编写的程序。
4.7 多进程实现进程替换
exec这一系列函数实际上是多进程场景下使用的,即父进程运行,创建子进程,让子进程替换别的进程取运行。
子进程去做命令,父进程只需要等待关心结果就好。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/wait.h>
5 int main()
6 {
7 pid_t id=fork();
8 if(id<0)
9 {
10 perror("fork err");
11 return 1;
12 }
13 else if(id==0)
14 {
15 sleep(3);
16 execl("/usr/bin/ls","ls","-a","-l",NULL);
17 exit(1);
18 }
19 pid_t ret =waitpid(id,NULL,0);
20 if(ret>0)
21 {
22 printf("wait successed\n ");
23 }
24 return 0;
25 }
这样即使子进程执行失败,他也会exit退出,然后被回收,不会影响父进程。