进程通信————system V 消息队列 信号量
1.消息队列
1.1 原理
1.1.1 核心背景:进程通信的基础
进程通信的前提是让不同进程看到同一份资源,资源的形式决定了具体的通信方式。例如:
- 资源为文件缓冲区时,对应特定通信方式
- 资源为内存块时,对应另一种通信方式
1.1.2 消息队列的定义与特点
资源形式:由操作系统提供的消息队列
- 功能 1:使进程双方能看到同一个队列,为通信提供基础
- 功能 2:允许不同进程向内核发送带类型(用于区分发送方)的数据块
1.1.3 消息队列的工作流程
进程将经系统处理好的数据块通过操作系统列入队列,其他进程则可从队列中获取发给自己的数据块,以此实现进程间以数据块形式发送数据的通信。
1.1.4 消息队列的管理方式
操作系统通过 “先描述,再组织” 的方式对消息队列进行管理(即先对消息队列的属性等进行描述,再通过特定结构组织这些描述信息,实现有效管理)。
1.2 消息队列的属性
1.2.1 struct msqid_ds 结构体
专门用于描述消息队列的属性,是操作系统管理消息队列的 “描述” 基础。
struct msqid_ds {struct ipc_perm msg_perm; /* 消息队列的权限信息(ipc_perm 结构体) */time_t msg_stime; /* 最后一次调用 msgsnd 发送消息的时间 */time_t msg_rtime; /* 最后一次调用 msgrcv 接收消息的时间 */time_t msg_ctime; /* 最后一次修改队列属性的时间(如 msgctl(IPC_SET)) */unsigned long __msg_cbytes;/* 队列中所有消息的总字节数(系统内部维护) */msgqnum_t msg_qnum; /* 队列中当前的消息数量 */msglen_t msg_qbytes; /* 队列允许的最大字节数(容量上限) */pid_t msg_lspid; /* 最后一次调用 msgsnd 的进程 ID */pid_t msg_lrpid; /* 最后一次调用 msgrcv 的进程 ID */unsigned long __unused4; /* 未使用 */unsigned long __unused5; /* 未使用 */
};
1.2.2 struct ipc_perm 结构体
存储所有 System V IPC 对象(包括消息队列、共享内存、信号量)的通用权限信息,是struct msqid_ds的成员之一。
struct ipc_perm {key_t __key; /* 消息队列的键值(由 msgget 传入) */uid_t uid; /* 所有者的用户 ID */gid_t gid; /* 所有者的组 ID */uid_t cuid; /* 创建者的用户 ID */gid_t cgid; /* 创建者的组 ID */unsigned short mode; /* 权限模式(类似文件权限,如 0666) */unsigned short __seq; /* 序列号(系统内部用于标识对象) */unsigned long __unused1; /* 未使用(预留字段) */unsigned long __unused2; /* 未使用(预留字段) */
};
1.3 消息队列相关命令
1.查看消息队列信息:
ipcs -q
- 功能:显示当前系统中存在的消息队列相关信息。
2.删除指定消息队列:
ipcrm -q [msgid]
- 功能:根据消息队列的 ID(msgid),删除对应的消息队列。
- 说明:需将命令中的msgid替换为实际要删除的消息队列的 ID。
2.信号量
2.1 原理
2.1.1 共享内存的局限
共享内存通过让多个进程直接访问同一块物理内存实现高效通信,但因缺乏同步机制,可能导致数据不一致。
例如:多个进程同时读写时,A 正在写入且未完成,部分数据被 B 读取,导致双方收发的数据一致。
2.1.2 重要概念
- 共享资源及其问题:多个进程能看到的同一份资源,若不保护会导致数据不一致。
- 加锁(互斥):解决共享资源问题的办法,保证任何时候只允许一个执行流访问共享资源。
- 临界资源:任何时候只允许一个执行流访问(执行访问代码)的共享资源,通常是一段由操作系统或用户维护的内存空间。
- 临界区:访问临界资源的代码(例如 100 行代码中,可能专门用于访问临界资源的代码就几行)。
2.1.3 现象解释:多进程 / 线程并发打印内容错乱
问:多进程/多线程并发循环打印时,为什么显示器上的内容会错乱,有时还和命令行混合在一起?
答:因为显示器可看作一个文件,数据先写入其缓冲区再刷新显示。多进程/线程会同时访问作为共享资源的显示器,若没有互斥或保护机制,各进程/线程的输出数据会在缓冲区相互干扰,导致内容错乱,甚至与命令行内容混合。
2.1.4 信号量的理解
- 本质:计数器(类似int cnt;)。
- 作用:描述临界资源中资源的数量。
- 工作逻辑:在计算机中,临界资源被划分为多个小块。并发执行访问时,若多个执行流的数量超过临界资源小块的数量,就可能出现多个执行流访问同一小块资源的情况,进而导致数据不一致。为避免这种问题,引入计数器cnt:每当一个执行流要访问资源时,cnt就减1(表示申请资源);当cnt为0时,说明资源已被申请完毕,后续执行流需等待,直到有执行流释放资源后再进行分配。
- 结论
- 权限标识:计数器申请成功,即表示该执行流获得了访问资源的权限。
- 预定机制:申请计数器资源后,执行流并未立即访问共享资源,因此计数器本质是对资源的一种预定机制。
- 数量控制:计数器能有效限制进入共享资源的执行流数量,避免因并发访问导致的数据不一致问题。
- 访问前提:每个执行流若要访问共享资源的一部分,不能直接访问,必须先申请计数器。
2.1.5 二元信号量
- 当临界资源只有一份时,计数器未被申请时为 1,申请后为 0,只能取 1 和 0 两态,称为二元信号量,本质是一把锁。
- 此时临界资源作为整体被申请和释放,而非分成多块。
2.1.6 信号量计数器的安全问题
要访问临界资源需申请信号量计数器资源,这意味着信号量计数器本身也是共享资源,而信号量要保证自身不被同时申请,关键在于确保计数器的减减操作安全——整数减减操作并不安全,因为C语言中一条cnt减减语句在汇编层面会分解为三步:
- 将cnt从内存移到 CPU 寄存器
- 在 CPU 内执行减减操作
- 将结果写回内存中的
cnt
进程运行中可能随时切换,若在上述步骤中切换,会导致多个执行流同时访问cnt
,引发减减操作异常。
2.1.7 信号量操作(PV 操作)的原子性
- P 操作:申请信号量,本质是对计数器做减减操作。
- V 操作:释放资源,本质是对计数器做加加操作。
- 原子性:操作只有 “未做” 和 “做完” 两种状态,没有 “正在做” 的中间状态(技术上,单条语句执行具有原子性)。
2.1.8 信号量作为进程通信方式的原因
- 通信不仅包括数据传递,进程间的协同也是一种通信。
- 协同的本质是通信,因此信号量需被所有通信进程可见。
2.2 信号量属性
2.2.1 struct semid_ds 结构体
专门用于描述 System V 信号量集的属性信息,包含信号量集的权限、操作时间、信号量数量等关键信息,是系统管理信号量集的核心数据结构。
struct semid_ds {struct ipc_perm sem_perm; /* 信号量集的通用权限信息(继承自 ipc_perm) */time_t sem_otime; /* 最后一次执行 semop 操作(信号量操作)的时间 */time_t sem_ctime; /* 最后一次修改信号量集属性的时间(如 semctl 操作) */unsigned short sem_nsems; /* 信号量集中包含的信号量数量 *//* 可能包含其他系统预留字段(如 __unused 等) */
};
2.2.2 struct ipc_perm 结构体
存储所有 System V IPC 对象(包括信号量、消息队列、共享内存)的通用权限信息,是 struct semid_ds
(以及 struct msqid_ds
、struct shmid_ds
)的成员之一,用于统一管理 IPC 对象的所有权和访问权限。
struct ipc_perm {key_t __key; /* IPC 对象的键值(由创建时传入,如 semget 的 key 参数) */uid_t uid; /* 所有者的用户 ID */gid_t gid; /* 所有者的组 ID */uid_t cuid; /* 创建者的用户 ID */gid_t cgid; /* 创建者的组 ID */unsigned short mode; /* 权限模式(类似文件权限,如 0666 表示所有者、组、其他用户均可读写) */unsigned short __seq; /* 序列号(系统内部用于唯一标识 IPC 对象) *//* 可能包含预留的未使用字段(如 __unused1、__unused2 等) */
};
2.3 IPC 在内核中的数据结构设计笔记
2.3.1 IPC 资源的整合与核心管理结构
操作系统将所有 IPC 资源(共享内存、消息队列、信号量等)整合在 IPC 模块中,管理这些资源实际是管理对应的结构体(semid_ds、msqid_ds、shmid_ds等)。
核心管理方式:通过struct ipc_perm* array[]数组实现管理,对 IPC 资源的增删查改操作均转化为对该数组的相应操作。
2.3.2 数组的资源存储与唯一性确认
- 创建资源时,对应结构体的第一个字段均为
struct ipc_perm
类型(如共享内存shmid_ds
的shm_perm
),该字段的地址会存入数组,数组下标即为资源的 ID(shmid
、msgid
、semid
)。 - 对 IPC 资源的增删查改,实际转化为对该数组的相应操作。
资源定位与唯一性确认:进程通过用户层的_key
定位资源,遍历数组并比较每个资源ipc_perm
字段中的_key
,确认是否为目标资源。
2.3.3 资源访问与类型区分机制
- 字段访问方式:不同资源类型的ID可能会出现冲突,当要访问某个资源时,例如以ipc_perm array[0]为例,若想访问shmid_ds里的shm_atime,由于ipc_perm array[0]中存放了对应资源第一个字段(即struct ipc_perm类型)的地址,而该字段是shmid_ds结构的第一个字段,因此可以通过将该地址强转为struct shmid_ds*类型的指针,进而访问到其中的shm_atime字段。
- 类型区分依据:数组之所以能知道要强转成什么类型,是因为ipc_perm在内核层对应的kern_ipc_perm结构中包含mode选项,通过在ipc_perm中添加类型标志,代码就能区分它所代表的是哪种IPC资源,因此msgget、shmget、semget返回的其实是ipc_perm指针数组的下标,这类似于C++中的多态机制。
2.3.4 IPC 资源 ID 的特点
IPC资源返回的ID与文件描述符不同,其数值可大可小,这是因为ID来源于操作系统维护的一个独立数组(不隶属于进程,无法与进程强关联),该数组的下标呈线性递增,当大到一定程度时会绕回零。