旧游无处不堪寻
无寻处,惟有少年心
操作系统(一)

操作系统的历史


早期操作系统

一开始,操作系统甚没有做太多事情。基本上,它只是一组常用函数库。通常,在这些老的大型机系统上,一次运行一个程序,由操作人员来控制。这种计算模式被称为批(batch)处理,先把一些工作准备好,然后由操作人员以分批的方式运行。

不仅库函数

早期任何程序都可以读取任何文件,所以隐私的概念消失了。因此,将一个文件系统(file system)实现为一个库是没有意义的。实际上,还需要更多的管理。因此,系统调用(system call)的概念诞生了,系统调用和过程调用之间的关键区别在于,系统调用将控制转移(跳转)到 OS 中,同时提高硬件特权级别(hardware privilege level)。用户应用程序以所谓的用户模式(user mode)运行,这意味着硬件限制了应用程序的功能。例如,以用户模式运行的应用程序通常不能发起对磁盘的 I/O 请求,不能访问任何物理内存页或在网络上发送数据包。在发起系统调用时(通常通过一个称为陷阱(trap)的特殊硬件指令,硬件将控制转移到预先指定的陷阱处理程序(trap handler),同时将特权级别提升到内核模式(kernel mode)。在内核模式下,操作系统可以完全访问系统的硬件,因此可以执行诸如发起 I/O 请求或为程序提供更多内存等功能。当操作系统完成请求的服务时,它通过特殊的陷阱返回(return-from-trap)指令将控制权交还给用户,该指令返回到用户模式,同时将控制权交还给应用程序,回到应用离开的地方。

多道程序时代

操作系统的真正兴起在大主机计算时代之后,即小型机(minicomputer)时代。由于希望更好地利用机器资源,多道程序(multiprogramming)变得很普遍。操作系统不是一次只运行一项作业,而是将大量作业加载到内存中在它们之间快速切换,从而提高 CPU 利用率。

进程

进程的非正式定义非常简单: 进程就是运行中的程序。程序本身是没有生命周期的,它只是存在磁盘上面的一些指令。

虽然只有少量的物理 CPU 可用,但是操作系统需要提供几乎有无数个 CPU 可用的假象。其是通过虚拟化 CPU 来提供这种假象。通过让一个进程只运行一个时间片,然后切换到其他进程,这就是时分共享(time sharing)CPU 技术,允许用户运行多个并发进程。

要实现 CPU 的虚拟化,需要一些机制和策略来保证。例如上下文切换(context switch)和调度策略(scheduling policy)。

进程创建

操作系统如何启动并运行一个程序是我们接下来要介绍的事情。操作系统运行程序必须做的第一件事是将代码和所有静态数据(例如初始化变量)加载(load)到内存中,具体来说是加载到进程的地址空间中。
程序最初以某种可执行格式驻留在磁盘上,将程序和静态数据加载到内存中的过程,需要操作系统从磁盘读取这些字节。
在早期的操作系统中,加载过程尽早(eagerly)完成,现代操作系统惰性(lazily)执行该过程,即仅在程序执行期间需要加载的代码或数据片段,才会加载。

将代码和静态数据加载到内存后,操作系统在运行此进程之前还需要执行其他一些操作。必须为程序的运行时栈(run-time stack 或 stack)分配一些内存。操作系统也可能为程序的堆(heap)分配一些内存。

操作系统还将执行一些其他初始化任务,特别是与 I/O 相关的任务。例如,默认情况下每个进程都有 3 个打开的文件描述符(file descriptor),用于标准输入、输出和错误。

上述过程完成之后,操作系统会启动程序,将 CPU 的控制权转移到新创建的进程中,使得程序开始运行。

进程状态

进程在给定时间可能处于的不同状态(state)。进程可以处于以下 3 种状态之一:

  • 运行(running): 在运行状态下,进程正在处理器上运行。这意味着它正在执行指令
  • 就绪(ready): 在就绪状态下,进程已准备好运行,但由于某种原因,操作系统选择不在此时运行该进程
  • 阻塞(blocked): 在阻塞状态下,一个进程执行了某种操作,直到发生其他事件时才会准备运行。一个常见的例子是,当进程向磁盘发起 I/O 请求时,它会被阻塞,因此其他进程可以使用处理器

进程一般会在就绪状态和运行状态之间转换。从就绪到运行意味着该进程已经被调度(scheduled)。从运行转移到就绪意味着该进程已经取消调度(descheduled)。

数据结构

操作系统是一个程序,和其他程序一样,它有一些关键的数据结构来跟踪各种相关的信息。例如,为了跟踪每个进程的状态,操作系统可能会为所有就绪的进程保留某种进程列表(process list),以及跟踪当前正在运行的进程的一些附加信息。

操作系统追踪进程的一些重要信息。对于停止的进程,寄存器上下文将保存其寄存器的内容。当一个进程停止时,它的寄存器将被保存到这个内存位置。通过恢复这些寄存器(即将它们的值放回实际的物理寄存器中),操作系统可以恢复运行该进程。它被称为上下文切换(context switch),之后我们会具体讨论。xv6 的 proc 结构如下:

// the registers xv6 will save and restore
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 };

// 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
};