学习了一段时间的APUE,不得不说确实是本好书,但是我进步很慢,一直很苦恼。某人指点我说应尝试把自己学到的东西写出来,这样会有很大的帮助。所以从今天起尝试一下,希望能够坚持。
下面开始吧。
fork 函数可以用来为一个现有进程创建一个子进程。用fork 函数创建的子进程是父进程的副本,它会copy一份父进程所有的数据空间、堆和栈等,但是子进程和父进程并不共享存储空间,仅共享正文段。下面我们看一下下面的例子。
#include "apue.h"
int glob=6;
char buf[]="a write to stdout\n";
int main(void)
{
int var;
int pid;
var=88;
if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
{
err_sys("write error!\n");
}
printf("before fork!\n");
if((pid=fork())<0)
{
err_sys("fork error!\n");
}
else if(pid==0) // this is child process for pid == 0
{
glob++;
var++;
}
else //this is parent process
{
sleep(2); //wait for child finishing
}
printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);
exit(0);
}
执行这段程序:
[root@localhost APUE]# ./fork
a write to stdoutbefore fork!
pid=13848,glob=7,var=89
pid=13847,glob=6,var=88
正如书中所说,fork 的返回值在父进程和子进程中不同。在父进程中,fork 函数返回所创建的子进程的进程号,在子进程中则返回0.之所以要在父进程中返回子进程ID是因为在父进程中并没有一种方法能够获得所有子进程的进程ID,而在子进程中却可以始终通过getppid 函数来获取父进程的ID。
在回到程序中可以注意到一下几点:
1.通过fork 返回的pid 是否为0可以区分父进程和子进程。并且在子进程中修改变量(无论全局变量或局部变量)均不会对父进程造成影响,反过来也是。
2.fork 之后是父进程先执行还是子进程先执行是不确定的,取决于内核使用的调度算法。本例中在父进程中通过sleep(2) 让父进程睡眠,等待子进程执行完毕。在实际应用中还有诸多其他方式保证父子进程同步。
3.将标准输出重定位定位到一个文件(如temp.out)时,执行结果如下:
[root@localhost APUE]# ./fork > temp.out
[root@localhost APUE]# cat temp.out
a write to stdout
before fork!
pid=17896,glob=7,var=89
before fork!
pid=17895,glob=6,var=88
通过观察上面的输出我们可以看到,write 只输出了一次, printf 输出了两次。想要分析这个现象的原因,就得说说Unix 的缓冲机制。众所周知,Unix 的write,read等等操作是不带缓冲的,因此在fork 之前调用write ,终端只输出一次。但printf 函数在向终端输出的时候是行缓冲,在向文件输出的时候是全缓冲。因此在向终端输出时,因为printf 的内容以‘\n’结尾,因此缓冲区经换行符冲洗变为空,而printf 像文件输出时为全缓冲,在printf后其内容依然留在了缓冲区中,在fork 子进程时缓冲区连同其中的数据一同复制到子进程中,最终在程序最后的exit 函数的作用下冲洗缓冲区的副本进行输出。
所以当把printf 函数里面内容的‘\n’去掉,再次执行程序,便会得到如下结果:
[root@localhost APUE]# ./fork
a write to stdout
before fork!pid=4935,glob=7,var=89
before fork!pid=4934,glob=6,var=88
关于这个问题,昨天和同宿舍的人吃饭,他给我出了一个关于fork 的题目。当时在喝二锅头,晕晕的没搞明白,今天试着回想了一下,然后写出来了,也放在这里谈论一下吧。
#include"apue.h"
int main()
{
int i;
int pid;
for(i=0;i<=1;i++)
{
if((pid =fork()<0))
{
err_sys("fork error!\n");
}
else
{
printf("-");
}
}
exit(0);
}
问我应该输出多少个 ‘-’ ,答案是8个。确实已经晕了,由于没答上来,就自罚了一口(真无聊啊我们。。。)。
刚我简单在纸上画了画,终于画明白了。描述一下我的理解吧,不知道正确不正确。
(1)这个程序最开始只有一个父进程P0,输出缓冲区是空的。
(2)在执行了第一个fork 之后,创建了一P0的子进程P0_1,它的缓冲区是复制的P0的缓冲区,因此也是空的。
(3)在P0和P0_1中分别执行了一次 print ,由于没有换行符,所以缓冲区并没有被冲洗,因此现在他们的缓冲区中各有一个 ‘-’ 。
(4)现在开始执行第二个 fork ,第二个fork 为P0和P0_1分别创建了一个子进程。P0的第二个子进程为P0_2,其缓冲区复制P0,内包含一个 ‘-’;P0_1的子进程为P0_1_1,其缓冲区复制P0_1的缓冲区,因此内包含一个‘-’。截止到目前,四个进程的缓冲区中均包含一个‘-’。
(5)接下来所有进程再一次的执行printf ,向各自的缓冲区在写进一个 ‘-’,因此每个进程的缓冲区中均有2个 ‘-’。
(6)在 exit 的冲洗下,所有缓冲区的内容输出到屏幕,得到8个‘-’。
对程序稍微做一下修改,把最后一句 exit(0) 改为 _Exit(0),程序什么也不输出了,考虑到两者区别,确实验证了我的想法。
再对程序做一下修改,把其中的printf("-") 改为printf("-\n"),程序会输出什么呢?让我们来分析一下,多了‘\n’相当于每次都冲洗了缓冲区,缓冲区中的数据会马上输出到终端,因此在执行第一个fork 的时候,P0和P0_1两个进程各打印出一个 ‘-\n’ ,执行第二个fork 后四个进程再各打印一个 ‘-\n’,这样算下来就一共有六个 ‘-\n’.并且和用exit或者_Exit结束程序都没关系。
4.关于文件共享。父进程中的所有文件描述符也都复制到子进程中,父、子相同打开文件描述符共享一个文件表项,因此父、子进程共享一个文件偏移量。如果父子进程没有同步操作的话,这种共享文件偏移量的形式会造成父子进程输出混在一起。因此fork之后处理文件描述符一般有两种情况:
(1)父进程等待子进程完成,比如第一个例子中用的sleep 等方式。
(2)父子进程执行不同的程序段,比如用if 区分pid是否为0 的那种方式,在不同的程序段中处理不同的文件描述符。