Linux 下的 C 语言共享内存(Shared Memory)是进程间通信(IPC)中最快的一种方式。它允许两个或多个进程访问同一块物理内存空间,因为数据不需要在客户机和服务器之间复制,所以速度非常快。
在 Linux 中,主要有两套共享内存的 API 标准:
- System V 共享内存 (老牌,经典,使用 shmget, shmat 等)
- POSIX 共享内存 (新标准,更符合文件操作习惯,使用 shm_open, mmap 等)
下面我将详细介绍 POSIX 共享内存的用法,并提供一个包含**同步机制(互斥锁)**的完整代码示例。
核心 API 简介
使用 POSIX 共享内存主要涉及以下几个步骤和函数:
shm_open(): 创建或打开一个共享内存对象。- 类似于
open(),但它不操作普通文件,而是操作内存对象。 - 返回一个文件描述符(file descriptor)。
- 类似于
ftruncate(): 设置共享内存对象的大小。- 新创建的共享内存对象大小为 0,必须调用此函数设置大小。
mmap(): 将共享内存对象映射到进程的虚拟地址空间。- 这是读写数据的关键步骤,映射后就可以像操作普通指针一样操作这块内存。
munmap(): 解除映射。- 进程结束或不再需要时调用。
shm_unlink(): 删除共享内存对象。- 类似于
unlink(),将对象从系统中移除。如果不调用,即使进程结束,内存对象也会一直存在于/dev/shm中(直到重启)。
- 类似于
进阶示例:带互斥锁同步的共享内存
为什么需要同步?
共享内存本身是最快的 IPC 方式,但它不提供 任何同步机制。如果一个进程正在写,另一个进程同时在读,就会读到脏数据。因此,在生产环境中,我们通常会在共享内存中放入一个互斥锁(Mutex)。
场景设计
- Protocol.h: 定义共享的数据结构。
- Writer.c: 初始化共享内存和锁,写入数据。
- Reader.c: 打开共享内存,加锁读取数据。
1. 公共头文件 (protocol.h)
这里定义了共享内存的名称、大小以及数据结构。
c
#ifndef PROTOCOL_H
#define PROTOCOL_H
#include <pthread.h>
// 共享内存对象的名称,必须以 "/" 开头
#define SHM_NAME "/demo_posix_shm"
// 定义共享内存中存放的数据结构
typedef struct {
// 进程间互斥锁,必须设置为 PTHREAD_PROCESS_SHARED
pthread_mutex_t mutex;
// 一个标志位,用于简单的逻辑控制(例如:数据是否已更新)
int data_version;
// 实际的数据载荷
char buffer[256];
} SharedData;
#endif
2. 写入端 (writer.c)
负责创建共享内存、初始化互斥锁,并写入数据。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // For O_* constants
#include <sys/mman.h> // For mmap, shm_open
#include <unistd.h> // For ftruncate, close
#include <pthread.h> // For mutex
#include <errno.h>
#include "protocol.h"
int main() {
// 1. 创建共享内存对象
// O_CREAT: 不存在则创建
// O_RDWR: 读写模式
// O_TRUNC: 如果已存在,将其截断(清空)
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR | O_TRUNC, 0666);
if (shm_fd == -1) {
perror("shm_open failed");
exit(EXIT_FAILURE);
}
// 2. 设置共享内存大小
if (ftruncate(shm_fd, sizeof(SharedData)) == -1) {
perror("ftruncate failed");
exit(EXIT_FAILURE);
}
// 3. 内存映射
SharedData *shared_mem = (SharedData *)mmap(NULL, sizeof(SharedData),
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
if (shared_mem == MAP_FAILED) {
perror("mmap failed");
exit(EXIT_FAILURE);
}
// 4. 初始化互斥锁 (非常关键的一步)
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// 设置互斥锁属性为进程间共享 (PTHREAD_PROCESS_SHARED)
// 默认的互斥锁只能在同一个进程的线程间使用
if (pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0) {
perror("pthread_mutexattr_setpshared failed");
exit(EXIT_FAILURE);
}
pthread_mutex_init(&shared_mem->mutex, &attr);
// 初始化数据
shared_mem->data_version = 0;
memset(shared_mem->buffer, 0, sizeof(shared_mem->buffer));
printf("Writer: Shared memory initialized. Start writing...\n");
// 5. 模拟多次写入
for (int i = 1; i <= 5; i++) {
sleep(1); // 模拟耗时操作
// --- 进入临界区 ---
pthread_mutex_lock(&shared_mem->mutex);
shared_mem->data_version = i;
snprintf(shared_mem->buffer, sizeof(shared_mem->buffer), "Message ID #%d", i);
printf("Writer: Wrote '%s'\n", shared_mem->buffer);
pthread_mutex_unlock(&shared_mem->mutex);
// --- 退出临界区 ---
}
printf("Writer: Done. Waiting for reader to finish (press Enter to cleanup)...\n");
getchar();
// 6. 清理资源
// 销毁锁
pthread_mutex_destroy(&shared_mem->mutex);
pthread_mutexattr_destroy(&attr);
// 解除映射
munmap(shared_mem, sizeof(SharedData));
// 关闭文件描述符
close(shm_fd);
// 删除共享内存对象
shm_unlink(SHM_NAME);
printf("Writer: Cleanup complete.\n");
return 0;
}
3. 读取端 (reader.c)
负责打开共享内存,并在锁的保护下读取数据。
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>
#include "protocol.h"
int main() {
// 1. 打开共享内存对象
// 注意:这里不需要 O_CREAT,因为假设 Writer 已经创建了
int shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
if (shm_fd == -1) {
perror("Reader: shm_open failed (is writer running?)");
exit(EXIT_FAILURE);
}
// 2. 内存映射
SharedData *shared_mem = (SharedData *)mmap(NULL, sizeof(SharedData),
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
if (shared_mem == MAP_FAILED) {
perror("mmap failed");
exit(EXIT_FAILURE);
}
printf("Reader: Attached to shared memory. Start reading...\n");
int last_read_version = 0;
// 3. 循环读取
for (int i = 0; i < 10; i++) {
sleep(1);
// --- 进入临界区 ---
pthread_mutex_lock(&shared_mem->mutex);
if (shared_mem->data_version > last_read_version) {
printf("Reader: Detected new data (v%d): %s\n",
shared_mem->data_version, shared_mem->buffer);
last_read_version = shared_mem->data_version;
} else {
printf("Reader: No new data yet...\n");
}
pthread_mutex_unlock(&shared_mem->mutex);
// --- 退出临界区 ---
}
// 4. 解除映射
munmap(shared_mem, sizeof(SharedData));
close(shm_fd);
// 注意:Reader 通常不负责 shm_unlink,除非它是最后一个退出的进程
printf("Reader: Detached and exiting.\n");
return 0;
}
编译与运行
POSIX 共享内存和 pthread 库通常需要链接 rt (Realtime) 和 pthread 库。
编译命令:
bash
gcc writer.c -o writer -lrt -lpthread
gcc reader.c -o reader -lrt -lpthread
运行步骤:
-
打开一个终端,运行 Writer:
bash./writerWriter 会输出正在写入的消息。
-
打开另一个终端,运行 Reader:
bash./readerReader 会检测到 Writer 写入的新数据并打印出来。
-
观察系统状态 :
在程序运行时,你可以查看
/dev/shm目录,你会看到一个名为demo_posix_shm的文件:bashls -l /dev/shm/
关键点总结
- 链接库 :编译时必须加上
-lrt(对于shm_open) 和-lpthread(对于互斥锁)。 MAP_SHARED:在mmap时必须使用MAP_SHARED,否则修改对其他进程不可见。- 互斥锁属性 :普通的
pthread_mutex只能用于线程同步。用于进程间同步时,必须调用pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED)。 - 生命周期 :POSIX 共享内存具有内核持久性 。如果程序崩溃且没有调用
shm_unlink,该内存对象会一直存在,直到系统重启或手动删除(rm /dev/shm/demo_posix_shm)。