进程管理(三) 同步与互斥(详解四大经典问题:生产消费者|哲学家进餐|理发师问题|读者写者问题)

本文深入探讨了进程间的同步与互斥问题,包括临界资源的概念、信号量的使用方法,以及生产者-消费者问题、哲学家进餐问题、理发师问题和读者-写者问题等经典案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(三)同步与互斥

        间接相互制约关系(互斥):若某一进程要求使用某种资源,而该资源正在被另一个进程占用,并且该资源不允许两个资源同时占用,那么只能等占用资源的进程释放资源后再使用,这种制约关系为互斥关系,因其制约形式是“进程——资源——进程”,故称为间接相互制约关系。

        直接相互制约关系(同步):某一进程若收不到另一个进程给他提供的必要信息就不能继续下去,这种情况表明了两个进程要在某些点上交换信息,相互交流的情况。这种制约形式是“进程——进程”,也因此称为直接相互制约关系(同步)。

        Statement:同类进程即为互斥关系,不同类进程为同步关系。例如:生产者和生产者是互斥关系,生产者和消费者是同步关系。

1、临界资源与临界区

临界资源:同时仅允许一个进程使用或访问的资源称为临界资源,需要不同进程互斥访问。

临界区:进程中用于访问临界资源的一段代码,是属于对应的进程的。

2、信号量及同步原语

        信号量是一个确定的二元组(s, q),其中s是一个具有非负初值的整型变量,q是一个初始状态为空的队列。整型变量s表示系统中某类资源的数目,当其值大于0时,表示系统中存在的可用资源数目,当其小于0时,表示系统中因请求该资源而被阻塞的进程的数目。除信号量的初值外,信号量的操作只能通过P操作和V操作来改变,操作系统利用它的状态对进程和资源进行管理。

        P操作:P(s):  s = s – 1 若s >= 0则继续,否则阻塞该进程,并将该进程挂在等待队列中。

struct semaphore{
    int count;
    queue q
};

P(s){
    s.count--
    if(s.count < 0){
        该进程阻塞
        将该进程插入阻塞队列s.q
    }
}

        V操作:V(s) : 若s > 0则继续,否则,从该进程等待队列中移出第一个进程,使其变为就绪状态,插入就绪队列当中,然后继续执行。

V(s){
    s.count++
    if(s.count <= 0){
        在s.q中取出第一个进程p
        将p插入就绪队列
    }
}

注意:

        P和V均为不可分割的原子操作,若执行P/V操作,必须执行完毕。

        P和V在系统中一定是成对实现的但未必在同一个进程中。

实现进程同步:

考虑同步关系:并发进程P1和P2,P1中有一条语句S1,P2中有一条语句S2,则通过信号量实现进程同步如下:

semaphore N = 0

P1(){
    ...
    S1
    V(N)
    ...
}

P2(){
    ...
    P(N)
    S2
    ...
}

实现进程互斥:

考虑互斥问题:P1和P2共享资源S,且S只能同时供一个进程使用或访问,则通过信号量实现进程互斥过程如下:

semaphore  N = 1
P1(){
    ...
    P(N)
    访问S
    V(N)
    ...
}

P2(){
    ...
    P(N)
    访问S
    V(N)
    ...
}

3、经典同步问题

        生产者-消费者问题

        问题描述:一组生产者向一组消费者提供产品,他们共享一个有界的缓冲区,生产者向其中投入产品,消费者取走产品,循环往复,以至无穷......

semaphore full = 0
semaphore empty= n
semaphore mutex = 1

Producer(){
    while(1){
        produce an item
        P(empty)            //获取空位置
        P(mutex)            //获取缓冲区访问权限,与上一行不可调换顺序,否则可能死锁
        将产品放入缓冲池
        V(mutex)            //释放缓冲区访问权限
        V(full)             //占位
    }
}

Consumer(){
    while(1){
        P(full)             //获取产品位置
        P(mutex)            //获取缓冲区访问权限,与上一行不可调换顺序,否则可能死锁
        取出产品
        V(mutex)            //释放缓冲区访问权限
        V(empty)            //空出一个位置
    }
}

注意:

P(full)/P(empty)与P(mutex)顺序不能变换,否则可能出现死锁。例如,某一时刻缓冲区已经满,而生产者先P(mutex)成功,这时,由于empty = 0,故P(empty)会被挂起,生产操作不能进行,而消费者因为不能申请到对缓存区的访问也不能取走一个产品,故生产者和消费者会陷入相互等待,形成死锁。因此P操作必须后对互斥信号量进行操作。

mutex不能省略!例如,在生产者和消费者不唯一的情况下,两个生产者同时进行了P(empty)操作,第一个生产者执行了buffer(in) = nextp操作,这时,该生产者还未来得及执行in= (in + 1) % n 的操作,而第二个生产者以及执行了buffer(in) = nextp操作,这样就将两个新产品放在了同一个位置上,因此在多生产者和多消费者的情况下,一定要设置mutex互斥信号量,以保证对缓冲池的互斥访问。简而言之,互斥信号量就是为了同类进程互斥准备的。

        哲学家进餐问题

        问题定义:5个哲学家围桌而坐,每两个哲学家之间放一支筷子,哲学家们在思考和干饭之间切换状态,当他们想干饭时,他们拿起左右的筷子进餐。也就是每支筷子会被 其左右两个哲学家使用......

        分析:该问题筷子是临界资源,不能同时被两个哲学家使用,首先可以通过如下信号量来控制互斥过程。

semaphore Fork[5] = {1, 1, 1, 1, 1}

philosopher(int i){
    while(1){
        think();
        P(Fork[i % 5])              // 申请使用第1根筷子
        P(Fork[(i + 1) % 5])        // 申请使用第2根筷子
        ganfan()
        V(Fork[i % 5])
        V(Fork[(i + 1) % 5])
    }
}

注意,这种算法存在问题,就是五个哲学家同时拿起左手边的筷子时,五个哲学家都不能拿起右手边的筷子,因此陷入交替等待的死锁状态。解决办法:同时只允许四个哲学家同时进餐,或者规定奇数号的哲学家每次先拿起左手边的筷子,偶数号的哲学家每次先拿起右手边的筷子。

每次只允许4个哲学家同时进餐

semaphore Fork[5] = {1, 1, 1, 1, 1}
flag = 4

philosopher(int i){
    while(1){
        think();
        P(flag)
        P(Fork[i % 5])              // 申请使用第1根筷子
        P(Fork[(i + 1) % 5])        // 申请使用第2根筷子
        ganfan()
        V(flag)
        V(Fork[i % 5])
        V(Fork[(i + 1) % 5])
    }
}

奇数号哲学家先拿左边筷子

semaphore Fork[5] = {1, 1, 1, 1, 1}

philosopher(int i){
    while(1){
        if(i % 2 == 0){
            think();
            P(Fork[i % 5])              // 申请使用左手边筷子
            P(Fork[(i + 1) % 5])        // 申请使用右手边筷子
            ganfan()
            V(Fork[i % 5])
            V(Fork[(i + 1) % 5])
        }else{
            think();
            P(Fork[(i + 1) % 5])        // 申请使用右手边筷子
            P(Fork[i % 5])              // 申请使用左手边筷子
            ganfan()
            V(Fork[i % 5])
            V(Fork[(i + 1) % 5])
        }
    }
}

        理发师问题

        问题描述:理发店里有一位理发师、一把理发椅和若干供顾客等待的椅子(n个),若没有顾客理发,则理发师坐在理发椅上睡觉;当一个顾客到来时,他会唤醒理发师,若理发师正在给顾客理发,如果有空凳子,则顾客等待;否则顾客离开。要为理发师和顾客设计一段程序来描述其活动。

int chairs = n + 1;
semaphore ready = 0;
semaphore finish= 1;
semaphore mutex = 1;

barber(){
    while(1){
        P(ready);        //看看有没有顾客,如果没有就阻塞
        理发();          
        P(mutex);        //理发结束,对chairs进行操作
        chairs++;
        V(mutex);
        V(finish)        //理发师空闲
    }
}

customer(){
    P(mutex)                //申请更改chair
    if(chairs > 0){
        chairs--;           //找个凳子
        V(mutex);           //释放mutex
        V(ready);           //多了个准备好的顾客
        P(finish);          //等待空闲的理发师来给理发
    }else{
        V(mutex);           //释放mutex
    }
}

        读者-写者问题

        问题定义:读者和写者共同访问一个共享文档,访问需遵循如下规律:

        多个读者可以同时读文件

        一次只能有一个写者写文件

        写者写文件时不能有读者正在读文件

 情况一:读者优先算法:

int readernumber = 0;
semaphore rmutex = 1;    //读者需互斥访问readernumber对其进行修改
semaphore mutex = 1;     //保证对数据区的写互斥

writer(){
    while(1){
        P(mutex)
        write()
        V(mutex)
    }
}

reader(){
    while(1){
        P(rmutex)                // 申请修改readernumber 
        if(readernumber == 0){   
        /* 如果此时readernumber为0,说明第一个读者刚要访问该共享文件,因此要禁止后面到来的写者进入,而若此前已有读者正在访问过程中,那么写者已经被阻塞 
            同时,如果已有写者进入,那么读者会被阻塞*/
            P(mutex)
        }
        readernumber++           // 读者数量加1
        V(rmutex)                // 释放readernumber权限,允许其他读者访问之
        read()                   // 进行读操作
        P(rmutex)                // 再次申请readernumber修改权限
        readernumber--           // 读者数-1
        if(readernumber == 0){
            /** 如果是最后一个读者退出文件,释放mutex,允许后续写者进入   */
            V(mutex)
        }
        V(rmutex)                // 释放readernumber权限,允许其他读者访问之
    }
}

        这种情况是利于读者访问的,因为如果当前有两个读者进程在读文档,一个写者进程被阻塞,此时再来一个读者进程,那么这个读者进程将被允许读文档,而阻塞的写者进程只能在所有读者都退出进程才能被访问。

情况二:写者优先算法

semaphore rmutex = wmutex = r = w = 1
int readercount = 1
int writercount= 1

reader(){
    while(1){
        P(r)
        P(rmutex)
        if(readercount == 0){
            P(w)
        }
        readercount++
        V(rmutex)
        V(r)
        read()
        P(rmutex)
        if(readercount == 1){
            V(w)
        }
        readercount--
        V(rmutex)
    }
}

writer(){
    while(1){
        P(wmutex)
        if(writercount == 0){
            P(r)
        }
        writercount++
        V(wmutex)
        P(w)
        write()
        V(w)
        P(wmutex)
        writercount--
        if(writercount == 0){
            V(r)
        }
        V(wmutex)
    }
}

在本例中信号量来控制写者进程的进入,若有写着存在则占用该信号量,阻止后续读者进如临界区,而w信号量代表对临界区进行写操作的权力,当有读者占用临界区时,占用信号量w以阻止写者进行写操作。本解法的rmutex和wmutex信号量表示对读者、写者计数器互斥操作控制的信号量。

情况三:读者写者公平

semaphore mutex = rmutex = wmutex = 1
int readercount = 0

reader(){
    while(1){
        P(wmutex)        //检测是否有写者
        P(rmutex)
        if(readercount == 0){
            P(mutex)
        }
        readercount++
        V(rmutex)        //恢复rmutex
        V(wmutex)        //恢复wmutex
        read()
        P(rmutex)        //申请readercount使用
        readercount--
        if(readercount == 0){
            V(mutex)     //释放数据区,允许写者进入
        }
        V(rmutex)
    }
}

writer(){
    while(1){
        P(wmutex)        //检测是否有其他写者存在,无写者时进入
        P(mutex)         //申请数据区访问
        write()          //进行写操作
        V(mutex)         //释放数据区,允许其他进程写
        V(wmutex)        //恢复wmutex
    }
}

实验题目: 生产者消费者(综合性实验) 实验环境: C语言编译器 实验内容: ① 由用户指定要产生的进程及其类别,存入进入就绪队列。    ② 调度程序从就绪队列中提取一个就绪进程运行。如果申请的资源被阻塞则进入相应的等待队列,调度程序调度就绪队列中的下一个进程。进程运行结束时,会检查对应的等待队列,激活队列中的进程进入就绪队列。运行结束的进程进入over链表。重复这一过程直至就绪队列为空。    ③ 程序询问是否要继续?如果要转直①开始执行,否则退出程序。 实验目的: 通过实验模拟生产者消费者之间的关系,了解并掌握他们之间的关系及其原理。由此增加对进程同步问题的了解。 实验要求: 每个进程有一个进程控制块(PCB)表示。进程控制块可以包含如下信息:进程类型标号、进程系统号、进程状态、进程产品(字符)、进程链指针等等。 系统开辟了一个缓冲区,大小由buffersize指定。 程序中有个链队列,一个链表。一个就绪队列(ready),两个等待队列:生产者等待队列(producer);消费者队列(consumer)。一个链表(over),用于收集已经运行结束的进程 本程序通过函数模拟信号量的操作。 参考书目: 1)徐甲同等编,计算机操作系统教程,西安电子科技大学出版社 2)Andrew S. Tanenbaum著,陈向群,马红兵译. 现代操作系统(第2版). 机械工业出版社 3)Abranham Silberschatz, Peter Baer Galvin, Greg Gagne著. 郑扣根译. 操作系统概念(第2版). 高等教育出版社 4)张尧学编著. 计算机操作系统教程(第2版)习题解答实验指导. 清华大学出版社 实验报告要求: (1) 每位同学交一份电子版本的实验报告,上传到202.204.125.21服务器中。 (2) 文件名格式为班级、学号加上个人姓名,例如: 电子04-1-040824101**.doc   表示电子04-1班学号为040824101号的**同学的实验报告。 (3) 实验报告内容的开始处要列出实验的目的,实验环境、实验内容等的说明,报告中要附上程序代码,并对实验过程进行说明。 基本数据结构: PCB* readyhead=NULL, * readytail=NULL; // 就绪队列 PCB* consumerhead=NULL, * consumertail=NULL; // 消费者队列 PCB* producerhead=NULL, * producertail=NULL; // 生产者队列 over=(PCB*)malloc(sizeof(PCB)); // over链表 int productnum=0; //产品数量 int full=0, empty=buffersize; // semaphore char buffer[buffersize]; // 缓冲区 int bufferpoint=0; // 缓冲区指针 struct pcb { /* 定义进程控制块PCB */ int flag; // flag=1 denote producer; flag=2 denote consumer; int numlabel; char product; char state; struct pcb * processlink; …… }; processproc( )--- 给PCB分配内存。产生相应的的进程:输入1为生产者进程;输入2为消费者进程,并把这些进程放入就绪队列中。 waitempty( )--- 如果缓冲区满,该进程进入生产者等待队列;linkqueue(exe,&producertail); // 把就绪队列里的进程放入生产者队列的尾部 void signalempty() bool waitfull() void signalfull() void producerrun() void comsuerrun() void main() { processproc(); element=hasElement(readyhead); while(element){ exe=getq(readyhead,&readytail); printf("进程%d申请运行,它是一个",exe->numlabel); exe->flag==1? printf("生产者\n"):printf("消费者\n"); if(exe->flag==1) producerrun();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值