目录一、什么是共享内存?二、共享内存的原理共享内存的本质共享内存的管理三、系统调用接口介绍shmget创建共享内存ftok() 函数生成 key 值shmid和key对比shmat挂接接口shmdt去挂接shmctl关闭共享内存共享内存状态结构体 struct shmid_ds四、代码实现MakefileShm.hppServer.ccClient.cc运行:进行通信:五、共享内存的特性六、总结前面我们学习了匿名管道和命名管道今天我们学习另一种通信方式 : System V共享内存通信方式一、什么是共享内存?共享内存就是由内核开辟一块公共物理内存多个进程将这块公共的物理内存映射到各自的进程虚拟地址空间进而实现共享从而直接读写同一块内存数据无需内核拷贝数据所以共享内也是速度最快的进程间通信方式。二、共享内存的原理进行进程间通信的本质就是要让不同进程看到同一份资源那么共享内存又是如何做的呢请看下图 :上图包含物理内存、两个进程(进程A和B)以及每个进程对应的内核数据结构task_struct(进程描述符)、虚拟地址空间、页表。我们以进程 A 为例如果要使用共享内存顾名思义首先就要有内存即先使用某种手段在物理内存上申请内存这种手段就是系统调用首先通过系统调用向操作系统申请一块物理内存这块申请的物理内存就是共享内存。申请成功后操作系统会通过页表为进程 A 建立 “物理地址 ↔ 虚拟地址” 的映射关系将这块共享的物理内存映射到进程 A 虚拟地址空间的共享区并向用户层返回共享区的起始虚拟地址。接下来进程 B 要与进程 A 通信此时共享内存已存在进程 B 只需通过系统调用获取该共享内存操作系统同样会通过页表为进程 B 建立 “物理地址 ↔ 虚拟地址” 的映射关系将同一块物理内存映射到进程 B 虚拟地址空间的共享区并返回对应的起始虚拟地址。此时进程 A 和进程 B 虽为不同进程却能通过各自的虚拟地址访问同一块物理内存从而实现高效通信共享内存默认支持双向读写具体通信方向由不同场景决定。释放共享内存的步骤与建立过程相反进程调用系统调用(shmdt)操作系统清除页表中该进程对应的 “虚拟地址→物理地址” 映射关系将共享内存从进程虚拟地址空间中分离当所有进程都与共享内存分离后还要调用系统调用(shmctl)操作系统才会真正释放这块物理内存。上述操作不能由进程直接完成因为进程的虚拟地址空间是独立的若由进程自行申请内存该内存就应归属于当前进程从而导致其他进程无法访问。因此共享内存的创建、映射、释放等操作必须由操作系统内核亲自完成。操作系统通过一系列的系统调用接口如shmget、shmat、shmdt、shmctl向进程提供服务进程若要创建共享内存、挂载到地址空间等操作就要使用对应的系统调用由内核执行具体的内存管理和映射工作。共享内存的本质也就是让不同进程看到同一份资源并且一个进程创建另一个进程接受并使用这和命名管道是一样的。共享内存的本质共享内存的本质就是内核在物理内存中开辟的一块公共物理内存区域它是多个进程共享数据的载体不属于任何单个进程只由内核统一管理。进程视角的共享内存进程无法直接访问物理内存必须通过系统调用将这块物理内存映射到自己的虚拟地址空间得到一个虚拟地址指针之后读写这个指针就等价于读写这块共享物理内存。内核视角的共享内存内核会用一个结构体来描述这块物理内存的属性(大小、权限、挂载进程数等并通过 shmid 来标识它这个结构体 对应的物理内存共同构成了完整的 “共享内存对象”。共享内存的管理既然进程A和进程B会共享使用一块共享内存那么就会有其他的进程使用另一块共享内存所以共享内存就需要被操作系统统一管理。 管理的方式仍然是先描述再组织。先描述 : 内核会用一个结构体 (struct shmid_kernel来描述这块物理内存的属性(大小、权限、挂载进程数等)并通过shmid来标识它这个结构体 对应的物理内存共同构成了完整的 “共享内存对象”。再组织通过链表、数组等数据结构将这些结构体组织起来后续对共享内存的增、删、查、改操作就转化为对该数据结构的操作。三、系统调用接口介绍在介绍系统调用之前我们先梳理一下整个共享的流程:首先进程 A 会调用第一个系统调用接口 (shmget) 创建共享内存内核开辟一块物理内存即共享内存本体。需要注意的是是内核创建的共享内存并非进程 A。又因为进程看不到并且不会和这块共享物理内存打交道所以进程只能通过第二个系统调用接口 (shmat)将共享内存挂接到自己的虚拟地址空间从而得到虚拟地址的起始地址。下来进程 B 为了获取同一块共享内存进程 B 也使用这个系统调用接口挂接到自己的虚拟地址空间从而双方共用一块物理内存。下来准备工作已经完毕进程AB之间就可以进行通信(读写数据)通信完毕后双方就先要取消各自挂接的共享内存就涉及到第三个系统调用的接口 (shmdt)进程 A 先取消挂接接着进程 B 取消挂接两个进程都取消挂接之后。最后还要删除这块共享内存删除共享内存就要用到第四个系统调用接口 (shmctl)随后内核真正释放物理内存整个流程结束。shmget创建共享内存如何创建获取共享内存那么就是使用第一个系统调用shmget其中sh的意思是share共享m是memory内存shmget的意思即为共享内存获取分配的意思。shmget 的作用是在内核中分配或查找一块共享内存段并返回其唯一标识符 shmid。第一个参数 key 的类型是 key_t本质是一个整数通常是 32 位作用就是让不同的进程通过同一个 key 找到共享内存。key 相当于是“共享内存的名字/身份证号”不同进程只要约定好同一个 key就能定位到同一块共享内存。key 有两种常见生成方式:方式一是手动指定整数 : 直接写一个固定整数比如 0x666 或 1234写法简单测试方便。但是容易和系统中其他 IPC 资源冲突不适合工程使用。ftok() 函数生成 key 值方式二是用 ftok() 函数生成 :ftok() 的作用是把文件路径和项目标识组合成一个唯一的 IPC 键能极大降低冲突概率。第一个参数 const char *pathname 代表路径参数本质是指向一个已存在的文件/目录路径(必须真实存在不能是不存在的路径)。内核层面上 ftok() 会读取这个文件的 inode 号(文件系统中唯一标识文件的编号)作为生成 key 的基础信息。注意只要路径指向的是同一个文件(inode 不变)多次调用 ftok() 都会得到相同的 key。如果文件被删除重建inode 会变化生成的 key 也会改变。可以用普通文件或目录但必须保证通信双方都能访问到这个路径。第二个参数 int proj_id 用来项目标识参数本质是一个 8 位的标识符(取值范围是 0-255通常用字符如 a、b 表示。作用就是在同一个文件(同一个 inode)上区分不同的 IPC 资源。比如同一个文件可以生成多个 keyftok(path, a) 对应共享内存ftok(path, b) 对应消息队列互不冲突。注意只有低 8 位有效高位会被忽略所以用 aASCII 97)和用 0x61 效果完全一样。通信双方必须使用完全相同的 pathname 和 proj_id才能得到同一个 key进而打开同一段 IPC 资源。如果计算的这个key真的产生了冲突即与原有的svstemV系列的共享内存或者信号量消息队列中的原有的key值产生了冲突如何做很简单那么作为用户更换一个pathname或者proj_id即可。第二个参数 size 的类型是 size_t是无符号整数类型表示字节数量。size 的作用用来指定共享内存的大小。创建时是申请内存的依据获取已有共享内存时一般填 0 或不大于原大小的值。内核按 4KB(4096 字节)为一页向上取整分配物理内存申请1 ~ 4096字节实际会分配 1 页(4096 字节)申请4097 ~ 8192字节实际会分配 2 页(8192 字节)以此类推。但进程只能安全使用自己指定的 size 字节超出会导致段错误。共享内存大小在创建时固定后续不能扩容或缩容。第三个参数 shmflg 是用来设置共享内存的访问权限(类似文件权限)。控制 shmget 是用来创建新共享内存还是获取已有共享内存。它的类型是 int由权限位 创建标志按位或组成。权限位(低9位) 的格式是 0664、0644、0777等以 0 开头表示八进制。分别对应当前用户、同组用户、其他用户的读/写/执行 权限。常用权限是 0666 (所有进程均可读写)。创建标志位(控制行为):IPC_CREAT : 如果 key 对应的共享内存不存在就创建如果已存在就直接获取并返回 shmid。IPC_CREAT | IPC_EXCL : 必须是不存在才能创建成功如果已存在直接报错返回 -1。保证本次一定是新建不会拿到旧的共享内存。第三个参数的常见组合用法:IPC_CREAT | 0666 存在则获取不存在则创建。IPC_CREAT | IPC_EXCL | 0666 必须全新创建否则失败。直接传 0666 只获取已存在的不创建。返回值 : 创建成功返回一个非负整数 shmid即共享内存标识符后 shmat、shmdt、shmctl都靠它来标识这块共享内存。失败则返回 -1并通过 errno 错误码说明失败原因。shmid 的本质就是内核用来唯一标识一块共享内存的 ID 编号作用和文件系统中的inode 号非常相似inode 唯一标识一个文件shmid 唯一标识一块共享内存。在内核内部每一块共享内存都由一个结构体struct shmid_kernel来描述里面记录着各种共享内存的信息共享内存的大小、对应的物理内存起始地址、挂载进程数量、权限信息、创建时间、关联的 key 等。内核会用数组或类似结构管理所有共享内存对象shmid 本质上就是这个结构体在数组中的索引/编号内核通过它来定位、索引、操作对应的共享内存管理结构体进而操作这块物理内存。shmid和key对比key 偏向进程间约定是进程A和进程B商量好的一个编号目的是大家凭这个编号找到同一块共享内存属于进程之间的协议不是内核内部管理用的。shmid 完全是内核层面是内核自己分配、自己管理的编号内核用它找对应的结构体、物理内存进程拿到后只是“转手交给内核”自己不解释它。key 是进程之间约定的“用户层标识”shmid 是内核内部使用的“内核层标识”shmat挂接接口shmatshare memory attach即共享内存挂载是将内核中已创建的共享物理内存挂接映射到进程的虚拟地址空间让进程可以像访问普通内存一样读写共享内存。第一个参数是由 shmget 返回的共享内存 ID用来指定要挂载哪块共享内存。第二个参数指定把共享内存映射到进程虚拟地址空间的哪个位置。但是我们知道应该挂接到哪里吗实际上是不知道的所以这里我们设置为 nullptr 让内核自动选择合适的虚拟地址。第三个参数是挂载标志一般我们传默认值 0 表示以读写方式挂载共享内存即可。对于 shmat 的返回值 shmaddr 其实是操作系统在进程虚拟地址空间中为共享内存分配的起始虚拟地址。所以对于 shmat 的返回值我们是一定要接收的因为后续执行共享内存去挂载操作时需要使用该地址作为参数。返回值类型为 void*本身不具备可操作性无法直接解引用或下标访问。实际使用时需强制类型转换为具体指针类型若按字节流 / 字符串处理 → 转换为 char*若按自定义结构体处理 → 转换为对应结构体指针这与 malloc 返回 void* 后需强转的逻辑完全一致。此时我们可以将共享内存挂接到进程地址空间上了那么可不可以将共享内存在地址空间上去挂接操作呢可以的通过 shmdt 即可。shmdt去挂接shmdt 与 shmat 是一对互补操作用于将共享内存从进程的虚拟地址空间中分离去挂载。参数 shmaddr 就是由 shmat 返回的共享内存虚拟地址起始指针也就是进程之前挂载时得到的那个地址。返回值成功时返回 0表示共享内存已从当前进程虚拟地址空间分离。失败返回 -1并设置 errno 来指示错误原因(如传入的地址无效、未挂载过等)。shmctl关闭共享内存shmctl 是共享内存的控制操作接口ctl 就是 control 控制的意思用于查询、修改或删除共享内存段是整个共享内存生命周期的收尾操作。第一个参数 shmid 就是由 shmget 返回的共享内存标识符用于指定要操作的目标共享内存。第二个参数 cmd 是一个整数类型的命令它是一个宏定义通过宏定义不同位置的比特位传达不同的命令下面我们认识一下宏定义控制命令决定要执行的操作核心命令如下IPC_STAT 获取共享内存的状态信息将内核中的 shmid_ds 结构体拷贝到 buf 中。IPC_SET 修改共享内存的权限等属性需有权限将 buf 中的数据同步到内核。IPC_RMID 标记删除共享内存最常用将共享内存标记为待删除等待最后一个挂载进程分离后内核才会真正释放物理内存。第三个参数 buf 时指向 struct shmid_ds 结构体的指针用于传递或接收共享内存的状态信息若执行 IPC_RMID 关闭删除共享内存时通常传 NULL 。共享内存状态结构体 struct shmid_ds下面我们来看一个结构体 struct shmid_ds这个结构体就是共享内存状态结构体这个结构体是内核用来描述一块共享内存完整状态的数据结构也是 shmctl 中 IPC_STAT/IPC_SET 命令的核心数据载体。字段类型含义shm_permstruct ipc_perm共享内存的权限与归属信息包含创建者 UID、GID、访问权限位等类似文件的权限信息。shm_segszsize_t共享内存段的总大小字节数即shmget时指定的size。shm_atimetime_t最后一次 挂载shmat 的时间戳。shm_dtimetime_t最后一次 分离shmdt 的时间戳。shm_ctimetime_t共享内存创建时间或最后一次通过shmctl修改属性的时间戳。shm_cpidpid_t创建该共享内存进程的 PID。shm_lpidpid_t最后一次执行shmat或shmdt操作进程的 PID。shm_nattchshmatt_t当前挂载该共享内存的进程数量nattch number of attaches。与 shmctl 的配合使用 :IPC_STAT将内核中该共享内存的 shmid_ds 数据拷贝到用户提供的 buf 指针中用于查询状态。IPC_SET将用户 buf 中的部分字段如 shm_perm 权限同步到内核修改共享内存属性。IPC_RMID无需使用 buf传 NULL直接标记共享内存为待删除。shm_nattch 是判断共享内存是否还在被使用的核心字段当 nattch 为 0 且被标记为删除时内核才会释放物理内存。四、代码实现我们要实现一个基于 System V 共享内存的双进程通信 Demo核心目标是让两个独立进程通过一块共享内存完成高效数据传递 —— 一个进程负责写入数据另一个进程负责读取数据。为了让代码结构清晰、易于复用和维护我们将整个项目拆分为四个核心文件分别承担不同职责形成 “底层封装 业务逻辑 工程构建” 的分层架构。Shm.hpp作为共享内存的核心封装层将 shmget、shmat、shmdt、shmctl 等底层系统调用封装为面向对象的 Shm 类统一管理共享内存的创建、获取、挂载、分离和删除对外提供简洁易用的接口屏蔽底层细节。Server.cc作为共享内存的管理者与读端负责创建共享内存、循环读取其中的数据并在程序结束时完成共享内存的销毁是整个通信流程的 “资源主控方”。Client.cc作为共享内存的使用者与写端仅获取已创建的共享内存向其中写入用户输入的数据不参与共享内存的创建与销毁体现 “按需使用” 的设计思想。Makefile作为工程化构建脚本定义编译规则实现一键生成服务端与客户端可执行文件同时提供清理功能让项目构建更高效、规范。MakefileShm.hppCreate() 和 Get() 本质上都是通过 shmget 获取共享内存底层逻辑完全一致仅在创建标识 shmflg 上存在差异因此我们将生成 key、调用 shmget、错误处理、保存 shmid 等公共代码抽取到 GetHelper 函数中实现复用Create() 传入 IPC_CREAT | IPC_EXCL | 0666 用于创建全新共享内存Get() 传入 IPC_CREAT 用于获取已存在的共享内存两者通过传递不同参数共用同一套底层逻辑既简化了代码又让结构更加清晰统一。Server.cc服务端 Create ()创建共享内存是资源的所有者。一块共享内存只创建一次被多个进程 Get 使用。Client.cc客户端 Get ()获取共享内存是资源的使用者。运行:运行的同时使用监控脚本进行检测期望观察到挂接数字的变化:这个ipcs监控脚本用于实时查看系统共享内存状态输出信息包含七列核心字段key是进程间约定的查找标识shmid是内核分配的唯一操作 IDowner是共享内存所属用户perms是访问权限bytes是共享内存大小nattch是当前挂载进程数量挂载 1、分离 - 1status标记内存是否待删除。脚本通过每秒刷新输出让我们直观观察共享内存从创建、挂载、使用到分离、删除的完整生命周期变化。还要补充的是我们还可以在命令行中查看共享内存删除共享内存:命令行中查看共享内存使用的是ipcs -m命令行中删除共享内存使用的是ipcrm -m 共享内存标识符首先我们先运行服务端:这是运行 ./Server 后打印的共享内存初始化信息代表共享内存刚创建完成、还未被任何进程挂载的状态key0x66024001, _shmid 1key由 ftok(/tmp, 0x66) 生成的进程间约定键值用于让客户端定位到同一块共享内存。_shmid是内核为这块共享内存分配的唯一标识符 shmid后续挂载、分离、删除操作都依赖它。shm_nattch 是当前挂载这块共享内存的进程数。这里为 0说明共享内存已创建但还没有任何进程执行 Attach() 挂载符合刚创建后的初始状态。shm_segsz: 0x80 是共享内存的总大小字节。0x80 是十六进制等于十进制的 128和我们代码中定义的 const int gsize 128 完全对应验证了共享内存大小创建正确。服务端启动调用 Create() 创建共享内存 → nattch 0。调用 Attach() 挂载 → nattch 1。客户端启动调用 Get() 获取共享内存 → nattch 保持 1。调用 Attach() 挂载 → nattch 2两个进程同时挂载。客户端退出调用 Detach() 分离 → nattch 1只剩服务端挂载。服务端退出调用 Detach() 分离 → nattch 0。调用 Delete() 标记删除 → 共享内存被内核回收从列表中消失。这里的 shmid 是内核分配给共享内存的唯一序列号每次创建新的共享内存都会自动加 1即使旧的共享内存被删除编号也不会复用只会持续递增直到系统重启后才会重新从 0 开始计数以此保证每个共享内存的 ID 在系统内全局唯一。key 值是进程中用来定位共享内存的唯一标识由 ftok 生成在共享内存创建时就永久绑定它的存在与是否有进程挂载nattch 数量无关无论是否有进程使用只要共享内存未被内核删除key 值就一直存在用于让客户端和服务端能找到同一块共享内存。进行通信:上面我们所做的一切都是通信前的准备真正的关键是用共享内存来进行进程间通信所以下面我们分别要对Server.cc和Client.cc文件进行调正调整为一方发送信息一方接收信息从而实现通信功能:Server.cc :服务端作为共享内存的创建者和读取端首先创建内核中的物理内存并通过 Attach 将这块内存映射到自己进程的虚拟地址 _start_addr。服务端使用 cout shm_start 读取数据shm_start 是 char* 类型的指针代表共享内存的起始地址。cout 在识别到 char* 时会自动对地址进行解引用从指针指向的内存地址开始读取数据直到遇到结束符这个过程本质就是通过虚拟地址访问物理内存不需要显式使用 * 也能完成解引用操作从而直接获取客户端写入的数据。Client.cc :客户端作为共享内存的使用者和写入端通过 Get 获取服务端创建的物理内存并使用 Attach 将同一块内存映射到自己的虚拟地址 _start_addr。客户端使用 memset(shm_start, 0, size) 清空内存该函数会接收地址并对地址解引用将数据写入物理内存同时使用 strncpy(shm_start, data, size-1) 写入用户输入的数据该函数同样会对传入的地址进行解引用把数据直接存到指针指向的物理内存中。因为服务端和客户端映射同一块物理内存所以客户端通过解引用写入的数据服务端可以立刻通过解引用读取实现真正高效的进程间通信。memset(shm_start, 0, size在干嘛shm_start 是一个指针本质就是一个虚拟地址指向共享内存的起始位置。memset 函数的作用是把一段内存区域全部设为某个值这里是 0。它接收地址 shm_start然后对这个地址做解引用// 伪代码memset 内部大概是这样 for (int i 0; i size; i) { *(shm_start i) 0; // 对地址解引用把 0 写到物理内存里 }strncpy(shm_start, data, size-1在干嘛strncpy 是 “字符串拷贝” 函数作用是把一段字符串数据拷贝到目标地址。它接收目标地址 shm_start然后对这个地址做解引用就是把用户输入的字符串通过解引用直接写到共享内存的物理地址里// 伪代码strncpy 内部大概是这样 for (int i 0; i size-1 data[i] ! \0; i) { *(shm_start i) data[i]; // 对地址解引用把字符写到物理内存里 } *(shm_start size-1) \0; // 保证字符串结束共享内存之所以能通信核心就是两个进程映射到同一块物理内存并通过_start_addr解引用直接读写这块内存。_start_addr是共享内存的起始虚拟地址进程并不能直接用 “地址” 传递信息而是对这个地址进行解引用去访问地址指向的物理内存客户端对自己的_start_addr解引用写入数据数据就存进公共物理内存服务端对自己的_start_addr解引用读取数据就能直接拿到内容。两个进程的虚拟地址虽然不同但解引用后指向同一块物理内存一方写入、一方读取不需要任何数据拷贝这就是共享内存高效通信的核心原理。通信结果:客户端输入的nihao、1、2、3等数据能够实时、准确地传递到服务端并完整打印输出无乱码、无脏数据、无信息丢失。右侧ipcs显示挂载数为2证明服务端与客户端成功挂载同一块物理内存通过虚拟地址_start_addr的解引用操作实现了高效、实时、可靠的进程间通信整体效果符合预期。五、共享内存的特性1. 共享内存是进程间通信IPC中速度最快的方式其核心原因在于零数据拷贝。与管道等通信机制不同管道的数据需要经过 “用户层 → 内核缓冲区 → 用户层” 的两次拷贝而共享内存直接让进程映射到同一块物理内存。进程 A 和进程 B 通过各自的页表将同一块物理内存挂载到自己的虚拟地址空间中。进程 B 直接通过虚拟地址解引用向物理内存写入数据进程 A 则通过自己的虚拟地址解引用直接读取同一块内存中的数据数据没有在用户态和内核态之间反复搬运实现了最高效的直接数据传递。2. 除了零拷贝的高性能共享内存还具备两个关键特性一是缺乏原生的进程间协同与同步互斥机制这意味着操作系统不会自动保护数据一致性读写操作需要用户自行通过信号量等机制实现同步否则容易出现竞争问题二是数据格式与访问完全由用户自定义系统仅提供物理内存映射用户可以根据需求将共享内存当作字符数组、结构体或其他数据结构来使用自由控制数据的写入与读取格式。正是凭借 “零拷贝、高速、用户自定义” 的特点共享内存成为大数据传输、高频通信场景的首选 IPC 方式。共享内存为什么缺乏同步互斥共享内存的设计追求极致速度它仅提供物理内存映射让进程直接读写内存不涉及任何阻塞、等待或保护机制。多个进程可以同时访问同一块内存内核不会干预访问顺序因此共享内存没有原生同步与互斥能力必须依靠信号量、互斥锁等外部工具保证数据安全。与管道的本质区别管道是基于内核缓冲区的通信方式由内核管理数据的读写顺序自带同步、互斥与阻塞机制读空会等待、写满会阻塞数据不会被破坏。但管道需要内核拷贝数据速度较慢而共享内存无需拷贝、速度最快但完全没有同步保护需要用户自己管理进程访问秩序。六、总结本文介绍了SystemV共享内存通信机制的原理与实现。共享内存通过内核开辟公共物理内存区域由多个进程映射到各自虚拟地址空间实现高效通信是速度最快的IPC方式。文章详细讲解了共享内存的创建、挂接、读写和释放流程以及相关系统调用接口(shmget/shmat/shmdt/shmctl)的使用方法。通过代码示例展示了双进程通信的具体实现并分析了共享内存零拷贝、高速度但缺乏同步机制的特性。最后比较了共享内存与管道的本质区别指出共享内存适合大数据传输但需要额外同步机制保障数据安全。感谢大家的观看!
Linux 进程间通信(四)System V共享内存
目录一、什么是共享内存?二、共享内存的原理共享内存的本质共享内存的管理三、系统调用接口介绍shmget创建共享内存ftok() 函数生成 key 值shmid和key对比shmat挂接接口shmdt去挂接shmctl关闭共享内存共享内存状态结构体 struct shmid_ds四、代码实现MakefileShm.hppServer.ccClient.cc运行:进行通信:五、共享内存的特性六、总结前面我们学习了匿名管道和命名管道今天我们学习另一种通信方式 : System V共享内存通信方式一、什么是共享内存?共享内存就是由内核开辟一块公共物理内存多个进程将这块公共的物理内存映射到各自的进程虚拟地址空间进而实现共享从而直接读写同一块内存数据无需内核拷贝数据所以共享内也是速度最快的进程间通信方式。二、共享内存的原理进行进程间通信的本质就是要让不同进程看到同一份资源那么共享内存又是如何做的呢请看下图 :上图包含物理内存、两个进程(进程A和B)以及每个进程对应的内核数据结构task_struct(进程描述符)、虚拟地址空间、页表。我们以进程 A 为例如果要使用共享内存顾名思义首先就要有内存即先使用某种手段在物理内存上申请内存这种手段就是系统调用首先通过系统调用向操作系统申请一块物理内存这块申请的物理内存就是共享内存。申请成功后操作系统会通过页表为进程 A 建立 “物理地址 ↔ 虚拟地址” 的映射关系将这块共享的物理内存映射到进程 A 虚拟地址空间的共享区并向用户层返回共享区的起始虚拟地址。接下来进程 B 要与进程 A 通信此时共享内存已存在进程 B 只需通过系统调用获取该共享内存操作系统同样会通过页表为进程 B 建立 “物理地址 ↔ 虚拟地址” 的映射关系将同一块物理内存映射到进程 B 虚拟地址空间的共享区并返回对应的起始虚拟地址。此时进程 A 和进程 B 虽为不同进程却能通过各自的虚拟地址访问同一块物理内存从而实现高效通信共享内存默认支持双向读写具体通信方向由不同场景决定。释放共享内存的步骤与建立过程相反进程调用系统调用(shmdt)操作系统清除页表中该进程对应的 “虚拟地址→物理地址” 映射关系将共享内存从进程虚拟地址空间中分离当所有进程都与共享内存分离后还要调用系统调用(shmctl)操作系统才会真正释放这块物理内存。上述操作不能由进程直接完成因为进程的虚拟地址空间是独立的若由进程自行申请内存该内存就应归属于当前进程从而导致其他进程无法访问。因此共享内存的创建、映射、释放等操作必须由操作系统内核亲自完成。操作系统通过一系列的系统调用接口如shmget、shmat、shmdt、shmctl向进程提供服务进程若要创建共享内存、挂载到地址空间等操作就要使用对应的系统调用由内核执行具体的内存管理和映射工作。共享内存的本质也就是让不同进程看到同一份资源并且一个进程创建另一个进程接受并使用这和命名管道是一样的。共享内存的本质共享内存的本质就是内核在物理内存中开辟的一块公共物理内存区域它是多个进程共享数据的载体不属于任何单个进程只由内核统一管理。进程视角的共享内存进程无法直接访问物理内存必须通过系统调用将这块物理内存映射到自己的虚拟地址空间得到一个虚拟地址指针之后读写这个指针就等价于读写这块共享物理内存。内核视角的共享内存内核会用一个结构体来描述这块物理内存的属性(大小、权限、挂载进程数等并通过 shmid 来标识它这个结构体 对应的物理内存共同构成了完整的 “共享内存对象”。共享内存的管理既然进程A和进程B会共享使用一块共享内存那么就会有其他的进程使用另一块共享内存所以共享内存就需要被操作系统统一管理。 管理的方式仍然是先描述再组织。先描述 : 内核会用一个结构体 (struct shmid_kernel来描述这块物理内存的属性(大小、权限、挂载进程数等)并通过shmid来标识它这个结构体 对应的物理内存共同构成了完整的 “共享内存对象”。再组织通过链表、数组等数据结构将这些结构体组织起来后续对共享内存的增、删、查、改操作就转化为对该数据结构的操作。三、系统调用接口介绍在介绍系统调用之前我们先梳理一下整个共享的流程:首先进程 A 会调用第一个系统调用接口 (shmget) 创建共享内存内核开辟一块物理内存即共享内存本体。需要注意的是是内核创建的共享内存并非进程 A。又因为进程看不到并且不会和这块共享物理内存打交道所以进程只能通过第二个系统调用接口 (shmat)将共享内存挂接到自己的虚拟地址空间从而得到虚拟地址的起始地址。下来进程 B 为了获取同一块共享内存进程 B 也使用这个系统调用接口挂接到自己的虚拟地址空间从而双方共用一块物理内存。下来准备工作已经完毕进程AB之间就可以进行通信(读写数据)通信完毕后双方就先要取消各自挂接的共享内存就涉及到第三个系统调用的接口 (shmdt)进程 A 先取消挂接接着进程 B 取消挂接两个进程都取消挂接之后。最后还要删除这块共享内存删除共享内存就要用到第四个系统调用接口 (shmctl)随后内核真正释放物理内存整个流程结束。shmget创建共享内存如何创建获取共享内存那么就是使用第一个系统调用shmget其中sh的意思是share共享m是memory内存shmget的意思即为共享内存获取分配的意思。shmget 的作用是在内核中分配或查找一块共享内存段并返回其唯一标识符 shmid。第一个参数 key 的类型是 key_t本质是一个整数通常是 32 位作用就是让不同的进程通过同一个 key 找到共享内存。key 相当于是“共享内存的名字/身份证号”不同进程只要约定好同一个 key就能定位到同一块共享内存。key 有两种常见生成方式:方式一是手动指定整数 : 直接写一个固定整数比如 0x666 或 1234写法简单测试方便。但是容易和系统中其他 IPC 资源冲突不适合工程使用。ftok() 函数生成 key 值方式二是用 ftok() 函数生成 :ftok() 的作用是把文件路径和项目标识组合成一个唯一的 IPC 键能极大降低冲突概率。第一个参数 const char *pathname 代表路径参数本质是指向一个已存在的文件/目录路径(必须真实存在不能是不存在的路径)。内核层面上 ftok() 会读取这个文件的 inode 号(文件系统中唯一标识文件的编号)作为生成 key 的基础信息。注意只要路径指向的是同一个文件(inode 不变)多次调用 ftok() 都会得到相同的 key。如果文件被删除重建inode 会变化生成的 key 也会改变。可以用普通文件或目录但必须保证通信双方都能访问到这个路径。第二个参数 int proj_id 用来项目标识参数本质是一个 8 位的标识符(取值范围是 0-255通常用字符如 a、b 表示。作用就是在同一个文件(同一个 inode)上区分不同的 IPC 资源。比如同一个文件可以生成多个 keyftok(path, a) 对应共享内存ftok(path, b) 对应消息队列互不冲突。注意只有低 8 位有效高位会被忽略所以用 aASCII 97)和用 0x61 效果完全一样。通信双方必须使用完全相同的 pathname 和 proj_id才能得到同一个 key进而打开同一段 IPC 资源。如果计算的这个key真的产生了冲突即与原有的svstemV系列的共享内存或者信号量消息队列中的原有的key值产生了冲突如何做很简单那么作为用户更换一个pathname或者proj_id即可。第二个参数 size 的类型是 size_t是无符号整数类型表示字节数量。size 的作用用来指定共享内存的大小。创建时是申请内存的依据获取已有共享内存时一般填 0 或不大于原大小的值。内核按 4KB(4096 字节)为一页向上取整分配物理内存申请1 ~ 4096字节实际会分配 1 页(4096 字节)申请4097 ~ 8192字节实际会分配 2 页(8192 字节)以此类推。但进程只能安全使用自己指定的 size 字节超出会导致段错误。共享内存大小在创建时固定后续不能扩容或缩容。第三个参数 shmflg 是用来设置共享内存的访问权限(类似文件权限)。控制 shmget 是用来创建新共享内存还是获取已有共享内存。它的类型是 int由权限位 创建标志按位或组成。权限位(低9位) 的格式是 0664、0644、0777等以 0 开头表示八进制。分别对应当前用户、同组用户、其他用户的读/写/执行 权限。常用权限是 0666 (所有进程均可读写)。创建标志位(控制行为):IPC_CREAT : 如果 key 对应的共享内存不存在就创建如果已存在就直接获取并返回 shmid。IPC_CREAT | IPC_EXCL : 必须是不存在才能创建成功如果已存在直接报错返回 -1。保证本次一定是新建不会拿到旧的共享内存。第三个参数的常见组合用法:IPC_CREAT | 0666 存在则获取不存在则创建。IPC_CREAT | IPC_EXCL | 0666 必须全新创建否则失败。直接传 0666 只获取已存在的不创建。返回值 : 创建成功返回一个非负整数 shmid即共享内存标识符后 shmat、shmdt、shmctl都靠它来标识这块共享内存。失败则返回 -1并通过 errno 错误码说明失败原因。shmid 的本质就是内核用来唯一标识一块共享内存的 ID 编号作用和文件系统中的inode 号非常相似inode 唯一标识一个文件shmid 唯一标识一块共享内存。在内核内部每一块共享内存都由一个结构体struct shmid_kernel来描述里面记录着各种共享内存的信息共享内存的大小、对应的物理内存起始地址、挂载进程数量、权限信息、创建时间、关联的 key 等。内核会用数组或类似结构管理所有共享内存对象shmid 本质上就是这个结构体在数组中的索引/编号内核通过它来定位、索引、操作对应的共享内存管理结构体进而操作这块物理内存。shmid和key对比key 偏向进程间约定是进程A和进程B商量好的一个编号目的是大家凭这个编号找到同一块共享内存属于进程之间的协议不是内核内部管理用的。shmid 完全是内核层面是内核自己分配、自己管理的编号内核用它找对应的结构体、物理内存进程拿到后只是“转手交给内核”自己不解释它。key 是进程之间约定的“用户层标识”shmid 是内核内部使用的“内核层标识”shmat挂接接口shmatshare memory attach即共享内存挂载是将内核中已创建的共享物理内存挂接映射到进程的虚拟地址空间让进程可以像访问普通内存一样读写共享内存。第一个参数是由 shmget 返回的共享内存 ID用来指定要挂载哪块共享内存。第二个参数指定把共享内存映射到进程虚拟地址空间的哪个位置。但是我们知道应该挂接到哪里吗实际上是不知道的所以这里我们设置为 nullptr 让内核自动选择合适的虚拟地址。第三个参数是挂载标志一般我们传默认值 0 表示以读写方式挂载共享内存即可。对于 shmat 的返回值 shmaddr 其实是操作系统在进程虚拟地址空间中为共享内存分配的起始虚拟地址。所以对于 shmat 的返回值我们是一定要接收的因为后续执行共享内存去挂载操作时需要使用该地址作为参数。返回值类型为 void*本身不具备可操作性无法直接解引用或下标访问。实际使用时需强制类型转换为具体指针类型若按字节流 / 字符串处理 → 转换为 char*若按自定义结构体处理 → 转换为对应结构体指针这与 malloc 返回 void* 后需强转的逻辑完全一致。此时我们可以将共享内存挂接到进程地址空间上了那么可不可以将共享内存在地址空间上去挂接操作呢可以的通过 shmdt 即可。shmdt去挂接shmdt 与 shmat 是一对互补操作用于将共享内存从进程的虚拟地址空间中分离去挂载。参数 shmaddr 就是由 shmat 返回的共享内存虚拟地址起始指针也就是进程之前挂载时得到的那个地址。返回值成功时返回 0表示共享内存已从当前进程虚拟地址空间分离。失败返回 -1并设置 errno 来指示错误原因(如传入的地址无效、未挂载过等)。shmctl关闭共享内存shmctl 是共享内存的控制操作接口ctl 就是 control 控制的意思用于查询、修改或删除共享内存段是整个共享内存生命周期的收尾操作。第一个参数 shmid 就是由 shmget 返回的共享内存标识符用于指定要操作的目标共享内存。第二个参数 cmd 是一个整数类型的命令它是一个宏定义通过宏定义不同位置的比特位传达不同的命令下面我们认识一下宏定义控制命令决定要执行的操作核心命令如下IPC_STAT 获取共享内存的状态信息将内核中的 shmid_ds 结构体拷贝到 buf 中。IPC_SET 修改共享内存的权限等属性需有权限将 buf 中的数据同步到内核。IPC_RMID 标记删除共享内存最常用将共享内存标记为待删除等待最后一个挂载进程分离后内核才会真正释放物理内存。第三个参数 buf 时指向 struct shmid_ds 结构体的指针用于传递或接收共享内存的状态信息若执行 IPC_RMID 关闭删除共享内存时通常传 NULL 。共享内存状态结构体 struct shmid_ds下面我们来看一个结构体 struct shmid_ds这个结构体就是共享内存状态结构体这个结构体是内核用来描述一块共享内存完整状态的数据结构也是 shmctl 中 IPC_STAT/IPC_SET 命令的核心数据载体。字段类型含义shm_permstruct ipc_perm共享内存的权限与归属信息包含创建者 UID、GID、访问权限位等类似文件的权限信息。shm_segszsize_t共享内存段的总大小字节数即shmget时指定的size。shm_atimetime_t最后一次 挂载shmat 的时间戳。shm_dtimetime_t最后一次 分离shmdt 的时间戳。shm_ctimetime_t共享内存创建时间或最后一次通过shmctl修改属性的时间戳。shm_cpidpid_t创建该共享内存进程的 PID。shm_lpidpid_t最后一次执行shmat或shmdt操作进程的 PID。shm_nattchshmatt_t当前挂载该共享内存的进程数量nattch number of attaches。与 shmctl 的配合使用 :IPC_STAT将内核中该共享内存的 shmid_ds 数据拷贝到用户提供的 buf 指针中用于查询状态。IPC_SET将用户 buf 中的部分字段如 shm_perm 权限同步到内核修改共享内存属性。IPC_RMID无需使用 buf传 NULL直接标记共享内存为待删除。shm_nattch 是判断共享内存是否还在被使用的核心字段当 nattch 为 0 且被标记为删除时内核才会释放物理内存。四、代码实现我们要实现一个基于 System V 共享内存的双进程通信 Demo核心目标是让两个独立进程通过一块共享内存完成高效数据传递 —— 一个进程负责写入数据另一个进程负责读取数据。为了让代码结构清晰、易于复用和维护我们将整个项目拆分为四个核心文件分别承担不同职责形成 “底层封装 业务逻辑 工程构建” 的分层架构。Shm.hpp作为共享内存的核心封装层将 shmget、shmat、shmdt、shmctl 等底层系统调用封装为面向对象的 Shm 类统一管理共享内存的创建、获取、挂载、分离和删除对外提供简洁易用的接口屏蔽底层细节。Server.cc作为共享内存的管理者与读端负责创建共享内存、循环读取其中的数据并在程序结束时完成共享内存的销毁是整个通信流程的 “资源主控方”。Client.cc作为共享内存的使用者与写端仅获取已创建的共享内存向其中写入用户输入的数据不参与共享内存的创建与销毁体现 “按需使用” 的设计思想。Makefile作为工程化构建脚本定义编译规则实现一键生成服务端与客户端可执行文件同时提供清理功能让项目构建更高效、规范。MakefileShm.hppCreate() 和 Get() 本质上都是通过 shmget 获取共享内存底层逻辑完全一致仅在创建标识 shmflg 上存在差异因此我们将生成 key、调用 shmget、错误处理、保存 shmid 等公共代码抽取到 GetHelper 函数中实现复用Create() 传入 IPC_CREAT | IPC_EXCL | 0666 用于创建全新共享内存Get() 传入 IPC_CREAT 用于获取已存在的共享内存两者通过传递不同参数共用同一套底层逻辑既简化了代码又让结构更加清晰统一。Server.cc服务端 Create ()创建共享内存是资源的所有者。一块共享内存只创建一次被多个进程 Get 使用。Client.cc客户端 Get ()获取共享内存是资源的使用者。运行:运行的同时使用监控脚本进行检测期望观察到挂接数字的变化:这个ipcs监控脚本用于实时查看系统共享内存状态输出信息包含七列核心字段key是进程间约定的查找标识shmid是内核分配的唯一操作 IDowner是共享内存所属用户perms是访问权限bytes是共享内存大小nattch是当前挂载进程数量挂载 1、分离 - 1status标记内存是否待删除。脚本通过每秒刷新输出让我们直观观察共享内存从创建、挂载、使用到分离、删除的完整生命周期变化。还要补充的是我们还可以在命令行中查看共享内存删除共享内存:命令行中查看共享内存使用的是ipcs -m命令行中删除共享内存使用的是ipcrm -m 共享内存标识符首先我们先运行服务端:这是运行 ./Server 后打印的共享内存初始化信息代表共享内存刚创建完成、还未被任何进程挂载的状态key0x66024001, _shmid 1key由 ftok(/tmp, 0x66) 生成的进程间约定键值用于让客户端定位到同一块共享内存。_shmid是内核为这块共享内存分配的唯一标识符 shmid后续挂载、分离、删除操作都依赖它。shm_nattch 是当前挂载这块共享内存的进程数。这里为 0说明共享内存已创建但还没有任何进程执行 Attach() 挂载符合刚创建后的初始状态。shm_segsz: 0x80 是共享内存的总大小字节。0x80 是十六进制等于十进制的 128和我们代码中定义的 const int gsize 128 完全对应验证了共享内存大小创建正确。服务端启动调用 Create() 创建共享内存 → nattch 0。调用 Attach() 挂载 → nattch 1。客户端启动调用 Get() 获取共享内存 → nattch 保持 1。调用 Attach() 挂载 → nattch 2两个进程同时挂载。客户端退出调用 Detach() 分离 → nattch 1只剩服务端挂载。服务端退出调用 Detach() 分离 → nattch 0。调用 Delete() 标记删除 → 共享内存被内核回收从列表中消失。这里的 shmid 是内核分配给共享内存的唯一序列号每次创建新的共享内存都会自动加 1即使旧的共享内存被删除编号也不会复用只会持续递增直到系统重启后才会重新从 0 开始计数以此保证每个共享内存的 ID 在系统内全局唯一。key 值是进程中用来定位共享内存的唯一标识由 ftok 生成在共享内存创建时就永久绑定它的存在与是否有进程挂载nattch 数量无关无论是否有进程使用只要共享内存未被内核删除key 值就一直存在用于让客户端和服务端能找到同一块共享内存。进行通信:上面我们所做的一切都是通信前的准备真正的关键是用共享内存来进行进程间通信所以下面我们分别要对Server.cc和Client.cc文件进行调正调整为一方发送信息一方接收信息从而实现通信功能:Server.cc :服务端作为共享内存的创建者和读取端首先创建内核中的物理内存并通过 Attach 将这块内存映射到自己进程的虚拟地址 _start_addr。服务端使用 cout shm_start 读取数据shm_start 是 char* 类型的指针代表共享内存的起始地址。cout 在识别到 char* 时会自动对地址进行解引用从指针指向的内存地址开始读取数据直到遇到结束符这个过程本质就是通过虚拟地址访问物理内存不需要显式使用 * 也能完成解引用操作从而直接获取客户端写入的数据。Client.cc :客户端作为共享内存的使用者和写入端通过 Get 获取服务端创建的物理内存并使用 Attach 将同一块内存映射到自己的虚拟地址 _start_addr。客户端使用 memset(shm_start, 0, size) 清空内存该函数会接收地址并对地址解引用将数据写入物理内存同时使用 strncpy(shm_start, data, size-1) 写入用户输入的数据该函数同样会对传入的地址进行解引用把数据直接存到指针指向的物理内存中。因为服务端和客户端映射同一块物理内存所以客户端通过解引用写入的数据服务端可以立刻通过解引用读取实现真正高效的进程间通信。memset(shm_start, 0, size在干嘛shm_start 是一个指针本质就是一个虚拟地址指向共享内存的起始位置。memset 函数的作用是把一段内存区域全部设为某个值这里是 0。它接收地址 shm_start然后对这个地址做解引用// 伪代码memset 内部大概是这样 for (int i 0; i size; i) { *(shm_start i) 0; // 对地址解引用把 0 写到物理内存里 }strncpy(shm_start, data, size-1在干嘛strncpy 是 “字符串拷贝” 函数作用是把一段字符串数据拷贝到目标地址。它接收目标地址 shm_start然后对这个地址做解引用就是把用户输入的字符串通过解引用直接写到共享内存的物理地址里// 伪代码strncpy 内部大概是这样 for (int i 0; i size-1 data[i] ! \0; i) { *(shm_start i) data[i]; // 对地址解引用把字符写到物理内存里 } *(shm_start size-1) \0; // 保证字符串结束共享内存之所以能通信核心就是两个进程映射到同一块物理内存并通过_start_addr解引用直接读写这块内存。_start_addr是共享内存的起始虚拟地址进程并不能直接用 “地址” 传递信息而是对这个地址进行解引用去访问地址指向的物理内存客户端对自己的_start_addr解引用写入数据数据就存进公共物理内存服务端对自己的_start_addr解引用读取数据就能直接拿到内容。两个进程的虚拟地址虽然不同但解引用后指向同一块物理内存一方写入、一方读取不需要任何数据拷贝这就是共享内存高效通信的核心原理。通信结果:客户端输入的nihao、1、2、3等数据能够实时、准确地传递到服务端并完整打印输出无乱码、无脏数据、无信息丢失。右侧ipcs显示挂载数为2证明服务端与客户端成功挂载同一块物理内存通过虚拟地址_start_addr的解引用操作实现了高效、实时、可靠的进程间通信整体效果符合预期。五、共享内存的特性1. 共享内存是进程间通信IPC中速度最快的方式其核心原因在于零数据拷贝。与管道等通信机制不同管道的数据需要经过 “用户层 → 内核缓冲区 → 用户层” 的两次拷贝而共享内存直接让进程映射到同一块物理内存。进程 A 和进程 B 通过各自的页表将同一块物理内存挂载到自己的虚拟地址空间中。进程 B 直接通过虚拟地址解引用向物理内存写入数据进程 A 则通过自己的虚拟地址解引用直接读取同一块内存中的数据数据没有在用户态和内核态之间反复搬运实现了最高效的直接数据传递。2. 除了零拷贝的高性能共享内存还具备两个关键特性一是缺乏原生的进程间协同与同步互斥机制这意味着操作系统不会自动保护数据一致性读写操作需要用户自行通过信号量等机制实现同步否则容易出现竞争问题二是数据格式与访问完全由用户自定义系统仅提供物理内存映射用户可以根据需求将共享内存当作字符数组、结构体或其他数据结构来使用自由控制数据的写入与读取格式。正是凭借 “零拷贝、高速、用户自定义” 的特点共享内存成为大数据传输、高频通信场景的首选 IPC 方式。共享内存为什么缺乏同步互斥共享内存的设计追求极致速度它仅提供物理内存映射让进程直接读写内存不涉及任何阻塞、等待或保护机制。多个进程可以同时访问同一块内存内核不会干预访问顺序因此共享内存没有原生同步与互斥能力必须依靠信号量、互斥锁等外部工具保证数据安全。与管道的本质区别管道是基于内核缓冲区的通信方式由内核管理数据的读写顺序自带同步、互斥与阻塞机制读空会等待、写满会阻塞数据不会被破坏。但管道需要内核拷贝数据速度较慢而共享内存无需拷贝、速度最快但完全没有同步保护需要用户自己管理进程访问秩序。六、总结本文介绍了SystemV共享内存通信机制的原理与实现。共享内存通过内核开辟公共物理内存区域由多个进程映射到各自虚拟地址空间实现高效通信是速度最快的IPC方式。文章详细讲解了共享内存的创建、挂接、读写和释放流程以及相关系统调用接口(shmget/shmat/shmdt/shmctl)的使用方法。通过代码示例展示了双进程通信的具体实现并分析了共享内存零拷贝、高速度但缺乏同步机制的特性。最后比较了共享内存与管道的本质区别指出共享内存适合大数据传输但需要额外同步机制保障数据安全。感谢大家的观看!