48 System V 共享内存

共享内存是 IPC 机制中最快的一种,因为共享内存段会成为进程用户空间内存的一部分,因此在申请完成之后无需内核介入便可通信。作为对比,管道数据生产者需要将数据从用户缓冲区(如果使用 stdio)写入到内核缓冲区,管道数据的消费者也需要从内核缓冲区读取信息。

使用方式:

  • shmget() 创建或取得共享内存段的标识符。
  • shmat() 来附上共享内存段。
  • shmdt() 来分离共享内存段,此操作完成之后共享内存段将无法访问。在进程终止时,未分离的共享内存段会自动分离。
  • shmctl() 来删除共享内存段。只有所有附加到共享内存段的进程都与其分离后,内存段才会被销毁

创建 System V 共享内存段

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

标志 IPC_CREAT 表示如果不存在和 key 对应的段,就创建一个新的;IPC_EXCL 表示共享内存段必须由当前进程创建,否则返回错误并设置 errno 为 EEXIST,使用时要同时指定 IPC_CREAT(man 手册中没讲不同时指定会怎么样)。其他标志略。

如果 size 不是页的整数倍,则会向上取整。如果用 shmget() 获得既有段的标识符,则 size 无意义,但不能大于创建时指定的大小。

附加 System V 共享内存段

#include <sys/shm.h>

void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
int shmdt(const void *shmaddr);

shmaddr 可以为 NULL,此时会自动选择一个地址。shmaddr 不为 NULL 时,如果该地址已经不能使用,则会返回错误,关于 shmaddr 不为 NULL 的使用含义这里略去(一般用 NULL 也就够了)。

shmflg

SHM_RDONLY  只读(默认是读写)
SHM_REMAP   替换已有映射
SHM_RND     将 shmaddr 向上舍入到 SHMLBA(shared memory low boundary address)的整数倍

SHMLBA 与提升 CPU 快速缓冲性能有关,在 x86 上 SHMLBA 和页大小相同。

查看进程的内存映射情况

2025/3/8 内核地址空间在用户地址空间的上面,地址会更大一些。

可以用 Linux 特有的 /proc 文件系统(/proc/PID/maps)来查看进程的内存映射情况。

每个链接到 GLIBC 的进程都会有 [vdso] 内存段,这是由系统映射的动态链接库。C 语言标准库的实现者可以使用 vdso 中的函数,从而自动选择更适合的系统调用发起方式(__kernel_vsyscall),或者在有替代版本可用时避免发起系统调用。对于 C 语言标准库的使用者而言,“一个(man 手册 section 2 的)函数是否是通过中断发起系统调用来实现”不那么重要。不同架构上 vsdo 库的名称不同、其中包含的函数也不同,这些函数基本都和时间、CPU 状态有关系。举例:

   **x86-64 functions**
       The table below lists the symbols exported by the vDSO.  All of
       these symbols are also available without the "__vdso_" prefix,
       but you should ignore those and stick to the names below.
       symbol                 version
       ─────────────────────────────────
       __vdso_clock_gettime   LINUX_2.6
       __vdso_getcpu          LINUX_2.6
       __vdso_gettimeofday    LINUX_2.6
       __vdso_time            LINUX_2.6

我尝试过将 man 手册中的 void *vdso = (uintptr_t) getauxval(AT_SYSINFO_EHDR) 作为 dlsym() 的句柄,但是失败了。这个 vdso 地址的用法和 dlopen() 打开的共享库有一点差异,可以参考 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/vDSO/parse_vdso.c?id=v3.8 来使用。用的时候直接把整个文件加入到工程中应该就可以了。

getauxv() 的手册中对 vdso 的说法:… the virtual Dynamic Shared Object (vDSO) that the kernel creates in order to provide fast implementations of certain system calls.

在共享内存段中存储指针

建议在共享内存段中存储偏移,而不是绝对地址,因为共享内存段被附加的位置是不确定的。

共享内存控制操作

IPC_RMID:删除共享内存段。在一些系统上,如果共享内存段还有进程附着,那么其他进程还能继续附加该段(比如 Linux)。但是有些系统则禁止进程附加到已经被删除的内存段。Linux 中 System V 共享内存段的这种行为和“文件系统中文件被删除、只有当前已经打开了文件的进程可以继续使用文件”有差异。

IPC_STAT:复制 shmid_ds 数据结构到缓冲区中。经我测试,权限访问位中存储的 SHM_LOCKEDSHM_DEST 位默认值都是 0(前者代表页锁定,后者代表当所有进程都和共享内存段分离之后,将共享内存段删除)。

不知道为什么,我只有 root 身份才能成功。以同一个普通用户身份创建的 System V 共享内存段,不能用同一个用户身份来获取信息。

SHM_LOCKSHM_UNLOCK:上锁和解锁。在 Linux 2.6.10 以前,只有特权用户(CAP_IPC_LOCK)能将共享内存段缩进内存。在 Linux 2.6.10 之后,只要进程的有效用户 ID 和段的所有者或创建者的用户 ID 匹配,进程 RLIMIT_MEMLOCK 的资源限制也足够,那么非特权进程也能进行共享内存段的锁定。