说明
- fork()函数通过系统调用创建一个与原来进程几乎完全相同的新进程,新进程为子进程,原进程为父进程。特点是:调用一次,返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误(创建新进程失败),fork返回一个负值;
我们可以通过fork返回的值来判断当前进程是子进程还是父进程。 - 进程调用fork,当控制转移到内核后,内核将做以下事情:
1)分配新的内存块和内核数据结构给子进程
2)将父进程的部分数据结构内容拷贝至子进程(写时拷贝)
3)添加子进程到系统进程列表当中
4)fork返回,开始调度器调度
举例
下面用一些例子详细解析fork函数
例0
例0:
void fork0()
{
if (fork() == 0) {
printf("Hello from child\n");
}
else {
printf("Hello from parent\n");
}
}
运行结果
fork产生了一个新的子进程,根据fork返回值的结果不同,明确区分执行不同代码
coral@coral-VirtualBox:~/homework/fork$ ./forks 0
Hello from parent
Hello from child
进程结构图
-------------------------> printf("Hello from child\n")
|
|
|
|
|
|
---------+-----------------------> printf("Hello from parent\n")
fork
例1
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);
}
运行结果
据fork返回值的结果不同,明确区分执行不同代码,同时还有一条两个进程中都存在的语句,两个进程都会执行它。
coral@coral-VirtualBox:~/homework/fork$ ./forks 1
Parent has x = 0
Bye from process 3354 with x = 0
Child has x = 2
Bye from process 3355 with x = 2
进程结构图
printf,x=2
---------------->------------------->printf(getpid()),x=2
|
|
|
|
|
| printf,x=0
---------+---------------->------------------>printf(getpid()),x=0
fork
x=1
例2
void fork2()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
运行结果
第一次
coral@coral-VirtualBox:~/homework/fork$ ./forks 2
L0
L1
Bye
coral@coral-VirtualBox:~/homework/fork$ L1
Bye
Bye
Bye
第二次
coral@coral-VirtualBox:~/homework/fork$ ./forks 2
L0
L1
Bye
coral@coral-VirtualBox:~/homework/fork$ Bye
L1
Bye
Bye
由于父、子进程运行的顺序不是确定的,取决于系统的调度,所以输出结果也是不确定的
进程结构图
------------------> printf("Bye\n")
|
|
printf("L1\n") |
--------->---------+------------------>printf("Bye\n")
| Fork
|
| ------------------->printf("Bye\n")
| |
| |
printf("L0\n") | printf("L1\n") |
+---------------+--------->--------+--------------------> printf("Bye\n")
Fork Fork
例3
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
运行结果
同例2
coral@coral-VirtualBox:~/homework/fork$ ./forks 3
L0
L1
L2
Bye
coral@coral-VirtualBox:~/homework/fork$ Bye
L2
Bye
L1
L2
Bye
Bye
Bye
L2
Bye
Bye
进程结构图
-----------------> printf("Bye\n")
printf("L2\n") |fork
--------->---------+---------------->printf("Bye\n")
| --------------->printf("Bye\n")
printf("L1\n") | printf("L2\n") |fork
-------->-----------+--------->--------+---------------->printf("Bye\n")
| fork
| ----------------> printf("Bye\n")
| printf("L2\n") | fork
| -------->----------+--------------->printf("Bye\n")
| | --------------->printf("Bye\n")
printf("L0\n") | printf("L1\n") | printf("L2\n") |
------------------+---------->-------+--------->---------+--------------->printf("Bye\n")
fork fork fork
例4
void fork4()
{
printf("L0\n");
if (fork() != 0) {
printf("L1\n");
if(fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
运行结果
第一次fork,父进程执行括号内的语句,打印L1,子进程打印Bye,第二次fork,同理,子进程执行仅括号外的语句,打印Bye,而父进程执行完括号内语句后,再执行括号外语句,同例1
coral@coral-VirtualBox:~/homework/fork$ ./forks 4
L0
L1
L2
Bye
coral@coral-VirtualBox:~/homework/fork$ Bye
Bye
进程结构图
Bye
-------------->+
|
| Bye
| ---------------->+
L0 | L1 | L2 Bye | |
+--------+----->------+-------------->+--------------->+
Fork Fork
例5
void fork5()
{
printf("L0\n");
if (fork() == 0) {
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./forks 5
L0
Bye
coral@coral-VirtualBox:~/homework/fork$ L1
Bye
L2
Bye
进程结构图
L2 Bye
------->+------->-------------->+
|
|
L1 | Bye
------>+------+-------------->+
| Fork
|
|
|
L0 | Bye
+--------+------------>+
Fork
例6
void cleanup(void) {
printf("Cleaning up\n");
}
void fork6()
{
atexit(cleanup);
fork();
exit(0);
}
atexit函数是一个特殊的函数,它是在正常程序退出时调用的函数,我们把他叫为登记函数
函数原型:int atexit (void (*)(void))):
⼀个进程可以登记若⼲个函数,这些函数由exit⾃动调⽤,这些函数被称为终⽌处理函数(即atexit函数内的参数), atexit函数可以登记这些函数。 exit调⽤终⽌处理函数的顺序和atexit登记的顺序相反,如果⼀个函数被多次登记,也会被多次调⽤。
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./forks 6
Cleaning up
coral@coral-VirtualBox:~/homework/fork$ Cleaning up
进程结构图
-------------->+cleanup
|
|
|
|
+--------+-------------->+cleanup
Fork
6-1
#include<stdio.h>
#include<stdlib.h> //atexit函数所属头文件
void func1()
{
printf("The process is done...\n");
}
void func2()
{
printf("Clean up the processing\n");
}
void func3()
{
printf("Exit sucessful..\n");
}
int main()
{
//其作用是注册某一个函数,当进程执行结束时,会自动调用注册的函数
//注册几次,就执行几次
atexit(func1);
atexit(func2);
atexit(func3);
exit(0);
}
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./test6
func3
func2
func1
例7
void fork7()
{
if (fork() == 0) {
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1) ;
}
}
运行结果
由于在父进程中存在死循环,而子进程停止后,只有等到他的父进程来回收他,否则将一直存在于内存中,成为僵尸进程。可通过命令top来查看所有僵尸进程。
coral@coral-VirtualBox:~/homework/fork$ ./forks 7 &
[1] 4387
coral@coral-VirtualBox:~/homework/fork$ Running Parent, PID = 4387
Terminating Child, PID = 4388
ps
PID TTY TIME CMD
4375 pts/4 00:00:00 bash
4387 pts/4 00:00:04 forks
4388 pts/4 00:00:00 forks <defunct>
4389 pts/4 00:00:00 ps
例8
void fork8()
{
if (fork() == 0) {
printf("Running Child, PID = %d\n",
getpid());
while (1) ;
} else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
运行结果
子进程成为僵尸进程
coral@coral-VirtualBox:~/homework/fork$ ./forks 8
Terminating Parent, PID = 4644
coral@coral-VirtualBox:~/homework/fork$ Running Child, PID = 4645
ps
PID TTY TIME CMD
4631 pts/4 00:00:00 bash
4645 pts/4 00:00:29 forks
4653 pts/4 00:00:00 ps
例9
请问下面的程序一共输出多少个“-”?
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
int i;
for(i=0; i<2; i++)
{
fork();
printf("-");
}
return 0;
}
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./test1
--coral@coral-VirtualBox:~/homework/fork$ ------
程序结构图
printf("-")
--------------------->+ 输出两个“-”
|
|
printf("-") | printf("-")
------>+------+-------------------->+
| fork
| i=1 printf("-")
| --------------------->+ 输出两个“-”
| |
| |
| printf("-")| printf("-")
+--------+------>+-----+-------------------->+
Fork Fork
i = 0 i = 1
答案为8个。在这之前我们需要知道:
- fork函数会把它所在语句以后的语句复制到一个子进程,单独执行。
- 在fork的调用处,整个父进程空间会原封不动地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区等。
- 程序遇到“\n",或是EOF,或是缓冲区满,或是文件描述符关闭,或是主动flush,或是程序退出,就会把数据刷出缓冲区。
而在这里,由于标准输出是行缓冲,遇到“\n"才会把数据刷出缓冲区。printf("-")把“-”放在缓存中,并没有真正输出,在fork的时候将复制到子进程空间中。因此有两个子进程将多输出1个“-”。
对于上面的问题,我们如果修改一下上面的printf的那条语句为:
printf("-\n");
或
printf("-");
fflush(stdout);
就没有问题了(就是6个“-”了),因为程序遇到“\n”,就会把“-”刷出缓冲区,不会再复制到子进程空间。
例10
#include "stdio.h"
#include "sys/types.h"
#include "unistd.h"
int main()
{
pid_t pid1;
pid_t pid2;
pid1 = fork();
pid2 = fork();
printf("pid1:%d, pid2:%d\n", pid1, pid2);
}
已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它新进程执行。
1、请说出执行这个程序后,将一共运行几个进程。
2、如果其中一个进程的输出结果是“pid1:1001, pid2:1002”,写出其他进程的输出结果(不考虑进程执行顺序)。
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./test2
pid1:4754, pid2:4755
coral@coral-VirtualBox:~/homework/fork$ pid1:4754, pid2:0
pid1:0, pid2:4756
pid1:0, pid2:0
例11
int main(){
return fork()&&fork()||fork();
}
问题:1.一共产生几个进程 2.返回值为1的概率为多少?
进程结构图
例12
int main(int argc, char* argv[]){
fork();
fork() && fork() || fork();
fork();
}
不算main这个进程自身,到底创建了多少个进程啊?
进程结构图
例13
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int i=0;
printf("i son/pa ppid pid fpid/n");
//ppid指当前进程的父进程pid
//pid指当前进程的pid,
//fpid指fork返回给当前进程的值
for(i=0;i<2;i++){
pid_t fpid=fork();
if(fpid==0)
printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid);
else
printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);
}
return 0;
}
运行结果
进程结构图
同例9
例14
#include "csapp.h"
void doit()
{
if (fork()==0){
fork();
printf("hello\n");
exit(0);
}
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./test3
hello
coral@coral-VirtualBox:~/homework/fork$ hello
hello
进程结构图
例15
将doit函数中exit(0)改为return
运行结果
coral@coral-VirtualBox:~/homework/fork$ ./test3
hello
coral@coral-VirtualBox:~/homework/fork$ hello
hello
hello
hello
进程结构图
-
exit(0):exit 是一个函数,正常运行程序并退出程序,是系统调用级别的,exit它表示了一个进程的结束;
exit(1):非正常运行导致退出程序;
return():返回函数值,是关键字。 -
return是函数的退出(返回);exit是进程的退出。return用于结束一个函数的执行,将函数的执行信息传出给其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS或其父进程,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出, 非0 为非正常退出。
所以在例14的doit函数中,子进程直接结束,不会再返回到主函数中,并执行最后一条打印指令。而在例15中,子进程也会返回到主函数,故会打印最后一条指令。 -
另外
在stdlib.h中exit函数是这样子定义的:void exit(int status)。这个系统调用是用来终止一个进程的,无论在程序中的什么位置,只要执行exit,进程就会从终止进程的运行。而另一个和它很相似的函数_exit()也位于unistd.h中,相比于exit(),_exit()函数的功能最为简单,直接终止进程的运行,释放其所使用的内存空间,并销毁在内存中的数据结构,而exit()在于在进程退出之前要检查文件的状态,将文件缓冲区中的内容写回文件。
例16
#include "csapp.h"
void end(void)
{
printf("2");fflush(stdout);
}
int main()
{
if(fork()==0)
atexit(end);
if(fork()==0){
printf("0");fflush(stdout);
}
else{
printf("1");fflush(stdout);
}
exit(0);
}
coral@coral-VirtualBox:~/homework/fork$ ./test5
1coral@coral-VirtualBox:~/homework/fork$ 01202
coral@coral-VirtualBox:~/homework/fork$ ./test5
1coral@coral-VirtualBox:~/homework/fork$ 01202
coral@coral-VirtualBox:~/homework/fork$ ./test5
1coral@coral-VirtualBox:~/homework/fork$ 01202
coral@coral-VirtualBox:~/homework/fork$ ./test5
1coral@coral-VirtualBox:~/homework/fork$ 01202
+-->printf("0")+--->printf("2")
|
+----->atexit+-+-->printf("1")+--->printf("2")
| Fork
| Fork
+---+--------------+-->printf("1")
Fork |
+-->printf("0")