C语言共享内存编程完全指南

系统编程进程间通信

共享内存概述

共享内存(Shared Memory)是进程间通信(IPC)中效率最高的方式。它允许多个进程访问同一块物理内存区域,数据无需在内核空间和用户空间之间拷贝,因此传输速度接近内存读写极限。管道、消息队列等IPC方式需要数据经过内核中转(两次拷贝:用户->内核->用户),而共享内存只需一次映射,进程直接读写同一块内存。

为什么共享内存效率最高

传统IPC方式的瓶颈在于数据拷贝。以管道为例:发送进程将数据从用户缓冲区拷贝到内核缓冲区,接收进程再从内核缓冲区拷贝到用户缓冲区,总共两次拷贝。而共享内存通过 mmap 将同一块物理内存映射到多个进程的虚拟地址空间,进程读写共享内存等同于读写自己的内存,零拷贝完成数据传递。

与其他IPC方式对比

  • 管道/命名管道:半双工/全双工,数据需经内核拷贝,适合流式数据传输
  • 消息队列:消息粒度通信,有格式边界,但每次收发涉及系统调用和数据拷贝
  • 共享内存:最快,零拷贝,但需自行实现同步机制
  • 信号量:不传输数据,仅用于同步,常与共享内存配合
  • Socket:跨网络通信,开销最大,但最通用

System V共享内存

System V共享内存是最早的共享内存API,使用整型键值(key)标识共享内存段,通过 shmget、shmat、shmdt、shmctl 四个系统调用完成操作。

API详解

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

/* 创建或获取共享内存段 */
int shmget(key_t key, size_t size, int shmflg);

/* 将共享内存段连接到进程地址空间 */
void *shmat(int shmid, const void *shmaddr, int shmflg);

/* 断开共享内存连接 */
int shmdt(const void *shmaddr);

/* 共享内存控制操作 */
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

关键参数说明:

  • key:使用 ftok 生成,或指定 IPC_PRIVATE 创建私有段
  • shmflg:权限标志(如 0666)组合 IPC_CREAT(不存在则创建)和 IPC_EXCL(已存在则失败)
  • shmat 的 shmflg:SHM_RDONLY(只读映射),0(读写映射)
  • shmctl 的 cmd:IPC_STAT(获取信息)、IPC_SET(设置属性)、IPC_RMID(删除段)

完整代码示例

/* shm_sv_writer.c - 写入端 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 4096
#define SHM_KEY  0x1234

typedef struct {
    int  ready;          /* 数据就绪标志 */
    char message[256];  /* 消息内容 */
} shm_data_t;

int main(void) {
    /* 创建共享内存段 */
    int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid < 0) { perror("shmget"); return 1; }

    /* 连接到进程地址空间 */
    shm_data_t *shm = (shm_data_t *)shmat(shmid, NULL, 0);
    if (shm == (void *)-1) { perror("shmat"); return 1; }

    /* 写入数据 */
    shm->ready = 0;
    strncpy(shm->message, "Hello from writer!", sizeof(shm->message));
    __sync_synchronize();  /* 内存屏障,确保写入顺序 */
    shm->ready = 1;

    printf("Writer: data written, waiting for reader...\n");
    sleep(5);

    /* 断开连接并删除共享内存 */
    shmdt(shm);
    shmctl(shmid, IPC_RMID, NULL);
    printf("Writer: shared memory removed\n");
    return 0;
}
/* shm_sv_reader.c - 读取端 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 4096
#define SHM_KEY  0x1234

typedef struct {
    int  ready;
    char message[256];
} shm_data_t;

int main(void) {
    /* 获取已存在的共享内存段 */
    int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid < 0) { perror("shmget"); return 1; }

    /* 连接到进程地址空间 */
    shm_data_t *shm = (shm_data_t *)shmat(shmid, NULL, SHM_RDONLY);
    if (shm == (void *)-1) { perror("shmat"); return 1; }

    /* 等待数据就绪 */
    while (!shm->ready) {
        usleep(10000);  /* 10ms轮询 */
    }
    __sync_synchronize();  /* 内存屏障 */

    printf("Reader: received \"%s\"\n", shm->message);

    /* 断开连接 */
    shmdt(shm);
    return 0;
}
# 编译与运行(先启动writer,再启动reader)
gcc -o shm_writer shm_sv_writer.c
gcc -o shm_reader shm_sv_reader.c
./shm_writer &
./shm_reader

System V共享内存的管理命令:ipcs -m 查看共享内存段、ipcrm -m shmid 删除指定段。注意:进程异常退出时共享内存不会自动删除,需手动清理。

POSIX共享内存

POSIX共享内存是更现代的API,使用文件名而非整型键标识共享内存对象,API更简洁,与文件系统语义一致。

API详解

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

/* 创建或打开共享内存对象 */
int shm_open(const char *name, int oflag, mode_t mode);

/* 设置共享内存对象大小 */
int ftruncate(int fd, off_t length);

/* 内存映射 */
void *mmap(void *addr, size_t length, int prot,
            int flags, int fd, off_t offset);

/* 解除映射 */
int munmap(void *addr, size_t length);

/* 关闭共享内存对象 */
int close(int fd);

/* 删除共享内存对象 */
int shm_unlink(const char *name);

关键参数说明:

  • name:以 / 开头的名称,如 /my_shm,在 /dev/shm/ 下创建对应文件
  • oflag:O_RDWR | O_CREAT | O_EXCL,与文件打开标志一致
  • prot:PROT_READ | PROT_WRITE,映射区域的保护权限
  • flags:MAP_SHARED(共享映射,修改对其他进程可见)

完整代码示例

/* shm_posix_writer.c - POSIX共享内存写入端 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define SHM_NAME "/my_posix_shm"
#define SHM_SIZE 4096

typedef struct {
    int  ready;
    char message[256];
} shm_data_t;

int main(void) {
    /* 创建共享内存对象 */
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd < 0) { perror("shm_open"); return 1; }

    /* 设置大小 */
    if (ftruncate(fd, SHM_SIZE) < 0) {
        perror("ftruncate"); return 1;
    }

    /* 映射到进程地址空间 */
    shm_data_t *shm = (shm_data_t *)mmap(
        NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shm == MAP_FAILED) { perror("mmap"); return 1; }
    close(fd);  /* 映射后可关闭fd */

    /* 写入数据 */
    shm->ready = 0;
    strncpy(shm->message, "Hello from POSIX shm!", sizeof(shm->message));
    __sync_synchronize();
    shm->ready = 1;

    printf("Writer: data written\n");
    sleep(5);

    munmap(shm, SHM_SIZE);
    shm_unlink(SHM_NAME);
    return 0;
}
/* shm_posix_reader.c - POSIX共享内存读取端 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define SHM_NAME "/my_posix_shm"
#define SHM_SIZE 4096

typedef struct {
    int  ready;
    char message[256];
} shm_data_t;

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

    shm_data_t *shm = (shm_data_t *)mmap(
        NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (shm == MAP_FAILED) { perror("mmap"); return 1; }
    close(fd);

    while (!shm->ready) usleep(10000);
    __sync_synchronize();

    printf("Reader: received \"%s\"\n", shm->message);

    munmap(shm, SHM_SIZE);
    return 0;
}
# 编译(需链接 rt 库)
gcc -o posix_writer shm_posix_writer.c -lrt
gcc -o posix_reader shm_posix_reader.c -lrt
./posix_writer &
./posix_reader

POSIX共享内存对象存在于 /dev/shm/ 目录下,可直接用 ls /dev/shm/ 查看。与System V不同,shm_unlink 删除后引用计数为0时自动释放。

mmap文件映射

mmap 是最通用的内存映射接口,不仅可用于共享内存,还可将文件映射到内存,实现文件的高效读写。

共享映射 vs 私有映射

  • MAP_SHARED:修改对其他映射同一文件的进程可见,且会写回底层文件
  • MAP_PRIVATE:修改不可见,写时复制(Copy-on-Write),不改变底层文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(void) {
    const char *path = "/tmp/mmap_test.dat";
    const size_t size = 4096;

    /* 创建文件并设置大小 */
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) { perror("open"); return 1; }
    if (ftruncate(fd, size) < 0) { perror("ftruncate"); return 1; }

    /* 共享映射:修改会写回文件 */
    char *shared = (char *)mmap(
        NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared == MAP_FAILED) { perror("mmap shared"); return 1; }

    strcpy(shared, "Hello from MAP_SHARED!");
    msync(shared, size, MS_SYNC);  /* 同步到文件 */
    printf("Shared mapping: %s\n", shared);
    munmap(shared, size);

    /* 私有映射:修改不影响文件 */
    char *priv = (char *)mmap(
        NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
    if (priv == MAP_FAILED) { perror("mmap private"); return 1; }

    strcpy(priv, "Modified in MAP_PRIVATE");
    printf("Private mapping: %s\n", priv);
    /* 文件中仍是 "Hello from MAP_SHARED!" */
    munmap(priv, size);

    close(fd);
    unlink(path);
    return 0;
}

匿名映射

匿名映射无需文件支撑,MAP_ANONYMOUS 配合 MAP_SHARED 可在有亲缘关系的进程间共享内存:

/* 匿名共享映射 - 父子进程间通信 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>

int main(void) {
    size_t size = 4096;
    char *shm = (span class="kw">char *)mmap(
        NULL, size, PROT_READ | PROT_WRITE,
        MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shm == MAP_FAILED) { perror("mmap"); return 1; }

    pid_t pid = fork();
    if (pid == 0) {
        /* 子进程:写入数据 */
        strcpy(shm, "Hello from child!");
        printf("Child: wrote message\n");
    } else {
        /* 父进程:等待后读取 */
        waitpid(pid, NULL, 0);
        printf("Parent: read \"%s\"\n", shm);
    }

    munmap(shm, size);
    return 0;
}

匿名映射的优势:无需创建文件、无需管理文件描述符、fork后子进程自动继承映射。局限:仅限有亲缘关系的进程间通信。

同步机制

共享内存本身不提供任何同步机制,多个进程同时读写可能导致数据竞争。必须配合信号量、互斥锁或原子操作来保护共享数据的访问。

POSIX信号量

#include <semaphore.h>

/* 创建/打开有名信号量 */
sem_t *sem_open(const char *name, int oflag, ...);

/* 等待(P操作,减1) */
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);

/* 释放(V操作,加1) */
int sem_post(sem_t *sem);

/* 关闭/删除 */
int sem_close(sem_t *sem);
int sem_unlink(const char *name);

共享内存中嵌入互斥锁

POSIX互斥锁可以设置 PTHREAD_PROCESS_SHARED 属性,放置在共享内存中实现跨进程互斥:

#include <pthread.h>

typedef struct {
    pthread_mutex_t mutex;
    int              counter;
    char             data[1024];
} shared_data_t;

/* 初始化共享互斥锁 */
int shared_mutex_init(pthread_mutex_t *mutex) {
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    /* 关键:设置为进程间共享 */
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    /* 设置为健壮类型,防止持有锁的进程崩溃导致死锁 */
    pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);
    int ret = pthread_mutex_init(mutex, &attr);
    pthread_mutexattr_destroy(&attr);
    return ret;
}

/* 使用示例 */
void access_shared_data(shared_data_t *shm) {
    int ret = pthread_mutex_lock(&shm->mutex);
    if (ret == EOWNERDEAD) {
        /* 持有锁的进程崩溃,恢复互斥锁状态 */
        pthread_mutex_consistent(&shm->mutex);
        printf("Recovered from dead owner\n");
    }

    /* 安全访问共享数据 */
    shm->counter++;
    snprintf(shm->data, sizeof(shm->data), "count=%d", shm->counter);

    pthread_mutex_unlock(&shm->mutex);
}

重要提示:PTHREAD_MUTEX_ROBUST 属性是进程间共享互斥锁的关键。当持有锁的进程意外崩溃时,其他进程调用 pthread_mutex_lock 会返回 EOWNERDEAD,此时必须调用 pthread_mutex_consistent 恢复锁的一致性。

实战:生产者-消费者模型

以下实现基于共享内存+POSIX信号量的完整生产者-消费者模型。共享内存中维护一个固定大小的环形缓冲区,信号量控制同步。

/* prodcons.h - 生产者消费者模型头文件 */
#ifndef PRODCONS_H
#define PRODCONS_H

#include <stddef.h>
#include <stdint.h>
#include <semaphore.h>

#define BUF_SIZE    8       /* 环形缓冲区容量 */
#define ITEM_SIZE   256     /* 每个数据项最大长度 */
#define SHM_NAME    "/prodcons_shm"
#define SEM_EMPTY   "/prodcons_empty"
#define SEM_FULL    "/prodcons_full"
#define SEM_MUTEX   "/prodcons_mutex"

typedef struct {
    uint32_t head;                    /* 读指针 */
    uint32_t tail;                    /* 写指针 */
    uint32_t count;                   /* 当前元素数 */
    uint32_t total_produced;          /* 已生产总数 */
    uint32_t total_consumed;          /* 已消费总数 */
    char     items[BUF_SIZE][ITEM_SIZE]; /* 环形缓冲区 */
} ringbuf_t;

#endif

生产者实现

/* producer.c - 生产者 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include "prodcons.h"

int main(int argc, char *argv[]) {
    int num_items = 20;
    if (argc > 1) num_items = atoi(argv[1]);

    /* 创建共享内存 */
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd < 0) { perror("shm_open"); return 1; }
    ftruncate(fd, sizeof(ringbuf_t));

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

    /* 初始化环形缓冲区 */
    memset(buf, 0, sizeof(*buf));

    /* 创建信号量 */
    sem_t *sem_empty = sem_open(SEM_EMPTY, O_CREAT, 0666, BUF_SIZE);
    sem_t *sem_full  = sem_open(SEM_FULL,  O_CREAT, 0666, 0);
    sem_t *sem_mutex = sem_open(SEM_MUTEX, O_CREAT, 0666, 1);

    if (sem_empty == SEM_FAILED || sem_full == SEM_FAILED ||
        sem_mutex == SEM_FAILED) {
        perror("sem_open"); return 1;
    }

    /* 生产数据 */
    for (int i = 0; i < num_items; i++) {
        sem_wait(sem_empty);    /* 等待空位 */
        sem_wait(sem_mutex);    /* 获取互斥锁 */

        /* 写入环形缓冲区 */
        uint32_t idx = buf->tail;
        snprintf(buf->items[idx], ITEM_SIZE,
                  "Item-%d (produced by %d)", i, getpid());
        buf->tail = (buf->tail + 1) % BUF_SIZE;
        buf->count++;
        buf->total_produced++;

        printf("Produced: %s [buf_count=%u]\n",
               buf->items[idx], buf->count);

        sem_post(sem_mutex);    /* 释放互斥锁 */
        sem_post(sem_full);     /* 增加填充数 */

        usleep(100000);  /* 模拟生产耗时 */
    }

    printf("Producer done: %u items produced\n", buf->total_produced);

    munmap(buf, sizeof(ringbuf_t));
    sem_close(sem_empty);
    sem_close(sem_full);
    sem_close(sem_mutex);
    return 0;
}

消费者实现

/* consumer.c - 消费者 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include "prodcons.h"

int main(int argc, char *argv[]) {
    int num_items = 20;
    if (argc > 1) num_items = atoi(argv[1]);

    /* 打开共享内存 */
    int fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (fd < 0) { perror("shm_open"); return 1; }

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

    /* 打开信号量 */
    sem_t *sem_empty = sem_open(SEM_EMPTY, 0);
    sem_t *sem_full  = sem_open(SEM_FULL,  0);
    sem_t *sem_mutex = sem_open(SEM_MUTEX, 0);

    if (sem_empty == SEM_FAILED || sem_full == SEM_FAILED ||
        sem_mutex == SEM_FAILED) {
        perror("sem_open"); return 1;
    }

    /* 消费数据 */
    for (int i = 0; i < num_items; i++) {
        sem_wait(sem_full);     /* 等待有数据可消费 */
        sem_wait(sem_mutex);    /* 获取互斥锁 */

        /* 从环形缓冲区读取 */
        uint32_t idx = buf->head;
        printf("Consumed: %s [buf_count=%u]\n",
               buf->items[idx], buf->count);
        buf->head = (buf->head + 1) % BUF_SIZE;
        buf->count--;
        buf->total_consumed++;

        sem_post(sem_mutex);    /* 释放互斥锁 */
        sem_post(sem_empty);    /* 增加空位数 */

        usleep(200000);  /* 模拟消费耗时 */
    }

    printf("Consumer done: %u items consumed\n", buf->total_consumed);

    munmap(buf, sizeof(ringbuf_t));
    sem_close(sem_empty);
    sem_close(sem_full);
    sem_close(sem_mutex);

    /* 清理:由最后退出的进程负责 */
    shm_unlink(SHM_NAME);
    sem_unlink(SEM_EMPTY);
    sem_unlink(SEM_FULL);
    sem_unlink(SEM_MUTEX);

    return 0;
}
# 编译(需链接 rt 和 pthread 库)
gcc -o producer producer.c -lrt -lpthread
gcc -o consumer consumer.c -lrt -lpthread

# 运行(先启动生产者,再启动消费者)
./producer 20 &
./consumer 20

模型说明:信号量 sem_empty 初始值为 BUF_SIZE(空闲槽数),sem_full 初始值为0(已填充数),sem_mutex 初始值为1(互斥锁)。生产者先获取空位再获取锁,消费者先获取数据位再获取锁,互斥锁保证同一时刻只有一个进程操作环形缓冲区。

总结

  • System V共享内存 - 最古老的API,使用key/shmid标识,需手动ipcrm清理,适合维护旧系统
  • POSIX共享内存 - 现代API,使用文件名标识,API简洁,/dev/shm/ 下可直观管理
  • mmap文件映射 - 最通用,支持共享/私有/匿名三种模式,匿名映射适合fork的父子进程
  • 同步机制 - 共享内存本身无同步,必须配合信号量或进程间互斥锁(PTHREAD_PROCESS_SHARED)
  • 健壮互斥锁 - PTHREAD_MUTEX_ROBUST防止进程崩溃导致死锁,EOWNERDEAD时调用pthread_mutex_consistent恢复
  • 实战要点 - 环形缓冲区是共享内存的经典数据结构,三信号量模型(empty/full/mutex)是生产者-消费者的标准解法

共享内存是进程间通信性能最高的方式,但编程复杂度也最高。核心挑战在于同步机制的设计:信号量适合简单的计数同步,互斥锁适合保护复杂数据结构的访问。在工业级项目中,建议封装共享内存管理类,自动处理创建、映射、同步和异常恢复。对于无亲缘关系的进程,推荐POSIX共享内存+有名信号量的组合;对于fork产生的父子进程,匿名映射+mmap是最简单的方案。