0%

关于fork函数的使用

基本必备知识

fork函数的基本说明

介绍

fork函数用于创建一个新的进程,下面引用一段话介绍一下:

一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己

函数原型

1
pid_t fork( void);

其中pid_t是一个宏定义,其本身是int型的数据。

函数的返回

fork函数调用非常的神奇,一次调用将会有两次放回,一次是在父进程之中返回,另一次是在子进程里面返回。
而父进程的的返回又分为两种情况:

  • 返回值大于零:成功创建子进程并返回子进程的PID
  • 返回值为-1:发生错误,创建子进程失败
  • 返回值为0:子进程的返回值是0

wait函数与waitpid函数的基本说明

waitpid函数

waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束.

函数原型

1
pid_t waitpid(pid_t pid, int *statusp, int options);
  1. pid所等待的子进程
    pid参数有种情况:
    • pid > 0只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
    • pid = 0waitpid所等待的进程是同一个进程组里的任意一个子进程。
    • pid = -1wait所等待的进程是父进程的任意一个子进程。
    • pid < -1waitpid所等待的进程是一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
  2. statusp回收的子进程的退出状态
    • 如果statusp是一个空指针,则表示父进程不关心子进程结束的状态。
    • 如果statusp不是一个空指针,则会在status放上关于导致返回的子进程的状态信息,相关信息请看下面的表格
宏(返回值) 信息
WIFEXITED(status) 如果子进程通过调用exit或者一个返回(returm)正常终止,就返回真。
WEXITSTATUS(status) 返回一个正常终止的子进程的退出状态。
WIFSIGNALED(status) 如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
WTERMSIG(status) 返回导致子进程终止的信号的编号。
WIFSTOPPED(status) 如果引起返回的子进程当前是停止的,那么就返回真。
WSTOPSIG(status) 返回引起子进程停止的信号的编号。
WIFCONTINUED(status) 如果子进程收到SIGCONT信号重新启动,则返回真。
  1. options默认行为
    可以通过将options设置为常量WNOHANGWUNTRACEDWCONTINUED或是它们的组合来修改默认行为。
常量 行为
WNOHANG 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
WUNTRACED 挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID为导致返回的已终止或被停止子进程的PID。默认的行为是只返回已终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用。
WCONTINUED 挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。

函数的返回

  • 如过成功则返回子进程的 PID
  • 如果参数optionsWNOHANG则返回 0
  • 如果发生了其他的错误则会返回 -1

wait函数

函数原型

1
pid_t wait(int *status);

其实调用wait(&status)等价于调用waitpid(- 1, &status, 0 ),这里就不对这个函数做过多的解释了。

函数的返回

  • 如过成功则返回子进程的 PID
  • 如果发生了错误则会返回 -1

signal函数的基本说明

函数的声明

1
void (*signal(int sig, void (*func)(int)))(int);

signal函数用于修改信号与其相关联的默认行为,例如接收到SIGKILL信号默认行为就是终止接收进程,接收到SIGCHLD信号默认行为就是忽略这个信号。有一点特殊的是,SIGSTOPSIGKILL,这两个的信号的默认行为是无法修改的。

参数

  • sig:待修改关联的信号
  • func:修改关联信号的新处理函数

fork函数的使用例子

例子1

代码:

1
2
3
4
5
6
7
8
9
void fork0() 
{
if (fork() == 0) {
printf("Hello from child,pid = %d\n",getpid());
}
else {
printf("Hello from parent,pid = %d\n",getpid());
}
}

它输出的内容是:

1
2
Hello from parent,pid = 18913
Hello from child,pid = 18914

fork函数的介绍里面有说过,fork函数是一次调用两次返回,我们就可以通过在父进程与子进程中返回的内容的不同来分辨当前进程是父进程还是子进程,从而达到我们所需要的效果。
fork函数在子进程之中返回 0,所以Hello from child,pid = 18914应是由子进程在完成 if 条件语句后转跳到printf函数进行输出。而父进程中fork函数返回的是子进程的pid(18914),则Hello from parent,pid = 18913应是由父进程进行输出。

例子2

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void fork1()
{
int x = 1;
pid_t pid = fork();

if (pid == 0) {
printf("Child has x = %d\n", ++x);
}
else {
printf("Parent has x = %d\n", --x);
}
printf("Bye from process %d with x = %d\n", getpid(), x);
}

输出到屏幕的内容如下:

1
2
3
4
5
linux> ./f 1
Parent has x = 0
Child has x = 2
Bye from process 16 with x = 0
Bye from process 17 with x = 2

父进程调用fork函数创建了一个新的进程活后,此时系统之中就会出现了两个基本一样的进程,无论是代码或是数据都基本一样,就相当于把父进程复制了一份,但由于fork函数返回的值不同,会有相应的逻辑判断,从而导致基本一样的进程执行的代码却不太一样。

例子3

代码:

1
2
3
4
5
6
7
8
9
10
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}

输出到屏幕的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
linux> ./f 3
L0 from process 18
L1 from process 18
L1 from process 19
L2 from process 18
L2 from process 20
L2 from process 19
L2 from process 21
Bye from process 18
Bye from process 22
Bye from process 20
Bye from process 23
Bye from process 19
Bye from process 24
Bye from process 21
Bye from process 25

主要是理解了fork函数的使用后,弄清楚它的逻辑,既能够很清楚的理解它的输出为什么是这样。当然,不同的机器跑出来的结果有可能会不一样,个别内容的输出顺序可能会跟我上面的不一样,这得看CPU如何处理这些进程。
下面是一个大致的流程图:
fork3

例子4

代码:

1
2
3
4
5
6
7
8
9
10
11
void fork5()
{
printf("L0 from process %d,ppid:%d\n", getpid(),getppid());
if (fork() == 0) {
printf("L1 from process %d,ppid:%d\n", getpid(),getppid());
if (fork() == 0) {
printf("L2 from process %d,ppid:%d\n", getpid(),getppid());
}
}
printf("Bye from process %d,ppid:%d\n", getpid(),getppid());
}

输出到屏幕的内容如下:

1
2
3
4
5
6
7
linux> ./f 5
L0 from process 38,ppid:4
Bye from process 38,ppid:4
L1 from process 39,ppid:38
Bye from process 39,ppid:1
L2 from process 40,ppid:39
Bye from process 40,ppid:1

下面是一个大致的流程图:
fork5

例子5

代码:

1
2
3
4
5
6
7
8
9
void fork18()
{
printf("fork");
if (fork() == 0)
{
printf("2018\n");
}
printf("2019\n");
}

你觉得这个代码所输出的内容会是什么呢?
输出到屏幕的内容如下:

1
2
3
4
linux> ./f 18
fork2019
fork2018
2019

其实这printf的缓冲机制有关,printf并不会直接把内容直接打印到屏幕上,而是放在了缓存之中,所以在执行fork的时候,缓冲区的内容也会被放到子进程相应的地方。一般来说,缓冲区满后或是遇到了换行符才会把内容输出到屏幕上。

例子6

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 void fork9_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig);
exit(0);
}
void fork9()
{
int child_status;
signal(SIGINT,fork9_handler);
if (fork() == 0) {
printf("HC: hello from child\n");
while(1)
;
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}

由于在子进程中有死循环,所以该程序无法正常的结束,可以另一个终端窗口发送SIGINT信号给子进程令程序正常结束。所以输出入下:

1
2
3
4
5
6
linux> ./f 9
HP: hello from parent
HC: hello from child
Process 90 received signal 2
CT: child has terminated
Bye

但是我在运行上面的程序时,在子进程死循环时挂起进程,再发送SIGINT信号给子进程,然而此时父进程却不能够正常的退出了:

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
linux> ./f 9
HP: hello from parent
HC: hello from child
^Z
[1]+ 已停止 ./f 9
linux> ps
PID TTY TIME CMD
4 tty1 00:00:00 bash
17 tty1 00:00:00 f
18 tty1 00:00:01 f
19 tty1 00:00:00 ps
linux> kill -2 18
Process 18 received signal 2
linux> ps
PID TTY TIME CMD
4 tty1 00:00:00 bash
17 tty1 00:00:00 f
18 tty1 00:00:01 f <defunct>
20 tty1 00:00:00 ps
linux> fg 1
./f 9
^Z
[1]+ 已停止 ./f 9
linux> ps
PID TTY TIME CMD
4 tty1 00:00:00 bash
17 tty1 00:00:00 f
18 tty1 00:00:01 f <defunct>
21 tty1 00:00:00 ps

且在另一个终端查看进程时仍能够看到父进程还在运行着,但是拿着同样的程序在另外一台机器上跑确是能够正常的退出,或许这是Windows子系统在某方面没做好吧。
于是我又再次修改了代码,进行实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 void fork9_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig);
exit(0);
}
void fork9()
{
int child_status;
signal(SIGCHLD, fork9_handler); //Add
signal(SIGINT,fork9_handler);
if (fork() == 0) {
printf("HC: hello from child\n");
while(1)
;
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
linux> ./f 9
HP: hello from parent
HC: hello from child
^Z
[1]+ 已停止 ./f 9
linux> ps
PID TTY TIME CMD
4 tty1 00:00:00 bash
16 tty1 00:00:00 f
17 tty1 00:00:01 f
18 tty1 00:00:00 ps
linux> kill -2 17
Process 17 received signal 2
linux> fg 1
./f 9
Process 16 received signal 17
linux> ps
PID TTY TIME CMD
4 tty1 00:00:00 bash
19 tty1 00:00:00 ps

这一次父进程能够正常的退出了,所以我个人怀疑是wait函数在Window子系统下会发生一些未知的错误。


参考资料