上一章讲到程序的状态,以及调度的实质,这一章主要将程序的在运行过程中的具体执行过程,包括进程的创建,终止。
进程的运行
进程的创建
进程在执行的过程中可以创建多个新的进程。创建进程成为父进程,而新的进程成为子进程。每个子进程又可以创建其他进程,从而形成进程树。下图为一个Linux系统的典型的进程树。
在linux 下可以通过 pstree 查看进程树。
linux_process_tree.png
父子进程
一般来说,当一个进程创建子进程时,该子进程会需要一定的资源来完成任务。子进程可以从操作系统那里直接获取资源,也可以只从父进程里获得。父进程可能在子进程之间非配资源或者共享资源(主要是打开的I/O设备,也有可能是内存)。
在进程创建新进程时,可能有两种情况:
- 父进程与子进程并发进行。
- 父进程等待,直到某个子进程执行完成。
新进程的地址空间也有两种可能:
- 子进程是父进程的复制品。
- 子进程加载另外一个程序。
这里看下Unix和Linux的具体实现吧。
每个进程都有一个唯一的整型进程标识符来标识。进程是通过fork()系统调用来创建的。每个进程的地址空间复制了原来的父进程的地址空间。这种机制允许父子进程之间可以轻松的通信。
这两个进程都继续执行fork()之后的指令。但是对于子进程,系统调用fork()的返回值是0,对于父进程,返回的是子进程的进程标识符。(具体实现参考《linux 内核源码剖析》) 根据返回返回值的不同对父子进程进行不同的操作。
通常fork()系统调用之后,子进程一般会使用exec()系统调用,以新的程序来进行替换新进程的内存空间,系统调用exec()加载二进制文件到内存中(破坏包含系统调用exec()的原来进程内存内容),并开始执行新的进程。
父进程能够创建更多的子进程,或者如果在子进程运行时没有什么可做,那么他就会使用系统调用*wait() * 把自己移出就绪队列,等待子进程完成,这里是任意一个子进程。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid;
// 调用fork系统调用创建新的进程
pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork Failed.\n");
return 1;
} else if (pid == 0) {
printf("I am child process.\n");
execlp("/bin/ls", "ls", NULL);
} else {
wait(NULL);
printf("I am parent process.\n");
}
return 0;
}
执行命令: ./fork
fork.png
进程的终止
当进程完成执行最后语句并通过系统调用exit()请求操作系统删除自身时,进程终止。这时,进程可以返回状态值到父进程(如果父进程调用了wait())。所有进程资源,如物理和虚拟内存,文件表,I/O缓冲区等,都由操作系统释放。
在其他情况下,进程也有可能出现进程终止。进程通过适当的系统调用,可以终止另一个进程。通常,只有终止进程的父进程才能执行这一系统调用,否则用户可以任意终止彼此的进程。
父进程终止子进程的原因有很多,如:
- 子进程使用了超过他分配的资源
- 分配给子进程的任务,不再需要
- 父进程正在退出,而且有些系统不支持无父进程的子进程继续运行。
第三种的情况,是在某些系统中存在,那么,父进程创建的所有的子进程都应该终止,比如关机,这种株连九族的现象叫做 级联终止,一般由操作系统来启动。
在Unix和Linux系统中,父进程可以通过系统调用wait() 等待子进程的终止,系统调用wait()可以通过参数,让父进程获取子进程的终止状态;wait() 也返回终止子进程的标识符,让父进程知道哪个子进程被终止了。
pid_t pid;
int status;
pid = wait(&status);
当一个进程终止时, 操作系统会释放其资源,不过,它位于进程表中的条目还是在的,知道父进程调用wait(); 这是因为进程表包含了进程的退出状态。
当进程已经终止,父进程没有wait()的进程,称为 僵尸进程。
所有进程在中的时候都会进入到这个状态,但一般僵尸进程只是很短暂的存在,一旦父进程调用了wait(), 僵尸进程的进程描述符和它的进程表中的条目都会被释放。
如果父进程没有执行wait()就终止了,那么它的子进程就变成了孤儿进程, 在Unix和Linux 系统,他们的父进程就会变成 pid = 1的init 进程。 进程init 会定期进行wait() 操作。以便收集孤儿进程的状态,并释放孤儿进程标识符和进程表条目。
网友评论