Linux进程详解

Linux进程详解

  • 1、进程概述
    • 1.1并行和并发
    • 1.2 PCB
    • 1.3 进程状态
    • 1.4 进程命令
  • 2、进程创建
    • 2.1 函数
    • 2.2 fork()解析
  • 3、父子进程
    • 3.1 进程执行位置
    • 3.2 循环创建子进程
    • 3.3 终端显示问题
    • 3.4 进程数数
  • 4、execl和execlp
    • 4.1 execl()
    • 4.2 execlp()
    • 4.3 函数的使用
  • 5、进程控制
    • 5.1 结束进程
    • 5.2 孤儿进程
    • 5.3 僵尸进程
    • 5.4 进程回收
      • 5.4.1 wait
      • 5.4.2 waitpid

原文链接

1、进程概述

从严格意义上来讲,程序和进程是两个不同的概念,他们的状态,占用的系统资源都是不同的。

  • 程序:就是磁盘上的可执行文件文件, 并且只占用磁盘上的空间,是一个静态的概念。

  • 进程:被执行之后的程序叫做进程,不占用磁盘空间,需要消耗系统的内存,CPU资源,每个运行的进程的都对应一个属于自己的虚拟地址空间,这是一个动态的概念。

1.1并行和并发

CPU在某个时间点只能处理一个任务。CPU会给每个进程被分配一个时间段,进程得到这个时间片之后才可以运行,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,CPU的使用权将被收回,该进程将会被中断挂起等待下一个时间片。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换,这样就可以避免CPU资源的浪费。

我们使用的计算机中启动的多个程序,从宏观上看是同时运行的,从微观上看由于CPU一次只能处理一个进程,所有它们是轮流执行的,只不过切换速度太快,我们感觉不到罢了,因此CPU的核数越多计算机的处理效率越高。
在这里插入图片描述

并发:第一幅图是并发。

  • 并发的同时运行是一个假象,咖啡机也好CPU也好在某一个时间点只能为某一个个体来服务,因此不可能同时处理多任务,这是通过上图的咖啡机/计算机的CPU快速的时间片切换实现的。
  • 并发是针对某一个硬件资源而言的,在某个时间段之内处理的任务的总量,量越大效率越高。

并行:第二幅图是并行。

  • 并行的多进程同时运行是真实存在的,可以在同一时刻同时运行多个进程
  • 并行需要依赖多个硬件资源,单个是无法实现的(图中有两台咖啡机)。

1.2 PCB

PCB - 进程控制块(Processing Control Block),Linux内核的进程控制块本质上是一个叫做 task_struct的结构体。在这个结构体中记录了进程运行相关的一些信息,下面介绍一些常用的信息:

  • 进程id:每一个进程都一个唯一的进程ID,类型为 pid_t, 本质是一个整形数

  • 进程的状态:进程有不同的状态, 状态是一直在变化的,有就绪、运行、挂起、停止等状态。

  • 进程对应的虚拟地址空间的信息。

  • 描述控制终端的信息,进程在哪个终端启动默认就和哪个终端绑定。

  • 当前工作目录:默认情况下, 启动进程的目录就是当前的工作目录

  • umask掩码:在创建新文件的时候,通过这个掩码屏蔽某些用于对文件的操作权限。

  • 文件描述符表:每个被分配的文件描述符都对应一个已经打开的磁盘文件

  • 和信号相关的信息:在Linux中 调用函数, 键盘快捷键, 执行shell命令等操作都会产生信号。

    • 阻塞信号集:记录当前进程中阻塞哪些已产生的信号,使其不能被处理
  • 未决信号集:记录在当前进程中产生的哪些信号还没有被处理掉。

  • 用户id和组id:当前进程属于哪个用户, 属于哪个用户组

  • 会话(Session)和进程组:多个进程的集合叫进程组,多个进程组的集合叫会话。

  • 进程可以使用的资源上限:可以使用shell命令ulimit -a查看详细信息。

1.3 进程状态

进程一共有五种状态分别为:创建态就绪态运行态阻塞态(挂起态)退出态(终止态)。其中创建态和退出态维持的时间是非常短的,稍纵即逝。我们主要是需要将就绪态, 运行态, 挂起态,三者之间的状态切换搞明白。
在这里插入图片描述

  • 就绪态: 万事俱备,只欠东风(CPU资源)
    • 进程被创建出来了,有运行的资格但是还没有运行,需要抢CPU时间片
    • 得到CPU时间片,进程开始运行,从就绪态转换为运行态。
    • 进程的CPU时间片用完了, 再次失去CPU, 从运行态转换为就绪态。
  • 运行态:获取到CPU资源的进程,进程只有在这种状态下才能运行
    • 运行态不会一直持续,进程的CPU时间片用完之后, 再次失去CPU,从运行态转换为就绪态
    • 只要进程还没有退出,就会在就绪态和运行态之间不停的切换。
  • 阻塞态:进程被强制放弃CPU,并且没有抢夺CPU时间片的资格
    • 比如: 在程序中调用了某些函数(比如: sleep()),进程又运行态转换为阻塞态(挂起态)
    • 当某些条件被满足了(比如:slee() 睡醒了),进程的阻塞状态也就被解除了,进程从阻塞态转换为就绪态。
  • 退出态: 进程被销毁, 占用的系统资源被释放了
    • 任何状态的进程都可以直接转换为退出态。

1.4 进程命令

查看进程

ps aux- a: 查看所有终端的信息- u: 查看用户相关的信息- x: 显示和终端无关的进程信息

杀死进程

kill -l  查看Linux的标准信号kill -9 进程id   

2、进程创建

2.1 函数

Linux中进程ID为 pid_t 类型,其本质是一个正整数,通过上边的ps aux命令已经得到了验证。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);

Linux中看似创建一个新的进程非常简单,函数连参数都没有,实际上如果想要真正理解这个函数还是得死几个脑细胞。

2.2 fork()解析

启动磁盘上的应用程序, 得到一个进程, 如果在这个启动的进程中调用**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;
}

3、父子进程

3.1 进程执行位置

在父进程中成功创建了子进程,子进程就拥有父进程代码区的所有代码,那么子进程中的代码是在什么位置开始运行的呢?父进程肯定是从main()函数开始运行的,子进程是在父进程中调用fork()函数之后被创建, 子进程就从fork()之后开始向下执行代码。
在这里插入图片描述

上图中演示了父子进程中代码的执行流程,可以看到如果在程序中对fork()的返回值做了判断,就可以控制父子进程的行为,如果没有做任何判断这个代码块父子进程都可以执行。在编写多进程程序的时候,一定要将代码想象成多份进行分析,因为直观上看代码就一份,但实际上数据都是多份,并且多份数据中变量名都相同,但是他们的值却不一定相同。

3.2 循环创建子进程

掌握了进程创建函数之后,实现一个简单的功能,在一个父进程中循环创建3个子进程,也就是最后需要得到4个进程,1个父进程,3个子进程,为了方便验证程序的正确性,要求在程序中打印出每个进程的进程ID。

// process_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{for(int i=0; i<3; ++i){pid_t pid = fork();printf("当前进程pid: %d\n", getpid());}return 0;
}# 编译
$ gcc process_loop.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

通过程序打印的信息发现程序循环了三次,最终得到了8个进程,也就是创建出了7个子进程,还是上面跟大家讲的那句话,对应多进程的程序,一定要代码分成很多份去分析,并且如果没有在程序中加条件控制,所有的代码父子进程都是有资格执行的。接下来分析上边的编写的代码,通过画图的方式分析为什么得到了7个子进程:
在这里插入图片描述

  • 上图中的树状结构,蓝色节点代表父进程:

    • 循环第一次 i = 0,创建出一个子进程,即红色节点,子进程变量值来自父进程拷贝,因此 i=0
    • 循环第二次 i = 1,蓝色父进程和红色子进程都去创建子进程,得到两个紫色进程,子进程变量值来自父进程拷贝,因此 i=1
    • 循环第三次 i = 2,蓝色父进程和红色、紫色子进程都去创建子进程,因此得到4个绿色子进程,子进程变量值来自父进程拷贝,因此 i=2
    • 循环第三次 i = 3,所有进程都不满足条件 for(int i=0; i<3; ++i)因此不进入循环,退出了。

通过上面的分析,最终得到解决方案,我们可以只让父进程创建子进程,如果是子进程不让其继续创建子进程,因此只需要在程序中添加关于父子进程的判断即可。

修改之后的代码如下:

// 需要在上边的程序中控制不让子进程, 再创建子进程即可
// process_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{pid_t pid;// 在循环中创建子进程for(int i=0; i<3; ++i){pid = fork();if(pid == 0){// 不让子进程执行循环, 直接跳出break;}}printf("当前进程pid: %d\n", getpid());return 0;
}# 编译
$ gcc process_loop.c# 执行
$ ./a.out
当前进程pid: 2727
当前进程pid: 2730
当前进程pid: 2729
当前进程pid: 2728

在多进程序中,进程的执行顺序是没有规律的,因为所有的进程都需要在就绪态争抢CPU时间片,抢到了就执行,抢不到就不执行,但是不用担心,默认进程的优先级是相同的,操作系统不会让某一个进程一直抢不到CPU时间片。

3.3 终端显示问题

在执行多进程程序的时候,经常会遇到下图中的问题,看似进程还没有执行完成,貌似是因为什么原因被阻塞了,实际上终端是正常的,当我们通过键盘输入一些命令,终端也能接受输入并且输出相关信息,那么为什么终端会显示成这个样子呢?
在这里插入图片描述

  • 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)
{// 子进程
}

3.4 进程数数

思考一个问题,当父进程创建一个子进程,那么父子进程之间可以通过全局变量互动,实现交替数数的功能吗?不过不确定可以写一段测试代码:

// number.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>// 定义全局变量
int number = 10;int main()
{printf("创建子进程之前 number = %d\n", number);pid_t pid = fork();// 父子进程都会执行这一行printf("当前进程fork()的返回值: %d\n", pid);//如果是父进程if(pid > 0){printf("我是父进程, pid = %d, number = %d\n", getpid(), ++number);printf("父进程的父进程(终端进程), pid = %d\n", getppid());sleep(1);}else if(pid == 0){// 子进程number += 100;printf("我是子进程, pid = %d, number = %d\n", getpid(), number);printf("子进程的父进程, pid = %d\n", getppid());}return 0;
}

编译程序并测试:

$ gcc number.c
$ ./a.out 
创建子进程之前 number = 10
当前进程fork()的返回值: 3513
当前进程fork()的返回值: 0
我是子进程, pid = 3513, number = 110
子进程的父进程, pid = 3512我是父进程, pid = 3512, number = 11	# 没有接着子进程的110继续数,父子进程各玩各的,测试失败了
父进程的父进程(终端进程), pid = 2175

通过验证得到结论:两个进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间,两个同名全局变量存储在不同的虚拟地址空间中,二者没有任何关联性。如果要进行进程间通信需要使用:管道,共享内存,本地套接字,内存映射区,消息队列等方式。

4、execl和execlp

在项目开发过程中,有时候有这种需求,需要通过现在运行的进程启动磁盘上的另一个可执行程序,也就是通过一个进程启动另一个进程,这种情况下我们可以使用 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个函数做了进一步的封装,下面介绍一下。

4.1 execl()

该函数可用于执行任意一个可执行程序,函数需要通过指定的文件路径才能找到这个可执行程序。

#include <unistd.h>
// 变参函数
int execl(const char *path, const char *arg, ...);
  • 参数:
    • path: 要启动的可执行程序的路径, 推荐使用绝对路径
    • arg: ps aux 查看进程的时候, 启动的进程的名字, 可以随意指定, 一般和要启动的可执行程序名相同
    • … : 要执行的命令需要的参数,可以写多个,最后以 NULL 结尾,表示参数指定完了。
  • 返回值:如果这个函数执行成功, 没有返回值,如果执行失败, 返回 -1

4.2 execlp()

该函数常用于执行已经设置了环境变量的可执行程序,函数中的 p就是path,也是说这个函数会自动搜索系统的环境变量PATH,因此使用这个函数执行可执行程序不需要指定路径,只需要指定出名字即可。

// p == path
int execlp(const char *file, const char *arg, ...);
  • 参数:
    • file: 可执行程序的名字
      • 在环境变量PATH中,可执行程序可以不加路径
      • 没有在环境变量PATH中, 可执行程序需要指定绝对路径
    • arg: ps aux 查看进程的时候, 启动的进程的名字, 可以随意指定, 一般和要启动的可执行程序名相同
    • … : 要执行的命令需要的参数,可以写多个,最后以 NULL 结尾,表示参数指定完了。
  • 返回值:如果这个函数执行成功, 没有返回值,如果执行失败, 返回 -1

4.3 函数的使用

关于exec族函数,我们一般不会在进程中直接调用,如果直接调用这个进程的代码区代码被替换也就不能按照原来的流程工作了。我们一般在调用这些函数的时候都会先创建一个子进程,在子进程中调用 exec 族函数,子进程的用户区数据被替换掉开始执行新的程序中的代码逻辑,但是父进程不受任何影响仍然可以继续正常工作。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main()
{// 创建子进程pid_t pid = fork();// 在子进程中执行磁盘上的可执行程序if(pid == 0){// 磁盘上的可执行程序 /bin/ps
#if 1execl("/bin/ps", "title", "aux", NULL);// 也可以这么写// execl("/bin/ps", "title", "a", "u", "x", NULL);  
#elseexeclp("ps", "title", "aux", NULL);// 也可以这么写// execl("ps", "title", "a", "u", "x", NULL);
#endif// 如果成功当前子进程的代码区别 ps中的代码区代码替换// 下面的所有代码都不会执行// 如果函数调用失败了,才会继续执行下面的代码perror("execl");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");printf("++++++++++++++++++++++++\n");}else if(pid > 0){printf("我是父进程.....\n");}return 0;
}

第二个参数相当于就是进程的名字

5、进程控制

进程控制主要是指进程的退出, 进程的回收和进程的特殊状态 孤儿进程和僵尸进程。

5.1 结束进程

如果想要直接退出某个进程可以在程序的任何位置调用exit()或者_exit()函数。函数的参数相当于退出码, 如果参数值为 0 程序退出之后的状态码就是0, 如果是100退出的状态码就是100。

// 专门退出进程的函数, 在任何位置调用都可以
// 标准C库函数
#include <stdlib.h>
void exit(int status);// Linux的系统函数
// 可以这么理解, 在linux中 exit() 函数 封装了 _exit()
#include <unistd.h>
void _exit(int status);

在 main 函数中直接使用 return也可以退出进程, 假如是在一个普通函数中调用 return 只能返回到调用者的位置,而不能退出进程。

// ***** return 必须要在main()函数中调用, 才能退出进程 *****
// 举例:
// 没有问题的例子
int main()
{return 0;	// 进程退出了
}// 不能退出的例子 //int func()
{return 666;	// 返回到调用者调用该函数的位置, 返回到 main() 函数的第19行
}int main()
{// 调用这个函数, 当前进程能不能退出? ===> 不能int ret = func();
}

5.2 孤儿进程

在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行这时候这个子进程就可以被称之为孤儿进程。

操作系统是非常关爱运行的每一个进程的,当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程(有干爹了)。如果使用Linux没有桌面终端,这个领养孤儿进程的进程就是 init 进程(PID=1),如果有桌面终端,这个领养孤儿进程就是桌面进程。

那么问题来了,系统为什么要领养这个孤儿进程呢?在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,这件事儿干爹就可以代劳了,这样可以避免系统资源的浪费。

下面这段代码就可以得到一个孤儿进程:

int main()
{// 创建子进程pid_t pid = fork();// 父进程if(pid > 0){printf("我是父进程, pid=%d\n", getpid());}else if(pid == 0){sleep(1);	// 强迫子进程睡眠1s, 这个期间, 父进程退出, 当前进程变成了孤儿进程// 子进程printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());}return 0;
}# 程序输出的结果
$ ./a.out 
我是父进程, pid=22459
我是子进程, pid=22460, 父进程ID: 1		# 父进程向退出, 子进程变成孤儿进程, 子进程被1号进程回收

5.3 僵尸进程

在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行, 子进程先于父进程结束, 子进程无法释放自己的PCB资源, 需要父进程来做这个件事儿, 但是如果父进程也不管, 这时候子进程就变成了僵尸进程。

僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。 僵尸进程就相当于是一副已经腐烂只剩下骨头的尸体。

僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。

运行下面的代码就可以得到一个僵尸进程了:

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;
}

在这里插入图片描述
在这里插入图片描述
defunct, 表示进程已经死亡

消灭僵尸进程的方法是,杀死这个僵尸进程的父进程,这样僵尸进程的资源就被系统回收了。通过kill -9 僵尸进程PID的方式是不能消灭僵尸进程的,这个命令只对活着的进程有效,僵尸进程已经死了,鞭尸是不能解决问题的。

5.4 进程回收

为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有两种,一种是阻塞方式wait(),一种是非阻塞方式waitpid()。

5.4.1 wait

这是个阻塞函数,如果没有子进程退出, 函数会一直阻塞等待, 当检测到子进程退出了, 该函数阻塞解除回收子进程资源。这个函数被调用一次, 只能回收一个子进程的资源,如果有多个子进程需要资源回收, 函数需要被调用多次。

// man 2 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
      • 回收子进程资源的时候出现了异常

下面代码演示了如何通过 wait()回收多个子进程资源:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 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){// 回收子进程的资源// 子进程由多个, 需要循环回收子进程资源pid_t ret = wait(NULL);if(ret > 0){printf("成功回收了子进程资源, 子进程PID: %d\n", ret);}else{printf("回收失败, 或者是已经没有子进程了...\n");break;}printf("我是父进程, pid=%d\n", getpid());}}else if(pid == 0){sleep(30);// 子进程, 执行这句代码之后, 子进程退出了printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());}return 0;
}

5.4.2 waitpid

waitpid() 函数可以看做是 wait() 函数的升级版,通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。

该函数函数原型如下:

// man 2 waitpid
#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,表示要回收这个进程组的所有子进程资源
    • status: NULL, 和wait的参数是一样的

    • options: 控制函数是阻塞还是非阻塞

      • 0: 函数是行为是阻塞的 ==> 和wait一样
      • WNOHANG: 函数是行为是非阻塞的
  • 返回值:

    • 如果函数是非阻塞的, 并且子进程还在运行, 返回0
    • 成功: 得到子进程的进程ID
    • 失败: -1
      • 没有子进程资源可以回收了, 函数如果是阻塞的, 阻塞会解除, 直接返回-1
      • 回收子进程资源的时候出现了异常

下面代码演示了如何通过 waitpid()阻塞回收多个子进程资源:

// 非阻塞处理
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
// 和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){   sleep(30);// 子进程, 执行这句代码之后, 子进程退出了printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());}return 0;
}

下面代码演示了如何通过 waitpid()非阻塞回收多个子进程资源:

// 非阻塞处理
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.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){// 回收子进程的资源// 子进程由多个, 需要循环回收子进程资源// 子进程退出了就回收, // 没退出就不回收, 返回0int 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){sleep(2);// 子进程, 执行这句代码之后, 子进程退出了printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());}return 0;
}

加个sleep会输出的更加明显。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/208067.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Oracle忘记所有密码怎么办

最近遇到一个Oracle的问题&#xff0c;密码要过期了&#xff0c;但是除了用户密码&#xff0c;其他密码都不知道了&#xff0c;修改不了密码怎么办呢&#xff1f; 试了各种方法&#xff0c;最终下面的方式生效了&#xff1a; 首先&#xff0c;使用orapwd生成新的密码文件&…

selenium 工具 的基本使用

公司每天要做工作汇报&#xff0c;汇报使用的网页版&#xff0c; 所以又想起 selenium 这个老朋友了。 再次上手&#xff0c;发现很多接口都变了&#xff0c; 怎么说呢&#xff0c; 应该是易用性更强了&#xff0c; 不过还是得重新看看&#xff0c; 我这里是python3。 pip安装…

有文件实体的后门无文件实体的后门rootkit后门

有文件实体后门和无文件实体后门&RootKit后门 什么是有文件的实体后门&#xff1a; 在传统的webshell当中&#xff0c;后门代码都是可以精确定位到某一个文件上去的&#xff0c;你可以rm删除它&#xff0c;可以鼠标右键操作它&#xff0c;它是有一个文件实体对象存在的。…

熬夜会秃头——beta冲刺Day4

这个作业属于哪个课程2301-计算机学院-软件工程社区-CSDN社区云这个作业要求在哪里团队作业—beta冲刺事后诸葛亮-CSDN社区这个作业的目标记录beta冲刺Day4团队名称熬夜会秃头团队置顶集合随笔链接熬夜会秃头——Beta冲刺置顶随笔-CSDN社区 一、团队成员会议总结 1、成员工作进…

(详细教程)笔记本电脑安装Ubuntu系统

1.前言 老的小米笔记本淘汰了&#xff0c;装一下linux系统玩一下。 使用工具如下&#xff1a;一台小米笔记本pro15.6一个惠普32G U盘一个台式机用于下载镜像等资源 2.下载Ubuntu桌面版 cn.ubuntu.com/download/de… 这里我下载的是 22.04.3 LTS 3.下载烧录工具&#xff0c…

Lattice-Based Blind Signatures: Short, Efficient, and Round-Optimal

目录 摘要引言 Lattice-Based Blind Signatures: Short, Efficient, and Round-Optimal CCS 2023 摘要 我们提出了一种基于随机预言机启发式和标准格问题&#xff08;环/模块SIS/LWE和NTRU&#xff09;的2轮盲签名协议&#xff0c;签名大小为22KB。该协议是全面优化的&#xf…

如何做接口测试?接口测试工具有哪些?

回想入职测试已经10年时间了&#xff0c;初入职场的我对于接口测试茫然不知。后来因为业务需要&#xff0c;开始慢慢接触接口测试。从最开始使用工具进行接口测试到编写代码实现接口自动化&#xff0c;到最后的测试平台开发。回想这一路走来感触颇深&#xff0c;因此为了避免打…

分享82个节日PPT,总有一款适合您

分享82个节日PPT&#xff0c;总有一款适合您 82个节日PPT下载链接&#xff1a;https://pan.baidu.com/s/1boDTl3PiHFXLJ890CoUfJA?pwd8888 提取码&#xff1a;8888 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;收集整理更不易。…

MathType 7.5.2中文版软件使用期到了怎么办?

MathType 7.5.2中文版作为一款专业的公式编辑器&#xff0c;MathType受到很多人的青睐&#xff0c;它可以将编辑好的公式保存成多种图片格式或透明图片模式&#xff0c;可以很方便的添加或移除符号、表达式等模板&#xff08;只需要简单地用鼠标拖进拖出即可)&#xff0c;也可以…

39.从0到上线三天搭建个人网站(第三天)

点赞收藏加关注&#xff0c;你也能住大别墅&#xff01; 一、第三天主要工作 1.完成detail页面的开发 2.将所有数据以及部分静态资源存在uniCloud&#xff0c;为以后做管理后台做准备 3.创建云对象getData&#xff0c;在beforecreate&#xff08;&#xff09;中获取数据 4.…

详解原生Spring框架下的类切入点表达式与切入点函数

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

126. 单词接龙 II

126. 单词接龙 II 需要注意的是&#xff0c;由于要找最短路径&#xff0c;连接 dot 与 lot 之间的边就不可以被记录下来&#xff0c;同理连接 dog 与 log 之间的边也不可以被记录。这是因为经过它们的边一定不会是最短路径。因此在广度优先遍历的时候&#xff0c;需要记录的图…

分享88个节日PPT,总有一款适合您

分享88个节日PPT&#xff0c;总有一款适合您 88个节日PPT下载链接&#xff1a;https://pan.baidu.com/s/1mfLrdlB9Y1jqz2vkVIwBNA?pwd6666 提取码&#xff1a;6666 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;收集整理更不易…

Linux部分基础指令讲解

目录 1.echo指令 2.more指令 3.less指令&#xff08;重要&#xff09; 4.head指令 5.tail指令 6.管道| 7.时间相关的指令 8.cal指令 9.find指令 10.grep指令 1.echo指令 我们先看效果 如图所示我们可以看到显示器显示出了hellow world和hellow这两句话&#xff0c;我们的echo的…

超分辨率重建

意义 客观世界的场景含有丰富多彩的信息&#xff0c;但是由于受到硬件设备的成像条件和成像方式的限制&#xff0c;难以获得原始场景中的所有信息。而且&#xff0c;硬件设备分辨率的限制会不可避免地使图像丢失某些高频细节信息。在当今信息迅猛发展的时代&#xff0c;在卫星…

【EI会议征稿】第四届生物信息学与智能计算国际学术研讨会(BIC 2024)

第四届生物信息学与智能计算国际学术研讨会&#xff08;BIC 2024&#xff09; 2024 4th International Conference on Bioinformatics and Intelligent Computing 2024年第四届生物信息学与智能计算国际学术研讨会 &#xff08;BIC 2024&#xff09;将定于2024年1月26-28日在…

Golang数据类型(数字型)

Go数据类型&#xff08;数字型&#xff09; Go中数字型数据类型大致分为整数&#xff08;integer&#xff09;、浮点数&#xff08;floating point &#xff09;和复数&#xff08;Complex&#xff09;三种 整数重要概念 整数在Go和Python中有较大区别&#xff0c;主要体现在…

C++的explicit和隐式转换

隐式转换是指在某些情况下&#xff0c;编译器会自动进行类型转换&#xff0c;将一种类型的值转换为另一种类型&#xff0c;以满足表达式的要求。这种转换是隐式进行的&#xff0c;不需要显式地调用转换函数或构造函数。 int a 5; double b a; // int 到 double 的隐式转换上…

利用 FormData 实现文件上传、监控网路速度和上传进度

利用 FormData 实现文件上传 基础功能&#xff1a;上传文件 演示如下&#xff1a; 概括流程&#xff1a; 前端&#xff1a;把文件数据获取并 append 到 FormData 对象中后端&#xff1a;通过 ctx.request.files 对象拿到二进制数据&#xff0c;获得 node 暂存的文件路径 前端…

【c++|SDL】开始使用之---demo

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 SDL 记录 1. hello word #include<SDL2/SDL.h>SDL_Window* g_pWindow 0; SDL_Renderer* g_pRenderer 0;int main(int argc, char* args[]) {//…