操作系统——L7-进程同步与信号量
haoteby 2025-07-08 18:08 7 浏览
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;
}
}
相关推荐
- 手把手教你构建一个简单的Eclipse RCP应用
-
EclipseRCP应用,通常用来构建跨平台的图形化管理客户端,Eclipse从IBM开源以来,一直占据开源Java开发平台的头把交椅,现在仍然收到很多人的追捧。今天就带大家通过一个简单的例子:开发...
- Eclipse配置maven 环境(maven的配置、以及eclipse中配置maven)
-
Eclipse配置maven环境的先决条件是,Windows系统已经配置好maven环境Eclipse配置maven环境步骤如下:一、给Eclipse添加本地maven...
- 如何在Eclipse中搭建Zabbix源码的调试和开发环境
-
Zabbix是一款非常优秀的企业级软件,被设计用于对数万台服务器、虚拟机和网络设备的数百万个监控项进行实时监控。Zabbix是开放源码和免费的,这就意味着当出现bug时,我们可以很方便地通过调试源码来...
- Eclipse中将现有的maven项目 导入Git,并发布到
-
Eclipse中将现有的maven项目导入Git,并发布到github一、Eclipse中将现有的maven项目导入Git1.将本地的maven项目,添加他的子项目到git仓库,并发布到githu...
- eclipse安装图解(eclipse安装教程2021)
-
下载eclipse之前请先安装jdk、查看自己电脑系统是多少位第一步:打开官网https://www.eclipse.org/downloads/第二步:点击DownloadPackages第三...
- Eclipse IDE for C/C++ Developers 开发环境搭建详解
-
EclipseIDEforC/C++Developers开发环境搭建详解1.到官网下载eclipseforC/C++Developmer解压就行2.下载MinGW用来编译C/C+...
- 来来来!一文告诉你Eclipse的正确安装使用姿势,你都清楚吗?
-
前言本学习笔记是有关如何设置Eclipse的详细说明。即使你天天在使用它,但是,相信我,或许你并不足够了解它。安装Java运行时环境Eclipse是Java应用程序,因此设置Eclipse的第一步是安...
- 纯干货!Eclipse的安装与使用(eclipse 安装教程)
-
之前有人给小华君留言,说让小华君讲一讲Eclipse,那好,我们今天就简单地讲一下。讲得也是基础部分,如题,主要是Eclipse的安装与使用。废话不多说,开始讲。Eclipse是Java开发的集成开发...
- 2020 最新版jdk & eclipse下载安装 之JDK(一)
-
首次安装Eclipse,去官网下载资源找不对安装包,安装之后又报错,如果和我一样的话,那就来看我的分享吧安装eclipse前,需要先安装JDK软件首先,到oracle官网下载JDK安装包下载链接:...
- Eclipse 安装教程(附安装包下载)(eclipse安装教程最新版)
-
Eclipse软件介绍是一个开放源代码、基于Java的可扩展开发平台。它本身只是一个框架和一组服务,通过插件组件构建开发环境。幸运的是,Eclipse附带了一个标准的插件集,包括Java开发工具(Ja...
- JDK安装、Eclipse安装及运行环境配置
-
1、eclipse下载打开地址:http://www.eclipse.org/downloads/;根据自己机器的操作系统,页面上显示适应机器操作系统的Eclipse下载列表,也可以点击下图所示位置切...
- Ubuntu Linux 21.10官方壁纸现已提供下载 最高8192×4608分辨率
-
距离十月份的Ubuntu21.10Linux发行版的到来,已只有数周的时间。在今年4月介绍了与之有关的大量细节之后,Canonical现又放出了代号为“ImpishIndri”的这一大...
- Linux 4.7系统内核发布:支持RX 480
-
经过一周休假之后,LinusTorvalds今天正式发布了新版LinuxKernel4.7,可在官网直接下载。Linux4.7版内核的开发启动于5月29日,经过了七个RC候选版,加入了不少新特...
- 开发企业官网就用这个基于SpringBoot的CMS系统,真香
-
前言推荐这个项目是因为使用手册部署手册非常...
- 非常详细的Linux系统安装教程!建议收藏
-
公众号:老油条IT记一、下载ISO镜像#官网:CentOS:http://mirror-status.centos.org/#cn#其他:网易:http://mirrors.163.com/cento...