《操作系统导论》第4章 | 进程
操作系统为正在运行的程序提供的抽象,就是所谓的进程(process),一个进程就是一个正在运行的程序。为了理解进程的构成,我们必须理解它的机器状态:程序在运行时可以读取或更新的内容。一个明显的机器状态就是内存。指令、正在运行的程序读取和写入的数据都在内存中。因此进程可以访问的内存(地址空间,address space)是该进程的一部分。进程机器状态的另一部分是寄存器。许多指令明确地读取或更新寄存器,它们对于执行该进程很重要。例如,程序计数器(Program Counter,PC)告诉我们程序当前正在执行哪个指令;栈指针和相关的帧指针用于管理函数参数栈、局部变量和返回地址。
进程创建
操作系统运行程序必须做的第一件事是将代码和所有静态数据加载到内存(进程的地址空间)中。程序最初以某种可执行格式驻留在磁盘上。因此,将程序和静态数据加载到内存中的过程,需要操作系统从磁盘读取这些字节,并将它们放在内存中的某处。早期的操作系统中,加载过程会在运行程序之前全部完成。现代操作系统惰性地执行该过程,即仅在程序执行期间需要加载的代码或数据片段,才会加载。
将代码和静态数据加载到内存后,操作系统在运行此进程之前还需要为程序的运行时栈分配一些内存(C程序使用栈存放局部变量、函数参数和返回地址)。操作系统分配这些内存,并提供给进程。操作系统也可能会用参数初始化栈。具体来说,它会将参数填入main()
函数,即argc
和argv
数组。
操作系统也可能为程序的堆分配一些内存。在C程序中,堆用于显式请求的动态分配数据。程序通过调用malloc()
来请求这样的空间,并通过调用free()
来明确地释放它。操作系统还将执行一些其他初始化任务,特别是与输入/输出(I/O)相关的任务。例如,在UNIX系统中,默认情况下每个进程都有3个打开的文件描述符,分别用于标准输入、输出和错误。
进程状态
运行:在运行状态下,进程正在处理器上运行。这意味着它正在执行指令。
就绪:在就绪状态下,进程已准备好运行,但由于某种原因,操作系统选择不在此时运行。
阻塞:在阻塞状态下,一个进程执行了某种操作,直到发生其他事件时才会准备运行。一个常见的例子是,当进程向磁盘发起I/O请求时,它会被阻塞,因此其他进程可以使用处理器。
根据操作系统的载量,进程会在就绪状态和运行状态之间转换。从就绪到运行意味着该进程已经被调度。从运行转移到就绪意味着该进程已经取消调度。一旦进程被阻塞(例如,通过发起I/O操作),OS将保持进程的这种状态,直到发生某种事件(例如,I/O完成)。此时,进程再次转入就绪状态(也可能立即再次运行,如果操作系统这样决定)。
想象两个正在运行的进程,每个进程只使用CPU(它们没有I/O)。在这种情况下,每个进程的状态可能如下所示。
下面例子中,0号进程在运行一段时间后发起I/O请求。此时,进程0被阻塞,等待I/O完成。OS发现进程0不使用CPU并开始运行进程1。当进程1运行时,I/O完成,将进程0移回就绪状态。最后,进程1结束,进程0运行,然后完成。
数据结构
下面的代码展示了OS需要跟踪xv6内核中每个进程的信息类型。可以看到,对于停止的进程,寄存器上下文将保存其寄存器的内容。当一个进程停止时,它的寄存器将被保存到这个内存位置。通过恢复这些寄存器(将它们的值放回实际的物理寄存器中),操作系统可以恢复运行该进程,这就是上下文切换(context switch)。
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
从上述代码中还可以看到,除了运行、就绪和阻塞之外,还有其他的一些进程状态。有时候系统会有一个初始(initial)状态,表示进程在创建时处于的状态。另外,一个进程可以处于已退出但尚未清理的最终(final)状态(僵尸状态)。这个最终状态允许其他进程(通常是创建进程的父进程)检查进程的返回代码,并查看刚刚完成的进程是否成功执行。完成后,父进程将进行最后一次调用(例如,wait()
),以等待子进程的完成,并告诉操作系统它可以清理这个正在结束的进程的所有相关数据结构。