LV05-01-进程-02-进程的创建与回收

本文主要是进程的创建与回收的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、进程诞生与结束

1. 创建子进程

1.1 fork()

1.1.1 函数说明

在 linux 下可以使用 man fork 命令查看该函数的帮助手册。

1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

【函数说明】该函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

【函数参数】 none

【返回值】返回值为 pid_t 类型,该函数被调用一次,能够返回两次,它可能有三种不同的返回值:

  • 在父进程中, fork 返回新创建子进程的进程 ID 。
  • 在子进程中, fork 返回 0 。
  • 如果出现错误, fork 返回一个负值(一般应该是 -1 ),并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <sys/types.h>
#include <unistd.h>

/* 至少应该有的语句 */
pid_t pid;
pid = fork();

【注意事项】

(1)完成 fork 函数调用后将存在两个进程,一个是原进程(父进程)、另一个则是创建出来的子进程,并且每个进程都会从 fork() 函数的返回处继续执行,会导致调用 fork() 返回两次值,子进程返回一个值、父进程返回一个值。

(2)一般使用场景:

  • 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。
  • 一个进程要执行不同的程序。这种情况,通常在子进程从 fork() 函数返回之后立即调用 exec 族函数来实现。

1.1.2 使用实例1

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
pid_t pid;
printf("before father fork!\n");
/* 父进程创建子进程 */
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
printf("pid=%d\n",(int)pid);
printf("after father fork!\n");

return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
before father fork!
pid=9343
after father fork!
pid=0
after father fork!

可以看到, fork 后边的程序执行了两次,并且函数也返回了两个值。

1.1.2 使用实例2

fork() 调用成功后,将会在父进程中返回子进程的 PID ,而在子进程中返回值是 0 ;如果调用失败,父进程返回值 -1 ,不创建子进程,并设置 errno ,这样我们其实可以进行判断,让让两个进程执行不同的内容。

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>

#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
pid_t pid;
printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("father process printf<pid:%d,child pid:%d>\n", getpid(), pid);
while(1)
{
sleep(1);
}
}
else if(pid == 0)/* 子进程 */
{
printf("child process printf<pid:%d,father pid:%d>\n", getpid(), getppid());
while(1)
{
sleep(1);
}
}
else if(pid < 0)
{
perror("fork");
return 0;
}

return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
before father fork!
-----------------
father process printf<pid:9707,child pid:9708>
child process printf<pid:9708,father pid:9707>

由于设置了进程休眠,所以我们可以重新开启一个终端,输入以下命令查看两个进程的 PID :

1
ps -elf|grep a.out

该命令执行完毕后将显示以下信息:

1
2
3
0 S hk          9707    9425  0  80   0 -   628 hrtime 10:03 pts/0    00:00:00 ./a.out
1 S hk 9708 9707 0 80 0 - 628 hrtime 10:03 pts/0 00:00:00 ./a.out
0 S hk 9802 9782 0 80 0 - 4446 pipe_r 10:12 pts/1 00:00:00 grep --color=auto a.out

第四、五列表示进程的 PID 和 PPID 。

1.2 父子进程文件共享

由于后文需要,这部分笔记就写这里啦。调用 fork() 函数之后,子进程会获得父进程所有文件描述符的副本。

image-20220522144142850

子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实现了共享,对于缓冲区也是一样(后边有例子说明)。例如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。

1.2.1 缓冲区共享实例

点击查看实例1——缓冲区的共享
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
printf("Hello World!");
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (fork())
{
case -1:
perror("fork error");
return -1;
case 0: /* 子进程 */
fflush(stdout);
sleep(1);
default: /* 父进程 */
fflush(stdout);
sleep(1);
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
Hello World!Hello World!

其实我们可以刷新缓冲区的函数去掉,这个时候会发现什么都不会弄输出,当父子进程都使用了刷新缓冲区的函数后,原来没有打印的数据将会打印两次,这也说明了子进程继承了父进程的缓冲区。

1.2.2 文件共享实例1

点击查看实例1——文件共享(创建进程前打开文件)
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>

#include <sys/types.h>
#include <unistd.h>

#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
pid_t pid;
int fd;
int i;

fd = open("./test.txt", O_RDWR | O_TRUNC);
if (fd < 0)
{
perror("open error");
return -1;
}
pid = fork();
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (pid)
{
case -1:
perror("fork error");
return -1;
case 0: /* 子进程 */
for (i = 0; i < 4; i++) /*循环写入4次 */
write(fd, "1234", 4);
close(fd);
default: /* 父进程 */
for (i = 0; i < 4; i++) /*循环写入4次 */
write(fd, "ABCD", 4);
close(fd);
}
return 0;
}

在终端执行以下命令:

1
2
3
# 注意要先创建测试文件
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,然后在终端执行 cat test.txt ,则会看到终端有如下信息输出:

1
ABCDABCD1234ABCDABCD123412341234

上述结果可知,此种情况下,父、子进程分别对同一个文件进行写入操作,结果是接续写,不管是父进程,还是子进程,在每次写入时都是从文件的末尾写入,很像使用了 O_APPEND 标志的效果。其原因是,子进程继承了父进程的文件描述符,两个文件描述符都指向了一个相同的文件表,意味着它们的文件偏移量是同一个,相互影响,子进程改变了文件的位置偏移量就会作用到父进程,同理,父进程改变了文件的位置偏移量就会作用到子进程。

1.2.3 文件共享实例2

点击查看实例2——文件共享(父子进程中打开文件)
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>

#include <sys/types.h>
#include <unistd.h>

#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
pid_t pid;
int fd;
int i;

pid = fork();
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (pid)
{
case -1:
perror("fork error");
return -1;
case 0: /* 子进程 */
fd = open("./test.txt", O_WRONLY);
if (fd < 0)
{
perror("open error");
return -1;
}
for (i = 0; i < 4; i++) /*循环写入4次 */
write(fd, "1234", 4);
close(fd);
default: /* 父进程 */
fd = open("./test.txt", O_WRONLY);
if (fd < 0)
{
perror("open error");
return -1;
}
for (i = 0; i < 2; i++) /*循环写入4次 */
write(fd, "ABCD", 4);
close(fd);
}
return 0;
}

在终端执行以下命令:

1
2
3
# 注意要先创建测试文件
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,然后在终端执行 cat test.txt ,则会看到终端有如下信息输出:

1
ABCDABCD12341234

上述结果可知,此种情况下,两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。导致了最终的结果与预想的并不一致。

1.3 vfork()

在 linux 下可以使用 man vfork 命令查看该函数的帮助手册。

1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

【函数说明】该函数通过系统调用创建一个与原来进程几乎完全相同的进程。

【函数参数】 none

【返回值】返回值为 pid_t 类型,该函数被调用一次,能够返回两次,它可能有三种不同的返回值:

  • 在父进程中, vfork 返回新创建子进程的进程 ID 。
  • 在子进程中, vfork 返回 0 。
  • 如果出现错误, vfork 返回一个负值(一般应该是 -1 ),并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <sys/types.h>
#include <unistd.h>

/* 至少应该有的语句 */
pid_t pid;
pid = vfork();

【注意事项】

1.4 fork与vfork的区别

fork 和 vfork 都可以创建子进程且功能一致,那有什么区别呢?

(1) vfork() 与 fork() 一样都创建了子进程,但 vfork() 函数并不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec (或 _exit ),于是也就不会引用该地址空间的数据。不过在子进程调用 exec 或 _exit 之前,它在父进程的空间中运行、子进程共享父进程的内存。这种优化工作方式的实现提高的效率;但如果子进程修改了父进程的数据(除了 vfork 返回值的变量)、进行了函数调用、或者没有调用 exec 或 _exit 就返回将可能带来未知的结果。

(2) vfork() 保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行。

虽然 vfork() 系统调用在效率上要优于 fork() ,但是 vfork() 可能会导致一些难以察觉的程序 bug ,所以尽量避免使用 vfork() 来创建子进程,虽然 fork() 在效率上并没有 vfork() 高,但是现代的 Linux 系统内核已经采用了写时复制技术来实现 fork() ,其效率较之于早期的 fork() 实现要高出许多,除非速度绝对重要的场合,我们的程序当中应舍弃 vfork() 而使用 fork() 。

2. 进程的诞生

上边我么已经知道了如何创建一个子进程,并使用子进程执行不同的程序。 Linux 系统下的所有进程都是由其父进程创建而来,例如 shell 终端通过命令的方式执行一个程序 ./file_name ,那么 file_name 进程就是由 shell 终端进程创建出来的, shell 终端就是该进程的父进程。

在 Ubuntu 系统下使用 ps -aux 命令可以查看到系统下所有进程信息,如下:

1
2
3
4
5
6
7
8
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root 1 0.0 0.3 165856 12436 ? Ss 5月21 0:04 /sbin/init splash
root 2 0.0 0.0 0 0 ? S 5月21 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 5月21 0:00 [rcu_gp]
root 4 0.0 0.0 0 0 ? I< 5月21 0:00 [rcu_par_gp]
root 6 0.0 0.0 0 0 ? I< 5月21 0:00 [kworker/0:0H-events_highpri]

# 后边的全部省略 ... ...

上边信息中进程号为 1 的进程便是所有进程的父进程,通常称为 init 进程,它是 Linux 系统启动之后运行的第一个进程,它管理着系统上所有其它进程, init 进程是由内核启动,理论上说它没有父进程。

3. 进程的结束

那一个进程是怎样结束的呢?其实前边有说过,进程结束一般来说有两种终止方式,正常终止和异常终止。

进程的正常终止有多种不同的方式,例如在 main 函数中使用 return 返回、调用 exit() 函数结束进程、调用 _exit() 或 _Exit() 函数结束进程等。异常终止通常也有多种不同的方式,例如在程序当中调用 abort() 函数异常终止进程、当进程接收到某些信号导致异常终止等。下边就来了解一下正常终止的几个函数吧( return 语句用的很多,这里就不做说明了,但是要注意的是若是 return 在函数中使用,则会使函数结束,若是在主程序中使用,则会直接结束整个程序)。

3.1 _exit()

3.1.1 函数说明

在 linux 下可以使用 man _exit 命令查看该函数的帮助手册。

1
2
#include <unistd.h>
void _exit(int status);

【函数说明】该函数会立刻结束当前进程或者当前程序,清除其使用的内存空间,并销毁其在内核中的各种数据结构。

【函数参数】

  • status : int 类型,表示返回给父进程的状态值,一般来说, 0 表示正常结束;其他的数值表示出现了错误,进程非正常结束。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
_exit(n);

【注意事项】

(1) _exit 函数不会刷新 I/O 缓冲区(下边的程序可以说明这个问题)。

(2)此函数调用后不会返回, 并且会传递 SIGCHLD 信号给父进程, 父进程可以由 wait 函数取得子进程结束状态。

(3)此函数为系统调用。

3.1.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
printf("Hello World!");
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (fork())
{
case -1:
perror("fork error");
_exit(-1);
case 0: /* 子进程 */
_exit(0);
while(1) sleep(1);
default: /* 父进程 */
_exit(0);
while(1) sleep(1);
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会什么也不显示,为什么呢?其实前边我们知道, printf 函数是将数据输出到标准输出,一般指我们的屏幕,而要输出的数据先会被放入缓冲区,只有当后边遇到有 \n 或者其他刷新缓冲区的时候才会显示出来,而上边的程序,一条也不满足,所以不会有任何的输出,但是程序却是正常结束了,说明父子进程都已经退出了。

3.2 _Exit()

在 linux 下可以使用 man 3 _Exit 命令查看该函数的帮助手册。

1
2
#include <stdlib.h>
void _Exit(int status);

【函数说明】该函数会立刻结束当前进程或者当前程序,清除其使用的内存空间,并销毁其在内核中的各种数据结构(与 、_exit 等价)。

【函数参数】

  • status : int 类型,表示返回给父进程的状态值,一般来说, 0 表示正常结束;其他的数值表示出现了错误,进程非正常结束。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <stdlib.h>

/* 至少应该有的语句 */
_Exit(n);

【注意事项】

(1) _Exit 函数不会刷新 I/O 缓冲区(下边的程序可以说明这个问题)。

(2)此函数调用后不会返回, 并且会传递 SIGCHLD 信号给父进程, 父进程可以由 wait 函数取得子进程结束状态。

(3)此函数为系统调用。

3.3 exit()

3.3.1 函数说明

在 linux 下可以使用 man 3 exit 命令查看该函数的帮助手册。

1
2
#include <stdlib.h>
void exit(int status);

【函数说明】该函数会结束当前进程或者当前程序,在整个程序中,只要调用 exit ,就结束。

【函数参数】

  • status : int 类型,表示返回给父进程的状态值,一般来说, 0 表示正常结束;其他的数值表示出现了错误,进程非正常结束。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <stdlib.h>

/* 至少应该有的语句 */
exit(n);

【注意事项】

(1) exit 函数会执行的动作如下:

  • 如果程序中通过 atexit 注册了进程终止处理函数,那么会调用终止处理函数。
  • 刷新 stdio 流缓冲区。
  • 执行 _exit() 系统调用。

(2) exit() 函数与 、_exit() 函数最大的区别就在于 exit() 函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是清理 I/O 缓冲。

3.3.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
printf("Hello World!");
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (fork())
{
case -1:
perror("fork error");
_exit(-1);
case 0: /* 子进程 */
exit(0);
while(1) sleep(1);
default: /* 父进程 */
exit(0);
while(1) sleep(1);
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
Hello World!Hello World!

为什么会显示两次呢?,这是因为父进程创建子进程的时候,子进程也会继承父进程的缓冲区,而 exit 函数退出时会刷新缓冲区,于是就显示了两次了。

3.4 结束顺序问题

当我们 fork() 之后,产生了父子进程,结束的时候会有以下情况:

  • 若父进程先结束

(1)子进程成为孤儿进程,被 init 进程收养;

(2)子进程变成后台进程;

  • 若子进程先结束

(1)父进程进行回收,则正常结束;

(2)父进程未及时回收,则子进程变为僵尸进程;

【注意】需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait() 将其回收,避免僵尸进程。

3.4.1 父进程先结束

父进程先结束的时候,子进程按理说会变成孤儿进程,然后被 init 进程收养,其父进程的 PID 应该为 1 ,接下来我们来看一看是否是这样呢?

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
pid_t pid;

pid = fork();
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0: /* 子进程 */
printf("This is child process!<self PID:%d, father PID:%d>\n", getpid(), getppid());
while(1)
{
sleep(1);
}
default: /* 父进程 */
printf("This is father process!<self PID:%d, father PID:%d>\n", getpid(), getppid());
exit(0);
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
This is father process!<self PID:11861, father PID:9425>
This is child process!<self PID:11862, father PID:11861>

可以发现父子进程均执行了,然后程序退出了,可是我们在终端中执行以下命令:

1
ps -elf|grep a.out

不出意外的话,然后会看到以下信息:

1
2
1 S hk         11862     935  0  80   0 -   628 hrtime 16:20 pts/0    00:00:00 ./a.out
0 S hk 11867 9425 0 80 0 - 4446 pipe_r 16:21 pts/0 00:00:00 grep --color=auto a.out

这个 a.out 进程依然存在,父进程变为 935 ,并非是 1 。

上边例程让我们发现收养子进程的并非是 1 ,这是怎么回事?我们来看一看这个 935 是什么进程:

1
ps -elf|grep 935

会得到以下信息:

1
2
3
4
5
4 S hk           935       1  0  80   0 -  5402 ep_pol 5月21 ?       00:00:02 /lib/systemd/systemd --user
5 S hk 951 935 0 80 0 - 25558 - 5月21 ? 00:00:00 (sd-pam)
0 S hk 1073 935 0 80 0 - 22669 ep_pol 5月21 ? 00:00:00 /usr/bin/pipewire
0 S hk 1076 935 0 80 0 - 20706 ep_pol 5月21 ? 00:00:00 /usr/bin/pipewire-media-
# 下边的省略 ......

我们发现,这个 935 进程的父进程是 1 ,而又有一大堆以 935 为父进程的子进程,查阅资料知道,我们使用的是带图形界面的 Ubuntu ,它的终端 Shell 是 init 的一个子进程,终端中的孤儿进程会被 Shell 收养了。当我们切换到字符界面的时候重新运行以上的命令时就会发现收养 a.out 进程的为 init 了。

  • 字符化界面控制终端和图形界面伪终端切换
1
2
Crtl+Alt+F3或者Ctrl+Fn+Alt+F3  # 图形界面伪终端切换到字符化界面控制终端
Ctrl+Alt+F2或者Ctrl+Fn+Alt+F2 # 字符化界面控制终端切换到图形界面伪终端

3.4.2 子进程先结束

子进程先结束的时候,若父进程未回收,那么就会变成僵尸进程。

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
pid_t pid;

pid = fork();
/* fork()创建子进程时会复制缓冲区,所以上边会打印两遍 */
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0: /* 子进程 */
printf("This is child process!<self PID:%d, father PID:%d>\n", getpid(), getppid());
while(1)
{
sleep(1);
}
default: /* 父进程 */
printf("This is father process!<self PID:%d, father PID:%d>\n", getpid(), getppid());
exit(0);
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
This is father process!<self PID:12013, father PID:9425>
This is child process!<self PID:12014, father PID:12013>

可以发现父子进程均执行了,然后程序并未退出了,我们重新开一个终端,在终端中执行以下命令:

1
ps -elf|grep a.out

不出意外的话,然后会看到以下信息:

1
2
3
0 S hk         12013    9425  0  80   0 -   628 hrtime 16:32 pts/0    00:00:00 ./a.out
1 Z hk 12014 12013 0 80 0 - 0 - 16:32 pts/0 00:00:00 [a.out] <defunct>
0 S hk 12021 11992 0 80 0 - 4446 pipe_r 16:33 pts/1 00:00:00 grep --color=auto a.out

子进程的状态变为 Z ,标识该进程为一个僵尸进程。

4. 竞争条件

调用 fork() 之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行,这里出现了一个问题,调用 fork() 之后,无法确定父、子两个进程谁将率先访问 CPU ,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU ),这将导致谁先运行、谁后运行这个顺序是不确定的

那要是需要控制执行顺序呢?这个时候可以通过采用采用某种同步技术来实现,比如信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它,这在后边学习进程通信的时候会进行详细学习。

二、多个子进程的创建

1. 情况一

第一种情况就是父进程创建子进程,然后父进程和子进程又创建各自的子进程,可以看一个实例。

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
pid_t pid;
int i;
printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
for(i = 0; i < 3; i++)
{
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("[i=%d]:father process <pid:%d,child pid:%d,father return:%d>\n", i, getpid(), pid, pid);
sleep(1);
}
else if(pid == 0)/* 子进程 */
{
printf("[i=%d]:child process<pid:%d,father pid:%d,child return:%d>\n", i, getpid(), getppid(), pid);
sleep(1);
}
else if(pid < 0)
{
perror("fork");
exit(-1);
}
}
sleep(50);
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[i=0]:father process <pid:13499,child  pid:13500,father return:13500>
[i=0]:child process<pid:13500,father pid:13499,child return:0>
[i=1]:father process <pid:13499,child pid:13501,father return:13501>
[i=1]:father process <pid:13500,child pid:13502,father return:13502>
[i=1]:child process<pid:13501,father pid:13499,child return:0>
[i=1]:child process<pid:13502,father pid:13500,child return:0>
[i=2]:father process <pid:13499,child pid:13503,father return:13503>
[i=2]:father process <pid:13501,child pid:13504,father return:13504>
[i=2]:child process<pid:13503,father pid:13499,child return:0>
[i=2]:father process <pid:13500,child pid:13505,father return:13505>
[i=2]:child process<pid:13504,father pid:13501,child return:0>
[i=2]:father process <pid:13502,child pid:13506,father return:13506>
[i=2]:child process<pid:13505,father pid:13500,child return:0>
[i=2]:child process<pid:13506,father pid:13502,child return:0>

可以发现父子进程均执行了,在终端中执行以下命令查看进程创建情况:

1
2
pstree|grep a.out   # 以树的形式显示进程 a.out 及其子进程情况
ps -axjf|grep a.out # 以树的形式显示进程 a.out 及其子进程情况

个人感觉 pstree 的显示方式更好看,不出意外的话,然后会看到以下信息:

1
2
3
4
|         |-gnome-terminal--+-bash---a.out-+-a.out-+-a.out---a.out
| | | | -a.out
| | | |-a.out---a.out
| | | -a.out

通过实例,我们知道直接将进程的创建放入循环,会创建出这样的父子进程关系:

1
2
3
4
5
# pstree -p|grep a.out 可得出带有进程号的树状图(我只保留了进程相关信息)
a.out(13499)-+-a.out(13500)-+-a.out(13502)---a.out(13506)
| -a.out(13505)
|-a.out(13501)---a.out(13504)
-a.out(13503)

为什么会这样呢?我们来分析一下,每次父进程创建子进程后,子进程在下一次循环时也会继续创建子进程,原来的父进程也会创建子进程,于是出现了很多的进程,如下图所示:

1

2. 情况二

我想要实现这样的效果,一个父进程,下边四个子进程,这四个子进程是兄弟关系,如下图:

image-20220522195535797

这样的话,怎么实现呢?可以想象一下,其实我们上边创建的时候子进程也进入了循环内,就会执行 fork ,那创建完子进程后,这个子进程若是退出循环的话,他不就不会创建子进程了,只有最开始的父进程一直创建子进程。

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
pid_t pid;
int i;
printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
for(i = 0; i < 4; i++)
{
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("[i=%d]:father process <pid:%d,child pid:%d,father return:%d>\n", i, getpid(), pid, pid);
sleep(1);
}
else if(pid == 0)/* 子进程 */
{
printf("[i=%d]:child process<pid:%d,father pid:%d,child return:%d>\n", i, getpid(), getppid(), pid);
sleep(1);
break;
}
else if(pid < 0)
{
perror("fork");
exit(-1);
}
}
sleep(50);
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
10
before father fork!
-----------------
[i=0]:father process <pid:13789,child pid:13790,father return:13790>
[i=0]:child process<pid:13790,father pid:13789,child return:0>
[i=1]:father process <pid:13789,child pid:13791,father return:13791>
[i=1]:child process<pid:13791,father pid:13789,child return:0>
[i=2]:father process <pid:13789,child pid:13792,father return:13792>
[i=2]:child process<pid:13792,father pid:13789,child return:0>
[i=3]:father process <pid:13789,child pid:13793,father return:13793>
[i=3]:child process<pid:13793,father pid:13789,child return:0>

可以发现父子进程均执行了,在终端中执行以下命令查看进程创建情况:

1
2
pstree|grep a.out   # 以树的形式显示进程 a.out 及其子进程情况
ps -axjf|grep a.out # 以树的形式显示进程 a.out 及其子进程情况

个人感觉 pstree 的显示方式更好看,不出意外的话,然后会看到以下信息:

1
|         |-gnome-terminal--+-bash---a.out---4*[a.out]

通过实例,我们知道直接将进程的创建放入循环,会创建出这样的父子进程关系:

1
2
3
4
5
# pstree -p|grep a.out 可得出带有进程号的树状图(我只保留了进程相关信息)
a.out(13789)-+-a.out(13790)
|-a.out(13791)
|-a.out(13792)
-a.out(13793)

3. 情况三

我想要实现这样的效果, A→B→C→D→E→F ,如下图:

image-20220522200626150

这样的话,怎么实现呢?跟上边一样的思路,我们让父进程创建子进程,然后父进程跳出循环,下一次循环的时候就只有子进程创建子进程了。

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
pid_t pid;
int i;
printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
for(i = 0; i < 4; i++)
{
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("[i=%d]:father process <pid:%d,child pid:%d,father return:%d>\n", i, getpid(), pid, pid);
sleep(1);
break;
}
else if(pid == 0)/* 子进程 */
{
printf("[i=%d]:child process<pid:%d,father pid:%d,child return:%d>\n", i, getpid(), getppid(), pid);
sleep(1);
}
else if(pid < 0)
{
perror("fork");
exit(-1);
}
}
sleep(50);
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
10
before father fork!
-----------------
[i=0]:father process <pid:13997,child pid:13998,father return:13998>
[i=0]:child process<pid:13998,father pid:13997,child return:0>
[i=1]:father process <pid:13998,child pid:13999,father return:13999>
[i=1]:child process<pid:13999,father pid:13998,child return:0>
[i=2]:father process <pid:13999,child pid:14000,father return:14000>
[i=2]:child process<pid:14000,father pid:13999,child return:0>
[i=3]:father process <pid:14000,child pid:14001,father return:14001>
[i=3]:child process<pid:14001,father pid:14000,child return:0>

可以发现父子进程均执行了,在终端中执行以下命令查看进程创建情况:

1
2
pstree|grep a.out   # 以树的形式显示进程 a.out 及其子进程情况
ps -axjf|grep a.out # 以树的形式显示进程 a.out 及其子进程情况

个人感觉 pstree 的显示方式更好看,不出意外的话,然后会看到以下信息:

1
|         |-gnome-terminal--+-bash---a.out---a.out---a.out---a.out---a.out

通过实例,我们知道直接将进程的创建放入循环,会创建出这样的父子进程关系:

1
2
# pstree -p|grep a.out 可得出带有进程号的树状图(我只保留了进程相关信息)
a.out(13997)---a.out(13998)---a.out(13999)---a.out(14000)---a.out(14001)

三、进程回收

上边我们了解到,子进程先结束的时候会变成僵尸进程,虽然进程已经结束吗,但是由于没有回收,还是占用着一些资源,以僵尸进程的状态存在。我们这个时候就需要给子进程“收尸”。

1. wait()

1.1 函数说明

在 linux 下可以使用 man 3 wait 命令查看该函数的帮助手册。

1
2
3
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);

【函数说明】该函数以等待进程的任一子进程终止,同时获取子进程的终止状态信息并回收子进程的残留资源。

【函数参数】

  • stat_loc : int * 类型,参数 stat_loc 用于存放子进程终止时的状态信息(包括子进程返回值和结束方式的地址),可以为 NULL ,表示直接释放子进程 PCB ,不接收子进程终止时的状态信息。
点击查看子进程终止的状态信息判别

参数 stat_loc 不为 NULL 的情况下, wait() 会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 stat_loc 参数。以下只列举几个,更为详细的还是需要查看手册。

WIFEXITED(stat_loc)如果子进程正常终止,则返回true;
WEXITSTATUS(stat_loc)返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或exit()时指定的退出状态;wait()获取得到的status参数并不是调用_exit()或exit()时指定的状态,可通过WEXITSTATUS宏转换;
WIFSIGNALED(stat_loc)如果子进程被信号终止,则返回true;
WTERMSIG(stat_loc)返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
WCOREDUMP(stat_loc)如果子进程终止时产生了核心转储文件,则返回true;

【返回值】返回值为 pid_t 类型,成功时返回回收的子进程的进程号( PID );失败时返回 EOF 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <sys/types.h>
#include <sys/wait.h>

/* 至少应该有的语句 */
int status;
wait(&status);
/* 或者 */
wait(NULL);

【注意事项】

(1)若子进程没有结束,父进程一直阻塞(暂时停止目前进程的执行, 直到有信号来到或子进程结束)。

(2) 若有多个子进程,哪个先结束就先回收。

1.2 调用wait() 后发生了什么?

  • 调用 wait() 函数,如果其所有子进程都还在运行,则 wait() 会一直阻塞等待,直到某一个子进程终止;
  • 如果进程调用 wait() ,但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait() 将返回错误,也就是返回 -1 、并且会将 errno 设置为 ECHILD 。
  • 如果进程调用 wait() 之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait() 也不会阻塞。 wait() 函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”。所以在调用 wait() 函数之前,已经有子进程终止了,意味着正等待着父进程为其“收尸”,所以调用 wait() 将不会阻塞,而是会立即替该子进程“收尸”、处理它的“后事”,然后返回到正常的程序流程中,一次 wait() 调用只能处理一次。

1.3 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

#include <sys/wait.h>

int main(int argc, char *argv[])
{
int i = 0;
pid_t pid;
int status;

printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("father process printf<pid:%d,child pid:%d,father return:%d>\n", getpid(), pid, pid);
/* 回收结束的子进程 */
wait(&status);
printf("Get child status=%x,WEXITSTATUS(status)=%d\n", status, WEXITSTATUS(status));
while(1)
{
sleep(1);
printf("father sleep!\n");
}
}
else if(pid == 0)/* 子进程 */
{
printf("child process printf<pid:%d,father pid:%d,child return:%d>\n", getpid(), getppid(), pid);
while(i++ < 3)
{
printf("i = %d\n", i);
sleep(1);
}
printf("child will exit!\n");
sleep(1);
exit(2);
}
else if(pid < 0)
{
perror("fork");
exit(-1);
}

return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
before father fork!
-----------------
father process printf<pid:14221,child pid:14222,father return:14222>
child process printf<pid:14222,father pid:14221,child return:0>
i = 1
i = 2
i = 3
child will exit!
Get child status=200,WEXITSTATUS(status)=2
father sleep!
father sleep!
father sleep!
father sleep!

可以看到,刚开始的时候,父进程一直处于阻塞状态,当子进程结束的时候,父进程才开始运行。

2. waitpid()

2.1 函数说明

在 linux 下可以使用 man waitpid 命令查看该函数的帮助手册。

1
2
3
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);

【函数说明】该函数以等待进程的任一子进程终止,同时获取子进程的终止状态信息并回收子进程的残留资源,从本质上讲,系统调用 waitpid() 和 wait() 的作用是完全相同的,但 waitpid() 多出了两个可由用户控制的参数 pid 和 options ,从而为我们编程提供了另一种更灵活的方式。

【函数参数】

  • pid : pid_t 类型,参数 pid 用于表示需要等待的某个具体子进程。
点击查看 pid 参数的详细说明
pid > 0表示等待进程号为pid的子进程
pid = 0则等待与调用进程(父进程)同一个进程组的所有子进程
pid < -1则会等待进程组标识符与pid绝对值相等的所有子进程
pid = -1则等待任意子进程。wait(&status)与waitpid(-1, &status, 0)等价
  • wstatus : int * 类型,参数 wstatus 用于存放子进程终止时的状态信息(包括子进程返回值和结束方式的地址),可以为 NULL ,表示直接释放子进程 PCB ,不接收子进程终止时的状态信息。
点击查看子进程终止的状态信息判别

参数 wstatus 不为 NULL 的情况下, wait() 会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 wstatus 参数。以下只列举几个,更为详细的还是需要查看手册。

WIFEXITED(wstatus)如果子进程正常终止,则返回true;
WEXITSTATUS(wstatus)返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或exit()时指定的退出状态;wait()获取得到的status参数并不是调用_exit()或exit()时指定的状态,可通过WEXITSTATUS宏转换;
WIFSIGNALED(wstatus)如果子进程被信号终止,则返回true;
WTERMSIG(wstatus)返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
WCOREDUMP(wstatus)如果子进程终止时产生了核心转储文件,则返回true;
  • option : int 类型,是一个位掩码,可以包括 0 个或多个标志,表示函数返回的方式。
点击查看 option 的详细取值
0不使用下边的标志。
WNOHANG如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待(不会像wait那样永远等下去),可以实现轮训poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于0表示没有发生改变。
WUNTRACED除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
WCONTINUED返回那些因收到SIGCONT信号而恢复运行的子进程的状态信息。

【返回值】返回值为 pid_t 类型,

  • 当正常返回的时候, waitpid 返回收集到的子进程的进程ID。
  • 设置了选项 WNOHANG ,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0 。
  • 失败时返回 EOF ,当 pid 所指示的子进程不存在,或此进程存在,但不是调用进程的子进程, waitpid 就会出错返回,这时 errno 被设置为 ECHILD 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
/* 需要包含的头文件 */
#include <sys/types.h>
#include <sys/wait.h>

/* 至少应该有的语句 */
int status;
waitpid(pid, &status, 0);
waitpid(pid, &status, WNOHANG);
waitpid(-1, &status, 0);
waitpid(-1, &status, WNOHANG);

【注意事项】 wait(&status) 与 waitpid(-1, &status, 0) 等价。

2.2 阻塞方式实例

点击查看实例——阻塞方式
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

#include <sys/wait.h>
#include <errno.h>

int main(int argc, char *argv[])
{
int i = 0;
int ret = 0;
pid_t pid;
int status;

printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
for(i = 1; i<=3; i++)
{
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("[i=%d]father process<pid:%d,child pid:%d>\n", i, getpid(), pid);
}
else if(pid == 0)/* 子进程 */
{
printf("[i=%d]child process<pid:%d,father pid:%d>\n", i, getpid(), getppid());
sleep(i);
_exit(i);
}
else if(pid < 0)
{
perror("fork");
exit(-1);
}
}
sleep(1);
printf("~~~~~~~~~~~~~~\n");
for (i = 1; i <= 3; i++)
{
ret = waitpid(-1, &status, 0); /* 阻塞式等待回收子线程 */
if (ret == -1)
{
if (ECHILD == errno)
{
printf("There are no child processes waiting for recycling!\n");
exit(0);
}
else
{
perror("wait error");
exit(-1);
}
}
printf("Reclaim child process<%d>, Termination status<%d>\n", ret, WEXITSTATUS(status));
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
before father fork!
-----------------
[i=1]father process<pid:14627,child pid:14628>
[i=2]father process<pid:14627,child pid:14629>
[i=3]father process<pid:14627,child pid:14630>
[i=2]child process<pid:14629,father pid:14627>
[i=3]child process<pid:14630,father pid:14627>
[i=1]child process<pid:14628,father pid:14627>
~~~~~~~~~~~~~~
Reclaim child process<14628>, Termination status<1>
Reclaim child process<14629>, Termination status<2>
Reclaim child process<14630>, Termination status<3>

2.3 轮训方式实例

点击查看实例——轮训方式
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

#include <sys/wait.h>
#include <errno.h>

int main(int argc, char *argv[])
{
int i = 0;
int ret = 0;
pid_t pid;
int status;

printf("before father fork!\n-----------------\n");
/* 父进程创建子进程 */
for(i = 1; i<=3; i++)
{
pid = fork();
/* 下边的代码父进程执行一次,子进程也执行一次 */
if(pid > 0)/* 父进程 */
{
printf("[i=%d]father process<pid:%d,child pid:%d>\n", i, getpid(), pid);
}
else if(pid == 0)/* 子进程 */
{
printf("[i=%d]child process<pid:%d,father pid:%d>\n", i, getpid(), getppid());
sleep(i);
_exit(i);
}
else if(pid < 0)
{
perror("fork");
exit(-1);
}
}
sleep(1);
printf("~~~~~~~~~~~~~~\n");
while(1)
{
ret = waitpid(-1, &status, WNOHANG); /* 轮训式等待回收子线程 */
if (ret < 0)
{
if (ECHILD == errno)
{
printf("There are no child processes waiting for recycling!\n");
exit(0);
}
else
{
perror("wait error");
exit(-1);
}
}
else if (ret == 0)
continue;
else
printf("Reclaim child process<%d>, Termination status<%d>\n", ret, WEXITSTATUS(status));
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
before father fork!
-----------------
[i=1]father process<pid:14868,child pid:14869>
[i=2]father process<pid:14868,child pid:14870>
[i=3]father process<pid:14868,child pid:14871>
[i=2]child process<pid:14870,father pid:14868>
[i=1]child process<pid:14869,father pid:14868>
[i=3]child process<pid:14871,father pid:14868>
~~~~~~~~~~~~~~
Reclaim child process<14869>, Termination status<1>
Reclaim child process<14870>, Termination status<2>
Reclaim child process<14871>, Termination status<3>
There are no child processes waiting for recycling!

3. waitid()

在 linux 下可以使用 man 3 waitid 命令查看该函数的帮助手册。

1
2
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

【函数说明】该函数以等待进程的任一子进程终止,同时获取子进程的终止状态信息并回收子进程的残留资源。 waitid 提供了更加丰富的选项,使用 waitid 可以实现 wait 和 waitpid 的所有功能。

【函数参数】

  • idtype 和 id :分别为 idtype_t 类型和 id_t ,这两个参数配合使用,两者共同决定需要等待的子进程。
点击查看 两个参数的使用
P_PID等待特定的进程:id包含等待的子进程ID
P_PGID等待任何在特定进程组里的子进程:id包含等待的子进程的进程组ID
P_ALL等待所有子进程:id被忽略
  • infop : siginfo_t * 类型,是指向 siginfo 结构的指针。该结构包含了有关引起子进程状态改变的生成信号的详细信息。
点击查看结构体成员情况

可以使用 man siginfo_t 查看详细定义情况,不过打开后需要搜索一下这个结构体名称就可以找到啦。

1
2
3
4
5
6
7
8
9
typedef struct {
int si_signo; /* Signal number */
int si_code; /* Signal code */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
void *si_addr; /* Address of faulting instruction */
int si_status; /* Exit value or signal */
union sigval si_value; /* Signal value */
} siginfo_t;
  • option : int 类型,是一个位掩码,表示函数返回的方式。
点击查看 option 的详细取值
WCONTINUED等待之前停止但被继续的,但其状态还没有被报告的进程
WEXITED等待已经退出的进程
WNOHANG在没有可用的子进程退出状态时,立即返回,而不是阻塞
WNOWAIT不摧毁子进程的退出状态。子进程的退出状态可以被随后的wait、waitid或waitpid得到
WSTOPPED等待一个停止的且状态还没有被报告的进程

【返回值】返回值为 int 类型,成功时返回 0 ;失败时返回 EOF 。

【使用格式】 none

【注意事项】此函数目前还未使用过,后续有用到再补充。

4. SIGCHLD

除了上边介绍的三个函数和可以实现进程的回收外,我们还可以通过信号来完成进程回收,在子进程结束的时候,会发出这样一个信号 SIGCHLD ,当发生以下两种情况时,父进程会收到该信号:

  • 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
  • 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。

子进程的终止属于异步事件,父进程事先是无法预知的,如果父进程有自己需要做的事情,它不能一直 wait() 阻塞等待子进程终止(或轮训),这样父进程将啥事也做不了,这个时候就可以通过 SIGCHLD 信号来帮助完成进程的回收。 SIGCHLD 信号的系统默认处理方式是将其忽略,所以我们要捕获它、绑定信号处理函数,在信号处理函数中调用 wait() 收回子进程,回收完毕之后再回到父进程自己的工作流程中。具体是通过信号捕获的两个函数 signal() 和 sigaction() 函数,这两个函数都可以完成对子进程的回收,可具体的可以看后边的笔记,信号的使用属于进程间的通信,写了专门的几篇笔记。