4. System V 共享内存4.1 概念与原理概念共享内存是一种高效且快速的进程间通信方式。它允许多个进程将同一块物理内存区域映射到各自的虚拟地址空间中从而直接进行数据的读写无需经过内核中转。一旦共享内存映射到进程的地址空间进程间的数据传递不再涉及内核——也就是说进程不再需要通过执行系统调用来传递彼此的数据直接访问内存即可。通信流程创建共享内存 → 构建映射 → 进程通信 → 解除映射 → 删除共享内存原理操作系统在共享内存的创建和映射过程中完成了以下工作创建共享内存OS 在内核中分配一块物理内存区域并建立对应的内核数据结构shmid_ds来管理这块内存。构建页表映射进程调用shmat时OS 在进程的虚拟地址空间中共享区即堆栈之间的 mmap 映射区申请一段虚拟内存并建立页表将这段虚拟内存映射到共享内存的物理地址。进程通信映射完成后多个进程的虚拟地址空间中的不同虚拟地址通过各自的页表最终指向同一块物理内存。进程直接读写这块内存数据即刻被其他进程可见。以上工作全部由操作系统完成。OS 向我们提供系统调用接口我们调用这些接口来完成共享内存的创建、映射、解除映射和删除。附图解释引用计数当通信结束需要释放虚拟共享内存空间、删除页表映射、取消关联关系。OS 如何知道这块共享内存是否还有进程在使用——引用计数。shmid_ds结构体中的shm_nattch字段记录了当前有多少个进程 attach映射了这块共享内存。只有引用计数归零时共享内存才真正被释放。这与管道写端的引用计数原理一致——都是通过计数来管理资源的生命周期。先描述再组织当系统中有多组进程进行通信时就会存在大量共享内存有的正在使用有的准备释放有的已经标记为删除。OS 需要管理它们——方法仍然是经典的先描述再组织。描述每个共享内存对应一个shmid_ds内核数据结构。组织所有shmid_ds被组织在系统中进程的task_struct通过关联关系找到它。进程与共享内存的关系本质上就是task_struct与shmid_ds的内核数据结构关系。4.2 共享内存数据结构struct shmid_ds { struct ipc_perm shm_perm; /* 操作权限 */ size_t shm_segsz; /* 段大小字节 */ time_t shm_atime; /* 最后 attach 时间 */ time_t shm_dtime; /* 最后 detach 时间 */ time_t shm_ctime; /* 最后修改时间 */ pid_t shm_cpid; /* 创建者 PID */ pid_t shm_lpid; /* 最后操作者 PID */ shmatt_t shm_nattch; /* 当前 attach 数引用计数 */ ... }; struct ipc_perm { key_t __key; /* 我们设置的 key 就存储在这里 */ uid_t uid; /* 所有者有效 UID */ gid_t gid; /* 所有者有效 GID */ uid_t cuid; /* 创建者有效 UID */ gid_t cgid; /* 创建者有效 GID */ unsigned short mode; /* 权限 SHM_DEST SHM_LOCKED 标志 */ unsigned short __seq; /* 序列号 */ };4.3 共享内存的唯一标识key为了让不同进程看到同一块共享内存OS 需要给共享内存提供一个唯一标识——这就是key。为什么 key 要让用户传入如果 key 由内核自动生成由于进程具有独立性进程 A 创建共享内存后进程 B 无法知道它的 key 值是多少也就无法访问同一块共享内存。因此通信双方只需事先约定好相同的参数文件路径 项目 ID各自调用ftok()就能生成相同的 key再用这个 key 调用shmget()就能看到同一块共享内存。进程Aftok(/tmp/foo, A) → key0x1234 → shmget(0x1234, ...) 进程Bftok(/tmp/foo, A) → key0x1234 → shmget(0x1234, ...) ↓ 看到同一块共享内存inode 和 PID 是 OS 发的身份证管理权在 OS共享内存的 key 是用户自己造的接头暗号双方提前对好暗号才能找到同一个共享内存。4.4ftok()— 生成 key 值#include sys/types.h #include sys/ipc.h key_t ftok(const char *pathname, int proj_id);功能将已存在的文件路径pathname和项目标识符proj_id通过特定算法转化为一个唯一的 key 值用于标识共享内存、消息队列、信号量集等 System V IPC 对象。参数pathname一个必须存在于文件系统中的文件路径。proj_id用户自定义的项目标识符用于区分同一文件的不同 IPC 资源通常为 8 位整数0~255。返回值成功返回唯一 key 值失败返回-1并设置errno。注意频繁调用ftok可能会发生键值冲突实际使用中需注意参数选择。4.5shmget()— 创建/获取共享内存#include sys/ipc.h #include sys/shm.h int shmget(key_t key, size_t size, int shmflg);功能创建一个新的共享内存或获取一个已存在的共享内存。参数key共享内存的标识键值由ftok生成。size共享内存的大小字节。如果是获取已存在的共享内存可为 0。shmflg标志位由权限标志和创建控制标志组成。返回值成功返回共享内存标识符shmid非负整数失败返回-1并设置errno。shmflg取值实际使用// 获取或创建权限 0666 int shmid shmget(key, size, IPC_CREAT | 0666); // 必须创建全新的已存在就报错权限 0666 int shmid shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);注意IPC_EXCL单独使用无意义必须配合IPC_CREAT一起使用。IPC_CREAT是获取或创建IPC_CREAT | IPC_EXCL是必须创建已存在就报错。权限位用|拼接在shmflg中和文件权限的用法完全一致最终权限还会受umask影响。4.6 key 和 shmid 的区别两者都是确保共享内存唯一性和可访问性的重要机制但作用层面不同用户用 key 找内核说我要那块共享内存内核找到后返回 shmid 说给你个号以后用它叫我。之后所有操作都用 shmidkey 完成任务退场。4.7shmctl()— 共享内存管理共享内存生命周期随内核即使进程退出只要没有显式删除共享内存一直存在。这不同于文件——文件被进程打开后进程退出时引用计数减到 0就会释放。那怎么删除共享内存命令行删除ipcs -m # 查看所有共享内存 ipcrm -m shmid # 按 shmid 删除注意不是 key代码级删除shmctl()#include sys/ipc.h #include sys/shm.h int shmctl(int shmid, int cmd, struct shmid_ds *buf);功能用来控制共享内存的各种属性如获取属性、设置属性、删除共享内存。参数shmid共享内存的标识符不是 key用户所有对共享内存的管理都用 shmid。cmd要执行的操作。buf指向shmid_ds结构体的指针用于传递或接收共享内存的属性信息。返回值成功返回0失败返回-1并设置errno。cmd常用命令注意IPC_RMID只是标记删除不会立即销毁。如果还有其他进程 attach 着shm_nattch 0共享内存会等到最后一个进程 detach 后才真正释放。标记删除后shm_perm.mode中的SHM_DEST标志位会被设置。4.8 命令行查看共享内存ipcs是显示和管理系统中进程间通信IPC资源的工具。选项作用ipcs -m显示共享内存相关信息ipcs -q显示消息队列相关信息ipcs -s显示信号量集相关信息ipcs -a显示全部默认$ ipcs -m ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x66030843 0 xqq 0 4096 0字段含义key共享内存的键值十六进制shmid共享内存标识符owner创建者perms权限bytes大小字节nattch当前 attach 进程数引用计数statusdest表示已标记删除等待 nattch 归零4.9shmat()— 进程挂接共享内存void *shmat(int shmid, const void *shmaddr, int shmflg);功能将共享内存连接到进程地址空间从而构建页表映射。参数shmid共享内存的标识符。shmaddr指定映射到地址空间的具体起始位置。通常设为NULL让 OS 自动选择。shmflg控制挂接方式的标志位。通常设为0表示可读可写模式。返回值成功返回映射后的虚拟地址失败返回(void *) -1。shmaddr的两种行为其他说明实际上malloc的原理也类似——先申请虚拟地址空间然后构建页表映射返回起始虚拟地址。所以使用共享内存与使用malloc没本质区别。4.10shmdt()— 断开共享内存连接int shmdt(const void *shmaddr);功能断开共享内存与调用进程的地址空间连接解除映射。并不会直接删除共享内存。参数shmaddr之前shmat返回的起始虚拟地址。返回值成功返回0失败返回-1。shmdt是shmat的逆操作。挂接时 OS 已经把地址和共享内存的对应关系记下来了所以解除时一个地址就够了。4.11 client server 共享内存通信实例SHM.hpp#include iostream #include sys/ipc.h #include sys/shm.h #include unistd.h #include sys/types.h #include cstdlib #include cstring #define PATH . #define PROJ_ID 0x666 #define SIZE 4096 const int gdefaultid -1; const int gmode 0666; #define USER user #define CREATER creater #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) class SHM { public: SHM(const SHM s) delete; SHM operator(const SHM s) delete; SHM(const std::string pathname, int projid, const std::string usertype) : _shmid(gdefaultid), _size(SIZE), _start_mem(nullptr), _user_type(usertype) { _key ftok(pathname.c_str(), projid); if (_key 0) { ERR_EXIT(ftok fail); } if (_user_type CREATER) { Create(); } else if (_user_type USER) { Get(); } else { ERR_EXIT(NONE); } SHMAttach(); } ~SHM() { SHMDetach(); if (_user_type CREATER) { Destroy(); } } int size() { return _size; } void *VirtualAddr() { std::cout VirtualAddr: _start_mem std::endl; return _start_mem; } void Attr() { struct shmid_ds ds; int n shmctl(_shmid,IPC_STAT,ds);//ds:输出型参数 std::coutshm_segsz:ds.shm_segszstd::endl; std::coutstd::hexkey:ds.shm_perm.__keystd::endl; // ^以16进制打印 } private: int _shmid; int _size; void *_start_mem; std::string _user_type; key_t _key; void createHelper(int flag) { std::cout _key std::endl; //_shmid shmget(key, _size, IPC_CREAT | IPC_EXCL | gmode); _shmid shmget(_key, _size, flag); if (_shmid 0) { ERR_EXIT(shmget fail); } std::cout _shmid std::endl; } void Destroy() { if (_user_type CREATER) { if (_shmid gdefaultid) return; int n shmctl(_shmid, IPC_RMID, nullptr); if (n 0) { std::cout delete shm: _shmid success std::endl; _shmid gdefaultid; } else { ERR_EXIT(shmctl fail); } } } void Create() { createHelper(IPC_CREAT | IPC_EXCL | gmode); } // 创建端和获取段只有shmflg不同 void Get() { createHelper(IPC_CREAT | gmode); } void SHMAttach() { _start_mem shmat(_shmid, nullptr, 0); // if ((long long)_start_mem 0) //(void *) -1 is returned if (_start_mem (void *)-1) // 推荐写法虽然上面也行 { ERR_EXIT(shmat fail); } if (_user_type CREATER) { memset(_start_mem, 0, _size); } std::cout attach success std::endl; } void SHMDetach() { int n shmdt(_start_mem); if (n 0) { ERR_EXIT(shmdt fail); } std::cout Detach success std::endl; } };server.cc#includeSHM.hpp int main() { SHM shm(PATH,PROJ_ID,CREATER); shm.Attr(); char* mem(char*)shm.VirtualAddr(); int time 30; while(time--) { std::coutmemstd::endl; sleep(1); } return 0; }client.cc#includeSHM.hpp int main() { // 获取共享内存 // 映射到自己的地址空间 SHM shm(PATH,PROJ_ID,USER); char* mem(char*)shm.VirtualAddr(); for(char cA;cZ;c) { mem[c-A]c; sleep(1); } return 0; }常见坑shmat权限拒绝shmget成功创建了共享内存但shmat时被拒绝。原因创建共享内存时忘记设置权限位IPC_CREAT | IPC_EXCL只控制创建行为不控制访问权限。缺少权限位时默认权限通常为0000。最后运行结果4.12 共享内存为什么是最快的 IPC通过上面的实例可以看到管道需要调用write/read系统调用数据要经过用户态↔内核态两次拷贝。共享内存没有使用系统调用。共享内存位于堆栈之间的内核共享区该区域属于用户空间用户可以直接通过指针读写。管道进程A → write用户→内核→ 管道缓冲区 → read内核→用户→ 进程B 共享内存进程A → 直接写共享内存 → 进程B 直接读同一块物理内存零拷贝4.13 共享内存与动态库加载的类比假如对方不是进程而是磁盘文件——开辟了一段共享内存空间后将磁盘上的文件 load 到内存里然后让多个进程各自将该区域映射到自己的虚拟地址空间建立页表映射多个进程就可以同时看到这个文件了。这不就是动态库的加载吗对进程做内存级重定向我们就可以用用户地址方式访问库方法了。虽然内核使用的不是 System V 模式而是mmap的方式但基本原理就是这样。System V 共享内存、mmap、动态库加载——三者在多进程共享物理内存这一点上殊途同归。4.14 共享内存的缺点缺乏同步机制共享内存不提供进程间协同的任何机制。这会导致多个进程同时访问共享内存时出现数据不一致和数据竞争等问题一个进程正在写入另一个进程同时读取 → 可能读到不完整的数据两个进程同时写入 → 数据覆盖混乱进程间协同机制确保多个进程在访问公共资源时能够正确地同步、互斥以及协调彼此的操作。对比管道管道在操作系统中自带协同机制——读写操作具有原子性阻塞机制也起到了协同作用缓冲区满时写阻塞缓冲区空时读阻塞。解决方案管道 共享内存混合同步在共享内存之外引入一根管道作为通知通道写入端 读取端 │ │ │ 1. 写入数据到共享内存 │ 1. read(管道) 阻塞等待... │ 2. write(管道, !, 1) ──────→│ 2. 解除阻塞知道数据就绪 │ │ 3. 读取共享内存中的数据 │ │ 4. 再次 read(管道) 阻塞等待...优势说明简单不需要信号量、互斥锁等额外 IPC 机制可靠管道的阻塞/唤醒机制由内核保证不丢通知解耦数据通道共享内存和同步通道管道完全分离共享内存负责快管道负责准——这个方案把两者的优势完美结合。实际工程中更常用信号量/futex 做同步但管道方案是入门的最佳实践4.15 共享内存的大小为什么必须是 4KB 的整数倍在内核中共享内存的分配以页Page为单位一个页的大小通常是4KB4096 字节。当用户申请的大小不是 4KB 的整数倍时OS 会做向上取整分配最小的满足需求的整数个页。原因这是由操作系统的内存管理单元MMU决定的。内存管理以页为最小单位页表结构页表的每一项映射一个完整的物理页无法映射半个页。物理内存分配内核的伙伴系统以页为单位管理物理内存。共享性要求共享内存需要被多个进程同时映射不同进程的页表必须指向同一块物理页。shmget中的size只是一个请求值——内核实际分配ceil(size / 4096) × 4096字节但用户可用的字节数仍然是请求的size。多余的字节虽然分配了但不能使用属于内部碎片。5. System V 消息队列5.1 原理与概念什么是消息队列消息队列是一种进程间通信IPC机制允许多个进程通过发送和接收带有类型的数据块消息进行通信。这些消息在内核维护的队列中按照先进先出FIFO的顺序存储。发送进程将消息添加到队列的末尾接收进程从队列的头部读取消息消息队列也遵循 System V IPC 标准资源的生命周期随内核——进程退出后消息队列依然存在直到显式删除或系统重启。管理方式先描述再组织不同进程之间可能创建多个消息队列队列一多OS 就要对它们进行管理。管理方式仍然是经典的先描述再组织描述每个消息队列对应一个msqid_ds内核数据结构组织通过key唯一标识进程通过相同的key找到同一个消息队列特点基本组件消息队列特别适用于异步消息传递和任务队列等场景。5.2 消息队列核心操作5.3msgget()— 创建/获取消息队列#include sys/types.h #include sys/ipc.h #include sys/msg.h int msgget(key_t key, int msgflg);功能创建或获取一个消息队列。参数key由ftok生成的键值。msgflg标志位取值与shmflg完全一致IPC_CREAT、IPC_CREAT | IPC_EXCL需配合权限位如0666。返回值成功返回消息队列标识符msgid失败返回-1。5.4msgsnd()— 发送消息#include sys/types.h #include sys/ipc.h #include sys/msg.h int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);功能将一条消息发送到消息队列的队尾。参数msqid消息队列标识符。msgp指向消息结构体的指针。msgsz消息数据mtext的大小不是整个结构体的大小。msgflg通常设为0阻塞模式或IPC_NOWAIT非阻塞。返回值成功返回0失败返回-1。消息结构体struct msgbuf { long mtype; // 消息类型必须 0 char mtext[0]; // 消息数据 };5.5msgrcv()— 接收消息#include sys/types.h #include sys/ipc.h #include sys/msg.h ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);功能从消息队列中接收一条消息。参数msqid消息队列标识符。msgp指向消息结构体的指针接收数据。msgsz消息数据mtext的最大大小。msgtyp消息类型筛选——这是消息队列的核心特性。msgflg通常设为0阻塞模式或IPC_NOWAIT。返回值成功返回实际读取的字节数失败返回-1。msgtyp的取值注意msgsz指的是mtext的大小不是sizeof(msgbuf)。msgsnd和msgrcv都是如此。5.6msgctl()— 控制消息队列#include sys/types.h #include sys/ipc.h #include sys/msg.h int msgctl(int msqid, int cmd, struct msqid_ds *buf);功能控制消息队列获取属性、设置属性、删除队列。参数msqid消息队列标识符。cmd操作命令。buf指向msqid_ds结构体的指针。返回值成功返回0失败返回-1。cmd常用命令命令功能IPC_STAT获取队列的内核数据结构信息存入bufIPC_SET设置队列属性如权限IPC_RMID立即删除消息队列不管是否还有未读消息内核数据结构struct msqid_ds { struct ipc_perm msg_perm; /* 权限结构与 shmid_ds 一致这也是 System V 标准带来的便利 */ time_t msg_stime; /* 最后一次 msgsnd 时间 */ time_t msg_rtime; /* 最后一次 msgrcv 时间 */ time_t msg_ctime; /* 创建时间或最后一次 msgctl 修改时间 */ unsigned long msg_cbytes; /* 队列中当前字节数 */ msgqnum_t msg_qnum; /* 队列中当前消息数 */ msglen_t msg_qbytes; /* 队列允许的最大字节数 */ pid_t msg_lspid; /* 最后一次 msgsnd 的进程 PID */ pid_t msg_lrpid; /* 最后一次 msgrcv 的进程 PID */ };msg_perm与共享内存中的ipc_perm结构完全一致——这也是 System V 标准带来的便利一套权限模型统一管理共享内存、消息队列、信号量。5.7 通信示例设计在实际的 client/server 通信中可以通过定义公共头文件来统一消息格式// comm.hpp #define PATHNAME . #define PROJID 0x666 #define CLIENT_TYPE 1 #define SERVER_TYPE 2 struct msgbuf { long mtype; // 消息类型必须 0 char mtext[1024]; // 消息数据 };使用方式5.8 命令行查看与删除与共享内存类似只是选项从-m变成-q5.9 消息队列 vs 共享内存 vs 管道6. 信号量6.1 为什么需要信号量我们已经知道进程间通信的前提是让不同进程看到同一份资源这种资源叫做共享资源。但在多执行流场景下共享资源可能同时被多个执行流尝试访问、修改如果不加以保护就会导致数据不一致、资源竞争和死锁等问题。常见的保护机制主要包括临界资源与临界区保护临界资源本质是保护临界区——确保在任何时刻只有一个执行流能够进入临界区这个机制叫互斥从而保证数据的一致性和正确性。原子性一个操作被认为是原子的意味着此操作要么完全执行成功要么完全不执行不存在中间状态即不可分割不能被其他执行流中断。比如我们申请锁来保护临界区锁本身也是共享的谁来保护锁的安全答案就是锁的设计本身就是原子的——锁的申请要么完成要么没完成没有申请中的状态。在并发编程中原子性用于确保多个线程或进程对共享资源进行操作时不会导致数据不一致或不确定的结果。6.2 信号量原理与概念信号量在进程间通信IPC中扮演着重要的角色。尽管它的目的不是直接传递数据而是通过维护一个计数器来实现对共享资源的协同访问。信号量一种用于控制多个进程对共享资源访问的同步机制主要用于解决互斥和同步问题确保在任何时候只有有限数量的进程可以访问特定的资源。本质一个整型计数器表示可用的临界资源数目。核心操作PV 操作均为原子性操作。访问流程申请信号量P 操作对资源的预定→ 访问临界资源 → 释放信号量V 操作生活类比电影院电影院本质上是一个临界资源包含了很多子资源座位。有两个问题不能出现票卖多了人数大于座位数多出来的人找不到位置票号重复两个人拿到同一个座位我们买票时票买到了这个位置就是我的——即使那场电影我没去看也要留着。所以买票就是对资源的预定机制即要访问临界资源就要先预定。引申到共享内存假设临界资源是共享内存将这块共享内存分成一块一块的不同区域不同的进程使用共享内存的不同块互不影响可以并发访问通过信号量控制不要让太多进程进来同时控制不要访问同一个位置信号量就是临界资源中资源数量的多少的计数器。信号量本身也是共享资源注意信号量本身就是共享资源因为要被多个进程看到。信号量用来保护共享资源的竞争问题那么谁来保护信号量呢这个问题涉及多线程后续会详细讲解。二元信号量与多元信号量将电影院的故事升级如果有人想在看电影时不被打扰就有了超级 VIP 放映厅——只有一个座位。PV 操作资源整体使用 vs 分块使用6.3 为什么信号量被归为进程间通信信号量不传递数据但传递的是访问权控制信息。System V 把三类资源统称为 IPC 对象三者都用key标识都用ipcs查看都用ipcrm删除统一管理。信号量被归入 IPC是因为它协调进程间的行为——它不运货但管着谁先过路口。6.4 信号量相关接口semget()— 创建/获取信号量集#include sys/types.h #include sys/ipc.h #include sys/sem.h int semget(key_t key, int nsems, int semflg);功能创建或获取一个信号量集。参数key由ftok生成的键值。nsems信号量集中信号量的数量。可以只创建 1 个也可以一次创建多个信号量集。semflg标志位与shmflg、msgflg一致IPC_CREAT、IPC_CREAT | IPC_EXCL 权限位。返回值成功返回信号量集标识符semid失败返回-1。semctl()— 控制信号量集#include sys/types.h #include sys/ipc.h #include sys/sem.h int semctl(int semid, int semnum, int cmd, ...);功能控制信号量集初始化、获取/设置属性、删除等。参数semid信号量集标识符。semnum信号量在集合中的编号从 0 开始用于指定操作哪一个信号量。cmd操作命令。...可变参数根据cmd传入不同类型的数据通常为union semun。cmd常用命令union semununion semun { int val; /* SETVAL 时的值 */ struct semid_ds *buf; /* IPC_STAT / IPC_SET 时的缓冲区 */ unsigned short *array; /* GETALL / SETALL 时的数组 */ struct seminfo *__buf; /* IPC_INFO 时的缓冲区Linux 特有 */ };注意创建和初始化是分开的并非原子操作。需要先semget创建再semctl(semid, semnum, SETVAL, ...)初始化。semop()— 信号量操作PV 操作#include sys/types.h #include sys/ipc.h #include sys/sem.h int semop(int semid, struct sembuf *sops, unsigned nsops);功能对信号量集中的信号量执行P 操作或V 操作原子操作。参数semid信号量集标识符。sops指向sembuf结构体数组的指针。nsops数组中sembuf的数量。struct sembufstruct sembuf { unsigned short sem_num; /* 信号量编号哪一个 */ short sem_op; /* 操作类型做什么-1P操作1V操作 */ short sem_flg; /* 操作标志通常为 0阻塞或 IPC_NOWAIT、SEM_UNDO */ };6.5 信号量的内核数据结构两级结构semid_ds信号量集一个 IPC 对象 │ └── sem_array[]多个信号量每个都是一个独立计数器 ├── sem[0]: semval1, sempid... ├── sem[1]: semval0, sempid... └── sem[2]: semval3, sempid...第一级semid_ds— 信号量集描述符struct semid_ds { struct ipc_perm sem_perm; /* 权限结构与 shmid_ds、msqid_ds 一致 */ time_t sem_otime; /* 最后一次 semop 时间 */ time_t sem_ctime; /* 创建时间或最后一次 semctl 修改时间 */ unsigned long sem_nsems; /* 该集合中信号量的数量 */ };第二级sem— 单个信号量struct sem { unsigned short semval; /* 信号量当前值 */ pid_t sempid; /* 最后一个操作该信号量的进程 PID */ unsigned short semncnt; /* 等待信号量值增加等资源可用的进程数 */ unsigned short semzcnt; /* 等待信号量值变为 0 的进程数 */ };sem结构体中没有显式的锁字段。锁的语义是由semval 内核等待队列 原子semop三者配合隐式实现的。semncnt和semzcnt记录的是阻塞进程的数量实际等待队列由内核维护。6.6 key 冲突问题共享内存、消息队列、信号量三者都使用同一个ftok算法生成 key。那么 key 会不会冲突会冲突。所以在 OS 中共享内存、消息队列、信号量被全部当作同一种资源来管理——这正是 System V 标准的统一设计。三者在内核中被组织在同一个全局数据结构中。6.7 内核如何组织管理核心结构ipc_id_array内核中有一个全局数组或基数树里面的元素类型是struct kern_ipc_perm *指针。这个数组使用了柔性数组方便随时扩容。C 语言实现多态虽然共享内存shmid_ds、消息队列msqid_ds、信号量semid_ds的内核结构体不完全一样但它们的第一个成员都是struct ipc_perm。struct shmid_ds { struct ipc_perm shm_perm; // 第一个成员 // ... }; struct msqid_ds { struct ipc_perm msg_perm; // 第一个成员 // ... }; struct semid_ds { struct ipc_perm sem_perm; // 第一个成员 // ... };内核数组存储的是kern_ipc_perm *指针分别指向这些结构体的开头。由于第一个成员相同将来通过container_of宏或直接强制类型转换就可以从kern_ipc_perm *推算回外层完整结构体读取整个共享内存、消息队列或信号量的信息。这就是用C 语言实现多态kern_ipc_perm类似于基类shmid_ds、msqid_ds、semid_ds就是派生类。id 就是数组下标我们之前使用的shmid、msgid、semid其实就是这个全局数组的下标。将来用这个下标就可以在数组里面索引到对应的结构体。ipc_id_array[] │ ├── [0] → kern_ipc_perm* → 强转 → shmid_ds共享内存 ├── [1] → kern_ipc_perm* → 强转 → semid_ds信号量集 ├── [2] → kern_ipc_perm* → 强转 → msqid_ds消息队列 └── ...