目录

1 简介

1.1 本质

1.2 原理

1.3 实现方式

1.4 特点总结

1.5 同步机制

2 C语言实现

2.1 System V程序实现

2.1.1 接口讲解

2.1.2 使用示例

2.2 POSIX 程序实现

2.2.1接口说明

2.2.2 使用示例

3 面试常见问题


1 简介

1.1 本质

共享内存是一种进程间通信(IPC)机制,允许多个进程直接访问同一块内存区域,从而实现高速数据交换。

一句话本质: 让不同进程“共享同一片内存”,从而跳过内核缓冲区,数据不需要多次拷贝。


1.2 原理

  • 操作系统在内核中分配一块共享内存段。

  • 进程通过系统调用将这块内存映射(mmap/attach)到自己的地址空间

  • 多个进程可同时读写这块区域。

  • 因为没有内核中转,所以速度最快

  • 但必须使用**同步机制(如信号量、互斥锁)**来防止读写冲突。


1.3 实现方式

两种主流标准👇

标准 接口 特点
System V shmget, shmat, shmdt, shmctl 老标准,兼容性好,常用于Linux内核传统接口
POSIX shm_open, mmap, shm_unlink 新标准,文件描述符式操作,更现代、可与文件系统结合

1.4 特点总结

  • 速度最快 的进程间通信方式(零拷贝)

  • 🚫 无内核缓冲区(直接读写)

  • 🧩 需要同步机制 保证一致性

  • 📦 可配合信号量、互斥锁等实现安全通信

1.5 同步机制

共享内存只是提供一块可以被多个进程同时访问的内存区域。多个进程同时读写可能导致数据竞争,因此需要同步机制。

同步方式 说明 适用场景
System V 信号量(semget / semop / semctl) 共享内存 + 信号量 是最经典组合,用信号量实现互斥锁或同步信号。 精细控制访问顺序,如“生产者-消费者”模型。
POSIX 信号量(sem_open / sem_wait / sem_post) 跨进程、接口更现代、更方便。 新项目建议使用。
文件锁(flock / fcntl) 利用文件描述符锁来保护共享区数据访问。 简单粗暴、但性能略低。
自旋锁 / 互斥变量(pthread_mutex_t) 若共享内存用于多线程(同进程或共享 pthread mutex 的跨进程模式)。 多线程场景或轻量同步。

2 C语言实现

2.1 System V程序实现

2.1.1 接口讲解

1️⃣ key_t ftok(const char *pathname, int proj_id)

用途:
生成唯一的 IPC 键(key_t),供 shmget / msgget / semget 等使用。

参数:

  • pathname:必须指向已存在的文件(一般是/tmp目录下的文件)。

  • proj_id:1–255 的小整数,用来区分不同 IPC 对象。

返回值:

  • 成功 → key_t(非负整数)

  • 失败 → (key_t)-1,并设置 errno

常见错误:

errno 说明
ENOENT 指定的 pathname 不存在。
EACCES 没有访问 pathname 的权限。

2️⃣ int shmget(key_t key, size_t size, int shmflg)

用途:
创建或获取一个共享内存段,返回标识符 shmid

参数:

  • key:由 ftokIPC_PRIVATE 获得,也可以直接指定key,在很多嵌入式系统中只要双方(创建端与访问端)约定同一个 key 值即可共享同一段内存。

  • size:共享内存段大小(字节)。首次创建时有效。

  • shmflg:权限和标志的组合。

    • 权限位:

           0600 —— 只有创建者(owner)能读写

           0660 —— 创建者和同组用户可读写

           0666 —— 所有人都能读写

  • 标志:

    • IPC_CREAT → 如果指定 key 对应的段不存在,则创建一个新段;否则返回已有段的 shmid

    • IPC_EXCL → 若已存在则失败(与 IPC_CREAT 组合使用)。

返回值:

  • 成功 → 非负整数 shmid,是段标识后续可以使用

  • 失败 → -1 并设置 errno

常见错误:

errno 原因与说明
EACCES 指定 key 对应的段存在,但调用进程没有足够权限。
EEXIST 当同时使用 IPC_CREAT | IPC_EXCL 且对象已存在时发生
EINVAL 传入的 size 太小或太大(超过系统上限 SHMMAX),或段已存在且小于当前的传入size。
ENOENT 指定 key 的段不存在,且未指定 IPC_CREAT
ENOMEM 系统共享内存资源不足,无法分配新的段。

3️⃣ void *shmat(int shmid, const void *shmaddr, int shmflg)

用途:
将共享内存段映射到当前进程地址空间。

参数:

  • shmid:由 shmget 返回的段标识符。

  • shmaddr:希望映射到的地址,一般传 NULL 让内核自动选择。

  • shmflg:映射模式:

    • 0 → 可读写;

    • SHM_RDONLY → 只读;

    • 可附加 SHM_RND (地址向下取整至 SHMLBA 边界)。

返回值:

  • 成功 → 有效指针。

  • 失败 → (void*)-1 并设置 errno

常见错误:

errno 说明
EACCES 没有对应的访问权限(例如只读进程尝试写)。
EINVAL 无效的 shmidshmaddr 不满足对齐要求。
EIDRM 对应段已被标记删除(IPC_RMID)。
ENOMEM 内核无法在进程地址空间中分配映射。

4️⃣ int shmdt(const void *shmaddr)

用途:
解除当前进程对共享内存段的映射(detach)。

参数:

  • shmaddr:由 shmat 返回的地址。

返回值:

  • 成功 → 0

  • 失败 → -1 并设置 errno

常见错误:

errno 说明
EINVAL 提供的地址不是任何已映射的共享内存起始地址。

5️⃣ int shmctl(int shmid, int cmd, struct shmid_ds *buf)

用途:
控制或查询共享内存段的属性,或删除它。

常见 cmd 值:

cmd 用途
IPC_STAT 读取共享内存的属性(放入 buf)。
IPC_SET 修改共享内存属性(从 buf 读取)。需有权限。
IPC_RMID 标记删除。最后一个 detach 后真正销毁。

返回值:

  • 成功 → 0

  • 失败 → -1 并设置 errno

常见错误:

errno 说明
EACCES 调用进程无权限执行所请求的操作。
EINVAL 无效的 shmidcmd 值。
EIDRM 段已被标记删除。
EPERM 非段所有者或管理员尝试 IPC_RMID / IPC_SET

2.1.2 使用示例

sv_writer.c

// sv_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>

#define SHM_PATH "/tmp"   // ftok 要求存在的路径
#define SHM_PROJ_ID  'S'
#define SHM_SIZE 4096
#define SEM_NAME "/sv_sem"

struct shm_area {
    int ready;          // 0 = not ready, 1 = ready
    char msg[SHM_SIZE - sizeof(int)];
};

int main(void){
    key_t key = ftok(SHM_PATH, SHM_PROJ_ID);
    if (key == (key_t)-1){ perror("ftok"); return 1; }

    int shmid = shmget(key, sizeof(struct shm_area), IPC_CREAT | 0666);
    if (shmid == -1){ perror("shmget"); return 1; }

    struct shm_area *p = (struct shm_area *) shmat(shmid, NULL, 0);
    if (p == (void *)-1){ perror("shmat"); return 1; }

    // 创建/打开命名信号量,初始值 0(等待 reader)
    sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
    if (sem == SEM_FAILED){ perror("sem_open"); shmdt(p); return 1; }

    // 写数据
    p->ready = 0; // 标记未就绪
    const char *text = "Hello from System V shared memory!";
    strncpy(p->msg, text, sizeof(p->msg)-1);
    p->msg[sizeof(p->msg)-1] = '\0';

    // 内存写完后设置 ready 并通知 reader
    __sync_synchronize(); // memory barrier
    p->ready = 1;
    if (sem_post(sem) == -1) perror("sem_post");

    printf("writer: wrote message and posted semaphore\n");

    // 等一会儿再清理(让 reader 有时间读)——生产环境请使用更健壮的同步/协议
    sleep(2);

    // detach 并请求删除共享内存(IPC_RMID)
    if (shmdt(p) == -1) perror("shmdt");
    if (shmctl(shmid, IPC_RMID, NULL) == -1) perror("shmctl(IPC_RMID)");

    // 关闭并 unlink 信号量
    if (sem_close(sem) == -1) perror("sem_close");
    if (sem_unlink(SEM_NAME) == -1) perror("sem_unlink");

    return 0;
}

sv_reader.c

// sv_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>

#define SHM_PATH "/tmp"
#define SHM_PROJ_ID  'S'
#define SHM_SIZE 4096
#define SEM_NAME "/sv_sem"

struct shm_area {
    int ready;
    char msg[SHM_SIZE - sizeof(int)];
};

int main(void){
    key_t key = ftok(SHM_PATH, SHM_PROJ_ID);
    if (key == (key_t)-1){ perror("ftok"); return 1; }

    int shmid = shmget(key, sizeof(struct shm_area), 0666);
    if (shmid == -1){ perror("shmget"); return 1; }

    struct shm_area *p = (struct shm_area *) shmat(shmid, NULL, SHM_RDONLY);
    if (p == (void *)-1){ perror("shmat"); return 1; }

    sem_t *sem = sem_open(SEM_NAME, 0);
    if (sem == SEM_FAILED){ perror("sem_open"); shmdt((void*)p); return 1; }

    // 等待 writer 发信号
    if (sem_wait(sem) == -1){ perror("sem_wait"); }

    // 确认 ready
    while (!p->ready) { /* small spin */ usleep(1000); }

    printf("reader: got message: \"%s\"\n", p->msg);

    if (shmdt((void*)p) == -1) perror("shmdt");
    if (sem_close(sem) == -1) perror("sem_close");

    return 0;
}

运行顺序:先启动 sv_reader(它会阻塞等待信号量),再 sv_writer(写并 sem_post)。也可以先 writer,reader 也能打开后等信号。


2.2 POSIX 程序实现

2.2.1接口说明

  1. int shm_open(const char *name, int oflag, mode_t mode);

    • 用途:创建或打开一个 POSIX 共享内存对象,返回文件描述符(像 open)。

    • 参数:

      • name:以 '/' 开头的名字(例如 "/my_shm"),在命名空间中全局可见。

      • oflagO_CREATO_EXCLO_RDWRO_RDONLY 等。

      • mode:权限位(如 0666),仅在 O_CREAT 时生效。

    • 返回:fd >= 0 成功;失败返回 -1(errno)。

  2. int ftruncate(int fd, off_t length);

    • 用途:设置共享内存对象的大小(创建后必须设置大小才能 mmap)。

    • 返回:0 成功;-1 失败。

  3. void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

    • 用途:把共享内存对象映射到进程地址空间。

    • 参数:

      • protPROT_READ / PROT_WRITE

      • flagsMAP_SHARED(必须)等。

    • 返回:指针;失败返回 MAP_FAILED(并设置 errno)。

  4. int munmap(void *addr, size_t length);

    • 解除映射,返回 0 成功,-1 失败。

  5. int close(int fd);

    • 关闭文件描述符(不删除对象)。

  6. int shm_unlink(const char *name);

    • 删除 POSIX 共享内存对象(类似 unlink),返回 0 成功,-1 失败。删除后仍可被已经打开的 fd / mmap 使用,直到全部 close/unmap 为止。

错误 errnoEEXIST, ENOENT, ENOMEM, EINVAL 等。

2.2.2 使用示例

posix_writer.c

// posix_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>      // O_CREAT...
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <semaphore.h>
#include <errno.h>

#define SHM_NAME "/my_posix_shm"
#define SEM_NAME "/posix_sem"
#define SHM_SIZE 4096

struct shm_area {
    int ready;
    char msg[SHM_SIZE - sizeof(int)];
};

int main(void){
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1){ perror("shm_open"); return 1; }

    if (ftruncate(fd, sizeof(struct shm_area)) == -1){ perror("ftruncate"); close(fd); return 1; }

    struct shm_area *p = mmap(NULL, sizeof(struct shm_area), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED){ perror("mmap"); close(fd); return 1; }

    sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
    if (sem == SEM_FAILED){ perror("sem_open"); munmap(p, sizeof(*p)); close(fd); return 1; }

    // 写入
    p->ready = 0;
    const char *text = "Hello from POSIX shared memory!";
    strncpy(p->msg, text, sizeof(p->msg)-1);
    p->msg[sizeof(p->msg)-1] = '\0';

    __sync_synchronize();
    p->ready = 1;

    if (sem_post(sem) == -1) perror("sem_post");

    printf("posix writer: wrote and posted\n");

    sleep(2);

    // 清理
    if (munmap(p, sizeof(*p)) == -1) perror("munmap");
    close(fd);
    if (shm_unlink(SHM_NAME) == -1) perror("shm_unlink"); // 若想保留以供后续使用,可注释
    if (sem_close(sem) == -1) perror("sem_close");
    if (sem_unlink(SEM_NAME) == -1) perror("sem_unlink");

    return 0;
}

posix_reader.c

// posix_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <semaphore.h>

#define SHM_NAME "/my_posix_shm"
#define SEM_NAME "/posix_sem"
#define SHM_SIZE 4096

struct shm_area {
    int ready;
    char msg[SHM_SIZE - sizeof(int)];
};

int main(void){
    int fd = shm_open(SHM_NAME, O_RDONLY, 0666);
    if (fd == -1){ perror("shm_open"); return 1; }

    struct shm_area *p = mmap(NULL, sizeof(struct shm_area), PROT_READ, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED){ perror("mmap"); close(fd); return 1; }

    sem_t *sem = sem_open(SEM_NAME, 0);
    if (sem == SEM_FAILED){ perror("sem_open"); munmap((void*)p, sizeof(*p)); close(fd); return 1; }

    if (sem_wait(sem) == -1) perror("sem_wait");

    while (!p->ready) usleep(1000);

    printf("posix reader: got message: \"%s\"\n", p->msg);

    munmap((void*)p, sizeof(*p));
    close(fd);
    sem_close(sem);

    return 0;
}

运行顺序同上:通常先运行 reader(阻塞),再 writer。也可先 writer,reader 启动后会立即读取(取决于是否 shm_unlink 被调用等)。


3 面试常见问题

1. 什么是共享内存?它和其他进程通信方式(管道、消息队列、socket)有什么区别?
共享内存是一种由操作系统内核维护的进程间通信机制,允许多个进程直接访问同一块物理内存区域。
与管道、消息队列、socket 的区别是:共享内存无需数据拷贝,是所有 IPC 中速度最快的方式;但它本身不提供同步,需要程序员自己控制访问顺序。


2. System V 共享内存的创建和使用流程是什么?涉及哪些系统调用?
典型流程如下:

  1. ftok() 生成 key(或自定义 key、IPC_PRIVATE)。

  2. 调用 shmget() 创建或打开共享内存段。

  3. 使用 shmat() 将该段映射到本进程地址空间。

  4. 读写共享内存内容。

  5. shmdt() 解除映射。

  6. 最后用 shmctl(shmid, IPC_RMID, NULL) 删除共享内存段。


3. shmget()、shmat()、shmdt()、shmctl() 的作用和参数说明。

  • shmget(key, size, shmflg):创建或打开共享内存段。key 为标识,size 为大小,shmflg 为权限和标志(如 IPC_CREAT|0666)。返回 shmid。

  • shmat(shmid, addr, flag):将共享内存映射到当前进程地址空间。addr 一般传 NULL,flag 可为 0 或 SHM_RDONLY。返回映射指针。

  • shmdt(addr):解除映射。成功返回 0。

  • shmctl(shmid, cmd, buf):控制命令,如 IPC_RMID 删除、IPC_STAT 获取信息。


4. key_t key 是什么?IPC_PRIVATE 的作用是什么?
key 是共享内存的唯一标识,同一 key 表示同一段共享内存。
IPC_PRIVATE 是一个特殊常量(通常值为 0),表示“私有段”,仅能在父子进程间共享,系统不会为其匹配已存在的段。


5. 如何通过 ftok() 生成共享内存的 key?如果路径或 proj_id 不同会怎样?
key_t ftok(const char *pathname, int proj_id) 根据文件 inode 和 proj_id 组合生成 key。
如果路径或 proj_id 任意一个不同,生成的 key 通常也不同。路径文件必须存在。


6. 共享内存创建后,如果再次调用 shmget() 指定相同 key 但不同 size,会返回什么?
如果共享内存已经存在,shmget() 会忽略传入的 size,只要权限允许,仍返回原来的 shmid。
但若新传入的 size 小于已存在段的大小,或大于系统上限,会返回 -1,并设置 errno=EINVAL。


7. 如何判断共享内存是否已经存在?
调用 shmget(key, size, 0666),若返回 -1 且 errno=ENOENT 表示不存在;
若成功返回 shmid,则表示已存在。也可加上 IPC_CREAT|IPC_EXCL,若已存在会返回 -1 并置 errno=EEXIST。


8. shmat() 返回的地址是怎么确定的?能否指定固定地址?
默认传入 NULL,让系统自动选择合适的虚拟地址;
也可以传入一个固定地址(不推荐),系统会尝试在该处映射,但若该地址被占用,会失败。


9. 为什么共享内存是最快的进程间通信方式?
因为通信双方访问的是同一块物理内存,没有数据在内核与用户空间之间复制。
其他 IPC(管道、消息队列)都要在内核中转,产生额外的复制与上下文切换。


10. 为什么共享内存需要同步机制?
因为多个进程可能同时修改同一内存区域,若不控制访问顺序,会产生数据竞争、写覆盖或读取中间态数据。


11. System V 共享内存是否提供同步机制?如果没有,如何实现同步?
System V 共享内存本身不带同步功能。
可用以下方式实现同步:

  • System V 信号量(semget、semop)。

  • POSIX 信号量(sem_init、sem_wait、sem_post)。

  • 互斥锁放在共享内存中并设置属性 PTHREAD_PROCESS_SHARED


12. 在多进程读写共享内存时,如何避免竞争条件?
可通过信号量、互斥锁或原子操作控制访问顺序。
例如:写前加锁、写后解锁,读前也加锁保证不会读取到未完成的数据。


13. 一个进程写、多个进程读的场景是否一定需要加锁?为什么?
若写入是原子操作(如单个 4 字节整数更新),读方可不加锁。
但若写入结构体或连续字段,更新不是原子的,读者可能看到部分更新的数据,因此仍需同步或使用“版本号两次读一致”方案。


14. 写入共享内存时可能出现“中间态”吗?
会。如果一次写入包含多个成员字段,CPU 可能分多次写入内存,读进程可能读到部分旧数据、部分新数据,这就是中间态。


15. 对于结构体写入共享内存时,哪些操作是原子的?
单次写入等于或小于 CPU 原子宽度的基本类型(通常 ≤8 字节的整型或指针)是原子的。
结构体整体写入或浮点、字符串等多字段更新不是原子的。


16. 如何保证跨进程的数据一致性?
常见方法:

  1. 加锁保护写入。

  2. 使用版本号机制:写前自增序号为奇数,写完改为偶数,读方前后两次序号相等即认为数据一致。

  3. 使用原子变量或内存屏障控制读写顺序。


17. 如果进程异常退出,共享内存段会怎样?
共享内存仍然存在,因为它由内核维护。
只有调用 shmctl(shmid, IPC_RMID, NULL) 才会标记删除,最后一个 detach 的进程退出后才真正释放。


18. 如何删除共享内存段?什么时候应该删除?
shmctl(shmid, IPC_RMID, NULL) 删除。
一般在通信双方结束时删除;若可能异常退出,可在启动时检测并清理“孤儿”段。


19. 如何查看系统当前的共享内存段(命令行工具)?
使用 ipcs -m 查看,ipcrm -m <shmid> 删除指定共享内存段。


20. System V 与 POSIX 共享内存的区别?(API、命名、生命周期等)
System V:通过 key 标识,用 shmget/shmat/shmdt/shmctl
POSIX:通过名字标识(如 /shm_name),用 shm_open/mmap/munmap/shm_unlink
System V 段存在于内核直到删除,POSIX 段通常与文件描述符关联,控制更方便。


21. System V 共享内存的权限控制是怎样的?
权限位与文件类似:

  • 0400 所有者可读

  • 0200 所有者可写

  • 0040 组可读

  • 0020 组可写

  • 0004 其他可读

  • 0002 其他可写
    一般使用 06660600


22. 共享内存的最大大小受哪些系统参数限制?
主要受以下内核参数影响(可通过 cat /proc/sys/kernel/... 查看):

  • kernel.shmmax:单段最大字节数。

  • kernel.shmall:系统允许分配的总页数。

  • kernel.shmmni:系统可创建的段数量。


23. 在嵌入式系统中使用共享内存时有哪些注意事项?
内存资源有限,应控制段大小;
避免频繁创建/销毁;
在 ARM 等弱内存序平台需显式内存屏障;
同步可用轻量信号量或简易锁;
若无 MMU,要确保共享区映射在同一物理地址上。


24. 如何在多线程环境中使用共享内存?线程间和进程间共享的区别是什么?
线程间共享同一地址空间,不需要共享内存;
进程间地址空间独立,必须通过共享内存实现。
多线程访问共享内存同样需要锁,只是锁可以是普通线程锁,不需要 PTHREAD_PROCESS_SHARED


25. shmctl(…, IPC_RMID, …) 删除共享内存后,已有进程还能访问吗?
能。删除标记只是延迟生效。
只要进程仍然 attach,该段不会立即释放;最后一个 detach 或进程退出后,内核才真正销毁。


26. 如何实现共享内存的环形缓冲区或生产者–消费者模型?
在共享内存中建立循环队列结构,包含写指针、读指针、缓冲区。
写进程更新写指针,读进程更新读指针;
使用信号量或互斥锁保证指针更新的原子性。


27. 共享内存是否可以在不同用户或不同权限进程间使用?
可以,只要权限(mode bits)允许或属主、属组匹配。
系统管理员也可通过 ipcrm 管理不同用户的段。


28. 如何调试共享内存中的数据内容?
可使用 ipcs -m 查看段号;
用调试器(gdb)或小程序 shmat() 附加到段,直接打印结构体;
或在 /dev/shm/(POSIX)下直接用 hexdump/od 查看内容。


29. 当程序崩溃或异常退出时,如何防止遗留“孤儿”共享内存段?
在程序启动时扫描 ipcs -m 并删除不再使用的段;
也可以注册 atexit() 或信号处理函数,在进程结束时自动调用 shmctl(…, IPC_RMID, NULL)


30. 如果两个进程使用不同的结构体版本(字段不一致),会有什么后果?
因为共享内存只是原始字节区,结构体布局不同会导致读写错位,造成数据错误或崩溃。
必须保持两端结构体定义、字节对齐、字节序完全一致;推荐在结构体中添加版本号字段以检测兼容性。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐