前言
不知道一篇能写成什么样子。第一篇是随便写,当时写完打算第二篇应该认真写才是,这一拖就是半个月。
操作系统也就进程、调度、内存、io与文件这几个部分。如果有机会我还想早点学到嵌入式系统与分布式系统的特点。
处理机调度
调度就是按某种算法选择一个进程将处理机资源给它(处理机资源即单核CPU)
处理机调度分为高中低三种调度,也有按长中短分的,也有把IO调度单独拿出来的,我只写前者。
- 高级调度是作业调度。将作业从外存调入内存,然后给它建立一个PCB,作为一个进程等待被执行。
- 低级调度是进程调度、切换。给进程处理机,让它被执行。一是对原来运行进程各种数据的保存。二是对新的进程各种数据的恢复。
- 中级调度是内存调度。内存中部分没有被执行的进程可以先调回外存,等到有处理机后再调入内存。
\;
调度原则
- 空闲让进。就绪队列没有进程时,此时来了一个进程,一定能让它被执行。
- 忙则等待。如果有其他进程在执行,并且新的进程的优先级也没有其他进程的高,那就只有等待了。
- 有限等待。每个进程的等待时间不能是无穷的,不能有饥饿甚至饿死的情况。
- 让权等待。当进程不能进入临界区时,应该立即释放处理机,防止进程忙等待。
什么是临界区、退出区…
/*
进入区:检查进程是否进入临界区,如果可以,设置【正在访问临界资源】的标志
*/
/*
临界区:访问临界区资源
*/
/*
退出区:解除【正在访问临界资源】的标志
*/
/*
剩余区:其他操作
*/
\;
调度的时机
当前进程主动放弃CPU
- 进程正常终止
- 运行过程中发生异常而终止
- 进程主动请求阻塞
当前进程被动放弃CPU
- 分给进程的时间片用完
- 有更紧急的事件要处理
- 有优先级更高的进程进入就绪队列
\;
不能进行调度、切换的情况
处理机中断的过程中。
中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换。
进程在操作系统内核程序临界区中。
在原子操作过程中。
原子操作/原语不可中断,比如修改PCB中的进程状态标志,并把PCB放入相应队列。
\;
抢占式与非抢占式
非抢占式是只允许进程主动放弃处理机。在运行过程中即便有更紧迫任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。
抢占式是当一个进程正在处理机上运行时,如果另一个更有意义、更紧迫的进程需要使用处理机,则立即暂停正在运行的进程。将处理机分配给更紧迫的。
\;
进程七状态模型
进程的挂起态是暂时调用到外存等待的进程状态。还可以分为就绪挂起和阻塞挂起。如果只有一个挂起态的话,也可以构造六状态模型。
挂起和阻塞都不能获得CPU服务,区别是挂起态将进程映像调到外存了(PCB还在内存),而阻塞态下进程映像还在内存中。
\;
进程两状态模型
进程是否在执行,根据这个可以构造最简单的进程状态模型——进程只有两个状态,运行与未运行态。
\;
调度算法的评价指标
CPU利用率:忙碌时间与总运行时间之比。
系统吞吐量:单位时间内完成作业的数量,即总共完成的作业数与总共花了多少时间之比。
周转时间:作业完成时刻减去作业提交时刻
带权周转时间:作业周转时间与作业实际运行时间之比
等待时间:进程被建立之后的等待时间之和,即作业上外存后备队列中等待时间加上进程的等待时间
响应时间:从提交请求到响应的时间之差
\;
\;
调度算法
先来先服务(First Come First Serve)
用于作业调度时,将作业按顺序加入后备队列。
用于进程调度时,将进程按顺序加入就绪队列。
按顺序一个一个地执行,公平、简单、不会有饥饿。
非抢占
\;
短作业优先(Shortest Job First)
SJF是非抢占式的,还有一种Shortest Process First也是非抢占的。
短作业优先也有抢占式版本【Shortest Remaining Time Next】
对于非抢占式的来说,每次调度时,操作系统会选择当前已到达的且剩余运行时间最短的作业或进程。因为是非抢占式的,所以就算又有剩余运行时间更短的进程到达,也要等这个进程主动结束或放弃才行。
对于抢占式的来说,每当有进程加入就绪队列,就要重新进行调度,选择此时剩余运行时间最短的。
\;
高响应比优先(Highest Response Ratio Next)
不会有饥饿,即可用于作业调度,也可用于进程调度。
只有当前运行的进程放弃CPU时才需要调度。调度时计算就绪队列的进程的响应比,选择响应比最高的进程,并给它分配CPU。
非抢占
响 应 比 = 等 待 时 间 + 要 求 服 务 的 时 间 要 求 服 务 的 时 间 响应比 = \frac{等待时间 + 要求服务的时间}{要求服务的时间} 响应比=要求服务的时间等待时间+要求服务的时间
- 要求服务的时间也就是剩余的运行时间。
\;
时间片轮转调度(Round-Robin )
不会有饥饿,只用于进程调度。
公平,轮流为各个进程服务。按照抵达就绪队列的顺序,轮流让各个进程执行一个时间片。就算一个时间片内进程没执行完,也要被剥夺CPU,将进程重新放入就绪队列队尾重新排队。
抢占
\;
优先级调度(Priority)
有抢占式,也有非抢占式。即可用于作业调度,也可用于进程调度。
按优先级来调度进程,有静态优先级与动态优先级。动态是可变的,比如如果某个进程等待时间太长,则可以适当提高优先级。
一般系统进程大于用于进程,IO型进程大于计算型进程,前台进程大于后台进程(比如手机,重要的是用户的交互,所以表面上的快更重要)。
\;
多级反馈队列调度(Multi-level Feedback Queue )
抢占式,只用于进程调度,会导致饥饿。
多级反馈队列调度结合了其他几个调度算法的优点,拥有时间片、优先级等特点。
- 设置多级队列,优先级从高到低,时间片大小从小到大。
- 新的进程先进入第一级队列,按FCFS原则等待CPU。等到CPU并且自己的时间片用完后还没结束的话,就把这个进程放入第二级队列队尾。最后一级队列的进程被执行后被结束就放入最后一级队列队尾。
- 当第k级队列为空时,为k+1级队列分配时间片,即CPU往下逐次执行。
\;
\;
同步与互斥
同步不是同时,相反同步是不同时。在某些做事步骤上必须由先后顺序,必须一前一后。这种直接的制约关系就是同步。
互斥的概念比同步大,可以说同步包含于互斥之中。互斥不要求两件事执行的先后顺序,只需要不同时就行。
- 在研究多进程/线程问题时,需要分别考虑其互斥关系和同步关系。根据互斥关系构造互斥变量,根据先后关系构造同步变量。有时,可以省略互斥变量,单单用同步的就行了。
\;
信号量
信号量是一个变量,用来表示系统中某种资源的数量。
信号量实现进程同步
- 分析什么地方需要同步(一前一后)
- 设置同步变量S,初始化为0
- 在前操作后V(S) —— P、V操作即加锁解锁或者阻塞释放。
- 在后操作前P(S)
用信号量解决生产者-消费者问题
有一个或者多个生产者生产某种类型的数据,并放置在缓冲区;有一个消费者从缓冲区取数据,每次取一个;系统避免对缓冲区的重复操作——在任何时候只能有一个生产者或者消费者访问缓冲区。
当缓冲区已满时,生产者不会继续向其中添加数据;当缓冲区为空时,消费者不会从其中取出数据。
/* 二元信号量的原语的定义 */
struct binary_semaphore{
enum{zero ,one}value;
queueType queue;
};
void semWaitB(binary_semaphore S){
if(S.value==one)S.value=zero;
else{
/*把当前进程插入等待队列*/
/*阻塞当前进程*/
}
}
void semSignalB(semaphore S){
if(S.queue is empty)S.value=one;
else{
/*把进程P从等待队列中移除*/
/*把进程P插入就绪队列*/
}
}
/* program producerconsumer */
int N;
binary_semaphore S=1,delay=0;
void producer(){
while(1){
produce();//生产数据
semWaitB(S);//阻塞
append();//放入数据
if(++N==1)semSignalB(delay);
semSignalB(S);//释放
}
}
void consumer(){
semWaitB(delay);
while(1){
semWait(S); //阻塞
take();//取出数据
semSignalB(S); //释放
consumer();//消费数据
if(--N==0)semWaitB(delay);
}
}
void main(){
N=0;
parbegin(producer,consumer);//构造producer\consumer两个进程/线程
}
\;
管程
管程是一种高效的同步机制,引入管程是因为信号量存在问题——编写程序困难、易出错。
管程是由一个或多个过程(方法),一个初始化序列和局部数据组成的软件模块。
局部数据变量只能被管程的过程访问,任何外部过程都不能访问
一个进程通过调用管程的一个过程进入管程
任何时刻只能有一个进程在管程中。调用管程的其他进程都被阻塞,等待被调用。
用管程解决生产者-消费者问题
管程模块boundedbuffer控制着用于保存和取回字符的缓存区,管程中有两个条件变量(使用结构cond声明)。当缓冲区中至少有增加一个字符的空间时,notfull为真;当缓冲区至少有一个字符时,notempty为真。
/*program producerconsumer */
monitor boundedbuffer;
char buffer[N];//分配N个数据空间
int nextin,nextout;//缓冲区指针(索引)
cond notfull,notempty;//同步变量
void append(char x){
if(count==N) wait(notfull); //如果缓冲区满了,就等待notfull,即等待消费者取出
buffer[nextin]=x;
nextin=(nextin+1)%N;
count++; //空间减少,数据增加
signal(notempty); //放入了,所以不为空了
}
void take(char x){
if(count==0) wait(notempty); //如果为空,就等待notempty,即等待生产者放入
x=buffer[nextout];
nextout=(nextout+1)%N;
count--; //空间增加,数据减少
signal(notfull); //拿出了,所以不为满了
}
{ //管程体
nextin=nextout=count=0;//初始化缓冲区
}
//生产者,不断地生产-放入-生产-
void producer(){
char x;
while(1){
produce(x);
append(x);
}
}
//消费者,不断地取出-消费-取出-
void consumer(){
char x;
while(1){
take(x);
consume(x);
}
}
void main(){
parbegin(producer,consumer);
}
管程优于信号量是因为其所有的同步机制都被限制在管程内部,所以易于验证同步的正确性,而且容易检测出错误。此外,如果一个管程被正确地编写,则所有进程对受保护资源的访问都是正确的;而对信号量而言,只有当所有访问资源的进程都被正确地编写时,资源访问才是正确的。
\;
\;
死锁
死锁是一组相互竞争系统资源或进程通信的进程间的“永久”阻塞。
当一组进程中的每个进程都在等待某个事件(比如等待所请求的资源被释放),而只有这组进程中的其他被阻塞的进程才可以触发该事件,这时就称这组进程发生死锁。因为没有事件能够被触发,所以死锁是永久性的。与并发进程管理中的其他问题不同,死锁问题没有一种有效的通用解决方案。
死锁的三个必要条件
- 互斥。一次只有一个进程可以使用一个资源,其他进程不能访问已分配给其他进程资源。
- 占有且等待。当一个进程等待其他进程时,继续占有已分配的资源。
- 不可抢占。不能强行抢占进程已占有的资源。
另外,构成死锁存在的充分条件还有
- 循环等待。存在一个封闭的进程链,使得每个进程至少占有此链中下一个 进程所需要的一个资源。