信号量及其使用和实现(超详细)

互斥锁,我们刚刚讨论过了,通常认为是最简单的同步工具。本节将会讨论一个更棒的工具,它的功能类似于互斥锁,但是它能提供更为高级的方法,以便进程能够同步活动。

一个信号量 S 是个整型变量,它除了初始化外只能通过两个标准原子操作:wait () 和 signal() 来访问:
  • 操作 wait() 最初称为 P(荷兰语proberen,测试);
  • 操作 signal() 最初称为 V(荷兰语verhogen,增加);

可按如下来定义wait ():
wait(S){
    while (S <= 0)
        ;// busy wait
    S--;
}
可按如下来定义signal():
signal(S) {
    S++;
}
在 wait() 和 signal() 操作中,信号量整数值的修改应不可分割地执行。也就是说,当一个进程修改信号量值时,没有其他进程能够同时修改同一信号量的值。另外,对于 wait(S),S 整数值的测试(S < 0)和修改(S--)也不能被中断。

首先,我们看看如何使用信号量。

信号量的使用

操作系统通常区分计数信号量与二进制信号量。计数信号量的值不受限制,而二进制信号量的值只能为 0 或 1。因此,二进制信号量类似于互斥锁。事实上,在没有提供互斥锁的系统上,可以使用二进制信号量来提供互斥。

计数信号量可以用于控制访问具有多个实例的某种资源。信号量的初值为可用资源数量。当进程需要使用资源时,需要对该信号量执行 wait() 操作(减少信号量的计数)。当进程释放资源时,需要对该信号量执行 signal() 操作(增加信号量的计数)。当信号量的计数为 0 时,所有资源都在使用中。之后,需要使用资源的进程将会阻塞,直到计数大于 0。

我们也可以使用信号量来解决各种同步问题。例如,现有两个并发运行的进程:P1 有语句 S1 而 P2 有语句 S2。假设要求只有在 S1 执行后才能执行 S2。我们可以轻松实现这一要求:让 P1 和 P2 共享同一信号量 synch,并且初始化为 0。

在进程 P1 中,插入语句:

S1;
signal (synch);

在进程 P2 中,插入语句:

wait (synch);
S2;

因为 synch 初始化为 0,只有在 P1 调用 signal(synch) ,即 S1 语句执行之后,P2 才会执行 S2。

信号量的实现

回想一下,互斥锁实现具有忙等待。刚才描述的信号量操作 wait() 和 signal(),也有同样问题。

为了克服忙等待需要,可以这样修改信号量操作 wait() 和 signal() 的定义:当一个进程执行操作 wait() 并且发现信号量值不为正时,它必须等待。然而,该进程不是忙等待而是阻塞自己。阻塞操作将一个进程放到与信号量相关的等待队列中,并且将该进程状态切换成等待状态。然后,控制转到 CPU 调度程序,以便选择执行另一个进程。

等待信号量 S 而阻塞的进程,在其他进程执行操作 signal() 后,应被重新执行。进程的重新执行是通过操作 wakeup() 来进行的,它将进程从等待状态改为就绪状态。然而,进程被添加到就绪队列。(取决于 CPU 调度算法,CPU 可能会也可能不会从正在运行的进程切换到新的就绪进程。)

为了实现这样定义的信号量,我们按如下定义信号量:
typedef struct {
    int value;
    struct process *list;
} semaphore;
每个信号量都有一个整数 value 和一个进程链表 list。当一个进程必须等待信号量时,就被添加到进程链表。操作 signal() 从等待、进程链表上取走一个进程,并加以唤醒。

现在,信号量操作 wait() 可以定义如下:
wait(semaphore *S) {
    S->value--;
    if (S->value < 0) {
        add this process to S->list;
        block();
    }
}
而信号量操作 signal() 可定义如下:
signal(semaphore *S) {
    S->value++;
    if (S->value <= 0) {
        remove a process P from S->list;
        wakeup(P);
    }
}
操作 block() 挂起调用它的进程。操作 wakeup(P) 重新启动阻塞进程 P 的执行。这两个操作都是由操作系统作为基本系统调用来提供的。

注意,这样实现的信号量的值可以是负数,而在具有忙等待的信号量经典定义下,信号量的值不能为负。如果信号量的值为负,那么它的绝对值就是等待它的进程数。出现这种情况源于,在实现操作 wait() 时互换了递减和测试的顺序。

通过每个进程控制块 PCB 的一个链接字段,等待进程的链表可以轻松实现。每个信号量包括一个整数和一个 PCB 链表指针。向链表中增加和删除进程以便确保有限等待的一种方法采用 FIFO 队列,这里的信号量包括队列的首指针和尾指针。然而,一般来说,链表可以使用任何排队策略。信号量的正确使用不依赖于信号量链表的特定排队策略。

关键的是,信号量操作应原子执行。我们应保证:对同一信号量,没有两个进程可以同时执行操作 wait() 和 signal()。这是一个临界区问题。

对于单处理器环境,在执行操作 wait() 和 signal() 时,可以简单禁止中断。这种方案在单处理器环境下能工作,这是因为一旦中断被禁用,不同进程指令不会交织在一起。只有当前运行进程一直执行,直到中断 被重新启用并且调度程序重新获得控制。

对于多处理器环境,每个处理器的中断都应被禁止;否则,在不同处理器上不同的运行进程可能会以任意不同方式一起交织执行。每个处理器中断的禁止会很困难,也会严重影响性能。因此,SMP 系统应提供其他加锁技术,如 compare_and__swap() 或自旋锁,以确保 wait() 与 signal() 原子执行。

重要的是,对于这里定义的操作 wait() 和 signal(),我们并没有完全取消忙等待。我们只是将忙等待从进入区移到临界区。此外,我们将忙等待限制在操作 wait() 和 signal() 的临界区内,这些区比较短(如经合理编码,它们不会超过 10 条指令)。因此,临界区几乎不被占用,忙等待很少发生,而且所需时间很短。对于应用程序,存在一种完全不同的情况,即临界区可能很长(数分钟或数小时)或几乎总是被占用。在这种情况下,忙等待极为低效。

死锁与饥饿

具有等待队列的信号量实现可能导致这样的情况:两个或多个进程无限等待一个事件,而该事件只能由这些等待进程之一来产生。当出现这样的状态时,这些进程就为死锁

为了说明起见,假设有一个系统,它有两个进程 P0 和 P1,每个访问共享信号量 S 和 Q,这两个信号量的初值均为 1:
P0 P1
wait(S);  wait(Q);
wait(Q);   wait(S);
signal(S); signal(Q);
signal(Q); signal(S);

假设 P0 执行 wait(S),接着 P1 执行 wait(Q)。当 P0 执行 wait(Q) 时,它必须等待直到 P1 执行 signal(Q)。类似地,当 P1 执行 wait(S) 时,它必须等待直到 P0 执行 signal(S)。由于这两个操作 signal() 都不能执行,这样 P0 和 P1 就死锁了。

我们说一组进程处于死锁状态:组内的每个进程都等待一个事件,而该事件只可能由组内的另一个进程产生。这里主要关心的事件是资源的获取和释放。然而,其他类型的事件也能导致死锁。

与死锁相关的另一个问题是无限阻塞或饥饿,即进程无限等待信号量。如果对与信号量有关的链表按 LIFO 顺序来增加和删除进程,那么可能发生无限阻塞。

优先级的反转

如果一个较高优先级的进程需要读取或修改内核数据,而且这个内核数据当前正被较低优先级的进程访问(这种串联方式可涉及更多进程),那么就会出现一个调度挑战。由于内核数据通常是用锁保护的,较高优先级的进程将不得不等待较低优先级的进程用完资源。如果较低优先级的进程被较高优先级的进程抢占,那么情况变得更加复杂。

比如,假设有三个进程 L、M 和 H,它们的优先级顺序为 L<M<H。假定进程 H 需要资源 R,而 R 目前正在被进程 L 访问。通常,进程 H 将等待 L 用完资源 R。但是,现在假设进程 M 进入可运行状态,从而抢占进程 L。间接地,具有较低优先级的进程 M,影响了进程 H 应等待多久,才会使得进程 L 释放资源 R。

这个问题称为优先级反转。它只出现在具有两个以上优先级的系统中,因此一个解决方案是只有两个优先级。然而,这对于大多数通用操作系统是不够的。通常,这些系统在解决问题时采用优先级继承协议。

根据这个协议,所有正在访问资源的进程获得需要访问它的更高优先级进程的优先级,直到它们用完了有关资源为止。当它们用完时,它们的优先级恢复到原始值。在上面的示例中,优先级继承协议将允许进程 L 临时继承进程 H 的优先级,从而防止进程 M 抢占执行。当进程 L 用完资源 R 时,它将放弃继承的进程 H 的优先级,以采用原来的优先级。因为资源 R 现在可用,进程 H(而不是进程 M)会接下来运行。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


起步 处理器架构,参考 x86是指intel的开发的一种32位指令集 intel和amd早期的cpu都支持这种指令集 AMD比Intel率先制造出了商用的兼容x86的CPU,AMD称之为AMD64 Intel选择了设计一种不兼容x86的全新64为指令集,称之为IA-64,后来支持AMD64的指令集,
pscp pscp -P 22 C:\work\test.txt root@192.168.1.5:/home/data pscp -P 22 root@192.168.1.5:/home/data/test.txt C://work// 检索 find / -name default.config
文件处理 ls -a # 显示所有文件 ls -l # 显示详细信息 ls -d # 显示路径 mkdir /目录名称 # 创建目录 cd /目录名称 # 切换目录 pwd # 显示当前路径 rmdir /目录名称 # 删除目录 cp -rp [目录名称] [目标目录] # 复制目录到目标目录 cp
准备一台电脑(我就用联想拯救者r7000演示) 参考博客制作启动盘 插上U盘,启动电脑,一直按F2 进入如下页面后,将U盘设置为第一启动项,点击exit,保存并退出 之后进入如下页面,选择第三项 进入如下页面,选择第四项 进入如下页面,选择第一项,选中后,先不要点Enter 按e键,将inst.st
认识 Linux系统是参考了UNIX系统作为模板开发的,但没有使用UNIX的代码;是UNIX的一种,但不是衍生版 在Linux内核的基础上开发是发行版 分区 逻辑分区永远从5开始 步骤 挂载:可理解为分配盘符,挂载点即是盘符名;不同之处:Linux中是以空目录名称作为盘符 Hda 第一块硬盘 Hda
文件处理命令 以 . 开头的文件是隐藏文件 以 - 开头表示这是一个文件 以 d 开头表示是一个目录 以 l 开头表示是一个软链接 第一个root是所有者,第二个root是所属组 ls -h 以文件默认大小后缀 显示 ls -i 查看i节点(唯一标识) 所有者:只能有一个,可变更 所属组:只能有一个
参考 01 02 03 前提环境 本地安装VirtualBox,并安装CentOS8,配置网络后,window系统上putty能连接到CentOS8服务器 配置步骤 右键服务器复制 启动复制后的服务器,查看ip和hostname发现和原来的服务器一样,需要修改 hostname # 查看主机名 vi
文件搜索命令 星号匹配任意字符,问号匹配任意单个字符 -iname 根据文件名查找且不区分大小写 -ok 命名会有一个询问的步骤 如果没有找到指定文件,可输入命令:updatedb 更新文件资料库;除tmp目录不在文件资料库收录范围之内 locate -i 文件名 # 检索时不区分大小写 which
安装环境 安装最新版的Virtual Box,点击安装 下载centos8镜像 创建虚拟机,可参考 选择下载到本地的镜像 设置启动顺序 点击启动 启动过程中报错:“FATAL:No bootable medium found!” 1.没有选择iso镜像 2.光驱没有排在第一位置 3.镜像只能选择x8
Linux严格区分大小写 所有内容文件形式保存,包括硬件 Linux不靠扩展名区分文件类型 挂载:将设备文件名和挂载点(盘符)连接的过程 Linux各个目录的作用 bin表示二进制 服务器注意事项 远程服务器不允许关机,只能重启 重启时应该关闭服务 不要在服务器访问高峰运行高负载命令 远程配置防火墙
IDE连接Linux,上传下载文件 参考1 参考2 连接Linux 上传下载文件 本地项目打包后上传 查看是否上传成功,右键下载 补充 后端项目开发完成后,需clean掉临时文件target文件夹,且只推送修改过的文件 前端项目开发的过程中,需要在每个子组件中使用scoped,确保每个子组件中的编码
起步 LTS与普通版本的区别 LTS版本的发布周期更长,更加稳定 安装jdk sudo mkdir /usr/lib/jvm # 在Ubuntu中创建目录 pscp D:\安装包\linux源码包\jdk-8u291-linux-x64.tar.gz chnq@192.168.0.102:/tmp
前言 最近在b站上看了兄弟连老师的Linux教程,非常适合入门:https://www.bilibili.com/video/BV1mW411i7Qf 看完后就自己来试着玩下,正好手上有台空闲的电脑就尝试不使用虚拟机的方式安装Linux系统 安装步骤 制作启动盘 下载ISO镜像,我这里下载的是Cen
新建虚拟电脑 设置内存和处理器 设置硬盘大小 完成 设置 查看光驱 设置启动顺序 点击启动 选择第1项 进入图形安装界面 选择安装位置,开始安装 设置root密码 重启 登录 查看本地文件夹 配置网络,点击设置 查看宿主机ip C:\Users\ychen λ ipconfig 无线局域网适配器 W
源码包安装需手动下载后安装 二进制包则在package目录下 rpm命令管理rpm包 若某个rpm包依赖于某个模块,需要到网站www.rpmfind.net查询该模块依赖的包,安装这个包后自动安装模块,之后就能安装rpm包了 安装升级时使用包全名 查询卸载时使用包名 虚拟机中的Linux系统安装rp
首先进入命令模式,再输入以下命令 命令模式用于输入命令 插入模式可对文件编写操作 编辑模式下的命令是在冒号后输入 :12, 15d # 删除指定范围的行,这里是删除12到15行 :n1,n2s/old/new/g ## 表示从n1行到n2行,old表示旧的字符串 vim使用小技巧:自定义快捷键,如快
使用源码包安装,需要自己指定安装位置,通常是 /usr/local/软件名/ linux中要想启动执行文件,应使用绝对路径 /绝对路径/rpm包名 start ## 执行方式一 service rpm包名 start ## 执行方式二 使用源码包安装后,由于自定义安装路径,就不能使用service命
网络命令 在收邮件的用户中,输入 mail 可查看邮件信息,输入序列号查看详细信息 在mail命令下,输入h 查看所有邮件的列表 输入:d 序列号 # 删除邮件 last # 统计所有用户登录或重启时间,用于日志查询 lastlog # 显示包括未登录用户的登录时间 lastlog -u 用户id
若要使用yum管理,必须能连接网络,首先配置网络IP 进入yum源文件中启动容器 使用yum源头安装rpm包不需要进入package路径,同时也不需要使用包全名,会有yum自动管理 安装软件组
简介 client即是本机安装的docker,相当于git Docker_host相当于centos系统 registry则是docker仓库,相当于GitHub 镜像用于创建docker容器,一个镜像可以创建多个docker容器 容器是由镜像创建的运行实例,(镜像相当于类,容器相当于类创建的对象)