【Linux】消息队列和信号量

1、System-V 消息队列

1.1 消息队列的概念

有内核维护的一个队列容器,属于 System‑V IPC

消息队列是一种由操作系统内核维护的、支持双向通信的队列结构,进程A、进程B不直接互相拷贝数据,A把带类型的数据块(消息节点)丢进内核队列,B从队列读取消息,双向都可以收发

消息队列每个数据块(消息)必须包含一个类型字段(type),用于区分消息的发送者或类别,接收方可以根据类型有选择的接收消息

和管道的区别:

  1. 管道是字节流,没有边界,消息队列是数据块,一条一条独立消息
  2. 管道单向,消息队列队列全双工
  3. 消息节点自带消息类型

1.2 消息队列接口

1.2.1msgget:创建获取资源

  • key:ftok生成,用来让不同进程找到同一个消息队列
  • flag:IPC_CREAT:不存在则创建;存在直接获取,IPC_CREAT | IPC_EXCL:不存在创建;存在返回出错,保证新建
  • 返回值:msgid,消息队列用户标识符

1.2.2 msgctl:控制队列(删除/获取属性)

  • msqid:消息队列ID,是msgget()调用成功后返回的整形标识符
  • op:控制命令,告诉内核要做什么操作
  • buf:struct msqid_ds * 结构体指针,op命令是IPC_STAT表示作为输出参数,内核把队列信息填充进这个结构体,IPC_SET表示作为输入参数,程序把要修改的属性填进去传给内核,IPC_RMID表示不需要结构体,直接传NULL
  • 返回值:成功返回0,失败返回-1

描述消息队列的结构体:

1.2.3 msgsnd:发送数据,msgrcv接收数据

用户自定义消息结构体模板:

cpp 复制代码
struct msgbuf{
    long mtype;   //消息类型,必须>0
    char mtext[1]; //消息数据,大小自定义
};
  • msqid:表示消息队列的ID,msgget函数的返回值
  • msgp:发送的数据块,是一个struct msgbuf结构体类型,用户可以自己定义
  • msgsz:数据块的大小
  • msgtyp:数据块类型
  • msgflg:一般设置为0,消息队列满了就阻塞等待

2、信号量

2.1 并发

2.1.1 共享资源

多个执行流可以同时看见并且访问的公共资源,比如共享内存、显示器stdout、管道缓冲区等,但多个执行流并发访问共享资源会产生数据不一致,需要引入保护机制

2.1.2 互斥

任意时刻,只允许一个执行流访问共享资源,一个执行流访问期间,别的执行流不能进来打扰

2.1.3 临界资源和临界区

临界资源:被互斥机制保护起来的共享资源被称为临界资源

程序代码中,访问临界资源的那一部分代码被称为临界区

2.1.4 原子性

原子性指一个操作,要么完全做完,要么完全不做,不存在中间状态,中途不能打断,在计算机中,原子操作不会被CPU时间片打断,信号量内部的加减操作,必须是原子操作,是实现互斥的底层保障

2.2 信号量

信号量的本质是一个内核维护的计数器,用来统计可用临界资源的数量

举例:一个放映厅一共100个座位

  1. 放映厅相当于共享/临界资源
  2. 如果放入101人,必然两个人抢同一个座位,对应进程中:多个进程同时读写同一个共享资源,发生数据冲突,互相覆盖,程序出错
  3. 买票相当于申请信号量,买票的本质是对共享资源的预定机制,一旦购票成功即使未实际入座,该座位他人也无法使用,票卖完再来的人只能阻塞等待

类比信号量:

  • 信号量初始值为100
  • 申请资源为P操作,计数器--
  • 释放资源V操作,计数器++
  • 等待:P操作阻塞,进程挂起等待

所以信号量本质是一个内核维护的计数器,用来记录可用临界资源的数量,核心作用:资源预定机制

信号量本身就是共享资源,信号量的申请是原子操作,信号量的释放也是原子操作

2.3 二元信号量

临界资源只有一份,计数器的值只有1和0,如果申请资源计数器从1变为0,释放资源操作计数器从0变成1。这种只有0和1两种状态的计数器叫做二元信号量

二元信号量的作用是实现互斥,同一时刻只允许一个进程进入临界区,保护整块共享资源,整体申请,整体释放

2.4 信号量的问题

信号量伪代码:

cpp 复制代码
sem = 100; //初始100个资源
P() {
    if(sem > 0) {
        sem--; //买票成功
        继续执行,访问临界资源;
    } else {
        进程阻塞等待; //票卖完,等别人释放
    }
}

V() {
    sem++; //看完电影,归还票
    唤醒等待的进程;
}

为什么不能用普通int变量充当信号量:

  1. P操作和V操作不是原子操作,进程调度会打破逻辑循环

如果有两个进程A和B,sem=1,进程A判断sem大于0,判断后操作系统时间片到,进程A被切走,进程B上CPU,同样判断sem大于0,B进入临界区,切回进程A后,A执行后面的代码,sem变成-1,这会使A、B两个进程同时进入临界资源,互斥失效

  1. sem--C语言的代码底层汇编有三步

把内存中 sem 的值读到 CPU 寄存器;CPU 寄存器内部做自减;把计算结果写回内存。这三步中间的任意时刻进程都有可能被切换

  1. 多进程之间,普通全局变量无法共享

每个进程有独立虚拟地址空间。A 进程定义的全局 int 变量,B 进程看不到;就算父子进程,fork 之后发生写时拷贝,修改互不影响。

2.5 信号量的接口

2.5.1 semget:创建/获取信号量集

  • key:键值,用ftok()生成。多个进程使用相同 key,就能打开同一个内核信号量集,实现多进程看到同一个信号量
  • nsems:信号量集里面信号量的个数,不是信号量初始值
  • semflg:标志位

2.5.2 semctl:信号量集控制接口

  • semid:semget 返回的 id
  • semnum:要操作的信号量在集合里的下标;只用 1 个信号量就填 0
  • op:命令

常用命令:

SETVAL初始化某一个信号量的 sem_val,设置计数器初值,

需要传入联合体union semun

eg:把0 号信号量初始化为 1

cpp 复制代码
union semun un;
un.val = 1;
semctl(semid, 0, SETVAL, un);

2.5.3 semop:执行 P、V 操作、申请释放信号量

  • semid:信号量集 id
  • sops:struct sembuf结构体数组,可以一次批量操作多个信号量
  • nsops:数组元素个数;只操作 1 个信号量填 1

struct sembuf 结构体:

cpp 复制代码
struct sembuf {
    unsigned short sem_num; //信号量下标,只用一个填0
    short sem_op;           //核心,操作数
    short sem_flg;          //标志位,一般填0
};
  • sem_op:sem_op = -1 表示P操作,sem_op = 1 表示V操作

3、内核数据结构设计

3.1 三个 IPC 资源

共享内存:struct shmid_kernel

cpp 复制代码
struct shmid_kernel /* private to the kernel */
{
	struct kern_ipc_perm	shm_perm;    // IPC权限块
	struct file *		    shm_file;    // 关联shmem临时文件对象(内核私有!用户态看不见)
	int			            id;          // ipc id下标
	unsigned long		    shm_nattch;  // 当前attach挂载的进程计数
	unsigned long		    shm_segsz;   // 共享内存段大小(字节)
	time_t			        shm_atim;    // 最后shmat挂载时间
	time_t			        shm_dtim;    // 最后shmdt卸载时间
	time_t			        shm_ctim;    // 最后修改权限/属性时间
	pid_t			        shm_cprid;   // 创建该shm段的进程pid
	pid_t			        shm_lprid;   // 最后一次执行shmat/shmdt的进程pid
};
  • 消息队列:struct msg_queue
cpp 复制代码
struct msg_queue {
	struct kern_ipc_perm	q_perm;        // IPC权限块(key、uid、gid、mode)
	time_t			q_stime;        // 最后一次 msgsnd 发送时间
	time_t			q_rtime;        // 最后一次 msgrcv 接收时间
	time_t			q_ctime;        // 最后修改队列属性时间
	unsigned long		q_cbytes;       // 队列当前总字节数
	unsigned long		q_qnum;         // 当前队列消息条数
	unsigned long		q_qbytes;       // 队列允许最大总字节
	pid_t			q_lspid;        // 最后执行msgsnd的pid
	pid_t			q_lrpid;        // 最后执行msgrcv的pid

	struct list_head	q_messages;     // 消息链表头,挂struct msg消息节点
	struct list_head	q_receivers;    // 阻塞等待读消息的进程链表
	struct list_head	q_senders;      // 阻塞等待写消息的进程链表
};
  • 信号量集:struct sem_array
cpp 复制代码
struct sem_array {
	struct kern_ipc_perm	sem_perm;	/* IPC权限 */
	time_t			sem_otime;	/* 最后一次semop操作时间 */
	time_t			sem_ctime;	/* 最后修改权限时间 */
	unsigned long		sem_nsems;	/* 集合内信号量的个数 */
	struct sem		*sem_base;	/* 指向数组:每个struct sem代表一个信号量 */

	struct list_head	sem_pending;	/* 等待信号量的阻塞任务队列(等待条件满足) */
	struct list_head	sem_pending_alter; /* 等待修改信号量值的阻塞任务队列 */
};

这三个结构体都有一个共同的数据类型:struct kern_ipc_perm

cpp 复制代码
struct kern_ipc_perm {
	spinlock_t lock;
	int     delete;
	key_t   __key;
	uid_t   uid;
	gid_t   gid;
	uid_t   cuid;
	gid_t   cgid;
	mode_t  mode;
	unsigned long seq;
	void    *security;
};

struct kern_ipc_perm用来存储 key、权限、uid/gid等

C 语言规则:结构体变量地址 = 结构体第一个成员的起始地址

于是内核设计一个指针数组,数组中存储kern_ipc_perm*

struct ipc_ids:

cpp 复制代码
struct ipc_ids {
	int in_use;
	int max_id;
	unsigned short seq;
	unsigned short seq_max;
	struct semaphore sem;	
	struct ipc_id_ary nullentry;
	struct ipc_id_ary* entries;
};

不管是消息队列、共享内存、信号量对象,都把对象的地址直接赋值给数组指针

需要读取 key、权限:直接通过kern_ipc_perm*访问成员

需要访问资源特有的字段:把基类指针强制类型转换回原本完整结构体指针

这就是C 语言手写实现多态思想 :基类指针指向不同子类对象,kern_ipc_perm相当于基类shmid_kernel/msg_queue/sem_array 相当于子类,内核用一个统一数组,管理全部三类 IPC 资源

cpp 复制代码
struct ipc_id_ary {
    int size;
    struct kern_ipc_perm *p[NUM]; // p[] 就是图右侧蓝色方框数组
};

struct ipc_ids内部就持有这个ipc_id_ary ,用户层面上使用的shmid、msqid、semid等本质就是内核中数组p的下标

相关推荐
智者向内寻求力量1 小时前
【记录】ubuntu 24.04 安装在levnoval PC上之后,蓝牙设备找不到解决方法
linux·服务器·ubuntu
2301_810730101 小时前
K8s的Pod管理
linux·容器·kubernetes
tedcloud1231 小时前
diagram-design 怎么安装?用 AI 自动生成更专业的架构图、流程图
linux·运维·前端·人工智能·开源·流程图
huainingning1 小时前
Linux和Windows系統SCP命令传输文件
linux·运维·服务器
2401_862880821 小时前
Linux应用层开发 --- 多任务
linux·服务器·c语言
朝发如雪2 小时前
快速掌握Linux(2)(时间相关函数)(文件IO)
linux·python·算法
郭涤生2 小时前
VScode回归ACM模式(古法编程)配置
linux·ide·vscode
盐焗鹌鹑蛋2 小时前
【Linux】ELF与库加载
java·linux·服务器
梦诺2 小时前
LINUX定时执行脚本
linux·运维·服务器