摘要:本文详解介绍fork()函数的基本使用,以及父子进程之间的关系.子进程对变量的改变不会影响到父进程、子进程对父进程文件流缓冲区的处理和子进程对父进程打开的文件描述符的处理.
创建进程
1.fork()函数
函数定义:#include <unistd.h>
pid_t fork(void);
返回值:如果返回值大于零,表明处于父进程上下文环境中,返回值是子进程的ID.如果返回值是零,表明处于子进程上下文环境中.其他返回值(小于零)表明调用fork()函数出错,仍处于父进程上下文环境中.
函数说明:
由fork()函数创建的新进程被称为子进程.fork()函数被调用一次,但返回两次,两次的返回值不同,子进程的返回值是0,父进程的返回值是新进程的进程ID.
一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID.
一个进程只会有一个父进程,所以任意一个子进程都可以通过调用getppid()函数获取其父进程的进程ID.
fork()函数调用成功后,将为子进程申请PCB和用户内存空间.子进程是父进程的副本.在用户空间将复制父进程用户空间所有数据(代码段、数据段、BBS、堆、栈),复制父进程内核空间PCB中的绝大多数信息.子进程从父进程继承下例属性:有效用户、组号、进程组号、环境变量、对文件的执行时关闭标志、信号处理方式设置、信号屏蔽集合、当前工作目录、根目录、文件模式掩码、文件大小限制和打开的文件描述符(特别注意:共用同一文件表项).
子进程在创建后和父进程同时执行,竞争系统资源,谁先谁后,取决于内核所使用调度算法.子进程的执行位置为fork返回位置.
2.创建子进程
例子1:演示fork函数的基本使用方法.#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid;
if((pid=fork())==-1)
printf("fork error\n");
printf("a example of fork,pid = %d\n",getpid());
return 0;
}
输出:
:a example of fork,pid = 2798:a example of fork,pid = 2797
从例子1可以看出,fork()函数后的代码在子进程中也被执行.实际上,其他代码也在子进程的代码段中,只是子进程执行的位置为fork返回位置,其之前的代码无法执行罢了.
例子2:返回值大于0(返回PID)的代码在父进程执行,返回值为0则在子进程执行.
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
if((pid=fork())==-1)
{
printf("fork error\n");
}
else if(pid == 0 )
{
printf("pid:%d in the child process \n",pid);
}
else
{
printf("pid:%d in the parent process\n",pid);
}
return 0;
}
输出:
:pid:2923 in the parent process:pid:0 in the child process
3.子进程对变量的改变不会影响到父进程
例子3:子进程和父进程各有一份变量的副本#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int glob = 10;
int main()
{
int var = 100;
pid_t pid = getpid();
printf("before fork:\n");
printf("pid=%d, glob=%d, var=%d\n",pid,glob,var);
printf("after fork:\n");
if((pid=fork())<0)
{
printf("error fork:%m\n");
exit(-1);
}
else if(pid==0)
{
glob++;
var++;
}
else
{
sleep(2);
}
printf("pid = %d, glob = %d, var = %d\n",getpid(),glob,var);
return 0;
}
输出:
:before fork::pid=2664, glob=10, var=100
:after fork:
:pid = 2665, glob = 11, var = 101
:pid = 2664, glob = 10, var = 100
可以看出,对于变量glob和var,在子进程中进行了自加,但是在父进程中,变量的值没有改变;显然,父子进程各自拥有这一变量的副本,互不影响.
4.子进程对父进程文件流缓冲区的处理
文件流缓冲区的资源位于用户空间,因此,在创建子进程时,子进程的用户空间将复制父进程的用户空间所有信息,显然,也包含流缓冲区的内容.如果留缓冲区中有临时的信息,则通同样复制到子进程的用户空间流缓冲中.例子4:子进程对父进程文件流缓冲区的处理.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
//有回车,先输出
printf("before fork,have enter\n");
//没有回车,先输出到缓冲区
printf("before fork,no enter:pid=%d\t",getpid());
pid = fork();
if(pid == 0)
{
printf("\nchild,after fork:pid=%d\n",getpid());
}
else
{
printf("\nparent,after fork: pid=%d\n",getpid());
}
return 0;
}
输出:
:before fork,have enter:before fork,no enter:pid=2977
:parent,after fork: pid=2977
:before fork,no enter:pid=2977
:child,after fork:pid=2978
为什么“before fork,have enter”只输出一次?
首先明确一点,如果标准输出连到终端设备,则它是行缓冲或者全缓冲.“before fork,have enter”只输出一次,是因为标准输出缓冲区由换行符冲洗,在下面创建子进程时,复制的缓冲区已经没有该数据了.
为什么“before fork,no enter:pid=2977”输出两次?
子进程和父进程都输出了“before fork,no enter:pid=2977”,但是,为什么子进程会输出呢?子进程开始执行的位置是在fork函数返回处,不会执行fork函数之前的代码,虽然该代码被复制到子进程中.之所以出现两次输出,是因为父进程中的“printf("before fork,no enter:pid=%d\t",getpid());”没有回车,就是这条语句输出的结果还在缓冲区,缓冲区没有冲洗,没有真正输出.在创建子进程时,这父进程的缓冲区也会被复制到子进程的进程空间中,所以子进程在输出时,刷新缓冲区时,也会将“printf("before fork,no enter:pid=%d\t",getpid());”的结果输出.
如果程序是这样子运行 ./a.out >out.txt.查看out.txt文件,可得到以下结果.
before fork,have enter
before fork,no enter:pid=2716
parent,after fork: pid=2716
before fork,have enter ====》多了这一行
before fork,no enter:pid=2716
child,after fork:pid=2717
为什么?“before fork,have enter”输出了两次.
标准输出重定向到文件时,在调用fork函数时,该数据还在缓冲区,没有输出,这里的换行符没有像上面那样起到刷新缓冲区的作用,只是简单的换行,所以当父进程将数据复制到子进程时,该缓冲区的数据也被复制到子进程了.
5.子进程对父进程打开的文件描述符的处理
fork函数创建子进程后,子进程将复制父进程的数据段、BBS段.代码段.堆空间、栈空间和文件描述符,而对于文件描述符关联的内核文件表项,则是此采用共享的方式.例子5: 子进程对父进程打开的文件描述符的处理
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
pid_t pid;
int fd;
int i=1;
int status;
char *ch1="advanced";
char *ch2=" programming";
char *ch3=" int the unix Environment";
fd = open("test.txt",O_RDWR|O_CREAT,0644);
if(fd==-1)
{
printf("open or creat file error:%m\n");
exit(-1);
}
write(fd,ch1,strlen(ch1));
pid=fork();
if(pid==-1)
{
printf("error fork\n");
exit(-1);
}
else if(pid==0)
{
i=2;
printf("in child process\n");
printf("i=%d\n",i);
if(write(fd,ch2,strlen(ch2))==-1)
{
printf("child write error:%m\n");
exit(-1);
}
}
else
{
sleep(1);
printf("int parent process\n");
printf("i=%d\n",i);
if(write(fd,ch3,strlen(ch3))==-1)
{
printf("parent wirte error%m\n");
exit(-1);
}
wait(&status);
}
return 0;
}
输出:cat test.txt
:advanced programming int the unix Environment可以看出,父子进程共同对一个文件操作,且写入数据不交叉覆盖,说明父子进程共享同一个文件偏移量,共享文件表项.如图1子进程对打开文件的处理方式.

图1 子进程对打开文件的处理方式