Fork函数的运用:父进程通过调用Fork函数来创建一个新的子进程。
那么,什么是进程呢?进程的经典定义就是一个执行中程序的实例,即指程序的一次运行过程。程序是指按某种方式组合形成的代码和数据集合,代码即是机器指令序列,因而程序是一个静态的概念。不同于程序,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态的含义。
1.同一个程序处理不同的数据就是不同的进程。
2.进程是操作系统对cpu执行的程序的运行过程的一种抽象。进程会有自己的生命周期,它由于任务的启动而创建,随着任务的完成或终止而消亡,它所占用的资源也随着进程的终止而释放。
3.一个可执行目标文件即程序可被加载执行多次,也即,一个程序可能对应多个不同的进程。
引入进程是为了给应用程序提供以下两方面的抽象:
1.一个独立的逻辑控制流。每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执行过程中独占使用处理器。
2.一个私有的虚拟地址空间。每个进程拥有一个私有的虚拟地址空间,使得程序员以为自己的程序在执行过程中独占使用存储器。
进程的引入简化了程序员的编程以及语言处理系统的处理,即简化了编程、编译、链接、共享和加载等整个过程。
以上就是对进程的解释,下面回到Fork函数:
新创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用Fork函数时,子进程可以读写父进程中打开的任何文件。父进程和子进程最大的区别在于他们有不同的PID(PID是指process id 即进程标识符)。
那如何分辨程序是在父进程还是子进程中进行呢?
Fork函数只被一次调用却会有两次返回:一次是在调用父进程中,一次是在新创建的子进程中。在父进程中,Fork函数返回子进程的PID(大于0的数);在子进程中,Fork函数返回0。
如果你是第一次学习Fork函数,可以通过画进程图来帮助理解。进程图是刻画程序语句的偏序的一种简单的前趋图。每个顶点a对应于一条程序语句的执行。有向边a->b表示语句a发生在语句b之前。边上可以标注一些信息,例如一个变量的当前值。对应于printf语句的顶点可以标记上printf的输出(这里输出的都是hello便没有标记)。每张图从一个顶点开始,对应于调用main的父进程。这个顶点没有入边并且只有一个出边。每个进程顶点序列结束于一个对应exit调用的顶点。这个顶点只有一条入边没有出边。
下面用两个程序及运行结果来理解Fork函数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int x = 1;
if (fork() == 0)
printf("p1: x=%d\n",++x);
printf("p2: x=%d\n",--x);
exit(0);
}
进程图如下:
程序的运行结果如下:
此时子进程的输出是:p1:x=2 p2:x=1
父进程的输出是:p2:x=0
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int x = 3;
if (fork() != 0)
printf("x=%d\n",++x);
printf("x=%d\n",--x);
exit(0);
}
进程图如下:
运行结果如下:
注意到上图两次都是同样的代码,运行的结果却不一样,原因如下:父进程和子进程是并发独立进程,内核能够以任何方式交替执行他们的逻辑控制流中的指令。作为程序员,我们绝不能对不同进程中指令的交替执行做任何的假设。但是可以干预它们执行的顺序,可用sleep函数让父进程或子进程休眠一会或者用wait函数(下面会介绍)。
入手题目之前要先知道return与exit的区别:
1. exit用于结束正在运行的整个程序,它将参数返回给OS,把控制权交给操作系统;而return 是退出当前函数,返回函数值,把控制权交给调用函数。
2. exit是系统调用级别,它表示一个进程的结束;而return 是语言级别的,它表示调用堆栈的返回。
3. 在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程。
4. void exit(int status); 一般status为0,表示正常退出,非0表示非正常退出。
此区别转载于下面的链接,更多详情可参考下面的链接:
链接:https://blog.csdn.net/firefly_2002/article/details/7960595
以下还有几个题目判断会有几个“hello”的输出:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int i;
for(i=0;i<2;i++)
fork();
printf("hello\n");
exit(0);
}
进程图如下:
运行结果如下:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void doit()
{
if(fork() == 0)
{
fork();
printf("hello\n");
return;
}
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
进程图如下:
运行结果如下:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void doit()
{
if(fork() == 0)
{
fork();
printf("hello\n");
exit(0);
}
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
进程图如下:
运行结果如下:
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void doit()
{
fork();
fork();
printf("hello\n");
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
进程图如下:
运行结果如下:
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
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);
}
进程图如下:
运行结果如下:
getpid()函数的作用是返回调用进程的PID,getppid()函数的作用是返回它父进程的PID(创建调用进程的进程),在这个函数中能够很好地验证当fork()==0时,子进程会执行if里面的语句,用这些函数能够帮助我们理解在程序中父进程和子进程的运行过程。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
int main()
{
printf("L0\n");
if (fork() != 0) { //父进程会执行里面的语句
printf("L1\n");
if (fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n"); //所有进程都会执行
return 0;
}
进程图如下:
运行结果如下:
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
int main()
{
printf("L0\n");
if (fork() == 0) { //与上面程序相反这只有子进程会执行
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
return 0;
}
进程图如下:
运行结果如下:
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
void cleanup(void) {
printf("Cleaning up\n");
}
int main()
{
atexit(cleanup); //atexit注册了cleanup函数
fork();
exit(0);
}
运行结果如下:
atexit()函数是用来调用终止函数的,在此程序中执行到atexit()函数时并不会立即输出cleanup,而是会先执行fork函数,然后进程与父进程都会输出clesnup语句(先注册后调用)。
更多的相关atexit()函数的信息可参考下面博客:
链接:https://blog.csdn.net/chan0311/article/details/71023454
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
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+c强行终止,Ctrl+z将其挂起在后台运行):
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
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);
}
}
运行结果如下(这个程序会终止):
用ps命令可以查看进程的状态
以上两个程序差异性的原因:父进程是shell main创建的,上面的程序,父进程在死循环中出不来故一直不能出现shell命令行;下面的程序中父进程结束自然回到shell命令行中。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
int main()
{
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
}
else {
printf("HP: hello from parent\n");
wait(&child_status); //调用wait函数,等到子进程终止了就执行下一条语句
printf("CT: child has terminated\n"); //这条语句一定是在HC的后面出现
}
printf("Bye\n");
}
运行结果如下:
一个进程可以通过调用waitpid()函数来等待它的子进程终止或者停止。
pid_t waitpid(pid_t pid,int *statusp,int options); 当options=0时,waitpid会挂起调用进程的执行,直到它的等待集合中一个子进程终止,若等待集合中的一个子进程在刚被调用就已经终止了,该函数就会立即返回已终止进程的PID。此时,已终止进程已经被回收。其中,等待集合的成员是由第一个参数pid来确定的,若pid>0,则等待集合就是一个单独的子进程,他的进程ID等于pid;若pid=-1,那么等待集合就是由其父进程所有的子进程组成的。
pid_t wait(int *statusp);是waitpid函数的简化版本,调用wait(&status)等价于调用waitpid(-1,&statusp,0)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
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);
}
}
运行结果如下:
程序使用了waitpid函数,不按照特定的顺序等他N个子进程终止,在第一个for循环里面,父进程创建N个子进程,里面的子进程都以唯一的退出状态退出。在后一个循环里面,父进程调用wait函数,在每个子进程终止的时候,对wait函数的调用都会返回。在N次循环里面对检查N个子进程退出状态,若子进程是调用exit函数终止的,那么父进程提取出退出状态并把它输到stdout(标准输出设备上)。调用wait函数相当于调用第一个参数为-1的waitpid函数,它是等待所有里子进程最早终止。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
int main()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */ //非正常退出
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
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);
}
}
运行结果如下:
这个程序与上面程序的不同在于waitpid参数不同,下面这个程序会等进程ID等于pid的子进程正常退出然后输出语句。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
下面是一道企业面试题:
int main(void)
{
int i;
for(i=0; i<2; i++){
fork();
printf("*");
}
return 0;
}
这一题若是简单的按照上面画进程图的步骤很容易得出会输出六颗星,但是结合fork函数创建子进程的过程及输出的概念,就能知道这道实际上会输出八颗星。
运行结果如下:
printf若没有遇到\n(换行符)就会将输出存储在缓冲区,前面提到用fork函数创建子进程时,子进程能够得到父进程的代码和数据段、堆、共享库以及用户栈。所以把缓冲区里的数据也复制到子进程里,下面附图帮助理解:
要想要解决缓冲区这个问题 : 1.加上换行符\n,换行符能够输出行冲区里的数据并刷新缓冲区; 2.使用fflush函数去清空缓冲区;
下面试验第二种方法:
int main(void)
{
int i;
for(i=0; i<2; i++){
fork();
printf("*");
fflush(stdout);//此函数在stdio.h头文件里面,stdout是标准输出设备
}
return 0;
}
运行结果如下(只有六颗星):
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
最后上一道稍难的题目:假设下面这个程序编译后名字是main,请问这个程序执行后,系统总共会出现多少个main进程?
pid_t Fork(void){ //为了便于观察结果,我将fork()封装为Fork()函数,每个进程都将输出一颗星
pid_t pid;
if((pid=fork())>0) printf("*\n");
return pid;
}
int main(int argc,char *argv[])
{
Fork();
Fork()&&Fork()||Fork();
//A&&B||c
Fork();
}
运行结果(每颗星代表一个进程,共19颗星):
弄明白这道题首先要先能理解A&&B||C:若A是true则接着看B,若B是true,则跳过C
若A是true则接着看B,若B是false,则接着看C
若A是false则跳过B,接着看C
这里所说的true或false分别对应题目中的父进程和子进程。
为了区分五个Fork函数,第一个和最后一个分别用1和5表示,中间三个用分别用A、B、C表示,父进程图如下(共19个Fork):
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
水平有限,若有不足,欢迎指正!