共享内存概述
共享内存(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是最简单的方案。