当前位置: 首页 > article >正文

Linux 消息队列信号量

目录

一、前言

二、消息队列

1、原理

2、接口

2.1创建消息队列

2.2释放消息队列 

2.3发送数据

2.4 接收数据

四、IPC在内核中地数据结构设计

五、信号量

1、互斥

2、信号量 

3、信号量的接口

3.1申请信号量

3.2删除信号量

3.3信号量的操作


一、前言

我们讲进程通信,本质是让不同的进程看到同一份资源。

我们已经给大家介绍了管道,共享内存,接下来我们给大家简单的介绍一下消息队列。

二、消息队列

1、原理

如果我们想让进程间通信,那么必要的条件就是让不同的进程看到同一份资源,这个资源可以是文件缓冲区,内存块,队列等。

对于消息队列,就是让同一个进程看到同一个队列

允许不同的进程向内核中发送带类型的数据块

A进程 <---- 数据块的形式发送数据 ----> B进程

这个消息队列只能由操作系统来提供,而且也一定要先描述在管理。 

2、接口

2.1创建消息队列

man msgget

int msgget(key_t key, int msgflg);

key是我们上一篇博客讲的,用ftok这套算法创建同一个key

第二个参数也是IPC_CREAT和IPC_EXCL 

msgget的返回值是成功返回一个消息队列标识符,失败返回-1。与前面的共享内存是极度相似的 

我们可以用ipcs -q命令来查看消息队列

用ipcrm -q “消息队列的标识符"来去除消息队列

2.2释放消息队列 

man msgctl

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

它与释放共享内存的接口msgctl是类似的,对于第二个参数,如果要释放,还是这个IPC_RMID

共享内存的属性结构体是 struct shmid_ds

消息队列的属性结构体是 struct msqid_ds

他们内部包含了一个同样的结构体struct ipc_perm

在下面我们给大家讲解IPC在内核中数据结构的设计会具体讲解。

2.3发送数据

man msgend

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

  1. 第一个参数是消息队列的标识符
  2. 第二个参数是要发送数据块的起始地址
  3. 第三个是要发送数据块的大小
  4. 第四个一般直接设置为0

这个第二个参数是void*的原因是要自己定义一个struct结构体,如同上面所示的那个msgbuf,只要保证第一个是类型,第二个是内容即可

2.4 接收数据

man msgsnd

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

  1. 第一个参数是从哪个消息队列拿
  2. 第二个参数和第三个参数与第一个函数是同样的道理,相当于一个缓冲区
  3. 第四个参数是要读取的哪一种类型的数据。也是我们在结构体中定义的
  4. 第五个参数和前面一样,默认为0即可

四、IPC在内核中地数据结构设计

在操作系统中,所有的IPC资源,被整合在一起,放到操作系统的IPC模块中。

如下所示,是System V方式的三种IPC方式的内核数据结构

 

我们在上面也提到过,他们的内核里都有一个结构体struct ipc_perm

在操作系统里有一个struct ipc_perm *Arry的数组,数组对所有的IPC进行管理

未来当我们要去寻找一个共享内存是否存在的时候,就会直接去遍历这个数组里面的key,然后从而就可以进行直接比对key就知道了它是否已经创建了。 我们创建共享内存和消息队列返回的id即为数组下标

那我们要不要担心这个数组会满呢?

这个数组下标是不断增大的,线性递增的,当达到最高值的时候,回绕到0。

那么如何访问其他的成员呢?

注意看,这里正好是放在了第一个字段,所以这个字段的地址正好就是这个数据结构的地址。我们只需要将这个地址强转为这个数据结构的地址,然后就可以访问其他成员了!

那么现在问题来了,我们怎么知道我们要转成什么类型呢?

这其实是什么这个第一个字段的 xxx_perm结构体里面,有一个字段标志着是哪种资源,所以可以知道强转成什么类型

即OS能区分指针指向的对象的类型

而上面的这个操作,其实我们仔细一想,这不就是多态吗。struct ipc_perm是基类,其他的这些struct xxxid_ds就是子类

五、信号量

当我们A进程和B进程向同一份资源写入数据的时候,当A正在写入时,写了一部分,就被B拿走了,导致双方和收的数据不完整。 这个就会导致数据不一致的问题。

将多个进程同时看到的那一份资源叫做临界资源。我们仔细观察可以看到,在 server 和 client 中访问共享内存 / 临界资源的代码实际上只有少部分几行。造成读写不一致可能就是这部分代码导致的,我们将访问临界资源的代码叫做临界区,为了避免这种数据不一致问题,我们要对临界区进行某种保护,这种保护就被称作为互斥

互斥:只允许一个执行流访问共享资源。

1、互斥

所谓互斥就是有一块空间,在任何时候有且仅能有一个进程在进行访问(生活中最典型的互斥场景就是去上洗手间),互斥本身是一种串行化执行(也就是说,共享内存中就是因为并行读写执行才导致的数据不一致问题),而后面一般互斥是通过锁来完成的这里可以提一种二元信号量来完成串行执行(我们也能猜到加锁和解锁是有代码的) 。所以串行化的过程本质是对临界区资源加锁和解锁,从而完成互斥操作。也就是说,client 和 server 都必须遵守 “要进入临界区就得加锁,退出临界区就得解锁” 这一原则。

这里我们理解一下什么是原子性概念:

这里再感性的理解一遍原子性概念,其实说白了,就是要么做了,要么没做。比如一个进程想往共享内存里写 "Hello World",写完 "Hello" 时的这个状态叫做写入中,那么在写入过程中是不能被打搅的,得等到全部写完为止。也就是说,在其他人看来,这里写入过程的状态只有两种,其一是还没写,而其二是写完了,这就是原子性。

最典型的应用就是,假设我们在农商银行里有 1000 元,在建设银行里有 500 元,然后我们想进行转帐:农商账号 -= 200;建设账号 += 200。其中,当我们从农商账号转账到建设账号的时候,系统崩溃了,此时建设银行账号还是 500 元,但是农商银行账号少了 200 元。这个现象说白了就是当某个任务正在进行时,突然因为某些原因而导致任务中断,这就叫做不是原子性。所以这个转账的过程要不就不做,要不就必须得做成功,或者转账失败了也能保证农商账号的钱不受影响,这就是原子性。

我们也可以采用互斥的方案来保证原子性。

总结:

  • 于各个进程要求共享资源,而且有些资源需要互斥使用,那么各进程竞争使用这些资源,进程之间的这种关系就叫作进程的互斥。
  • 系统中某些资源一次只允许一个进程使用,这样的资源被称为为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫做临界区。
  • 特性:IPC 资源必须删除,否则不会自己清除,除非重启,所以 System V IPC 资源的生命周期随内核。

2、信号量 

信号量本质上是一把计数器,用来描述临界资源中资源数量的多少。

这个时候我们提出一个预定机制。

在宿舍的时候,虽然没躺着,但是那个床位依旧是属于我们的。我们在网上买电影票看电影,虽然还没到时间去看,但我们很清楚,到了一定的时间我们就能看到。所以现实生活中存在很多 “预定机制”,因为不提前享受,所以在卖票的时候就得保证一人一座,不能超过电影院的承受能力。

我们把临界资源划分为若干份

我们害怕多个执行流访问同一份资源,我们引用一个计数器,其实这个计数器就是信号量。

假设这里面被划分为12份,int cnt=12。 当申请一次计数器资源,本质上是预定临界资源内的一份资源,所以cnt--;当要释放一个进程不再访问,则cnt++;当cnt<=0,表示资源被申请完了,如果还有执行流申请,就不打算分配资源了。

总结

  • 申请了计数器成功,就表示具有访问资源的权限。
  • 申请了计数器资源,这里这是申请计数器资源,还没有访问想要的资源。这里只是对资源的预定机制。
  • 计数器可以有效保证进入共享资源的执行流的数量。
  • 每一个执行流想访问共享资源要先申请计数器资源。

如果我们前面所提到的放映厅只有一个座位!,那么我们只需要一个值为1的计数器。

此时只有一个人能抢到这份资源,只有一个人能进放映厅看电影。即看电影期间只有一个执行流在访问临界资源。

这就是互斥!!!

所以我们把值只能为1,0两态的计数器叫做二元信号量,本质就是一个锁

上面中,我们凭什么让计数器为1??这是因为资源为1了,也就是说本质就是将临界资源不要分成很多块了,而是当作一个整体,整体申请,整体释放!!

信号计数器保护的是临界资源,要想保护别人,就要保证自己的安全。

这里我们申请信号量,对计数器--为P操作。

释放资源,释放信号量,对计数器++为V操作。

这里我们把申请和释放的操作叫做PV操作。

PV操作是原子的。原子性

3、信号量的接口

3.1申请信号量

下面的系统调用的功能就是申请一个信号量集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

第一个参数是key,我们用ftok获取

第二个参数申请几个资源,如果申请一个就是1

第三个参数是设置为O_CREAT和O_EXCL,和前面一样

返回值就是信号量集标识符

这里需要注意,多个信号量和信号量是几是不一样的

3.2删除信号量

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

第一个参数是信号量的标识符

第二个是几个信号量

第三个删除操作

可变部分可以传递信号量对应的结构体

 

同时这个函数除了删除信号量,也可以设置信号量

如果只有一个信号量,那么这个编号直接设置为0,cmd设置为SET。最后可变部分传递这个联合体,最终这个信号量初始值就被设置为对应的值

3.3信号量的操作

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);

 

第一个函数中

第一个参数是对哪一个信号量进行操作

第二个参数是一个我们自定义的结构体

可以看到 System V 标准下的 ipc 共享内存机制其实蛮复杂的, 但其实共享内存又是 System V 标准下最简单的一套机制,所以当我们看到这里的时候也不难,相对更复杂的是消息队列机制,最复杂的是信号量机制。实际在公司中很少自己写这些东西,特别是消息队列和信号量,所以目前就先了解共享内存机制,知道是其底层是怎么通信的即可。


http://www.kler.cn/a/572170.html

相关文章:

  • 3d投影到2d python opencv
  • 基于 HTML、CSS 与 JavaScript 的计时器
  • 【每日学点HarmonyOS Next知识】web播放音频、接口调用不成功、底部横幅路由问题、富文本问题、onLoadIntercept修改header
  • 算法思想-贪心算法
  • 3.多线程获取音频AI的PCM数据
  • CSDN博客写作教学(五):从写作到个人IP的体系化构建(完结篇)
  • react 父组件调用子组件方法:forwardRef + useImperativeHandle
  • Qt常用控件之滑动条QSlider
  • doris:Iceberg
  • Windows 10 下 SIBR Core (i.e. 3DGS SIBR Viewers) 的编译
  • go前后端开源项目go-admin,本地启动
  • 【目录爆破与文件枚举工具对比】
  • 商淘云:跨境电商源码网站开发部署需要注意的三大关键点
  • QCP:数字科技先锋者 引领数字金融时代
  • SQL经典常用查询语句
  • 今天来介绍和讨论 AGI(通用人工智能)
  • 一种中文分词的动态规划模型
  • 纯前端实现「羊了个羊」小游戏(附源码)
  • DeepSeek掘金——DeepSeek-R1驱动的金融分析师
  • android13打基础: 控件alertdialog