【Linux】进程控制
🌈个人主页:秦jh__https://blog.csdn.net/qinjh_?spm=1010.2135.3001.5343
🔥 系列专栏:https://blog.csdn.net/qinjh_/category_12625432.html
目录
fork函数初识
fork 函数返回值
写时拷贝
fork调用失败的原因
进程终止
进程退出场景
进程常见退出方法
进程等待
进程等待必要性
进程等待的方法
wait方法
waitpid方法
获取子进程status
非阻塞等待
前言
💬 hello! 各位铁子们大家好哇。
今日更新了Linux的进程控制的内容
🎉 欢迎大家关注🔍点赞👍收藏⭐️留言📝
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
//返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中 fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程。
fork 函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid。
为什么父进程返回的是子进程的pid?
为了让父进程方便对子进程进行标识,进而进行管理。
写时拷贝
通常,父子代码共享,父子不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。 (进程的独立性)
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
进程终止
进程终止做的事:
- 释放曾经的代码和数据所占据的空间
- 释放内核数据结构
内核数据结构中,PCB会被延期处理,因为有一种状态是僵尸状态。
#include<stdio.h>2 #include<unistd.h>3 4 int main()5 {6 printf("I am process, pid:%d, ppid:%d\n",getpid(),getppid());7 sleep(2);8 return 100; 9 }
任何命令行启动的进程,它的父进程都是bash,所以ppid都一样。
echo是内建命令,打印的都是bash内部的变量数据。?是一个变量名。
echo $?表示的是父进程获取到的,最近一个子进程退出的退出码。
main函数的返回值叫做进程的退出码。
退出码:
- 为0,标识成功
- 不为0,表示失败
第一个echo $?返回./myprocess 的退出码,第二个echo $?返回上一个echo $?的退出码
虽然echo $?没有创建子进程,但它是由父进程执行的,所以他也会影响?的值。
#include<stdio.h> 2 #include<unistd.h> 3 #include<string.h> 4 5 int main() 6 { 7 for(int errcode=0;errcode<=255;errcode++) 8 { 9 printf("%d:%s\n",errcode,strerror(errcode)); 10 } 11 printf("I am process, pid:%d, ppid:%d\n",getpid(),getppid()); 12 sleep(2); 13 return 0; 14 }
退出码不为0表示失败。不同的非0值,一方面表示失败,另一方面表示失败的原因。
strerror函数会将错误码转成对应的错误描述,如下图;
父进程为什么要得到子进程的退出码呢?
因为要知道子进程的退出情况。(成功还是失败,失败的原因是什么),然后展现给用户看。
退出码可以使用系统默认的,也可以自定义。
进程退出场景
进程终止的3中情况:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码异常终止
代码跑完,结果不正确的原因可以通过退出码确定。
一旦出现异常,退出码就没有意义了。
进程出异常,本质是因为进程收到了OS发给进程的信号。
int main() 6 { 7 int *p=NULL; 8 while(1)9 {10 printf("I am a process,pid:%d\n",getpid());11 sleep(1);12 *p=100;//野指针 13 } 19 sleep(2); 20 return 0; 21 }
当外面运行上面代码后,会报段错误。 OS就会提前终止进程。
我们把代码里的野指针注释掉,此时代码正常运行,一直循环。此时我们给该进程发11号信号,该进程即使没有错误,收到信号后,也会进行对应的报错。 所以说进程出异常,本质是因为进程收到了OS发给进程的信号。
所以如果进程异常了,我们可以通过退出信号,就可以判断进程为什么异常了,此时的退出码是无意义的。
在用户层面上,要确定进程是什么情况:
- 先确认是否异常
- 如果不是异常,就一定是代码跑完了,看退出码即可。
衡量一个进程退出,只需要两个数字:退出码和退出信号。
进程的PCB里面有退出信号和退出码,当进程退出时,会释放代码和数据,但是PCB会保存一段时间,该进程变成Z(僵尸)状态。父进程就可以从子进程的PCB中拿到退出信息。
进程常见退出方法
正常终止:
- main函数return,表示进程终止(非main函数的return,都只是表示函数结束)
- 调用exit函数 注意:在代码的任意位置调用exit,都表示进程退出
- _exit (系统调用)
下面是exit的使用举例:
_exit和exit在使用上没什么区别,只有一个细微的差别,如下例子:
上图是带\n的。结果打印并且换行了。
上面是不带\n的。结果打印了但没换行。
上面是不带\n的_exit的使用。结果什么也没打印。
结论:exit会在进程退出的时候,冲刷缓冲区,_exit不会。
exit在底层是调用_exit的。 由上面的结论可得,缓冲区在_exit之上,不然_exit也会冲刷缓冲区。
进程等待
进程等待必要性
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
- 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
wait方法
1 #include <stdio.h> 2 #include <unistd.h>3 #include <string.h>4 #include <stdlib.h>5 #include <sys/types.h>6 #include <sys/wait.h>7 8 void ChildRun()9 {10 int cnt = 5;11 while(cnt)12 {13 printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), get ppid(), cnt);14 sleep(1);15 cnt--;16 }17 }18 19 int main()20 {21 printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());22 23 pid_t id = fork();24 if(id == 0)25 {26 // child27 ChildRun();28 printf("child quit ...\n");29 exit(0);30 }31 // fahter32 pid_t rid = wait(NULL);33 if(rid > 0)34 {35 printf("wait success, rid: %d\n", rid);36 }37 }
作用:等待任何一个子进程退出
返回值:等待成功时,返回子进程的pid。失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
运行上面的代码,结果如下图:
上面代码if后面不需要else就表示是父进程的代码了。因为if里面即子进程里面用exit退出了,所以后面的都是父进程的。
下面做出修改:
运行结果:
修改后的代码先让父进程休眠十秒。子进程运行五秒后退出,此时由于父进程还在休眠无法回收,所以子进程就变成Z状态,再过五秒后,子进程就被父进程回收了。
如果我们把sleep(10)注释掉,此时父进程开始就马上进入等待,等待期间父进程不会被调度。如果子进程没有退出,父进程其实一直在阻塞等待。
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
- pid:
- Pid=-1,等待任一个子进程。与wait等效。
- Pid>0.等待其进程ID与pid相等的子进程。
- status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
waitpid有三个参数,当pid,即第一个参数为-1时,等待任意一个子进程,与wait等效。
当第一个参数pid>0时,就会等待其进程ID与pid相等的子进程 。
如下图,此时等待上方父进程的子进程。
等待失败例子:
当我们把pid给一个错误的,此时进程就是等待失败。
获取子进程status
第二个参数status代表的是子进程的退出信息。
退出信息=退出码+退出信号
- wait和waitpid,都有一个status参数,该参数是一个输出型参数。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待(只研究status低16比特 位):
最低7位表示终止信号,9到16位表示退出码。所以退出码的范围是0~255。退出信号的范围是0~125。这两个范围足以表示退出码的退出信号的情况了。
19 int main() 20 { 21 printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());22 23 pid_t id = fork(); 24 if(id == 0) 25 { 26 // child 27 ChildRun(); 28 printf("child quit ...\n");29 exit(1); 30 } 31 sleep(7); 32 // fahter 33 // pid_t rid = wait(NULL);34 int status=0; 35 pid_t rid=waitpid(id,&status,0);36 if(rid > 0) 37 { 38 printf("wait success, rid: %d\n", rid);39 } 40 else 41 { 42 printf("wait failed !\n");43 } 44 sleep(3); 45 printf("father quit, status:%d, child quit code : %d,child quit signal: % d\n",status,(status>>8)&0xFF,status&0x7F); 46 }
上面是通过status退出信息来获取退出码和退出信号的代码。
status右移8位,然后与0xFF即二进制的8个1进行按位与,获取退出码。
status按位与0x7F即二进制的7个1来获取退出信号。
结果表明,前面所说都是正确的。
实际上我们不使用位操作符处理status,而是使用两个宏,WIFEXITED和WEXITSTATUS。
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
非阻塞等待
我们用的大部分接口都是阻塞等待接口,在阻塞等待时,父进程干不了别的事,一直在等待子进程退出。下面介绍非阻塞等待。
这里也需要用到一个宏:WNOHANG
1 #include <stdio.h>2 #include <unistd.h>3 #include <string.h>4 #include <stdlib.h>5 #include <sys/types.h>6 #include <sys/wait.h>7 8 void ChildRun()9 {10 int cnt = 5;11 while(cnt)12 {13 printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid (), cnt);14 sleep(1);15 cnt--;16 }17 }18 19 int main()20 {21 printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());22 23 pid_t id = fork();24 if(id == 0)25 {26 // child27 ChildRun();28 printf("child quit ...\n");29 exit(123);30 }31 32 //father33 while(1)34 { 35 int status=0;36 pid_t rid=waitpid(id,&status,WNOHANG); //非阻塞等待37 if(rid==0)38 {39 sleep(1);40 printf("child is running, father check next time!\n");41 //DoOtherThing();42 } 43 else if(rid>0)44 {45 if(WIFEXITED(status))46 { 47 printf("child quit success, child exit code:%d\n",WEXITSTATUS(status));48 }49 else50 {51 printf("child quit unnormal!\n");52 }53 break;54 } 55 else56 {57 printf("waitpid failed!\n");58 break;59 } 60 }
使用WNOHANG的时候,需要使用循环结构。因为WNOHANG只会查看一次子进程是否结束,使用循环结构就可以到最后判断子进程是什么情况了。即非阻塞等待的时候+循环=非阻塞轮询。
在非阻塞等待时,父进程可以在每次查看子进程的间隙做其他事情。
pid_ t waitpid(pid_t pid, int *status, int options);
pid_t >0 :等待成功,子进程退出了,并且父进程回收成功
pid_t <0 :等待失败了
pid_t =0 :检测是成功的,只不过子进程还没退出,需要下一次进行重复等待。