进程经常需要与其他进程通信。进程间通信的问题:
第一个问题对于线程来说比较容易,因为它们共享一个地址空间。但是另外两个问题同样适用于线程。
Example:假脱机目录的例子
竞争条件:两个或者多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。
凡涉及共享内存、共享文件以及共享任何资源的情况都会引起与前面类似的错误。
解决关键:需要互斥(mutual exclusion),即以某种手段确保当一个进程在使用一个共享变量或者文件时,其他进程不能进行相似的操作。
临界区:对共享内存进行访问的片段称为临界区(critical region)。
目标:适当安排,使得两个进程不同时处于临界区中。
对于一个好的解决方案,需要满足下面4个条件:
实现互斥的方案
进程进入临界区后屏蔽所有中断,离开之前再打开中断。
屏蔽中断后时钟中断也被屏蔽,只有发生时钟中断时才会进行进程切换,因此屏蔽之后CPU不会再切换到别的进程。
缺点:屏蔽中断的权利交给用户进程是不明智的。
设想有一个共享(锁)变量,其初始值为0。当一个进程想进入临界区时,他首先测试这把锁。
缺点:与脱机目录一样的疏漏。
用于忙等待的锁称为自旋锁(spin lock)
该方案要求两个进程严格轮流进入临界区。
使用共享变量。
#define FALSE 0
#define TRUE 1
#define N 2 /* number of processes */
int turn; /* whose turn is it? */
int interested[N]; /* all values initially 0 (FALSE) */
void enter region(int process) /* process is 0 or 1 */
{
int other; /* number of the other process */
other = 1 − process; /* the opposite of process */
interested[process] = TRUE; /* show that you are interested */
tur n = process; /* set flag */
while (turn == process && interested[other] == TRUE); /* null statement */ ;
}
void leave region(int process) /* process: who is leaving */
{
interested[process] = FALSE; /* indicate departure from critical region */
}
硬件支持的方案,有下面的指令:
TSL RX, LOCK
称为测试并加锁,将一个内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值。读字和写字的操作保证是不可分割的,即该指令结束之前其他处理器不允许访问该内存字。执行TSL指令的CPU将锁住内存总线,以进制其他CPU在本指令结束之前访问该内存字。
一个可以替代TSL
的指令是XCHG
,它原子性的交换了两个位置的内容。
Peterson解法和TSL或者XCHG解法都是正确的,但是都有忙等待的缺点。忙等待解法本质上是:当一个进程想进入临界区时,先检查是否允许进入,若不允许,则该进程将原地等待,直到允许为止。
忙等待可能造成优先级反转问题:两个进程H和L,H优先级高于L,H在就绪态时H可以随时运行。某一时刻,L处于临界区中,此时H变到就绪态,准备运行(例如,一条I/O操作结束)。现在H开始忙等待,但由于当H就绪时L不会被调度,也就无法离开临界区,所以H将永远一直等下去。被称为优先级反转问题。
进程间通信原语:无法进入临界区时将阻塞,而不是忙等待。最简单的是sleep
和wake up
。
sleep
:引起调用进程阻塞的系统调用,即被挂起,直到有另一个进程将其唤醒。wake up
:有一个参数,即要被唤醒的进行。生产者:把信息放入缓冲区;缓冲区已满时休眠
消费者:从缓冲区中取出信息;缓冲区已空时休眠
上述的程序存在竞争条件:当count=0时,消费者读取count,判断得count=0,此时时间片轮转到生产者,生产者读取为0,进行生产;产出1后wake up
消费者。但是此时消费者并为逻辑上进入睡眠,所以wake up
信号量丢失,然后当消费者下次运行时,他读到的count是0,所以睡眠。而生产者迟早会填满整个缓冲区,然后睡眠。这样一来,两个进程都将永远睡眠下去。
down
操作:
检查信号量的值是否大于零
检查数值、修改变量值以及可能发生的睡眠均为一个单一的,不可分割的原子操作。
up
操作:
检查信号量的值增加1,唤醒一个进程。
对一个有进程在其上睡眠的信号量执行一次up操作后,该信号量的值仍是0,但在其上睡眠的进程却少了一个。
信号量的增加和进程的唤醒同样也是不可分割的。不会有进程因为执行up
而阻塞。
down
和up
操作也可以称为P
操作和V
操作。
通过up
和down
作为系统调用实现,而且操作系统只需在执行以下操作时屏蔽所有中断:测试信号量、更新信号量以及在需要的时候使某个进程睡眠。
该方法使用了三个信号量:
full
:记录充满的缓冲槽的数目;初值为0empty
:记录空缓冲槽的数目;初值为缓冲区中槽的数目mutex
:确保生产者和消费者不会同时访问缓冲区信号量的另一种用途是用来实现同步。信号量full
和empty
用来保证某种事件顺序发生或者不发生。
互斥量是信号量的简化版本、互斥量仅仅适用于管理共享资源或指责一小段代码。
互斥量是一个可以处于两态之一的变量:解锁和加锁。实际上常常食用一个整型量,0表示解锁,其他的所有值表示加锁。
互斥量有两个过程,当一个线程(或进程)访问临界区时,他调用mutex_lock。
如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区
如果互斥量已经加锁,那么调用线程被阻塞,直到在临界区的线程完成并调用mutex_unlock
如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁
mutex_lock
和mutex_unlock
的实现代码如下:
futex
futex
:Linux的一个特性,实现了基本的锁(很像互斥锁),但避免了陷入内核。
因为来回切换到内核花销很大,所以这样做可观地改善了性能。
f u t e x 包含两个部分 { 一个内核服务 一个用户库 futex包含两个部分\begin{cases}一个内核服务\\一个用户库\end{cases} futex包含两个部分{一个内核服务一个用户库
内核服务
提供一个等待队列,允许多个进程在一个锁上等待。
用户库
pthread
中的互斥量pthread
提供了一系列的用来同步线程的函数。
基本机制是使用一个可以被锁定和解锁的互斥量保护每个临界区
互斥量在允许或阻塞对临界区的访问上很有用
提供了另一种同步机制条件变量
条件变量则允许线程由于一些未达到的条件而阻塞
pthread_cond_wait
:阻塞调用线程直到另一个线程向它发起信号pthread_cond_signal
:发起唤醒条件变量与互斥量经常一起使用。这种模式用于让一个线程锁住一个互斥量,然后当它不能获得他所期待的结果时等待一个条件变量。
管程:高级同步原语,是一个由过程、变量及数据结构组成的集合。
任何一个时刻,管程中只能有一个活跃进程,可以有效完成互斥。
互斥:使用互斥量和信号量;编译器安排互斥,操作概率小很多
无法运行时阻塞:条件变量以及相关的两个操作wait
和signal
消息传递是机器间的信息交换方法。这种进程间通信方法使用两条原语:send
和receive
。
send(destination,&message)
:向一个给定的目标发送消息
和
receive(source,&message)
:从一个源接收一个消息。
如果没有消息可用,则接收者可能被阻塞,直到一条消息到达,或者带着一个错误码返回。
区分新老消息:序号
消息丢失:发送确认
有些情况下可以允许写操作来更新数据结构,即便有其他的进程正在使用它。关键在于确保每个读操作要么读取旧的版本要么读取新的版本,但绝不能是新旧数据的一些奇怪组合。
存在多个进程竞争CPU的情况,必须要选择下一个要运行的进程。在操作系统中,完成选择工作的这一部分称为调度程序,该程序使用的算法称为调度算法。
线程的处理与进程相似,先讨论进程,然后讨论线程调度的独特文体。
批处理阶段:依次运行磁带上的每一个作业
多道程序设计:批处理和分时服务相结合
个人计算机:两个方向:唯一进程(不用调度选择);或者多个调度程序但是先后顺序或者时间要求可能没那么严格。
网络服务器:多个进程经常竞争CPU
几乎所有的进程I/O请求好计算都是交替突发的。如下图,某些进程花费了大多数时间在计算上(图a),而其他进程(如图b)则在等待I/O上花费了绝大多数的时间。根据进程在等待I/O上花费的时间,前者称为计算密集型(compute-bound),后者称为I/O密集型(I/O-bound)。
随着CPU变得越来越快,更多的进程倾向于I/O密集型。这种现象之所以发生是因为CPU改进得比磁盘快,其结果是,未来对I/O密集型进程的调度处理更为重要。
基本思想:如果要运行I/O密集型进程,那么应该让它尽快得到机会,以便发出磁盘请求并保持磁盘始终忙碌。
两种调度算法:
不同的环境需要不同的调度算法,列举三种环境:
不同的环境 { 批处理:非抢占式也是可以接受的 交互式:抢占是必须的 实时:抢占有时是不必要的 不同的环境\begin{cases}批处理:非抢占式也是可以接受的\\交互式:抢占是必须的\\实时:抢占有时是不必要的\end{cases} 不同的环境⎩ ⎨ ⎧批处理:非抢占式也是可以接受的交互式:抢占是必须的实时:抢占有时是不必要的
吞吐量:每小时完成的作业数量
周转时间:从一个批处理作业提价到完成时刻为止的统计平均时间。该数据度量了用户得到输出所需的平均等待时间
响应时间:发出命令到得到响应的时间
进程按照它们请求CPU的顺序使用CPU,有一个就绪进程的单一队列。作业不会因为运行时间太长而被中断。当其他作业进入时,它们排列到队列队尾。
优点:易于理解,便于应用
缺点:
非抢占式批处理算法,平均周转时间最短。
只有在所有作业都可以同时运行时,最短作业优先算法才是最优化的。
最短作业优先的抢占式版本。
选择剩余运行时间最短的进程运行。
每个进程被分配一个时间片,允许该进程在该时间片中运行。
时间片轮转中唯一有趣的是时间片的长度从一个进程切换到另一个进程是需要一定时间的——保存装入寄存器值集内存映像、更新各种表格和列表、清除和重新调入内存高速缓存等。需要考虑真正工作时间与切换开销(上下文切换)耗费的时间的比值。
轮转调度做了一个隐含的假设:所有的进程同等重要。
优先级调度基本思想:每个进程被赋予一个优先级,允许优先级最高的进程可以最先运行。
为防止高优先级的进程无休止的运行下去:
优先级调度的一种实现:一组进程分成若干类,并且在各类之间采用优先级调度,而在各个进程内部采用轮转调度:
设立优先级类,属于最高优先级的进程类运行1个时间片,属于次高优先级类的进程运行2个时间片,再次一级运行4个时间片,以此类推,当一个进程用完分配的时间片后,他被移到下一类。
对于批处理系统:最短作业意味着最短响应时间。
交互式进程通常遵循下列模式:等待命令、执行命令、等待命令、执行命令,不断重复。如果将每个命令看做一个独立的“作业”,可以通过最短作业优先来使响应时间最短。唯一的问题是如何从当前进程中找到那个最短的进程。
找到最短进程的方法:
老化技术
运行估计运行时间最短的那个。假设每条命令的预估运行时间为 T 0 T_0 T0。假设下一次运行时间为 T 1 T_1 T1,用这两个值加权来进行最短时间估计时间: a T 0 + ( 1 − a ) T 1 aT_0+(1-a)T_1 aT0+(1−a)T1。通过选择a的值来决定是尽快忘掉老的运行时间还是在较长一段时间内记住他们。当 a = 1 2 a=\frac{1}{2} a=21时,可以得到下面的序列:
T 0 , T 1 / 2 + T 0 / 2 , T 2 / 2 + T 1 / 4 + T 0 / 4 , T 3 / 2 + T 2 / 4 + T 1 / 8 + T 0 / 8 T_0,\quad T_1/2+T_0/2,\quad T_2/2+T_1/4+T_0/4,\quad T_3/2+T_2/4+T_1/8+T_0/8 T0,T1/2+T0/2,T2/2+T1/4+T0/4,T3/2+T2/4+T1/8+T0/8
可以看到,在三轮过后, T 0 T_0 T0在新的估计值中占的比例是 1 / 8 1/8 1/8
有时把这种通过当前测量值和先前估计值加权得到下一个新的估计值的技术称为老化(aging)。
实时系统是一种时间起着主导作用的系统。
实时系统通常可以分为硬实时和软实时:
可调度的实时系统:
实时系统可以按照响应方式进一步分类成周期性事件和非周期性事件。一个系统可能响应多个周期性的事件流。根据每个事件要处理的时间长短,系统甚至可能无法处理完所有的事件。例如:有m个周期事件,事件 i i i以周期 P i P_i Pi发生,并需要 C i C_i Ci秒CPU处理一个事件,那么可以处理事件的条件是:
∑ i = 1 m C i P i ≤ 1 \sum\limits_{i=1}^m\frac{C_i}{P_i}\leq 1 i=1∑mPiCi≤1
满足这个条件的系统称为可调度的。
调度机制与调度策略分离:调度算法以某种形式参数化,参数由用户进程填写。
用户级线程:
由运行时系统将上述进程的调度算法应用在线程上。
问题背景:
哲学家的生活中有两种交替活动时段:吃饭和思考。饿了时,试图分两次去拿左边和右边的叉子,每次拿一把,不分次序。如果成功得到两把叉子,就开始吃饭,吃完后放下叉子继续思考。
饥饿(starvation):所有程序都在不停地运行,但都无法取得进展,称为饥饿。
#define N 5 /* number of philosophers */
#define LEFT (i+N−1)%N /* number of i’s left neighbor */
#define RIGHT (i+1)%N /* number of i’s right neighbor */
#define THINKING 0 /* philosopher is thinking */
#define HUNGRY 1 /* philosopher is trying to get for ks */
#define EATING 2 /* philosopher is eating */
typedef int semaphore; /* semaphores are a special kind of int */
int state[N]; /* array to keep track of everyone’s state */
semaphore mutex = 1; /* mutual exclusion for critical regions */
semaphore s[N]; /* one semaphore per philosopher */
void philosopher(int i) /* i: philosopher number, from 0 to N−1 */
{
while (TRUE) { /* repeat forever */
think(); /* philosopher is thinking */
take_forks(i); /* acquire two for ks or block */
eat(); /* yum-yum, spaghetti */
put_forks(i); /* put both for ks back on table */
}
}
void take forks(int i) /* i: philosopher number, from 0 to N−1 */
{
down(&mutex); /* enter critical region */
state[i] = HUNGRY; /* record fact that philosopher i is hungry */
test(i); /* try to acquire 2 for ks */
up(&mutex); /* exit critical region */
down(&s[i]); /* block if for ks were not acquired */
}
void put forks(i) /* i: philosopher number, from 0 to N−1 */
{
down(&mutex); /* enter critical region */
state[i] = THINKING; /* philosopher has finished eating */
test(LEFT); /* see if left neighbor can now eat */
test(RIGHT); /* see if right neighbor can now eat */
up(&mutex); /* exit critical region */
}
void test(i) /* i: philosopher number, from 0 to N−1 */
{
if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING)
{
state[i] = EATING;
up(&s[i]);
}
}
读者写者问题,为数据库访问建立了一个模型。
第一个读者对信号量db
执行down
操作,随后的读者只是递增一个计数器rc
。当读者离开时,他们递减这个计数器,而最后一个读者则对信号量执行up
操作,这样允许一个被阻塞的写者(如果存在的话)可以访问数据库。