进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程提供给应用程序的两个假象:
- 一个独立的逻辑控制流,仿佛程序独占处理器
- 一个私有的地址空间,程序中的代码和数据好像是系统内存中唯一的对象
逻辑控制流
进程是轮流使用处理器的,每个进程执行它的流的一部分,然后被抢占(暂时挂起),轮到其他教程。对于一个运行在这些教程之一的上下文中的程序,看上去就像是在独占地使用处理器。唯一的反面例证是,如果我们精确地测量每条指令使用的时间,会发现在程序中一些指令的执行之间,CPU 好像会周期性地停顿。然而,每次处理器停顿,它随后会继续执行我们的程序,并不改变程序内存位置或寄存器内容。
私有地址空间
进程为每个程序提供它自己的私有地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读写的。
尽管和每个私有地址空间相关联的内存的内容一般是不同的,但每个空间都有相同的通用结构。
地址空间底部是保留给用户程序的,包括通常的代码、数据、堆和栈段。代码段总是从地址 0x400000 开始。地址空间顶部保留给内核(操作系统常驻内存的部分)。地址空间的这个部分包含内核在代表进程执行指令时(比如当应用程序执行系统调用时)使用的代码、数据和栈。
用户模式和内核模式
为了使操作系统内核提供进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式(超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个 I/O 操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据,必须通过系统调用接口间接地访问。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回用户模式。
上下文切换
操作系统内核使用一种称为上下文切换(context switch)的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在较低层的异常机制之上的。
内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括各种通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。当内核调度了一个新的教程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
上下文切换
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个 read 系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另一个进程,而不是等待数据从磁盘到达。另一个示例是 sleep 系统调用,它显式地请求让调用进程休眠。一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。
中断也可能引发上下文切换。比如,所有的系统都有某种产生周期性定时器中断的机制,通常为每 1 毫秒或每 10 毫秒。每次发生定时器中断时,内核就能判定当前进程已经运行了足够长的时间,并切换到一个新的进程。
在这个例子中,A 初始运行在用户模式中,直到它通过执行系统调用 read 陷入到内核。内核中的陷阱处理程序请求从磁盘到内存的数据传输。
磁盘取数据要用一段相对较长的时间(数量级为几十毫秒),所以内核执行从 A 到 B 的上下文切换,而不是在这个间歇时间内等待,什么都不做。注意在切换之前,内核正代表 A 在用户模式下执行指令(即没有单独的内核进程)。在切换的第一部分中,内核代表 A 在内核模式下执行指令。然后在某一时刻,它开始代表 B (仍然是内核模式下)执行指令。在切换之后,内核代表 B 在用户模式下执行指令。
随后,B 在用户模式下运行一会儿,直到磁盘发出一个中断信号,表示数据已经从磁盘传送到了内存。内核判定 B 已经运行了足够长的时间,就执行一个从 B 到 A 的上下文切换,将控制返回给 A 中紧随在系统调用 read 之后的那条指令。进程 A 继续运行,直到下一次异常发生,依此类推。