进程的定义:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
进程的组成
⚠️区分就绪态和阻塞态
进程状态转换
进程是一个独立的运行单位,是操作系统进行资源分配和调度的基本单位,由进程控制块PCB(核心部分)、程序段和数据段组成
PCB是进程存在的唯一标志,进程被创建时,操作系统会为其创建PCB,进程结束时,回收其PCB
多个PCB组织方式
程序的代码(指令序列,可被调度到CPU执行
⚠️程序段可被多个进程共享
存储原始数据、运行过程中产生的各种数据
- 下列C语言程序中的内容和相关数据结构位于:
全局赋值变量-正文段;未赋值局部变量-栈段;函数调用实参传递值-栈段;用malloc要求动态分配的存储区-堆段;常量值-正文段;进程优先级-PCB
原语
操作系统创建新进程:
引起进程创建的事件
进程终止:(终止原语)
引起进程终止的事件
进程通信是指进程之间的信息交换
每个进程拥有各自的内存地址空间。为了保证安全,一个进程不能直接访问另一个进程的地址空间
PV操作是低级通信方式,高级通信方式主要有以下三种:共享存储、消息传递、管道通信
设置一个共享空间,两个进程对共享空间的访问是互斥的(互斥访问通过os提供的工具实现,例如PV操作
数据以格式化的消息为单位,进程通过os提供的原语进行数据交换
- 用信箱实现进程间相互通信需要有两个通信原语:发送原语和接收原语
管道是指用于连接读写进程的一个共享文件(pine文件),实质是在内存中开辟一个固定大小的缓冲区
引入线程目的:减少程序在并发执行时所付出的时空开销,提高操作系统并发性能
⚠️:引入线程后,进程只作为除了CPU之外的系统资源的分配单位,而线程作为处理机的分配单元
由于一个进程内有多个线程,若线程的切换发生在同一个进程内部,则只需要很小的时空开销
传统操作系统 | 引入线程的操作系统 | |
---|---|---|
调度 | 进程是拥有资源,独立调度的基本单位。每次调度需要上下文切换,开销大 | 线程是独立调度的基本单位,线程切换开销远小于进程切换。同一个进程内线程切换不会引起进程切换 |
并发性 | 进程可以并发执行 | 进程可以并发执行,线程也可以并发执行。提高系统资源利用率和系统吞吐量 |
拥有资源 | 进程是系统中拥有资源的基本单位 | 线程不拥有系统资源,只有一点运行必不可少的资源。线程可以访问其隶属进程的所有资源 |
独立性 | 每个进程都有独立的地址空间和资源,除了共享全局变量,不允许其他进程访问 | 某进程中的线程对其他进程不可见。同一进程中的不同线程共享进程的地址空间和资源 |
系统开销 | 创建、撤销进程,上下文切换开销大 | 线程切换开销小,线程之间同步和通信容易实现 |
支持多处理机系统 | 进程只能在一个处理机上运行 | 可以将进程中的多个线程分配到多个处理机上执行 |
线程状态之间的转换和进程状态转换一致
用户程序启动时,通常只有一个称为“初始化线程”的线程在执行,其功能是创建新线程
创建新线程时,需要用一个线程创建函数,提供相应参数
线程的实现分为用户级线程(User-Level Thread, ULT)和内核级线程(Kernel-Level Thread)(内核支持的线程)
某些系统同时支持ULT和KLT,由链接方式的不同可以分为多对一、一对一和多对多模型
1 . 系统动态DLL库中的系统线程,被不同进程调用,属于相同的线程
处理机调度:对处理机进行分配,从就绪队列中按照一定算法,选择一个进程将处理机分配给它运行,以实现进程并发执行
调度有三个层次:高级调度、中级调度、低级调度
作业调度从外存的后备队列中选择一批作业进入内存,为其建立进程,这些进程被送入就绪队列。进程调度从就绪队列中选择一个进程,将其状态改为运行态,将CPU分配给它。中级调度是为了提高内存利用率,将暂时不能运行的进程挂起
评价不同调度算法的标准
操作系统中,用于调度和分派CPU的组件称为调度程序,主要由以下三部分组成
调度程序是操作系统内核程序,请求调度的事件发生后才可能运行调度程序,调度新的就绪程序后才会引起进程切换
需要进行进程调度与切换的情况
不能进行进程调度与切换的情况
临界资源:一个时间段只允许一个进程使用的资源,各个进程需要互斥地访问临界资源
临界区:访问临界资源的代码
内核程序临界区:用于访问某种内核数据结构,例如进程的就绪队列
上下文切换:切换CPU到另一个进程需要保存当前进程的状态并恢复另一个进程的状态,这个任务称为上下文切换。上下文切换一定发生在内核态
模式切换:用户态和内核态之间的切换。模式切换时可能没有发生进程切换
操作系统存在多种调度算法,有些适用于作业调度,有些适用于进程调度,有的两者都适用
调度算法包括:
饥饿:某个进程/作业长期得不到服务
FCFS调度算法是一种最简单的调度算法,可用于作业调度和进程调度
通常
主要用于作业调度,是FCFS和SJF调度算法的综合平衡
适用于分时操作系统
在系统中设置多个就绪队列,将不同类型或性质的进程固定分配到不同队列,每个队列可以实施不同的调度算法
对临界资源的互斥访问,在逻辑上可以分为四个部分
临界区是进程中访问临界资源的代码段,进入区和退出区是负责实现互斥的代码段
- 临界区是指并发进程中访问共享变量段的代码程序
- 一个系统中有5个并发进程涉及某个相同变量A,变量A的相关临界区是由5个临界区组成的
进程同步:同步也称作直接制约关系,是指为了完成某种任务而建立的两个或多个进程,因为在需要某些位置上协调他们的工作次序而产生的制约关系
- 必须对并发进程进行同步的原因:并发进程是异步的
进程互斥:又称间接制约关系,当一个进程进入临界区使用临界资源时,另一个进程必须等待。当占用临界资源的进程退出临界区后,另一进程才可以访问该临界资源
为了实现对临界资源的互斥访问,同时保证整体性能,需要遵循以下原则
- (21 408) 其中1,2,3必须满足,4可以不满足(例如Peterson算法
可以在执行函数时被os中断,去处理其他任务的函数
- 一个进程映像由程序、数据以及PCB组成,其中共享程序段必须用可重入编码编写
在进入区设置并检查一些标志来表明是否有进程在临界区中,若已经有,则在进入区通过循环检查进行等待,进程离开临界区后则在退出区修改标志
思想:设置一个公共变量turn
,turn==0
时允许进程P0访问,turn==1
允许进程P1访问…
问题:必须交替访问,如果turn==0
而P0不进入临界区,那么turn
将无法被修改,P1也就永远无法进行。违反了“空闲让进”的原则
思想:设置一个数组flag,标记各个进程是否想要进入临界区。每个进程在进入临界区之前,先检查当前是否有其他进程想进入临界区,如果没有,则自身标志设置为true
然后开始访问
存在的问题:如果进程P1确定将访问临界区,但还没置flag,此时进程切换P2,P2也将可以访问临界区。违反了“忙则等待”的原则
问题:虽然解决了“忙则等待”的问题,但可能双方都置flag,违反了“空闲让进”和“有限等待”的原则,可能会出现饥饿现象
思想:为了防止两个进程为了进入临界区无限等待,设置变量turn
,标识临界区是否有进程在使用。
计算机提供特殊的硬件指令,允许对一个字中的内容进行修正等。通过硬件支持实现临界段问题的方法称为低级方法,或称元方法
思想:利用“开/关中断指令”实现,不允许中断也就意味着不能发生进程切换,也就不可能发生同时访问临界区的情况
TestAndSet,简称TS指令,又称TSL指令(TestAndSetLock)
TS指令用硬件实现,执行的过程不允许被中断
// lock表示临界区是否被锁
// 检查并上锁
bool TestAndSet(bool *lock){
bool old = *lock;
*lock = true;
return old;
}
// TSL实现互斥
while(TestAndSet(&lock)){...};
// 临界区
lock = false;
剩余区
Swap指令,又称Exchange指令、XCHG指令。用硬件实现,不允许被中断
Swap(bool* a, bool* b){
bool temp = *a;
*a = *b;
*b = temp;
}
// 实现互斥
// lock表示临界区是否上锁
bool old = true;
while(old) swap(&lock, &old);
// 临界区
lock = false;
// 剩余区
一个进程在进入临界区时获得锁(acquire),在退出临界区时释放锁(release)
每个互斥锁有一个bool
变量avaiable
,表明锁是否可用,进程试图获取不可用的锁时会被阻塞,直到锁被释放
acquire(){
while(!available){...}
available = false;
}
release(){
available = true;
}
互斥锁的缺点是没有遵循忙等待,浪费CPU周期
信号量只能被两个标准原语wait和signal访问,又称P操作和V操作
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量
整型信号量存在的问题:只要S<=0,就会不断测试。不满足“让权等待”的原则,会发生忙等
记录型信号量是不存在忙等现象的进程同步机制。除了表示资源数量的整型变量value
,再增加一个进程链表L,用于链接所有等待该资源的进程
优点:遵循了“让权等待”的原则,不会出现”忙等“现象
tips:默认S为记录型信号量
- PV操作是一种低级进程通信原语
- PV操作是由两个不可被中断的过程组成的
- PV操作实现进程同步,信号量的初始值由用户确定
- 一个进程在互斥信号量mutex上执行V(mutex)操作导致唤醒另一个进程时,执行V操作后mutex的值为:小于等于0
每一对前驱关系都是一个进程同步问题(需要保证一前一后的关系,设置多个信号量
管程是一种特殊的软件模块(有点像类),由以下部分组成
管程的基本特征:
- 管程是进程同步工具,解决信号量机制大量同步操作分散的问题
- 管程每次只允许一个进程进入管程
- 管程中signal操作的作用和信号量机制中的V操作不同
- 管程是被进程调用的,管程是语法范围,无法创建和撤销
- 管程执行x.wait()时所做的工作:阻塞该进程,并将其插入x的阻塞队列中
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区取出一个产品使用(产品理解为一种数据
生产者和消费者共享一个初始为空,大小为n的缓冲区
只有缓冲区没有满,生产者才能把产品放入缓冲区,否则必须等待
缓冲区不空时,消费者才能从中取出产品,否则必须等待
缓冲区是临界资源,进程必须互斥访问
semaphore mutex = 1; // 互斥信号量
semaphore empty = n; // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0; // 同步信号量,表示产品的数量
producer(){
while(1){
生产一个产品;
P(empty);
P(mutex);
产品放入缓冲区;
V(mutex);
V(full);
}
}
consumer(){
while(1){
P(full);
P(mutex);
从缓冲区使用取出一个产品;
V(mutex);
V(empty);
使用产品;
}
}
桌子上有一只盘子,每次只能向其中放入一个水果。爸爸只放苹果,妈妈只放橘子;儿子只吃橘子,女儿只吃苹果。
盘子空时,爸妈才能向盘子中放水果;盘子中有自己想吃的水果时,儿女才能从盘子中取出水果
semaphore apple = 0; // 苹果
semaphore orange = 0; // 橘子
semaphore plate = 1; // 盘子
dad(){
while(1){
准备一个苹果;
P(plate);
苹果放入盘子;
V(apple);
}
}
mom(){
while(1){
准备一个橘子;
P(plate);
橘子放入盘子;
V(orange);
}
}
daughter(){
while(1){
P(apple);
从盘中取出苹果;
V(plate);
吃掉苹果;
}
}
son(){
while(1){
P(orange);
从盘中取出橘子;
V(plate);
吃掉橘子;
}
}
- 九个生产者、六个消费者共享容量为8的缓冲器的生产者-消费者问题中,互斥使用缓冲器的信号量初始值为:1
有读者和写者两组并发进程,共享一个文件,要求如下:
- 允许多个读者同时对文件执行读操作
- 只允许一个写者往文件中写信息
- 任意写者在完成写操作之前不允许其他读者或写者工作
- 写者执行写操作前,必须让已有的读者和写者全部退出
semaphore rw = 1; // 实现读写互斥
int count = 0; // 记录当前有几个读进程在访问文件
semaphore mutex = 1;// 保证对count变量的互斥访问
// semaphore w = 1; // 实现写优先
writer(){
while(1){
// P(w);
P(rw);
写文件;
V(rw);
// V(w);
}
}
reader(){
// P(w);
P(mutex);
if(count == 0) // 第一个读进程来上锁
P(rw);
++ count;
V(mutex);
// V(w);
读文件;
P(mutex);
-- count;
if(count == 0) // 最后一个读进程来解锁
V(rw);
V(mutex);
}
count
, 遇到不太好解决的同步-互斥问题,需要考虑能否通过互斥访问的计数器count
解决一张圆桌上有五名哲学家,每两个哲学家之间的桌子上摆一根筷子,桌子中间是食物
哲学家思考时,不影响其他人。哲学家饥饿时,会试图拿起左右两根筷子(一根一根拿起),如果筷子在其他人手上,则需要等待。饥饿的哲学家只有同时拿着两根筷子才可以开始进餐。进餐完毕后,放下筷子继续思考
一个系统有三个抽烟者进程和一个供应者进程
每个抽烟者需要用三种材料卷烟并抽掉。第一个拥有烟草,第二个拥有纸,第三个拥有胶水
供应者无限地供应三种材料,每次将两种材料放在桌子上,拥有第三种材料的抽烟者就会使用它们卷烟并抽掉,然后给供应者一个信号告诉完成了,供应者就会放另外两种材料,这个过程一直重复(让三个抽烟者轮流抽烟
semaphore offer1 = 0;
semaphore offer2 = 0;
semaphore offer3 = 0;
semaphore finish = 0;
int i = 0; // 用于实现三个抽烟者轮流抽烟
provider(){
while(1){
if(i == 0){
组合1放在桌子上;
V(offer1);
}else if(i == 1){
组合2放在桌子上;
V(offer2);
}else{
组合3放在桌子上
V(offer3);
}
i = (i+1)%3;
P(finish);
}
}
smoker1(){
while(1){
P(offer1);
拿走组合1,卷烟抽掉;
V(finish);
}
}
smoker2(){
while(1){
P(offer2);
拿走组合2,卷烟抽掉;
V(finish);
}
}
smoker3(){
while(1){
P(offer3);
拿走组合3,卷烟抽掉;
V(finish);
}
}
在并发环境下,各进程竞争资源导致的一种互相等待对方手中的资源,导致各个进程都被阻塞,都无法向前推进的现象 。如果没有外力推动,进程将无法向前推进
例如:某计算机系统只有一台打印机和一个输入设备,P1占有打印机,同时请求输入设备;P2占有输入设备,同时请求打印机。此时两个进程陷入死锁
系统中不可剥夺资源的数量不能满足多个进程运行的需要
进程在运行过程中,请求和释放资源的顺序不当;信号量使用不当也会产生死锁
只要以下任意一个条件不满足,死锁就不会发生
为了系统不发生死锁,必须设法破坏四个必要条件之一
破坏导致死锁的必要条件
避免死锁属于事先预防策略
安全序列:如果系统按照这种序列分配资源,可以满足每个进程对资源的最大需求,则每个进程都可以顺利完成
只要找出一个安全序列,系统就是安全状态。安全序列可能会有多个
系统处于安全状态时,一定不会发生死锁;进入不安全状态,有可能发生死锁;发生了死锁一定在不安全状态
银行家算法核心思想:在进程提出资源申请时,先预判这次分配是否会导致系统进入不安全状态,如果会,则暂时阻塞该进程
银行家算法过程-解题
1. **若系统中有n个进程,每个进程需要使用某种临界资源m个,则不会发生死锁的该类资源的总数至少为:m+(n-1)(m-1)**
2. 区分死锁预防和死锁避免
3. 产生死锁的根本原因在于系统资源分配不足和进程推进顺序非法
(17 408)