前驱图和程序执行
- 前驱图:用于描述程序执行顺序的有向无环图,A是B的前驱表示B执行之前A必须完成。
- 程序的顺序执行
顺序执行的特征:顺序性(每一步必须在下一步开始之前结束)、封闭性(资源只有本程序能修改,执行结果不受外界影响)、可再现性(只要环境与初始条件相同,结果必定相同)。 - 程序的并发执行
并发执行的特征:间断性、失去封闭性、不可再现性。
进程的描述
- 进程的定义和特征
进程的定义(有多个):
- 进程是程序的一次执行。
- 进程是一个程序及其数据在处理机上顺序执行是所发生的活动。
- 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
进程的特征:动态性、并发性、独立性、异步性。 - 进程的基本状态及转换
三种基本状态:就绪状态、执行状态、阻塞状态。
三种基本状态的转换:
创建状态和终止状态
- 创建状态:创建一个进程比较复杂,要经过多个步骤(首先由进程申请PCB空间,并向PCB中填写用于控制和管理的信息;然后为进程分配运行必须的资源;最后把该进程转入就绪状态,插入就绪队列),如果创建时系统资源不足,创建未完成,无法调度执行,此时称为创建状态。
- 终止状态:两个步骤,首先等待操作系统善后处理;最后将其PCB清零,返回系统空间。 - 挂起操作和进程状态的转换
为什么要引入挂起机制?终端用户的需求、父进程请求、负荷调节的需要、操作系统的需要。引入挂起之后,进程状态机为:
- 进程管理中的数据结构
计算机系统中,每个资源、每个进程都设置了一个数据结构以表征其实体。此数据结构中包含资源或进程的标识、描述、状态等信息及一批指针。
其中,进程的信息表(进程控制块PCB)的作用为:作为独立运行的基本单位的标志;能实现间断性运行方式;提供进程管理所需要的信息;提供进程调度所需要的信息;实现与其他进程的同步与通信;
进程控制块中的信息:
1)、进程标识符,唯一的标识一个进程。一个进程通常有两种标识符,外部标识符和内部标识符。
2)、处理机状态,处理机的上下文,由处理机中各种寄存器的内容组成。
3)、进程调度信息,进程状态、进程优先级、调度所需其他信息(如以等待CPU的时间总和,已执行时间总和)、事件(阻塞原因)。
4)、进程控制信息,程序和数据地址、进程同步和通信机制(如消息队列、信号量等,可能全部或部分的存放在PCB中)、资源清单(如除CPU之外的全部所需资源和已分配到该进程的资源)、链接指针(本进程所在队列的下一个PCB的首地址)。
进程控制块的组织方式:线性方式(所有PCB在一张线性表中)、链接方式(多个PCB链表队列,如就绪队列、阻塞队列)、索引方式(按照进程状态建立索引表,索引表中存放处于该状态的PCB在PCB表中的地址)。
进程控制
包含:创建新进程、终止已完成进程、将因发生异常情况而无法继续运行的进程置于阻塞状态、负责进程运行中的状态转换等功能。进程控制一般由OS内核中的原语来实现的。
- 操作系统的内核
包含支撑功能:中断处理、时钟管理、原语操作;资源管理功能:进程管理、存储器管理、设备管理; - 进程的创建
进程有层次结构,所有的进程都由一个根进程创建,每个进程都可以创建自己的子进程。
引起进程创建的事件:用户登录、作业调度、提供服务、应用请求。
进程创建过程:
1. 申请空白PCB,为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
2. 为新进程分配其运行所需要的资源,包括各种物理或逻辑资源。如内存、文件、IO设备和CPU时间等。
3. 初始化进程控制块PCB。
4. 如果进程就绪队列能接受新进程,就将新进程插入就绪队列。 - 进程的终止
引起进程终止的事件:正常结束、异常结束、外界干预。
进程终止过程:
1. 根据被终止的进程标识符,从PCB集合中检索出该进程的PCB,从中读出进程状态。
2. 若正在执行,就立即终止。并置调度标志为真,用于指示该进程被终止后应重新进行调度。
3. 若进程还有子孙进程,应将其所有子孙进程都终止。
4. 将被终止的进程所拥有的全部资源归还父进程或者系统。
5. 将被终止进程的PCB从所在队列中移出,等待其他程序来搜集信息。 - 进程的阻塞与唤醒
引起进程阻塞或唤醒的事件:向系统请求共享资源失败、等待某种操作的完成、新数据尚未到达、等待新任务的到达。
进程阻塞过程:正在执行的进程,发生上述事件后调用block原语将自己阻塞,是进程的主动行为。进入block过程后,立即停止进程执行,PCB状态改为阻塞,并将PCB插入阻塞队列,最后转调度程序重新调度,将处理机分配给就绪的进程。
进程唤醒过程:当进程所期待的事件发生时,有该事件相关进程调用wakeup原语唤醒此进程。首先把PCB从阻塞队列中移出,把状态改为就绪,然后插入就绪队列。 - 进程的挂起与激活
进程同步(重要)
- 进程同步的基本概念
两种形式的制约关系:间接相互制约关系和直接相互制约关系。
临界资源:如打印机等硬件资源属于临界资源,需要进程之间互斥的共享。临界区:无论硬件还是软件临界资源,多个进程必须互斥的访问,把每个进程中访问临界资源的代码叫做临界区(临界区是某个进程中的一段代码)。
同步机制应遵循的规则:空闲让进、忙则等待、有限等待、让权等待。 - 硬件同步机制
可以利用软件来同步,但是较为复杂,现在的计算机提供了特殊的硬件指令,来解决临界区的问题。
1. 关中断:最简单的方法之一,在进入锁测试之前关闭中断,不响应中断,不会引发调度,也就不会发生进程或线程切换。完成锁测试并上锁之后再打开中断。
缺点:滥用关中断权力可能导致严重后果;关中断时间过长影响系统效率;不适用与多CPU系统,只关一个CPU上的中断并没有作用。
2. 利用Test-and-Set指令实现互斥:允许对一个字中的内容进行检测和修正。(一条指令不可分割。)
3.利用Swap指令实现进程互斥:交换两个字的内容。 - 信号量机制
1.整型信号量:表示资源数目的整型量S,除初始化外,只能通过两个标准的原子操作来访问(wait(S)、signal(S))来访问。
wait操作只要是信号量S小于等于0,就会不断的进行测试,进程进入忙等状态。
2.记录型信号量:采用让权等待策略。但会出现新问题,多个进程等待同一资源,所以需要在整型资源数目值之外新增进程链表指针用来链接等待的进程。
3.AND型信号量:有些场合一个进程需要访问多个资源后方能执行任务,此时多个资源都是临界资源,需要AND型信号量。
4.信号量集:PV操作只能对信号量进行加一减一,当一次需要N个资源时需要多次P操作,而且系统还需要拒绝申请的资源数过少的请求。所以进程申请某类资源时,每次分配前要测试资源数量,判断是否大于可分配下限值。 - 信号量应用
使用信号量实现进程互斥:为某临界资源设置信号量mutex为1,再把每个进程中的临界区放在wait(mutex)和signal(mutex)之间即可。
利用信号量实现前驱关系:如S1、S2分别为两个进程中的两个操作,要求S1先于S2执行,可以这么写:
//设置信号量S,初始化为0。
//如果进程2先执行,因为S为0,所以进程2必须等待进程1用signal增加信号量才能执行。
//进程1
S1;signal(S);
//进程2
wait(S);S2;
使用这种方式可以在多个进程之间的语句之间构成前驱后继的关系。
- 管程机制
管程的定义:系统中可用少量信息和对该资源所执行的操作来表征该资源。
由定义可知管程由4部分组成:管程的名称;局部于管程的共享数据结构说明;对该数据结构进行操作的一组过程;对局部于管程的共享数据设置初始值的语句。
条件变量:利用管程实现进程同步时,必须设置同步工具(如两个同步操作原语wait和signal),当某进程通过管程请求获得临界资源未能满足时,管程调用wait语句使该进程等待,并将其排列在等待队列上,仅当另一进程释放该资源时,管程调用signal原语唤醒等待队列中的队首进程。
经典进程的同步问题
- 生产者-消费者问题
1.利用记录型信号量解决:利用信号量empty和full分别表示缓冲池中空缓冲区和满缓冲区的数量。
//生产或者消费的数组下标
int in=0,out=0;
//数组型的缓冲区
item buffer[n];
//进程互斥信号量mutex和缓冲区数量
semaphore mutex=1,empty=n,full=0;
void proceducer{
while(true){
producer an item nextp;
//放入缓冲区,一定要先看缓冲区信号量再看互斥信号量
//否则,如果缓冲区满,生产者拿到互斥锁或缓冲区空,消费者拿到互斥锁,都会引起死锁
wait(empty);
wait(mutex);
buffer[in] = nextp;
in = (in + 1) % n;
signal(mutex);
signal(full);
}
}
void consumer{
while(true){
wait(full);
wait(mutex);
nextc = buffer[out];
out = (out + 1) % n;
signal(mutex);
signal(empty);
comsumer the item in nextc;
}
}
2.利用AND信号量解决:使用Swait操作替换连续两个wait操作,意思是缓冲区信号量和互斥信号量同时满足时才加锁,不用强调先后顺序。Ssignal操作也可以替换连续两个signal,但是并不影响什么,因为释放操作并不会阻塞进程造成死锁。
3.利用管程解决:管程可以自动的保证同时只有一个进程使用管程,避免了程序员自己书写互斥信号量。
//如下是一个管程,有属性值,有方法,还有初始化的代码块,看样子是一个类。
Monitor producerConsumer{
item buffer[N];
int in, out;
condition notFull, notEmpty;
int count;
public:
{in=0;out=0;count=0;}
void put(item x){
//如果满了就挂起等待不满的信号量,让另一个进程使用管程。
if(count>=N) cwait(notFull);
//否则进入执行
buffer[in] = x;
in = (in+1) % N;
count++;
csignal(notEmpty);
}
void get(item x){
if(count<=0) cwait(notEmpty);
x = buffer[out];
out = (out+1) % N;
count--;
csignal(notFull);
}
}PC;
//最后的PC应该指的是实例化的管程对象
使用上述的管程解决生产者消费者问题:
void producer(){
item x;
while(true){
produce an item in nextp;
PC.put(x);
}
}
void consumer(){
item x;
while(true){
PC.get(x);
consume the item in nextc;
}
}
- 哲学家进餐问题
- 利用记录型信号量解决:5只筷子每只设置一个信号量。
semaphore chopstick[5] = {1, 1, 1, 1, 1};
//第i位哲学家进餐可如下描述
while(true){
wait(chopstick[i]);
wait(chopstick[(i+1) % 5]);
eat;
signal(chopstick[i]);
signal(chopstick[ i+1 ]);
think;
}
上述方式在5个哲学家同时拿起左边的筷子进餐时,会出现死锁,所有哲学家都在等待别人放下自己右边的筷子。因而有以下三个办法解决:
(1) 、最多允许四位哲学家同时去拿左边的筷子。
(2) 、仅当哲学家左右两边的筷子都可用时,才允许他拿起筷子进餐。
(3) 、奇数号的哲学家先拿左边的筷子,偶数号的相反。
- 利用AND信号量解决:哲学家进餐本质上是AND信号量中所说的多个临界资源的情况
semaphore chopstick[5] = {1, 1, 1, 1, 1};
while(true){
think;
Swait(chopstick[i], chopstick[(i+1) % 5]);
eat;
Ssignal(chopstick[i], chopstick[(i+1) % 5]);
}
- 读者写者问题
- 利用记录型信号量解决:
semaphore rmutex = 1, wmutex = 1;
int readcount = 0;
void reader(){
while(true){
wait(rmutex);
//如果没有进程在读,需要抢写入信号量
if(readcount == 0) wait(wmutex);
readcount++;
signal(rmutex);
//read operation;
wait(rmutex);
readcount--;
//当前的PV操作之间不会有新的读取进程进入
//此时readcount为0表示没有任何进程占用文件
//可以释放写入信号量,让写入进程在读取进程被挡住的时候进入
if(readcount == 0) signal(wmutex);
signal(rmutex);
}
}
void writer(){
while(true){
wait(wmutex);
//write operation;
signal(wmutex);
}
}
- 利用信号量集机制:解决最多允许RN个读者
int RN;
semaphore L = RN, mutex = 1;
void reader(){
while(true){
//下面这句等价于wait(L),用来控制读者数量
Swait(L, 1, 1);
//下面这句不消耗信号量,只用来检测信号量是否存在
//此信号会被写者消耗,应该可以和上句互换位置
Swait(mutex, 1, 0);
//read operation;
Ssignal(L, 1);
}
}
void writer(){
while(true){
//mutex参数块用于阻止其它写者和新的读者进入,L参数块用于检测是否存在正在读的读者
Swait(mutex, 1, 1, L, RN, 0);
//write operation;
Ssignal(mutex, 1);
}
}
进程通信
- 进程通信的类型
- 共享存储器系统:互相通信的进程可以共享某些数据结构或共享存储区,依照此区别分为基于共享数据结构的通讯方式和基于共享存储区的通讯方式。
- 管道通信系统:“管道”,是指用于连接一个读进程和写进程以实现它们之间通信的一个共享文件,又名pipe文件。写进程以字符流的形式将大量数据送入管道,而读进程从管道中接收数据。
为协调双方通信,管道机制必须提供以下三方面的协调能力:互斥,当一个进程正在对pipe文件进行读写操作时,其它进程必须等待;同步,写进程输入一定数量(比如4KB)的数据时,便睡眠等待,直到读进程把这些数据读走,同样,当读进程读空管道时,也睡眠等待直到写进程在管道中写入数据;确认对方存在,只有对方存在时才进行通信。 - 消息传递系统:将发送的消息按一定格式,通过操作系统提供的通信命令传递。按照实现方式分为直接通信方式和间接通信方式。
- 客户机-服务器系统:利用远程方法调用(RPC)来完成进程间通信。
- 消息传递通信的实现方式(上述第三点)
- 直接消息传递系统:发送进程利用OS所提供的发送命令(原语)直接把消息发送给目标进程。
(1)、直接通信原语
对称寻址方式:要求发送进程和接受进程都必须以显式的方式提供对方的标识符
如:send(receiver, message);和receive(sender, message);
里面的发送者和接受者都写死了,缺点是如果某个进程改名了,写死的地方改起来不方便。
非对称寻址方式:只要一方写标识符即可。
如:send(P, message);和receive(id, message);
在使用打印服务的情况中,接受信息的打印进程不需要知道谁在使用自己,可以用id变量表示发送者,只要求发送者写明接受者的标识。
(2)、消息的格式
可以采用处理简单的定长的格式,也可以采用处理较为麻烦的变长的格式,方便发送较长消息,根据不同情况选择不同的格式。
(3)、进程的同步方式
两个通信的进程,它们之间的同步情况共有三种情况:① 发送和接收进程都阻塞,这种情况用于进程之间的紧密同步,发送方和接收方之间没有缓存时使用;② 发送进程不阻塞,接收进程阻塞,应用最广的同步方式,接收进程等接收到消息的时候被唤醒工作;③ 发送和接收进程都不阻塞,也是非常常见的同步方式,平时,两个进程都正常运转,发生某件事时把自己阻塞起来等待。
(4)、通信链路
进程之间通信,要有通信链路,通信链路的建立方式有两种方式。① 发送进程在发送信息之前显式的使用“建立连接”命令,请求系统建立链路,使用完成后拆除链路,常用于计算机网络中。② 直接使用系统提供的发送命令,由系统自动的建立链路,常用于单机系统中。链路本身也有两类,分为单向通信链路和双向通信链路。 - 间接消息传递系统(信箱通信):利用对一个中间实体(如存放在内存缓冲区的共享数据结构)来完成通信,发送方把消息放在中间实体(信箱)中,由核准过后的接收方取出,即可实现实时通信也可实现非实时通信。
(1)、信箱的结构:分为存放描述信息的信箱头和由若干个存放消息的信箱格组成的信箱体。
(2)、信箱通信原语:系统要提供创建和撤销信箱的命令,以及利用信箱发送消息和接收消息的命令。
(3)、信箱的类型:信箱可有操作系统创建,也可由用户进程创建,谁创建的是就是拥有者。据此可把信箱分为三类:
私用邮箱,用户进程为自己创建,可读,其他用户只能将自己的消息发送到信箱中。创建信箱的进程结束后信箱消失。
公用邮箱,操作系统创建,供核准进程使用,可读写,系统运行期间始终存在。
共享邮箱,某进程创建并制定共享进程,可读写。
(4)、信箱通信时,发送者和接收者之间存在四种关系:一对一,多对一(提供服务的进程与多个用户交互),一对多(广播进程对多个进程广播),多对多。 - 直接消息传递系统实例
利用原语send和receive来传递消息,需要用到一个缓冲区(此缓冲区包含发送者标识,长度、消息正文和指向下一个缓冲区的指针)作为中介。
使用send原语发送之前,要先在发送者进程自己的内存空间设置一个发送区,里面有消息正文,发送进程标识符,消息长度等信息。调用send原语时,先把发送区中的信息拷贝到缓冲区中,然后获取到接收者进程的内部标识,再然后使用接收者PCB中有关通信的数据项(PCB,进程控制块,里边是有关此进程的变量,这些变量里面当然会有自己的消息队列句柄)将缓冲区挂载到接收者进程上。
使用receive原语接收消息时,把自己消息队列句柄上挂在的缓冲区中的内容复制到自己进程的内存空间中的接收区中,然后释放缓冲区。
//receiver是接收者标识,a是发送者的发送区
void send(receiver, a){
//根据发送区中消息的长度申请缓冲区
i = getBuff(a.size);
//然后把a中的信息拷贝到缓冲区
copy info of a to buffer;
//获取接收者的内部标识
j = getId(PCBset, receiver);
//每个进程的消息队列句柄都是临界资源,操作时要加锁
wait(j.mutex);
//把缓冲区挂载到接收者的消息队列句柄上
insert(&j.mq, i);
signal(j.mutex);
//增加消息队列的资源信号量
signal(j.sm);
}
//b是接收者的接收区
void receive(b){
j = getId(PCBset, receiver);
wait(j.sm);
wait(j.mutex);
remove(&j.mq);
signal(j.mutex);
copy info of buffer to b;
free(i);
}
线程(Threads)的基本概念
以前,进程是OS 分配资源 和 调度 的基本单位,后来为了进一步提高并发能力,提出了线程,作为OS调度的基本单位,分配资源还是以进程为基本单位。当然,线程也有自己用的少量资源,比如线程控制块TCB、一组寄存器和堆栈等等。
书上讲了很多方面,用来让读者理解线程是什么,和进程是什么关系,先略过。
线程的实现
实现方式有三种:内核支持线程、用户级线程、组合方式,书上讲了他们的优缺点,还讲了每种方式有可操作性的具体实现,这里也先略过了