System V 共享内存

共享内存是 Linux 下最快的进程间通信(IPC)形式。


一、System V 共享内存的定义与本质

1. 什么是共享内存?

共享内存 是一块可以被多个进程直接访问的物理内存区域。System V 共享内存是 AT&T System V 操作系统引入的 IPC 机制,后被所有 Unix 系统(包括 Linux)采用。

  • 关键特性:多个进程将同一块物理内存映射到各自的虚拟地址空间。
  • 数据传递:一个进程写入共享内存,另一个进程立即可以看到,中间不经过内核,也没有数据拷贝
    在这里插入图片描述

2. 核心数据结构

每个共享内存段在内核中由 struct shmid_ds 描述,关键字段如下:

struct shmid_ds {
    struct ipc_perm shm_perm;   // 所有者、权限等
    int shm_segsz;              // 段大小(字节)
    __kernel_time_t shm_atime;  // 最后附加时间
    __kernel_time_t shm_dtime;  // 最后分离时间
    __kernel_time_t shm_ctime;  // 最后修改时间
    __kernel_ipc_pid_t shm_cpid;// 创建者 PID
    __kernel_ipc_pid_t shm_lpid;// 最后操作者 PID
    unsigned short shm_nattch;  // 当前附加的进程数
};

3. 标识方式

  • key_t:全局键值,通常由 ftok() 生成,用于多个进程协商同一块共享内存。
  • shmid:成功创建或获取后返回的整数标识符,后续所有操作(附加、控制、删除)都使用它。

4. 生命周期

System V 共享内存的生命周期随内核

  • 即使所有进程都调用了 shmdt(分离),共享内存对象依然存在。
  • 必须显式调用 shmctl(IPC_RMID) 或使用命令 ipcrm -m shmid 删除,否则会一直占用资源,直到系统重启。

容易忽视:这与管道、FIFO 等随进程结束而自动销毁的 IPC 完全不同,务必注意资源释放。


二、共享内存的优势与必须面对的挑战

1. 为什么需要共享内存?—— 传统 IPC 的性能瓶颈

以管道或消息队列为例,数据传递路径为:

进程A → 内核缓冲区 → 进程B

数据需要在内核空间与用户空间之间拷贝两次,每次拷贝都涉及系统调用和上下文切换,开销较大。

而共享内存的路径是:

进程A → 物理内存 ← 进程B

一旦映射完成,进程读写共享内存就像访问自己的栈或堆一样,不再需要系统调用,数据拷贝次数为 。因此共享内存是最快的 IPC 机制

2. 共享内存的最大缺点:无内置同步

共享内存本身不提供任何同步与互斥机制。如果多个进程同时写入同一位置,数据会被破坏;如果读写并发,可能读到不完整的数据。

例如:进程 A 正在写一个 10 字节的结构,写到第 5 个字节时被调度出去,进程 B 来读,就会读到一半新一半旧的数据。

结论:使用共享内存时,必须配合信号量或互斥锁等同步机制,否则程序必然出错。


三、API 详解、代码示例、同步方案与管理

1. 核心 API 讲解

shmget —— 创建或获取共享内存
int shmget(key_t key, size_t size, int shmflg);
  • key:通常由 ftok() 生成,也可用 IPC_PRIVATE(用于父子进程)。
  • size:共享内存大小(字节),建议为系统页大小(通常 4096)的整数倍。
  • shmflg:权限标志(如 0666)与 IPC_CREATIPC_EXCL 的组合。
    • IPC_CREAT:若不存在则创建。
    • IPC_EXCL:与 IPC_CREAT 同时使用,若已存在则返回错误。
  • 返回值:成功返回 shmid,失败返回 -1。
shmat —— 附加到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmaddr:指定附加的虚拟地址,通常传 NULL 让内核自动选择。
  • shmflg
    • SHM_RDONLY:只读附加。
    • SHM_RND:配合非空 shmaddr 时,将地址向下取整到 SHMLBA 的倍数。
  • 返回值:成功返回映射的虚拟地址,失败返回 (void*)-1
shmdt —— 分离
int shmdt(const void *shmaddr);
  • 参数为 shmat 返回的地址。
  • 成功返回 0,失败返回 -1。
  • 注意:分离并不删除共享内存,只是当前进程不再访问它。
shmctl —— 控制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

常用 cmd

  • IPC_RMID:标记共享内存为待删除。当所有进程都分离后,系统真正释放资源。
  • IPC_STAT:获取状态信息,存入 buf
  • IPC_SET:修改权限等信息。
    在这里插入图片描述

2. 完整示例(无同步版本)

下面演示一个简单的“服务器写入 A~Z,客户端读取并打印”的例子。

comm.h

#ifndef COMM_H
#define COMM_H

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

#define PATHNAME "."
#define PROJ_ID 0x6666

int createShm(int size);
int getShm(int size);
int destroyShm(int shmid);

#endif

comm.c

#include "comm.h"

static int commShm(int size, int flags)
{
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if(_key < 0){
        perror("ftok");
        return -1;
    }
    int shmid = shmget(_key, size, flags);
    if(shmid < 0){
        perror("shmget");
        return -2;
    }
    return shmid;
}

int createShm(int size)
{
    return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}

int getShm(int size)
{
    return commShm(size, IPC_CREAT | 0666);
}

int destroyShm(int shmid)
{
    if(shmctl(shmid, IPC_RMID, NULL) < 0){
        perror("shmctl");
        return -1;
    }
    return 0;
}

server.c(生产者)

#include "comm.h"
#include <unistd.h>

int main()
{
    int shmid = createShm(4096);
    char *addr = shmat(shmid, NULL, 0);
    sleep(2);  // 等待客户端附加

    int i = 0;
    while(i < 26){
        addr[i] = 'A' + i;
        i++;
        addr[i] = 0;
        sleep(1);
    }

    shmdt(addr);
    sleep(2);
    return 0;
}

client.c(消费者)

#include "comm.h"
#include <unistd.h>

int main()
{
    int shmid = getShm(4096);
    sleep(1);
    char *addr = shmat(shmid, NULL, 0);
    sleep(2);  // 等待服务器开始写入

    int i = 0;
    while(i < 26){
        printf("client# %s\n", addr);
        sleep(1);
        i++;
    }

    shmdt(addr);
    destroyShm(shmid);
    return 0;
}

运行结果可能不确定(例如读到空串或不完整的字符串),这正是因为没有同步导致的。

3. 怎么做同步?—— 三种常用方案

因为共享内存自身无同步,必须额外加锁或信号量。

方案一:POSIX 命名信号量(推荐)
#include <semaphore.h>
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
sem_wait(sem);   // P 操作
// 访问共享内存
sem_post(sem);   // V 操作
sem_close(sem);
sem_unlink("/mysem");
方案二:System V 信号量
int semid = semget(key, 1, IPC_CREAT | 0666);
struct sembuf sb = {0, -1, 0};  // P 操作
semop(semid, &sb, 1);
// 访问共享内存
sb.sem_op = 1;                  // V 操作
semop(semid, &sb, 1);
方案三:跨进程互斥锁(需共享内存)
pthread_mutex_t *mutex = mmap(...);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mutex, &attr);

关键:无论哪种方案,同步对象本身必须放在所有进程都能访问的地方(命名信号量自动实现;匿名信号量和互斥锁需要放在共享内存中)。

4. 共享内存的管理与调试

查看系统中的共享内存段
ipcs -m

输出示例:

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0x12345678 32768      user       666        4096       2
  • nattch:当前附加到此段的进程数。
  • status:若显示 dest 表示已被标记删除(IPC_RMID)。
手动删除共享内存
ipcrm -m shmid
# 或按 key 删除
ipcrm -M key
常见问题与解决
问题 可能原因 解决方法
shmget 返回 EEXIST 对象已存在且使用了 IPC_EXCL 去掉 IPC_EXCL 或改用 IPC_CREAT
进程退出后共享内存仍存在 未调用 shmctl(IPC_RMID) 在程序中注册 atexit 或信号处理函数删除
shmat 返回 EINVAL 大小超过系统限制或 shmid 无效 检查 shmid,调整 shmmax
数据错乱 缺少同步 加入信号量或互斥锁

5. 补充:System V 共享内存 vs POSIX 共享内存

特性 System V POSIX
创建 shmget + shmat shm_open + mmap
标识 key_t + shmid 文件路径名(/name
大小 创建时固定 ftruncate 可动态调整
删除 shmctl(IPC_RMID) shm_unlink
查看工具 ipcs -m ls /dev/shm
接口风格 专用 IPC 函数 类文件操作
新项目推荐 不推荐 推荐

建议:除非需要兼容老旧系统,否则新项目优先使用 POSIX 共享内存。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐