我们开始第八章啦,process control。要说UNIX的进程控制,当然首先说进程的创建、执行和终止啦,对应的也就是fork, exec, exit。那么我们首先来说fork。
要说fork,当然先看定义啦:
#include <unistd.h> pid_t fork(void); /*Returns: 0 in child, process ID of child in parent, −1 on error*/其实我们从定义看不出来什么,只是知道如果返回0那就代表当前执行的是子进程,返回process ID(>0)代表当前执行的是父进程,返回-1当然是错误啦。其实,初学者(包括我)看定义,只是模糊的知道个概念,所以细节,就要看如下代码啦(fork代码1):
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; char *message; int n; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { message = "This is the child\n"; n = 6; } else { message = "This is the parent\n"; n = 3; } for(; n > 0; n--) { printf("%s", message); sleep(1); } return 0; }往往初学者的困惑就在if(pid == 0)之后,前面的很容易懂,我提两点:
(1) pid_t是fork返回的格式,用就行了; (2) pid<0的话,说明子进程没创建成功,当然相应的errno也会被设置。
那好,要想懂后面的东西,直接下面的运行过程图:
我们没有直接看运行结果,直接来看过程图,就是为了通过分析过程来导出结果,最后再验证结果对不对。我们来看分析:
(1) 父进程初始化,它有一个非负整型的唯一ID,有PCB(包括ID,进程状态,内存信息,fd table等等)。
(2) 执行到了fork,那么该进程就会创建它的子进程。怎么创建呢?子进程获得父进程数据空间、堆、栈的copy,父子进程不共享这些东西,那共享什么呢?共享.text段(当然在后面file sharing也会共享file tables,先不提这个)。那子进程是不是在创建时立即获得父进程的这些信息的copy呢?不是,有种Copy-On-Write(COW)技术,初始exec时把这数据段、堆栈设为只读并对父子进程共享,谁要修改这些值的时候再copy。
或者可以这样说,这个时候父子进程看起来完全一样,fork刚进kernel,还没有返回;
(3) “调用一次,返回两次”。来吧,现在有两个一模一样的进程都等待fork从kernel中返回。先返回哪个呢?不知道。这个取决于内核的调度算法。我们知道的是,如果fork返回了值,那么它给父进程返回的是刚创建的子进程ID(>0),给子进程返回的是0;
(4) 我们说了,哪个先返回,哪个先执行,取决于调度算法,我们不知道。好吧,那就假设(!)父进程先返回吧!那么它当然执行else里面的东西:初始化message = "This is the parent"; n = 3; 好啦,这个过程其实很快,我们假设(!)子进程还没返回,那么就继续执行,进入for语句,执行第一个printf,完毕后sleep(1)。
(5) 那好,这1秒钟对计算机来说很长了,假设(!)这时候子进程的pid==0也返回啦。在父进程等待的1秒钟里,它执行到printf,并也是sleep(1)。
(6) 所以,结果很有可能是这样的:父子进程交替执行,直到结束。
我们有很多假设(!),所以执行顺序也有很大的不确定性。来,看实际运行的结果:
$ ./a.out This is the child This is the parent This is the child This is the parent This is the child This is the parent This is the child $ This is the child This is the child啊~差不多吧,只是在这个结果中,fork先返回了子进程。但是!哎?倒数第二行是怎么回事?那个$在那里怎么那么碍眼。原因嘛,是这样的:
首先,这个程序是在shell下运行的吧?那么这个parent的父进程是谁?对啦!是shell!parent执行完最后一个printf之后,当然要exit,虽然此时它的子进程还没有执行完呢,但shell可不知道这个最小的子进程的存在,所以就直接输出$符号啦!(其实这个时候shell又可以接受其它命令正常工作了)但这个失去了爸比的子进程该怎么办呢?它被init(pID=1)收养(inherited)啦,我们后边会说到这个“孤儿院”进程和另外一个重要的swapper。如果此时打印这个被收养的子进程的父ID(getppid),就会发现它是1啦!
那么,OK,fork代码1我们分析完毕。
好困啊,回去睡觉去,明天接着写。