(创建,终止,退出,等待))
进程创建写时拷贝(补充)关于写时拷贝在上一篇博客里已经提到过了只不过现在想补充一个点fork之后父子进程的代码和数据是共享的一般来说代码是只读的数据是读写的但是fork之后会特殊一点代码依旧是只读的数据也会变成只读在父子进程没有任何一方要修改数据时父子进程的代码和数据是映射到同一个物理内存空间的。假设现在有一方进程要修改共享数据了OS会介入因为现在数据是只读的OS就会先检查一下是野指针还是进程被挂起数据不在物理内存里了.....如果都不是就说明是父子进程有一方要修改数据就是要发生写时拷贝了数据再从原来的读权限变成读写权限。进程终止进程终止的时候内核会释放内核数据结构代码和数据。注意进程终止时候会先释放代码和数据内核数据结构最后才释放因为有僵尸进程。你的进程终止会有几种情况1.代码跑完结果正确2.代码跑完结果不正确3.代码没跑完进程异常了。其中第三种情况会在后边的博客里体现本篇只讨论前两种你怎么知道你的结果对还是不对答案main函数的返回值这个返回值其实也叫做进程的退出码为0就说明进程是正常结束的不为0就说明进程的结果不正确。除了0之外的数字都表示不同的含义用于告诉OS非正常退出的具体原因这个含义可以是日后你的公司自定义的也可以是系统提供(规定)的。echo $? 可以查看最近一个进程执行完的结果。($相当于指针解引用?就代表一个变量)。计算机适合看数字来知道错误原因但是人不适合因此strerror这个函数可以帮我们查看具体每一个退出码的含义。(下图只是片段)还有一个码叫做库函数调用失败的错误码存在errno这个全局变量里边(C语言里边)错误码和退出码只是名字不一样实际上意思都是一个意思可以混用。接下来简单说一下进程终止的第三种情况代码没跑完进程异常了。首先在这种情况下return没有执行退出码是没有意义的那是什么原因导致进程异常了呢答被信号终止了。之前的博客里也提到过了kill指令可以发出信号让进程终止通过kill -l可以查看所有的信号编号(注意是从1开始的)。综上进程退出的3种情况可以再细化总结一下1.代码跑完(代码运行期间没有收到信号)可以理解成收到了0信号(因为信号编号从1开始0本身没意义) return 0 - 信号数字0 退出码02.信号数字0 退出码!03.信号数字!0 退出码无意义也就是说进程执行的结果状态可以用两个数字表示一个是信号数字一个是退出码。用户本身不需要维护当一个进程退出的时候OS会把进程退出的详细信息写入到进程的task_struct里所以进程退出的时候需要僵尸维护自己的退出状态。进程退出这里主要是讲的是进程终止的前两种情况。在不考虑异常的情况下如何让进程退出1.main函数return2.在任意地方调用exit进程都会结束。(非main函数return函数结束非main函数exit进程结束)。除此之外还有一个系统调用_exit功能跟exit一样exit为最佳实践。exit VS _exitexit是库函数_exit是系统调用库函数是在上层系统调用是在下层exit终止进程之后会强制刷新缓冲区但是_exit不会。因此可以得出一个结论缓冲区和刷新缓冲区的操作一定不在内核里缓冲区其实是C/C维护的(具体之后说)。进程等待进程等待就是子进程退出之后进入僵尸状态等待父进程来回收它。进程等待是很有必要的子进程进入僵尸状态之后OS会保留它的内核数据结构外界也杀不掉它因为它已经死了这就导致内核数据结构一直留在内存里会有内存泄露的问题所以等待是必须的不等会有内存泄露问题。还有一种非必须情况就是可能父进程要获取子进程的退出信息(退出码或者退出信号这个也在内核数据结构里)。所以需要进程等待解决子进程的僵尸问题。验证父进程等待能解决子进程的僵尸问题。我们用到了wait系统调用用来回收子进程(waitpid函数的功能完全覆盖wait所以waitpid是最佳实践)。它的参数你先不要管我们就先当它是NULL然后如果等待成功了就返回子进程的PID等待失败就返回-1。(下边代码意思子进程运行总时间为5s父进程总共休眠10s在子进程运行完还要休眠5s这个5s可以看到子进程的僵尸状态子进程运行完再等5s之后父进程等待子进程成功然后父进程自己退出。)1 #includestdio.h 2 #includeunistd.h 3 #includestdlib.h 4 #includesys/wait.h 5 #includesys/types.h 6 #includeunistd.h 7 8 int main() 9 { 10 pid_t id fork(); 11 if(id 0) 12 { 13 //child 14 int cnt 5; 15 while(cnt--) 16 { 17 printf(我是子进程pid为%d\n, getpid()); 18 sleep(1); 19 } 20 exit(0); 21 } 22 else 23 { 24 sleep(10); 25 //parent---fork给父进程返回子进程的pid 26 pid_t rid wait(NULL); 27 if(rid id) 28 { 29 printf(等待成功pid%d, getpid()); 30 } 31 exit(0); 32 } 33 34 return 0; 35 }由观察结果能看出来当父进程一回收子进程立马从僵尸状态变成死亡状态了(死亡状态看不到查不出来就表示死亡了)。对于wait系统调用还想说一点就是如果父进程wait子进程但是子进程就是没有退出则父进程会阻塞在wait函数中(阻塞等待)。验证如何获取子进程的退出信息。用waitpid函数。先来看看waitpid函数的相关信息。注pid-1options0时waitpid和wait在功能上没有任何区别。pid_ t waitpid(pid_t pid, int *status, int options);返回值当正常返回的时候waitpid返回收集到的⼦进程的进程ID如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在(比如说父进程等待的不是自己子进程就会调用出错)参数pidPid-1,等待任⼀个⼦进程。与wait等效。Pid0.等待其进程ID与pid相等的⼦进程。status: 输出型参数WIFEXITED(status): 若为正常终⽌⼦进程返回的状态则为真(查看进程是否是正常退出信号数字是否为0)WEXITSTATUS(status): 若WIFEXITED⾮零提取⼦进程退出码(查看进程的退出码)options:默认为0表⽰阻塞等待WNOHANG: 若pid指定的⼦进程没有结束则waitpid()函数返回0不予以等待。若正常结束则返回该⼦进程的ID。重点来说一下第二个参数statusstatus参数是一个输出型参数是获取子进程退出信息的子进程退出有3种情况就是上文进程退出那个标题里说的那3种也就是说子进程的退出信息本质上是要得到子进程退出的时候的那两个数字(信号数字和退出码)。那一个int类型的status变量怎么能同时得到两个数字呢一个int类型4个字节32bit其中前16个bit不考虑主要是考虑后16个bit后16bit中后8位表示信号编号前8位表示退出码。(注因为后8位信号位有1位表示core dump因此实际上信号位有7位数字)。接下来来看一个案例验证一下这个事。1 #includestdio.h 2 #includeunistd.h 3 #includestdlib.h 4 #includesys/types.h 5 #includesys/wait.h 6 7 int main() 8 { 9 pid_t id fork(); 10 if(id 0) 11 { 12 //child 13 int cnt 5; 14 while(cnt--) 15 { 16 printf(我是子进程PID: %d\n, getpid()); 17 } 18 exit(1); 19 } 20 else 21 { 22 //parent 23 int status 0; 24 pid_t rid waitpid(id, status, 0); 25 if(rid id) 26 { 27 printf(等待成功status: %d\n, status); 28 } 29 exit(0); 30 } 31 return 0; 32 }上文代码里子进程的退出码为1父进程里waitpid的参数status获得了1然后最终打印结果是256原因就是上文说过的由于父进程是正常return终止因此后8位信号位为全0也就是00000000前8位退出码为1也就是00000001最终status的二进制表示为0000000100000000转化成10进制就是256。根据status还可以得到子进程的退出码或者信号数字。退出码(status 8) 0xFF(11111111)信号数字status 0x7F(01111111)(信号位只有7位)注意这里的0xFF和0x7F也是int类型有32个bit位因为C语言里进行运算时有隐式类型转换。父进程通过waitpid是如何得到子进程的退出信息的子进程退出后会进入Z状态OS会保留Z状态时进程的PCBPCB里的exit_code和exit_signal里会保存子进程的退出信息waitpid的第一个参数是子进程的PID凭借PID找到对应的子进程并且读取子进程PCB里的信息。那如果waitpid的第一个参数为-1呢为-1表示获取任何一个子进程的退出信息在父进程的task_struct里边有一个子进程链表一个兄弟进程链表通过便利它们就可以获取到进程状态为Z的进程的退出信息了。上文我们获取status里子进程的退出码和信号数字是用的位运算的方式除此之外waitpid还给我们提供了许多的宏比较重要的如下(上文介绍waitpid那里截图下来的)WIFEXITED(status)这个宏里是用来检测信号是否为0WEXITSTATUS(status)是用来获取子进程的退出码。说实话就是这两个宏就是系统帮我们把位操作给搞完了不用我们自己搞了。之前一直忽略了waitpid的第三个参数上文所有的代码都是传的默认值0当传0的时候其实就是让父进程阻塞等待子进程退出接收到子进程的退出信息之后父进程才会执行自己之后的代码。这样其实会占用资源因为父进程自己也是一个进程但是它就光等了没有干自己的事情有点浪费因此waitpid的第三个参数也给了我们第二种选择WNOHANG(等待不要宕机了)传这个参数时父进程是会非阻塞等待的非阻塞等待的意思就是调用waitpid检测子进程是否退出时如果子进程没有退出父进程不会阻塞在那里而是继续执行它自己后边的代码然后在执行过程中父进程会非阻塞轮询(自己实现的waitpid不会帮你干它只会检测一次再要检测要重新调用)。结合以上全部关于waitpid这个系统调用的解释我们整合一下完整版代码如下。(用到了几乎所有的waitpid的返回值以及参数的值等信息)。1 #includestdio.h 2 #includeunistd.h 3 #includestdlib.h 4 #includesys/types.h 5 #includesys/wait.h 6 7 int main() 8 { 9 pid_t id fork(); 10 if(id 0)//创建失败 11 { 12 perror(fork); 13 exit(1); 14 } 15 else if(id 0) 16 { 17 //child 18 int cnt 5; 19 while(cnt--) 20 { 21 printf(我是子进程PID: %d\n, getpid()); 22 sleep(1); 23 } 24 exit(0); 25 } 26 else 27 { 28 //parent 29 //写成循环才能实现非阻塞轮询的效果 30 while(1) 31 { 32 int status 0; 33 //单次调用waitpid的作用只有两个,非阻塞检测和回收子进程 34 pid_t rid waitpid(id, status, WNOHANG); 35 if(rid 0)//子进程已经退出 36 { 37 if(WIFEXITED(status)) 38 { 39 printf(等待成功, 子进程PID:%d, 退出信息:%d, 退出码:%d\n, rid, status, WEXITSTATUS(status)); 40 } 41 else 42 { 43 printf(异常退出\n); 44 } 45 break; 46 } 47 else if(rid 0)//子进程未退出 48 { 49 printf(子进程正在运行父进程还需等待\n); 50 usleep(100000); 51 } 52 else//检测失败 53 { 54 perror(waitpid); 55 break; 56 } 57 } 58 } 59 return 0; 60 }非阻塞等待和阻塞等待哪一种更高效一点呢你问等待效率高是不是比的是等待时间的长或者短而不管是非阻塞等待还是阻塞等待等待的时间都由子进程决定所以其实不存在谁效率高的问题只不过是阻塞等待在等待期间什么都没干非阻塞等待在等待期间可以干其他的事情它把等待的时间利用了起来。接下来来说说非阻塞等待能干啥其他的事情(下边的截图是基于上边最终版完整代码多出部分的截图目的仅为让我们知道非阻塞等待的时候是可以干其他事情的)。