一 进程终止
(1)exit和return
先前已经了解了进程创建,以及进程大致相关的数据结构,但是有个小知识一直没提及,那就是exit,还有就是return 0。这两个的作用有点相似,都可以终止进程,但又有点不同,例如return 只有在main函数内才可以结束进程,而exit在任意地方都可以结束进程。
终止场景
1:代码正确跑完,结果正确
2: 代码跑完,结果不正确
3: 异常终止
我们之前写代码的时候总是先写个main函数,然后return 0,那为什么返回0,返回给谁呢?在linux下是bash创建子进程是帮我们执行代码的,bash这个父进程是需要关心子进程执行的怎么样了,因为bash要向用户报告执行情况,所以需要子进程的退出信息。
echo $?能获得最近进程的退出码,获得退出码后,lastcode貌似会被更新为0,所以这个时候再来一次echo $呢?,此时获得的是lastcode的初始值0,可能有人会说第二次echo &?获得的lastcode是echo获得的退出码,我认为echo此时没有创建子进程,为什么说echo $?的时候不创建子进程呢? 如果创建了子进程,第一次echo $?会更新lastcode,那退出码不就一直是子进程echo自己的吗,实际上如下:
退出码是1,如果是echo的退出码,那绝对是正常退出的返回码0,不会是1。
后面我实现shell的时候浅浅想过这个问题,而且我认为echo有时候是会创建子进程的,例如重定向的时候,具体原因不好说,但和重定向有不小的关系,后续再谈。
(2)出错码errno介绍
因为库函数的返回值并不能说明出错原因,所以想要知道出错码可以看——errno,errno会记录最后一个调用失败的库函数的错误码,但是要转为错误原因,还得用strerror,perror内部也是用了strerror和errno,strerror和perror的区别在于,perror可以给出错文字添加额外的字符串,例如函数名,可以提醒出错位置,可以精准锁定bug。
(3)出现异常,获得的退出码无效?
有一种解释是说出现异常往往是return之前发生的,没有退出码,所以无效,实际上我去测试让子进程sleep的时候被kill掉,此时还没return,但是退出码却有值,虽然显示的是未知错误,我估计是调用main函数的CRT_START()函数发现main没有退出码,然后外部又要mian的退出码,只能给个随机值了,这个随机值可能就对应一个未知错误。
此时这个退出码无疑是无意义的,但是还有种情况是我们正常return 了一个未知的退出码,此时进程还没结束(因为main函数结束了,但不代表进程结束了),此时进程被kill了,这个退出码是有用的。
这两种情况都有异常和退出码,但无法区分这个异常是在返回前出的,还是已经main函数已经return了后出的,感觉也不好区分,所以只能认为退出码无效。简单理解就是你这个进程出了异常,多多少少进程本身都有点错误,所以我们认为执行结果—退出码不能相信。
如何判断有无异常,看有无信号?(这个下面进程等待会提及)
(4)exit和_exit区别
_exit是系统调用接口,而exit内部是封装了_exit的,我们来看看它们在结束程序时有什么不同。
test.c ? ? ?? buffers
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 int main()
6 {
7 printf("begin\n");
8 int ret = fork();
9 if(ret==0)
10 {
11 while(1)
12 {
13 printf("我是子进程,id:%d",getpid());
14 exit(0);
15 }
16 }
17 else
18 {
19 int cnt = 1;
20 while(cnt)
21 {
22 printf("我是父进程,id:%d",getpid());
23 cnt--;
24 _exit(0);
25 }
26 }
27 return 0;
28 }
~
执行结果如下,有个问题就是,父进程打印的数据没了,子进程的却会显示,奇怪这是为什么呢?
这个就和缓冲区有关了,在windows我们对缓冲区没太多感觉,应该是vs做了特殊封装,让我们几乎感觉不到缓冲区,printf打印其实是往缓冲区打印的,而exit会刷新这个缓冲区,这个缓冲区是用户的,_exit这个系统调用看不见,简单理解缓冲区就是一段malloc的空间,设计者在写系统调用的时候,他怎么会去看你用户malloc的空间要不要刷新呢?
二 什么是进程等待
是调用系统调用waitpid和wait查看子进程状态和回收子进程资源。
三 为什么要有进程等待
首先子进程是父进程创建出来帮自己干活的,所以设计者认为需要给父进程提供能获取到子进程退出信息的接口,有很多场景我们都是必须要知道子进程的退出信息的,这个接口是必要的,这就是需要进程等待的原因。
那不能用全局变量吗,不行,因为进程具有独立性,父进程看不到子进程的数据,所以拿不到。还有就是子进程退出后会把退出信息存到pcb数据结构中,这个数据结构存在内核数据区,是操作系统内部的数据,是不允许用户访问,简单理解,父进程是我们用户写的代码运行产生的,也就代表用户,所以父进程无法访问子进程的数据,所以必须通过系统调用返回数据。
而为了让父进程能获取到,会让子进程退出后保持僵尸,不让系统将其处理掉,等待父进程读取状态信息,当父进程获取到子进程退出信息后,就可以销毁子进程了,所以进程等待还附有回收资源的功能。
四 进程等待代码(使用用例)
目前来看,进程等待是必须的,因为要用于回收子进程资源。那如何进行进程等待,如何使用系统调用呢?
wait介绍
man 3 wait查看man手册中的wait函数介绍。wait参数先不管,等会介绍完waitpid也就知道了
先来看看如何使用。
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6 int main()
7 {
8 int id = fork();//创建子进程
9 if(id < 0)
10 {
11 perror("fork error:");
12 }
13 else if(id == 0)//子进程
14 {
15
16 int cnt = 5;
17 while(cnt--)
18 {
19 printf("我是子进程: pid:%d , ppid: %d \n",getpid(),getppid());
20 sleep(1);
21 }
22 exit(0); 退出
23 }
24 else
25 {
26 int cnt = 10;
27 while(cnt--)
28 {
29 printf("我是父进程: id: %d pid: %d \n ",getpid(),getppid());
30 sleep(1);
31 }
32 //等待子进程
33 int ret = wait(NULL);
34 if(ret == id )//返回正确
35 {
36 printf("回收完成\n");
37 }
38 else
39 {
40 perror("wait\n");
41 }
42 sleep(3);
43 }
44 return 0;
45 }
让子进程先退出,父进程一直在printf,此时子进程保持僵尸状态,执行到wait后,回收子进程。
若是子进程不退呢,父进程将会进入阻塞状态。之前在进程状态描述(scanf场景下)的阻塞状态是在等硬件资源,而现在是在等函数返回值,是在等软件资源,所以说等待资源就会处于阻塞状态。
wait是等任意一个子进程,那多个子进程会先回收谁,这是不确定的。
waitpaid介绍
可获得指定子进程的退出信息
参数1:传要回收子进程的pid,如果为-1,表示任意回收一个子进程。
参数2:status参数介绍,首先呢它是可以获取子进程的退出码,若不需要,传NULL即可。
59 int id = fork();//创建子进程
60 if(id == 0)
61 {
63 exit(1);
64 }
69 int status = 0;
70 int ret = waitpid(-1, &status,0);
71 printf("回收完成,id : %d 退出码: %d \n",ret,status);
73 return 0;
74 }
退出码为什么是256呢,子进程不是exit(1)吗,按道理退出码应该是1,这是为什么呢?这就要说status的构成了。0 - 6号比特位上存信号,第7位上是用于core dump,8-15用于退出码,其余位置暂时不关心。
那如何提取退出码和信号呢?位运算呗。但是这个对程序员的要求有点高,它需要程序员了解status的构成,所以系统提供了一些宏来帮助提取。
printf("退出码: %d,信号: %d\n",WIFEXITED(status),WEXITSTATUS(status));
返回值和等待失败
当我们等的不是自己的子进程时,这个时候就会等待失败,然后waitpid返回-1,等待成功返回子进程的pid。
参数3 option介绍
给0选择阻塞等待,就是父进程一直等子进程跑完代码,此时父进程啥也不干。这样有点浪费时间,所以就有了非阻塞轮询,给参数三WNOHANG传,
就是如果子进程没弄好,就直接返回,然后我们外部写个循环,循环调用waitpid,这就是非阻塞轮询时可以做自己的简单代码。