进程

1. 概念

进程是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文(context)中。上下文由程序正确运行所需的状态组成。这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程提供给应用程序的关键抽象包括:

  1. 一个独立的逻辑控制流,它提供一个假象,好像程序独占地使用处理器;
  2. 一个私有的地址空间,它提供一个假象,好像程序独占地使用存储器系统。

2. 逻辑控制流

逻辑控制流是指程序计数器的序列,异常处理程序、进程、信号处理程序、线程和 Java 进程都是逻辑控制流的例子。 一个逻辑控制流的执行与另一个重叠被称为并发流(concurrent flow)。更准确地说,X 和 Y 互相并发,当且仅当 X/Y 在 Y/X 开始之后和 Y/X 结束之前开始。多个流并发地执行的一般现象称为并发(concurrency)。一个进程和其他进程轮流执行被称为多任务(multitasking),也叫时间分片(time slicing)。一个进程执行它的控制流的一部分的每一个时间段叫做时间偏(time slice)。

3. 用户模式和内核模式

处理器用某个控制寄存器中的一个模式位(mode bit)来限制一个应用可以执行的指令以及可以访问的地址空间范围。 一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。而在用户模式中的进程必须通过系统调用接口间接地访问内核代码和数据。运行应用程序代码的进程,初始时在用户模式中。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。

4. 上下文切换

操作系统内核使用一种称为上下文切换(context switch)的异常控制流来实现多任务。 上下文是指内核重新启动一个被抢占的进程所需的状态。上下文由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,如描绘地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。 内核为每个进程维持一个上下文。

内核中的调度器(scheduler)可以在进程执行的某些时刻,决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定叫做调度(schdule)。在内核调度了一个新的进程后,它就抢占了当前进程,并使用上下文切换基址来将控制转移到新的进程。

5. 进程的控制

每个进程都有一个唯一的非负的进程 ID(PID)。我们可以认为进程总是处于三种状态之一:

  1. 运行。进程要么在 CPU 上执行,要么等待被执行且最终会被内核调度。
  2. 停止。进程的执行被挂起(suspend)且不会被调度。
  3. 终止。进程永远的停止了。

当收到 SIGSTOP、SIGTSTP、SIDTTIN 或者 SIGTTOU 信号时,进程就停止,并保持停止直到收到一个 SIGCONT 信号。进程收到 SIGCONT 信号时再次开始运行。 进程终止的原因有三种:1)收到一个信号,该信号的默认行为是终止进程;2)从主程序返回;3)调用 exit 函数。

父进程通过 fork 函数创建一个新的运行子进程。新创建的子进程几乎但不完全与父进程相同。 子进程得到与父进程用户级虚拟地址空间相同但独立的一份拷贝,包括文本、数据和 bss 段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。子进程与父进程最大的区别在于 PID 不同。

fork 函数调用一次返回两次。在子进程中,fork 返回 0;在父进程中,fork 返回子进程的 PID。

当一个进程由于某种原因终止时,进程被保持在一种已终止的状态中,直到被父进程回收(reap)。当父进程回收已终止的子进程时,内核将子进程的退出状态传给父进程,然后抛弃已终止的进程。一个终止了但未被回收的进程称为僵死进程(zombie)。

如果父进程没有回收它的僵死子进程就终止了,那么内核会安排 init 进程来回收它们。 init 进程的 PID 为 1,在系统初始化时由内核创建。

一个进程可以通过调用 waitpid 函数来等到它的子进程终止或停止。

#include <sys/types.h>
#include <sys/wait.h>
int main() {
    pid_t waitpid(pid_t pid, int *status, int options);   
}

默认的,waitpid 挂起调用进程的执行,直到它的等到集合中的一个子进程终止。

execve 函数在当前进程的上下文中加载并运行一个新程序。

#include <unistd.h>

int execve(const char *filename, const char *argv[], const char *envp[]);

fork 函数在新的子进程中运行相同的程序,execve 函数在当前进程的上下文中加载并运行一个新的程序。 新的程序覆盖当前进程的地址空间,但并没有创建一个新进程,仍具有相同的 PID,并且继承了调用 execve 函数时已打开的所有文件描述符。

笔记来源

深入理解计算机系统(原书第2版)