在Linux进程间通信(IPC)的诸多方案中,共享内存始终以“极致高效”的标签占据特殊地位。相较于管道、消息队列等需要频繁在用户态与内核态之间拷贝数据的机制,共享内存直接让多个进程访问同一块物理内存区域,彻底规避了数据拷贝的性能损耗,成为高并发、大数据量场景下的优选方案。但高效的背后,是内核对内存资源的精密调度与进程地址空间的巧妙映射。
不少开发者在实际应用中,往往只关注API的调用,却对“共享内存如何被内核创建与管理”“进程如何将共享内存映射到自身虚拟地址空间”“写时复制(COW)等机制如何保障内存高效利用”等底层逻辑一知半解。这也导致在面对复杂场景的性能优化或问题排查时难以突破瓶颈。本文将从底层逻辑出发,逐层拆解Linux共享内存的实现机制:从内核数据结构的管理逻辑,到虚拟内存与物理内存的映射原理,再到核心机制的运行细节,带你跳出API的表层认知,真正理解共享内存高效运行的底层密码。
一、什么是进程间通信(IPC)
1.1 进程的独立性与隔离性
在 Linux 系统中,进程是资源分配和调度的基本单位,其设计初衷是相互独立。每个进程都拥有自己独立的地址空间,就像是一个个独立的小世界,彼此之间相互隔离。这意味着一个进程无法直接访问另一个进程的内存空间、文件描述符等资源。例如,当我们在系统中同时运行浏览器和文本编辑器这两个进程时,浏览器进程不能直接读取或修改文本编辑器进程正在处理的文档数据,反之亦然。这种独立性和隔离性为系统的稳定性和安全性提供了保障。
1.2 IPC 的必要性
然而,在实际的应用场景中,进程之间往往需要相互协作来完成复杂的任务。比如,在一个服务器程序中,可能有多个进程分别负责处理不同的业务逻辑。为了解决进程之间由于隔离性导致的无法直接交互的问题,就需要一种机制来实现进程间的数据交换、资源共享和工作协调,这就是进程间通信(IPC,Inter-Process Communication)机制存在的必要性。
1.3 常见 IPC 方式简介
Linux 系统提供了多种常见的 IPC 方式,每种方式都有其特点和适用场景:
- 管道(Pipe):包括匿名管道和命名管道。匿名管道通常用于具有亲缘关系的进程之间,如父子进程,它是一种半双工的通信方式,数据只能单向流动。例如,在 Shell 脚本中,我们可以使用
ls | grep "test" 这样的命令,其中 | 就是匿名管道。命名管道则突破了亲缘关系的限制,可以在任意进程之间通信,它在文件系统中以特殊文件的形式存在。
- 消息队列(Message Queue):进程可以向消息队列中发送消息,也可以从消息队列中读取消息。消息队列中的消息具有一定的格式和优先级,它可以实现异步通信。
- 信号量(Semaphore):主要用于控制多个进程对共享资源的访问,它本质上是一个计数器,通过对计数器的操作来实现对资源的同步访问。例如,当多个进程需要访问同一个文件时,可以使用信号量来确保同一时间只有一个进程能够对文件进行写操作。
- 共享内存(Shared Memory):多个进程可以共享同一块内存区域,这使得它们可以直接读写这块内存中的数据,无需进行数据复制,因此是一种高效的 IPC 方式,特别适合需要频繁进行大量数据传输和共享的场景。
- 信号(Signal):是一种异步通知机制,用于通知进程发生了某种特定事件,如用户按下
Ctrl+C 组合键会向当前进程发送 SIGINT 信号。
在这些常见的 IPC 方式中,共享内存的优势十分明显。与管道和消息队列相比,共享内存不需要在进程间复制数据,减少了数据传输的开销;和信号量相比,共享内存更侧重于数据的共享。在大数据处理、实时通信等对数据传输效率要求极高的场景中,共享内存的高性能特点使其成为首选的 IPC 方式。
二、Linux 共享内存初相识
2.1 定义与概念
共享内存,简单来说,就是一段被多个进程共同映射到各自地址空间的物理内存区域。在 Linux 系统中,每个进程都有自己独立的虚拟地址空间,正常情况下,它们无法直接访问其他进程的内存。但共享内存打破了这种限制,通过操作系统的特殊映射机制,让多个进程能够看到并访问同一块物理内存。
举个形象的例子,假如每个进程都是一个独立的房间,进程的地址空间就是房间里摆放物品的空间。而共享内存就像是一个大家都可以进入的公共储物间,不同房间(进程)的人都能直接进去存放和取用物品(数据)。在一个多进程的视频编辑软件中,视频解码进程可以将解码后的视频帧数据直接存入共享内存,然后视频特效处理进程和视频合成进程都能从这个共享内存中读取这些视频帧数据,进行后续的处理。

对于上图我的理解是:当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们需要使用信号量来实现同步与互斥。
对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。
2.2 为什么共享内存是最快的 IPC 方式
在所有的 IPC 方式中,共享内存之所以速度最快,其核心原因在于数据的读写方式。以管道通信为例,当一个进程向管道写入数据时,数据需要从用户空间复制到内核空间的管道缓冲区;而另一个进程从管道读取数据时,又要将数据从内核空间的管道缓冲区复制回用户空间。这个过程涉及两次数据拷贝,会消耗较多的时间和系统资源。
而共享内存则不同,多个进程直接对同一块物理内存进行读写操作,无需在用户空间和内核空间之间进行数据拷贝。这就好比在接力比赛中,传统的 IPC 方式就像是运动员要把接力棒先交给裁判(内核),再由裁判转交给下一个运动员,而共享内存则是运动员之间直接交接接力棒,大大减少了数据传输的中间环节,从而显著提高了通信效率。在实时图像识别系统中,图像采集进程将采集到的图像数据直接存入共享内存,图像识别进程可以立即从共享内存中读取数据进行分析,几乎没有数据传输的延迟。
2.3 共享内存与其他 IPC 方式的比较
- 通信速度:与管道和消息队列相比,共享内存无需数据拷贝,速度优势明显。消息队列在数据传输时,需要在用户态和内核态之间进行多次数据复制,并且消息队列的管理也会带来一定的开销。而共享内存直接在物理内存上进行操作,数据传输几乎是瞬间完成的。
- 数据量:共享内存适合传输大量数据,因为它可以直接访问物理内存,不受消息大小等限制。而管道和消息队列通常会有数据大小的限制。在处理大型数据库查询结果的共享时,共享内存可以轻松应对大量数据的传输。
- 同步机制:共享内存本身不提供同步机制,需要用户自己实现同步,比如使用信号量来控制对共享内存的访问,防止数据冲突。而管道和消息队列在一定程度上提供了同步机制。在多进程并发访问共享内存时,如果没有正确的同步机制,就可能出现数据不一致的问题。
从这些方面的比较可以看出,共享内存以其高效的数据传输能力,在对通信速度和数据量要求较高的场景中具有独特的优势。
三、共享内存的映射原理
3.1 虚拟地址与物理地址映射基础
在深入了解共享内存的映射原理之前,我们先来回顾一下虚拟地址与物理地址的映射基础。在现代操作系统中,为了实现进程的独立性和内存的有效管理,每个进程都被分配了一个独立的虚拟地址空间。以 32 位操作系统为例,虚拟地址空间的大小为 2^32 字节,即 4GB。
但实际上,物理内存的大小往往远小于虚拟地址空间的大小,并且多个进程需要共享物理内存。为了实现虚拟地址到物理地址的转换,操作系统引入了页表(Page Table)这一关键数据结构。页表是一个存储在内存中的表格,它记录了虚拟地址与物理地址之间的映射关系。
具体来说,虚拟地址空间被划分为一个个固定大小的页(Page),在 Linux 系统中,常见的页大小为 4KB。同样,物理内存也被划分为大小相同的页帧(Page Frame)。页表中的每一项称为页表项(Page Table Entry,PTE),每个页表项对应一个虚拟页,其中记录了该虚拟页对应的物理页帧的地址,以及一些访问权限标志。
当进程访问一个虚拟地址时,CPU 会首先根据虚拟地址的页号在页表中查找对应的页表项。如果找到有效的页表项,就可以从中获取到对应的物理页帧地址,再结合虚拟地址的页内偏移,就能得到最终的物理地址,从而完成内存访问。如果页表项无效,说明该虚拟地址对应的物理内存尚未分配或不在内存中,这时会触发缺页中断。
在内核中,共享内存的管理依赖于一系列的数据结构,其中最重要的是 shmid_ds 和 ipc_perm。shmid_ds 结构体用于描述一个共享内存段的相关信息,其定义大致如下:
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; /* 创建该共享内存段的进程ID */
pid_t shm_lpid; /* 最后一次对共享内存段执行操作(attach或detach)的进程ID */
shmatt_t shm_nattch;/* 当前连接到该共享内存段的进程数 */
...
};
shm_perm:是一个 ipc_perm 类型的结构体,用于描述共享内存的权限和所有者信息,包括所有者 ID、组 ID、权限标志等。
shm_segsz:记录了共享内存段的大小,内核根据这个大小来分配和管理物理内存。
shm_atime、shm_dtime 和 shm_ctime:分别记录了共享内存段的最后一次被 attach、detach 以及创建或最后一次修改的时间。
shm_cpid 和 shm_lpid:分别保存了创建该共享内存段的进程 ID 和最后一次对共享内存段执行操作(attach 或 detach)的进程 ID。
shm_nattch:表示当前连接到该共享内存段的进程数,这是一个非常重要的计数,当这个计数变为 0 时,内核知道没有进程在使用该共享内存段了,就可以考虑释放相关的资源。
ipc_perm 结构体定义如下:
struct ipc_perm {
key_t key; /* 共享内存的键值,用于唯一标识共享内存段 */
uid_t uid; /* 所有者的用户ID */
gid_t gid; /* 所有者的组ID */
uid_t cuid; /* 创建者的用户ID */
gid_t cgid; /* 创建者的组ID */
unsigned short mode; /* 共享内存的访问权限,类似文件权限 */
unsigned short seq; /* 序列号,用于标识共享内存对象的版本 */
};
通过 ipc_perm 结构体,内核可以有效地管理共享内存的访问权限,确保只有授权的进程能够访问共享内存。
3.2 共享内存的映射机制
共享内存的映射机制是将物理内存中的共享内存段映射到进程的虚拟地址空间,使得进程可以像访问普通内存一样访问共享内存。这个过程涉及到内存管理单元(MMU)和页表的协同工作。
在 Linux 系统中,每个进程都有自己独立的虚拟地址空间,当进程调用 shmat 函数将共享内存映射到自身地址空间时,内核会进行以下操作:
- 查找共享内存段:内核首先根据
shmat 函数传入的共享内存标识符 shmid,在系统维护的共享内存段列表中查找对应的 shmid_ds 结构体。
- 分配虚拟地址空间:内核为共享内存分配一段虚拟地址空间,通常会选择一个合适的空闲区域。如果用户在调用
shmat 时指定了 shmaddr(一般为 NULL,表示由系统自动选择),内核会尝试将共享内存映射到指定地址。
- 建立页表映射:确定好虚拟地址后,内核通过 MMU 建立虚拟地址与共享内存物理地址之间的映射关系,这一过程涉及到修改进程的页表。对于共享内存,内核会在进程的页表中添加相应的表项,将虚拟地址与共享内存的物理页帧号关联起来。
- 设置访问权限:根据
shmid_ds 中 shm_perm 结构体的权限设置以及 shmat 函数传入的 shmflg 标志,内核设置页表中对应表项的访问权限位。
通过以上步骤,共享内存成功地映射到了进程的虚拟地址空间,进程就可以通过访问虚拟地址来读写共享内存中的数据。
3.3 内核与用户态的交互
用户态进程通过系统调用(如 shmget、shmat、shmdt、shmctl 等)与内核交互来实现共享内存的操作。
以 shmget 系统调用为例,当用户态进程调用 shmget 时,会发生以下过程:
- 陷入内核态:用户态进程执行
shmget 函数,触发一个软中断,使得 CPU 从用户态切换到内核态。
- 参数传递:用户态进程传入的参数通过特定的寄存器或栈传递给内核。
- 内核处理:内核中的共享内存管理模块会根据传入的参数执行相应的操作。如果是创建共享内存,会在系统的共享内存段列表中添加新的
shmid_ds 结构体。
- 返回用户态:内核完成操作后,将结果传递回用户态进程,然后通过中断返回指令将 CPU 从内核态切换回用户态。
同样,对于 shmat、shmdt 和 shmctl 等系统调用,用户态进程也是通过类似的方式与内核交互。这种内核与用户态的交互机制,保证了共享内存的安全、高效管理和使用。
四、Linux共享内存的实现机制
4.1 共享内存在内核中的数据结构
在 Linux 内核中,共享内存的管理依赖于一系列精心设计的数据结构,其中 struct shmid_ds 和 struct shmid_kernel 是两个关键的结构体。
(1)struct shmid_ds 主要用于存储用户可见的共享内存元信息,其定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* 操作权限,包含UID、GID、模式等 */
size_t shm_segsz; /* 共享内存段的大小(字节) */
time_t shm_atime; /* 最后一次映射时间 */
time_t shm_dtime; /* 最后一次解除映射时间 */
time_t shm_ctime; /* 最后一次修改时间 */
pid_t shm_cpid; /* 创建者的PID */
pid_t shm_lpid; /* 最后一次操作者的PID */
unsigned short shm_nattch;/* 当前映射到该共享内存段的进程数 */
// 一些兼容性保留字段
unsigned short shm_unused;
void *shm_unused2;
void *shm_unused3;
};
shm_perm:这个子结构体用于控制共享内存的访问权限。
shm_segsz:明确指定了共享内存段的大小,以字节为单位。
shm_atime、shm_dtime 和 shm_ctime:分别记录了共享内存最后一次被映射、解除映射和修改的时间。
shm_cpid 和 shm_lpid:记录了共享内存的创建者和最后一次操作它的进程的 PID。
shm_nattch:表示当前有多少个进程映射到了该共享内存段。这个字段对于内核判断是否可以安全地销毁共享内存非常重要。
(2)struct shmid_kernel 则是内核私有的管理结构体,包含了一些内核级别的字段:
struct shmid_kernel {
struct kern_ipc_perm shm_perm; // IPC权限控制块,包含键值(key)、所有者UID等
struct file *shm_file; // 关联的shm文件对象,实现物理内存与文件系统的关联
unsigned long shm_nattch; // 映射计数
size_t shm_segsz; // 段大小
struct pid *shm_cprid; // 创建者PID(内核态)
struct pid *shm_lprid; // 最后操作者PID
// 其他内核级字段
};
shm_perm(内核版):与 struct shmid_ds 中的 shm_perm 类似,但包含了更多内核管理所需的信息,比如共享内存的键值。
shm_file:这是一个指向虚拟文件系统(VFS)中与共享内存相关的文件对象的指针。通过这个指针,内核将共享内存与文件系统关联起来,实现了共享内存的文件系统式管理。
shm_nattch(内核版):同样用于记录映射到共享内存的进程数。
这些数据结构紧密协作,构成了共享内存在内核中的管理体系。
4.2 共享内存的创建与销毁流程
(1)创建过程。 在 Linux 系统中,创建共享内存主要通过 shmget 函数来完成,其函数原型为:
int shmget(key_t key, size_t size, int shmflg);
key:是一个整数值,用于标识共享内存段。通常可以使用 ftok 函数生成。
size:指定了要创建的共享内存段的大小,以字节为单位。这个大小必须是系统页面大小的整数倍。
shmflg:是一组标志位,常用的有 IPC_CREAT 和 IPC_EXCL。
当进程调用 shmget 函数创建共享内存时,内核会执行以下操作:
- 检查键值:内核首先检查系统中是否已经存在具有相同
key 值的共享内存段。
- 分配物理内存:如果系统中不存在,内核会为新的共享内存段分配物理内存。
- 初始化数据结构:内核创建一个新的
struct shmid_kernel 和 struct shmid_ds 结构体实例。
- 返回标识符:完成上述操作后,内核返回新创建的共享内存段的标识符
shmid。
(2)销毁过程。 删除共享内存使用 shmctl 函数,其函数原型为:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
当 cmd 参数为 IPC_RMID 时,表示要删除共享内存段。
当进程调用 shmctl 函数删除共享内存时,内核会进行如下判断和资源回收操作:
- 检查引用计数:内核首先检查
shm_nattch 字段,查看当前是否还有进程映射到该共享内存段。如果大于 0,内核不会立即删除,而是将其标记为待删除状态。
- 回收物理内存:当
shm_nattch 为 0 时,内核开始回收共享内存占用的物理内存。
- 释放数据结构:内核释放为共享内存段创建的相关结构体实例以及资源。
4.3 进程如何附加(attach)和分离(detach)共享内存
(1)附加过程。 进程将共享内存映射到自己的地址空间是通过 shmat 函数实现的,其函数原型为:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:是共享内存的标识符。
shmaddr:指定共享内存段连接到进程地址空间的起始地址。通常设置为 NULL。
shmflg:是一组标志位,通常设置为 0。
当进程调用 shmat 函数附加共享内存时,具体步骤如下:
- 查找共享内存信息:内核根据传入的
shmid,查找对应的管理结构体。
- 分配虚拟地址空间:内核在进程的虚拟地址空间中查找一段合适的空闲区域。
- 建立映射关系:内核更新进程的页表,将共享内存的物理页帧与进程虚拟地址空间中分配的区域建立映射关系。
- 更新引用计数:内核将
shm_nattch 字段加 1。
- 返回映射地址:内核返回共享内存映射到进程地址空间的起始地址。
(2)分离过程。 进程将共享内存从自己的地址空间中分离出来使用 shmdt 函数,其函数原型为:
int shmdt(const void *shmaddr);
shmaddr 是之前通过 shmat 函数连接共享内存段时返回的地址指针。
当进程调用 shmdt 函数分离共享内存时,内核会进行以下处理:
- 解除映射关系:内核根据传入的
shmaddr,在进程的页表中查找对应的页表项并删除,解除映射关系。
- 更新引用计数:内核将
shm_nattch 字段减 1。
- 检查是否删除共享内存:如果
shm_nattch 减为 0,且共享内存段已经被标记为待删除状态,内核会执行删除操作。
4.4 写时拷贝(Copy-on-Write)机制在共享内存中的应用
写时拷贝(Copy-on-Write,简称 COW)是一种高效的内存管理技术,其核心原理是:多个进程在初始阶段共享相同的物理内存页,只有当某个进程尝试对共享内存进行写操作时,系统才会为该进程分配新的物理内存页,并将原共享内存页的数据复制到新的物理内存页中,然后该进程对新的物理内存页进行写操作。
在共享内存的读写过程中,写时拷贝机制发挥着重要作用:
- 节省内存资源:在多个进程共享同一块共享内存时,如果所有进程都只是进行读操作,那么它们可以一直共享相同的物理内存页,无需为每个进程单独分配物理内存。
- 提高效率:因为只有在进程进行写操作时才会触发内存复制操作,而大多数情况下,进程在一段时间内可能只是进行读操作。这样就避免了不必要的内存复制开销。
从实现层面来看,写时拷贝机制依赖于页表项中的标志位来实现。当多个进程共享同一块共享内存时,对应的页表项会被设置为只读权限。当某个进程尝试写操作时,CPU 会检测到页表项的只读标志,触发缺页中断。在缺页中断处理程序中,系统会为该进程分配新的物理内存页,将原共享内存页的数据复制到新页中,然后更新页表项。通过这种方式,写时拷贝机制在保证数据一致性和安全性的前提下,最大限度地提高了内存的利用率和系统的性能。
五、Linux共享内存相关API 调用
5.1 shmget 函数:创建共享内存段
shmget 函数用于创建一个新的共享内存段,或者获取一个已经存在的共享内存段的标识符。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:是一个整数值,用于唯一标识共享内存段。
size:指定了要创建或获取的共享内存段的大小,单位是字节。
shmflg:是一组标志位,常用的有 IPC_CREAT 和 IPC_EXCL。
shmget 函数如果执行成功,会返回共享内存段的标识符(shmid);如果执行失败,返回值为 -1。
下面是一个使用 shmget 函数创建共享内存段的简单示例代码:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#define SHM_SIZE 1024 // 共享内存大小为1024字节
int main(){
key_t key;
int shmid;
// 使用IPC_PRIVATE创建一个新的共享内存段,仅供当前进程使用
key = IPC_PRIVATE;
// 创建共享内存段,权限设置为所有者可读写
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0600);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
printf("Shared memory created. shmid = %d\n", shmid);
return 0;
}
5.2 ftok 函数:生成共享内存的唯一标识
ftok 函数的作用是根据给定的文件路径和一个项目标识符,生成一个唯一的键值(key)。
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:是一个已经存在的文件路径名。
proj_id:是一个字符型的项目标识符。
下面是一个使用 ftok 函数生成 key 值的示例代码:
#include <sys/ipc.h>
#include <stdio.h>
int main(){
key_t key;
const char *pathname = "/tmp/testfile"; // 假设这个文件存在
int proj_id = 0x123; // 项目标识符
key = ftok(pathname, proj_id);
if (key == -1) {
perror("ftok failed");
return 1;
}
printf("Generated key = %x\n", key);
return 0;
}
5.3 shmat 函数:将共享内存映射到进程地址空间
shmat 函数用于将共享内存段连接到调用进程的地址空间。
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:是 shmget 函数返回的共享内存段的标识符。
shmaddr:通常设置为 NULL,表示由系统自动选择地址。
shmflg:通常设置为 0。
shmat 函数如果执行成功,会返回一个指向共享内存起始地址的指针;如果执行失败,返回值为 (void *)-1。
下面是一个使用 shmat 函数的示例代码:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SHM_SIZE 1024
int main(){
key_t key;
int shmid;
char *shm_ptr;
key = IPC_PRIVATE;
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0600);
if (shmid == -1) { perror("shmget failed"); exit(1); }
// 将共享内存映射到进程地址空间
shm_ptr = (char *)shmat(shmid, NULL, 0);
if (shm_ptr == (char *)-1) { perror("shmat failed"); exit(1); }
printf("Shared memory attached at address %p\n", shm_ptr);
// 向共享内存中写入数据
strcpy(shm_ptr, "Hello, shared memory!");
return 0;
}
5.4 shmdt 函数:断开共享内存连接
shmdt 函数用于将共享内存段与当前进程的地址空间分离。
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmaddr 是之前通过 shmat 函数返回的地址指针。
下面是一个使用 shmdt 函数的示例代码:
// ... 前面代码同上,创建并映射共享内存 ...
strcpy(shm_ptr, "Hello, shared memory!");
// 分离共享内存
if (shmdt(shm_ptr) == -1) {
perror("shmdt failed");
exit(1);
}
printf("Shared memory detached\n");
5.5 shmctl 函数:共享内存的控制中心
shmctl 函数是共享内存的控制函数,用于执行各种控制操作。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:是共享内存段的标识符。
cmd:指定了要执行的控制操作,常用的有 IPC_RMID(删除)、IPC_STAT(获取状态)。
buf:是一个指向 struct shmid_ds 结构体的指针。
下面是一个使用 shmctl 函数删除共享内存段的示例代码:
// ... 前面代码同上,创建、映射、写入、分离共享内存 ...
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl(IPC_RMID) failed");
exit(1);
}
printf("Shared memory deleted\n");
六、实战演练:手撕共享内存代码
通过前面的理论知识和函数介绍,现在我们将通过一个完整的代码示例来将这些知识串联起来,实现一个简单的进程间通信示例。
6.1 代码示例
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define SHM_SIZE 1024 // 共享内存大小为1024字节
int main(){
key_t key;
int shmid;
char *shm_ptr;
// 使用ftok生成key值,假设当前目录下存在testfile文件
key = ftok("testfile", 1);
if (key == -1) { perror("ftok failed"); exit(1); }
// 创建共享内存段,权限设置为所有者可读写
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0600);
if (shmid == -1) { perror("shmget failed"); exit(1); }
// 将共享内存映射到进程地址空间
shm_ptr = (char *)shmat(shmid, NULL, 0);
if (shm_ptr == (char *)-1) { perror("shmat failed"); exit(1); }
// 创建子进程
pid_t pid = fork();
if (pid == -1) { perror("fork failed"); exit(1); }
else if (pid == 0) {
// 子进程,从共享内存读取数据
printf("Child process reading from shared memory...\n");
sleep(1); // 稍微等待一下,确保父进程已经写入数据
printf("Data read from shared memory: %s\n", shm_ptr);
// 分离共享内存
if (shmdt(shm_ptr) == -1) { perror("shmdt failed in child"); exit(1); }
} else {
// 父进程,向共享内存写入数据
printf("Parent process writing to shared memory...\n");
strcpy(shm_ptr, "Hello, from parent process!");
// 等待子进程完成读取
wait(NULL);
// 分离共享内存
if (shmdt(shm_ptr) == -1) { perror("shmdt failed in parent"); exit(1); }
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl(IPC_RMID) failed"); exit(1); }
printf("Shared memory deleted by parent.\n");
}
return 0;
}
代码解读:
- 生成key值:使用
ftok 函数生成 key 值。
- 创建共享内存段:调用
shmget 函数创建共享内存段。
- 映射共享内存:通过
shmat 函数将共享内存映射到当前进程的地址空间。
- 创建子进程:使用
fork 函数创建子进程。
- 子进程操作:子进程从共享内存中读取数据,然后分离共享内存。
- 父进程操作:父进程向共享内存写入数据,等待子进程结束,然后分离并删除共享内存段。
6.2 运行与调试
(1)编译代码: 将上述代码保存为 shared_memory_example.c 文件,然后使用 gcc 编译器进行编译:
gcc -o shared_memory_example shared_memory_example.c
(2)运行程序: 在终端中运行生成的可执行文件:
./shared_memory_example
(3)可能遇到的问题及解决方法:
ftok 失败:确保 ftok 函数中指定的文件确实存在。
shmget 失败:检查权限设置是否正确,确保共享内存段大小合理。
shmat 失败:检查 shmid 是否正确获取,以及权限设置。
- 共享内存数据不一致:这是由于没有正确处理同步机制导致的。可以使用信号量或互斥锁等同步工具来确保多个进程对共享内存的访问是安全的,这是编写高效可靠C/C++程序的重要原则。
通过对共享内存从 API 调用到内核映射的全流程分析,我们深入了解了这一高效的进程间通信方式。从用户态的 API 调用,我们掌握了如何在应用层创建、映射、访问和管理共享内存;而在内核层面,共享内存依赖于复杂的内存映射机制和内核与用户态的交互,实现了进程间数据的高效共享。希望这篇文章能帮助你更深入地理解 Linux 共享内存,为你的系统编程和性能优化工作提供扎实的基础。如果你想了解更多底层原理或与其他开发者交流,欢迎访问云栈社区探讨。