fork和vfork

fork和vfork

fork基本概念

一个进程,包括代码,数据和分配给该进程的资源。fork()通过系统调用创建一个与原来进程几乎完全相同的进程。也就是两个进程做完全相同的事.但是如果初始参数或者传入的变量不同,两个进程也可以做不同的事情。

一个进程调用fork()函数后,系统先给新的进程分配资源。如存储数据和代码的存储空间。然后吧原来的进程的所有值都复制到新进程中,只有少数值与原来的进程的值不同,相当于克隆了一个自己。

子进程是父进程的副本。子进程获得父进程数据空间、堆和栈的副本。父进程和子进程并不共享这些空间。父进程和子进程共享正文段。
父进程中的所有打开文件描述符都被复制到子进程中。父进程和子进程每个相同的打开文件描述符共享一个文件表项。

创建新进程成功后,系统中出现两个几乎完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

由fork函数创建的新进程被称为子进程。fork函数被调用一次,但是返回两次。父进程返回的值是新进程的进程ID,而子进程返回的值是0。

fork函数返回值的三种情况

  • 返回子进程Id给父进程
    • 因为一个进程的子进程可能有多个,并且没有一个函数可以获得一个进程的所有子进程ID。
  • 返回给子进程值为0
    • 一个进程只会有一个父进程,所以子进程总是可以调用getpid以获得当前进程Id以及调用getppid获得父进程Id.
  • 出现错误,返回负值
    • 当前进程数已经达到系统规定的上限,这时errno的值被设置为EAGAIN
    • 系统内存不足,这时errno的值被设置为ENOMEM

子进程执行代码开始位置

fork可以创建一个子进程并完全复制父进程,但是子进程是从fork后面那条指令开始执行的.如果子进程也从main开始执行所有指令,那么它执行到fork指令时必然会创建一个新的子进程. 从而导致不停地创建子进程,程序永不结束。

fork创建子进程

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
#include <iostream>
#include <stdlib.h>
#include <unistd.h>

using namespace std;


int main() {
pid_t pid;
string message;
int n;
cout << "fork program starting" << endl;
pid = fork();
switch (pid) {
case -1:
perror("fork failed");
break;
case 0:
message = "this is the child";
n = 5;
break;
default:
message = "this is the parent";
n = 3;
break;
}
for (; n > 0; --n) {
cout << message << endl;
sleep(3);
}
return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
xixi@xixi:~/linux_cource/11.3$ ./a.out 
fork program starting
this is the parent
this is the child
this is the parent
this is the child
this is the parent
this is the child
this is the child
xixi@xixi:~linux_cource/11.3$ this is the child

xixi@xixi:~/linux_cource/11.3$

程序在调用fork时被分为两个独立的进程.当用fork启动一个子进程时, 子进程就有了它自己的生命周期并将独立运行.
父进程在子进程之前结束, 因此在输出内容中有一个shell提示符.

让父进程等待子进程结束

如果希望父进程等待子进程结束,可以调用wait函数.
函数原型: pid_t wait(int* stat_loc);
wait系统调用返回子进程的pid, 通常是已经结束运行的子进程的pid.状态信息允许父进程了解子进程的退出状态, 即子进程的main函数返回的值或者子进程中exit函数的退出码.如果stat_loc不是空指针, 状态信息将被写入到它所指向的位置.

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
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

using namespace std;


int main() {
pid_t pid;
string message;
int n;
int exit_code;
cout << "fork program starting" << endl;
pid = fork();
switch (pid) {
case -1:
perror("fork failed");
exit(1);
case 0:
message = "this is the child";
n = 5;
exit_code = 37;
break;
default:
message = "this is the parent";
n = 3;
exit_code = 0;
break;
}
for (; n > 0; --n) {
cout << message << endl;
sleep(1);
}
if (pid != 0) {
int stat_val;
pid_t child_pid;
child_pid = wait(&stat_val);
cout << "child has finished: PID: " << child_pid << endl;
if (WIFEXITED(stat_val))
cout << "child exited with code " << WEXITSTATUS(stat_val) << endl;
else {
cout << "Child terminated abnormally" << endl;
}
}
return exit_code;
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
fork program starting
this is the parent
this is the child
this is the parent
this is the child
this is the parent
this is the child
this is the child
this is the child
child has finished: PID: 15467
child exited with code 37

查看僵尸进程

子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才会结束。因此,进程表中代表子进程的表项不会立即释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时候它将成为一个死进程或者僵尸进程。
所以,僵尸进程就是这样一种进程:它自身运行已经结束,但是进程控制块还没有释放。因为还有其他进程需要它的运行状态等信息。

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
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

using namespace std;


int main() {
pid_t pid;
string message;
int n;
int exit_code;
cout << "fork program starting" << endl;
pid = fork();
switch (pid) {
case -1:
perror("fork failed");
exit(1);
case 0:
message = "this is the child";
n = 5;
exit_code = 37;
break;
default:
message = "this is the parent";
n = 10;
exit_code = 0;
break;
}
for (; n > 0; --n) {
cout << message << endl;
sleep(1);
}
if (pid != 0) {
int stat_val;
pid_t child_pid;
child_pid = wait(&stat_val); // stat_val中保存这子进程的状态信息
cout << "child has finished: PID: " << child_pid << endl;
if (WIFEXITED(stat_val))
cout << "child exited with code " << WEXITSTATUS(stat_val) << endl;
else {
cout << "Child terminated abnormally" << endl;
}
}
return exit_code;
}

运行过程中在命令行中查看wait进程的信息(上面这个程序在编译时被命名为wait)

1
2
3
4
5
6
7
8
xixi@xixi:~$ ps -f -C wait
UID PID PPID C STIME TTY TIME CMD
xixi 16389 13738 0 19:32 pts/3 00:00:00 ./wait
xixi 16390 16389 0 19:32 pts/3 00:00:00 ./wait
xixi@xixi:~$ ps -f -C wait
UID PID PPID C STIME TTY TIME CMD
xixi 16389 13738 0 19:32 pts/3 00:00:00 ./wait
xixi 16390 16389 0 19:32 pts/3 00:00:00 [wait] <defunct>

上面的运行结果可以看出,一开始子进程正常运行,后来子进程变成了僵尸进程。
如果父进程异常终止,子进程将自动把pid等于1的进程(即init进程)作为自己的父进程。子进程现在是一个不再运行的僵尸进程,但是由于父进程异常终止,所以它由init进程接管。僵尸进程将一直保留在进程表中知道被init进程发现并释放,进程表越大,这一过程越慢。

常用的两种应用场景

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段.这在网络服务中是常见的。
    • 父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求的到达。
  • 一个进程要执行一个不同的程序,这是shell常见的情况。子进程从fork返回后立即调用exec。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pid_t pid;
int ctr = 0;
cout << "当前进程, id=" << getpid() << endl;

// 在这个位置,仅仅父进程会执行
cout << "unique" << endl;

pid = fork(); // 仅调用一次,返回两次
if (pid < 0) {
cout << "异常退出" << endl;
exit(1);
} else if (pid == 0) {
++ctr;
cout << "进入子进程, 当前进程id=" << getpid() << " 父进程id=" << getppid() << " ctr=" << ctr << endl;
} else {
++ctr;
cout << "当前进程id=" << getpid() << " ctr=" << ctr << endl;
}

// 在这个位置,则父子进程都会执行
cout << "reduant" << endl;

执行结果

1
2
3
4
5
6
当前进程, id=7346
unique
当前进程id=7346 ctr=1
reduant
进入子进程, 当前进程id=7347 父进程id=7346 ctr=1
reduant

执行fork前,ctr=0;执行fork后,对于父进程而言会执行ctr自增和打印进程信息两个操作。因此打印出的ctr=1。
对于子进程而言,由于执行fork前ctr=0,因此在子进程中ctr=0,同样子进程执行ctr自增和打印进程信息操作。因此打印出的ctr=1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pid_t pid;
int loop;
// cout << "我是父进程, 当前进程, id=" << getpid() << endl;
for (loop = 0; loop < 2; ++loop) {
pid = fork(); //一个子进程第一次调用fork,返回0,表示该进程本身是子进程,第二次fork产生一个以当前子进程为父进程的子进程
if (pid < 0) {
cout << "异常退出" << endl;
exit(1);
} else if (pid == 0) {
cout << "loop=" << loop << " 我是进程" << getpid() << " 父进程id=" << getppid() << " 我刚刚fork了子进程 pid=" << pid<< endl;
} else {
cout << "loop=" << loop << " 我是进程" << getpid() << " 父进程id=" << getppid() << " 我刚刚fork了子进程 pid=" << pid
<< endl;
sleep(5);
}
}

执行结果

1
2
3
4
5
6
loop=0 我是进程5947 父进程id=19893 我刚刚fork了子进程 pid=5948
loop=0 我是进程5948 父进程id=5947 我刚刚fork了子进程 pid=0
loop=1 我是进程5948 父进程id=5947 我刚刚fork了子进程 pid=5949
loop=1 我是进程5949 父进程id=5948 我刚刚fork了子进程 pid=0
loop=1 我是进程5947 父进程id=19893 我刚刚fork了子进程 pid=5952
loop=1 我是进程5952 父进程id=5947 我刚刚fork了子进程 pid=0

过程解释
1566478650926

  1. 进程5947使用fork产生一个子进程5948. fork执行时loop=0.因此子进程5948中loop=0.
  2. 子进程5948第一次调用fork的返回值pid是0,表明进程5948是子进程, 因此直接打印出”loop=0 我是进程5948 父进程id=5947 我刚刚fork了子进程 pid=0”; 当进程5948第二次调用fork时, 产生一个新的子进程5949. 在进程5948中,调用fork函数前, loop=1,因此在新的子进程5949中loop=1. 也因此子进程5949只会调用一次fork(就会因为loop>=2而跳出循环). 在子进程5949的运行过程中只会打印出”loop=1 我是进程5949 父进程id=5948 我刚刚fork了子进程 pid=0”
  3. 而对于进程5947, 在第一次循环结束后继续执行第二次循环. 此时loop=1. 调用fork后, 产生一个新的子进程5952, 该子进程中loop=1. 因此类似的, 进程5952只会打印出”loop=1 我是进程5952 父进程id=5947 我刚刚fork了子进程 pid=0”

父进程和子进程共享文件

父进程中的所有打开文件描述符都被复制到子进程中。父进程和子进程每个相同的打开文件描述符共享一个文件表项。

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
//
// Created by xixi2 on 2019/10/19.
//
// fork产生子进程后,子进程和父进程共享文件描述符

#include <sys/types.h>
#include <unistd.h> // read, write
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h> // needed for open

int main() {
int fd1, fd2, fd3;
int cnt;
char buff[20];
pid_t pid;
fd1 = open("data.txt", O_RDWR);
pid = fork();
if (pid == 0) {
off_t off0 = lseek(fd1, 0, SEEK_CUR);
printf("old offset for child: %d, ", off0);
cnt = read(fd1, buff, 10); // read成功返回时返回接收的字节数

// 返回值是新的文件偏移量,这里将新的文件偏移量设置为当前文件偏移量加上0,即直接返回当前文件偏移量
off_t off1 = lseek(fd1, 0, SEEK_CUR);
buff[cnt] = '\0';
printf("pid# %d, new offset: %d, content: %s#\n", getpid(), off1, buff);

exit(0);
}
// 让父进程睡眠一会,先让子进程读,修改文件偏移量
sleep(2);
off_t off2 = lseek(fd1, 0, SEEK_CUR);
printf("old offset for father: %d, ", off2);
cnt = read(fd1, buff, 10);
buff[cnt] = '\0';
off_t off3 = lseek(fd1, 0, SEEK_CUR);
printf("pid# %d, new offset: %d, content: %s#\n", getpid(), off3, buff);

return 0;
}

运行结果

1
2
3
4
5
xixi2@xixi2:~$ cat data.txt 
hello there i am tom who are you
xixi2@xixi2:~$ ./ttt
old offset for child: 0, pid# 27898, new offset: 10, content: hello ther#
old offset for father: 10, pid# 27897, new offset: 20, content: e i am tom#

可以看到,当子进程读文件,移动文件偏移量后;可以在父进程中看到文件偏移量的变化。

参考文献

[1] https://www.cnblogs.com/bastard/archive/2012/08/31/2664896.html
[2] https://www.jianshu.com/p/586300fdb1ce