什么是信号量-linux快速入门教程
1. 简介
在本教程中,我们将深入探讨一个功能强大且众所周知的进程同步工具:信号量。
我们将研究信号量操作、类型及其实现。然后,我们将探讨一些多线程案例,在这些情况下,使用信号量可以帮助我们克服可能的进程同步问题。
2. 什么是信号量?
信号量是一个整数变量,在多个进程之间共享。使用信号量的主要目的是对并发环境中的公共资源进行进程同步和访问控制。
信号灯的初始值取决于手头的问题。通常,我们使用可用资源的数量作为初始值。在以下部分中,我们将提供更多在不同用例中初始化信号量的示例。
这是一个同步工具,不需要忙于等待。因此,当进程由于无法访问资源而无法运行时,操作系统不会浪费 CPU 周期。
2.1. 信号量操作
信号量有两个不可分割的(原子)操作,即:wait和signal。这些操作有时简称为P与V。
在本文中,让我们重点介绍在操作系统内核中实现的信号量,一般情况下P和V操作作为系统调用原始实现来呈现。
设S是一个信号量,它的整数值表示可用资源的数量。现在让我们看看wait-等待操作是如何工作的:
wait(S)如果函数大于零(有可用资源可供分配),则该函数只会递减。如果已为零(没有要分配的可用资源),则S调用进程进入睡眠状态。
现在让我们检查一下signal操作:
如果没有其他进程正在等待资源,则signal操作将递增 S。否则,不会增加其值,而是选择等待进程由操作系统调度程序唤醒。因此,该进程将获得对资源的控制。
2.2. 信号量类型
有两种类型的信号量:
- 二进制信号量
- 计数信号量
二进制信号量只能有两个整数值:0 或 1。它更易于实现并提供互斥。我们可以使用二进制信号量来解决关键部分问题。
一些有经验的读者可能会将二进制信号量与互斥锁混淆。有一个常见的误解,认为它们可以互换使用。但实际上,信号量是一种信号机制,而另一方面,互斥锁是一种锁定机制。因此,我们需要知道二进制信号量不是互斥锁。
计数信号量又是一个整数值,其范围可以跨越不受限制的域。我们可以使用它来解决资源分配等同步问题。
3. 信号量实现
如上所述,我们专注于在操作系统内核中实现的信号量。
没有繁忙等待的实现需要一个整数值(用于保存信号量值)和一个指向等待列表中下一个进程的指针。该列表包含操作上进入休眠状态的进程。内核使用两个附加操作:and来命令进程。
我们可以将信号量实现视为一个关键的节问题,因为我们不希望多个进程同时访问信号量变量。
几种编程语言支持并发和信号量。例如,Java支持信号量,我们可以在多线程程序中使用它们。
4. 进程同步
在多线程环境中,进程同步意味着并发进程有效地共享系统资源。确保同步执行需要一种方法来协调使用共享数据的进程。令人满意的同步机制在避免死锁和匮乏的基础上提供并发性。
谨慎使用时,信号量是一种功能强大的同步工具,可以启用进程同步。接下来让我们来看看如何解决一些问题。
4.1. 避免死锁
当一组进程处于等待其他成员操作的状态被阻塞时,就会发生死锁。为了避免可能的死锁,我们需要小心如何实现信号量。让我们检查一个死锁的情况:
在这里,Process 0 并Process 1 应该有一些可以单独运行的代码。
考虑一种情况,即Process 0执行 wait(S) 并在此之后立即中断。然后让我们假设Process 1开始运行。最初,在信号量上等待Q,Process 1然后在 wait(S) 语句上被阻止,因此它进入睡眠状态。
当内核再次运行时,行中的下一个操作正在wait (Q)。由于信号量Q已经被Process 1使用,现在我们遇到了两个进程都在等待对方的情况,因此出现了死锁。
如果我们要反转任何进程中行的顺序并使用通用排序,我们将省略此示例中的死锁。
4.2. 避免饥饿
我们需要注意的下一个概念是饥饿。饥饿意味着无限期地阻塞一个进程。例如,死锁情况可能会导致某些进程出现饥饿。
一个著名的饥饿例子是餐饮哲学家问题,我们将在以下各节中介绍。在这个问题上,五位哲学家围坐在一张圆桌旁。他们共用五根筷子。每个哲学家要么思考,要么吃东西。要吃饭,哲学家需要两根筷子:
这个假设的问题象征着饥饿。如果一个哲学家的过程因为无法获得筷子而永远受阻,他就会饿死。
使用进程同步,我们希望确保每个进程迟早都会得到服务。
4.3. 优先级倒置
信号量使用效率低下可能导致的另一个可能问题是优先级反转。当优先级较低的作业优先于优先级较高的作业时,会发生优先级倒置。
假设一个低优先级进程持有一个信号量,这是高优先级进程所需要的。此外,假设中等优先级进程也在调度程序队列中等待:
在这种情况下,内核不会调度低优先级进程阻止高优先级进程。相反,内核继续执行中等优先级作业,导致高优先级作业继续等待。
一种可能的解决方案是在信号量上使用优先级继承。
5. 信号量的作用
现在我们已经定义了信号量是什么,让我们来探索信号量的一些用例。
5.1. 临界截面问题
5.1. 临界截面问题
临界区是程序代码的一部分,我们希望避免并发访问。我们可以使用二进制信号量来解决临界区部分问题。在这种情况下,信号量的初始值在内核中为 1:
在上面的例子中,我们保证在临界区访问中的相互排斥。

等待进程不是忙碌地等待,而是在临界区等待轮到它时处于休眠状态。
然后执行信号操作,内核将一个睡眠进程添加到就绪队列中。如果内核决定运行该进程,它将继续进入临界区。
通过这种方式,我们可以确保在任何时候只有一个进程处于临界区。
5.2. 按序执行
假设我们S1之前S2要执行代码片段。换句话说,我们希望Process 1在执行S2之前等待,直到Process 0 完成执行S1:

我们可以通过使用信号量轻松解决此问题,其初始值设置为 0。让我们修改代码:

通过使用信号量x,我们保证在执行该部分S2之前Process 1等待,直到Process 1 执行完毕S1。
5.3. 生产者-消费者问题
接下来让我们考虑一下众所周知的生产者-消费者问题。在这个问题中,有一个有限的缓冲区,由buf_size单元格组成。换句话说,缓冲区可以存储最大数量为buf_size个元素。两个进程正在访问缓冲区,即:Producer和Concusmer。
为了克服这个问题,我们使用一个计数信号灯S来表示完整单元格的数量:
最初,S=0。当生产者将项目放入缓冲区时,它会通过信号操作增加信号量。相反,当消费者消费项目时,通过等待操作,信号量会减少。
当使用者使用缓冲区中的最后一项时,最后一个等待操作将其置于睡眠状态。
5.4. 有界缓冲区生产者-消费者问题
在上面的解决方案之上,如果缓冲区已满,我们可以通过再添加两个信号量来强制生产者休眠。假设我们引入表示缓冲区中可用单元格数量的信号量empty。我们还介绍了信号量full,表示缓冲区中完整单元格的数量。在这种情况下,我们在缓冲区S上使用信号量。同样,生产者和使用者进程同时运行。
我们修改生产者和使用者代码以包含信号量:
此处,计数信号量表示完整单元格的数量,而计数信号量full,empty表示有界缓冲区上可用的单元格数。此外,信号量 S 可保护缓冲区免受并发访问。
我们先观察生产者的行为:empty如果信号量变为 0,则生成者进入睡眠状态,因为缓冲区中不再有空单元格。
此外,生产者在访问缓冲区S之前等待信号量。如果另一个进程已经在等待S,它将进入睡眠状态。
在生成一个新项目后,生产者向信号灯发出full信号,从而触发唤醒任何可能的消费者的任何等待过程。
同样,使用者使用相同的信号量:
full如果信号量为零,则缓冲区中没有等待的项目。消费者被置于睡眠状态。
上的S等待操作可确保在给定时间仅由一个进程访问缓冲区。因此,打开S的信号会唤醒任何等待的进程。
最后,empty信号灯上的信号唤醒任何有东西要放入缓冲区的生产者。最后,消费者已准备好消费下一件商品。
此解决方案可确保创建者休眠,直到缓冲区中有可用插槽。相反,消费者会睡觉,直到有物品可以消费。
5.5. 读写器问题
这个问题就是有几个readers,writers在同一数据集上工作。进程readers仅读取数据集;它们不执行任何更新。而write进程既可以读取也可以写入。
我们在这里遇到的同步问题是在任何给定时间只允许一个writer访问数据集。我们可以允许多个读者同时阅读而没有任何问题。
假设我们对数据集有锁定。第一个reader应该锁定数据集,然后readers可以继续并开始读取。最后一个reader应在完成读取时释放锁。
当 writer开始读取或停止读取时,它应该为其他writers和readers锁定/解锁数据集。
要在此问题中启用进程同步,我们可以使用一个整数值和两个信号量。
让我们将整数的read初始化为零,它表示readers访问文件的次数。这不是信号量,只是一个整数。
另外,让我们将信号量S初始化为 1,这样可以防止read变量被多个进程更新。
最后,假设我们将信号量write初始化为 1,这可以保护数据集不被多个writers同时访问。所以writer和reader进程演示伪代码以下:
这种情况下,写入器只等待并写入信号量上的信号。如果它能获得写信号量,它就可以继续。否则,等待操作将阻塞它。
一旦写入操作完成,写入器就发出写信号量的信号。因此,其他进程可以访问该数据集。
reader的工作流程更为复杂。它做的第一件事是增加read_count。为此reader需要获取信号量S来修改read_count值,以确保它不会被多个进程同时访问。
如果当前没有其他reader正在访问该数据集,则在增量操作之后,read_count变为1。在这种情况下,reader在访问数据集之前等待信号量写入,以确保没有writer正在访问。在释放信号量S之后,reader继续执行读取操作。
最后,当reader完成对数据集的处理后,需要递减read_count的值。同样,它获取信号量S,更改read_count变量,最后释放信号量S。
5.6. 餐饮哲学家问题
餐饮哲学家问题是一个著名的思想实验,用于测试同步工具。
我们有五个哲学家围坐在一张桌子旁,拿着五根筷子。每当哲学家想吃饭时,它需要两根筷子。在这个问题中,筷子是我们想要在进程之间同步的资源。
让我们使用信号量实现一个解决方案。首先,我们初始化信号量,它将五根筷子表示为5个数组。然后,我们实现一个as:
用餐哲学家问题是一个著名的用于测试同步工具的思想实验。
五位哲学家围坐在一张桌子旁,拿着五根筷子。每当哲学家想吃东西的时候,就需要两把筷子。在这个问题中,筷子是我们想要在进程之间同步的资源。
让我们使用信号量实现一个解决方案。首先,我们初始化信号量,它将5根筷子表示为一个长度是5的数组。然后,我们实现一个用餐哲学家问题解决方案如下:
在这个解决方案中,当哲学家i决定吃东西时,它首先等待信号量i。然后在获得它之后,哲学家等待信号量(i+1)%5。当两根筷子都拿到了,它就可以吃了。
当哲学家吃饱时,它通过调用相应信号灯上的信号操作,以相同的顺序释放筷子。
我们需要记住,此示例并不能保证无死锁解决方案。我们可以实施一个小的调整来保证所有三个条件。
6. 结论
信号量是一个非常强大的进程同步工具。
在本教程中,我们首先通过定义两个原子操作来总结信号量的工作原理:等待和信号。在更好地了解可能的进程同步问题后,我们介绍了一些示例,以更好地了解如何有效地使用信号量。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· 分享4款.NET开源、免费、实用的商城系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· Windows 提权-UAC 绕过