20. 共享内存与mmap.md 11 KB

20. 共享内存与mmap

一、概述

共享内存是最快的进程间通信(IPC)方式,允许多个进程直接访问同一块物理内存。

二、共享内存概念

2.1 优势

  • 零拷贝:进程直接读写共享内存,无需内核中转
  • 高效:比管道、消息队列快一个数量级
  • 随机访问:不像管道那样只能顺序读写

2.2 挑战

  • 同步问题:需要配合信号量等机制保证数据一致性
  • 缓存一致性:多CPU缓存同步问题

三、System V共享内存

3.1 API详解

#include <sys/shm.h>
#include <sys/ipc.h>

// 创建共享内存段
int shmget(key_t key, size_t size, int shmflg);
// key: 共享内存标识符(通常用ftok生成)
// size: 共享内存大小
// shmflg: 标志(IPC_CREAT|权限)
// 返回值:共享内存ID(shmid),失败返回-1

// 将共享内存映射到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
// shmid: 共享内存ID
// shmaddr: 映射地址(通常为NULL,由系统选择)
// shmflg: 标志(SHM_RDONLY只读)
// 返回值:映射后的地址,失败返回(void*)-1

// 分离共享内存
int shmdt(const void *shmaddr);
// 返回值:成功返回0,失败返回-1

// 控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// cmd: IPC_STAT(获取状态), IPC_SET(设置状态), IPC_RMID(删除)

3.2 代码示例:进程间共享数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    key_t key = ftok("/tmp", 'a');
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:写入数据
        char *shm = shmat(shmid, NULL, 0);
        if (shm == (void*)-1) {
            perror("shmat");
            exit(1);
        }
        strcpy(shm, "Hello from child process!");
        shmdt(shm);
        exit(0);
    } else {
        // 父进程:等待子进程写入后读取
        wait(NULL);
        char *shm = shmat(shmid, NULL, SHM_RDONLY);
        if (shm == (void*)-1) {
            perror("shmat");
            exit(1);
        }
        printf("Parent received: %s\n", shm);
        shmdt(shm);
        shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
    }
    return 0;
}

编译命令:

gcc -o shm_demo shm_demo.c
./shm_demo

3.3 ftok函数

#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
// pathname: 存在的文件路径
// proj_id: 项目ID(低8位有效)
// 返回值:key_t,失败返回-1

四、mmap内存映射

4.1 mmap/munmap API

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// addr: 映射起始地址(通常NULL,系统自动选择)
// length: 映射长度
// prot: 保护标志
// flags: 映射标志
// fd: 文件描述符
// offset: 文件偏移量
// 返回值:映射后的地址,失败返回MAP_FAILED

int munmap(void *addr, size_t length);
// addr: 映射地址
// length: 映射长度
// 返回值:成功返回0,失败返回-1

4.2 保护标志(prot)

PROT_READ    // 可读
PROT_WRITE   // 可写
PROT_EXEC    // 可执行
PROT_NONE    // 不可访问

4.3 映射标志(flags)

MAP_SHARED    // 共享映射:修改会写回文件,其他进程可见
MAP_PRIVATE   // 私有映射:写时复制(COW)
MAP_FIXED     // 精确指定地址(危险)
MAP_ANONYMOUS // 匿名映射(不关联文件)
MAP_ANON      // MAP_ANONYMOUS别名

4.4 代码示例:文件映射

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main() {
    int fd = open("test.txt", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    write(fd, "Hello mmap!", 11);

    struct stat st;
    fstat(fd, &st);
    size_t length = st.st_size;

    char *mapped = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(1);
    }
    close(fd);

    printf("Original: %s\n", mapped);

    // 修改映射内容(会同步到文件)
    strcpy(mapped, "Modified mmap!");
    printf("Modified: %s\n", mapped);

    // 同步到磁盘
    msync(mapped, length, MS_SYNC);

    munmap(mapped, length);
    return 0;
}

编译命令:

gcc -o mmap_file mmap_file.c
./mmap_file
cat test.txt  # 验证文件内容已修改

4.5 代码示例:匿名共享映射

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    size_t length = 1024;
    int flags = MAP_SHARED | MAP_ANONYMOUS;
    char *shared = mmap(NULL, length, PROT_READ | PROT_WRITE, flags, -1, 0);
    if (shared == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程写入
        strcpy(shared, "Hello from child!");
        munmap(shared, length);
        exit(0);
    } else {
        // 父进程等待并读取
        wait(NULL);
        printf("Parent received: %s\n", shared);
        munmap(shared, length);
    }
    return 0;
}

编译命令:

gcc -o mmap_anon mmap_anon.c
./mmap_anon

五、mmap设备映射

5.1 嵌入式设备内存映射

在嵌入式系统中,常用mmap映射设备寄存器或framebuffer。

5.2 代码示例:Framebuffer映射

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <sys/ioctl.h>

int main() {
    int fb_fd = open("/dev/fb0", O_RDWR);
    if (fb_fd == -1) {
        perror("open framebuffer");
        exit(1);
    }

    struct fb_var_screeninfo vinfo;
    if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
        perror("ioctl");
        close(fb_fd);
        exit(1);
    }

    size_t screensize = vinfo.yres_virtual * vinfo.xres_virtual * vinfo.bits_per_pixel / 8;
    char *fb = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (fb == MAP_FAILED) {
        perror("mmap framebuffer");
        close(fb_fd);
        exit(1);
    }

    // 在屏幕中心画一个白点
    size_t x = vinfo.xres / 2;
    size_t y = vinfo.yres / 2;
    size_t location = (x + vinfo.xoffset) * (vinfo.bits_per_pixel / 8) +
                     (y + vinfo.yoffset) * vinfo.line_length;

    // 假设32位色深
    if (vinfo.bits_per_pixel == 32) {
        *(uint32_t*)(fb + location) = 0xFFFFFFFF; // 白色
    }

    munmap(fb, screensize);
    close(fb_fd);
    return 0;
}

编译命令:

gcc -o fb_demo fb_demo.c
./fb_demo

5.3 GPIO设备寄存器映射

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define GPIO_BASE 0x3F200000  // Raspberry Pi GPIO基地址
#define GPIO_SIZE 0x1000

int main() {
    int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (mem_fd == -1) {
        perror("open /dev/mem");
        exit(1);
    }

    void *gpio = mmap(NULL, GPIO_SIZE, PROT_READ | PROT_WRITE,
                      MAP_SHARED, mem_fd, GPIO_BASE);
    if (gpio == MAP_FAILED) {
        perror("mmap GPIO");
        close(mem_fd);
        exit(1);
    }

    // 读取GPIO状态寄角器(偏移0x34)
    volatile uint32_t *gplev0 = (volatile uint32_t*)((char*)gpio + 0x34);
    printf("GPIO State: 0x%08X\n", *gplev0);

    munmap(gpio, GPIO_SIZE);
    close(mem_fd);
    return 0;
}

编译命令:

gcc -o gpio_demo gpio_demo.c
sudo ./gpio_demo  # 需要root权限访问/dev/mem

六、共享内存的同步问题

6.1 为什么需要同步

  • 多进程同时读写共享内存会导致数据竞争
  • 需要信号量或其他同步机制保证互斥

6.2 信号量配合使用

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

#define SHM_NAME "/my_shared_mem"
#define SEM_NAME "/my_semaphore"

typedef struct {
    int value;
    sem_t sem;
} shared_data_t;

int main() {
    // 创建共享内存
    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    ftruncate(shm_fd, sizeof(shared_data_t));
    shared_data_t *shared = mmap(NULL, sizeof(shared_data_t),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);

    // 初始化信号量
    sem_init(&shared->sem, 1, 1); // 1: 进程间共享
    shared->value = 0;

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:递增共享变量
        for (int i = 0; i < 100000; i++) {
            sem_wait(&shared->sem);
            shared->value++;
            sem_post(&shared->sem);
        }
        munmap(shared, sizeof(shared_data_t));
        exit(0);
    } else {
        // 父进程:递增共享变量
        for (int i = 0; i < 100000; i++) {
            sem_wait(&shared->sem);
            shared->value++;
            sem_post(&shared->sem);
        }
        wait(NULL);
        printf("Final value: %d\n", shared->value);

        sem_destroy(&shared->sem);
        munmap(shared, sizeof(shared_data_t));
        shm_unlink(SHM_NAME);
    }
    return 0;
}

编译命令:

gcc -o shm_sync shm_sync.c -lrt -lpthread
./shm_sync

七、嵌入式场景

7.1 Framebuffer映射

  • 直接映射显存,高效图形渲染
  • 支持硬件加速和DMA操作

7.2 多进程共享数据

  • 数据采集进程与处理进程共享缓冲区
  • 实时数据处理流水线

7.3 设备寄存器映射

  • 直接操作硬件寄存器
  • 避免系统调用开销
  • 适用于GPIO、UART、SPI等外设控制

7.4 DMA缓冲区映射

  • 与DMA控制器共享内存
  • 高速数据传输(如网络包、视频流)

八、注意事项

  1. 权限管理:/dev/mem需要root权限
  2. 缓存一致性:多核CPU需考虑缓存同步
  3. 内存对齐:某些硬件要求特定对齐方式
  4. 错误处理:mmap失败返回MAP_FAILED(不是NULL)
  5. 资源清理:使用后munmap,删除shm_open创建的对象

九、面试要点

  1. mmap与read/write的区别:mmap零拷贝,减少内核态/用户态切换
  2. MAP_SHARED与MAP_PRIVATE:共享映射会同步文件,私有映射写时复制
  3. 共享内存的同步机制:信号量、互斥锁、条件变量
  4. mmap在嵌入式中的应用:framebuffer、设备寄存器、DMA缓冲区
  5. 共享内存的优缺点:高效但需要同步,适用于大数据量IPC