【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. 共享内存是最快的进程间通信方式,无需数据拷贝,直接操作物理内存,适合传输大量数据。
相关推荐
AlfredZhao5 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户97183563346611 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪12 小时前
linux 拷贝文件或目录到指定的位置
linux
大树881 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
小宇宙Zz1 天前
Maven依赖冲突
java·服务器·maven
不会C语言的男孩1 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈1 天前
Unix 与 Linux 异同小叙
linux·服务器·unix