【Linux 系统】进程间的通信方式

一、理解共享内存的核心基础

1. 共享内存是什么?

普通进程的内存是相互隔离的(进程A不能直接访问进程B的内存),而共享内存 是操作系统在物理内存中开辟的一块公共内存区域,让多个进程可以直接映射到自己的私有地址空间中,就像操作自己的局部变量一样操作这块内存,是进程间通信中速度最快的方式(无需数据拷贝,直接访问物理内存)。

2. 核心原理(4个函数对应4个步骤)

整个共享内存的使用流程就像"租房-入住-退房-销毁房源":

  1. shmget()创建/获取共享内存(相当于找中介租/找一套已有的房源,获得房源编号)
  2. shmat()挂载共享内存(相当于拿到钥匙,把共享内存映射到自己的进程地址空间,才能读写)
  3. shmdt()卸载共享内存(相当于退房,断开自己的进程与共享内存的映射,不再访问)
  4. shmctl()控制/销毁共享内存(相当于房源到期,联系中介销毁房源,释放物理内存)

3. 共享内存的生命周期

共享内存的生命周期不依赖于进程 ,而是依赖于操作系统内核

  • 即使创建/使用共享内存的进程全部退出,共享内存也会一直存在于内核中,直到被 shmctl() 主动销毁,或者重启操作系统。
  • 这一点和管道不同(管道随进程退出而销毁),也是共享内存的关键特点(避免数据丢失,但也可能造成内存泄露,必须手动销毁)。

4. 关键前提:两个进程如何找到同一块共享内存?

共享内存的key值 (一个唯一的整数,相当于房源的唯一编号),两个进程必须使用相同的 key 值,才能找到同一块共享内存,实现通信。


二、分步实现代码(两个独立文件:进程A、进程B)

因为是两个独立进程(不是父子进程,更贴近实际开发场景),我们创建两个 .c 文件,分别对应进程A(写数据)和进程B(读数据)。

前置准备

编译运行依赖GCC编译器,两个文件要在同一目录下操作。


三、进程A(写数据:shm_writer.c,对应需求中的进程A)

功能:创建共享内存 → 挂载 → 写入 "i am process A" → 卸载 → (可选:等待进程B读取后销毁)

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>   // 共享内存、消息队列等IPC通信的头文件
#include <sys/shm.h>   // 共享内存核心函数的头文件
#include <string.h>
#include <unistd.h>

// 1. 定义共享内存的关键参数(两个进程必须一致!)
#define SHM_KEY 0x12345678  // 唯一key值(自定义,只要是非0整数即可,两个进程要相同)
#define SHM_SIZE 1024       // 共享内存大小(1024字节,足够存储要写入的字符串)

int main() {
    int shmid;  // 共享内存ID(相当于房源编号,由shmget返回)
    char *shm_addr;  // 挂载后,共享内存在当前进程中的地址(相当于房间的门牌号)

    // 2. 步骤1:创建共享内存(shmget())
    // 参数说明:
    // ① key:共享内存唯一标识(两个进程要相同)
    // ② size:共享内存大小(字节)
    // ③ IPC_CREAT | IPC_EXCL | 0666:创建新的共享内存,若已存在则报错;0666是读写权限(所有用户可读写)
    shmid = shmget((key_t)SHM_KEY, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1) {
        perror("shmget failed (创建共享内存失败)");
        return 1;
    }
    printf("进程A:共享内存创建成功,共享内存ID = %d\n", shmid);

    // 3. 步骤2:挂载共享内存(shmat())
    // 参数说明:
    // ① shmid:共享内存ID(shmget返回的房源编号)
    // ② shmaddr:指定映射地址(NULL表示让系统自动分配,推荐)
    // ③ shmflg:挂载权限(0表示可读可写)
    shm_addr = (char *)shmat(shmid, NULL, 0);
    if (shm_addr == (char *)-1) {  // 挂载失败返回 (void*)-1,强制转换为char*判断
        perror("shmat failed (挂载共享内存失败)");
        // 挂载失败,先销毁已创建的共享内存,避免内存泄露
        shmctl(shmid, IPC_RMID, NULL);
        return 1;
    }
    printf("进程A:共享内存挂载成功,映射地址 = %p\n", shm_addr);

    // 4. 步骤3:向共享内存写入数据(直接操作shm_addr,和操作普通数组一样)
    const char *write_data = "i am process A";
    // 把字符串复制到共享内存(strcpy:字符串拷贝,自动携带\0结束符)
    strcpy(shm_addr, write_data);
    printf("进程A:已向共享内存写入数据:%s\n", write_data);

    // 5. 步骤4:卸载共享内存(shmdt())
    // 参数:shm_addr(挂载时返回的映射地址,相当于归还钥匙)
    if (shmdt(shm_addr) == -1) {
        perror("shmdt failed (卸载共享内存失败)");
    } else {
        printf("进程A:共享内存卸载成功\n");
    }

    // 6. (可选)等待进程B读取完成后,销毁共享内存(避免手动清理)
    // 共享内存生命周期不依赖进程,这里休眠10秒,给进程B足够的读取时间
    printf("进程A:休眠10秒,等待进程B读取数据...\n");
    sleep(10);

    // 7. 步骤5:销毁共享内存(shmctl())
    // 参数说明:
    // ① shmid:共享内存ID
    // ② IPC_RMID:执行销毁操作(释放物理内存)
    // ③ buf:额外参数(NULL表示无需返回共享内存信息)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl failed (销毁共享内存失败)");
        return 1;
    }
    printf("进程A:共享内存已销毁,程序退出\n");

    return 0;
}

四、进程B(读数据:shm_reader.c,对应需求中的进程B)

功能:获取已创建的共享内存 → 挂载 → 读取内容并打印 → 卸载

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>

// 注意:这里的SHM_KEY和SHM_SIZE必须和进程A完全一致!
#define SHM_KEY 0x12345678
#define SHM_SIZE 1024

int main() {
    int shmid;
    char *shm_addr;

    // 1. 步骤1:获取已存在的共享内存(shmget(),不创建新的)
    // 参数说明:去掉 IPC_EXCL,只获取已存在的共享内存(进程A已创建)
    shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget failed (获取共享内存失败,可能进程A未运行)");
        return 1;
    }
    printf("进程B:共享内存获取成功,共享内存ID = %d\n", shmid);

    // 2. 步骤2:挂载共享内存(和进程A用法一致)
    shm_addr = (char *)shmat(shmid, NULL, 0);
    if (shm_addr == (char *)-1) {
        perror("shmat failed (挂载共享内存失败)");
        return 1;
    }
    printf("进程B:共享内存挂载成功,映射地址 = %p\n", shm_addr);

    // 3. 步骤3:从共享内存读取数据并打印(直接操作shm_addr)
    printf("进程B:从共享内存读取到数据:%s\n", shm_addr);

    // 4. 步骤4:卸载共享内存(和进程A用法一致)
    if (shmdt(shm_addr) == -1) {
        perror("shmdt failed (卸载共享内存失败)");
    } else {
        printf("进程B:共享内存卸载成功,程序退出\n");
    }

    return 0;
}

五、编译与运行(关键步骤,必须按顺序)

1. 编译两个文件

打开终端,进入文件所在目录,执行两条编译命令:

bash 复制代码
# 编译进程A(写数据)
gcc shm_writer.c -o shm_writer
# 编译进程B(读数据)
gcc shm_reader.c -o shm_reader

2. 运行顺序(必须先运行进程A,再运行进程B)

第一步:运行进程A(创建共享内存并写入数据)
bash 复制代码
./shm_writer

此时终端会输出:

复制代码
进程A:共享内存创建成功,共享内存ID = xxxx
进程A:共享内存挂载成功,映射地址 = 0xxxxxxxx
进程A:已向共享内存写入数据:i am process A
进程A:共享内存卸载成功
进程A:休眠10秒,等待进程B读取数据...

进程A会进入10秒休眠,此时共享内存已创建且数据已写入,等待进程B读取。

第二步:快速打开另一个终端,运行进程B(读取数据)
bash 复制代码
./shm_reader

此时终端会输出:

复制代码
进程B:共享内存获取成功,共享内存ID = xxxx(和进程A的ID一致)
进程B:共享内存挂载成功,映射地址 = 0xxxxxxxx(可能和进程A不同,正常)
进程B:从共享内存读取到数据:i am process A
进程B:共享内存卸载成功,程序退出
第三步:等待10秒后,进程A终端输出
复制代码
进程A:共享内存已销毁,程序退出

六、关键函数详细解释

1. shmget() - 创建/获取共享内存

  • 核心作用:申请或查找一块共享内存,返回唯一的共享内存ID(shmid)。
  • 关键参数:
    • key:共享内存唯一标识,两个进程必须相同,才能找到同一块共享内存。
    • size:共享内存大小,必须是正整数(推荐为页面大小的整数倍,一般4096字节,这里用1024足够)。
    • flag:权限标志,IPC_CREAT(创建新的)、IPC_EXCL(和IPC_CREAT配合,若已存在则报错)、0666(读写权限,和文件权限一致)。

2. shmat() - 挂载共享内存

  • 核心作用:将共享内存映射到当前进程的地址空间,返回映射后的内存地址(shm_addr),只有挂载后才能读写共享内存。
  • 关键参数:shmidshmget返回的ID)、NULL(系统自动分配映射地址)、0(可读可写权限)。
  • 注意:返回值为 (void*)-1 表示挂载失败,需要强制转换为对应类型判断。

3. shmdt() - 卸载共享内存

  • 核心作用:断开当前进程与共享内存的映射关系(相当于"退房"),但不会销毁共享内存
  • 关键参数:shm_addrshmat返回的映射地址)。
  • 注意:进程退出前必须卸载,否则会造成资源泄露。

4. shmctl() - 控制/销毁共享内存

  • 核心作用:对共享内存进行控制操作,最常用的是 IPC_RMID(销毁共享内存,释放物理内存)。
  • 关键参数:shmid(共享内存ID)、IPC_RMID(销毁操作)、NULL(无需额外信息)。
  • 注意:只有调用该函数,共享内存才会真正被销毁,否则会一直存在于内核中。

七、常见问题与注意事项

  1. 运行进程B报错"获取共享内存失败" :原因是没先运行进程A,或者进程A的SHM_KEY和进程B不一致。

  2. 共享内存泄露 :如果进程A异常退出,没有执行shmctl()销毁共享内存,可通过以下命令手动查看和删除:

    bash 复制代码
    # 查看所有共享内存
    ipcs -m
    # 删除指定ID的共享内存(替换xxxx为共享内存ID)
    ipcrm -m xxxx
  3. 字符串写入注意 :必须保证共享内存大小足够存储字符串(包括\0),否则会造成内存越界。


总结

  1. 共享内存是内核中的公共物理内存,生命周期不依赖进程,需用shmctl()手动销毁,核心流程是"创建/获取→挂载→读写→卸载→销毁"。
  2. 四个核心函数分工明确:shmget()(创/取)、shmat()(挂载)、shmdt()(卸载)、shmctl()(销毁)。
  3. 两个进程必须使用相同的key值和共享内存大小,才能实现通信,运行顺序需遵循"先写后读"。
  4. 共享内存是最快的进程间通信方式,无需数据拷贝,直接操作物理内存,适合传输大量数据。
相关推荐
Abona2 小时前
C语言嵌入式全栈Demo
linux·c语言·面试
心理之旅2 小时前
高校文献检索系统
运维·服务器·容器
Lenyiin2 小时前
Linux 基础IO
java·linux·服务器
The Chosen One9852 小时前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器
Kira Skyler3 小时前
eBPF debugfs中的追踪点format实现原理
linux
2501_927773073 小时前
uboot挂载
linux·运维·服务器
wdfk_prog4 小时前
[Linux]学习笔记系列 -- [drivers][dma]dmapool
linux·笔记·学习
Tim风声(网络工程师)4 小时前
防火墙-长链接、介绍作用
运维·服务器·网络
小徐敲java4 小时前
(运维)1Panel服务器面板Docker部署
运维·服务器·docker