fork()函数
fork()的基础知识
- 父进程通过调用fork函数来创建一个新的运行的子进程。
- 父进程和子进程之间最大的区别就是PID不同
1)在父进程中,fork返回新创建子进程的PID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值
fork()的特点
-
调用一次,返回两次
一次只在调用进程(父进程)中,fork返回子进程的PID。
一次是在新创建的子进程中,fork返回0。
-
并发执行
父进程和子进程是并发运行的独立进程。
内核能够以任意方式交替执行他们的逻辑控制流中的指令。
-
相同但是独立的地址空间
父进程和子进程会有相同的用户栈、相同的本地变量值、相同的堆、全局变量以及代码。
但是,父进程和子进程都是独立的进程,他们都有自己的私有地址空间。
-
共享文件
子进程可以读写父进程中打开的任何文件
有关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);
}
运行结果和流程图:
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
运行结果和流程图:
关于输出结果的顺序
对于运行在单处理器上的程序,对应所有原点的拓扑排序表示程序中语句的一个可行全序排列。
排列是 拓扑排序 ,当且仅当画出的每条边的方向都是从左到右的。
例如:
拓扑为:
->x=a
->x=b->x=c
那么输出:
abc,bac,bca都是有可能的
2.调用exit()和return时不同的输出
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
void doit()
{
if(fork()==0){
fork();
printf("hello\n");
exit(0);
}
return ;
}
int main()
{
doid();
printf("hello\n");
exit(0);
}
运行结果和流程图:
输出了3个hello,但是当我们doit函数中的 exit 更改为 return 时,让我们再来看下运行结果:
输出了5个hello,比之前多了2个,为啥会出现这种情况呢?
原因就是:
使用exit函数就结束了doit函数中创建的进程,而没有返回到main函数中,也就执行不了main函数中的printf语句。
而使用return则是退出if语句,返回到main函数中,故doit函数中创建的进程也会继续执行main函数中的printf语句。
3.使用waitpid函数
在介绍waitpid函数之前,我们先来了解一下有关 回收子进程 的相关知识
回收子进程
- 当一个进程由于某种原因终止时,内核并不是立即把他从系统中清除。相反,进程被保持一种已终止状态中,直到被他的父进程回收。一个终止了但还未被回收的进程称为僵死进程。
- 即使僵死子进程没有运行,他们仍然消耗系统的内存资源。
- 如果父进程没有回收它的僵死进程就终止了,那么内核会安排init进程去回收他们。
- 但是,一些长时间运行的程序,例如shell或服务器,总应该回收他们的僵死子进程,不然会对系统造成不小的负担。
所以,如何使我们的父进程来等待它的子进程终止或者停止,来将僵死子进程所占用的内存资源释放掉呢?
这时就需要我们的waitpid函数登场了,首先我们先来看下他的函数结构
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
我们这里仅对第一个参数pid进行解释,另外两个请大家自行百度
- 如果pid>0,那么等待集合就是一个单独的子进程,它的进程ID等于pid。
- 如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。
- 我们可以通过控制pid的值来指定子进程进行回收,这也是与wait函数的区别。大家可以自行运行一下代码中的fork10和fork11观察下输出有何不同。(代码会在文章最后给出)
我们来看一个代码实例
void fork9()
{
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
waitpid(-1, &child_status, 0);//等价于wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}
运行结果和流程图:
因为我们在父进程中加入waitpid函数,所以父进程会等待子进程结束再继续运行,所以输出结果只能是HP;HC;CT;Bye或是HC;HP;CT;Bye。
突然发现fflush函数忘记说,好像还挺重要的,以后再说吧。
既然说到waitpid 函数就简单讲一下wait函数吧
wait函数其实就是waitpid函数的简单版本:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statusp);
调用wait(&status)等价于调用waitpid(-1, &status, 0)。
4.使用fflush清除缓存
我们先来看一下fflush的函数结构
#include<stdio.h>
int fflush(FILE * stream);
函数说明:
- fflush()会强迫将缓冲区内的数据写回参数stream指定的文件中,如果参数stream为NULL,fflush()会将所有打开的文件数据更新。
返回值:成功返回0,失败返回EOF,错误代码存于errno中。
- fflush()也可用于标准输入(stdin)和标准输出(stdout),用来清空标准输入输出缓冲区。
stdin是standard input的缩写,即标准输入,一般是指键盘;标准输入缓冲区即是用来暂存从键盘输入的内容的缓冲区。
stdout是standard output 的缩写,即标准输出,一般是指显示器;标准输出缓冲区即是用来暂存将要显示的内容的缓冲区。
我们在fork函数中要注意的也就是输入输出缓冲区。
来,让我们看一个简单的嵌套循环的代码实例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i;
for (i=0; i<2; ++i) {
fork();
printf("A");
//fflush(stdout);
//printf("\n");
}
return 0;
}
输出结果:
嗯?不应该是6个A吗,为什么这里多了两个A?
让我们看一下流程图:
这里多出两个A的原因就是:
printf为标准输出(STDOUT),是有数据缓冲区的,遇到\n就认为一句完成即输出。
当fork()创建子进程时,子进程复制了父进程的数据段和堆栈段,包括printf的数据缓冲区也被复制。
所以fork()之后,printf打印的A放在缓存区中,等i=1时在fork()缓存区中的值被复制到两个子进程中了,就多了两次A打印。
所以流程图应该是这样:
所以我们可以使用 fflush(stdout) 或者加入 \n 来使缓冲区的数据输出,这样就可以得到我们想要的6个A的结果了。