一、进程
进程是指一段程序的执行过程,会消耗系统资源如CPU、内存、网络等。
一个进程包含静态代码段,数据,寄存器地址等
进程的特点
-
动态性(可动态地创建、结束进程)
-
并发性(进程被独立调度并占用处理器运行)
-
独立性(不同进程地工作不会相互影响)
-
制约性(因访问共享数据或进程同步产生制约)
进程控制块(PCB)是操作系统用于管理进程的信息集合,PCB描述进程的情况及状态,是进程存在的唯一标志。包含进程标识信息、处理机状态信息、进程控制信息
二、线程
由于进程内部的各部分可能不能并发执行,而拆分成多进程运行又会产生较大的开销。所以需要提出一种新的实体,这种实体之间可以并发执行且共享相同的地址空间。
线程是指进程中的一条执行流程
优点:位于同一进程的线程可以并发执行且共享相同的资源等
缺点:一个线程崩溃会导致同一进程的所有线程崩溃
每个线程有独立的寄存器和堆栈
进程是资源分配单位,线程是CPU调度单位
线程相较于进程能减少并发执行的时间和空间开销,表现为线程的创建时间和终止时间都比进程短;同一进程内的线程间切换时间比进程间切换时间短;由于同一进程的的各线程共享内存和文件资源,之间通信不需要通过内核
用户线程不依赖于操作系统的内核由用户级的线程库函数来完成线程管理
三、进程/线程状态
进程和线程的生命周期
线程的状态转换与进程相同,有就绪、运行、阻塞等状态
挂起是指当进程没有占用内存空间时,操作系统把该进程从内存转入外存,处于挂起的进程映像在磁盘上,以便操作系统更好的利用内存资源
当进程处于阻塞挂起状态(Block-suspend)时,进程在外存并等待某事件出现
当进程处于就绪挂起状态(Ready-suspend)时,进程在外存,进入内存即可运行
解挂/激活(Activate)是指把一个挂起的进程从外存转到内存上
当没有较高优先级的就绪进程或挂起就绪进程时,操作系统会把就绪挂起进程转换为就绪进程
当内存有足够的空闲空间时,操作系统会把一个高优先级阻塞挂起进程转换为阻塞进程
四、进程/线程间的通讯方式
IPC概述
直接通信
-
send(P1, message) 发送消息到进程1
-
receive(P2, message) 发送消息到进程2
-
自动建立通信链路
-
一条链路恰好对应一对通信进程
-
链路通常是双向的
间接通信
-
send(A message) 发送消息到队列A
-
receive(A, message) 从队列A接收消息
-
每一个消息队列有唯一的ID
-
只有共享了一个消息队列才能通信
-
通信链路和进程可以多对多
-
单向或者双向
信号
软件中断通知事件处理,例如SIGFPE, SIGKILL。原理时修改堆栈中的下一条指令的入口地址
进程接收到信号时可以执行进程自定义的信号处理函数,可以执行操作系统的默认操作,可以闭塞信号
信号不能传送任何数据
管道
用于一个进程的输出作为另一个进程的输入,Linux命令中的 " | " 符号
消息队列
消息以字节序列存储,规则为FIFO
共享内存
直接通讯。两个进程共享一段内存空间,可以直接从里面取数据。
- 快速方便地共享数据
- 但必须同步数据访问
信号量(Semaphore)
一个整形数据(sem),有两个原子操作(原子操作要么不执行要么执行完,不可分割)。可将信号量理解为当前可用资源数。P操作和V操作时改变信号量的唯一方式。
P( )操作:sem减1(该进程请求一个资源)
- 如果此时sem < 0(当前没有可用资源,且有 | sem | 个进程在等待),该进程等待。
- 如果此时sem >= 0(该进程获得资源),继续运行。
V( )操作:sem加1(该进程释放一个资源)
- 如果此时sem <= 0(等待队列中有进程等待),唤醒一个等待的进程
- 如果此时sem > 0,继续运行
信号量及操作的实现
class Semaphore{
int sem;
WairQueue q; //该信号量对应的等待队列
}
Semaphore::P(){
sem--;
if(sem<0){
Add this thread to q;
block(p);
}
}
Semaphore::V(){
sem++;
if(sem<=0){
remove a thread from q;
wakeup(t);
}
}
使用二进制信号量(初值为1)可以实现进程的互斥,即在临界区代码前执行P操作,临界区代码后执行V操作
mutex = new Semaphore(1);
//Process A
mutex->P();
/*
Critical Section;
*/
mutex->V();
使用二进制信号量(初值为0)可实现进程的同步,当线程A运行到某一个点时需要线程B运行完某一部分的代码才能继续执行
condition = new Semaphore(0);
// Thread A
...
condition->P();
...
// Thread B
...
condition->V();
...
使用信号量的特点
- 容易实现互斥和同步
- 阅读、开发代码比较困难
- 容易出错
- 不能解决死锁问题
经典同步问题:生产者和消费者
- 在任何一个时间只能有一个线程操作缓冲区(互斥问题),设置二进制信号量mutex = 1
- 当缓冲区全为空时,消费者必须等待生产者生产(同步问题),空缓冲区信号量为empty = n
- 当缓冲区全为满时,生产者必须等待消费者消费(同步问题),满缓冲区信号量为full = n
mutex = new Semaphore(1);
empty = new Semaphore(10);
full = new Semaphore(0);
//初始的缓冲区为空
void produce(){
empty->P();
mutex->P();
// add product to buffer
mutex->V();
full->V();
}
void consume(){
full->P();
mutex->P();
// remove product from buffer
mutex->V();
empty->V();
}
管程(Monitor)
管程包含一个锁(互斥变量,代表管程的使用权),若干个条件变量(每个条件变量对应有一个等待队列)。管程外有一个进入管程的等待队列。
- 当一个线程取得lock,可以访问管程中的数据,函数等
- 若这个线程有某个共享变量得不到满足,则该线程进入对应的等待队列,释放lock
Lock::Acquire() 等待lock可用,然后抢占
Lock::Release() 释放lock,唤醒进入管程队列的等待者
ConditionVariable::Wait() 释放lock,该进程进入对应的等待队列
ConditionVariable::Signal() 唤醒等待者
使用管程解决生产者消费者问题
class BoundedBuffer{
Lock lock;
int count=0; //当前缓冲区的产品数量
Condition notFull, notEmpty; //非满条件变量,非空条件变量
}
BoundedBuffer::produce(){
lock->Acquire(); //lock的控制在进程的最外侧,因为要保证一个管程只能被一个线程访问
while(count==n) notFull->Wait(&lock);
// add product to buffer;
count++;
notEmpty->Signal();
lock->Release();
}
BoundedBuffer:consume(){
lock->Acquire();
while(count==0) notEmpty->Wait(&lock);
//remove product from buffer;
count--;
notFull->Signal();
lock->Release();
}
五、进程调度算法
调度原则
(1)CPU使用率:忙状态所占时间的百分比
(2)吞吐量:在单位时间内完成的进程数量
(3)周转时间:一个进程从初始化到结束的时间差
(4)等待时间:进程在就绪队列中的总时间
(5)响应时间:从一个请求提交到产生第一次响应所花费的总时间
优化调度算法的目的就是减少响应时间,增加吞吐量,减少等待时间
FCFS(先来先服务)
-
若有大作业先到达,后面的小作业的等待时间就会过长,这会造成较大的平均周转时间
-
CPU密集型进程会导致I/O设备闲置,导致I/O密集型进程也在等待,资源利用率较低
SPN(短进程优先)
-
选择下一个最短的进程进行处理。
-
非抢占式是指当一个长进程正在运行时有短进程到达,等该长进程处理结束后选择最短的进程调度
-
抢占式是指当一个长进程正在运行时有短进程到达,若长进程的剩余时间大于短进程时间,长进程转为就绪状态,短进程调度转为运行态
-
缺点是连续的短进程会使长进程饥饿、需要预估下一个CPU突发的持续时间
HRRN(最高响应比优先)
R = (waiting time + service time) / service time
- 关注进程等待了多长时间,R值较大的优先,不可抢占,饥饿现象因此得到了缓解
Round Robin(轮循)
以时间片为单位,让各个进程轮流占用CPU
-
优化了平均等待时间,但增加了进程切换的开销
-
时间片不能太大或太小,应调整时间片到开销占1%左右
Multilevel Feedback Queues(多级反馈队列)
有n个不同优先级的队列,一个进程可以在队列之间移动
在不同列队之间采用由下级调度,在一个队列中采用轮循算法调度
轮循时间片的大小随优先级降低而增加。如果进程在一个时间片中没有完成,降低一个优先级
- CPU密集型任务会很快降低优先级,使得这个任务能有更多的时间使用CPU
- I/O密集型任务会停留在高优先级
Fair-share Scheduling(公平共享调度)
控制用户对系统资源的访问
- 不重要的用户组无法垄断资源
- 未使用的资源按照已分配的比例分配
- 未达到资源使用率目标的组获得更高的优先级
六、死锁
死锁现象从生产者和消费者问题中可以体现。
死锁特征
- 互斥:一个时刻只能有一个进程使用资源
- 持有且等待:进程持有互斥资源且等待获取其他进程地额外资源
- 不可抢占
- 循环等待
死锁的处理方法
问题:要确保系统永远不会进入死锁状态,就会对系统做出较多约束,从而限制了其功能。当进入死锁状态时,恢复死锁需要很大的开销,影响效率。
实际上大多数操作系统忽略死锁问题,包括Unix
死锁预防
- 使资源共享(打破互斥条件)
- 一个进程只能同时占有一个资源,若占有资源不能请求其他资源(打破占有并等待条件),这样可能导致资源利用率低
- 若进程占有某些资源,并请求其他不能被立即分配的资源时,释放当前占有的资源(打破不可抢占条件)
- 对所有资源类型进行排序,并要求每个进程按照资源顺序申请(打破循环等待条件)
死锁避免
- 每个进程声明它需要的最大资源数
- 资源分配状态包括提供与分配的资源数量,进程需要的最大资源数量
死锁避免算法动态检查资源分配状态,确保永远不会出现循环等待状态(该状态为不安全状态,但不一定死锁)。安全状态是指以P1、P2…Pn的进程序列运行,对于每一个Pi,都有Pi要求的最大资源数能够由Pi当前占有资源数+所有Pj持有的资源数满足。
银行家算法
该算法数据包括
-
进程数量(n)
-
资源类型数量(m)
-
进程最大需求量(n行m列矩阵Max,Max[i] [j]=k表示进程 i 需要的类型 j 资源的实例 k 个)
-
剩余空闲资源数(长度为 m 的数组Available,Available[j] = c 表示类型 j 资源当前有 c 个空闲)
-
已分配资源数(n行m列矩阵Allocation,Allocation[i] [j]=k表示进程i当前占有类型 j 资源的实例 k 个)
死锁检测
将资源分配图简化为进程等待图
死锁恢复
- 终止所有死锁进程
- 在一个事件内终止一个进程直到恢复,终止进程的顺序应该依据优先级、运行时间、占用资源等
七、内存管理
地址空间 & 地址生成
物理地址空间:直接在硬件上计算的地址空间
逻辑地址空间:一个运行的程序所拥有的地址空间
连续内存分配:内存碎片与分区的动态分配
内存碎片分为外部碎片和内部碎片。一个程序请求内存空间时,系统给程序分配一段内存空间(分配单元)。多个分配单元不一定能刚好占满整个内存,其空闲区域称为外部碎片。一个程序所需空间不一定沾满整个分配单元,其空闲区域称为内部碎片。
分配策略
(1)首次适配:按地址顺序找到第一个可分配的区域。特点是简单,易于产生较大空闲块,但容易产生外部碎片
(2)最优适配:找到空闲区大小最接近的进行分配。特点是较小的程序多时非常有效,但容易产生很小的外部碎片,重分配慢
(3)最差适配:找到空闲区大小差距最大的进行分配。特点是中等程序效果好,但是破碎了大空闲块以致大分区无法被分配
连续内存分配:压缩式与交换式碎片整理
压缩式碎片整理,重置程序以合并小碎片
交换式碎片整理,在某个程序运行时需要更多的内存,抢占等待状态的程序或回收它们的内存
非连续内存分配:分段
根据地址映射关系,将程序的各个部分如堆、运行栈、数据、代码段分离分散到多个物理地址空间
非连续内存分配:分页
划分物理地址空间至固定大小的帧,一个物理地址包含帧号和帧内偏移
划分逻辑地址空间至固定大小的页,一个逻辑地址包含页号的页内偏移
如何以逻辑地址映射物理地址?
(1)根据页号和帧号的映射关系(页表)找到所在的帧号
(2)帧内偏移和页内偏移相同
(3)计算出物理地址
页面置换算法
(1)最优置换:对于每一个内存的页面,计算它的下一次访问的时间,还需要等待多长时间,选择等待时间最长的那个置换出去。这是一个理想情况,但在实际的操作系统很难去预测页的下一次访问时间
(2)先进先出(FIFO):最早进入内存的页被置换出去。性能较差,调出的页面可能是经常访问的页面
(3)最近最久未使用(LRU):选择最久未被访问的也置换出去
(4)时钟页面置换(Clock):在页表项中设置一个访问位,0表示未被访问,1表示已被访问,把所有页组成环形链表,指针指向最早进来的页。当缺页中断时,判断指针指向的页,若为0,直接淘汰,若为1,把该访问位置为0,指针指向下一个页,直到淘汰一个页。该算法兼顾了FIFO和LRU的特点,最早或最久未使用的页最可能被淘汰
(5)二次机会法(Enhanced Clock):在时钟页面置换算法的基础上,增加一个“脏”位(dirty bit,若该页执行过写操作,该位为1),具体置换规则如下图
(6)最不常用(LFU):选择被访问次数最少的页面置换出去