Linux进程间通信之内存映射

内存映射(mmap)

概述

内存映射(mmap)是一种将文件或设备映射到进程地址空间的机制。通过内存映射,进程可以像访问普通内存一样访问文件,也可以用于进程间通信。mmap提供了高效的文件I/O和进程间通信方式。

通信原理

基本概念

内存映射的特点:

  1. 文件映射:将文件映射到进程地址空间
  2. 共享映射:多个进程可以共享同一映射区域
  3. 零拷贝:直接内存访问,减少数据拷贝
  4. 虚拟内存:利用虚拟内存机制,按需加载
  5. 同步机制:可以自动同步到文件

实现机制

文件映射
  1. 创建映射

    • 使用mmap()将文件映射到进程地址空间
    • 返回映射区域的虚拟地址
    • 可以指定映射大小、权限、标志等
  2. 访问数据

    • 进程通过指针直接访问映射区域
    • 访问时触发页错误,内核按需加载文件内容
    • 修改数据时,根据标志决定是否写回文件
  3. 同步

    • msync():手动同步映射区域到文件
    • munmap():取消映射,自动同步
  4. 数据流向

    复制代码
    进程A  ──┐
             ├──>  [共享映射区域]  <──>  [文件/设备]
    进程B  ──┤
    进程C  ──┘
匿名映射(进程间通信)
  1. 创建匿名映射

    • 使用mmap()创建匿名映射(不关联文件)
    • 使用MAP_SHARED标志,多个进程可以共享
    • 通过fork()或显式共享实现进程间通信
  2. 共享机制

    • 父子进程通过fork()共享映射区域
    • 任意进程可以通过文件描述符共享(需要特殊处理)

API说明

mmap()

c 复制代码
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 功能:创建内存映射
  • 参数
    • addr:建议的映射地址(通常为NULL,让系统选择)
    • length:映射长度(字节)
    • prot:保护标志(PROT_READ可读,PROT_WRITE可写,PROT_EXEC可执行)
    • flags:映射标志(MAP_SHARED共享,MAP_PRIVATE私有,MAP_ANONYMOUS匿名等)
    • fd:文件描述符(匿名映射时为-1)
    • offset:文件偏移量(通常为0)
  • 返回值:成功返回映射地址,失败返回MAP_FAILED

munmap()

c 复制代码
int munmap(void *addr, size_t length);
  • 功能:取消内存映射
  • 参数
    • addr:映射地址
    • length:映射长度
  • 返回值:成功返回0,失败返回-1

msync()

c 复制代码
int msync(void *addr, size_t length, int flags);
  • 功能:同步映射区域到文件
  • 参数
    • addr:映射地址
    • length:同步长度
    • flags:同步标志(MS_SYNC同步写,MS_ASYNC异步写,MS_INVALIDATE使缓存无效)
  • 返回值:成功返回0,失败返回-1

示例代码

示例: 使用 mmap 读写文件

c 复制代码
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *path = "demo.txt";
    const char *msg  = "hello mmap\n";

    int fd = open(path, O_RDWR | O_CREAT, 0666);
    if (fd < 0) { perror("open"); return 1; }

    // 确保文件大小 >= 要写入的长度
    size_t len = strlen(msg);
    if (ftruncate(fd, len) == -1) { perror("ftruncate"); close(fd); return 1; }

    // 建立映射: 读写, 共享写回文件
    void *addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); close(fd); return 1; }

    // 写入数据
    memcpy(addr, msg, len);

    // 同步到文件
    if (msync(addr, len, MS_SYNC) == -1) { perror("msync"); }

    // 读取验证
    write(STDOUT_FILENO, addr, len);

    // 清理
    munmap(addr, len);
    close(fd);
    return 0;
}

示例: 多线程共享 mmap 区域并安全更新

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define THREADS 4
#define PER_THREAD 100000

struct shared {
    pthread_mutex_t lock;
    int counter;
};

void *worker(void *arg) {
    struct shared *sh = (struct shared *)arg;
    for (int i = 0; i < PER_THREAD; i++) {
        pthread_mutex_lock(&sh->lock);
        sh->counter++;
        pthread_mutex_unlock(&sh->lock);
    }
    return NULL;
}

int main() {
    const char *path = "shared.dat";
    int fd = open(path, O_RDWR | O_CREAT, 0666);
    if (fd < 0) { perror("open"); return 1; }

    // 准备文件长度
    if (ftruncate(fd, sizeof(struct shared)) == -1) { perror("ftruncate"); close(fd); return 1; }

    // 建立映射
    struct shared *sh = mmap(NULL, sizeof(struct shared),
                             PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (sh == MAP_FAILED) { perror("mmap"); close(fd); return 1; }

    // 初始化锁和计数器
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE); // 同一进程线程间共享
    pthread_mutex_init(&sh->lock, &attr);
    sh->counter = 0;

    // 创建线程
    pthread_t th[THREADS];
    for (int i = 0; i < THREADS; i++) {
        pthread_create(&th[i], NULL, worker, sh);
    }
    for (int i = 0; i < THREADS; i++) {
        pthread_join(th[i], NULL);
    }

    printf("Final counter = %d (expect %d)\n", sh->counter, THREADS * PER_THREAD);

    // 清理
    pthread_mutex_destroy(&sh->lock);
    munmap(sh, sizeof(struct shared));
    close(fd);
    return 0;
}

说明:

  • 使用 MAP_SHARED 将文件映射为共享区域
  • 通过 pthread_mutex_t 保证多线程更新的原子性
  • 如果要在多进程间共享, 将 PTHREAD_PROCESS_PRIVATE 改为 PTHREAD_PROCESS_SHARED, 并确保映射为 MAP_SHARED 且 mutex 存放于共享内存中

示例: 多进程(父子进程)共享 mmap 区域并安全更新

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>

#define PROCESSES 4
#define PER_PROCESS 100000

// 共享数据结构, 包含互斥锁和计数器
struct shared {
    pthread_mutex_t lock;  // 进程间共享的互斥锁
    int counter;           // 共享计数器
    int initialized;       // 初始化标志, 确保只初始化一次
};

int main() {
    const char *path = "shared_mmap.dat";
    int fd;
    struct shared *sh;
    pid_t pids[PROCESSES];
    int i;

    // 创建或打开共享文件
    fd = open(path, O_RDWR | O_CREAT, 0666);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 调整文件大小以容纳共享数据结构
    if (ftruncate(fd, sizeof(struct shared)) == -1) {
        perror("ftruncate");
        close(fd);
        return 1;
    }

    // 建立共享内存映射
    // MAP_SHARED 确保多个进程共享同一块物理内存
    sh = (struct shared *)mmap(NULL, sizeof(struct shared),
                               PROT_READ | PROT_WRITE,
                               MAP_SHARED, fd, 0);
    if (sh == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 父进程负责初始化共享数据结构
    if (sh->initialized == 0) {
        // 初始化进程间共享的互斥锁属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        // 关键: 设置为进程间共享, 这样多个进程可以使用同一个互斥锁
        pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
        // 初始化互斥锁
        if (pthread_mutex_init(&sh->lock, &attr) != 0) {
            perror("pthread_mutex_init");
            munmap(sh, sizeof(struct shared));
            close(fd);
            return 1;
        }
        pthread_mutexattr_destroy(&attr);
        
        // 初始化计数器
        sh->counter = 0;
        // 设置初始化标志, 防止子进程重复初始化
        sh->initialized = 1;
        printf("Parent: Initialized shared memory\n");
    }

    // 创建多个子进程
    for (i = 0; i < PROCESSES; i++) {
        pids[i] = fork();
        if (pids[i] < 0) {
            perror("fork");
            // 清理已创建的子进程
            for (int j = 0; j < i; j++) {
                kill(pids[j], SIGTERM);
            }
            munmap(sh, sizeof(struct shared));
            close(fd);
            return 1;
        } else if (pids[i] == 0) {
            // 子进程: 执行计数器递增操作
            printf("Child %d (PID %d): Starting increments\n", i, getpid());
            for (int j = 0; j < PER_PROCESS; j++) {
                // 加锁保护临界区
                pthread_mutex_lock(&sh->lock);
                sh->counter++;
                pthread_mutex_unlock(&sh->lock);
            }
            printf("Child %d (PID %d): Completed increments\n", i, getpid());
            // 子进程退出, munmap 会自动清理映射
            exit(0);
        }
    }

    // 父进程等待所有子进程完成
    printf("Parent: Waiting for all children to complete...\n");
    for (i = 0; i < PROCESSES; i++) {
        int status;
        waitpid(pids[i], &status, 0);
        if (WIFEXITED(status)) {
            printf("Parent: Child %d (PID %d) exited with status %d\n",
                   i, pids[i], WEXITSTATUS(status));
        }
    }

    // 验证最终结果
    printf("Parent: Final counter = %d (expected %d)\n",
           sh->counter, PROCESSES * PER_PROCESS);
    
    if (sh->counter == PROCESSES * PER_PROCESS) {
        printf("Parent: Success! Counter is correct.\n");
    } else {
        printf("Parent: Error! Counter mismatch (race condition detected).\n");
    }

    // 清理资源
    pthread_mutex_destroy(&sh->lock);
    munmap(sh, sizeof(struct shared));
    close(fd);
    // 可选: 删除共享文件
    // unlink(path);

    return 0;
}

说明:

  • 使用 MAP_SHARED 标志创建共享内存映射, 多个进程可以访问同一块物理内存
  • 使用 PTHREAD_PROCESS_SHARED 属性的互斥锁, 确保多个进程可以正确同步
  • 父进程负责初始化共享数据结构(互斥锁和计数器)
  • 多个子进程并发更新共享计数器, 通过互斥锁保证原子性
  • 所有进程通过 fork() 共享父进程的映射区域, 无需额外文件操作

示例: 多个独立进程通过文件共享 mmap 区域

c 复制代码
// writer.c: 写入进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

struct shared {
    pthread_mutex_t lock;
    char message[256];
    int ready;
};

int main() {
    const char *path = "shared_mmap.dat";
    int fd = open(path, O_RDWR | O_CREAT, 0666);
    if (fd < 0) { perror("open"); return 1; }

    if (ftruncate(fd, sizeof(struct shared)) == -1) {
        perror("ftruncate"); close(fd); return 1;
    }

    struct shared *sh = (struct shared *)mmap(NULL, sizeof(struct shared),
                                             PROT_READ | PROT_WRITE,
                                             MAP_SHARED, fd, 0);
    if (sh == MAP_FAILED) { perror("mmap"); close(fd); return 1; }

    // 初始化互斥锁(仅第一次)
    if (sh->ready == 0) {
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
        pthread_mutex_init(&sh->lock, &attr);
        pthread_mutexattr_destroy(&attr);
        sh->ready = 1;
    }

    // 写入数据
    pthread_mutex_lock(&sh->lock);
    snprintf(sh->message, sizeof(sh->message), "Hello from writer (PID %d)", getpid());
    printf("Writer: Wrote message\n");
    pthread_mutex_unlock(&sh->lock);

    sleep(2); // 等待读取进程读取

    munmap(sh, sizeof(struct shared));
    close(fd);
    return 0;
}
c 复制代码
// reader.c: 读取进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

struct shared {
    pthread_mutex_t lock;
    char message[256];
    int ready;
};

int main() {
    const char *path = "shared_mmap.dat";
    int fd = open(path, O_RDWR, 0666);
    if (fd < 0) { perror("open"); return 1; }

    struct shared *sh = (struct shared *)mmap(NULL, sizeof(struct shared),
                                              PROT_READ | PROT_WRITE,
                                              MAP_SHARED, fd, 0);
    if (sh == MAP_FAILED) { perror("mmap"); close(fd); return 1; }

    // 等待写入进程初始化
    while (sh->ready == 0) {
        usleep(100000); // 100ms
    }

    // 读取数据
    pthread_mutex_lock(&sh->lock);
    printf("Reader: Read message: %s\n", sh->message);
    pthread_mutex_unlock(&sh->lock);

    munmap(sh, sizeof(struct shared));
    close(fd);
    return 0;
}

说明:

  • 两个独立的程序通过同一个文件共享 mmap 映射区域
  • 写入进程负责初始化互斥锁和数据结构
  • 读取进程等待初始化完成后读取数据
  • 两个进程可以独立编译和运行, 通过文件系统协调

性能评价

优点

  1. 高效:零拷贝,直接内存访问
  2. 虚拟内存:利用虚拟内存机制,按需加载
  3. 大文件处理:适合处理大文件,无需一次性加载
  4. 进程间通信:可以用于进程间共享数据
  5. 灵活性:可以映射文件或创建匿名映射

缺点

  1. 需要同步:多进程访问需要同步机制
  2. 复杂性:API相对复杂,需要理解虚拟内存
  3. 系统限制:受虚拟内存大小限制
  4. 文件依赖:文件映射依赖文件系统

性能特点

  • 延迟:低(直接内存访问)
  • 吞吐量:高(零拷贝,适合大文件)
  • CPU占用:低(利用虚拟内存机制)
  • 内存占用:按需加载,节省内存

适用场景

  • ✅ 大文件处理
  • ✅ 需要高效文件I/O的场景
  • ✅ 进程间共享数据
  • ✅ 需要零拷贝的场景
  • ✅ 数据库、缓存等应用
  • ❌ 小文件简单读写(普通I/O更简单)
  • ❌ 不需要共享的场景

注意事项

  1. 同步机制:多进程访问共享映射时,需要同步机制(如信号量)
  2. 页大小对齐:注意系统页大小,某些操作需要页对齐
  3. 文件大小:映射大小不能超过文件大小(除非扩展文件)
  4. 权限设置:注意prot和flags的权限设置
  5. 错误处理:注意处理MAP_FAILED返回值
  6. 资源清理:使用完毕后调用munmap()取消映射
  7. 同步时机:根据需要使用msync()同步数据到文件
  8. 匿名映射:匿名映射用于进程间通信时,需要特殊机制共享

扩展阅读

  • man 2 mmap
  • man 2 munmap
  • man 2 msync
  • man 7 shm_overview
相关推荐
秋刀鱼 ..5 小时前
2026拓展现实、人机交互与人工智能国际研讨会(XRHCIAI 2026)
运维·人工智能·科技·机器学习·人机交互·制造
Jack.Jia5 小时前
【iperf3 linux版安装和使用】
linux
九思x5 小时前
通过docker-compose.yml文件一次性安装mysql、minio、redis服务
运维·docker·容器
soft20015256 小时前
《Rocky Linux 9.6 部署 MySQL 8.0 生产手册(含错误处理)》
linux·mysql·adb
猫豆~6 小时前
LAMP项目部署——8day
linux·运维·服务器·centos
毕设源码-郭学长6 小时前
【开题答辩全过程】以 基于微服务的网络运维管理系统设计与实现为例,包含答辩的问题和答案
运维·网络·微服务
徐徐图之!6 小时前
二、开始我们的运维之路&运维学习路线
linux·运维·云计算
源宇宙十三站6 小时前
Linux故障诊断系列2-诊断系统启动问题&识别硬件故障
linux
Ydwlcloud6 小时前
2025年腾讯云支付宝充值教程:步骤详解与支付渠道新思路
服务器·人工智能·云计算·腾讯云