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 |
|
运行结果
1 | xixi@xixi:~/linux_cource/11.3$ ./a.out |
程序在调用fork时被分为两个独立的进程.当用fork启动一个子进程时, 子进程就有了它自己的生命周期并将独立运行.
父进程在子进程之前结束, 因此在输出内容中有一个shell提示符.
让父进程等待子进程结束
如果希望父进程等待子进程结束,可以调用wait函数.
函数原型: pid_t wait(int* stat_loc);
wait系统调用返回子进程的pid, 通常是已经结束运行的子进程的pid.状态信息允许父进程了解子进程的退出状态, 即子进程的main函数返回的值或者子进程中exit函数的退出码.如果stat_loc不是空指针, 状态信息将被写入到它所指向的位置.
1 |
|
运行结果
1 | fork program starting |
查看僵尸进程
子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才会结束。因此,进程表中代表子进程的表项不会立即释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时候它将成为一个死进程或者僵尸进程。
所以,僵尸进程就是这样一种进程:它自身运行已经结束,但是进程控制块还没有释放。因为还有其他进程需要它的运行状态等信息。
1 |
|
运行过程中在命令行中查看wait进程的信息(上面这个程序在编译时被命名为wait)
1 | xixi@xixi:~$ ps -f -C wait |
上面的运行结果可以看出,一开始子进程正常运行,后来子进程变成了僵尸进程。
如果父进程异常终止,子进程将自动把pid等于1的进程(即init进程)作为自己的父进程。子进程现在是一个不再运行的僵尸进程,但是由于父进程异常终止,所以它由init进程接管。僵尸进程将一直保留在进程表中知道被init进程发现并释放,进程表越大,这一过程越慢。
常用的两种应用场景
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段.这在网络服务中是常见的。
- 父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求的到达。
- 一个进程要执行一个不同的程序,这是shell常见的情况。子进程从fork返回后立即调用exec。
1 | pid_t pid; |
执行结果
1 | 当前进程, id=7346 |
执行fork前,ctr=0;执行fork后,对于父进程而言会执行ctr自增和打印进程信息两个操作。因此打印出的ctr=1。
对于子进程而言,由于执行fork前ctr=0,因此在子进程中ctr=0,同样子进程执行ctr自增和打印进程信息操作。因此打印出的ctr=1。
1 | pid_t pid; |
执行结果
1 | loop=0 我是进程5947 父进程id=19893 我刚刚fork了子进程 pid=5948 |
过程解释
- 进程5947使用fork产生一个子进程5948. fork执行时loop=0.因此子进程5948中loop=0.
- 子进程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”
- 而对于进程5947, 在第一次循环结束后继续执行第二次循环. 此时loop=1. 调用fork后, 产生一个新的子进程5952, 该子进程中loop=1. 因此类似的, 进程5952只会打印出”loop=1 我是进程5952 父进程id=5947 我刚刚fork了子进程 pid=0”
父进程和子进程共享文件
父进程中的所有打开文件描述符都被复制到子进程中。父进程和子进程每个相同的打开文件描述符共享一个文件表项。
1 | // |
运行结果
1 | xixi2@xixi2:~$ cat data.txt |
可以看到,当子进程读文件,移动文件偏移量后;可以在父进程中看到文件偏移量的变化。
参考文献
[1] https://www.cnblogs.com/bastard/archive/2012/08/31/2664896.html
[2] https://www.jianshu.com/p/586300fdb1ce