1、Linux系统的进程控制 在Linux系统中,进程控制的功能是由内核的进程控制子系统实现的,并以系统调用的形式提供给用户进程或其他系统进程使用。,1. 进程的创建与映像更换 系统启动时执行初始化程序,启动进程号为1的init进程运行。系统中所有的其他进程都是由init进程衍生而来的。除init进程外,每个进程都是由另一个进程创建的。新创建的进程称为子进程,创建子进程的进程称为父进程。 Unix/Linux系统建立新进程的方式与众不同。它不是一步构造出新的进程,而是采用先复制再变身的两个步骤,即先按照父进程创建一个子进程,然后再更换进程映像开始执行。,1) 创建进程 创建一个进程的系统调用是fo
2、rk()。创建进程采用的方法是克隆,即用父进程复制一个子进程。做法是:先获得一个空闲的PCB,为子进程分配一个PID,然后将父进程的PCB中的代码及资源复制给子进程的PCB,状态置为可执行态。建好PCB后将其链接入进程链表和可执行队列中。此后,子进程与父进程并发执行。父子进程执行的是同一个代码,使用的是同样的资源。它与父进程的区别仅仅在于PID(进程号)、PPID(父进程号)和与子进程运行相关的属性(如状态、累计运行时间等),而这些是不能从父进程那里继承来的。,fork()系统调用 【功能】创建一个新的子进程。 【调用格式】int fork(); 【返回值】 0 向子进程返回的返回值,总为0
3、0 向父进程返回的返回值,它是子进程的PID。 -1 创建失败。 【说明】若fork()调用成功,则它向父进程返回子进程的PID,并向新建的子进程返回0。 图4-7描述了fork()系统调用的执行结果。,图 fork系统调用的执行结果,从图中可以看出,当一个进程成功执行了fork()后,从该调用点之后分裂成了两个进程:一个是父进程,从fork()后的代码处继续运行;另一个是新创建的子进程,从fork()后的代码处开始运行。由fork()产生的进程分裂在结构上很像一把叉子,故得名fork()。 与一般函数不同,fork()是“一次调用,两次返回”,因为调用成功后,已经是两个进程了。由于子进程是从
4、父进程那里复制的代码,因此父子进程执行的是同一个程序,它们在执行时的区别只在于得到的返回值不同。父进程得到的返回值是一个大于0的数,它是子进程的PID;子进程得到的返回值为0。,若程序中不考虑fork()的返回值,则父子进程的行为就完全一样了。但创建一个子进程的目的是想让它做另一件事。所以,通常的做法是:在fork()调用后,通过判断fork()的返回值,分别为父进程和子进程设计不同的执行分支。这样,父子进程执行的虽是同一个代码,执行路线却分道扬镳。图4-8描述了用fork()创建子进程的常用流程。,图 用fork创建子进程,例1 一个简单的fork_test程序: #include main
5、() int rid; rid = fork(); if (rid 0 ) / 父进程分支 printf(“I am parent, my rid is %d, my PID is %dn”, rid, getpid(); else / 子进程分支 printf(“I am child, my rid is %d, my PID is %dn”, rid, getpid(); ,注:程序中的getpid()是一个系统调用,它返回本进程的进程标识号PID。 fork_test程序运行时,父子进程将会输出不同的信息,如父进程的输出可能是“I am parent, my rid is 8229, m
6、y PID is 8228”;子进程的输出可能是“I am child, my rid is 0, my PID is 8229”。由于两进程是并发的,它们输出信息的先后次序不确定,有可能父先子后,也可能相反。,2) 更换进程映像 进程映像是指进程所执行的程序代码及数据。fork()是将父进程的执行映像拷贝给子进程,因而子进程实际上是父进程的克隆体。但通常用户需要的是创建一个新的进程,它执行的是一个不同的程序。Linux系统的做法是,先用fork()克隆一个子进程,然后在子进程中调用exec(),使其脱胎换骨,变换为一个全新的进程。 exec()系统调用的功能是根据参数指定的文件名找到程序文件
7、,把它装入内存,覆盖原来进程的映像,从而形成一个不同于父进程的全新的子进程。除了进程映像被更换外,新子进程的PID及其他PCB属性均保持不变,实际上是一个新的进程“借壳”原来的子进程开始运行。,exec()系统调用 【功能】改变进程的映像,使其执行另外的程序。 【调用格式】exec()是一系列系统调用,共有6种调用格式,其中execve()是真正的系统调用,其余是对其包装后的C库函数。 int execve(char *path, char *argv, char *envp); int execl(char *path, char *arg0, char *arg1, . char *arg
8、n, 0); int execle(char *path, char *arg0, char *arg1, . char *argn, 0, char *exvp); ,【参数说明】path为要执行的文件的路径名,argv为运行参数数组,envp为运行环境数组。arg0为程序的名称,arg1argn为程序的运行参数,0表示参数结束。例如: execl(“/bin/echo”, “echo”,“hello!”, 0); execle(“/bin/ls”, “ls”, “-l”, “/bin”, 0, NULL); 前者表示更换进程映像为/bin/echo文件,执行的命令行是“echo hello
9、!”。后者表示更换进程映像为/bin/ls文件,执行的命令行是“ls -l /bin”。 【返回值】调用成功后,不返回,调用失败后,返回 1。,与一般的函数不同,exec()是“一次调用,零次返回”,因为调用成功后,进程的映像已经被替换,无处可以返回了。图4-9描述了用exec()系统调用更换进程映像的流程。子进程开始运行后,立即调用exec(),变身成功后即开始执行新的程序了。,图4 用exec更换子进程的映像,例2 一个简单的fork-exec_test程序: #include main() int rid; rid = fork(); if (rid 0 ) printf(“I am p
10、arentn”); else printf(“I am child, Ill change to echo!n”); execl(“/bin/echo”, “echo”, “hello!”, 0); /更换为echo ,Fork()返回后,父子进程分别执行各自的分支,父进程输出信息“I am parent”。子进程输出信息“I am child, Ill change to echo!”,然后调用exec(),变换为echo程序。echo随即开始执行并输出字符串“hello!”。,2. 进程的终止与等待 1) 进程的终止与退出状态 导致一个进程终止运行的方式有两种:一是程序中使用退出语句主动终
11、止运行,我们称其为正常终止;另一种是被某个信号杀死(例如,在进程运行时按Ctrl+c键终止其运行),称为非正常终止。关于信号的介绍见0节。 用C语言编程时,可以通过以下4种方式主动退出: (1) 调用exit(status)函数来结束程序; (2) 在main()函数中用return status语句结束; (3) 在main()函数中用return语句结束; (4) main()函数结束。,以上4种情况都会使进程正常终止,前3种为显式地终止程序的运行,后1种为隐式地终止。正常终止的进程可以返回给系统一个退出状态,即前2种语句中的status。通常的约定是:0表示正常状态;非0表示异常状态,不
12、同取值表示异常的具体原因。例如对一个计算程序,可以约定退出状态为0表示计算成功,为1表示运算数有错,为2表示运算符有错,等等。如果程序结束时没有指定退出状态(如后两种退出),则它的退出状态是不确定的。 设置退出状态的作用是通知父进程有关此次运行的状况,以便父进程做相应的处理。因此,显式地结束程序并返回退出状态是一个好的Unix/Linux编程习惯,这样的程序可以将自己的运行状况告之系统,因而能很好地与系统和其他程序合作。,2) 终止进程 进程无论以哪种方式结束,都会调用一个exit()系统调用,通过这个系统调用终止自己的运行,并及时通知父进程回收本进程。exit()系统调用完成以下操作:释放进
13、程除PCB外的几乎所有资源;向PCB写入进程退出状态和一些统计信息;置进程状态为“僵死态”;向父进程发送“子进程终止(SIGCHLD)”信号;调用进程调度程序切换CPU的运行进程。,至此,子进程已变为“僵尸进程”,它不再具备任何执行条件,只是PCB还在。保留PCB的目的是为了保存有关该进程运行情况的重要的信息,比如这个进程的退出状态、运行时间的统计、收到信号的数目等。子进程的最后回收工作由父进程负责。父进程收集子进程的信息后将其PCB撤销。如果某一个进程由于某种原因先于子进程终止,由它创建的子进程就成为孤儿进程。当系统中出现孤儿进程时,init进程将会发现并收养它,成为它的父进程。由于init
14、进程不会退出,所以所有的进程都会被收养。最后,在系统关机之前,init进程要负责结束所有的进程。,exit()系统调用 【功能】使进程主动终止。 【调用格式】void exit(int status); 【参数说明】status是要传递给父进程的一个整数,用于向父进程通报进程运行的结果状态。status的含义通常是:0表示正常终止;非0表示运行有错,异常终止。,3) 等待与收集进程 在并发执行的环境中,父子进程的运行速度是无法确定的。但在许多情况下,我们希望父子进程的进展能够有某种同步关系。比如,父进程需要等待子进程的运行结果才能继续执行下一步计算,或父进程要负责子进程的回收工作,它必须在子进
15、程结束后才能退出。这时就需要通过wait()系统调用来阻塞父进程,等待子进程结束。 当父进程调用wait()时,自己立即被阻塞,由wait()检查是否有僵尸子进程。如果找到就收集它的信息,然后撤掉它的PCB;否则就阻塞下去,等待子进程发来终止信号。父进程被信号唤醒后,执行wait(),处理子进程的回收工作。经wait()收集后,子进程才真正的消失。,wait ()系统调用 【功能】阻塞进程直到子进程结束;收集子进程。 【调用格式】int wait(int *statloc); 【参数说明】*statloc保存了子进程的一些状态。如果是正常退出,则其末字节为0,第2字节为退出状态;如果是非正常退
16、出(即被某个信号所终止),则其末字节不为0,末字节的低7位为导致进程终止的信号的信号值。若不关心子进程是如何终止的,可以用NULL作参数,即wait(NULL)。,【返回值】 0 子进程的PID。 1 调用失败。 0 其他。 图4描述了用wait()系统调用等待子进程的流程。,图4 用wait实现进程的等待,例3 一个简单的wait-exit_test程序: #include #include main() int rid, cid, status; rid = fork(); if ( rid 0 ) printf(“fork error!”); exit(1); if ( rid = 0
17、) printf(“I am a child. I will sleep a while.n”); sleep(10); / 睡眠10秒 exit(0); cid=wait(,printf(“I catched a child with PID of %d.n”, cid); if (status 此例的执行过程是:父进程在创建子进程失败时会用exit(1)退出。成功创建子进程后,父进程调用wait()阻塞自己;子进程运行,先输出信息,睡眠10秒后用exit(0)退出向父进程发信号,告之自己已结束。父进程被唤醒后,从wait()返回,根据获得的子进程的PID和退出状态判断子进程的运行情况并输出相应的信息,然后用exit(0)退出。,3. 进程的阻塞与唤醒 运行中的进程,若需要等待一个特定事件的发生而不能继续运行下去时,则主动放弃CPU。等待的事件可能是一段时间、从文件中读出的数据、来自键盘的输入、某个资源被释放或是某个硬件产生的事件等。进程通过调用内核函数来阻塞自己,将自己加入到一个等待队列中。阻塞操作的步骤是:建立一个等待队列的节点,填入本进程的信息,将它链入指定的等待队列中;将进程的状态置为睡眠态;调用进程调度函数选择其他进程运行,并将本进程从可执行队列中删除。,