Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux System V 共享内存

Linux进程通信之System V 共享内存详解

作者:Howrun777

SystemV共享内存是一种高效的进程间通信机制,通过内核分配的物理内存区域,实现多个进程间的直接读写,具有零拷贝的特性,但需手动实现同步

System V 共享内存(Shared Memory)

System V 共享内存是内核在物理内存中划出的一块连续内存区域,允许多个进程将该区域映射到自身的虚拟地址空间中。进程对这块内存的读写操作完全等同于本地内存(无需系统调用 / 数据拷贝),是所有 IPC 机制中速度最快的(“零拷贝” 特性)。其核心价值是解决 “大量数据在进程间传输” 的性能问题,但本身无内置同步机制,需配合信号量等工具实现互斥 / 同步。

核心功能

核心特征

特性说明
零拷贝数据直接在进程虚拟地址空间读写,无read()/write()的拷贝开销(相比管道 / 消息队列的 2 次拷贝)
内核持久化内存段存在于内核中,进程退出后不消失,需显式删除
无内置同步共享内存无锁 / 阻塞机制,需配合信号量 / 互斥锁避免 “竞态条件”
地址独立不同进程映射到的虚拟地址不同,但指向同一块物理内存
大小对齐内存大小按系统页大小(通常 4KB)对齐,不足一页按一页分配

与其他 IPC 机制的性能对比

IPC 机制数据拷贝次数核心开销适用场景性能排序
System V 共享内存0 次仅内存映射 / 同步开销大量数据、高性能需求的通信1
System V 消息队列2 次(用户→内核→用户)系统调用 + 拷贝开销结构化、带类型的小数据通信2
命名管道(FIFO)2 次系统调用 + 拷贝开销简单流式数据通信3
匿名管道2 次系统调用 + 拷贝开销亲缘进程临时通信3

问题:同步依赖

共享内存的 “零拷贝” 优势也带来了风险:多个进程可同时读写同一块内存,若没有同步机制,会出现 “一个进程写了一半,另一个进程就读取” 的 “竞态条件”(数据错乱)。因此,共享内存必须配合信号量(System V/POSIX)或互斥锁使用,实现 “临界区独占访问”。

使用流程

  1. 用 ftok() 生成唯一键值;
  2. 创建内存段:进程调用shmget(),内核在物理内存中分配一块连续区域,初始化shmid_ds
  3. 映射到进程空间:进程调用shmat(),内核修改该进程的页表,将虚拟地址映射到共享物理内存;
  4. 进程读写:进程直接通过虚拟地址读写物理内存,数据对所有映射的进程立即可见;
  5. 解除映射:进程调用shmdt(),内核修改页表,解除虚拟地址与物理内存的关联;
  6. 删除内存段:最后一个进程解除映射后,调用shmctl(IPC_RMID),内核释放物理内存。

关键:多个进程的虚拟地址不同,但页表指向同一块物理内存,因此修改对所有进程可见。

函数解释

所有操作需包含以下头文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

1. ftok ():生成 IPC 唯一键值

将「文件路径」和「项目 ID」转换为key_t类型的整数键值,用于标识 System V IPC 对象(共享内存、消息队列、信号量)。不同进程使用相同的路径和项目 ID,可生成相同的键值,从而访问同一个 IPC 对象。

key_t ftok(const char *pathname, int proj_id);
参数说明
pathname必须是存在且可访问的文件路径(如/tmp/test),文件仅作为标识,无需读写。
proj_id项目标识(低 8 位有效,通常取 1-255),仅用于区分同一文件下的不同 IPC 对象。
返回值
注意事项

2. shmget ():创建 / 获取共享内存段

在内核中创建或获取共享内存段,返回唯一的shmid(共享内存标识符),内核会为每个共享内存段维护一个shmid_ds结构体(存储大小、权限、映射数等信息)。

int shmget(key_t key, size_t size, int shmflg);
参数说明
keyftok 生成的键值,或IPC_PRIVATE(创建私有共享内存)。
size共享内存大小(字节):- 创建时:必须指定(按系统页大小 4K 对齐,不足则向上取整);- 获取时:设为 0。
shmflg标志位(按位或组合):- IPC_CREAT:不存在则创建,存在则获取;- IPC_EXCL:与IPC_CREAT联用,若已存在则失败(确保创建新段);- 权限位:如0664(同文件权限,八进制)。
返回值
注意事项

3. shmat ():映射共享内存到进程地址空间

将内核中的共享内存段附加(映射) 到进程的虚拟地址空间,返回映射后的地址,进程可直接读写该地址(等同于操作普通内存)。

void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明
shmidshmget 返回的共享内存标识符。
shmaddr指定映射到进程的虚拟地址:- 设NULL(推荐):由系统自动分配;- 非 NULL:需对齐页大小,通常不推荐。
shmflg映射标志:- 0:默认,读写权限;- SHM_RDONLY:只读权限(需 shmget 设置读权限)。
返回值
注意事项

4. shmdt ():解除共享内存映射

将共享内存段与进程的虚拟地址空间分离(解除映射),仅断开关联,不删除内核中的共享内存。

int shmdt(const void *shmaddr);
参数说明
shmaddrshmat 返回的映射地址。
返回值
注意事项

5. shmctl ():控制共享内存段(核心:删除)

System V 共享内存的控制接口,支持获取状态、设置属性、删除共享内存(最常用IPC_RMID命令)。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明
shmidshmget 返回的共享内存标识符。
cmd操作命令(核心):- IPC_STAT:读取共享内存状态,存入buf;- IPC_SET:修改共享内存属性(从buf读取);- IPC_RMID:删除共享内存段(内核释放资源)。
bufstruct shmid_ds结构体指针:- IPC_STAT/IPC_SET:存储 / 读取状态;- IPC_RMID:设NULL即可。
核心结构体(简化版)
struct shmid_ds {
    struct ipc_perm shm_perm;  // 权限结构体(含所有者、权限位等)
    size_t shm_segsz;          // 共享内存大小(字节)
    pid_t shm_lpid;            // 最后操作的进程ID
    pid_t shm_cpid;            // 创建进程ID
    shmatt_t shm_nattch;       // 当前映射的进程数
    time_t shm_atime;          // 最后映射时间
    time_t shm_dtime;          // 最后解除映射时间
};
返回值
注意事项

完整示例(创建→写入→读取→删除)

1. 写进程(shm_write.c)

#include <stdio.h>      // 提供printf、perror等输入输出函数
#include <stdlib.h>     // 提供exit()退出函数(进程出错时终止)
#include <string.h>     // 提供strncpy字符串拷贝函数
#include <sys/ipc.h>    // 提供ftok()函数(生成IPC键值)
#include <sys/shm.h>    // 提供shmget/shmat/shmdt/shmctl等共享内存核心函数
#include <unistd.h>     // 提供getchar()(等待用户输入)、系统调用基础功能

// 2. 宏定义:把固定值抽出来,方便修改和理解
#define PATHNAME "/tmp/shm_test"  // ftok需要的文件路径(必须存在!小白要先touch这个文件)
#define PROJ_ID 100               // 项目ID(仅低8位有效,随便设1-255之间的数即可)
#define SHM_SIZE 4096             // 共享内存大小(字节),4096是系统页大小(对齐要求,不能随便设小)

int main() {
    // -------------------------- 步骤1:生成唯一的IPC键值 --------------------------
    // key_t是专门存IPC键值的类型(本质是整数)
    // ftok作用:把"文件路径+项目ID"转换成唯一整数,让读写进程能找到同一个共享内存
    key_t key = ftok(PATHNAME, PROJ_ID);
    // 检查ftok是否失败(返回-1就是失败)
    if (key == -1) {
        // perror:自动打印"xxx failed: 具体错误原因"(比如文件不存在会提示No such file)
        perror("ftok failed");
        exit(1);  // 1表示异常退出(0是正常退出),终止进程
    }

    // -------------------------- 步骤2:创建共享内存段 --------------------------
    // shmget作用:向内核申请一块共享内存,返回"共享内存ID(shmid)"(类似文件句柄)
    // 参数1:ftok生成的键值(标识共享内存)
    // 参数2:共享内存大小(必须是系统页大小的整数倍,4096是最常用的)
    // 参数3:标志位组合(IPC_CREAT=创建新的 | IPC_EXCL=如果已存在则报错 | 0664=权限,和文件权限一样)
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0664);
    // 检查shmget是否失败(比如内存不足、键值已存在)
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }
    // 打印shmid,方便调试(比如用ipcs -m命令查看时能对应上)
    printf("共享内存ID(shmid):%d\n", shmid);

    // -------------------------- 步骤3:把共享内存映射到进程地址空间 --------------------------
    // shmat作用:把内核里的共享内存,"挂到"当前进程的内存地址上,进程才能直接读写
    // 参数1:shmget返回的共享内存ID
    // 参数2:指定映射的地址(设NULL让系统自动分配,小白千万别改)
    // 参数3:映射权限(0=读写,SHM_RDONLY=只读)
    // 返回值:映射后的内存地址(进程直接操作这个地址就等于操作共享内存)
    char *shm_addr = (char *)shmat(shmid, NULL, 0);
    // 检查映射是否失败(返回(void*)-1就是失败,注意强制类型转换)
    if (shm_addr == (void *)-1) {
        perror("shmat failed");
        exit(1);
    }

    // -------------------------- 步骤4:向共享内存写入数据 --------------------------
    // 要写入的字符串(小白注意:C语言字符串末尾有个隐藏的'\0',表示结束)
    const char *msg = "Hello, Shared Memory!";
    // strncpy:把msg拷贝到共享内存地址shm_addr
    // 第三个参数:strlen(msg)+1 是为了把末尾的'\0'也拷贝过去(否则读的时候会乱码)
    strncpy(shm_addr, msg, strlen(msg) + 1);
    // 打印写入的内容,确认写成功
    printf("已向共享内存写入:%s\n", shm_addr);

    // -------------------------- 等待读进程读取数据 --------------------------
    // 暂停进程,等用户按回车再继续(给读进程留时间读取,否则写进程直接删了共享内存,读进程就读不到了)
    printf("请按回车键继续(此时可以启动读进程读取数据)...\n");
    getchar();  // 阻塞等待用户输入回车

    // -------------------------- 步骤5:解除共享内存映射 --------------------------
    // shmdt作用:把共享内存和当前进程"解绑"(进程不再能访问这个地址,但共享内存还在内核里)
    // 参数:shmat返回的映射地址
    if (shmdt(shm_addr) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    // -------------------------- 步骤6:删除共享内存(释放内核资源) --------------------------
    // shmctl作用:控制共享内存(这里用IPC_RMID命令删除)
    // 参数1:共享内存ID
    // 参数2:操作命令(IPC_RMID=删除共享内存)
    // 参数3:共享内存的状态结构体(删除时设NULL即可)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl failed");
        exit(1);
    }

    // 正常退出进程(0表示无错误)
    return 0;
}

2. 读进程(shm_read.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "/tmp/shm_test"
#define PROJ_ID 100
#define SHM_SIZE 4096

int main() {
    // 1. 生成相同键值
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key == -1) {
        perror("ftok failed");
        exit(1);
    }

    // 2. 获取已存在的共享内存(size设0,仅获取)
    int shmid = shmget(key, 0, 0);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }
    printf("shmid: %d\n", shmid);

    // 3. 映射(只读权限)
    char *shm_addr = (char *)shmat(shmid, NULL, SHM_RDONLY);
    if (shm_addr == (void *)-1) {
        perror("shmat failed");
        exit(1);
    }

    // 4. 读取数据
    printf("Read from shm: %s\n", shm_addr);

    // 5. 解除映射
    if (shmdt(shm_addr) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    return 0;
}

编译与运行

# 先创建标识文件
touch /tmp/shm_test

# 编译
gcc shm_write.c -o shm_write
gcc shm_read.c -o shm_read

# 先运行写进程
./shm_write
# 再新开终端运行读进程
./shm_read

常用辅助命令

命令说明
ipcs -m查看所有共享内存段
ipcrm -m <shmid>命令行删除指定共享内存
cat /proc/sys/kernel/shmmax查看共享内存最大限制

内核为每个共享内存段维护关键元数据结构 struct shmid_ds(类似文件的inode):

struct shmid_ds {
    struct ipc_perm shm_perm;  // 权限信息(UID/GID、访问权限)
    size_t          shm_segsz; // 共享内存段大小(字节,按页对齐)
    pid_t           shm_lpid;  // 最后操作的进程ID
    pid_t           shm_cpid;  // 创建进程ID
    shmatt_t        shm_nattch;// 当前映射该段的进程数
    time_t          shm_atime; // 最后映射时间
    time_t          shm_dtime; // 最后解除映射时间
    time_t          shm_ctime; // 最后修改时间
};

关键注意事项

1 同步是必选项

共享内存无任何内置同步机制,多个进程同时读写会导致数据错乱(如写进程写了一半,读进程就读取)。必须配合:

2 内存大小与对齐

3 资源泄漏问题

4 权限与访问控制

5 进程异常退出处理

System V 共享内存 vs POSIX 共享内存

特性System V 共享内存POSIX 共享内存(mmap+shm_open)
标识方式键值(key)+ 标识符(shmid)文件系统路径(/dev/shm/xxx)
持久化内核持久化(需显式删除)文件系统持久化(需 unlink)
接口复杂度较低(5 个核心函数)较高(mmap/shm_open/ftruncate)
跨进程可见性需 ftok 生成 key路径可见,更易管理
大小调整创建时固定可通过 ftruncate 动态调整
现代支持传统接口,维护中现代接口,推荐使用

总结

System V 共享内存是性能最高的 IPC 机制,核心是 “内核物理内存映射到进程虚拟地址空间”,实现零拷贝数据共享;

核心操作流程:ftok()生成键值 → shmget()创建 / 获取段 → shmat()映射到进程空间 → 读写数据 → shmdt()解除映射 → shmctl(IPC_RMID)删除段;

关键要点:

现代开发中,POSIX 共享内存(shm_open+mmap)更易管理,但 System V 共享内存仍是传统高性能 IPC 的核心选择。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文