8、共享内存实战:共享内存的权限控制、共享内存的大小限制、共享内存的生命周期、共享内存与信号量的配合使用、共享内存的优缺点、共享内存的应用场景、共享内存的常见错误与调试、共享内存综合案例

共享内存,说白了就是让两个或多个进程直接读写同一块物理内存。它不像管道、消息队列那样需要数据拷贝,所以速度极快。我刚开始接触Linux进程间通信时,觉得共享内存简直就是神器——后来才发现,用不好它,坑也不少。

8.1 共享内存的权限控制

共享内存的权限控制,核心在于shmget()的第三个参数——权限标志位。它和文件权限类似,但又不完全一样。

权限标志位格式:

IPC_CREAT | IPC_EXCL | 0666

其中 0666 表示所有用户可读写,0600 仅属主可读写。

我在项目中遇到过一个问题:两个进程用共享内存通信,一个进程是root启动的,另一个是普通用户。结果普通用户死活打不开共享内存。后来一查,是权限设成了0600。改成0666后问题解决。嗯,权限这东西,真不能想当然。

小技巧:ipcs -m 查看当前系统的共享内存段,权限列会显示类似 666 的数字。如果发现权限不对,可以用 ipcrm -M key 删除重建。

8.2 共享内存的大小限制

共享内存不是你想开多大就能开多大的。系统有硬性限制。我见过新手一上来就申请1GB共享内存,结果shmget()直接返回-1。

限制参数 默认值 说明
SHMMAX 通常为 32MB ~ 128MB 单个共享内存段的最大字节数
SHMMIN 1 单个共享内存段的最小字节数
SHMMNI 4096 系统允许的最大共享内存段数量
SHMALL 取决于内存大小 系统允许的共享内存总页数

你可以用 sysctl kernel.shmmax 查看当前值。想改的话,sysctl -w kernel.shmmax=268435456 就能临时改成256MB。不过我个人建议,别改太大——共享内存是物理内存,你占多了,其他进程就得去交换分区,系统性能会急剧下降。

注意:共享内存的大小必须是系统页大小的整数倍。用 getpagesize() 查看页大小,通常是4096字节。如果你申请5000字节,系统实际会分配8192字节,多出来的部分就是浪费。

8.3 共享内存的生命周期

共享内存的生命周期和进程不同步。这是它和管道、消息队列最大的区别。进程退出后,共享内存不会自动销毁。它一直留在内核里,直到你显式调用shmctl()删除,或者系统重启。

为什么会这样?因为共享内存的设计初衷就是让多个进程共享数据,哪怕所有进程都退出了,数据也应该保留,供后续启动的进程读取。你想想看,如果进程一退出内存就没了,那还叫「共享」吗?

我曾经在调试一个服务程序时,发现共享内存越用越多。查了半天,原来是服务异常退出时没来得及清理共享内存。每次重启都新建一个,旧的还留着。最后用 ipcs -m 一看,好家伙,几十个废弃的共享内存段。

建议:在程序入口处,先尝试用 shmget()IPC_EXCL 标志创建共享内存。如果失败,说明已经存在,就用 shmctl() 删除旧的再重建。这样能避免重复创建的问题。

8.4 共享内存与信号量的配合使用

共享内存本身没有同步机制。两个进程同时写同一块内存,数据就乱了。这时候就需要信号量来帮忙。

我习惯的做法是:用信号量保护共享内存的读写操作。写之前申请信号量,写完后释放。读之前也申请信号量,读完后释放。这样就能保证同一时刻只有一个进程在操作共享内存。

// 伪代码示例
#include <sys/sem.h>
#include <sys/shm.h>

// 创建信号量
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
// 初始化信号量值为1
semctl(semid, 0, SETVAL, 1);

// 创建共享内存
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
void *shmaddr = shmat(shmid, NULL, 0);

// 写操作
struct sembuf sb = {0, -1, 0};  // P操作,申请资源
semop(semid, &sb, 1);
// 写入共享内存
memcpy(shmaddr, data, size);
sb.sem_op = 1;  // V操作,释放资源
semop(semid, &sb, 1);

// 读操作
sb.sem_op = -1;
semop(semid, &sb, 1);
// 读取共享内存
memcpy(data, shmaddr, size);
sb.sem_op = 1;
semop(semid, &sb, 1);

核心要点:信号量的P操作和V操作必须成对出现。少一个,就会导致死锁。我见过最典型的错误就是:写操作时申请了信号量,但写完后忘记释放。结果读进程永远等不到信号量,整个系统卡死。

8.5 共享内存的优缺点

优点很明显:

  • 速度快:没有数据拷贝,直接读写内存。比管道快10倍以上。
  • 数据量大:可以传输任意大小的数据,不像管道有缓冲区限制。
  • 持久化:进程退出后数据还在,适合需要持久化的场景。

缺点也不少:

  • 同步复杂:必须自己用信号量或互斥锁控制并发访问。
  • 生命周期管理麻烦:容易产生废弃的共享内存段,占用系统资源。
  • 调试困难:多个进程同时操作同一块内存,出问题很难定位。
  • 跨平台性差:System V共享内存是Linux特有的,Windows上不兼容。

8.6 共享内存的应用场景

共享内存最适合的场景是:高频数据交换大数据量传输

我做过一个视频采集系统,摄像头每秒产生30帧图像,每帧2MB。如果用管道传输,CPU占用率直接飙到80%。换成共享内存后,CPU占用率降到10%以下。说白了,共享内存就是为这种场景设计的。

其他常见场景:

  • 数据库缓存:多个数据库进程共享同一块缓存数据。
  • 实时监控系统:采集进程写数据,展示进程读数据。
  • 游戏引擎:多个模块共享场景数据。
  • 日志系统:多个进程写日志到同一块共享内存,再由一个进程统一写入磁盘。

8.7 共享内存的常见错误与调试

我总结了几种最常见的错误:

  1. 权限错误:shmget()返回-1,errnoEACCES。检查权限标志位。
  2. 内存越界:写入的数据超过共享内存大小。这会导致其他进程读到脏数据,甚至段错误。
  3. 忘记shmdt()进程退出前没有分离共享内存。虽然进程退出时会自动分离,但这是个好习惯。
  4. 信号量死锁:申请了信号量但没释放,或者释放了多次。
  5. 共享内存泄漏:进程退出前没有调用shmctl()删除共享内存。用ipcs -m定期检查。

调试技巧:strace 跟踪进程的系统调用,能看到 shmget()shmat()shmdt() 的返回值。如果返回-1,strace 会直接显示错误原因,比看代码快多了。

8.8 共享内存综合案例

下面是一个完整的例子:两个进程通过共享内存交换数据,用信号量保证同步。

// shm_server.c - 创建共享内存并写入数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    // 创建信号量
    int semid = semget(1234, 1, IPC_CREAT | 0666);
    semctl(semid, 0, SETVAL, 1);

    // 创建共享内存
    int shmid = shmget(5678, SHM_SIZE, IPC_CREAT | 0666);
    char *shmaddr = (char *)shmat(shmid, NULL, 0);

    // 写入数据
    struct sembuf sb = {0, -1, 0};
    semop(semid, &sb, 1);
    strcpy(shmaddr, "Hello from server!");
    sb.sem_op = 1;
    semop(semid, &sb, 1);

    printf("Server: data written\n");
    sleep(10);  // 等待客户端读取

    // 清理
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
    semctl(semid, 0, IPC_RMID);
    return 0;
}
// shm_client.c - 读取共享内存中的数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/sem.h>

#define SHM_SIZE 1024

int main() {
    // 获取信号量
    int semid = semget(1234, 1, 0666);
    // 获取共享内存
    int shmid = shmget(5678, SHM_SIZE, 0666);
    char *shmaddr = (char *)shmat(shmid, NULL, 0);

    // 读取数据
    struct sembuf sb = {0, -1, 0};
    semop(semid, &sb, 1);
    printf("Client: received: %s\n", shmaddr);
    sb.sem_op = 1;
    semop(semid, &sb, 1);

    // 分离
    shmdt(shmaddr);
    return 0;
}

先运行shm_server,再运行shm_client。你会看到客户端成功读取到服务端写入的数据。这就是共享内存配合信号量的基本用法。

扩展思考:如果多个客户端同时读取,信号量会串行化所有操作。你可以用读写锁(信号量值为N)来优化:允许N个读者同时读,但写者必须独占。

共享内存知识体系 共享内存 权限控制 大小限制 生命周期 信号量配合 优缺点 应用场景 常见错误与调试 综合案例 共享内存是Linux进程间通信的核心机制之一

共享内存这块内容,说难不难,说简单也不简单。关键是要理解它的生命周期和同步机制。我刚开始用共享内存时,也踩过不少坑。但只要掌握了上面这些要点,你就能在实际项目中游刃有余了。

公众号:蓝海资料掘金营,微信deep3321