进程
创建进程
有4种事件导致进程的创建:
- 系统初始化
- 进程创建系统调用
- 用户请求创建一个新进程
- 批处理作业的初始化
新进程都是由一个已存在的进程执行了一个用于创建进程的系统调用而创建的。在UNIX中,只有一个系统调用可以创建新进程:fork。在调用fork之后,这两个进程拥有相同的存储映像、同样的环境字符串和相同的打开文件。通常,子进程接着执行execve或一个类似的系统调用,以修改其存储映像并执行一个新的程序。
进程创建之后,父进程和子进程各自拥有不同的地址空间。
进程的状态
进程在操作系统中存在三种状态:
- 运行态(进程正在占用CPU资源运行)
- 就绪态(进程获得了除CPU之外的一切资源,等待处理机调度)
- 阻塞态(进程正在某个事件的发生,在此之前无法继续执行)
进程的实现
为了实现进程模型,操作系统维护着一张表,称为进程表(process table)。每个进程占用一个进程表项,这个进程表项即进程控制块(PCB)。PCB包含了进程状态的重要信息,包括寄存器、程序计数器、程序状态字、堆栈指针、进程状态、内存分配状况、所打开文件的状态、账号和调度信息等。
中断向量包含中断服务程序的入口地址。进程中断时,中断硬件将程序计数器、程序状态字,还有一些寄存器等压入堆栈,计算机随即跳转到中断向量指向的地址。
多道程序设计模型
当进程的I/O时间占比越大,多道程序设计的道数应该越大,以提高CPU的利用率。
线程
进程和线程的比较
- 在许多应用中,同时发生着多种活动。其中某些活动随着时间的推移会被阻塞。通过将这些应用程序分解成可以准并运行的多个顺序线程,程序设计模型会变得更简单。
- 并行实体共享同一地址空间和所有可用数据的能力。
- 线程比进程更轻量级,他们更容易创建,也更容易撤销。创建一个线程较创建一个进程要快10-100倍。
- 如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而加快应用程序执行的速度。
经典的线程模型
理解进程的一个角度是,用某种方法把相关的资源集中在一起。进程有存放程序正文和数据及其他资源的地址空间。这些资源包括打开的文件、子进程等。把他们都放到进程中可以更容易管理。
进程拥有一个执行的线程,通常简写为线程。在线程中有一个程序计数器,用来记录接着要执行哪一条指令。线程拥有寄存器,用来保存线程当前的工作变量。线程还有一个堆栈,用来记录执行历史,其中每一帧保存了一个已调用但是还没有返回的过程。尽管线程必须在某个进程中执行,但是线程和它的进程是不同的概念,可以分别处理。进程用于把资源集中在一起,线程则是在CPU上被调度执行的实体。
每个进程有自己的地址空间、全局变量、打开文件、子进程等,每个线程则有自己的程序计数器、寄存器、堆栈、状态。
在用户空间实现线程
把整个线程包放在用户空间中,内核对线程包一无所知。每个进程需要有其专用的线程表,用来跟踪该进程中的线程。
用户级线程的优点:
- 线程切换至少比陷入内核要快一个数量级。同时,线程调度不需要陷阱,不需要上下文切换,也不需要对内存高速缓存进行刷新,这使得线程调度非常快捷。
- 用户级线程还允许每个进程有自己定制的调度算法。
用户级线程的问题:
- 如何实现阻塞系统调用;
- 在一个单独的进程内部,没有时钟中断,不能用轮转调度的方式调度进程,只能让线程自动放弃CPU。
在内核中实现线程
在内核中有用来记录系统所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。
内核的线程表保存了每个线程的寄存器、状态和其他信息。这些信息是传统内核所维护的每个单线程进程信息的子集。
混合实现
使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。
进程间通信(IPC)
竞争条件
两个或多个进程读写某些共享数据,而最后结果取决于进程运行的精确时序(运行结果和运行时序相关),这样的情况称为竞争条件(race condition)。
临界区
我们需要使用互斥(mutual exclusion),确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。我们把对共享内存进行访问的程序片段称作临界区域(critical region)或临界区(critical section)。
对于一个好的解决方案,需要满足一下4个条件:
- 任何两个进程不能同时处于临界区
- 不应对CPU的速度和数量做任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使进程无限期等待进入临界区
忙等待的互斥
1.屏蔽中断
在单处理系统中,最简单的方法是使每个进程在刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。这个方案并不好,因为把屏蔽中断的权力交给用户进程是不明智的。另一方面,对于内核来说,当它在更新变量或列表的几条指令期间将中断屏蔽是很方便的。
屏蔽中断对于操作系统本身而言是一项很有用的技术,但对于用户进程则不是一种合适的通用互斥机制。
2.锁变量
一个共享锁变量,其初始值为0,当一个进程想进入临界区时,他先测试这把锁,如果该锁的值为0,则该进程将其设置为1并进入临界区。若这把锁的值已经为1时,则该进程将等待直到其值变为0。这种方法会导致竞争条件,因为检查并设置锁变量这个操作不是原子性的。
3.严格轮换法
设置整型变量turn,初始值为0,用于记录轮到哪个进程进入临界区。
while(true) {
while(turn != 0);
critical_region();
turn = 1;
noncritical_region();
}
连续测试一个变量直到某个值出现为止,称为忙等待(busy waiting)。
只有在有理由认为等待时间非常短的情形下才使用忙等待。用于忙等待的锁,称为自旋锁(spin lock)。
当轮到进程0进入临界区,但是进程0正在执行非临界区的代码,这时如果进程1想要进入临界区也不得不等待,这违背了条件3:临界区外运行的进程不得阻塞其他进程。
4.Peterson算法
#define FALSE 0
#define TRUE 1
#define N 2
int turn; //轮到谁进入临界区
int interested[N];
void enter_region(int process)
{
int other;
other = 1 - process;
interested[process] = TRUE; //标识该进程希望进入临界区
turn = process;
while(turn == process && interested[other] == TRUE); //当轮到自己,但是对方希望进入临界区时等待
}
void leave_region(int process)
{
interested[process] = FALSE;
}
5.TSL指令
这是一个需要硬件支持的方案。许多计算机都有下面一条指令:
TSL RX, LOCK
这条指令称为测试并加锁(test and set lock),它将一个内存字lock读到寄存器中RX中,然后在该内存地址上存一个非零值。该操作是不可分割的,即该指令结束之前其他处理器均不允许访问该内存字,执行TSL指令的CPU将锁住内存总线,以防止其他CPU在本指令结束之前访问内存。
一个可替代TSL的指令是XCHG,它原子性地交换两个位置的内容。
忙等待的方法浪费CPU,并且,如果处于忙等待的进程优先级较高,CPU一直被该进程占用,进入临界区的低优先级的进程就永远无法离开临界区,这种情况被称为优先级反转问题(priority inversion problem)。
睡眠与唤醒
现在考察几条进程间通信原语,他们在无法进入临界区时阻塞,而不是忙等待。sleep是一个将引起调用进程阻塞的系统调用;wakeup调用有一个参数,即要被唤醒的进程。
生产者-消费者问题
以上方法,对于某些执行时序,生产者和消费者都会睡眠,这就导致了竞争条件的出现。
信号量
引入一个变量类型,称为信号量(semaphore)一个信号量的取值可为0或正值。
可以对信号量进行两种操作:P,V。这两种操作的特点是:检查数值、修改变量值以及可能发生的睡眠或唤醒操作作为一个单一的、不可分割的原子操作。
P对信号量的值减1,代表消费了资源;V对信号量的值增1,代表提供了资源。
用信号量解决生产者-消费者问题
该方法使用了三个信号量:full,用来记录充满的缓冲槽数目;empty,用来记录空的缓冲槽数目;mutex,用来确保生产者和消费者不会同时访问缓冲区。
注意P(empty)和P(mutex)的顺序(图中down(&empty)和down(&mutex))不能互换:如果先取得互斥信号量,发现空的缓冲槽数目为0,则生产者进入睡眠。这时虽然有满的缓冲槽,但是消费者不能取得互斥信号量,于是消费者也进入睡眠。这就导致了死锁。
同理,P(full)和P(mutex)也不能互换。但是两个V操作可以互换。
信号量的另一种用途是用于实现同步(synchronization)。信号量full和empty用来保证某种顺序的事件发生或不发生。在本例中,它们能保证缓冲区满的时候让生产者停止运行,缓冲区空的时候让消费者停止运行。
PV操作在同步互斥的应用总结:在同步问题中,若某个行为要用到某种资源,则在这个行为前面P这种资源一下;若某个行为会提供某种资源,则在这个行为后面V这种资源一下。
管程
1.管程的定义
管程是一组数据及定义在这组数据之上的对这组数据的操作组成的软件模块,这组操作能初始化并改变管程中的数据和同步进程。
2.管程的组成
- 局部于管程的共享结构数据说明
- 对该数据结构进行操作的一组过程
- 对局部于管程的共享数据设置初始值的语句
3.管程的基本特性
- 局部于管程的数据只能被局部于管程内的过程所访问
- 一个进程只有通过管程内的过程才能进入管程访问共享数据
- 每次仅允许一个进程在管程内执行某个内部过程
调度
调度的层次
- 作业调度。又称高级调度,其主要任务是按一定的原则从外存上处于后备状态的作业中挑选一个或多个作业,给他们分配内存、输入输出设备等必要的资源,并建立相应的进程,以使它们获得竞争处理机的权利。
- 中级调度。又称内存调度,其作用是提高内存利用率和系统吞吐量。将那些暂时不能运行的进程调至外存等待,这时进程的状态称为挂起态。当他们具备运行条件且内存有空闲时,再将它们调入内存,并修改为就绪态。
- 进程调度。又称低级调度,其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。
调度的时机
- 在创建一个新的进程后,需要决定运行父进程还是子进程。
- 在一个进程退出时必须做出调度决策。
- 当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。
- 在一个I/O中断发生时,必须做出调度决策。
调度的原则
- CPU利用率。
- 系统吞吐率。表示单位时间内CPU完成作业的数量。
- 周转时间。周转时间是指从作业提交到作业完成所经历的时间。
- 等待时间。等待时间是指进程处于等待处理机状态的时间之和。
- 响应时间。响应时间指从用户提交请求到系统首次产生相应所用的时间。
调度算法
先来先服务(first-come first-serve)
进程按照它们请求CPU的顺序使用CPU。通常适用于批处理系统。
短作业优先(shortest job first)
CPU每次选取预计运行时间最短的进程运行。
优先级调度算法(priority scheduling)
既可用于作业调度,又可用于进程调度。
在作业调度中,从后备作业队列选择优先级最高的一个或几个作业,将他们调入内存,分配必要的资源,创建进程并放入就绪队列;在进程中,优先级调度算法每次从就绪队列选择优先级最高的进程,将处理机分配给它。
高响应比优先调度算法
主要用于作业调度。每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。
响应比=(等待时间+要求服务时间) / 要求服务时间
时间片轮转调度算法(round robin)
适用于分时系统。系统将所有就绪进程排成一个队列,算法按照FCFS的原则,但每个进程只能运行一个时间片,如100ms。
多级反馈队列调度算法
- 设置多个就绪队列,各个队列具有不同优先级。
- 优先级越高,时间片越小。
- 一个新进程进入内存后,首先将它放入第1级队列,如果他在一个运行时间片后仍未完成,就将它放到第2级队列,依此类推。最后放到第n级队列中就采用时间片轮转的方式运行。
- 仅当 1 ~ i-1 级队列为空时才会调度第 i 级队列中的进程运行。