进程通信————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 重要概念

  1. 共享资源及其问题:多个进程能看到的同一份资源,若不保护会导致数据不一致。
  2. 加锁(互斥):解决共享资源问题的办法,保证任何时候只允许一个执行流访问共享资源。
  3. 临界资源:任何时候只允许一个执行流访问(执行访问代码)的共享资源,通常是一段由操作系统或用户维护的内存空间。
  4. 临界区:访问临界资源的代码(例如 100 行代码中,可能专门用于访问临界资源的代码就几行)。

2.1.3 现象解释:多进程 / 线程并发打印内容错乱

问:多进程/多线程并发循环打印时,为什么显示器上的内容会错乱,有时还和命令行混合在一起?
答:因为显示器可看作一个文件,数据先写入其缓冲区再刷新显示。多进程/线程会同时访问作为共享资源的显示器,若没有互斥或保护机制,各进程/线程的输出数据会在缓冲区相互干扰,导致内容错乱,甚至与命令行内容混合。

2.1.4 信号量的理解

  • 本质:计数器(类似int cnt;)。
  • 作用:描述临界资源中资源的数量。
  • 工作逻辑:在计算机中,临界资源被划分为多个小块。并发执行访问时,若多个执行流的数量超过临界资源小块的数量,就可能出现多个执行流访问同一小块资源的情况,进而导致数据不一致。为避免这种问题,引入计数器cnt:每当一个执行流要访问资源时,cnt就减1(表示申请资源);当cnt为0时,说明资源已被申请完毕,后续执行流需等待,直到有执行流释放资源后再进行分配。
  • 结论
    1. 权限标识:计数器申请成功,即表示该执行流获得了访问资源的权限。
    2. 预定机制:申请计数器资源后,执行流并未立即访问共享资源,因此计数器本质是对资源的一种预定机制。
    3. 数量控制:计数器能有效限制进入共享资源的执行流数量,避免因并发访问导致的数据不一致问题。
    4. 访问前提:每个执行流若要访问共享资源的一部分,不能直接访问,必须先申请计数器。

2.1.5 二元信号量

  • 当临界资源只有一份时,计数器未被申请时为 1,申请后为 0,只能取 1 和 0 两态,称为二元信号量,本质是一把锁。
  • 此时临界资源作为整体被申请和释放,而非分成多块。

2.1.6 信号量计数器的安全问题

要访问临界资源需申请信号量计数器资源,这意味着信号量计数器本身也是共享资源,而信号量要保证自身不被同时申请,关键在于确保计数器的减减操作安全——整数减减操作并不安全,因为C语言中一条cnt减减语句在汇编层面会分解为三步:

  1. 将cnt从内存移到 CPU 寄存器
  2. 在 CPU 内执行减减操作
  3. 将结果写回内存中的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_dsstruct 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_dsshm_perm),该字段的地址会存入数组,数组下标即为资源的 ID(shmidmsgidsemid)。
  • 对 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来源于操作系统维护的一个独立数组(不隶属于进程,无法与进程强关联),该数组的下标呈线性递增,当大到一定程度时会绕回零。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值