fork()函数用于父进程创建一个新的运行的子进程
一、父进程与子进程的关系
子进程几乎但是不完全与父进程相同。
子进程得到与父进程用户级虚拟地址空间相同的(但是独立)的一份副本,包括代码、数据段、堆、共享库和用户栈。
子进程还获得与父进程任何打开文件描述符相同的副本,这意味着,当父进程调用fork()时,子进程可以读写父进程种打开的任何文件。
父进程和子进程最大的区别在于他们有不同的PID。
二、fork()的特性
我们先来看一个简单的代码
int main()
{
pid_t pid;
int x=1;
pid=fork();
if(pid==0){ /*Child*/
printf("child:x=%d\n",++x);
exit(0);
}
/*Parent*/
printf("Parent:x=%d\n",--x);
exit(0);
}
//例子ex1
运行之后的结果是
parent:x=0
child:x=2
1、调用一次,返回两次。
fork函数被父进程调用一次,但是却返回两次。一次返回到父进程中,一次返回到新创建的子进程中。在子进程中,fork的返回值为0。
2、并发执行
父进程和子进程是并发运行的独立进程。内核能够以任何方式交替执行他们的逻辑控制流中的指令。在我的电脑系统上运行上述的例子时,可以看到父进程先完成printf语句,接着才是子进程。然而,在另一个系统上可能正好相反,一般而言,作为程序员,我们绝不能对不同进程中指令的交替执行做任何假设
3、相同但是独立的地址空间
父进程和子进程会有相同的用户栈、相同的本地变量值、相同的堆、全局变量值和代码。所以当fork函数在第6行返回时,本地变量x无论在父进程还是子进程中都是1。然而,他们二者都是独立的进程,他们都有自己的私有地址空间,所以他们对x的所作的任何改变都是独立的,不会反应在另一个进程的内存中。这就是为什么最后的输出值不同。
4、共享文件
父进程和子进程他们的输出都显示在屏幕上。原因是子进程继承了父进程所有的打开文件。当父进程调用fork时,stdout文件是打开的,并指向屏幕,子进程继承了这个文件,因此他的输出也是指向屏幕的。
三、进程图
进程图时刻画程序语句偏序的一种简单的前驱图
每个顶点对应于一条程序语句的执行
每个有向边a—>b代表a发生在语句b之前
边上可以标记一些信息
ex1的进程图可表示为
再看一个进程图,帮助我们理解在嵌套情况下,fork调用程序的情况
int main()
{
fork();
fork();
printf("hello\n");
exit(0);
}
它对应的进程图
运行的结果和进程图是一样的
四、fork举例
①普通调用
#include "csapp.h"
int main()
{
int i;
for(i=0;i<2;i++)
Fork();
printf("hello\n");
exit(0);
}
int main()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
根据运行结果我们可以大致可以看出,这个结果还是有一定顺序可依的,先运行了上半部分,再运行了下半部分。但是如果fork次数多起来,结果会是什么样?
int mian()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
结果如下,这次的结果看着非常的混乱。我们贴出流程图进行一下对比,方便起见,图上序号代表执行的顺序。
从流程图可以看出,这次的例子看样子不再顺序执行。其实这是因为子进程和父进程是并行的,不一定执行哪个进程,执行到哪一步。所以顺序是混乱的,而且我标注的只是可能的情况之一,肯定存在其他执行的顺序。不过有一点可以确定的是,这看似没有顺序的表面下是存在规则的。那便是,一个进程也是要从前往后,绝不可能从后向前。比如,流程图中,9号L2是永远不可能比8号L1先执行的。 但是13号Bye却可以在14号L2前执行,因为二者并不属于一个进程(一共存在八个进程)。
②exit和return
void doit()
{
if(fork()==0){
fork();
printf("hello\n");
exit(0);
}
return ;
}
int main()
{
doid();
printf("hello\n");
exit(0);
}
接下来,如果我们把if语句中的exit换成return会是什么样呢??
我们会发现,输出的hello多了两个。exit代表着退出程序,说明不再执行这个程序了。而return知识返回上一层函数。所以当exit换为return后,我们可以看到,两次fork后产生的子进程执行return回到了main函数,执行了main中的printf,所以输出结果不同。
③printf与清除缓存fflush
int main()
{
int i;
for(i = 0; i < 2; i++)
{
fork();
printf("A");
}
wait(NULL);
return 0;
}
这是一个很普通的嵌套调用,大概想象一下,他应该输出6个‘A’,我们运行一下
运行结果好像不太对,程序居然输出了8个'A',这是为什么?
原因在于,printf("A")只是把A放进了进程的缓冲区,在进程结束后,才输出A。然而,fork创建子进程时,子进程连带着父进程的缓冲区一起复制了,所以在整个进程的执行过程中,进程缓冲区情况如下
但是,如果我们把输出加换行,或者及时用fflush清理缓存,及时输出,效果就和预想一样。有多少printf就有多少个A
int main()
{
int i;
for(i = 0; i < 2; i++)
{
fork();
printf("A\n");
//fflush(stdout);
}
wait(NULL);
return 0;
}
④等待waitpid和wait
int main()
{
if(fork()==0){
printf("a");
fflush(stdout);
exit(0);
}
else{
printf("b");
fflush(stidou);
waitpid(-1,NULL,0);//此处waitpid效果与wait相同,一直等待子进程结束
}
printf("c");
fflush(stdout);
exit(0);
}
我的系统下先运行了父进程,父进程执行wait一直等待子进程结束,才执行printf("c")。若是注释wait,父进程先输出,子进程后输出
接下来我们看一下wait和waitpid的对比
#define N 10
int main()
{
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);
}
}
可以看出来,被回收的子进程的顺序是杂乱的。这是因为当父进程调用wait(&child_status),只要任一一个子进程成为了僵尸进程就会将其回收,并不会指定某一个特定的子进程。所以回收进程的顺序杂乱。
如果我们把 wait(&child_status) 换成 waitpid(pid[i],&child_status,0) ,看一下运行结果
这下回收子进程的是有顺序的。这是因为waitpid函数的第一个参数pid[i]如果>0,则是指定某个子进程。所以顺序结束的子进程也会被顺序回收。
更多有关wait和waitpid的详细用法可以参考我的另一篇博客https://blog.csdn.net/qq_43135849/article/details/103050773
⑤exit
void cleanup(void) {
printf("Cleaning up\n");
}
/**
atexit函数是注册终止函数
即结束进程时调用注册的函数
*/
void fork6()
{
atexit(cleanup);
fork();
exit(0);
}
可以看到最后出现了两个Cleaning up,父子进程各自结束后都调用了cleanup函数
⑥信号
int main()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
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 terminated abnormally\n", wpid);
}
}
第一个for循环产生了N个无限不停止的子进程,和正常的N个父进程。第二个for循环父进程向子进程发送kill的信号,子进程被杀死。第三个for循环回收僵尸进程,可以看到结果子进程不是正常结束。
void int_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
exit(0);
}
int main()
{
pid_t pid[N];
int i;
int child_status;
signal(SIGINT, int_handler);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
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 terminated abnormally\n", wpid);
}
}
利用signal修改关联行为。signal原型函数是sighandler_t signal ( int signum , sighandler_t handler ) ;
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为
若 handler 为SIG_IGN | 忽略类型为signum的信号 |
若handler 为SIG_DFL | 类型为signum的信号行为恢复为默认行为 |
若handler 为函数地址 | 这个函数被称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。通过把处理程序的地址传递好signal函数从而该改变默认行为,这叫做设置信号处理程序 |
第三种是我们重点研究的对象。所以根据下面的结果可以看到这个例子当中子进程最后是执行了exit正常退出,而非上一个例子非正常退出。
我们再看两个关于pause的例子
int ccount = 0;
void child_handler(int sig)
{
int child_status;
int pid = wait(&child_status);
ccount--;
printf("Received SIGCHLD signal %d for process %d\n", sig, pid); /* Unsafe */
fflush(stdout); /* Unsafe */
}
int main()
{
int pid[N];
int i;
ccount = N;
signal(SIGCHLD, child_handler);
for (i = 0; i < N; i++) {
if ((pid[i] = fork()) == 0) {
sleep(1);
exit(0); /* Child: Exit */
}
}
while (ccount > 0)
;
}
从结果我们可以看到 最终只回收了一个子进程,这是因为子进程睡眠1s,父进程陷入了循环,不能回收全部的子进程。
但是,我们稍微改一下。把while循环的内容改为pause();,把关联函数中回收子进程的wait放入循环
int ccount;
void child_handler2(int sig)
{
int child_status;
pid_t pid;
while ((pid = wait(&child_status)) > 0) {
ccount--;
printf("Received signal %d from process %d\n", sig, pid); /* Unsafe */
fflush(stdout); /* Unsafe */
}
}
int main()
{
pid_t pid[N];
int i;
ccount = N;
signal(SIGCHLD, child_handler2);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
sleep(1);
exit(0); /* Child: Exit */
}
while (ccount > 0) {
pause();
}
}
现在我们可以看到,这个程序把所有的子进程全部回收。结果在于pause().pause()函数使该进程暂停让出CPU,但是该函数的暂停是可被中断的睡眠,也就是说收到了中断信号之后可以继续执行,所以父进程可以被唤醒从而回收子进程。
⑦逻辑运算符
一道很经典的关于fork的面试题,运行以下代码,除去main进程,一共会产生多少子进程。方便起见,每产生一个子进程便输出一个语句,利于观察。
int Fork()
{
printf("Child is here");
int pid;
pid=fork();
return pid;
}
int main()
{
Fork();
Fork()&&Fork()||Fork();
Fork();
exit();
}
我们先复习一下逻辑运算符
A&&B | 若A=ture,则检查B;若A=false,则不检查B |
A||B | 若A=true,则不检查B;若A=false,则检查B |
我们来看一下Fork()&&Fork()||Fork()这句话,为了解释,我们分别命名为Fork_1,Fokr_2,Fork_3。看一下每个Fork取值情况。
一共存在五种情况,在这两条语句之前分别有一句Fork,所以除去main进程,一共有5*2*2-1=19个,我们来运行验证一下
⑧死循环
int main()
{
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 */
}
}
运行后可以发现,因为父进程陷入了死循环,程序一直无法结束。所以我们用Ctrl+Z停止进程,再通过ps命令查看进程情况
可以看到,子进程23110已经执行了exit,但是还没有被回收,变成了僵尸进程,这都是因为父进程23109处于死循环状态,无法回收子进程。只能手动杀死父进程23109
我们再来看一个例子
int main()
{
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);
}
}
它的运行结果就不太一样了
这下我们可以看到,程序会自动结束,不像上一个例子程序陷入死循环需要Ctrl+Z强制停止。这是因为在这个例子中,子进程陷入里死循环,父进程退出。子进程变成了孤儿进程,此时系统把init进程(所以进程的父进程)设为子进程的养父进程,代替父进程执行工作,收集子进程的信息。
⑨进程组
我们可以简单查一下进程组组号
int main()
{
if (fork() == 0) {
printf("Child1: pid=%d pgrp=%d\n",
getpid(), getpgrp());
if (fork() == 0)
printf("Child2: pid=%d pgrp=%d\n",
getpid(), getpgrp());
while(1);
}else printf("Parent pid = %d ",getpid());
}
可以这样理解一下,父进程的pid最为进程组的组号
int main()
{
if (fork() == 0) {
printf("Child: pid=%d pgrp=%d\n",
getpid(), getpgrp());
}
else {
printf("Parent: pid=%d pgrp=%d\n",
getpid(), getpgrp());
}
while(1);
}