- 进程的描述
通俗的讲,进程就是正在执行的程序或代码。我们知道,程序本身就是一堆代码,开始的时候存储在磁盘上,这时它是静态的、无生命的;只有当程序的代码被加载到内存中,代码才有了生命,才能被CPU动态的执行。
问题是,现在的操作系统可以并行的执行多个程序,也就是内存中同时存放着多个程序的代码,为了方便管理,必须要合理的组织它们。方式就是由操作系统给每段代码添加一些元数据,这些元数据就是PCB,即任务控制块。
不难理解的是,每个程序的代码实际上可以分为两部分:指令的数据。指令就是程序代码规定的各种操作;数据就是这些操作的对象。一个程序可以多次被加载到内存成为多个进程,比如同时打开两个vim分别编辑不同的文件。那么问题是:这时候有必要在内存中同时存放多个该程序的指令copy吗?答案是不必的。指令部分被设置为只读且允许系统中正在运行的两个或多个进程之间能够共享这一代码;而数据部分则被各个进程私有,不可以共享,比如每个vim都只能编辑自己的文件。
那么PCB里面都有些什么呢?
- 进程id。系统中每个进程有唯一的id,在C语言中用 pid_t 类型表示,其实就是一个非负整数。
- 进程的状态,有运行、挂起、停止、僵尸等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(Current Working Directory)。
- umask 掩码。
- 文件描述符表,包含很多指向 file 结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 控制终端、Session和进程组。
- 进程可以使用的资源上限(Resource Limit)。
可见,操作系统为了控制进程,PCB的结构还是挺复杂的。
- 进程的创建
经常听说“创建一个进程”,这到底是怎么回事呢?首先能想到的是,进程不是孙猴子,不可能自己蹦出来,肯定是别人“生”的。Linux中,进程是由父进程创建的,准确的说,是父进程中的代码的指令部分主动使用了创建进程的函数fork(),然后一个子进程就被“生”了出来。fork函数如何工作的呢?由于每个进程都有一个PCB,所以它首先要跟操作系统申请一个PCB(PCB是有限的),然后分配新进程内存,接着copy父进程的代码。实际上,fork就是很懒的复制了一下父进程,也就是说,fork函数调用过程中,内存中会会出现两个几乎一模一样的进程,当然除了进程号(它是唯一的)。进程的复制完成后,两个进程都有一个fork函数等待返回(注意,是返回,因为fork函数本身也是一条一条的代码,前面的一部分完成复制功能,子进程出现后,就到了返回的那部分代码了),而它们的返回结果是不同的(操作系统来控制返回结果):父进程中的fork返回子进程的pid;而子进程中的fork返回0;如果fork失败,返回的是-1。
fork只是创造出了两个几乎一样的进程,它们运行的是同样的代码,这和一开始说的可不一样,因为我们创造一个新进程大多是用来执行新程序代码的。这时我们需要exec类函数,当进程调用一种exec函数时,该进程的程序代码完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。exec 系统调用执行新程序时会把命令行参数和环境变量表传递给 main 函数。环境变量表是进程所处系统环境的一个描述,一段代码要正常执行必然要使用各种系统资源,环境变量表就是对它的一个抽象。但是,exec类函数是需要显式调用的,子进程不会主动加载新的程序代码!所以,一般是在父进程的代码中,根据fork的返回值写一个分支,子进程的分支中显式调用exec。
- 进程的终止(参考:Linux C一站式编程)
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,操作系统在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量 $? 查看,因为Shell是它的父进程,当它终止时Shell调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用 wait 或 waitpid 对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了。
如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为 init 进程。 init 是系统中的一个特殊进程,通常程序文件是 /sbin/init ,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止, init 就会调用 wait 函数清理它。
僵尸进程是不能用 kill 命令清除掉的,因为 kill 命令只是用来终止进程的,而僵尸进程已经终止了。所以一颗可行的办法是,kill其父进程。
wait 和 waitpid 函数的原型是:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用 wait 或 waitpid 时可能会:
+ 阻塞(如果它的所有子进程都还在运行)。
+ 带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
+ 出错立即返回(如果它没有任何子进程)。