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

深入了解linux系统—— System V之消息队列和信号量

System V IPC主要有三种通信方式:共享内存、消息队列、信号量;

现在来简单了解一下消息队列和信号量;以及内核当中是如何管理System VIPC资源的。

消息队列

消息队列的原理

要想实现进程间通信,就要让进程先看到同一份资源,管道是让进程看到同一个文件、共享内存是让进程看到同一份内存;

而消息队列就是在内核中维护一个队列,让两个进程看到同一个队列,让在这一个队列中读取和写入数据。

在这里插入图片描述

消息队列的接口

1. msgget创建消息队列

在这里插入图片描述

int msgget(key_t key, int msgflg);

参数:

key_t key:和共享内存的key一样,内核当做用来区别消息队列的标识符;由ftok生成;

int msgflg:和shmget第二个参数一样,标志位:IPC_CREATIPC_EXCL以及带上权限。

返回值:

如果创建成功,就返回消息队列的唯一标识符;

认个错创建失败就返回-1,并且错误码被设置。

2. msgctl控制消息队列

首先,释放消息队列可以使用指令ipcrm -q.

消息队列和共享内存一样,声明周期是随内核的;只要我们不释放,它就一直存在(系统关机/重启就没了)。

删除消息队列使用的接口是msgctl

在这里插入图片描述

msgctl是对消息队列进行管理的接口,可以使用它删除消息队列,也可以使用它来获取内核数据结构对象等等。

参数:

  • int msgid:消息队列的标识符idmsgget创建消息队列的返回值。
  • op:标志位,传递要对消息队列进行什么操作;删除IPC_RMID、获取内核数据结构对象IPC_STAT
  • struct msgid_ds *buf:输出型参数,如果要获取内核数据结构对象,就传递对应类型的指针;这里删除消息队列不关系就传nullptr

返回值:

这里只关心释放消息队列:

  • 如果释放成功就返回0
  • 释放失败就返回-1,并且错误码被设置。

3. msgsnd发送信息

共享内存在使用时需要映射到进程的地址空间中,空间拿到起始虚拟地址,这样就可以拿着起始虚拟地址去访问共享内存了;

而消息队列不同,在进程中只能拿到消息队列的标识符,那要想发送和接受信息,就只能通过系统调用;

在这里插入图片描述

发送信息用到的系统调用就是msgsnd

参数:

  • int msqidid:消息队列的标识符,msgget的返回值。
  • size_t msgsz:表示要写入内容的大小。
  • int msgflg:标志位,这里传0即可

这里msgsnd第二个参数,void msgp[.msgsz],本质上就是一个指针;

但是在发送信息时,需要我们自己构建一个类型,这个自定义类型形式如下:

在这里插入图片描述

这个自定义类型存在两个成员:

  • long mtype:表示发送信息的类型,要求必须大于0;为什么要存在这个呢?

    简单来说就是进程A和进程B要进行通信,要发送信息,那在读取信息时要知道这个信息是谁发送的;这个成员就来标识是谁发送的信息。

  • char mtext[1]:这个成员是一个柔性数组,这里存储的就是我们要发送的数据。

这里注意:一般情况下msgsnd中的msgsz参数指的是我们要发送数据的大小,不算mtype标志位。

4. msgrcv接受信息

能够发送信息,那也就可以接受信息;接受信息用到的系统调用是msgrcv

在这里插入图片描述

先来看msgrcv的参数:

  • msqid:消息队列的标识符;
  • msgp[.msgsz]:这是一个输出性参数参数,读取到的内容就存储在这个输出型参数指针指向的内容当中。
  • msgsz:标识要读取内容的大小;(一般情况下,并不算mtype标志位)
  • msgtyp这个参数指的是,要读取谁发送的信息(在发送信息时要带上mtype标志位就是记录是谁发送的信息)
  • msgflg:标志位,这里传0即可。

并发编程

System V IPC进程间通信有三种通信方式:共享内存、消息队列和信号量;

而信号量主要是用于同步和互斥的,这里简单提及一下并发编程的相关概念。

共享资源:多个进程能够看到的同一份资源。

要进行进程间通信,首先就要让不同的进程看到同一份资源。

临界资源:被保护起来的共享资源。

临界区:在进程中访问临界资源的程序段

看到同一份资源,这是进程间通信的前提;但是没有数据保护机制就会导致数据不一致。(共享内存,数据写入和读取就没有保护机制);

那如何进行数据保护呢?如何进行保护呢?

在进程中访问临界资源的程序端,也就是临界区,只要将临界区保护起来不就做到了保护共享资源的数据吗。

保护方法:加锁;保证只有一个进程同时访问临界资源、让进程访问临界资源有一定的顺序性。

互斥:任意时刻,只允许一个进程访问临界资源。

同步:多个执行流在访问临界资源时,具有一定的顺序性。

保护临界区,做法就是加锁;简单来说就是一个进程要访问临界资源,先申请锁,如果申请成功,其他在要访问临界资源要申请锁就申请失败,无法访问临界资源;进程在访问完临界资源之后,释放锁;这样其他进程就可以申请锁然后访问临界资源。

锁具有原子性

锁的原子性是指锁操作(获取和释放)作为一个不可分割的单元执行的特性。这是锁机制能够正确实现线程同步的基础。

  • 不可分割性:锁的获取和释放操作在执行过程中不会被中断,要么完全执行成功,要么完全不执行。
  • 互斥保证:由于原子性,同一时间只有一个线程能够成功获取锁,从而保证临界区代码的互斥访问。
  • 硬件支持:现代处理器通常提供特殊的原子指令(如CAS, Test-and-Set等)来实现锁的原子操作。
  • 内存可见性:锁的原子操作通常还包含内存屏障,确保锁状态的变化对所有线程可见。

信号量

信号量的本质

在共享内存中,没有保护机制就导致了数据的不一致,这个问题的一种解决方案就是:信号量。

那是如何解决的呢?

简单来说就是一个进程在访问临界资源之前要先申请锁(这里就是申请

信号量本质上就是一个计数器。

进程在访问临界资源之前,申请锁(就是对信号量--)当信号量减到0时进程就会阻塞,等待在访问临界资源的进程退出,释放锁(就是对信号量++)。

这里就像显示生活中买票一样(电影票、火车票、飞机票…),一个人买票成功,这个作座位就是他的,其他人就无法买到这个座位的票。

所以,进程访问临界资源要先申请信号量,本质上就是对临界资源的一种预定机制。

这里申请信号量,信号量++就是P操作、释放信号量,信号量++就是V操作。

在这里插入图片描述

共享内存是不同的进程看到同一份内存资源(在linux内核中这份内存资源是基于文件的文件缓冲区)、消息队列是不同的进程看到同一份内存中的同一个消息队列;那信号量呢?

不同的进程要对申请/释放 同一个信号量,首先就要先看到同一个信号量。

虽然信号量不是用来输出数据的,但它也属于通信范畴。

信号量的接口

1. semget创建信号量

在这里插入图片描述

这里第一个参数和shmgetmsgget一样,都是key值;在内核中用来区别信号量的;(使用ftok函数生成

第二个参数:表示要创建信号量的个数。

第三个参数:标志位、包含权限;(和shmgetmsgget一样)IPC_CREATIPC_EXCL和权限。

返回值:

如果创建成功就返回信号量的标识符;创建失败就返回-1

2. semctl控制信号量

在这里插入图片描述

semctlshmctlmsgctl一样,用来对信号量控制(包括释放、获取内核数据结构,设置信号量)

参数:

  • int semid:信号量标识符,semget的返回值。
  • int semnum:表示信号量在集合中的索引值。
  • int op:标志位,表示要对信号量进行什么操作;常见的标志有:IPC_RMID释放信号量、IPC_STAT获取内核数据结构、SET_VAL设置一个信号量的值、SET_ALL设置所有信号量的值、GET_VAL/GET_ALL获取一个/所有信号量的值。

对于...可变参数,取决于op标志位,一般是一个union semnu需要我们自己定义:

union semun {int val;                // SETVAL用的值struct semid_ds *buf;   // IPC_STAT, IPC_SET用的缓冲区unsigned short *array;  // GETALL, SETALL用的数组
};

返回值:

对于不同的标志位op,存在不同的返回值:

  • 成功:返回值取决于op
    • GETVAL:信号量的当前值
    • GETPID:最后操作信号量的进程PID
    • GETNCNT:等待信号量值增加的进程数
    • GETZCNT:等待信号量值变为0的进程数
    • 其他:0
  • 失败:返回 -1 并设置 errno

3. semop信号量操作

semget创建信号量,semctl控制信号量;那如何对信号量进程申请P和释放V呢?

在这里插入图片描述

参数:

  • int semid:信号量标识符
  • size_t nsops:要操作信号量的数量

对于第二个参数是struct sembuf类型的指针,它的组成:

struct sembuf {unsigned short sem_num;  // 信号量在集合中的索引short sem_op;           // 操作值(正数-V操作,负数-P操作)short sem_flg;          // 标志(如 IPC_NOWAIT, SEM_UNDO)
};

在我们进行操作时,就需要先构建一个struct sembuf类型的对象,再设置其中成员变量的值,来对信号量进行不同的操作。

返回值:

操作成功进返回0,失败则返回-1并且错误码被设置。

内核对于System V IPC资源的管理

了解了System VIPC进程间通信:共享内存、消息队列和信号量;从系统调用接口上就可以看出它们非常相似。

都是使用key值来创建唯一的资源,ctl控制资源(删除、获取内核数据结构)。

那在内核中,是如何管理System VIPC资源的呢?

还用,在内存中,存在非常多的进制需要进行通信,那就势必存在非常多的共享内存、消息队列或者信号量等资源;这些资源是新建的、有正在使用的、有即将释放的;

操作系统是不是也要将这些资源管理起来呢?如何管理?先描述后组织

在内核中一定存在描述共享内、消息队列和信号量对应的结构体;那操作系统是如何将这些IPC资源(结构体)组织起来的呢?

先来看一下共享内存、消息队列和信号量对应的结构体:

在这里插入图片描述

可以看到无论是shmid_dsmsqid_ds还是semid_ds,这些结构体的第一个变量都是struct ipc_perm类型的对象,那它是什么呢?

在这里插入图片描述

struct ipc_perm {key_t          __key;    /* IPC 对象的键值 */uid_t          uid;      /* 所有者的有效用户ID */gid_t          gid;      /* 所有者的有效组ID */uid_t          cuid;     /* 创建者的有效用户ID */gid_t          cgid;     /* 创建者的有效组ID */unsigned short mode;     /* 权限模式(读写权限) */unsigned short __seq;    /* 序列号(内部使用) */
};

可以看到在struct ipc_perm中存储着_key值,uidmode权限等等。

了解了内核中共享内存、消息队列和信号量对应的结构体,现在来看内核是如何将其组织起来的

首先在内核在存在一个ipc_ids的结构体,其中包含成员变量:当前IPC对象的数量、序列号、最大序列号和ipc_id_ary的数组(现在的新内核中已经使用IDR树代替该数组了)。

对应的ipc_id_ary也是一个结构体,其中包含数组总容量size和一个柔性数组p,这个柔性数组指针的类型是struct ipc_perm*

而共享内存struct shmid_ds、消息队列msqid_ds、信号量semid_ds结构体的第一个成员都是struct ipc_perm

这样,在ipc_id_ary中的柔性数组p指向这些结构体的第一个成员struct ipc_perm

虽然共享内存、消息队列和信号量的结构体各不相同,但是它们的第一个成员变量都是struct ipc_perm;这样p就指向不同结构体的第一个相同的成员变量。

在这里插入图片描述

我们知道结构体的第一个成员的地址和结构体整体的地址是一样的,所有在使用时找到对应的struct ipc_perm的地址之后,进行强制类型转换,就可以访问结构体中的其他成员了。

到这里懂了,在内核中共享内存、消息队列和信号量是统一进行管理的,它们都使用key值创建然后获得唯一的标识符;

所以,这里共享内存、消息队列和信号量使用的是一个数组的下标;

这里对应的标识符还是逐渐递增的,而在ipc_ids中还存储着最大序列号max_id,标识符在超过最大序列号之后就会轮回到起始位置。

到这里还存在一个疑问:在通过标识符(下标)找到对应的struct ipc_perm的地址之后,通过强制类型转换就可以访问其他成员;那操作系统是如何知道要转换成什么类型的呢?

这里,我们可以发现共享内存、消息队列和信号量虽然系统调用有相似性,但是还是不同的系统调用接口;

所以,在我们调用对应的系统调用时,我们调用哪一个系统调用,操作系统就对应的转换成什么类型。

到这里本篇文章的内容就结束了,感谢支持

http://www.dtcms.com/a/269407.html

相关文章:

  • Flask 解决 JSON 返回中文乱码问题方案
  • Bright Data MCP+Trae :快速构建电商导购助手垂直智能体
  • MySQL Galera Cluster部署
  • 算法化资本——智能投顾技术重构金融生态的深度解析
  • 【UE5】虚幻引擎的运行逻辑
  • 【操作系统】进程(二)内存管理、通信
  • 【喜报】第三届BDDM 会议成功申请 IEEE 冠名,并获得 IEEE 北京分会赞助!
  • 佰力博科技与您探讨电晕极化和油浴极化有什么区别?
  • maven 发布到中央仓库之持续集成-03
  • 当Powerbi遇到quickbi,性能优化方式对比
  • Unity实用技能-背景自适应文本
  • Docker部署QAnything2.0并接入大模型
  • 基于极大似然估计的Gm-APD信号提取算法2025.7.8
  • 技术演进中的开发沉思-28 MFC系列:关于C++
  • 界面控件Telerik UI for WinForms 2025 Q2亮点 - 支持.NET 10 Preview
  • AIGC与影视制作:技术革命、产业重构与未来图景
  • XCKU060‑2FFVA1156I Xilinx FPGA AMD Kintex UltraScale
  • 文献学习|全面绘制和建模水稻调控组景观揭示了复杂性状背后的调控架构。
  • django-ckeditor配置html5video实现视频上传与播放
  • 基于Hadoop的用户购物行为可视化分析系统设计与实现
  • stm32 H7 ADC DMA采集
  • 240.搜索二维矩阵Ⅱ
  • c++-引用(包括完美转发,移动构造,万能引用)
  • 华为OD机试 2025B卷 - 数组组成的最小数字(C++PythonJAVAJSC语言)
  • 【Python进阶篇 面向对象程序设计(3) 继承】
  • 使用 GDB 调试 Redis 服务进程指南
  • pyhton基础【25】面向对象进阶六
  • 【ARM AMBA AXI 入门 21.1 -- AXI partial 访问和软件的按字节访问关系】
  • Transformer模型架构深度讲解
  • 医疗AI底层能力全链条工程方案:从技术突破到临床落地