linux学习进展 进程间通讯——共享内存

在前几节的学习中,我们了解了管道、消息队列等进程间通讯(IPC)方式,它们虽能实现进程间的数据交互,但都存在一个共同的瓶颈:数据需要在用户空间与内核空间之间来回拷贝,导致通信效率受限。而本节要学习的共享内存,正是为解决这一痛点而生------它是所有IPC机制中效率最高的一种,核心优势在于"无数据拷贝",让多个进程直接访问同一块物理内存,实现高速数据共享。

本文将从共享内存的核心原理、三种主流实现方式、实操代码示例,到常见问题与避坑要点,逐步拆解,帮助大家彻底掌握这一重要的IPC技术,为后续学习高性能Linux应用开发打下基础。

一、共享内存核心原理

Linux中每个进程都有独立的虚拟地址空间,进程之间无法直接访问彼此的内存,必须通过内核作为中介。而共享内存的核心思想,就是让内核在物理内存中开辟一块连续的物理页帧,作为共享数据的载体,然后让多个进程将这块物理内存映射到各自的虚拟地址空间中。这样一来,进程对自身虚拟地址空间中"共享区域"的读写操作,会直接映射到同一块物理内存,无需经过内核缓冲区中转,也没有数据拷贝的开销,这也是其效率极高的根本原因。

共享内存的底层实现依赖Linux的三大核心机制,缺一不可:

物理内存管理:内核为共享内存分配连续的物理页帧,并通过专门的内核结构(如struct shmid_ds)记录其地址、大小、权限等信息;

虚拟内存区域(VMA):每个参与共享的进程,其虚拟地址空间中会新增一个VMA,标记为"共享"属性,用于标识映射的共享内存范围;

多级页表:内核修改每个进程的页表,将其VMA的虚拟地址映射到同一块物理内存页帧,保证不同进程的虚拟地址虽可不同,但最终指向的物理内存一致。

补充核心特征(必记):

高效性:无内核与用户空间的数据拷贝,是所有IPC方式中速度最快的;

虚拟地址独立性:不同进程映射的虚拟地址可不同,但指向同一块物理内存;

数据持久性:共享内存由内核管理,除非主动删除,即使创建它的进程退出,数据依然保留在物理内存中;

无同步机制:内核不提供任何互斥或同步保护,多进程同时读写会导致数据竞争,需配合信号量、互斥锁使用。

二、共享内存的三种主流实现方式

Linux提供了三种共享内存实现方案,底层均基于虚拟内存映射,但接口、管理方式和适用场景不同,我们分别拆解学习,重点掌握前两种。

(一)System V 共享内存(经典方案)

System V 共享内存是Linux早期的经典实现,基于System V IPC机制,通过"键值(key)"标识共享内存段,适用于无父子关系的独立进程间通信,接口成熟但调试难度稍高。

1. 核心接口(4个系统调用)

使用System V共享内存的全流程的是:生成键值 → 创建/获取共享内存 → 映射到进程地址空间 → 读写数据 → 解除映射 → 删除共享内存,对应以下4个核心接口:

ftok():生成唯一IPC键值,用于标识共享内存段。 原型:key_t ftok(const char *pathname, int proj_id); 参数说明:pathname是已存在的文件路径(保证进程间可见),proj_id是任意整型值(用于区分不同IPC资源);返回值为生成的key值,失败返回-1。

shmget():创建或获取共享内存段。 原型:int shmget(key_t key, size_t size, int shmflg); 参数说明:key是ftok生成的键值;size是共享内存大小(按4KB页对齐,不足一页会自动补齐);shmflg是标志位(常用IPC_CREAT:不存在则创建;IPC_EXCL:与IPC_CREAT配合,确保创建新段;同时可搭配权限位如0666);返回值为共享内存标识符(shmid),失败返回-1。

shmat():将共享内存映射到进程虚拟地址空间。 原型:void *shmat(int shmid, const void *shmaddr, int shmflg); 参数说明:shmid是shmget返回的标识符;shmaddr设为NULL,由系统自动分配映射地址;shmflg设为0(读写权限)或SHM_RDONLY(只读权限);返回值为映射后的虚拟地址指针,失败返回(void *)-1。

shmdt():解除共享内存与进程的映射。 原型:int shmdt(const void *shmaddr); 参数说明:shmaddr是shmat返回的虚拟地址指针;返回值0表示成功,-1表示失败。 注意:解除映射仅断开进程与共享内存的关联,不会删除共享内存本身。

shmctl():控制共享内存(核心用于删除)。 原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf); 参数说明:shmid是共享内存标识符;cmd是控制命令(常用IPC_RMID:删除共享内存,此时buf可设为NULL);buf用于存储共享内存属性(如IPC_STAT命令用于获取属性);返回值0表示成功,-1表示失败。

2. 特点与适用场景

优点:接口成熟、跨进程无依赖,支持权限管理,适用于传统多进程服务; 缺点:基于键值管理,不易调试,共享内存段有系统级数量限制; 适用场景:无父子关系的独立进程间共享数据,如传统后台服务进程间的通信。

(二)Posix 共享内存(现代推荐方案)

Posix 共享内存是POSIX标准定义的现代方案,基于文件系统(/dev/shm,临时内存文件系统)实现,通过文件路径标识共享内存,比System V更易用、易调试,是当前主流推荐方案。

1. 核心接口与流程

使用流程:创建/打开共享内存文件 → 设置大小 → 映射到进程地址空间 → 读写数据 → 解除映射 → 删除共享内存文件,核心接口如下:

shm_open():在/dev/shm中创建或打开共享内存文件。 原型:int shm_open(const char *name, int oflag, mode_t mode); 参数说明:name是共享内存文件路径(如"/my_shm");oflag是打开标志(O_CREAT:创建,O_RDWR:读写);mode是权限位(如0666);返回值为文件描述符,失败返回-1。

ftruncate():设置共享内存的大小。 原型:int ftruncate(int fd, off_t length); 参数说明:fd是shm_open返回的文件描述符;length是共享内存大小;返回值0表示成功,-1表示失败。

mmap():将共享内存文件映射到进程虚拟地址空间。 原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 参数说明:addr设为NULL(系统自动分配);length是共享内存大小;prot是保护权限(PROT_READ|PROT_WRITE:读写);flags设为MAP_SHARED(共享映射);fd是shm_open返回的文件描述符;offset设为0;返回值为映射后的虚拟地址指针,失败返回MAP_FAILED。

munmap():解除映射,与shmdt功能类似。

shm_unlink():删除/dev/shm中的共享内存文件,释放内核资源。

2. 特点与适用场景

优点:基于文件系统,易调试(可通过ls /dev/shm查看共享内存文件)、接口简洁、无数量限制; 缺点:依赖/dev/shm文件系统,跨主机不可用; 适用场景:现代Linux应用、父子/兄弟进程、无父子关系的独立进程间高速数据共享。

(三)匿名mmap共享内存(轻量方案)

匿名mmap是最轻量的共享内存方式,基于mmap系统调用实现,仅适用于父子进程间的通信,无需管理键值或文件,依赖fork()的写时复制(COW)机制。

核心原理:父进程调用mmap(MAP_ANONYMOUS | MAP_SHARED)创建匿名共享内存,映射到自身虚拟地址空间;父进程fork()创建子进程后,子进程会继承父进程的页表和VMA,此时父子进程的虚拟地址映射到同一块物理内存(页表项标记为只读);若仅读,无数据拷贝;若某一进程写入,触发COW机制,内核为写入方分配新物理页(打破共享)。

特点:最轻量、无额外API、无需管理键值/文件;仅支持父子进程;适用于父子进程间轻量数据共享(如子进程继承父进程大内存数据,避免拷贝)。

三、实操代码示例(System V 共享内存)

下面通过"生产者-消费者"模型,实现两个独立进程(写进程+读进程)通过System V共享内存通信,帮助大家掌握接口的实际使用。

1. 公共头文件(comm.hpp)

cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

// 生成key值(路径需存在,proj_id自定义)
#define PATHNAME "./test.txt"
#define PROJ_ID 0x6666
// 共享内存大小(4KB对齐)
#define SHM_SIZE 4096

// 创建/获取共享内存
int create_shm() {
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key == -1) {
        perror("ftok error");
        exit(1);
    }
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1) {
        perror("shmget error");
        exit(1);
    }
    return shmid;
}

// 获取已存在的共享内存
int get_shm() {
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key == -1) {
        perror("ftok error");
        exit(1);
    }
    int shmid = shmget(key, SHM_SIZE, 0); // 第三个参数设为0,仅获取
    if (shmid == -1) {
        perror("shmget error");
        exit(1);
    }
    return shmid;
}

// 挂载共享内存
void* attach_shm(int shmid) {
    void* addr = shmat(shmid, NULL, 0);
    if (addr == (void*)-1) {
        perror("shmat error");
        exit(1);
    }
    return addr;
}

// 解除挂载
void detach_shm(void* addr) {
    if (shmdt(addr) == -1) {
        perror("shmdt error");
        exit(1);
    }
}

// 删除共享内存
void delete_shm(int shmid) {
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl error");
        exit(1);
    }
}

2. 写进程(writer.cpp)

cpp 复制代码
#include "comm.hpp"

int main() {
    // 1. 创建共享内存
    int shmid = create_shm();
    // 2. 挂载共享内存
    char* addr = (char*)attach_shm(shmid);
    // 3. 向共享内存写入数据
    int count = 0;
    while (count < 10) {
        sprintf(addr, "hello shared memory! count: %d", count);
        count++;
        sleep(1); // 每隔1秒写一次
    }
    // 4. 解除挂载
    detach_shm(addr);
    // 5. 删除共享内存(可由任意进程执行,建议由最后退出的进程删除)
    delete_shm(shmid);
    return 0;
}

3. 读进程(reader.cpp)

cpp 复制代码
#include "comm.hpp"

int main() {
    // 1. 获取已存在的共享内存
    int shmid = get_shm();
    // 2. 挂载共享内存
    char* addr = (char*)attach_shm(shmid);
    // 3. 读取共享内存数据
    int count = 0;
    while (count < 10) {
        printf("read from shared memory: %s\n", addr);
        count++;
        sleep(1); // 每隔1秒读一次
    }
    // 4. 解除挂载
    detach_shm(addr);
    return 0;
}

4. 编译与运行

  1. 先创建test.txt文件(ftok依赖该文件):touch test.txt 2. 编译代码:g++ writer.cpp -o writerg++ reader.cpp -o reader 3. 运行:先启动读进程(./reader),再启动写进程(./writer),即可看到读进程实时读取写进程写入的数据。

四、常见问题与避坑要点

共享内存虽高效,但使用不当易出现问题,以下是学习和开发中最常遇到的坑,务必牢记:

1. 内存泄漏(最常见)

共享内存由内核管理,即使创建它的进程退出,若未调用shmctl(IPC_RMID)删除,内存会一直存在,导致内存泄漏。 解决方法:确保至少有一个进程(通常是最后退出的进程)执行删除操作;若忘记删除,可通过命令手动删除: ipcs -m 查看共享内存信息(获取shmid), ipcrm -m shmid 删除指定共享内存。

2. 数据竞争(数据混乱)

内核不提供同步机制,多进程同时读写共享内存时,会出现数据覆盖、错乱(如写进程未写完,读进程已开始读)。 解决方法:搭配信号量、互斥锁等同步机制,保证同一时间只有一个进程读写共享内存。

3. 键值不一致,进程找不到共享内存

ftok生成key值依赖路径名和proj_id,若两个进程使用的pathname不存在,或proj_id不同,会生成不同的key,导致无法找到同一个共享内存。 解决方法:确保所有进程使用相同的pathname(且文件存在)和proj_id;避免使用IPC_PRIVATE(仅适用于父子进程)。

4. 共享内存大小设置不合理

共享内存大小按4KB页对齐,若设置的size不是4KB的整数倍,内核会自动补齐(如设置4097字节,内核会分配8192字节),造成内存浪费;若实际写入数据超过设置的size,会导致越界访问,引发程序崩溃。 解决方法:根据实际需求设置size,尽量按4KB的整数倍设置;避免越界读写。

五、总结与拓展

本节我们掌握了共享内存的核心原理、三种实现方式及实操技巧,核心要点总结如下:

共享内存的核心优势是"无数据拷贝",效率最高,底层依赖物理内存、VMA和页表映射;

System V 适用于无父子关系的传统进程,Posix 是现代推荐方案,匿名mmap仅适用于父子进程;

使用时必须注意:避免内存泄漏、解决数据竞争、保证键值一致、合理设置内存大小;

共享内存常与信号量配合使用,实现"高效通信+同步互斥",这也是后续学习的重点。

拓展思考:对比管道、消息队列和共享内存的优缺点,思考在不同场景下该如何选择合适的IPC方式?下一节我们将学习信号量,掌握如何解决共享内存的数据竞争问题。

相关推荐
比昨天多敲两行1 分钟前
Linux命令行参数,环境变量和程序地址空间
linux·运维·服务器
長安一片月2 分钟前
snort安装与使用
linux·运维·服务器
凭X而动2 分钟前
postgresql18.1部署
数据库·postgresql
万邦科技Lafite2 分钟前
京东商品详情 API 接口全面讲解
java·数据库·redis·api·电商开放平台
无风听海6 分钟前
MongoDB GridFS 一些处理细节解析
数据库·mongodb
青云计划9 分钟前
Mysql
数据库·mysql
SelectDB18 分钟前
Agent 应用范式下,企业数据基础设施如何演进?
大数据·数据库·数据分析
kyle~25 分钟前
C++---段错误(SIGSEGV)
linux·运维·c++·机器人
咸甜适中26 分钟前
rust语言学习笔记Trait之 From 和 Into (类型转换)
笔记·学习·rust
Irene199127 分钟前
(表格+词源+前端类比的方式)记忆常用 Linux 命令
linux