linux多线(进)程编程——(9)信号量(一)

news2025/4/23 23:24:48

前言

在找到了共享内存存在的问题后,进程君父子着手开始解决这些问题。他们发明了一个新的神通——信号量。

信号量

信号量是一个计数器,用于管理对共享资源的访问权限。主要特点包括:
(1)是一个非负整数
(2)提供两种原子操作:P(等待)和V(释放)
(3)可以用于进程同步和互斥
(4)用于显示资源的数量
如何理解这些功能呢,他和普通的变量实现的计数器的区别是什么呢?请看下面的例子:

普通变量实现进程间共享资源计数的问题

假设有一个停车场,内部有若干停车位。当前剩余的停车位数量会显示在大门前,当进入一辆车时,大门打开,确保车辆进入后,大门合上,剩余停车位减1。随着规模的扩建,现在停车场增加了一个大门,共有东西两个大门。假设当前停车场有一个空位,如下图所示:
在这里插入图片描述
如图,剩余车位为1,有两辆车要进入我们的停车场。由于停车场的传感器机制要等到汽车确定进入停车场并关闭大门后,才会重新计算剩余停车场数量,对于图中情况,两个车都靠近停车场,将会出现下面的情况:
在这里插入图片描述
上图可以看到两辆车几乎同时进入停车场,且剩余车位仍然显示为1,因为大门没有及时合上。等到两辆车都进入停车场后,稍微晚一点进入的车辆发现了现在车场里面已经没有空位了,如下图所示。
在这里插入图片描述
这里停车场出现问题的主要原因在哪里呢?就是他加装了一个大门。我们现在来把停车场给抽象出来,如下表:

例子中的元素计算机
单个大门的停车场普通虚拟内存
单个大门单个进程
多个大门的停车场共享内存
多个大门多个进程
剩余车位显示屏普通变量实现的计数器

为了防止同时写入造成冲突,我们约束同一时刻只能有一个进程访问共享内存,一个进程进入后,表示共享资源个数的计数器会从1变成0。上节课我们讲过,对于普通变量实现的计数器的增减操作是非原子操作,会被拆分成3个机器指令。当两个进程同时想要访问共享内存时,如果时间刚刚好相差不大,就会引起两个进程同时获得计数器值为1的情况。就类似于上面的例子中两辆车几乎同时进入停车场。

信号量的使用

我们再来看一下信号量的优点:
信号量是一个计数器,用于管理对共享资源的访问权限。主要特点包括:
(1)是一个非负整数
(2)提供两种原子操作:P(等待:值减1)和V(释放:值加1)
(3)可以用于进程同步和互斥
(4)用于显示资源的数量
看到这里大家应该清楚了,信号量就是用来在进程间计数与同步的,那么让我来看看如何使用它们吧:

创建或获取信号量集合:

int semget(key_t key, int nsems, int semflg);

这些参数真的很多都是老朋友了,包括这些函数的命名方法,当然还是要介绍一下:
key:键值
nsems:表示在这个集合中创建多少个信号量
semflag:状态位
ret:返回值为信号量集合的ID

使用方法如下面代码块所示:

int main() {
    int semid = semget((key_t)1, 1, 0666 | IPC_CREAT);
    if (semid == -1) {
        printf("sem create fail\n");
        return 0;
    }
    printf("the id of sem is %d\n", id);
    return 0;
}

编译后运行程序,输出

lol@hyl:~/work/linux_study/sem$ gcc -o p1 proc1.c 
lol@hyl:~/work/linux_study/sem$ ./p1
the id of sem is 1

运行ipcs指令可以看到:

lol@hyl:~/work/linux_study/sem$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     
0x00000001 1          hyl        666        1      

到这里,我们ipcs指令下的三大元老:共享内存,消息队列,信号量就全部和大家见面了。同样,这个信号量是不依赖于进程的,属于操作系统的,因此我们发现主函数执行完成后我们的信号量仍然存在。为了防止发生资源泄露,我们需要删除这个信号量,输入:ipcrm -s 1,这里-s表示信号量的意思。从输出信息中我们可以发现信号量被删除了。

lol@hyl:~/work/linux_study/sem$ ipcrm -s 1
lol@hyl:~/work/linux_study/sem$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems    

信号量操作:

int semop(int semid, struct sembuf *sops, unsigned int nsops);

semid:信号量集合的ID
nsops:需要操作的信号量的个数,当集合内信号量数量大于1有效,一般另其为1
sopsstruct sembuf的地址,决定了信号量的行为
这里struct sembuf的定义为:

struct sembuf {
    unsigned short sem_num;  	// 信号量在集合中的索引
    short sem_op;   			// 操作值
    short sem_flg;  			// 操作标志
};

sem_num:要操作的信号量的索引,当集合内有多个信号量时才有用
sem_op
 大于0表示释放资源,信号量值增加sem_op
 小于0表示获取资源,信号量值减去sem_op,若操作后为值为负则不进行操作并阻塞线程
 等于0表示阻塞等待信号量值为0
sem_flg
=0 表示阻塞等待,默认操作
=IPC_NOWAIT 时表示不阻塞,直接返回错误,对应上面sem_op可能阻塞的情况
=SEM_UNDO 这个不做讨论

信号量控制:

int semctl(int semid, int semnum, int cmd, ...);

semid:信号量集ID
semnum:信号量在集合中的索引
cmd:控制命令
...:c语言中的变参数,当cmd为某些命令时需要追加参数,此时需要定义一个联合体
这个函数的用法使用代码来展示:

union semun {
    int val;                // SETVAL用的值
    struct semid_ds *buf;   // IPC_STAT, IPC_SET用的缓冲区
    unsigned short *array;  // GETALL, SETALL用的数组
};	// 用于追加参数的一个结构体,必须由用户定义

// 初始化信号量集合中的第一个信号量值为1
union semun arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
    perror("semctl SETVAL failed");
}

// 获取信号量值
int val = semctl(semid, 0, GETVAL);
printf("Current semaphore value: %d\n", val);

// 删除信号量集
if (semctl(semid, 0, IPC_RMID) == -1) {
    perror("semctl IPC_RMID failed");
}

简单代码案例

这里只是为了让大家理解信号量的用法,下一节还会有更加合适的案例去帮大家了解信号量。我们使用fork()创建了两个进程,子进程每两秒释放一个信号量,父进程每0.5秒获取一个信号量。信号量被初始化为5。
这里补充一点,对于fork()函数,我们之前讲过它会将当前进程内部的资源克隆一份。(不了解的同学请移步:linux多线(进)程编程——(2)身外化身fork())但是由于信号量是属于操作系统的,因此它不会fork不会克隆信号量,但是它创建出的子进程会继承信号量的操作权限。
完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int main() {
    int semid = semget((key_t)1, 1, 0666 | IPC_CREAT);
    if (semid == -1) {
        printf("sem create fail\n");
        return 0;
    }

    struct sembuf buf = {0};
    union sempun {
        int val;                // 用于SETVAL
        struct semid_ds *buf;   // 用于IPC_STAT/IPC_SET
        unsigned short *array;  // 用于GETALL/SETALL
    } arg;	// 这个联合体用于追加参数,由用户自己定义与创建
    arg.val = 5;
    if(-1 == semctl(semid, 0, SETVAL, arg)) {	// 将信号量值初始化为5
        printf("sem init fail!\n"); 
        semctl(semid, 0, IPC_RMID);
        return 0;
    }
    printf("sem init val is %d!\n", arg.val);

    pid_t id = fork();
    if(id < 0) {
        printf("fork ret -1, error!\n");
        semctl(semid, 0, IPC_RMID);
        return 0;
    }
    if(id == 0) {   // 子进程,每隔2秒释放一个信号量
        buf.sem_num = 0;
        buf.sem_op = 1;
        buf.sem_flg = 0;
        while(1) {
            semop(semid, &buf, 1);
            int val = semctl(semid, 0, GETVAL);
            printf("son: I release a semaphore, and the val is %d\n", val);
            sleep(2);   // 阻塞2秒
        }
    }
    else {      // 父进程,每隔0.5秒获取一个信号量
        buf.sem_num = 0;
        buf.sem_op = -1;	// 值小于1,获取信号量
        buf.sem_flg = 0;    // 设置为阻塞模式
        while(1) {
            semop(semid, &buf, 1);
            int val = semctl(semid, 0, GETVAL);
            printf("father: I get a semaphore, and the val is %d\n", val);
            usleep(500*1000);   // 阻塞0.5s
        }
    }
    semctl(semid, 0, IPC_RMID);
    return 0;
}

输出结果与分析
上面程序的执行结果如下所示。可见由于信号量初始值为5,父进程在程序开始时快速获取资源。等到信号量资源耗尽(值为0)时,父进程被阻塞,等待子进程释放资源。有意思的是,当子进程释放资源后,显示信号量的值是0而不是1。原因是由于父进程在阻塞等待,因此当子进程释放资源后,还没进入下一条指令,马上会被父进程拿走。最后子进程看到的信号量的值为0。

lol@hyl:~/work/linux_study/sem$ ./p1
sem init val is 5!
father: I get a semaphore, and the val is 4
son: I release a semaphore, and the val is 5
father: I get a semaphore, and the val is 4
father: I get a semaphore, and the val is 3
father: I get a semaphore, and the val is 2
son: I release a semaphore, and the val is 3
father: I get a semaphore, and the val is 2
father: I get a semaphore, and the val is 1
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0

强调一点,这里对信号量的增加删除操作全部都是由操作系统内核与底层硬件支持的原子操作,不会出现普通变量那种被打断的情况,因此信号量是进程间安全的。下面这张图就表示了使用信号量的车库:
在这里插入图片描述

小结

这节课我们讲解了用于实现共享内存进程间同步的信号量。
我们要知道为什么不能使用普通变量来实现进程间计数,以及为什么可以使用信号量实现进程间计数。(原子性)
大家还要熟悉一下相关的信号量的函数。
关于信号量的具体使用,为了方便大家理解,我将在下一节将其与共享内存结合,并且提供几个案例,让大家更好的理解信号量的用法。
传送阵:linux多线(进)程编程——(9)信号量(二)

结束语

在信号量的帮助,共享内存又一次成为了修真界人们经常使用的传音术。

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

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

相关文章

PFLM: Privacy-preserving federated learning with membership proof证明阅读

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目…

图片转base64 - 加菲工具 - 在线转换

图片转base64 - 加菲工具 先进入“加菲工具” 网 打开 https://www.orcc.top&#xff0c; 选择 “图片转base64”功能 选择需要转换的图片 复制 点击“复制”按钮&#xff0c;即可复制转换好的base64编码数据&#xff0c;可以直接用于img标签。

opencv 对图片的操作

对图片的操作 1.图片镜像旋转&#xff08;cv2.flip()&#xff09;2 图像的矫正 1.图片镜像旋转&#xff08;cv2.flip()&#xff09; 图像的旋转是围绕一个特定点进行的&#xff0c;而图像的镜像旋转则是围绕坐标轴进行的。图像的镜像旋转分为水平翻转、垂直翻转、水平垂直翻转…

LabVIEW数据采集与传感系统

开发了一个基于LabVIEW的智能数据采集系统&#xff0c;该系统主要通过单片机与LabVIEW软件协同工作&#xff0c;实现对多通道低频传感器信号的有效采集、处理与显示。系统的设计旨在提高数据采集的准确性和效率&#xff0c;适用于各种需要高精度和低成本解决方案的工业场合。 项…

【Easylive】​​Gateway模块 bootstrap.yml 解析

【Easylive】项目常见问题解答&#xff08;自用&持续更新中…&#xff09; 汇总版 Gateway模块 bootstrap.yml 常规解析 该配置文件定义了 Spring Cloud Gateway 的核心配置&#xff0c;包括 环境配置、服务注册、动态路由规则 等。以下是逐项解析&#xff1a; 1. 基础配…

matlab 环形单层柱状图

matlab 环形单层柱状图 matlab 环形单层柱状图 matlab 环形单层柱状图 图片 图片 【图片来源粉丝】 我给他的思路是&#xff1a;直接使用风玫瑰图可以画出。 rose_bar 本次我的更新和这个有些不同&#xff01;是环形柱状图&#xff0c;可调节细节多&#xff1b; 只需要函数…

文献×汽车 | 基于 ANSYS 的多级抛物线板簧系统分析

板簧系统是用于减弱或吸收动态系统中发生的应力、应变、偏转和变形等破坏性因素的机械结构。板簧系统可能对外力产生不同的响应&#xff0c;具体取决于其几何结构和材料特性。板簧系统的计算机辅助分析对于高精度确定系统的变形特性和结构特性至关重要。 在这项工作中&#xff…

RHCE 练习二:通过 ssh 实现两台主机免密登录以及 nginx 服务通过多 IP 区分多网站

一、题目要求 1.配置ssh实现A&#xff0c;B主机互相免密登录 2.配置nginx服务&#xff0c;通过多ip区分多网站 二、实验 实验开始前需准备两台 linux 主机便于充当服务端以及客户端&#xff0c;两台主机 IP 如下图&#xff1a; 实验1&#xff1a;配置 ssh 实现 A&#xff0…

瑞吉外卖-分页功能开发中的两个问题

1.分页功能-前端页面展示显示500 原因&#xff1a;项目启动失败 解决&#xff1a;发现是Category实体类中&#xff0c;多定义了一个删除字段&#xff0c;但是我数据库里面没有is_deleted字段&#xff0c;导致查询数据库失败&#xff0c;所以会导致500错误。因为类是从网上其他帖…

工业物联网安全网关 —— 安全OTA升级签名验证

这里写目录标题 工业物联网安全网关 —— 安全OTA升级签名验证一、项目背景与简介1.1 背景介绍1.2 OTA升级的安全挑战1.3 项目目标二、理论基础与关键技术2.1 数字签名基础2.2 OTA升级签名验证原理2.3 关键技术与安全算法三、系统架构设计3.1 系统模块划分3.2 系统架构图(Merm…

探索 Flowable 后端表达式:简化流程自动化

什么是后端表达式&#xff1f; 在 Flowable 中&#xff0c;后端表达式是一种强大的工具&#xff0c;用于在流程、案例或决策表执行期间动态获取或设置变量。它还能实现自定义逻辑&#xff0c;或将复杂逻辑委托…… 后端表达式在 Flowable 的后端运行&#xff0c;无法访问前端…

HDFS入门】HDFS安全与权限管理解析:从认证到加密的完整指南

目录 引言 1 认证与授权机制 1.1 Kerberos认证集成 1.2 HDFS ACL细粒度控制 2 数据加密保护 2.1 传输层加密(SSL/TLS) 2.2 静态数据加密 3 审计与监控体系 3.1 操作审计流程 3.2 安全监控指标 4 权限模型详解 4.1 用户/组权限模型 4.2 umask配置原理 5 安全最佳实…

性能比拼: Go vs Java

本内容是对知名性能评测博主 Anton Putra Go (Golang) vs Java: Performance Benchmark 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 在本视频中&#xff0c;我们将比较 Go 和 Java。 我们将基于 Golang 的 Fiber 框架和 Java 的 Spring Boot 创建几个简单的应用…

ElMessageBox消息弹框(vue3总结)

一 展示各种内容 const checkCheckbox (check: any, formEl: any) > {ElMessageBox({title: "服务协议及隐私权政策",message: h("p", null, [h("span", null, "我已阅读并同意 "),h("span",{style: "color: #477F…

Jupyter Notebook 中切换/使用 conda 虚拟环境的方式(解决jupyter notebook 环境默认在base下面的问题)

使用 nb_conda_kernels 添加所有环境 一键添加所有 conda 环境 conda activate my-conda-env # this is the environment for your project and code conda install ipykernel conda deactivateconda activate base # could be also some other environment conda in…

Tailwind CSS 开发入门:掌握基础语法要点

在前端开发中&#xff0c;Tailwind CSS 以原子化设计和实用类系统&#xff0c;构建精美页面的得力工具&#xff0c;摒弃传统 CSS 繁琐写法。掌握其基础语法是熟练运用它的关键&#xff0c;下面将详细介绍核心基础语法。 一、核心基础语法 1. 颜色类 Tailwind CSS 提供了丰富…

Java八股 深入理解Spring的AOP 面向切面编程 底层 保姆级教程 手写例子

目录 概念 AOP 术语 1. 连接点&#xff08;Jointpoint&#xff09;&#xff1a; 2. 切入点&#xff08;Pointcut&#xff09;&#xff1a; 3. 通知&#xff08;Advice&#xff09;&#xff1a; 4. 方面/切面&#xff08;Aspect&#xff09;&#xff1a; 5. 引入&#xff…

C++std::map

1. 概述​​ ​​定义​​&#xff1a;std::map 是C标准模板库&#xff08;STL&#xff09;中的关联容器&#xff0c;以键值对&#xff08;key-value pairs&#xff09;形式存储元素&#xff0c;支持快速查找和有序访问。 ​​- 头文件​​&#xff1a;#include ​​底层实现​…

dispaly: inline-flex 和 display: flex 的区别

display: inline-flex 和 display: flex 都是 CSS 中用于创建弹性盒子布局&#xff08;Flexbox&#xff09;的属性值&#xff0c;但它们之间有一些关键的区别&#xff0c;主要体现在元素如何在页面上被渲染和它们对周围元素的影响。 主要区别 1&#xff0c;块级 vs 行内块级 d…

性能比拼: Elixir vs Go(第二轮)

本内容是对知名性能评测博主 Anton Putra Elixir vs Go (Golang) Performance Benchmark (Round 2) 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 这是第二轮关于 Elixir 和 Go 的对比测试。我收到了一份来自 Elixir 创作者的 Pull Request &#xff0c;并且我认为…