第二章-进程管理

WanXing2022年7月30日大约 19 分钟

2.1.1 进程open in new window

进程是程序的一次执行过程,是系统资源分配和调度的基本单位

PCB:进程控制块,描述进程信息和状态。创建进程即创建PCB,是进程存在的唯一标志

进程的状态

  • 创建态:申请空白PCB,写入控制管理进程的信息,分配资源。进入就绪态

  • 就绪态:进程获得除了CPU以外的一切资源,进入就绪队列

  • 运行态:进程在CPU上运行。一旦时间片到了或者有更高优先级的进程,就回到就绪态;缺少资源,主动进入阻塞态

  • 阻塞态(等待态):进程缺少非CPU的资源或等待io,暂停运行。满足条件后,被动进入就绪态

  • 结束态:回收资源

闲逛进程idle:当没有其他就绪态进程时,运行闲逛进程,不访存

进程的控制

进程的状态转换,使用原语

  • 创建:新建PCB,分配资源,加入就绪队列

  • 终止:正常结束、异常结束、kill

  • 阻塞:PCB加入阻塞队列;

  • 唤醒:PCB从阻塞队列到就绪队列

  • 进程切换:保存上下文,将PCB放入队列,选择另一进程的PCB,更新页表等内存管理数据结构,恢复上下文

父进程和子进程的关系

父进程终止,子进程有两种选择:1.终止;2.子进程被init进程领养

子进程终止,资源归还父进程

父进程和子进程可以共享部分资源,不能同时使用临界资源

父进程能给子进程分配虚拟地址空间,但不共享虚拟地址空间

进程的组织

一个进程由pcb,程序段和数据段三部分构成

PCB的内容

  • 进程号
  • 进程状态,优先级
  • 进程队列指针
  • 程序和数据地址
  • CPU现场保护区

PCB的组织方式:

  • 链式:按照进程状态分成多个队列,每个队列里PCB相连
  • 索引方式:索引表

程序段:进入CPU的程序代码段

数据段:包括程序原始数据和执行时产生的结果

进程的通信

低级通信:低级通信指的就是通过PV操作控制信号量这个动作本身。只能传输少量信息,通常用于通知事件发生。

高级通信:用来传输大量数据

  • 共享存储:两进程独立的虚拟地址空间映射到一块相同的物理地址空间,使用同步互斥工具(PV)进行读写控制

    • 低级方式:基于数据结构的共享
    • 高级方式:基于存储区的共享
  • 消息传递:内核中存在若干消息队列,由标识符ID标记。写进程可以向队列中添加消息,读进程可以读取消息。消息是格式化的:[消息头|消息体]

    • 直接通信:直接把消息挂在接收进程的消息缓冲队列
    • 间接:有中间实体转发(信箱)
  • 管道通信:管道是内存中一个特殊的pipe文件,也是一个固定大小的缓冲区,大小一般为内存中的一页4KB。半双工通信,同一时刻只能单向。

    管道的同步互斥:管道写满时,写进程的write系统调用被阻塞,读进程才能读取;管道为空时,读进程的read调用被阻塞,写进程才能写

  • socket通信

2.1.2 线程

进程一旦阻塞,整个进程就会挂起,即使里面有部分函数不依赖于等待的资源也不能执行。于是引入线程,作为CPU调度的基本单位,一个进程分成多个线程。线程的本质就是函数的执行

1.线程的组织与控制

线程的状态:运行态,就绪态,阻塞态

每个线程有自己的线程控制块TCB:包括

  • 线程标识符
  • 寄存器值:PC,状态寄存器,通用寄存器
  • 线程运行状态
  • 优先级
  • 线程专有存储区:线程切换时保存现场
  • 堆栈指针:过程调用时保存局部变量和返回地址

线程的创建,终止

  • 同一进程的所有线程共享进程的堆、静态变量、全局变量、文件,可直接通信,切换开销小
  • 每个线程有自己的栈空间(存局部变量),局部变量不能共享,也不能共享线程专有的堆栈指针

2.线程的实现方式

用户级线程:

  • 存在在用户空间中
  • 应用程序通过线程库来管理用户级线程,线程库提供线程函数接口
  • 操作系统内核意识不到线程的存在,所以只能仍然以进程为单位分配CPU,不能利用多核CPU。用户级线程切换无需进入内核态
一个最简单的线程库,可以实现并发。
	while(true){
        if(i==0)视频聊天;
        if(i==1)文字聊天;
        if(i==2)文件传输;
        i=(i+1)%3;
    }
并且可见,一个用户线程被阻塞,其余的都会被阻塞

内核级线程:

  • 内核管理内核级线程
  • 线程控制块组成的线程表在内核空间中
  • 每个内核级线程对应用户空间的一个用户级线程
  • 可以使用多核CPU并行执行
  • 缺点是线程切换时,需要用户态和内核态之间的切换,系统开销大

总结:内核级线程是CPU调度的基本单位

3.多线程模型

多对一模型:一个内核级线程对应多个用户级线程。一个线程被阻塞,整个进程被阻塞。

一对一:一个内核级线程对应一个用户级线程。一个线程被阻塞,其他的继续执行。操作系统给每个用户线程一个线程控制块

多对多:用户级线程数>=内核级线程数

线程池

线程池是一种多线程处理形式,提前维护多个线程,避免了处理短时间任务时创建销毁线程的代价

协程

过多的线程会导致内存不够用,并且线程切换会有大量时间开销

协程本质上是代码块的相互切换执行,是线程内部的分时复用,一个线程包含多个协程。协程也有自己的栈,一般几十KB

2.2 处理机调度

调度程序通过调度算法从就绪队列选择一个进程执行,并决定时间片大小

调度的三个层次:

(1)作业调度:从外存挑选作业到内存

(2)进程调度:就绪队列-》运行

(3)中级调度:将暂时不运行的程序挂起,调出内存到外存

调度的时机

不能进程调度的情况:

  • 处理中断过程中
  • 进程在os内核程序临界区中(不是普通的临界区!!!)
  • 在屏蔽中断的原子操作过程中

能调度的时机

  • 发生调度条件,当前进程无法进行下去
  • 中断处理结束后

进程调度方式

抢占方式:一个进程执行时,有更重要的进程来,立即暂停进程

非抢占方式:继续执行,直到结束或进入阻塞态才会调度

平均周转时间=sum(各作业等待+执行时间)/n,也就是完成时间到达时间\color{#FF0000}{平均周转时间=sum(各作业等待+执行时间)/n,也就是完成时间-到达时间}

典型的调度算法

  • 先来先服务FCFS:每次从就绪队列中选择最先进入队列的进程。适合CPU密集型,不适合io密集型,对短进程不利

  • 短作业优先SJF

  • 优先级调度:静态优先级(创建进程时确定),动态(能够调整优先级)

  • 高响应比优先调度:

    响应比=等待时间+服务时间服务时间=1+等待时间服务时间响应比=\frac{等待时间+服务时间}{服务时间}=1+\frac{等待时间}{服务时间}

    综合了短作业优先(服务时间越短响应比越高),避免了饥饿现象(等待时间越长响应比越高)

  • 时间片轮转调度:时间片用完后,必须释放CPU给下一个进程,适用于分时系统,不会导致饥饿现象

  • 多级反馈队列调度:设置多个优先级不同的就绪队列,优先级越高时间片越小。

    新进程先放入第一级队列,若时间片用完时未完成,则进入第二级队列,第一级为空时才能运行第二列。不同用户均能满意

  • 多级队列调度:按进程类型设置多个队列

例题:P64(18),画甘特图,纵轴是各个进程的编号,横轴是时间,只要是在运行就画实线。

2.3 进程同步互斥

进程并发执行的异步性:并发的进程速度不可预知

临界资源:一次仅允许一个进程使用的资源

访问临界资源的过程分为:

  • 进入区:检查能否使用临界资源,若可则加锁
  • 临界区:访问临界资源的一段代码
  • 退出区:解锁
  • 剩余区:代码其余部分

同步:各进程必须有一个执行顺序,产生直接制约关系

互斥:间接制约关系,一个退出临界区另一个才能执行

1.互斥的四个原则

  • 空闲让进:空闲资源允许进程访问
  • 忙则等待:两个进程不能同时进入临界区
  • 有限等待:进程等待进入临界区的时间有限,保证不会饥饿
  • 让权等待:当进程无法进入临界区时,应立即释放CPU资源,而不是忙等(一直while(true))。让权等待原则不必须遵循

2.硬件和软件实现进程互斥

1.软件实现

(1)单标志法:turn标志,使用完让给对方。违反“空闲让进”,即一个进程不想进入临界区,另一个进程也进入不了

(2)双标志法先检查:先检查对方是否想进入,再上锁。可能会二者都进入临界区

(3)双标志法后检查:先上锁,再检查。二者都无法进入临界区

以上问题的关键在于上锁和检查两个操作被分隔开了

(4)Peterson法:

Pi(){
    flag[i]=true;turn=j;//表明自己想进入,又表示可以让给对方
    while(flag[j]&&turn==j);
    临界区;
    flag[i]=false;
}
Pj(){
    flag[j]=true;turn=i;
    while(flag[i]&&turn==i);
    临界区;
    flag[j]=false;
}
  • 先表明自己想进入,又表示可以让给对方。如果对方也想进入,且自己是最后谦让的,那么就真的让给对方,自己循环等待。
  • 可以解决单、双标志法的问题,实现临界区互斥,但仍不满足让权等待,会忙等
  • 没有饥饿问题

2.硬件实现

(1)中断屏蔽法:一个进程想访问临界区时,屏蔽中断,禁止切换其他进程。缺点是只能屏蔽一个CPU的中断,多核时其他CPU运行的进程可以访问临界区

(2)TSL指令(test and set):使用原子操作,使得上锁和检查两个操作不可分割。缺点:不满足让权等待,会忙等

(3)swap指令:和tsl逻辑相同

3.信号量,PV操作

信号量S(semaphore),使用wait和signal两种原语对信号量进行操作,分别简写为荷兰语的P和V

int S=1;
wait(S);//P:先检查后上锁(因为是原语所以不可分割)
使用资源;
signal(S);//V:释放资源

(1)整型信号量:S是一个整数,代表资源数量。不满足让权等待原则,仍需要忙等

wait(S){//新进程到来
    while(S<=0);
    S--;
}
signal(S){
    S++;
}

(2)记录型信号量:信号量S记录资源数量,同时维护一个阻塞队列

当进程申请不到资源时,不会忙等,而是加入阻塞队列中,当有资源被释放时,会从阻塞队列中唤醒一个进程。满足了让权等待原则

S的绝对值就是当前被阻塞的进程数

每当一个进程被阻塞,S的值-1;每当V操作一次,S的值+1,一个进程从阻塞队列进入就绪队列

typedef struct{
    int value;
    struct process *L;
}semaphore;
semaphore S;
S.value=1;

wait(S){
    S.value--;
    if(S.value<0){
        当前进程加入阻塞队列S.L
        block(S.L);//阻塞进程
    }
}
signal(S){
    S.value++;
    if(S.value<=0){
        从阻塞队列中移出一个进程P
        wakeup(P);//唤醒进程P,阻塞态到就绪态
    }
}

信号量实现同步、互斥

同步:将信号量初始化为0。希望P1的代码y在P2的x之后执行,在y前面检查是否有资源,x完成后释放资源

semaphore mutex=0;
P1(){
    x;
    V(mutex);
}
P2(){
    P(mutex);
    y;
}

互斥:信号量初始为1,用P和V操作夹紧临界区

semaphore mutex=1;
P1(){
    P(mutex);
    P1进入临界区;
    V(mutex);
}
P2(){
    P(mutex);
    P2进入临界区;
    V(mutex);
}

信号量实现同步关系

对有向无环图(节点是进程)的每条边都设置一个信号量

对于每个进程,首先检查前驱节点(P),进行操作,最后释放后继节点(V)。

4.管程和条件变量

管程:其实就是把同步互斥操作封装起来,成为一个类,每次只需要直接通过这个类的过程(函数)即可。

互斥由编译器负责,同步需要提前设置条件变量

condition full,empty;
int count=0;
void insert(item){
    if(count==n){
        wait(full);
    }
    count++;
    insert_item(item);
    if(count==1){
        signal(empty);
    }
}
item remove(){
    if(count==0){
        //当没有产品时不能remove,因此wait函数将调用wait操作的消费者进程加入条件变量empty的阻塞队列中
        wait(empty);
    }
    count--;
    if(count==n-1){
        signal(full);
    }
    return remove_item();
}
producer(){
    while(1){
        item=生产一个产品;
        insert(item);
    }
}
consumer(){
    while(1){
        item=remove();
        消费item;
    }
}

例如上面的代码,已经写好ProducerConsumer类,由编译器保证同时只能有一个进程使用类里面的insert或remove函数,实现互斥。(java的synchronize关键字)生产者和消费者直接调用函数即可

对于同步,则采用wait和signal函数(即P和V)来实现对条件变量empty和full的同步访问。与信号量不同的是,条件变量没有数值

5.经典同步互斥问题

生产者-消费者问题

缓冲区内有n个缓冲单元

两个同步关系:

  • 缓冲区没满,生产者才能将数据放入缓冲区。设置同步信号量empty,表示空的缓冲单元个数,初始为n

  • 缓冲区不空,消费者才能取走数据。设置同步信号量full,表示满的缓冲单元个数,初始为0

    注:并非缓冲区全空,生产者才能生产

一个互斥关系:

  • 任何两个进程不能同时访问缓冲区。设置互斥信号量mutex

不能改变P(empty)和P(mutex)的顺序,因为先检查缓冲区没满,才能继续上锁生产。否则当缓冲区满了,生产者已经上锁并且被阻塞(卡在empty),消费者也被阻塞(卡在mutex),造成死锁

semaphore mutex=1;//缓冲区互斥锁
semaphore empty=n;//空的缓冲单元数量
semaphore full=0;//满的缓冲单元数量
producer(){//生产者
    while(1){
        生产数据;
        P(empty);//检查是否有空的缓冲单元数,若有,则空缓冲单元-1
        P(mutex);
        将数据放入缓冲区;
        V(mutex);
        V(full);//满缓冲区+1
    }
}
consumer(){//消费者
    while(1){
        P(full);//检查满的缓冲单元数量,若有,则满缓冲单元-1
        P(mutex);
        取走一个数据;
        V(mutex);
        V(empty);//空缓冲区+1
        消费数据;
    }
}

读者-写者问题

读者和写者地位不同。没有同步关系

一个写进程和其他进程互斥:信号量rw实现

(1)读进程优先法

count:记录读者数量

mutex:保证对count的检查和count++两个操作不可分割

rw:一个写进程和其他进程互斥

第一个到来的读者进行rw上锁,最后一个读者进行rw解锁

int count=0;
semaphore mutex=1;
semaphore rw=1;
writer(){
    while(1){
        P(rw);
        writing;
        V(rw);
    }
}
reader(){
    while(1){
        P(mutex);
        if(count==0){//第一个读者负责上锁
            P(rw);
        }
        count++;
        V(mutex);
        
        reading;
        
        P(mutex);
        count--;
        if(count==0){//没有连续的读者了,才能解锁
            V(rw);
        }
        V(mutex);
    }
}

(2)读写公平法

信号量w保证到来的不同进程进入一个阻塞队列,先进先出,不会出现读进程后来但是优先于写进程的情况,保证读写公平

如果读者在写者前到来,此时读者在reading,写者会卡在P(rw)

如果写者在读者前到来,此时写者在writing,读者被卡在P(w)

int count=0;
semaphore mutex=1;
semaphore rw=1;
semaphore w=1;
writer(){
    while(1){
        P(w);
        P(rw);
        writing;//此时读者或其他写者被卡在P(w);
        V(rw);
        V(w);
    }
}
reader(){
    while(1){
        P(w);
        P(mutex);
        if(count==0){
            P(rw);
        }
        count++;
        V(mutex);
        V(w);

        reading;//此时写者被卡在P(rw);其他读者则跳过P(rw)也访问临界区

        P(mutex);
        count--;
        if(count==0){
            V(rw);
        }
        V(mutex);
    }
}

哲学家就餐问题

n个哲学家围坐,中间n根筷子,拿起两根才能进餐。专门解决临界资源数量>1的问题

三种方法防止死锁

  • 至多允许n-1个人同时进餐
  • 奇数号哲学家先拿左边筷子再拿右边,偶数号相反,这样避免了一些人占有一只筷子再等待另一只
  • 仅当左右筷子都可用时才能拿起筷子
//方法一
semaphore chopstick[n];
semaphore mutex=n-1;//至多允许n-1个人同时进餐
for(int i=0;i<n;++i){
    chopstick[i]=1;
}
Pi(){
    while(1){
        思考;
        P(mutex);
        P(chopstick[i]);//左手的筷子
        P(chopstick[(i+1)%n]);//右手的筷子
        就餐;
        V(chopstick[i]);
        V(chopstick[(i+1)%n]);
        V(mutex);
    }
}
//方法三
semaphore chopstick[n];
semaphore mutex=1;//拿起左右筷子的原子锁
for(int i=0;i<n;++i){
    chopstick[i]=1;
}
Pi(){
    while(1){
        思考;
        P(mutex);
        P(chopstick[i]);//左手的筷子
        P(chopstick[(i+1)%n]);//右手的筷子
        V(mutex);
        就餐;
        V(chopstick[i]);
        V(chopstick[(i+1)%n]);
    }
}

利用PV操作解题

先分析同步互斥关系,几个关系几个信号量。对于互斥,用PV夹紧互斥行为;对于同步,在a进程末尾加V,在b进程开始加P。

两个同步关系,一个互斥关系,需要三个信号量。

初始有几个资源,信号量就是初始化为几

2.4 死锁

多个进程因竞争资源造成相互等待。(区分:饥饿只是一个进程;死循环不是操作系统原因)

死锁产生的四个必要条件

  • 互斥条件:一段时间内资源只能被一个进程占有
  • 不剥夺条件:资源只能被主动释放,不能被抢夺
  • 请求并保持条件:进程一边保持资源不放,一边请求新的资源
  • 循环等待条件:存在循环等待链

死锁预防:破坏四个条件

  • 破坏互斥条件:互斥资源转为共享
  • 破坏不剥夺条件:必须释放;强行剥夺
  • 破坏请求并保持条件:开始一次性申请全部资源
  • 破坏循环等待条件:顺序资源分配法,把资源编号,拥有小编号才能申请大编号

死锁预防不改变进程之间的关系

死锁避免:银行家算法

事先计算本次分配的安全性(需要知道资源需求),避免进入不安全状态。

银行家算法用来试探分配(而不是限制顺序),避免进入死锁状态,但无法判断当前是否处于死锁状态

安全状态:进程推进的一个序列,使得可以顺序完成。(例如先让P2执行,完成后归还资源,再让其他执行。)进入安全状态一定不会发生死锁,不安全状态不一定死锁

银行家算法:现在已知每个进程对资源的最大需求量,已分配的资源量。

如果当前剩余可用资源>=一个进程的最大需求量-已分配资源量,则可以把该进程加入安全序列,然后当前剩余可用资源+=该进程已分配的资源量。进行迭代查询

银行家算法

死锁检测:画资源分配图

请求边:进程节点->资源节点:申请一个资源

分配边:资源节点->进程节点:已经分配了资源

死锁检测需要基于死锁定理:

如果某个进程申请各个资源数<=剩余资源数,则删掉它相连的所有边,称为孤立节点。如果最后所有点都是孤立的,则称图是可完全简化的,一定不会死锁;否则会发生死锁

死锁解除

资源剥夺法:挂起死锁进程,抢占死锁进程资源

撤销进程法:强制撤销死锁进程

进程回退法:进程回退到无死锁时

总结

死锁预防的破坏循环等待条件会限制进程申请资源的顺序

死锁避免(银行家算法)需要每个进程运行所需资源总量信息,死锁检测并不关心总量

死锁检测(死锁定理)会给可能导致死锁的进程分配资源,死锁避免不会

计算题

求不会发生死锁的最小资源数:sum{每个进程的最大需求量-1}+1

Loading...