近期学到了深入理解计算机系统基础第八章异常控制流,接下来说一说在这一章C位出道的fork()函数吧。
fork函数
父进程通过调用fork函数来创建一个新的运行的子进程。
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
返回:子进程返回0,父进程返回子进程的PID,如果创建出错,返回-1
可以看出,fork函数是一个无参数返回一个整数型的函数。
新创建的子进程几乎完全跟父进程相同(几乎相同!不完全)。子进程得到与父进程用户级虚拟地址空间相同的(但是独立)一份副本,包括代码、数据段、堆、共享库、用户栈(想成就是继承了父进程的所有知识、能力,子承父业吧)。两者最大区别在于不同的PID(进程号)。
fork函数最大特点:一次调用,两次返回!!! 现在父进程调用一次,子进程还来一次(说了子进程能干父进程所有事情,当然也要返回啦)
我们根据返回的值判断当前程序是处于子进程还是父进程:子进程返回0,父进程返回子进程PID(总不为0).
fork系统调用注意:
- fork系统调用之后父进程和子进程是交替执行,父子进程是处于不同空间中的。
- fork系统调用的一次调用存在两次返回,此时二个进程处于独立的空间,各自执行自己的参数。
我们根据以下操作运行例子1:
fork0
void fork0()
{
if (fork() == 0) {
printf("Hello from child\n");
}
else {
printf("Hello from parent\n");
}
}
可以看出,有两次输出;第一次到fork()==0的时候,是在父进程中执行,因为返回的不为0(子进程PID不为0),所有执行else后面语句;之后到子进程中(子进程跟父进程几乎一样),这时是在子进程中,所以fork返回0,执行child语句。
fork1
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()后,第一次是在父进程中执行,pid!=0(为子进程PID),所以执行–x和父进程PID(ppid)。
重点来了,之后到了子进程中:因为子进程里面内容(代码、数据)跟父进程一样,所有子进程里面的x还是=1(就是独立的副本),返回pid=0,执行++x和子进程的pid;可以看出子进程和父进程的ID是连着一起的,子进程PID就是父进程加1.
fork2
void fork2()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
首先在父进程中输出L0,然后第一次调用fork,此时已经有一个子进程(1),然后父进程中输出L1,再次调用一个fork有了新的子进程(2),同时子进程(1)由于跟父进程代码一样,也有一个子进程(1_1),接着父进程输出Bye,回到子进程(2)输出一样的Bye;接着回到子进程(1),输出L1、Bye、Bye(跟父进程一样操作)。
fork3
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
可以看出,不管fork调用几次,都是先进行父进程里面的操作,再进行相应的子进程的程序,多次用fork函数,就是在相应的父进程(或者子进程)再次创建子进程(子子进程),然后一样依照顺序来执行程序。
先输出L0,调用fork1,有了子进程1;然后父进程中输出L1(子进程1也要输出,不过要等着),然后又一次fork2,生成子进程2(子进程1也要生成子进程1_1),输出L2;执行fork3(漏画了),父进程生成子进程3,子进程生成子进程2_1,子进程1生成子进程1_2,子进程1_1生成子进程1_1_1,然后最后都输出Bye。要注意,是按照顺序(一条线)执行完,不能随意穿插。
fork4
printf("L0\n");
if (fork() != 0) {
printf("L1\n");
if (fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n");
这个程序又有意思了,首先输出L0,然后父进程调用fork,有了一个子进程1,父进程中fork返回不为0,所以输出L1,接着父进程又调用fork,有了子进程2,同理输出L2,然后输出Bye;返回到子进程2中,从fork2开始执行,由于返回0,直接到最外层输出Bye;再回到子进程1,从fork1执行,返回0,直接到最外层输出Bye。
fork5
void fork5()
{
printf("L0\n");
if (fork() == 0) {
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
这个程序跟上一个就是一个等号的区别,进程图原理一样,输出L0后,父进程返回0,直接输出Bye;然后到了fork1生成的子进程1,调用fork2,生成子进程1_1,子进程1中的fork1也返回0(子进程1是子进程1_1的父进程),直接Bye;最后进入子进程1_1,输出L2,在输出Bye.
fork6
void cleanup(void) {
printf("Cleaning up\n");
}
void fork6()
{
atexit(cleanup);
fork();
exit(0);
}
PS:C 库函数 int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func。您可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。
这里开始没注意,把cleanup函数放在了main函数后面,出了错,放在前面就好了。
父进程退出的时候执行一次Cleanup,子进程退出时也执行一次。
fork7
void fork7()
{
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
这里,父进程并没有退出,所以导致程序一直在运行,成为僵尸(zombies.)进程,一直在后台运行,具体表现如下,无法在终端创建新程序,只有Ctrl+C或者Ctrl+Z退出此程序才可以进行新的操作。
fork8
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
} else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
和前面的一样,父进程返回非0,所以输出PPID;然后进入子进程,输出PID,但是之后执行死循环,子进程并没有退出,成为zombies,浪费空间。
fork9
void fork9()
{
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}
PS:使用wait(&status)函数等待子进程信号,我们可以通过分析status来得到子进程的状态。
emmmm,又打错"}"位置了。。。。。。
这个程序使用了wait函数,就是等一会儿在运行wait后面的内容,那人家子进程还在排队等着呢,父进程要停一会儿就让人家运行呗。于是“HP”之后直接到子进程里面,输出“HC”之后直接退出;插了队,完事儿了就得让之前的继续嘛,于是父进程开始未完的事业,输出“CT。。。”,最后输出Bye。
fork10
#define N 5
/*
* fork10 - Synchronizing with multiple children (wait)
* Reaps children in arbitrary order
* WIFEXITED and WEXITSTATUS to get info about terminated children
*/
void fork10()
{
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
这个程序一直for循环,让每个子进程有了自己的状态值,相应的父进程一直在wait着子进程,当最深的子进程推出时,输出pid和状态值,之后再进行上一层的输出。
总结
linux创建一个新的进程是从复制开始的,在系统内核里首先是将父进程的进程控制块PCB进行拷贝,然后再根据自己的情况修改相应的参数,获取自己的进程号,再开始执行。我觉得整个过程重点就是理解子进程如何创建,在内核调用的几个重要的内核函数,以及子进程怎么返回开始执行的。