文章目录
1.进程控制
推荐大家可以观看B站大丙老师的教程很干货
1.1 进程基础知识概述
简介:
程序和进程时两个概念,两者的状态和占用系统资源都是不同的;
程序: 磁盘上的可执行文件,占用磁盘上的空间,是一个静态的概念
;
进程: 程序执行之后被称为进程,不占用磁盘空间,需要消耗系统 内存、CPU资源等,每个运行的进程都对应一个自己的虚拟内存地址,是一个动态的概念
;
小节:
动态性是进程最基本的特征
进程是资源分配、接受调度的基本单位
异步性会导致并发程序执行结果的不确定性
1.1.1 并发和并行
CPU时间片:
CPU在某一个时间点只能处理一个任务,但是操作系统都是支持多任务的,那计算机在只有一个CPU的情况下如何处理多任务?
CPU会给每个进程分配一个时间段 ,进程在获得到该时间段之后才可以运行,从而使得每个进程在宏观上同时运行,如果在时间片结束是进程还在运行,CPU的使用权限将被收回,该进程会被中断挂起并等待下一个时间片,如果进程在时间片结束前阻塞或者结束,则CPU会立即切换
,避免浪费CPU资源;
并发和并行
并发和并行都是完成多任务更加有效率的方式,但还是有一些区别的,并发,并行,可见他们的确是有区别的;
并行性: 两个或多个事件在同一时间同时发生
并发性: 两个或多个事件在同一时间间隔发生,交替进行。
1.1.2 PCB
PCB -进程控制块
,Linux内核的进程控制块本质上是一个task_struct 的结构体
。该结构体记录了进程运行相关的一些信息;
-
进程 id: 每一个进程都一个唯一的进程 ID,类型为
pid_t
, 本质是一个整形数 -
进程的状态: 进程有不同的状态,状态是一直在变化的,有
就绪、运行、挂起、停止
等状态。 -
进程对应的虚拟地址空间的信息。
-
描述控制终端的信息,进程在哪个终端启动默认就和哪个终端绑定。
-
当前工作目录:默认情况下,启动进程的目录就是当前的工作目录
-
umask 掩码:在创建新文件的时候,通过这个掩码屏蔽某些用于对文件的操作权限。
-
文件描述符表: 每个被分配的文件描述符都对应一个已经打开的磁盘文件
-
和信号相关的信息:在 Linux 中 调用函数 , 键盘快捷键 , 执行shell命令等操作都会产生信号。
-
阻塞信号集:记录当前进程中阻塞哪些已产生的信号,使其不能被处理
未决信号集:记录在当前进程中产生的哪些信号还没有被处理掉。 -
用户 id 和组 id:当前进程属于哪个用户,属于哪个用户组
-
会话(Session)和进程组:多个进程的集合叫进程组,多个进程组的集合叫会话。
-
进程可以使用的资源上限:可以使用 shell 命令 ulimit -a 查看详细信息。
1.1.3 进程的状态
进程一共有五种状态分别为:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态) 其中创建态和退出态维持的时间是非常短的,稍纵即逝。我们主要是需要将
就绪态 , 运行态 , 挂起态
,三者之间的状态切换搞明白;
注意:不能有阻塞态直接转换为运行态 ,也不能由就绪态直接转化为阻塞态(因为阻塞态是进程主动请求的,需要在进程运行时才能发出请求)
就绪态 : 万事俱备,只差CPU资源
- 进程被创建出来,有运行的资源但是没有运行,需要抢CPU时间片;
- 获得到时间片之后,进程开始运行,从就绪态转换为运行态;
- 进程的时间片用完之后,再次失去CPU,从运行态转换为就绪态;
运行态: 获取到CPU资源的进程,进程只有在此状态下才能运行
- 运行态不会持续,在CPU时间片使用完后,会由运行态转换为就绪态;
- 当进程主动申请资源(例如申请打印机资源),会主动有运行态转换为阻塞态;
阻塞态: 进程放弃CPU ,并没有抢时间片的资格
- 当进程主动申请资源(例如申请打印机资源),会主动有运行态转换为阻塞态;
- 当某些条件被满足了(比如:slee () 睡醒了),进程的阻塞状态也就被解除了,进程从阻塞态转换为就绪态。
退出态: 进程被销毁 ,占用的系统资源被释放
- 任何状态的进程都可以直接转换为退出态;
1.1.4 进程命令
在知晓如何创建进程之间,先学会怎么查看并关闭进程
查看进程
$ ps aux
- a :查看所有终端的信息
- u: 查看用户相关的信息
- x: 显示和终端无关的进程信息
如果特别想知道每个参数控制着哪些信息,可以通过 ps a, ps u, ps x
分别查看。
杀死进程
kill 命令
可以发送某个信号到对应的进程,进程收到信号后默认的处理动作是退出进程
;如果需要给进程发送信号,可以查看Linux提供的标准信号;
# Linux 中的标准信号
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
9 号信号(SIGKILL)的行为是无条件杀死进程,想要杀死哪个进程就可以把这个信号发送给这个进程,操作如下:
# 无条件杀死进程, 进程ID通过 ps aux 可以查看
$ kill -9 进程ID
$ kill -SIGKILL 进程ID
1.2 进程创建
操作系统层面进程创建流程:
1.2.1 函数
Linux中进程ID为
pid_t
类型,本质为一个正整数,例如:PID为1的进程时Linux系统中创建的第一个进程
- 获取当前进程的进程ID (PID)
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
- 获取当前进程的父进程ID (PPID)
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
- 创建一个新的进程
#include <unistd.h>
pid_t fork(void);
1.2.2 fork() 详解
pid_t fork(void);
启动磁盘上的应用程序,得到一个进程,如果在该进程中调用fork() 函数
,就会得到一个新的进程;我们称为子线程,之前提到过每一个进程对应一个属于自己的虚拟地址空间, 子进程的地址空间是基于父进程的地址空间拷贝出来的,虽然是拷贝的,但是两个虚拟地址空间中存储的信息不可能完全相同
,如图所示:
两者的虚拟地址空间相同点:
拷贝完成之后(此时刻),两个地址的用户区数据是相同的
,其中用户区数据主要包括:
- 代码区: 默认情况下,父子进程地址空间中源代码始终一致;
- 全局数据区: 父进程中的全局变量和变量值全部被拷贝一份放到子进程的地址空间;
- 堆区: 父进程中的堆区变量和变量值全部被拷贝一份放到子进程的地址空间;
- 内存映射区(动态库加载区): 父进程中数据信息被拷贝一份放到子线程中;
- 栈区: 父进程中的栈区变量和 变量值全部被拷贝一份放到子进程的地址空间;
- 环境变量: 默认情况下,父子进程地址空间中的环境变量始终相同;
- 文件描述符表: 父进程中被分配的文件描述符都会拷贝到子进程中,在子进程中可以使用他们打开对应文件;
两者的虚拟地址空间区别:
-
父子进程各自的虚拟地址空间是互相独立的,互不干扰;
-
父子进程地址空间代码区虽然相同,但是父子进程执行的代码逻辑可能是不同;
-
由于父子进程的代码执行逻辑可能不同,
因此在地址空间拷贝之后,全局数据区、栈区、堆区、内存映射区(动态库加载区)数据会各自发生变
化,且地址空间互相独立,因此不会互相覆盖; -
各个进程都有自己的进程ID,内核区储存的父子进程ID是不同的;
-
进程启动后进入就绪态,运行需要争抢CPU时间片而可能执行不同业务逻辑,所以父子进程的状态可能是不同的;
-
fork() 函数调用成功后,会返回两个值,父子进程的值不同;
该函数调用成功后,会由一个虚拟地址变成两个,每个地址空间中都会将fork() 的返回值记录下来,因此返回两个值;
- 父进程返回一个大于0的数(由于记录子进程的ID);
- 子进程返回值标记为0;
- 在程序中可以通过fork()函数判断当前是父进程还是子进程;
int main() { // 在父进程中创建子进程 pid_t pid = fork(); printf("当前进程fork()的返回值: %d\n", pid); if(pid > 0) { // 父进程执行的逻辑 printf("我是父进程, pid = %d\n", getpid()); } else if(pid == 0) { // 子进程执行的逻辑 printf("我是子进程, pid = %d, 我爹是: %d\n", getpid(), getppid()); } else // pid == -1 { // 创建子进程失败了 } // 不加判断, 父子进程都会执行这个循环 for(int i=0; i<5; ++i) { printf("%d\n", i); } return 0; }
1.3 父子进程
1.3.1 父子进程执行位置
在父进程中创建了子线程,子线程就拥有父进程代码区的所有代码,那么子进程中的代码在什么位置开始运行?
父进程在
main() 函数开始运行
, 子进程是在父进程中调用 fork() 函数之后被创建出来,子进程就从 fork() 函数之后开始运行;
上图演示了父子进程中代码的执行流程,由上可知可以通过fork() 函数的返回值做出判断,从而控制父子进程的行为。如果没有进行(ID)的判断这部分的代码块会被 父子两个进程执行;
在进行多进程程序的编写时,要将代码想成多份进行分析,在直观上看代码之后一份,实际上数据有多份,并且多份数据中变量名都相同,但是它们的值不一定相同;
1.3.2 循环创建多个子进程
尝试实现一个简单的功能,在一个父进程中循环创建三个子进程,最后得到4个进程(一个父进程,三个子进程),同时为了验证程序的正确性,会打印出每个进程的ID;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//循环创建3个子线程
for (int i=0;i<3;i++)
{
//此时会出现8个进程
pid_t pid = fork();
printf("当前进程ID: %d \n",getpid());
}
return 0;
}
编译上诉代码,会得到8个进程
# 编译
$ gcc fork.c
$./a.out
# 最终得到了 8个进程
当前进程pid: 18774 ------ 1
当前进程pid: 18774 ------ 1
当前进程pid: 18774 ------ 1
当前进程pid: 18777 ------ 2
当前进程pid: 18776 ------ 3
当前进程pid: 18776 ------ 3
当前进程pid: 18775 ------ 4
当前进程pid: 18775 ------ 4
当前进程pid: 18775 ------ 4
当前进程pid: 18778 ------ 5
当前进程pid: 18780 ------ 6
当前进程pid: 18779 ------ 7
当前进程pid: 18779 ------ 7
当前进程pid: 18781 ------ 8
出现该问题主要是没有对fork()函数的返回值理解 , 对应多进程的程序,需要将一份代码进行多份进行分析,同时如果没有在程序中加条件控制,所有的代码父子进程都是可以执行的
,如图所示:
上图中的树状结构,蓝色节点代表父进程:
- 循环第一次 i = 0,创建出一个子进程,即红色节点,子进程变量值来自父进程拷贝,因此 i=0
- 循环第二次 i = 1,蓝色父进程和红色子进程都去创建子进程,得到两个紫色进程,子进程变量值来自父进程拷贝,因此 i=1
- 循环第三次 i = 2,蓝色父进程和红色、紫色子进程都去创建子进程,因此得到 4 个绿色子进程,子进程变量值来自父进程拷贝,因此 i=2
- 循环第三次 i = 3,所有进程都不满足条件 for(int i=0; i<3; ++i) 因此不进入循环,退出了。
通过上面的分析,最终得到解决方案,我们可以只让父进程创建子进程,如果是子进程不让其继续创建子进程,因此只需要在程序中添加关于父子进程的判断即可。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//循环创建3个子线程
for (int i=0;i<3;i++)
{
pid_t pid = fork();
if (pid ==0)
{
//不让子进程执行循环,直接跳出
break;
}
}
printf("当前进程ID: %d \n",getpid());
return 0;
}
编译并执行:
liu@liu-Ubuntu:~/vscode$ gcc forkDemo.c
liu@liu-Ubuntu:~/vscode$ ./a.out
当前进程ID: 6583
当前进程ID: 6582
当前进程ID: 6584
当前进程ID: 6585
注意:
在多进程中,进程的执行顺序是没有规律的,所有的进程都需要在就绪态挣抢CPU时间片,默认进程的优先级是相应的,操作系统不会让某一个进程一直抢不到CPU时间片
1.3.3 终端显示问题
在执行多进程程序的时候,经常会遇到下图中的问题,看似进程还没有执行完成,貌似是因为什么原因被阻塞了,实际上终端是正常的,当我们通过键盘输入一些命令,终端也能接受输入并且输出相关信息,那么为什么终端会显示成这个样子呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BaCQkbQ3-1647056684599)(E:/富贵的编程日记/Linux系统/多进程终端显示问题.png)]
-
a.out 进程启动之后,共创建了 3 个子进程,其实 a.out 也是有父进程的就是当前的终端
-
终端只能检测到 a.out 进程的状态,a.out 执行期间终端切换到后台,a.out 执行完毕之后终端切换回前台
-
当终端切换到前之后,a.out 的子进程还没有执行完毕,当子进程输出的信息就显示到终端命令提示符的后边了,导致终端显示有问题,但是此时终端是可以接收键盘输入的,只是看起来不美观而已。
-
想要解决这个问题,需要让所有子进程退出之后再退出父进程,
比如:在父进程代码中调用 sleep ()
pid_t pid = fork(); if (pid >0) { //使父进程睡一会 sleep(3); } else if (pid ==0) { //子进程 }
1.3.4 进程通信(错误尝试)
在使用fork()函数尝试使用全局变量互动:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int number =10;
int main()
{
printf("创建子线程之 number:%d \n",number);
//创建子进程
pid_t pid = fork();
//父子进程都会执行这一步
printf("当前进程 number:%d \n",number);
//父进程
if (pid >0)
{
printf("我是父进程,pid =%d ,number =%d \n",getpid(),number);
printf("父进程的父进程(终端进程)的ID =%d \n",getppid());
}
//子进程
else if (pid ==0)
{
number +=100;
printf("我是子进程,pid =%d ,number =%d \n",getpid(),number);
printf("子进程的父进程ID =%d \n",getppid());
}
return 0;
}
编译并调试:
liu@liu-Ubuntu:~/vscode$ gcc fork.c -o fork
liu@liu-Ubuntu:~/vscode$ ./fork
创建子线程之 number:10
当前进程 number:10
我是父进程,pid =7495 ,number =10
父进程的父进程(终端进程)的ID =6315
当前进程 number:10
我是子进程,pid =7496 ,number =110
子进程的父进程ID =7495
通过验证得到结论:两个进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间,两个同名全局变量存储在不同的虚拟地址空间中,二者没有任何关联性。
注意:
如果要进行进程间通信需要使用: 管道,共享内存,本地套接字,内存映射区,消息队列
等方式。
1.4 exec 族函数
在实际开发中,需要在一个进程中开启另一个可执行程序;在这种情况下,我们可以使用 exec族函数
;
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
该族函数执行成功后不会返回
,因为调用进程的实体,用户区数据基本被替换掉了(代码段、数据段、堆区、栈区),只留下一些进程ID等表面信息;调用失败返回 -1,从原程序的调用点接着往下执行;
exec族函数
实际上没有创建新进程的能力,而是让启动的进程寄生在自身的虚拟地址空间之内
,并挖空自己的地址空间用户区,将新启动的进程数据填充进去
exec族函数中最常用的的: execl() 和 execlp()函数
。 这两个函数是对其他4个函数做了进一步的封装;
1.4.1 execl() 函数
该函数可以用于执行任意一个可执行程序,函数需要通过指定文件路径才能找到该可执行程序;
#include <unistd.h>
//变参函数
int execl (const char *path,const char *arg ,....);
参数:
path: 需要启动的可执行程序的路径(推荐使用绝对路径);
arg: ps aux 查看进程的时候吗,启动的进程的名字,可随意指定(通常和该可执行文件同名);
…: 需要执行的命令参数 ,可以写多个,最后以NULL结尾
;
返回值: 如果该函数执行成功,没返回值 ; 函数执行失败,返回 -1;
1.4.2 execlp() 函数
该函数常用于执行已经设置了环境变量的可执行程序 ,函数中的
p
就是path
,即此函数会自动搜索系统的环境变量PATH
,因此使用此函数不需要指定路径,只需要指定出名字
;
// p = path
int execlp(const char *file ,const char *arg ,.....);
参数:
file: 可执行程序的名字;
-
在环境变量 PATH 中,可执行程序可以不加路径;
-
没有在环境变量中 , 可执行程序需要指定绝对路径;
arg: ps aux 查看进程的时候 , 启动的进程名字,可以随意指定,一般设置和需要启动的可执行文件名一致;
… : 要执行的命令需要的参数 , 可写多个 ,最后以 NULL结尾 ,表示参数指定完了;
返回值: 该函数执行成功 ,没有返回值 ; 如果执行失败 , 返回 -1;
1.4.3 exec族函数使用
使用exec族函数 ,一般不会在进程中直接调用 。如果直接调用导致此进程的代码区代码被替换,不能按原来流程工作;
使用时一般先创建一个子进程,在子进程中调用exec族函数,子进程的用户区数据被替代为开始执行新的程序中的代码逻辑,但是父进程不会受任何影响依然可以继续正常工作;
execl() 和 execlp() 函数的使用方法:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
//创建子进程
pid_t pid =fork();
//子进程 中执行磁盘上的可执行程序
if (pid ==0)
{
//磁盘上的可执行程序 /bin/ps
#if 1
execl("/bin/ps","title","aux",NULL);
//也可以这样写
//execl("/bin/ps","title","a","u","x",NULL);
#else
execlp("ps","title","aux",NULL);
//也可以这样写
//execlp("ps","title","a","u","x",NULL);
#endif
//如果函数调用失败,才会执行以下代码
perror("execl");
printf("+++++++++++++++++\n");
}
//父进程
else if (pid >0)
{
printf("我是父进程!\n");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1lBJr8xq-1647056684600)(E:/富贵的编程日记/Linux系统/exec族函数.png)]
1.5 进程控制
进程控制包括:
进程退出、进程回收 、进程特殊状态
;进程的特殊状态 :
孤儿进程 、 僵尸进程
;
1.5.1 结束进程
如果想直接退出程序可以在程序的任何地方调用
exit()
或_exit()函数
。该函数相当于退出码 ;如果参数值为0 程序退出之后的状态码就是 0 ,如果为100,退出的状态就是100;
//退出进程的函数,任何位置都可调用
//标准C库函数
#include <stdlib.h>
int exit(int status);
//Linux的系统函数
#include <unistd.h>
int _exit(int status);
在 main 函数中直接使用 return 也可以退出进程,假如是在一个普通函数中调用 return 只能返回到调用者的位置,而不能退出进程。
// ***** return 必须要在main()函数中调用, 才能退出进程 *****
// 举例:
// 没有问题的例子
int main()
{
return 0; // 进程退出了
}
// 不能退出的例子 //
int func()
{
return 666; // 返回到调用者调用该函数的位置, 返回到 main() 函数的第19行
}
int main()
{
// 调用这个函数, 当前进程能不能退出? ===> 不能
int ret = func();
}
1.5.2 孤儿进程
在一个启动的进程中创建子进程,此时父子进程同时运行,但是父进程因为某些原因先退出了,子进程还在运行,这个子进程被称为
孤儿进程
(很贴切);
当然了操作系统会监控每一个进程,当检测到某个进程变成孤儿进程后,系统会给这个孤儿进程找个爹 ;如果使用Linux没有桌面终端,这个领养进程就是 init进程(PID =1),如果有桌面终端,这个领养进程就是桌面流程;
那么为什么系统需要领养这个孤儿进程?
在子进程退出时,进程中的用户区可以自己释放,但是进程内核区的pcb资源自己无法释放,必须由父进程来释放pcb资源,孤儿进程被领养后这件事由领养的父进程处理,可以避免系统资源浪费。
生成一个孤儿进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
//生成子线程
pid_t pid = fork();
//父进程
if (pid >0)
{
printf("父进程ID =%d \n",getpid());
}
else if (pid ==0)
{
//强迫子进程睡眠1s , 这期间父进程退出,当前进程变为孤儿进程
sleep(1);
printf("子进程ID = %d ,父进程ID = %d \n",getpid(),getppid());
}
return 0;
}
编译并运行:
# 程序输出的结果
$ ./a.out
我是父进程, pid=22459
我是子进程, pid=22460, 父进程ID: 1 # 父进程向退出, 子进程变成孤儿进程, 子进程被1号进程回收
1.5.3 僵尸进程
一个启动的进程中创建子进程,此时父子两个进程运行,随后父进程正常运行,
子进程先与父进程结束,子进程无法释放自己pcb资源,需要父进程来释放,但是父进程没有释放
,此时子进程就变成了僵尸进程;
僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。 僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。
生成5个僵尸进程:
int main()
{
pid_t pid;
// 创建子进程
for(int i=0; i<5; ++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}
// 父进程
if(pid > 0)
{
// 需要保证父进程一直在运行
// 一直运行不退出, 并且也做回收, 就会出现僵尸进程
while(1)
{
printf("我是父进程, pid=%d\n", getpid());
sleep(1);
}
}
else if(pid == 0)
{
// 子进程, 执行这句代码之后, 子进程退出了
printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
}
return 0;
}
编译并运行:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHpi02k7-1647056684600)(E:/富贵的编程日记/Linux系统/僵尸进程.png)]
注意:
消灭僵尸进程的方法是杀死僵尸进程的父进程,这样僵尸进程的资源就会被系统回收了。
再次kill 命令
僵尸进程是没效果的(僵尸进程本身就是死的);
1.5.4 进程回收
为了避免僵尸进程的产生,一般我们在父进程进行子进程的资源回收,回收方式有两种:
wait()阻塞式 ,waitpid() 非阻塞式;
wait() 函数
wait()函数是一个阻塞函数 ,如果没有子进程退出,函数会一直阻塞等待,当检测到子进程退出,该函数阻塞解除回收子进程资源
。wait()函数被调用一次只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次;
#include <sys/wait.h>
pid_t wait(int *status);
参数: 传出参数,通过传递出的信息判断回收的进程是怎么退出的,如果不需要该信息可指定NULL,取出整形变量中的数据需要使用一些宏函数;
-
WIFEXITED(status): 返回 1, 进程是正常退出的
-
WEXITSTATUS(status): 得到进程退出时候的状态码,相当于 return 后边的数值,或者 exit () 函数的参数
-
WIFSIGNALED(status): 返回 1, 进程是被信号杀死了WTERMSIG(status): 获得进程是被哪个信号杀死的,会得到信号的编号
返回值:
成功: 返回被回收的子进程ID
失败: -1
-
没有子进程资源可以回收了,函数的阻塞会自动解除,返回 -1;
-
回收子进程资源的时候出现了异常;
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
// 创建子进程
for(int i=0; i<5; ++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}
// 父进程
if(pid > 0)
{
// 需要保证父进程一直在运行
while(1)
{
//回收子进程的资源
pid_t ret =wait(NULL);
if (ret >0)
{
printf("成功的回收了子进程资源,子进程PID: %d \n",getpid());
}
else
{
printf("回收失败,或已经没有子进程了。。。。。\n");
break;
}
}
}
else if(pid == 0)
{
// 子进程, 执行这句代码之后, 子进程退出了
printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
}
return 0;
}
waitpid() 函数
waitpid () 函数可以看做是 wait () 函数的升级版,通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
#include <sys/wait.h>
// 这个函数可以设置阻塞, 也可以设置为非阻塞
// 这个函数可以指定回收哪些子进程的资源
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:
-
-1 : 回收所有的子进程资源,和wait() 是一样的,无差别回收,也需要循环回收;
-
大于0: 指定回收某一个进程的资源 , pid是需要回收的子进程的进程ID
-
0: 回收当前进程组的所有子进程ID
-
小于 -1: pid的绝对值代表进程组ID , 表示要回收组的所有子进程资源;
statue: NULL ,同wait()一致;
options: 控制函数是阻塞还是非阻塞
0:
函数行为是阻塞的;
WNOHANG:
函数是非阻塞的;
返回值:
- 如果函数是非阻塞的,并且子进程还在运行,返回 0
- 成功:得到子进程的进程 ID
- 失败: -1
- 没有子进程资源可以回收了,函数如果是阻塞的,阻塞会解除,直接返回 - 1
- 回收子进程资源的时候出现了异常
和wait() 行为一致,阻塞:
// 和wait() 行为一样, 阻塞
#include <sys/wait.h>
int main()
{
pid_t pid;
// 创建子进程
for(int i=0; i<5; ++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}
// 父进程
if(pid > 0)
{
// 需要保证父进程一直在运行
while(1)
{
// 回收子进程的资源
// 子进程由多个, 需要循环回收子进程资源
int status;
pid_t ret = waitpid(-1, &status, 0); // == wait(NULL);
if(ret > 0)
{
printf("成功回收了子进程资源, 子进程PID: %d\n", ret);
// 判断进程是不是正常退出
if(WIFEXITED(status))
{
printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status));
}
}
else
{
printf("回收失败, 或者是已经没有子进程了...\n");
break;
}
printf("我是父进程, pid=%d\n", getpid());
}
}
else if(pid == 0)
{
// 子进程, 执行这句代码之后, 子进程退出了
printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
}
return 0;
}
waitpid() 使用非阻塞回收多个子进程资源:
// 非阻塞处理
#include <sys/wait.h>
int main()
{
pid_t pid;
// 创建子进程
for(int i=0; i<5; ++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}
// 父进程
if(pid > 0)
{
// 需要保证父进程一直在运行
while(1)
{
// 回收子进程的资源
// 子进程由多个, 需要循环回收子进程资源
// 子进程退出了就回收,
// 没退出就不回收, 返回0
int status;
pid_t ret = waitpid(-1, &status, WNOHANG); // 非阻塞
if(ret > 0)
{
printf("成功回收了子进程资源, 子进程PID: %d\n", ret);
// 判断进程是不是正常退出
if(WIFEXITED(status))
{
printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status));
}
}
else if(ret == 0)
{
printf("子进程还没有退出, 不做任何处理...\n");
}
else
{
printf("回收失败, 或者是已经没有子进程了...\n");
break;
}
printf("我是父进程, pid=%d\n", getpid());
}
}
else if(pid == 0)
{
// 子进程, 执行这句代码之后, 子进程退出了
printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
}
return 0;
}