进程、地址空间、线程
由于计算机所拥有的物理资源有限,当多个程序在计算机上同时运行时,它们不可能连续地在处理器上运行或占用全部内存,因此我们需要进程的抽象在用户的一侧隐藏上下文切换等细节。 进程作为运行的程序的抽象,包含了一个程序的运行状态和这个程序所用的抽象内存及其中存储的数据。 我们将后一部分,即一个进程可以使用的全部内存的地址和它们存储的数据,称为这个进程的 地址空间(address space) 。我们可以将进程笼统地看做是一个或多个线程与一个地址空间的结合。
一方面,进程的结构必须能够囊括一个进程的运行状态,在由于中断、异常或系统调度进入内核态时我们应该能够将进程的运行状态放入内存中的某一位置,使我们可以在某一个时间点根据进程的优先级接着这次的进度再次运行该进程; 另一方面,由于进程在用户一侧对内存进行了抽象,我们需要在系统一侧包含从抽象内存向实际内存和外存(磁盘等存储设备)的映射,以便通过抽象内存的地址获得物理地址和其中存储的数据。 为了实现上述的第一个目的,即在进程中存储其运行状态,我们将在进程中存储处理器状态寄存器、指令计数器、栈指针、通用寄存器等代表了进程目前的运行状态的值。
由于现代计算机中往往有多个核或多个处理器,我们的程序可以达到 并行(parallelism,即在物理时间上同时运行,区别于多个进程在处理器上的并发(concurrency)),因此同一个进程可能同时包含有多个不同的运行状态。 为了更好地区分进程在一个处理器上的运行状态和进程本身的运行状态,我们在此引入 线程(thread) 的概念。
每个线程拥有一个 线程控制块(thread control block, TCB) ,用来存储我们上述提到的处理器状态寄存器、指令计数器、栈指针、通用寄存器等数值,但同一个进程中的所有线程都共享同一个地址空间。
一个用户进程所看到的内存空间是抽象的,区别于实际的物理内存,因此为了支持进程从抽象内存获取物理内存中存储的信息,我们需要在进程中包含从抽象内存向物理内存的映射,即该系统所用的地址转换方法所需要的信息。 地址转换有很多种不同方法,包括分页存储、分段存储等,它们都需要不同的信息来实现地址转换;我们将在下一章中详细介绍这些方法,这里我们将以最为简单的 Base and Bound 作为例子来解释这一概念。
1. 在 Base and Bound 方法中,系统将一段连续的物理内存分配给一个进程,Base 代表基地址,即系统分配给一个进程的可用内存的起始地址,2. Bound 代表这个进程可用的内存的最高地址.3. 为了读写物理内存,我们将基地址与抽象内存地址相加,获得实际地址,只要实际地址不大于 Bound,内核就会允许进程进行该操作。4. 因此在这个方法中,为了实现进程地址空间的抽象,进程在系统一侧的数据结构中必须包括这段可用的内存的基地址、可用长度。
为了能在一个进程开始运行的时候方便地载入上面提到的两方面的数据,我们需要一个数据结构来包含这些数据。这个数据结构就是 进程控制块(Process Control Block, PCB)。 对于内核来说,它是进程存在的唯一标识。
进程控制块中存储的信息除了上面提到的实现地址转换的信息和表示运行状态的线程信息以外,还包括了很多系统在调度进程时需要的信息,比如进程号、进程所处状态(我们将在下一节中更多地解释进程可能所处的状态与不同状态间的切换)、进程的优先级等等。
- 需要注意的是,我们不会将整个地址空间中包含的地址及其数据都存储在进程控制块里;我们只需要可以帮助我们从抽象内存地址获得实际物理地址的方法,而实际数据仍然存储在内存的对应位置中。
- 由于进程控制块中的信息包含了优先级、基地址、可用地址范围等用户进程不应该有权限修改的信息,用户进程不应该有权限修改进程控制块。因此我们将进程控制块存储在内核空间中。
进程在系统中的实现方式
进程状态与用户态和内核态之间的切换
进程控制块中包含了进程的状态;
- 那么,什么是进程的状态呢?我们已经提到过,一个进程可能在运行一段时间后被切换出去,一段时间后继续运行。显然,进程至少需要两个状态—— 运行态(running) 与 就绪态(ready) 。
进程在运行态中运行一段时间后被切换出去,排入就绪队列,等待继续运行。在三态模型中,进程还有一个状态,即 等待态(wait)。 在这个状态中,进程在等待某一事件完成,不会被排入就绪队列,直到该事件发生后,进程才会被排入就绪队列,可能被选为下一个运行的进程。这一等待事件可能包括等待系统调用完成,等待获得某一个锁或收到某一信号量的信号等等.
- 现在你只需要知道,处于就绪态的进程不会因为遇到任何事件而无法进入等待态;处于等待状态的进程在事件发生后考虑到优先级等问题,必须先回到就绪状态而不能直接运行。 新建态(new) 和 终止态(exit) 新建态对应着进程被创建时尚未加入就绪队列的状态。为了建立一个新的进程,内核需要给进程分配资源(如:建立进程控制块)、建立必要的管理信息;在这个过程中,进程就处于新建态。
终止态对应的是进程已经结束但尚未被系统撤销的状态。处于终止态的进程虽然已经结束,但其获得的资源还未被系统回收,因此系统仍然可以获得该进程结束时的信息。 定义这两种状态可以帮助我们避免一些我们不想看到的情况。比如,如果我们允许一个进程在获得全部资源以前就进入就绪队列,那么一个新进程可能在未获得全部资源前就开始运行,而产生错误。(这是因为我们是在另一个进程里通过系统调用创造了这个新的进程,我们会在本章的后几个章节详细阐明这个问题)同样的,如果一个进程 A 创建了另一个进程 B 而想要等待进程 B 运行完毕,从中获得一些信息,那么终止态的缺失可能导致进程 B 率先运行完毕后被系统撤销,使得进程 A 无法从进程 B 处获得任何信息。UNIX 系统中,如果发生一个进程创建另一个进程的情况,那么在老进程从新进程获取信息前,新进程会一直停留在终止态;如果新进程终止后,仍在运行的老进程没有从新进程获取信息,那么新进程就会成为我们所说的“僵尸进程”,停留在系统内存中,导致系统变慢。