【操作系统】信号量解决经典同步问题

文章目录

  • 1. 基本结构
  • 2. P,V操作
  • 3. 信号量的应用
    • 3.1 信号量实现进程互斥
    • 3.2 信号量实现前驱关系
  • 4. 用信号量解经典同步问题
    • 4.1 生产者消费者问题
    • 4.2 读者写者问题
    • 4.3 狒狒过桥问题
    • 4.4 理发师理发问题
    • 4.5 哲学家进餐问题

信号量机制是Dijkstra提出的一种卓有成效的进程同步工具。信号量有整形信号量、记录型信号量、AND型信号量等,这里主要介绍我们常见的记录型信号量。

1. 基本结构

typedef struct {int value;  //信号量值struct process_cntrol_block *list;  //阻塞队列
}semaphore;

在应用信号量的时候,信号量的值往往是临界资源的数量。当临界资源数量不足时,新的进程就阻塞,并插入到信号量阻塞队列中。

2. P,V操作

wait(S)和signal(S)操作是信号量机制的基本操作(通常称作P,V操作),定义如下:

wait(semaphore *S) {S->value--;  //信号量的值减一if(S->value < 0) block(S->list);  //如果信号量的值小于零,进程阻塞signal(semaphore *S) {S->value++;  //信号量的值加一if(S->value <= 0) wakeup(S->list);  //唤醒
}

在实现进程同步时,一般先将S->value的初值设定为临界资源的初始数量,一旦进程要请求一个临界资源,先对代表此进程的信号量执行P操作,也就意味着资源数减一,当该进程运行完毕时,执行V操作,意味着资源数加一。值得注意的是,如果当前资源数量为0,再有进程想请求临界资源,就会在执行P操作时进入阻塞队列,信号量值变为-1,此后执行P操作的进程也都会被阻塞,信号量的绝对值为阻塞的进程数目。

3. 信号量的应用

3.1 信号量实现进程互斥

有了P,V操作,我们就能很简单的实现进程互斥。方法是:设mutex为互斥信号量,初值为1。在需要互斥的临界区前后分别使用P,V操作即可,示意代码如下:

semaphore mutex = 1;  //一般初值为1的信号量用来实现互斥
P_A() {  //进程Awhile(1) {P(mutex);临界区;V(mutex);剩余区;}
}
P_B() {  //进程Bwhile(1) {P(mutex);临界区;V(mutex);剩余区;}
}

在互斥问题中,我们可以把P操作理解成“上锁”,把V操作理解成“解锁”,当P_A和P_B两个进程并发执行时,无论哪一个进程先执行到了P操作,都会接下来执行P操作的进程阻塞,直到前一个进程执行到了V操作为止,这样就实现了临界区的互斥。

3.2 信号量实现前驱关系

思路如下:为每一组想要实现前后关系的进程,都分别定义一个信号量,初值设为0,把你想要先执行的进程后面加一个V操作,想要后执行的进程前面加一个P操作。这样一来,你想要后执行的进程如果先执行了,就会因为执行了其前面的P操作而阻塞,直到你想要先执行的进程执行完了,执行V操作后,才能解除阻塞继续执行。以上就是用信号量实现前驱关系的过程。
假如我们要实现四个语句S1,S2,S3,S4需要按照一定的顺序同步执行,比如S1执行完S2,S3才能执行,S2,S3执行完了S4才能执行,代码如下:

p1() {S1; V(a); V(b);}
p2() {P(a); S2; V(c);}
p3() {P(b); S3; V(d);}
p4() {P(c); P(d); S4;}main() {semaphore a,b,c,d = 0;cobeginp1();p2();p3();p4();coend
}

4. 用信号量解经典同步问题

4.1 生产者消费者问题

假定生产者和消费者之间的公用缓冲池有n个缓冲区,生产者可以向缓冲区生产一个产品,消费者可以从缓冲区消耗一个产品,注意:生产者不能同时生产,消费者也不能同时消费,也不能同时生产和消费,当缓冲区空时无法消费,当缓冲区满时无法生产。请用信号量机制解决这一问题。
解法如下:

semaphore mutex = 1;  //实现生产与消费、生产与生产、消费与消费
semaphore full = 0, empty = n;  //full表示已用缓冲区数,empty表示空缓冲区数
void producer() {while(1) {P(empty);  //每次生产会减少一个空缓冲区,如果没有空缓存区了就阻塞P(mutex);  //保证互斥生产一个产品;V(mutex);  //保证互斥V(full);  //每次生产增加一个已用缓冲区}
}
void consumer() {while(1) {P(full);  //每次消费会减少一个已用缓冲区,如果没有已用缓存区了就阻塞P(mutex);消耗一个产品;V(mutex);V(empty);  //每次生产增加一个空缓冲区}
}

4.2 读者写者问题

假设一个文件可被多个进程共享,我们允许多个进程同时读这个共享对象,但是不允许一个进程写这个共享对象的同时,别的进程进行读或写。换句话说:读和读不互斥,读和写互斥,写和写互斥。请用信号量机制解决这一问题。
解法如下:

semaphore rmutex, wmutex = 1; //wmutex用以实现写进程与其它进程的互斥
int readcount = 0;  //记录读进程的数量
void reader() {while(1) {P(rmutex);  //见下文解释if(readcount == 0) P(wmutex);  //第一个读进程去把写进程上锁readcount++;  V(rmutex);  //见下文解释读者读;P(rmutex);  //见下文解释readcount--;if(readcount == 0) V(wmutex);  //解锁V(rmutex);  //见下文解释}
}
void writer() {while(1) {P(wmutex);  //实现写进程与其他进程都互斥写者写;V(wmutex);}
}

这个问题的程序中有个关键点,就是明明读进程是不互斥的,为什么还需要rmutex来实现if判断的互斥呢。原因如下:当我们在进程互斥中使用条件语句和数值变化的时候,如果不把那一段也实现互斥的话,很有可能出问题,比如在这段代码中,要是不加第5行和第8行的话:如果一个读进程执行完了第6行,还没有执行第7行的readcount++操作之前,这个读进程被剥夺了处理机,换另一个读进程上处理机运行了,那么另一个读进程就会阻塞在第6行的P操作上,就无法实现多个读者一起读了;10行和13行同理,如果一个读进程执行完了第11行就被剥夺处理机,换上另一个读进程也执行了11行,这样readcount被连减两次,那么这两个进程都能通过12行的if判断,会执行两次V操作,产生错误。

4.3 狒狒过桥问题

一个主修人类学、辅修计算机科学的学生参加了一个课题,调查非洲狒狒是否能被教会理解死锁。他找到一处很深的峡谷,在上边固定了一根横跨峡谷的绳索,这样狒狒就可以攀住绳索越过峡谷。同一时刻可以有几只狒狒通过,只要它们朝着相同的方向。但如果向东和向西的狒狒同时攀在绳索上则将发生死锁(狒狒将被卡在中间),因为它们无法在吊在峡谷上时从另一只的背上翻过去。如果一只狒狒想越过峡谷,它必须看当前是否有别的狒狒正在逆向通过。使用信号量写一个避免死锁的程序来解决该问题。
解法如下:

semaphore wmutex, emutex, mutex = 1; 
int wcount, ecount = 0;  //分别记录东西狒狒上绳索的个数
void west_monkey {while(1) {P(wmutex);  //使if语句互斥if(wcount == 0) P(mutex);  //第一个西狒狒给东狒狒上锁wcount++;V(wmutex);  //使if语句互斥西狒狒过桥;P(wmutex);wcount--;if(wcount == 0) V(mutex);V(wmutex);}
}
void east_monkey {  //和上面的一样while(1) {P(emutex);if(ecount == 0) P(mutex);ecount++;V(emutex);西狒狒过桥;P(emutex);ecount--;if(ecount == 0) V(mutex);V(emutex);}
}

该问题属于读者写者问题的改进,如果完全理解了读者写者问题的解法,那么这个问题也能很快解决。东狒狒之间不互斥,西狒狒之间不互斥,东西狒狒之间互斥,思路是第一个东狒狒给西狒狒上锁,第一个西狒狒给东狒狒上锁,注意if判断也要实现互斥。

4.4 理发师理发问题

理发店里有一位理发师、一把理发椅和n把供等候理发的顾客坐的椅子。如果没有顾客,则理发师便在理发椅上睡觉。当一个顾客到来时,他必须先叫醒理发师,如果理发师正在理发时又有顾客来到,则如果有空椅子可坐,他们就坐下来等。如果没有空椅子,他就离开。这里的问题是为理发师和顾客各编写一段程序来描述他们的行为,要求不能带有竞争条件。
解法如下:

semaphore customer, barber = 0;  //一开始没有顾客,理发师也是睡着的
semaphore mutex = 1;  //互斥信号量
int empty = N;  //空椅子数量为N
void Barber() {while(1) {P(customer);  //只有顾客进程的V执行后才能执行,没有顾客就阻塞(睡觉)P(mutex);  //把数量的变化实现互斥,以免影响到顾客进程的if判断语句empty++;  //椅子上的顾客起身V(barber);  //有了一个理发师可以理发V(mutex);理发;}
}
void Customer() {P(mutex);if(empty > 0) {empty--;  //不管是不是第一个顾客,来了先得坐凳子上,因为理发师在理发椅上睡觉呢V(customer);  //增加一个顾客,唤醒沉睡的理发师V(mutex); P(barber);  //消耗一个理发师,没有理发师就阻塞理发;} else {V(mutex);离开;}
}

这道题本身不难,但其中很多细节的实现需要实现,比如座位是有上限的,这样就不得不设置判断条件和计数,来使超过N个的顾客离开,而加入计数和判断条件后就又要实现其互斥,增加了问题的复杂性。

4.5 哲学家进餐问题

五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在桌子上有五只碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。请用信号量机制解决。
这是一个讲解死锁的时候的经典例子,解决这道题的直接思路如下:

semaphore chopstick[5] = {1,1,1,1,1};
void philosopher() {while(1) {/*当哲学家饥饿时,总是先拿左边的筷子,再拿右边的筷子*/P(chopstick[i]);P(chopstick[(i+1)%5]);吃饭;V(chopstick[i]);V(chopstick[(i+1)%5]);}
}

但是这样可能会出现死锁问题:如果所有哲学家都拿起了左手边筷子,五个进程就会死锁。对于避免哲学家进餐问题发生死锁的方法有很多,这里只讲一种:只有当同时一个哲学家能同时拿起左右两只筷子时,才允许拿筷子,解法如下:

semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1;
void philosopher() {while(1) {P(mutex);P(chopstick[i]);P(chopstick[(i+1)%5]);V(mutex);吃饭;V(chopstick[i]);V(chopstick[(i+1)%5]);}
}

但仅仅这样做还是有问题,如果一个哲学家获得了两只筷子,开始进餐,此时他左边或者右边的哲学家进入了临界区后,被阻塞在第6行或者第7行,那么其他的哲学家就无法进入临界区了,也就是说这个时间只能有一个哲学家进餐,但显然同一时间是可以有不相邻的两个哲学家同时进餐的。
对这个问题的更好解决方法可以去看Dijkstra在1965年给出的算法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384575.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【运筹与优化】单纯形法解线性规划问题(matlab实现)

文章目录单纯形法步骤&#xff1a;1.将线性规划问题化为标准形式2.列出单纯形表3.进行最优性检验4.从一个基可行解转换到另一个目标值更大的基可行解&#xff0c;列出新的单纯形表5.重复3、4直到计算结束为止举例单纯形法matlab实现单纯形法是一种解线性规划问题的算法&#xf…

【Linux系统编程学习】 GCC编译器

此为牛客网Linux C课程1.2&1.3的课程笔记。 0. 简介 1. gcc和g的安装 sudo apt install gcc g2. gcc常用参数选项 3. gcc工作流程 首先是预处理器对源代码进行预处理&#xff08;后缀名.i&#xff09;&#xff0c;主要做以下事情&#xff1a; 把头文件加入到源代码当中删…

Spring5底层原理之BeanFactory与ApplicationContext

目录 BeanFactory与ApplicationContext BeanFactory ApplicationContext 容器实现 BeanFactory实现 ApplicationContext实现 ClassPathXmlApplicationContext的实现 AnnotationConfigApplicationContext的实现 AnnotationConfigServletWebServerApplicationContext的实…

【Linux系统编程学习】 静态库的制作与使用

此为牛客网Linux C课程 1.4&1.5 的课程笔记。 0. 关于静态库与动态库 库就是封装好的、可服用的代码&#xff0c;而静态和动态是指链接。 这节课讲的是静态库&#xff0c;是指在链接阶段&#xff0c;会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中&…

【Linux系统编程学习】 动态库的制作与使用

此为牛客网Linux C课程1.6&1.7 的课程笔记。 1. 动态库命名规则 2. 动态库的制作 第一步&#xff0c;用gcc编译生成.o目标文件&#xff0c;注意要用-fpic参数生成与位置无关的代码&#xff1b; 第二步&#xff0c;用gcc的-shared参数生成动态库。 涉及到的两个参数之前学过…

【Linux系统编程学习】 静态库与动态库的对比与总结

此为牛客网Linux C课程 1.9 的课程笔记。 1. 前几节课知识总结 程序编译成为可执行文件的过程&#xff1a; 静态库制作过程&#xff1a; 动态库制作过程&#xff1a; 2. 静态库的优缺点&#xff1a; 3. 动态库的优缺点&#xff1a; 更多可参考&#xff1a;吴秦&#xff1…

【Linux系统编程学习】 Makefile简单入门

此为牛客网Linux C课程1.10&1.11&1.12 的课程笔记。 0. Makefile介绍 1. Makefile文件命名与规则 示例&#xff1a; 使用vim编写如下名为Makefile的文件&#xff1a; app:sub.o add.o mult.o div.o main.ogcc sub.o add.o mult.o div.o main.o -o appsub.o:sub.cgcc …

【Linux系统编程学习】 GDB调试器的简单使用

此为牛客网Linux C课程 1.13&1.14&1.15&1.16 的课程笔记。 0. GDB简介 1. 准备工作 想要使用gdb调试&#xff0c;首先需要用gcc的-g参数生成可执行文件&#xff0c;这样才能在可执行文件中加入源代码信息以便调试&#xff0c;但是注意这并不是将源文件嵌入到可执行…

【Linux系统编程学习】C库IO函数与系统IO函数的关系

此为黑马Linux课程笔记。 1. C标准IO函数工作流程 如图&#xff0c;以C库函数的fopen为例&#xff0c;其返回类型是FILE类型的指针&#xff0c;FILE类型包含很多内容&#xff0c;主要包含三个内容&#xff1a;文件描述符、文件读写指针的位置和I/O缓冲区的地址。 文件描述符&…

【Linux系统编程学习】 文件描述符

此为牛客网Linux C课程1.19课程笔记。 1. 文件描述符表 如图&#xff0c;我们知道每个进程都有其虚拟地址空间&#xff08;0~4G&#xff09;&#xff0c;其中3 ~ 4G部分为内核区。进程的进程控制块保存就在内核区&#xff0c;而PCB中维护一个打开文件描述符表&#xff0c;每个…

【Linux系统编程学习】Linux系统IO函数(open、read、write、lseek)

此为牛客网Linux C课程1.20课程笔记。 1.open函数 open函数有两种&#xff0c;分别是打开一个已经存在的文件和创建并打开一个不存在的文件。 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>// 打开一个已经存在的文件 int open(const…

【Linux系统编程学习】Linux进程控制原语(fork、exec函数族、wait)

此为牛客Linux C和黑马Linux系统编程课程笔记。 1. fork函数 1.1 fork创建单个子进程 #include<unistd.h> pid_t fork(void);作用&#xff1a;创建一个子进程。 pid_t类型表示进程ID&#xff0c;但为了表示-1&#xff0c;它是有符号整型。(0不是有效进程ID&#xff0…

【Linux系统编程学习】匿名管道pipe与有名管道fifo

此为牛客Linux C和黑马Linux系统编程课程笔记。 0. 关于进程通信 Linux环境下&#xff0c;进程地址空间相互独立&#xff0c;每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到&#xff0c;所以进程和进程之间不能相互访问&#xff0c;要交换…

【Linux系统编程学习】信号、信号集以其相关函数

此为牛客Linux C和黑马Linux系统编程课程笔记。 文章目录0. 信号的概念1. Linux信号一览表2. 信号相关函数3. kill函数4. raise函数5. abort函数6. alarm函数7. setitimer函数8. signal函数9. 信号集10. 自定义信号集相关函数11. sigprocmask函数12. sigpending函数13. sigacti…

【Linux系统编程学习】父进程捕获SIGCHLD信号以处理僵尸进程

配合之前说过的sigaction函数和waitpid函数&#xff0c;我们可以解决子进程变成僵尸进程的问题。 先看如下示例程序&#xff1a; #include <sys/time.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> …

【Linux系统编程学习】Linux线程控制原语

此为牛客Linux C课程笔记。 0. 关于线程 注意&#xff1a;LWP号和线程id不同&#xff0c; LWP号是CPU分配时间片的依据&#xff0c;线程id是用于在进程内部区分线程的。 1. 线程与进程的区别 对于进程来说&#xff0c;相同的地址(同一个虚拟地址)在不同的进程中&#xff0c;反…

【Linux网络编程学习】预备知识(网络字节序、IP地址转换函数、sockaddr数据结构)

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 网络字节序 我们已经知道&#xff0c;内存中的多字节数据相对于内存地址有大端和小端之分。 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分&#xff0c;那么如何定义网络数…

【Linux网络编程学习】socket API(socket、bind、listen、accept、connect)及简单应用

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 什么是socket 所谓 socket&#xff08;套接字&#xff09;&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进程利用网络协议交换…

【Linux网络编程学习】使用socket实现简单服务器——多进程多线程版本

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 多进程版 1.1 思路 大体思路与上一篇的单进程版服务器–客户端类似&#xff0c;都是遵循下图&#xff1a; 多进程版本有以下几点需要注意&#xff1a; 由于TCP是点对点连接&#xff0c;服务器主进程连接了一个客户端以后…

【Linux网络编程学习】I/O多路复用——select和poll

此为牛客Linux C课程和黑马Linux系统编程笔记。 0. I/O多路复用 所谓I/O就是对socket提供的内存缓冲区的写入和读出。 多路复用就是指程序能同时监听多个文件描述符。 之前的学习中写了多进程和多线程版的简单服务器模型&#xff0c;但是有个问题&#xff1a;每次新来一个客…