进程等待,进程替换
- 1.进程等待
- 1.1进程等待必要性
- 1.2进程等待的方法
- 1.2.1wait方法
- 1.2.2waitpid方法
- 1.2.3通过宏得到退出码
- 1.2.4 阻塞vs非阻塞
- 2.进程替换
- 2.1进程替换的目的
- 2.2execl替换函数
- 2.3理解原理
- 2.4其他替换接口
- 2.4.1execl
- 2.4.2execlp
- 2.4.3execv
- 2.4.4execvp
- 2.4.5 execle
- 2.4.6execvp
- 2.5总结
前面内容是关于进程创建和终止的内容,这里主要是进程等待和替换。
1.进程等待
1.1进程等待必要性
前面进程状态说过僵尸进程。
1.子进程退出,如果父进程对其不管不顾,就有可能造成僵尸进程,进而造成内存泄漏。
2.此外,处于僵尸的进程,已经处于退出状态了,就算使用kill -9也没有办法杀掉一个已经死掉的进程。
3.最后,父进程派给子进程的任务完成得如何,我们需要知道,如,子进程运行完成,结果是对还是不对,或者是否正常退出。
对于上述问题我们该如何解决呢?
就是通过今天讲的进程等待得方法来解释上述问题。
父进程通过进程等待的方式:回收子进程资源,获取子进程退出信息。
1.2进程等待的方法
1.2.1wait方法
status这个参数下面讲。
成功时,返回要等待进程的id。失败,返回-1;
下面这段代码,演示进程等待。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id=fork();
11 if(id == 0)
12 {
13 //子进程
14 int cnt=10;
15 while(cnt)
16 {
17 printf("我是子进程, pid:%d, ppid:%d, cnt = %d\n",getpid(),getppid(),cnt--);
18 sleep(1);
19 }
20 exit(1);
21 }
22 //父进程
23 sleep(15);
24 pid_t ret=wait(NULL);
25 printf("wait success:%d",ret);
26 sleep(2);
27 return 0;
28 }
当子进程退出时,处于僵尸状态(Z+),然后等到父进程对其回收子进程资源。这里wait参数设为NULL;主要是为了演示这一过程。下面就详细说这个参数。
while :;do ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v grep; sleep 1; done //循环执行这条指令
1.2.2waitpid方法
第一个参数是要等待的进程id,第二个参数获得子进程退出码,第三个参数是等待方式暂时默认为0(阻塞等待)。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id=fork();
11 if(id == 0)
12 {
13 //子进程
14 int cnt=5;
15 while(cnt)
16 {
17 printf("我是子进程, pid:%d, ppid:%d, cnt = %d\n",getpid(),getppid(),cnt--);
18 sleep(1);
19 }
20 exit(1);
21 }
22 //父进程
23 sleep(10);
24 int status=0;
25 // pid_t ret=wait(NULL);
26 pid_t ret=waitpid(id,&status,0);
27 // printf("wait success:%d",ret);
28 if(ret>0)
29 {
30 printf("wait success:%d,ret:%d\n",ret,status);
31 }
32 sleep(2);
33 return 0;
34 }
退出码256,我们的退出码135个,这里是肯定不对的。
获取子进程status
1.wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
2.如果传递NULL,表示不关心子进程的退出状态信息。
3.否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
4.status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
次低8位是退出码,看运行完,结果是否正确。
低7位标表示进程是否正常结束。如果是0表示正常结束,这个时候退出码才有意义。如果是!0退出码不管什么都没有意义。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id=fork();
11 if(id == 0)
12 {
13 //子进程
14 int cnt=5;
15 while(cnt)
16 {
17 printf("我是子进程, pid:%d, ppid:%d, cnt = %d\n",getpid(),getppid(),cnt--);
18 sleep(1);
19 }
20 exit(1);
21 }
22 //父进程
23 sleep(10);
24 int status=0;
25 // pid_t ret=wait(NULL);
26 pid_t ret=waitpid(id,&status,0);
27 // printf("wait success:%d",ret);
28 // printf("wait success:%d,ret:%d\n",ret,status);
29 if(ret>0)
30 {
31 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
32 }
33
34 sleep(2);
35 return 0;
36 }
再看一段退出信号不是0的代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id=fork();
11 if(id == 0)
12 {
13 int *p=NULL;
14 //子进程
15 int cnt=5;
16 while(cnt)
17 {
18 printf("我是子进程, pid:%d, ppid:%d, cnt = %d\n",getpid(),getppid(),cnt--);
19 sleep(1);
20 //让子进程立刻退出
21 *p=10;
22 }
23 exit(1);
24 }
25 //父进程
26 // sleep(10);
27 sleep(2);
28 int status=0;
29 // pid_t ret=wait(NULL);
30 pid_t ret=waitpid(id,&status,0);
31 // printf("wait success:%d",ret);
32 // printf("wait success:%d,ret:%d\n",ret,status);
33 if(ret>0)
34 {
35 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
36 }
37 sleep(2);
38 return 0;
39 }
退出信号不为0,退出码是谁都没有意义。这里显示0是因为我们status传递的是0。
kill -l //查看退出信号是什么
那么子进程退出码父进程是如何拿到的呢?
1.2.3通过宏得到退出码
每次得到退出码,退出信号。输出的时候都需要自己去手动写,系统给我们提供一个宏替换,方便使用。
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id=fork();
11 if(id == 0)
12 {
13 //int *p=NULL;
14 //子进程
15 int cnt=5;
16 while(cnt)
17 {
18 printf("我是子进程, pid:%d, ppid:%d, cnt = %d\n",getpid(),getppid(),cnt--);
19 sleep(1);
20 //让子进程立刻退出
21 // *p=10;
22 }
23 exit(1);
24 }
25 //父进程
26 // sleep(10);
27 sleep(2);
28 int status=0;
29 // pid_t ret=wait(NULL);
30 pid_t ret=waitpid(id,&status,0);
31 if(ret>0)
32 {
33 //判断是否正常退出
34 if(WIFEXITED(status))
35 {
36 //判断子进程运行结果是否正确
37 printf("exit code:%d\n",WEXITSTATUS(status));
38 }
39 else
40 {
41 printf("child exit not normal!\n");
42 }
43 }
44 return 0;
1.2.4 阻塞vs非阻塞
前面我们说了waitpid第三个参数,默认为0,为阻塞状态。
下面讲过小故事帮助理解阻塞,非阻塞。
马上就是期末考试了,张三想找李四恶补一下知识点。于是就约在李四宿舍楼下等他,张三到了之后给李四打电话,李四说他现在在忙其他事情需要一些事情,张三说没事,也没挂电话,就一直等着李四问他好了没。一直等到李四忙好才一起去补习知识点。
又一次快到期中考试了,张三又想找李四帮忙恶补一下知识点。还是老地方等着李四。张三这次还是到了之后给李四打电话,李四还是有事情要忙要等一会,但是这次张三挂了电话,然后一会给王五打电话,一会给其他人打招呼。然后才再给李四打电话。李四没好,张三又挂了电话,去忙其他事情,多次问了李四之后,李四才忙好,去给张三补习。
1.第一种张三不挂电话,检测李四状态--------阻塞
2.第二种张三给李四打电话,如果没有就绪,直接返回------这里每一次都是非阻塞等待,多次非阻塞等待------->轮询。
打电话—>系统调用wait/waitpid
张三—>父进程
李四—>子进程
见识见识非阻塞
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id=fork();
11 if(id == 0)
12 {
13 // int *p=NULL;
14 //子进程
15 int cnt=5;
16 while(cnt)
17 {
18 printf("我是子进程, pid:%d, ppid:%d, cnt = %d\n",getpid(),getppid(),cnt--);
19 sleep(1);
20 //让子进程立刻退出
21 // *p=10;
22 }
23 exit(1);
24 }
25 //父进程
26 // sleep(10);
27 int status=0;
28 while(1)//轮询
29 {
30 pid_t ret=waitpid(id,&status,WNOHANG);//非阻塞。子进程没有退出,父进程检测的时候,立即返回
31 if(ret == 0)
32 { //waitpid 调用成功 && 子进程没退出
33 printf("wait done,but child is runing.....\n");
34 }
35 else if(ret > 0)
36 {
37 //waitpid 调用成功 && 子进程退出了
38 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
39 break;
40 }
41 else
42 {
43 //调用失败
44 printf("waitpid call failed\n");
45 break;
46 }
47 sleep(1);
48 }
49 return 0;
非阻塞的好处:不会占用父进程的所有精力,可以在轮询期间干干别的。
2.进程替换
前面使用fork创建子进程,创建子进程的目的是什么呢?
2.1进程替换的目的
a.想让子进程执行父进程代码的一部分
执行父进程对应的磁盘代码的一部分
b.想让子进程执行一个全新程序
让子进程想办法,加载到磁盘上指定的程序。执行新程序的代码和数据 。
见识一下如何进行进程替换的。
2.2execl替换函数
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 printf("process is runing\n");
12 //所有exec系列接口,都必须在传参结束的时候,以NULL结尾
13 execl("/usr/bin/ls","ls","-l",NULL);
14 printf("hello\n");
15
16 return 0;
17 }
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 printf("process is runing\n");
12 //所有exec系列接口,都必须在传参结束的时候,以NULL结尾
13 //给可执行程序增加颜色
14 execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
15 printf("process running done...\n");
16
17 return 0;
18 }
见识见识猪跑后。理解理解原理。
2.3理解原理
程序替换的本质:就是将指定程序的代码和数据加载到指定的位置!覆盖自己的代码和数据。
所以进程替换的是没有创建新的进程。
exexl后面的printf没有执行了,是因为printf也是代码,是在execl之后的,execl执行完毕的时候,代码已经被全部覆盖了,开始执行新的程序的代码了,所以printf就无法执行。
execl调用失败,就是没有替换成功。
并且exec系列还没有返回值,因为成功了,就和接下来的代码无关了,判断毫无意义,excel只要返回了,一定是错误了。
那么子进程替换会影响父进程码?
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 //子进程
16 execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
17 exit(1);
18 }
19 //父进程
20 int status=0;
21 pid_t ret=waitpid(id,&status,0);
22 if(ret > 0)
23 {
24 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
25 }
26 return 0;
由上面结果得知:子进程替换,并不是影响父进程,是因为虚拟地址空间+页表保证进程独立性,一旦有执行流想要替换代码或者数据,发生写时拷贝。
2.4其他替换接口
2.4.1execl
1int execl(const char *path, const char *arg, …)
l---->list:将参数一个一个传入execl
2.4.2execlp
int execlp(const char *file, const char *arg, …)
p---->path:解决如何找到程序的功能,带p字符的函数,不用告诉我程序的路径,你只要告诉我是谁,我会自动在环境变量PATH,进行可执行程序的查找。
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 //子进程
16 // execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
17 // 这里有两个ls,其实并不重复,一个是告诉系统我要执行谁,一个是告诉系统,我想怎么执行
18 execlp("ls","ls","-l","--color=auto",NULL);
19 exit(1);
20 }
21 //父进程
22 int status=0;
23 pid_t ret=waitpid(id,&status,0);
24 if(ret > 0)
25 {
26 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
27 }
28 return 0;
2.4.3execv
int execv(const char *path, char *const argv[]);
v----->vector:可以将所有的执行参数,放入数组中,统一传递,而不用使用可变参数方案
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 //子进程
16 // execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
17 // 这里有两个ls,其实并不重复,一个是告诉系统我要执行谁,一个是告诉系统,我想怎么执行
18 // execlp("ls","ls","-l","--color=auto",NULL);
19 char* const argv[]={"ls","-l","--color=auto",NULL};
20 execv("/usr/bin/ls",argv);
21 exit(1);
22 }
23 //父进程
24 int status=0;
25 pid_t ret=waitpid(id,&status,0);
26 if(ret > 0)
27 {
28 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
29 }
30 return 0;
2.4.4execvp
int execvp(const char *file, char *const argv[]);
13 if(id == 0)
14 {
15 //子进程
16 // execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
17 // 这里有两个ls,其实并不重复,一个是告诉系统我要执行谁,一个是告诉系统,我想怎么执行
18 // execlp("ls","ls","-l","--color=auto",NULL);
19 char* const argv[]={"ls","-l","--color=auto",NULL};
20 // execv("/usr/bin/ls",argv);
21 execvp("ls",argv);
22 exit(1);
23 }
24 //父进程
25 int status=0;
26 pid_t ret=waitpid(id,&status,0);
27 if(ret > 0)
28 {
29 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
30 }
以上都是执行系统命令,那如果想执行我们自己写的程序呢?
1 #include<stdio.h>
2
3 //myexe
4 int main()
5 {
6 printf("我是一个C程序\n");
7 printf("我是一个C程序\n");
8 printf("我是一个C程序\n");
9 printf("我是一个C程序\n");
10 printf("我是一个C程序\n");
11 printf("我是一个C程序\n");
12 printf("我是一个C程序\n");
13 printf("我是一个C程序\n");
14
15 return 0;
16 }
我们知道make默认从上到下扫描文件,形成一个可执行目标文件。默认扫描第一个。如果想一次执行多个命令,怎么做?
.PHONY:all
all: mytest myexe
mytest:test.c
gcc -o $@ $^ -std=c11
myexe:myexe.c
gcc -o $@ $^ -std=c11
.PHONY:clean
clean:
rm -f mytest myexe
被PHONY修饰的是伪对象。这样一次形成两执行。
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 //子进程
16 // execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
17 // 这里有两个ls,其实并不重复,一个是告诉系统我要执行谁,一个是告诉系统,我想怎么执行
18 // execlp("ls","ls","-l","--color=auto",NULL);
W> 19 char* const argv[]={"ls","-l","--color=auto",NULL};
20 // execv("/usr/bin/ls",argv);
21 // execvp("ls",argv);
22 //执行自己写的程序,这里第二个参数可以不带./
23 execl("./myexe","myexe",NULL);
24 exit(1);
25 }
26 //父进程
27 int status=0;
28 pid_t ret=waitpid(id,&status,0);
29 if(ret > 0)
30 {
31 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
32 }
注意:可以执行不同语言的可执行程序。
1 #include<iostream>
2 using namespace std;
3
4 int main()
5 {
6 cout<<"hello C++"<<endl;
7 cout<<"hello C++"<<endl;
8 cout<<"hello C++"<<endl;
9 cout<<"hello C++"<<endl;
10 cout<<"hello C++"<<endl;
11 cout<<"hello C++"<<endl;
12 return 0;
13 }
~
还可以使用python,java等等进行替换。
程序替换,可以使用程序替换,调用任何后端语言对应的可执行程序。
2.4.5 execle
int execle(const char *path, const char *arg, …, char * const envp[]);
e:传入自定义环境变量
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
W> 15 char* const envp[]={(char*)"MYENV=11223344"};
16 execle("./myexe","myexe",NULL,envp);
17 exit(1);
18 }
19 //父进程
20 int status=0;
21 pid_t ret=waitpid(id,&status,0);
22 if(ret > 0)
23 {
24 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
25 }
26 return 0;
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main()
5 {
6 //系统自带
7 printf("PATH:%s\n",getenv("PATH"));
8 printf("PWD:%s\n",getenv("PWD"));
9 //自定义环境变量
10 printf("MYENV:%s\n",getenv("MYENV"));
11 printf("我是一个C程序\n");
12 printf("我是一个C程序\n");
13 printf("我是一个C程序\n");
14 printf("我是一个C程序\n");
15 printf("我是一个C程序\n");
16 printf("我是一个C程序\n");
17 printf("我是一个C程序\n");
18 printf("我是一个C程序\n");
19
20 return 0;
21 }
注意,传入自定义环境变量,在myexe中,只会显示自定义环境变量的值,系统自带的环境变量不显示。
如果想打印出系统自带的环境变量,需要使用系统提供给我们的environ。
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 extern char**environ;
16 //即使不传,子进程也能获取环境变量
17 execle("./myexe","myexe",NULL,environ);
18 //两种写法都可以
19 //execle("./myexe","myexe",NULL);
20 exit(1);
21 }
22 //父进程
23 int status=0;
24 pid_t ret=waitpid(id,&status,0);
25 if(ret > 0)
26 {
27 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
28 }
29 return 0;
如果即想要显示自定义环境变量,又想要显示系统默认的环境变量,就将指定环境变量导入到系统中,也就是environ指向的环境变量表
1 #include<stdio.h>
2 #include<assert.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8
9 int main()
10 {
11 pid_t id=fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 putenv((char*)"MYENV=11223344");//将指定环境变量导入到系统中 environ指向的环境变量表
16 extern char**environ;
17 execle("./myexe","myexe",NULL,environ);
18 exit(1);
19 }
20 //父进程
21 int status=0;
22 pid_t ret=waitpid(id,&status,0);
23 if(ret > 0)
24 {
25 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
26 }
27 return 0;
我们知道可执行程序是一个文件,放在磁盘里的,要程序要运行的时候,需要加载到内存,为什么要加载呢?这是操作系统体系所决定的,cpu只会和内存打交道。那么如何加载呢?
是linux中exec*系统接口(加载器)加载的。
请问程序是先加载还是先执行main呢?
肯定是先加载
虽然前4个exec系列接口没有环境变量参数,但是子进程照样还是可以拿到系统默认的环境变量。因为地址空间有一块区域,存放命令行参数环境变量。
2.4.6execvp
int execvpe(const char *file, char *const argv[],char *const envp[]);
这个函数就不再演示了,就是增加了自动去环境变量寻找程序路径。
2.5总结
上面共学了6个进程替换函数,其实还有一个,这一个才是真正执行程序替换。
这个函数属于系统调用接口,其他exec系列接口都是对其封装,主要是为了满足不同的应用场景。
自此关于进程等待,进程替换内容结束了,这篇博文比较长。喜欢的点赞,评论,收藏+关注!!!