操作系统——L7-进程同步与信号量
haoteby 2025-07-08 18:08 22 浏览
1、信号量
1.1 信号量的基本结构
关于如何使用信号量来设计生产者 - 消费者模型这里不做介绍。本节主要是想通过生产者 - 消费者模型来理解信号量的基本结构。我们先看看信号量的基本结构:
首先信号量是一个结构体
struct semaphore{
char *name;// 信号量的名字(这个其实可有可无)
int value; // 记录资源个数,就像 empty 一样。这个值不一定非要大于等于0,也可以是小于0的数,在后文有解释。
PCB *queue;// 记录等待在该信号量上的进程,方便之后进行唤醒。可以理解这是个队列,里面可以记录很多线程。
};
然后就是对信号量的操作(P/V 操作。其中P是指消费者,会消费资源;V是指生产者,会生产资源)
P(semaphore s){
s.value--;
if(s.value < 0)
sleep(s.queue); // 将当前线程睡眠,并将当前线程记录到 s.queue 队列中
}
V(semaphore s){
s.value++;
if(s.value <= 0)
wakeup(s.queue);// 唤醒 s.queue 队列中的线程。
}
有了信号量的基本结构后,我们在通过生产者 - 消费者模型来理解信号量的基本结构。为了方便分析,下面的信号量不使用结构体定义,信号量的操作函数(P、V)也是直接内联到代码中,下面代码中定义的互斥锁,是为了保护临界资源,至于互斥锁的实现原理个人认为应该和 1.2节:信号量临界区保护类似。
#define BUFFER_SIZE 10
item buffer[BUFFER_SIZE]; // 循环队列,队尾入,队首出(临界资源)
int in = out = 0; // in为队尾,out为队首
int empty = BUFFER_SIZE; // empty为队列中空位置的个数(信号量)
int full = 0; // full为队列中的元素个数(信号量)
mutex_def mutex; // 互斥锁
生产者进程不断生产 item,并将其存放到 buffer 队列中,消费者进程则不断从 buffer 队列中“消费” item。为了保证生产者进程和消费者进程能够有序推进,可以将它们设计为如下形式:
/*******生产者进程*******/
while (true) {
empty--; // P 操作,生产者消费 buffer 中的空位置
if(empty < 0)//若队列中已经没有空位置了则生产者停下
sleep(生产者进程);
mutex.lock(); // 上锁,这里上锁仅仅是为了确保只有一个线程执行下面两条语句
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
mutex.unlock(); // 解锁
full++; // V 操作,生产者生产资源并放如了 buffer 中
if(full <= 0)// 想想这里 full<=0 是什么意思?
wakeup(消费者进程);
}
/*******消费者进程*******/
while (true) {
full--;
if(full < 0) // 若队列为空则消费者停下
sleep(消费者进程);
mutex.lock(); // 上锁
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
mutex.unlock(); // 解锁
empty++;
// empty<=0 表示队列已经满了,目前还有生产者进程处于睡眠状态,但是现在已经有1个空位置了(empty++ 产生了一个空位置),因此需要唤醒一个生产者进程。
if(empty <= 0)
wakeup(生产者进程);
}
程序中的 empty 和 full 就是两个信号量。这里以 empty 为例来分析信号量。在上面的程序中 empty 有2个功能:(1)记录队列中空位置的个数;(2)根据 empty 的值来决定是否睡眠或唤醒生产者进程。什么是信号量? 记录一些信息(量),并根据这个信息决定睡眠还是唤醒(信号)。可以看出信号量有两个部分,量用来记录,信号用来sleep和wakeup。 相对于信号只有“有“或者”无“两种含义来说,信号量有着更多的含义。empty 的值有三种情况:
- empty > 0,表示 buffer 中还有 empty 个空位置;
- empty == 0,表示 buffer 中已经没有空位置(队列已满);
- empty < 0,表示 buffer 中还缺 empty 个空位置,即有 -empty 个进程需要向 buffer 存放生产好的 item,不过 buffer 已经没有空位置了,所以这 -empty 个进程会进入睡眠状态;
1.2 信号量临界区保护
在上一节的 P\V 操作中, s.value-- 和 s.value++ 在CPU中并不是一条指令完成的。如 s.value-- 可能是由几条指令组成:
register = s.value; // register为寄存器
register = register - 1;
s.value = register;
若几个进程同时调用了 P 操作,那么 s.value 运算结果可能就会产生错误。因此对 value 的加减运算应该加锁,即以上的3条指令应该一次完成,中间不能被打断。对信号量临界区的保护有3种方式:
- 面包店算法。可以有纯软件实现,缺点就是效率低
- 开关中断。在对 value 进行加减运算前,关闭中断,防止切换到其他进程,在运算结束后再打开中断。这个办法实现起来简单一些,但是只能再单CPU系统中使用,对于多CPU系统不适用。
- 硬件原子指令法。做一条硬件指令,用来检测锁是否已经被锁上。这种方式可以用于多CPU系统,但是这个需要硬件支持。如下图所示,TestAndSet() 为一条指令,函数内的操作都是一条指令完成,该函数用于检测锁是否已经被锁上。
1.2.1 轮换法
1.2.2 Peterson算法
结合了标记和轮转两种思想
标记为1,并且该1就1执行
1.2.3 面包店算法
1.2.4 关中断
1.2.5 硬件原子指令法
用临界区保护信号量,用信号量实现线程同步
2、Linux0.11中的进程同步案例
Linux0.11 中虽然没有实现信号量,但是有进程同步的地方。本节以 Linux0.11 中读磁盘的程序为例,看看 Linux0.11 中是如何做到进程同步的。下面是关于 Linux0.11 访问磁盘的介绍:
Linux 0.11访问磁盘的基本处理办法是在内存中划出一段磁盘缓存,用来加快对磁盘的访问。进程提出的磁盘访问请求首先要到磁盘缓存中去找,如果找到直接返回;如果没有找到则申请一段空闲的磁盘缓存,以这段磁盘缓存为参数发起磁盘读写请求。请求发出后,进程要睡眠等待(因为磁盘读写很慢,应该让出CPU让其他进程执行)。这种方法是许多操作系统(包括现代Linux、UNIX等)采用的较通用的方法。 ——摘取自实验指导书
发出读写请求的函数是由 make_request() 完成的,make_request() 中会调用 lock_buffer() 使进程进入睡眠。 lock_buffer() 函数如下:
// bh 为申请的内存缓冲区,bu中还有一个锁 b_lock,用于标记缓冲区是否被锁定
static inline void lock_buffer(struct buffer_head * bh){
cli(); // 关中断
while (bh->b_lock) // 若 b_lock == 1 则进入睡眠
sleep_on(&bh->b_wait); // 将当前进程睡眠在bh->b_wait
bh->b_lock=1; // 缓冲区上锁
sti(); // 开中断
}
可以看出Linux0.11是利用开关中断来保护信号量临界区的。sleep_on函数如下:
// p 是等待队列的队头指针
void sleep_on(struct task_struct **p){
struct task_struct *tmp; // 注意tmp是再内核栈中,而每一个进程都有一个内核栈。
...
tmp = *p; //tmp指向队头
*p = current; //p指向当前指针
current->state = TASK_UNINTERRUPTIBLE; // 将当前进程设为阻塞态
schedule(); //切到下一个进程去执行
// 当前进程被唤醒后,就会从这里重新开始执行。若 tmp 不为空则将tmp进程改为就绪态。
// 当tmp变为运行态后,又会从这里开始执行,从而又唤醒tmp的下一个进程,如此往复
// 直到将等待队列中的全部进程全部唤醒。
if (tmp)
tmp->state=0;
}
sleep_on() 比较难理解,其是它是通过 tmp 这个局部变量来形成一个等待队列的。看看下面这个图就容易理解了:
当请求成功,并获取到磁盘内容后,磁盘会引发系统中断来唤醒请求的进程:
static inline void unlock_buffer(struct buffer_head * bh){
...
bh->b_lock = 0; // 解锁缓冲区
wake_up(&bh->b_wait); // 唤醒请求磁盘的进程
}
wake_up() 的实现如下:
void wake_up(struct task_struct **p){
if (p && *p) {
(**p).state=0; // 将 p 置为就绪态
*p=NULL;
}
}
相关推荐
- 一日一技:用Python程序将十进制转换为二进制
-
用Python程序将十进制转换为二进制通过将数字连续除以2并以相反顺序打印其余部分,将十进制数转换为二进制。在下面的程序中,我们将学习使用递归函数将十进制数转换为二进制数,代码如下:...
- 十进制转化成二进制你会吗?#数学思维
-
六年级奥赛起跑线:抽屉原理揭秘。同学们好,我是你们的奥耀老师。今天一起来学习奥赛起跑线第三讲二进制计数法。例一:把十进制五十三化成二进制数是多少?首先十进制就是满十进一,二进制就是满二进一。二进制每个...
- 二进制、十进制、八进制和十六进制,它们之间是如何转换的?
-
在学习进制时总会遇到多种进制转换的时候,学会它们之间的转换方法也是必须的,这里分享一下几种进制之间转换的方法,也分享两个好用的转换工具,使用它们能够大幅度的提升你的办公和学习效率,感兴趣的小伙伴记得点...
- c语言-2进制转10进制_c语言 二进制转十进制
-
#include<stdio.h>intmain(){charch;inta=0;...
- 二进制、八进制、十进制和十六进制数制转换
-
一、数制1、什么是数制数制是计数进位的简称。也就是由低位向高位进位计数的方法。2、常用数制计算机中常用的数制有二进制、八进制、十进制和十六进制。...
- 二进制、十进制、八进制、十六进制间的相互转换函数
-
二进制、十进制、八进制、十六进制间的相互转换函数1、输入任意一个十进制的整数,将其分别转换为二进制、八进制、十六进制。2、程序代码如下:#include<iostream>usingna...
- 二进制、八进制、十进制和十六进制等常用数制及其相互转换
-
从大学开始系统的接触计算机专业,到现在已经过去十几年了,今天整理一下基础的进制转换,希望给还在上高中的表妹一个入门的引导,早日熟悉这个行业。一、二进制、八进制、十进制和十六进制是如何定义的?二进制是B...
- 二进制如何转换成十进制?_二进制如何转换成十进制例子图解
-
随着社会的发展,电器维修由继电器时代逐渐被PLC,变频器,触摸屏等工控时代所替代,特别是plc编程,其数据逻辑往往涉及到数制二进制,那么二进制到底是什么呢?它和十进制又有什么区别和联系呢?下面和朋友们...
- 二进制与十进制的相互转换_二进制和十进制之间转换
-
很多同学在刚开始接触计算机语言的时候,都会了解计算机的世界里面大多都是二进制来表达现实世界的任何事物的。当然现实世界的事务有很多很多,就拿最简单的数字,我们经常看到的数字大多都是十进制的形式,例如:我...
- 十进制如何转换为二进制,二进制如何转换为十进制
-
用十进制除以2,除的断的,商用0表示;除不断的,商用1表示余0时结束假如十进制用X表示,用十进制除以2,即x/2除以2后为整数的(除的断的),商用0表示;除以2除不断的,商用1表示除完后的商0或1...
- 十进制数如何转换为二进制数_十进制数如何转换为二进制数举例说明
-
我们经常听到十进制数和二进制数,电脑中也经常使用二进制数来进行计算,但是很多人却不清楚十进制数和二进制数是怎样进行转换的,下面就来看看,十进制数转换为二进制数的方法。正整数转二进制...
- 二进制转化为十进制,你会做吗?一起来试试吧
-
今天孩子问把二进制表示的110101改写成十进制数怎么做呀?,“二进制”简单来说就是“满二进一”,只用0和1共两个数字表示,同理我们平常接触到的“十进制”是“满十进一”,只用0-9共十个数字表示。如果...
- Mac终于能正常打游戏了!苹果正逐渐淘汰Rosetta转译
-
Mac玩家苦转译久矣!WWDC2025苹果正式宣判Rosetta死刑,原生游戏时代终于杀到。Metal4光追和AI插帧技术直接掀桌,连Steam都连夜扛着ARM架构投诚了。看到《赛博朋克2077》...
- 怎么把视频的声音提出来转为音频?音频提取,11款工具实测搞定
-
想把视频里的声音单独保存为音频文件(MP3/AAC/WAV/FLAC)用于配音、播客、听课或二次剪辑?本文挑出10款常用工具,给出实测可复现的操作步骤、优缺点和场景推荐。1)转换猫mp3转换器(操作门...
- 6个mp4格式转换器测评:转换速度与质量并存!
-
MP4视频格式具有兼容性强、视频画质高清、文件体积较小、支持多种编码等特点,适用于网络媒体传播。如果大家想要将非MP4格式的视频转换成MP4的视频格式的话,可以使用MP4格式转换器更换格式。本文分别从...