Linux 共享内存详解

1. 概述

1.1 什么是共享内存?

共享内存的定义

共享内存(Shared Memory)是一种进程间通信(IPC)机制,允许多个进程访问同一块物理内存区域。这是最快的 IPC 方式,因为数据不需要在内核和用户空间之间复制。

共享内存的特点

  1. 高性能:数据直接映射到进程地址空间,无需复制
  2. 低延迟:访问速度接近直接内存访问
  3. 大容量:可以共享大量数据
  4. 持久性:数据在进程退出后仍然存在(直到显式删除)

共享内存的用途

  1. 高性能数据传输:进程间大量数据交换
  2. 数据库系统:共享缓存和缓冲区
  3. 图形系统:共享图形缓冲区
  4. 科学计算:共享计算结果

1.2 Linux 中的共享内存类型

Linux 提供了两种主要的共享内存机制:

  1. System V 共享内存

    • APIshmget(), shmat(), shmdt(), shmctl()
    • 特点:传统的 IPC 机制,基于键值(key)
    • 实现 :基于 tmpfs 文件系统
  2. POSIX 共享内存

    • APIshm_open(), shm_unlink(), mmap()
    • 特点:POSIX 标准,基于文件名
    • 实现 :基于 tmpfs 文件系统(通常挂载在 /dev/shm
  3. 内存映射文件(mmap)

    • APImmap(), munmap()
    • 特点:可以映射文件或匿名内存
    • 实现:直接内存映射

1.3 共享内存与普通内存的区别

特性 普通内存 共享内存
可见性 仅当前进程可见 多个进程可见
生命周期 进程退出时释放 显式删除或系统重启
访问方式 直接访问 需要先 attach
同步 不需要 需要同步机制(信号量、锁等)

2. System V 共享内存

2.1 System V 共享内存 API

核心系统调用

  1. shmget():创建或获取共享内存段
  2. shmat():将共享内存段附加到进程地址空间
  3. shmdt():从进程地址空间分离共享内存段
  4. shmctl():控制共享内存段(获取信息、设置权限、删除等)

函数原型

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

// 创建或获取共享内存段
int shmget(key_t key, size_t size, int shmflg);

// 附加共享内存段到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);

// 从进程地址空间分离共享内存段
int shmdt(const void *shmaddr);

// 控制共享内存段
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

2.2 shmget() 的实现原理

shmget() 的作用

创建新的共享内存段或获取已存在的共享内存段的标识符。

内核实现

c 复制代码
// ipc/shm.c
SYSCALL_DEFINE3(shmget, key_t, key, size_t, size, int, shmflg)
{
    struct ipc_namespace *ns;
    static const struct ipc_ops shm_ops = {
        .getnew = newseg,  // 创建新段
        .associate = security_shm_associate,
        .more_checks = shm_more_checks,
    };
    struct ipc_params shm_params;
    
    ns = current->nsproxy->ipc_ns;
    
    shm_params.key = key;
    shm_params.flg = shmflg;
    shm_params.u.size = size;
    
    return ipcget(ns, &shm_ids(ns), &shm_ops, &shm_params);
}

newseg() 函数

c 复制代码
// ipc/shm.c
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
{
    key_t key = params->key;
    int shmflg = params->flg;
    size_t size = params->u.size;
    int error;
    struct shmid_kernel *shp;
    size_t numpages = (size + PAGE_SIZE - 1) >> PAGE_SIZE;
    struct file *file;
    char name[12];
    int id;
    void *user_memory;
    
    // 1. 检查大小限制
    if (size < SHMMIN || size > ns->shm_ctlmax)
        return -EINVAL;
    
    // 2. 分配 shmid_kernel 结构
    shp = kvmalloc(sizeof(*shp), GFP_KERNEL);
    if (!shp)
        return -ENOMEM;
    
    // 3. 创建 tmpfs 文件
    sprintf(name, "SYSV%08x", key);
    file = shmem_kernel_file_setup(name, size, acctflag);
    if (IS_ERR(file)) {
        error = PTR_ERR(file);
        goto no_file;
    }
    
    // 4. 初始化 shmid_kernel 结构
    shp->shm_file = file;
    shp->shm_perm.key = key;
    shp->shm_perm.mode = (shmflg & S_IRWXUGO);
    shp->shm_perm.security = NULL;
    shp->shm_segsz = size;
    shp->shm_nattch = 0;
    shp->shm_atim = 0;
    shp->shm_dtim = 0;
    shp->shm_ctim = ktime_get_real_seconds();
    shp->shm_cprid = get_pid(task_tgid(current));
    shp->shm_lprid = NULL;
    shp->shm_ns = ns;
    
    // 5. 注册到 IPC 命名空间
    id = ipc_addid(&shm_ids(ns), &shp->shm_perm, ns->shm_ctlmni);
    if (id < 0) {
        error = id;
        goto no_id;
    }
    
    // 6. 更新命名空间统计
    ns->shm_tot += numpages;
    
    return id;
}

关键步骤说明

  1. 大小检查

    • SHMMIN:最小共享内存段大小(通常 1 字节)
    • shm_ctlmax :最大共享内存段大小(可通过 sysctl 配置)
  2. 创建 tmpfs 文件

    • shmem_kernel_file_setup():在 tmpfs 文件系统中创建文件
    • 文件名SYSV + 8 位十六进制 key
    • 作用:共享内存段实际上是一个 tmpfs 文件
  3. 初始化结构

    • shmid_kernel:内核中的共享内存段结构
    • shm_perm:IPC 权限结构
    • shm_file:指向 tmpfs 文件的指针
  4. 注册到 IPC 命名空间

    • ipc_addid():分配唯一的共享内存 ID
    • 作用:将共享内存段添加到命名空间的 ID 表中

2.3 shmat() 的实现原理

shmat() 的作用

将共享内存段附加到调用进程的地址空间,返回映射的虚拟地址。

内核实现

c 复制代码
// ipc/shm.c
long do_shmat(int shmid, char __user *shmaddr, int shmflg, ulong *raddr, unsigned long shmlba)
{
    struct shmid_kernel *shp;
    unsigned long addr = (unsigned long)shmaddr;
    unsigned long size;
    struct file *file;
    int    err;
    unsigned long flags = MAP_SHARED;
    unsigned long prot;
    int acc_mode;
    struct ipc_namespace *ns;
    struct shm_file_data *sfd;
    int f_flags;
    unsigned long populate = 0;
    
    // 1. 获取共享内存段
    ns = current->nsproxy->ipc_ns;
    shp = shm_obtain_object_check(ns, shmid);
    if (IS_ERR(shp))
        return PTR_ERR(shp);
    
    // 2. 检查权限
    err = -EACCES;
    if (ipcperms(ns, &shp->shm_perm, acc_mode))
        goto out_unlock;
    
    // 3. 检查大小
    err = -EINVAL;
    size = i_size_read(file_inode(shp->shm_file));
    if (size < SHMMIN || size > (unsigned long)ns->shm_ctlmax)
        goto out_unlock;
    
    // 4. 创建 shm_file_data 结构
    sfd = kvmalloc(sizeof(*sfd), GFP_KERNEL);
    if (!sfd) {
        err = -ENOMEM;
        goto out_unlock;
    }
    
    file = alloc_file_clone(shp->shm_file, f_flags,
                            is_file_hugepages(shp->shm_file) ?
                            &shm_file_operations_huge :
                            &shm_file_operations);
    if (IS_ERR(file)) {
        err = PTR_ERR(file);
        goto out_freed;
    }
    
    sfd->id = shp->shm_perm.id;
    sfd->ns = get_ipc_ns(ns);
    sfd->file = shp->shm_file;
    sfd->vm_ops = NULL;
    file->private_data = sfd;
    
    // 5. 执行内存映射
    err = do_mmap_pgoff(file, addr, size, prot, flags, 0, &populate, NULL);
    *raddr = addr;
    
    if (err < 0) {
        fput(file);
        goto out_nattch;
    }
    
    // 6. 更新统计信息
    shp->shm_nattch++;
    shp->shm_atim = ktime_get_real_seconds();
    ipc_update_pid(&shp->shm_lprid, task_tgid(current));
    
    return err;
}

关键步骤说明

  1. 获取共享内存段

    • shm_obtain_object_check():从 IPC 命名空间获取共享内存段
    • 检查:验证 ID 是否有效
  2. 权限检查

    • ipcperms():检查进程是否有权限访问共享内存段
    • 权限:读、写权限
  3. 创建文件描述符

    • alloc_file_clone():为共享内存段创建文件描述符
    • 作用:每个进程都有独立的文件描述符,但指向同一个文件
  4. 内存映射

    • do_mmap_pgoff():将文件映射到进程地址空间
    • 标志MAP_SHARED 表示共享映射
    • 结果:返回映射的虚拟地址
  5. 更新统计

    • shm_nattch:附加计数加 1
    • shm_atim:更新最后附加时间
    • shm_lprid:更新最后附加进程 ID

2.4 shm_mmap() 的实现原理

shm_mmap() 的作用

当进程调用 mmap() 映射共享内存文件时,内核会调用 shm_mmap() 来处理映射。

内核实现

c 复制代码
// ipc/shm.c
static int shm_mmap(struct file *file, struct vm_area_struct *vma)
{
    struct shm_file_data *sfd = shm_file_data(file);
    int ret;
    
    // 1. 打开共享内存段(增加引用计数)
    ret = __shm_open(vma);
    if (ret)
        return ret;
    
    // 2. 调用底层文件的 mmap
    ret = call_mmap(sfd->file, vma);
    if (ret) {
        shm_close(vma);
        return ret;
    }
    
    // 3. 保存原始 vm_ops,替换为 shm_vm_ops
    sfd->vm_ops = vma->vm_ops;
    vma->vm_ops = &shm_vm_ops;
    
    return 0;
}

__shm_open() 函数

c 复制代码
// ipc/shm.c
static int __shm_open(struct vm_area_struct *vma)
{
    struct file *file = vma->vm_file;
    struct shm_file_data *sfd = shm_file_data(file);
    struct shmid_kernel *shp;
    
    // 1. 获取共享内存段
    shp = shm_lock(sfd->ns, sfd->id);
    if (IS_ERR(shp))
        return PTR_ERR(shp);
    
    // 2. 验证文件是否匹配
    if (shp->shm_file != sfd->file) {
        shm_unlock(shp);
        return -EINVAL;  // ID 被重用
    }
    
    // 3. 更新统计信息
    shp->shm_atim = ktime_get_real_seconds();
    ipc_update_pid(&shp->shm_lprid, task_tgid(current));
    shp->shm_nattch++;
    
    shm_unlock(shp);
    return 0;
}

关键点

  1. 引用计数 :每次映射时,shm_nattch 增加
  2. vm_ops 替换 :将 VMA 的 vm_ops 替换为 shm_vm_ops
  3. 文件验证:确保文件没有被删除和重用

2.5 shmdt() 的实现原理

shmdt() 的作用

从进程地址空间分离共享内存段。

内核实现

c 复制代码
// ipc/shm.c
SYSCALL_DEFINE1(shmdt, char __user *, shmaddr)
{
    return ksys_shmdt(shmaddr);
}

long ksys_shmdt(char __user *shmaddr)
{
    unsigned long addr = (unsigned long)shmaddr;
    unsigned long size;
    struct vm_area_struct *vma, *next;
    int retval = -EINVAL;
    
    // 1. 查找包含该地址的 VMA
    vma = find_vma(current->mm, addr);
    if (!vma || vma->vm_start > addr)
        return -EINVAL;
    
    // 2. 验证是否是共享内存映射
    if (vma->vm_ops != &shm_vm_ops)
        return -EINVAL;
    
    // 3. 取消映射
    size = vma->vm_end - vma->vm_start;
    retval = do_munmap(current->mm, addr, size, NULL);
    
    return retval;
}

shm_close() 函数

当 VMA 被取消映射时,内核会调用 shm_close()

c 复制代码
// ipc/shm.c
static void shm_close(struct vm_area_struct *vma)
{
    struct file *file = vma->vm_file;
    struct shm_file_data *sfd = shm_file_data(file);
    struct shmid_kernel *shp;
    struct ipc_namespace *ns = sfd->ns;
    
    down_write(&shm_ids(ns).rwsem);
    
    // 1. 获取共享内存段
    shp = shm_lock(ns, sfd->id);
    if (WARN_ON_ONCE(IS_ERR(shp)))
        goto done;
    
    // 2. 更新统计信息
    ipc_update_pid(&shp->shm_lprid, task_tgid(current));
    shp->shm_dtim = ktime_get_real_seconds();
    shp->shm_nattch--;
    
    // 3. 检查是否需要销毁
    if (shm_may_destroy(shp))
        shm_destroy(ns, shp);
    else
        shm_unlock(shp);
    
done:
    up_write(&shm_ids(ns).rwsem);
}

关键点

  1. 查找 VMA:根据地址查找对应的 VMA
  2. 验证类型:确保是共享内存映射
  3. 取消映射 :调用 do_munmap() 取消内存映射
  4. 更新统计 :减少 shm_nattch,更新分离时间

2.6 shmctl() 的实现原理

shmctl() 的作用

控制共享内存段,包括获取信息、设置权限、删除等。

内核实现

c 复制代码
// ipc/shm.c
long ksys_shmctl(int shmid, int cmd, struct shmid_ds __user *buf)
{
    struct shmid_kernel *shp;
    int err, version;
    struct ipc_namespace *ns;
    
    ns = current->nsproxy->ipc_ns;
    
    if (cmd < 0 || shmid < 0)
        return -EINVAL;
    
    switch (cmd) {
    case IPC_INFO:
    case SHM_INFO:
    case SHM_STAT:
    case SHM_STAT_ANY:
        // 获取信息
        return shmctl_info(ns, shmid, cmd, buf);
        
    case IPC_STAT:
    case SHM_STAT:
    case SHM_STAT_ANY:
        // 获取共享内存段状态
        return shmctl_stat(ns, shmid, cmd, buf);
        
    case IPC_SET:
        // 设置共享内存段属性
        return shmctl_set(ns, shmid, cmd, buf);
        
    case IPC_RMID:
        // 删除共享内存段
        return shmctl_rmid(ns, shmid, cmd);
        
    case SHM_LOCK:
        // 锁定共享内存段(防止交换)
        return shmctl_lock(ns, shmid);
        
    case SHM_UNLOCK:
        // 解锁共享内存段
        return shmctl_unlock(ns, shmid);
        
    default:
        return -EINVAL;
    }
}

IPC_RMID 的实现

c 复制代码
// ipc/shm.c
static int shmctl_rmid(struct ipc_namespace *ns, int shmid)
{
    struct shmid_kernel *shp;
    
    shp = shm_lock(ns, shmid);
    if (IS_ERR(shp))
        return PTR_ERR(shp);
    
    // 1. 检查权限
    if (ns->shm_rmid_forced ||
        (shp->shm_perm.mode & SHM_DEST) ||
        (shp->shm_nattch == 0)) {
        // 2. 标记为删除
        do_shm_rmid(ns, &shp->shm_perm);
        shm_unlock(shp);
    } else {
        // 3. 如果还有进程附加,只标记为删除
        shp->shm_perm.mode |= SHM_DEST;
        ipc_set_key_private(&shm_ids(ns), &shp->shm_perm);
        shm_unlock(shp);
    }
    
    return 0;
}

关键点

  1. IPC_STAT:获取共享内存段状态信息
  2. IPC_SET:设置共享内存段属性(权限、大小等)
  3. IPC_RMID:删除共享内存段(如果还有进程附加,标记为删除,最后一个进程分离时真正删除)
  4. SHM_LOCK/SHM_UNLOCK:锁定/解锁共享内存段,防止交换到磁盘

3. 底层实现原理

3.1 数据结构

shmid_kernel 结构

c 复制代码
// ipc/shm.c
struct shmid_kernel /* private to the kernel */
{
    struct kern_ipc_perm  shm_perm;  // IPC 权限结构
    struct file          *shm_file;   // 指向 tmpfs 文件的指针
    unsigned long        shm_nattch;  // 附加计数
    unsigned long        shm_segsz;   // 段大小
    time64_t             shm_atim;    // 最后附加时间
    time64_t             shm_dtim;    // 最后分离时间
    time64_t             shm_ctim;    // 最后修改时间
    struct pid           *shm_cprid;  // 创建进程 ID
    struct pid           *shm_lprid;  // 最后附加进程 ID
    struct ucounts       *mlock_ucounts;  // 锁定计数
    struct task_struct   *shm_creator;     // 创建进程
    struct list_head     shm_clist;        // 创建者列表
    struct ipc_namespace *ns;              // IPC 命名空间
};

shm_file_data 结构

c 复制代码
// ipc/shm.c
struct shm_file_data {
    int id;                              // 共享内存 ID
    struct ipc_namespace *ns;            // IPC 命名空间
    struct file *file;                   // 底层文件
    const struct vm_operations_struct *vm_ops;  // 原始 vm_ops
};

关键字段说明

  1. shm_perm

    • 作用:IPC 权限和标识
    • 包含:key、ID、权限、所有者等
  2. shm_file

    • 作用:指向 tmpfs 文件的指针
    • 类型struct file *
    • 作用:共享内存段实际上是一个 tmpfs 文件
  3. shm_nattch

    • 作用:当前附加到该段的进程数
    • 更新 :每次 shmat() 时增加,shmdt() 时减少
  4. shm_segsz

    • 作用:共享内存段的大小(字节)
    • 限制 :受 shm_ctlmax 限制

3.2 tmpfs 文件系统

tmpfs 的作用

System V 共享内存段实际上是在 tmpfs 文件系统中创建的文件。tmpfs 是一个基于内存的文件系统,数据存储在 RAM 中。

tmpfs 的特点

  1. 内存存储:数据存储在 RAM 中,访问速度快
  2. 可交换:如果内存不足,可以交换到交换分区
  3. 临时性:系统重启后数据丢失
  4. 动态大小:可以根据需要动态增长

shmem_file_setup() 函数

c 复制代码
// mm/shmem.c
struct file *shmem_file_setup(const char *name, loff_t size, unsigned long flags)
{
    int error;
    struct file *file;
    struct inode *inode;
    struct dentry *dentry, *root;
    struct qstr this;
    
    // 1. 获取 tmpfs 挂载点
    root = shm_mnt->mnt_root;
    
    // 2. 创建 dentry
    this.name = name;
    this.len = strlen(name);
    this.hash = 0;
    dentry = d_alloc(root, &this);
    if (!dentry)
        return ERR_PTR(-ENOMEM);
    
    // 3. 创建 inode
    inode = shmem_get_inode(root->d_sb, NULL, S_IFREG | S_IRWXUGO, 0, flags);
    if (!inode) {
        dput(dentry);
        return ERR_PTR(-ENOMEM);
    }
    
    // 4. 设置文件大小
    inode->i_size = size;
    d_instantiate(dentry, inode);
    inode->i_nlink = 0;
    
    // 5. 创建文件
    file = alloc_file_pseudo(inode, shm_mnt, dentry->d_name.name,
                              O_RDWR | (flags & O_ACCMODE),
                              &shmem_file_operations);
    
    return file;
}

关键点

  1. 文件创建:在 tmpfs 中创建文件
  2. 大小设置:设置文件大小为共享内存段大小
  3. 文件操作 :使用 shmem_file_operations 作为文件操作函数

3.3 内存映射机制

mmap() 的作用

将文件映射到进程地址空间,实现共享内存的访问。

映射流程

markdown 复制代码
1. 进程调用 shmat()
   ↓
2. 内核调用 do_mmap_pgoff()
   ↓
3. 创建 VMA(虚拟内存区域)
   ↓
4. 设置 VMA 属性(地址、大小、权限等)
   ↓
5. 将 VMA 添加到进程的 mm_struct
   ↓
6. 返回虚拟地址

VMA 结构

c 复制代码
// include/linux/mm_types.h
struct vm_area_struct {
    unsigned long vm_start;      // 起始地址
    unsigned long vm_end;        // 结束地址
    struct mm_struct *vm_mm;     // 所属进程
    pgprot_t vm_page_prot;       // 页面保护
    unsigned long vm_flags;      // 标志(MAP_SHARED 等)
    struct file *vm_file;        // 映射的文件
    void *vm_private_data;       // 私有数据
    const struct vm_operations_struct *vm_ops;  // 操作函数
    // ...
};

页面错误处理

当进程访问共享内存时,如果页面不在内存中,会触发页面错误:

c 复制代码
// mm/shmem.c
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct inode *inode = file_inode(vma->vm_file);
    struct page *page;
    vm_fault_t ret = VM_FAULT_LOCKED;
    
    // 1. 获取页面
    page = shmem_getpage_gfp(inode, vmf->pgoff, &vmf->page,
                              SGP_CACHE, vmf->gfp_mask, vma, vmf, NULL);
    
    if (!page)
        return VM_FAULT_OOM;
    
    // 2. 如果页面在交换分区,需要换入
    if (PageSwapCache(page)) {
        swap_readpage(page, true);
        wait_on_page_locked(page);
    }
    
    return ret;
}

关键点

  1. 延迟分配:页面在首次访问时才分配
  2. 交换支持:如果内存不足,页面可以交换到交换分区
  3. 写时复制 :如果使用 MAP_PRIVATE,会使用写时复制

3.4 同步机制

为什么需要同步

多个进程访问共享内存时,需要同步机制来避免竞争条件。

常见的同步机制

  1. 信号量(Semaphore)

    c 复制代码
    #include <sys/sem.h>
    
    // 创建信号量
    int semid = semget(key, 1, IPC_CREAT | 0666);
    
    // P 操作(等待)
    struct sembuf sop = {0, -1, 0};
    semop(semid, &sop, 1);
    
    // V 操作(释放)
    sop.sem_op = 1;
    semop(semid, &sop, 1);
  2. 互斥锁(Mutex)

    c 复制代码
    #include <pthread.h>
    
    // 在共享内存中创建互斥锁
    pthread_mutex_t *mutex = (pthread_mutex_t *)shm_addr;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(mutex, &attr);
    
    // 使用
    pthread_mutex_lock(mutex);
    // 临界区
    pthread_mutex_unlock(mutex);
  3. 条件变量(Condition Variable)

    c 复制代码
    #include <pthread.h>
    
    pthread_cond_t *cond = (pthread_cond_t *)shm_addr;
    pthread_condattr_t attr;
    pthread_condattr_init(&attr);
    pthread_condattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_cond_init(cond, &attr);
    
    // 等待
    pthread_cond_wait(cond, mutex);
    
    // 通知
    pthread_cond_signal(cond);

4. POSIX 共享内存

4.1 POSIX 共享内存 API

核心函数

  1. shm_open():创建或打开共享内存对象
  2. shm_unlink():删除共享内存对象
  3. mmap():映射共享内存到进程地址空间
  4. munmap():取消映射

函数原型

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

// 创建或打开共享内存对象
int shm_open(const char *name, int oflag, mode_t mode);

// 删除共享内存对象
int shm_unlink(const char *name);

// 映射共享内存
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

// 取消映射
int munmap(void *addr, size_t length);

4.2 shm_open() 的实现原理

shm_open() 的作用

/dev/shm 目录下创建或打开共享内存文件。

内核实现

c 复制代码
// mm/shmem.c
SYSCALL_DEFINE3(shm_open, const char __user *, name, int, oflag, umode_t, mode)
{
    struct path path;
    struct file *file;
    int err;
    unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
    
    if (oflag & ~(O_CREAT | O_RDWR | O_TRUNC | O_EXCL | O_CLOEXEC))
        return -EINVAL;
    
    if (oflag & O_CREAT) {
        if (oflag & O_EXCL)
            lookup_flags |= LOOKUP_EXCL;
        lookup_flags |= LOOKUP_CREATE;
    }
    
    // 1. 在 /dev/shm 中查找或创建文件
    err = user_path_create(AT_FDCWD, name, &path, lookup_flags);
    if (err)
        return err;
    
    // 2. 打开文件
    file = dentry_open(&path, oflag, current_cred());
    path_put(&path);
    
    if (IS_ERR(file))
        return PTR_ERR(file);
    
    // 3. 设置文件大小(如果是新创建的文件)
    if (oflag & O_CREAT) {
        if (oflag & O_TRUNC) {
            err = do_ftruncate(file, 0, 0);
            if (err)
                goto out;
        }
    }
    
    // 4. 返回文件描述符
    return fd_install(fd, file);
}

关键点

  1. 文件位置 :在 /dev/shm 目录下创建文件
  2. 文件系统:使用 tmpfs 文件系统
  3. 文件描述符 :返回文件描述符,可以用于 mmap()

4.3 POSIX vs System V 共享内存

特性 System V POSIX
标识方式 key_t key 文件名
标准 System V IPC POSIX
API shmget/shmat/shmdt shm_open/mmap/munmap
文件系统 tmpfs(内核内部) tmpfs(/dev/shm)
权限 IPC 权限 文件系统权限
删除 shmctl(IPC_RMID) shm_unlink()
可移植性 较低 较高

5. 使用示例

5.1 System V 共享内存示例

创建共享内存的进程

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

#define SHM_SIZE 1024
#define SHM_KEY 1234

int main(void)
{
    int shmid;
    char *shm_addr;
    struct sembuf sop;
    int semid;
    
    // 1. 创建信号量
    semid = semget(SHM_KEY, 1, IPC_CREAT | 0666);
    if (semid < 0) {
        perror("semget");
        exit(1);
    }
    
    // 初始化信号量为 1
    union semun {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
    } arg;
    arg.val = 1;
    semctl(semid, 0, SETVAL, arg);
    
    // 2. 创建共享内存
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(1);
    }
    
    // 3. 附加共享内存
    shm_addr = (char *)shmat(shmid, NULL, 0);
    if (shm_addr == (char *)-1) {
        perror("shmat");
        exit(1);
    }
    
    // 4. 写入数据
    strcpy(shm_addr, "Hello, Shared Memory!");
    
    // 5. 等待其他进程读取
    printf("Data written. Waiting for reader...\n");
    sleep(10);
    
    // 6. 分离共享内存
    shmdt(shm_addr);
    
    // 7. 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    semctl(semid, 0, IPC_RMID);
    
    return 0;
}

读取共享内存的进程

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

#define SHM_SIZE 1024
#define SHM_KEY 1234

int main(void)
{
    int shmid;
    char *shm_addr;
    struct sembuf sop;
    int semid;
    
    // 1. 获取信号量
    semid = semget(SHM_KEY, 0, 0);
    if (semid < 0) {
        perror("semget");
        exit(1);
    }
    
    // 2. 获取共享内存
    shmid = shmget(SHM_KEY, 0, 0);
    if (shmid < 0) {
        perror("shmget");
        exit(1);
    }
    
    // 3. 附加共享内存
    shm_addr = (char *)shmat(shmid, NULL, 0);
    if (shm_addr == (char *)-1) {
        perror("shmat");
        exit(1);
    }
    
    // 4. 读取数据
    printf("Data read: %s\n", shm_addr);
    
    // 5. 分离共享内存
    shmdt(shm_addr);
    
    return 0;
}

5.2 POSIX 共享内存示例

创建共享内存的进程

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

#define SHM_NAME "/my_shm"
#define SHM_SIZE 1024

int main(void)
{
    int shm_fd;
    char *shm_addr;
    
    // 1. 创建共享内存对象
    shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd < 0) {
        perror("shm_open");
        exit(1);
    }
    
    // 2. 设置共享内存大小
    if (ftruncate(shm_fd, SHM_SIZE) < 0) {
        perror("ftruncate");
        exit(1);
    }
    
    // 3. 映射共享内存
    shm_addr = (char *)mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE,
                            MAP_SHARED, shm_fd, 0);
    if (shm_addr == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
    
    // 4. 关闭文件描述符(不影响映射)
    close(shm_fd);
    
    // 5. 写入数据
    strcpy(shm_addr, "Hello, POSIX Shared Memory!");
    
    // 6. 等待其他进程读取
    printf("Data written. Waiting for reader...\n");
    sleep(10);
    
    // 7. 取消映射
    munmap(shm_addr, SHM_SIZE);
    
    // 8. 删除共享内存对象
    shm_unlink(SHM_NAME);
    
    return 0;
}

读取共享内存的进程

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

#define SHM_NAME "/my_shm"
#define SHM_SIZE 1024

int main(void)
{
    int shm_fd;
    char *shm_addr;
    
    // 1. 打开共享内存对象
    shm_fd = shm_open(SHM_NAME, O_RDONLY, 0);
    if (shm_fd < 0) {
        perror("shm_open");
        exit(1);
    }
    
    // 2. 映射共享内存
    shm_addr = (char *)mmap(NULL, SHM_SIZE, PROT_READ,
                            MAP_SHARED, shm_fd, 0);
    if (shm_addr == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
    
    // 3. 关闭文件描述符
    close(shm_fd);
    
    // 4. 读取数据
    printf("Data read: %s\n", shm_addr);
    
    // 5. 取消映射
    munmap(shm_addr, SHM_SIZE);
    
    return 0;
}

6. 性能优化和最佳实践

6.1 性能优化

1. 页面大小对齐

共享内存段的大小应该对齐到页面大小(通常 4KB):

c 复制代码
#define PAGE_SIZE 4096
size_t shm_size = (data_size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);

2. 避免频繁映射/取消映射

映射和取消映射有开销,应该保持映射直到不再需要:

c 复制代码
// 不好的做法
for (int i = 0; i < 1000; i++) {
    void *addr = shmat(shmid, NULL, 0);
    // 使用共享内存
    shmdt(addr);
}

// 好的做法
void *addr = shmat(shmid, NULL, 0);
for (int i = 0; i < 1000; i++) {
    // 使用共享内存
}
shmdt(addr);

3. 使用大页面

对于大块共享内存,可以使用大页面(Huge Pages)提高性能:

c 复制代码
// 创建大页面共享内存
shmid = shmget(key, size, IPC_CREAT | SHM_HUGETLB | 0666);

6.2 最佳实践

1. 错误处理

c 复制代码
int shmid = shmget(key, size, IPC_CREAT | 0666);
if (shmid < 0) {
    perror("shmget");
    exit(1);
}

void *addr = shmat(shmid, NULL, 0);
if (addr == (void *)-1) {
    perror("shmat");
    exit(1);
}

2. 清理资源

c 复制代码
// 分离共享内存
if (shmdt(addr) < 0) {
    perror("shmdt");
}

// 删除共享内存(最后一个进程)
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
    perror("shmctl");
}

3. 使用同步机制

c 复制代码
// 使用互斥锁保护共享内存访问
pthread_mutex_lock(mutex);
// 访问共享内存
pthread_mutex_unlock(mutex);

4. 检查共享内存状态

c 复制代码
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) < 0) {
    perror("shmctl");
    exit(1);
}

printf("Size: %lu\n", buf.shm_segsz);
printf("Attached: %lu\n", buf.shm_nattch);

7. 底层实现细节

7.1 tmpfs 文件系统

tmpfs 的挂载

System V 共享内存使用的 tmpfs 文件系统在内核启动时挂载:

c 复制代码
// mm/shmem.c
static struct vfsmount *shm_mnt;

static int __init init_tmpfs(void)
{
    // 注册 tmpfs 文件系统
    register_filesystem(&tmpfs_fs_type);
    
    // 挂载 tmpfs
    shm_mnt = kern_mount(&tmpfs_fs_type);
    if (IS_ERR(shm_mnt)) {
        pr_err("Could not kern_mount tmpfs\n");
        return PTR_ERR(shm_mnt);
    }
    
    return 0;
}

tmpfs 的特点

  1. 内存存储:所有数据存储在 RAM 中
  2. 可交换:如果内存不足,可以交换到交换分区
  3. 动态大小:可以根据需要动态增长
  4. 临时性:系统重启后数据丢失

7.2 页面分配机制

延迟分配(Lazy Allocation)

共享内存段创建时,并不立即分配所有页面,而是采用延迟分配策略:

c 复制代码
// mm/shmem.c
static int shmem_getpage_gfp(struct inode *inode, pgoff_t index,
                              struct page **pagep, enum sgp_type sgp,
                              gfp_t gfp, struct vm_area_struct *vma,
                              struct vm_fault *vmf, vm_fault_t *fault_type)
{
    struct address_space *mapping = inode->i_mapping;
    struct shmem_inode_info *info = SHMEM_I(inode);
    struct page *page;
    swp_entry_t swap;
    int error;
    
    // 1. 查找页面是否已存在
    page = find_get_entry(mapping, index);
    if (page && !xa_is_value(page)) {
        // 页面已存在
        *pagep = page;
        return 0;
    }
    
    // 2. 检查是否在交换分区
    swap = shmem_get_swap(info, index);
    if (swap.val) {
        // 从交换分区换入
        error = shmem_swapin_page(inode, index, &page, sgp, gfp, vma, fault_type);
        if (error)
            return error;
        *pagep = page;
        return 0;
    }
    
    // 3. 分配新页面
    page = shmem_alloc_and_acct_page(gfp, info, sgp, index);
    if (IS_ERR(page))
        return PTR_ERR(page);
    
    // 4. 添加到地址空间
    error = shmem_add_to_page_cache(page, mapping, index, gfp, NULL);
    if (error) {
        __free_page(page);
        return error;
    }
    
    *pagep = page;
    return 0;
}

关键点

  1. 延迟分配:页面在首次访问时才分配
  2. 交换支持:如果页面在交换分区,需要换入
  3. 缓存管理:使用地址空间(address_space)管理页面

7.3 页面错误处理

页面错误的触发

当进程访问共享内存时,如果页面不在内存中,会触发页面错误:

c 复制代码
// mm/memory.c
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
    pte_t entry;
    
    if (!vmf->pte) {
        // 页表项不存在,需要分配
        return do_fault(vmf);
    }
    
    entry = ptep_get(vmf->pte);
    if (!pte_present(entry)) {
        // 页面不在内存中
        if (pte_none(entry))
            return do_fault(vmf);
        if (pte_swp_swap_required(entry))
            return do_swap_page(vmf);
    }
    
    return 0;
}

shmem_fault() 函数

c 复制代码
// mm/shmem.c
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct inode *inode = file_inode(vma->vm_file);
    struct page *page;
    vm_fault_t ret = VM_FAULT_LOCKED;
    int error;
    
    // 1. 获取页面
    error = shmem_getpage_gfp(inode, vmf->pgoff, &page,
                              SGP_CACHE, vmf->gfp_mask, vma, vmf, NULL);
    if (error)
        return VM_FAULT_SIGBUS;
    
    // 2. 如果页面在交换分区,需要换入
    if (PageSwapCache(page)) {
        swap_readpage(page, true);
        wait_on_page_locked(page);
        if (!PageUptodate(page)) {
            unlock_page(page);
            put_page(page);
            return VM_FAULT_SIGBUS;
        }
    }
    
    // 3. 设置页表项
    vmf->page = page;
    return ret;
}

关键步骤

  1. 查找页面:在地址空间中查找页面
  2. 交换处理:如果页面在交换分区,需要换入
  3. 分配页面:如果页面不存在,分配新页面
  4. 设置页表:将页面映射到进程地址空间

7.4 写时复制(Copy-on-Write)

MAP_PRIVATE vs MAP_SHARED

  • MAP_SHARED:多个进程共享同一物理页面,一个进程的修改对其他进程可见
  • MAP_PRIVATE:每个进程有独立的页面副本,使用写时复制

写时复制机制

c 复制代码
// mm/memory.c
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *old_page = vmf->page;
    struct page *new_page;
    
    // 1. 检查是否允许写时复制
    if (!(vma->vm_flags & VM_WRITE))
        return VM_FAULT_SIGBUS;
    
    // 2. 分配新页面
    new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
    if (!new_page)
        return VM_FAULT_OOM;
    
    // 3. 复制页面内容
    copy_user_highpage(new_page, old_page, vmf->address, vma);
    
    // 4. 设置页表项
    pte_t entry = mk_pte(new_page, vma->vm_page_prot);
    entry = maybe_mkwrite(pte_mkdirty(entry), vma);
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    
    // 5. 更新统计
    update_mmu_cache(vma, vmf->address, vmf->pte);
    
    return VM_FAULT_WRITE;
}

关键点

  1. 共享内存使用 MAP_SHARED:所有进程共享同一物理页面
  2. 写时复制用于 MAP_PRIVATE:私有映射使用写时复制
  3. 性能考虑:写时复制可以减少内存使用,但首次写入时有开销

7.5 内存锁定(Memory Locking)

SHM_LOCK 的作用

锁定共享内存段,防止其被交换到磁盘。

实现原理

c 复制代码
// ipc/shm.c
static int shmctl_lock(struct ipc_namespace *ns, int shmid)
{
    struct shmid_kernel *shp;
    struct file *shm_file;
    int err;
    
    shp = shm_lock(ns, shmid);
    if (IS_ERR(shp))
        return PTR_ERR(shp);
    
    // 1. 检查权限
    if (!ns_capable(ns->user_ns, CAP_IPC_LOCK)) {
        shm_unlock(shp);
        return -EPERM;
    }
    
    // 2. 锁定文件
    shm_file = shp->shm_file;
    if (is_file_hugepages(shm_file))
        err = mlock_hugepage(shm_file);
    else
        err = shmem_lock(shm_file, 1, shp->mlock_ucounts);
    
    if (!err && !(shp->shm_perm.mode & SHM_LOCKED)) {
        shp->shm_perm.mode |= SHM_LOCKED;
        shp->mlock_ucounts = get_ucounts(current_ucounts());
    }
    
    shm_unlock(shp);
    return err;
}

关键点

  1. 防止交换:锁定的内存不会被交换到磁盘
  2. 权限要求 :需要 CAP_IPC_LOCK 权限
  3. 资源限制 :受 RLIMIT_MEMLOCK 限制

8. 故障排查和调试

8.1 查看共享内存状态

使用 ipcs 命令

bash 复制代码
# 查看所有共享内存段
ipcs -m

# 查看详细信息
ipcs -m -l

# 查看特定进程的共享内存
ipcs -m -p

使用 /proc 文件系统

bash 复制代码
# 查看进程的共享内存映射
cat /proc/<pid>/maps | grep shm

# 查看共享内存统计
cat /proc/sysvipc/shm

8.2 常见问题

1. 共享内存段已存在

c 复制代码
// 错误处理
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0) {
    if (errno == EEXIST) {
        // 共享内存段已存在,获取它
        shmid = shmget(key, 0, 0);
    } else {
        perror("shmget");
        exit(1);
    }
}

2. 权限不足

c 复制代码
// 检查权限
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) < 0) {
    perror("shmctl");
    exit(1);
}

// 检查是否有读权限
if (!(buf.shm_perm.mode & S_IRUSR)) {
    fprintf(stderr, "No read permission\n");
    exit(1);
}

3. 内存不足

c 复制代码
// 检查系统限制
struct shminfo info;
if (shmctl(0, IPC_INFO, (struct shmid_ds *)&info) < 0) {
    perror("shmctl");
    exit(1);
}

printf("Max size: %lu\n", info.shmmax);
printf("Total segments: %lu\n", info.shmmni);

8.3 调试技巧

1. 使用 strace 跟踪系统调用

bash 复制代码
strace -e trace=shmget,shmat,shmdt,shmctl ./program

2. 使用 gdb 调试

bash 复制代码
gdb ./program
(gdb) break shmat
(gdb) run
(gdb) print shm_addr

3. 检查内存泄漏

bash 复制代码
# 查看未删除的共享内存段
ipcs -m | grep -v "^key"

# 检查进程的共享内存映射
for pid in $(ps -eo pid); do
    echo "PID: $pid"
    cat /proc/$pid/maps | grep shm
done

9. 性能优化

9.1 大页面支持

使用大页面(Huge Pages)

大页面可以减少页表项数量,提高 TLB 命中率,从而提高性能。

c 复制代码
// 创建大页面共享内存
int shmid = shmget(key, size, IPC_CREAT | SHM_HUGETLB | 0666);

配置大页面

bash 复制代码
# 查看大页面信息
cat /proc/meminfo | grep Huge

# 配置大页面数量
echo 10 > /proc/sys/vm/nr_hugepages

9.2 内存对齐

页面大小对齐

共享内存段的大小应该对齐到页面大小:

c 复制代码
#define PAGE_SIZE 4096
size_t aligned_size = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);

缓存行对齐

对于频繁访问的数据结构,应该对齐到缓存行大小:

c 复制代码
#define CACHE_LINE_SIZE 64
struct data {
    char padding[CACHE_LINE_SIZE];
    int value;
} __attribute__((aligned(CACHE_LINE_SIZE)));

9.3 预分配页面

使用 mlock() 锁定内存

c 复制代码
// 锁定共享内存,防止交换
if (mlock(shm_addr, shm_size) < 0) {
    perror("mlock");
    exit(1);
}

预分配页面

c 复制代码
// 访问所有页面,触发分配
for (size_t i = 0; i < shm_size; i += PAGE_SIZE) {
    volatile char *p = (char *)shm_addr + i;
    *p = 0;  // 触发页面分配
}

10. 总结

10.1 关键点

  1. System V 共享内存

    • 基于 key 的标识方式
    • 使用 shmget/shmat/shmdt/shmctl API
    • 基于 tmpfs 文件系统
  2. POSIX 共享内存

    • 基于文件名的标识方式
    • 使用 shm_open/mmap/munmap API
    • 更符合 POSIX 标准
  3. 底层实现

    • 共享内存段是 tmpfs 文件
    • 通过 mmap() 映射到进程地址空间
    • 使用 VMA 管理映射
    • 延迟分配页面
  4. 同步机制

    • 需要额外的同步机制(信号量、互斥锁等)
    • 避免竞争条件

10.2 适用场景

  1. 高性能数据传输:进程间大量数据交换
  2. 数据库系统:共享缓存和缓冲区
  3. 图形系统:共享图形缓冲区
  4. 科学计算:共享计算结果

10.3 最佳实践

  1. 错误处理:始终检查系统调用的返回值
  2. 资源清理:及时分离和删除共享内存
  3. 同步机制:使用适当的同步机制保护共享数据
  4. 性能优化:使用大页面、内存对齐等技术

文档结束

7.1 关键点

  1. System V 共享内存

    • 基于 key 的标识方式
    • 使用 shmget/shmat/shmdt/shmctl API
    • 基于 tmpfs 文件系统
  2. POSIX 共享内存

    • 基于文件名的标识方式
    • 使用 shm_open/mmap/munmap API
    • 更符合 POSIX 标准
  3. 底层实现

    • 共享内存段是 tmpfs 文件
    • 通过 mmap() 映射到进程地址空间
    • 使用 VMA 管理映射
  4. 同步机制

    • 需要额外的同步机制(信号量、互斥锁等)
    • 避免竞争条件

7.2 适用场景

  1. 高性能数据传输:进程间大量数据交换
  2. 数据库系统:共享缓存和缓冲区
  3. 图形系统:共享图形缓冲区
  4. 科学计算:共享计算结果

相关推荐
Shawn_CH8 小时前
Linux 系统启动流程详细解析
嵌入式
Shawn_CH8 小时前
Linux top、mpstat、htop 原理详解
嵌入式
俊俊谢8 小时前
华大HC32F460配置JTAG调试引脚为普通GPIO(PB03、PA15等)
嵌入式硬件·嵌入式·arm·嵌入式软件·hc32f460
Shawn_CH1 天前
epoll_wait 及相关函数原理详解
嵌入式
Shawn_CH1 天前
Linux 进程冻结机制原理详解
嵌入式
黑客思维者3 天前
XGW-9000系列高端新能源电站边缘网关硬件架构设计
网络·架构·硬件架构·嵌入式·新能源·计算机硬件·电站
神圣的大喵3 天前
平台无关的嵌入式通用按键管理器
c语言·单片机·嵌入式硬件·嵌入式·按键库
网易独家音乐人Mike Zhou3 天前
【嵌入式模块芯片开发】LP87524电源PMIC芯片配置流程,给雷达供电的延时上电时序及API函数
c语言·stm32·单片机·51单片机·嵌入式·电源·毫米波雷达
Nerd Nirvana3 天前
WSL——Windows Subsystem for Linux流程一览
linux·运维·服务器·windows·嵌入式·wsl·wsl2