Linux进程间通信之共享内存实现篇

Linux 内核中共享内存的实现(基于 2.6.12)

核心文件与路径

  • ipc/shm.c: System V 共享内存系统调用与核心逻辑
  • include/linux/shm.h: 结构体与常量定义(shmid_kernel#ifdef __KERNEL__ 块内, 仅内核可见)
  • ipc/util.c: IPC 公共 ID/权限/锁管理
  • mm/: 通用内存管理接口(页分配、映射)

核心数据结构

shmid_kernel(位于 include/linux/shm.h

// include/linux/shm.h (在 #ifdef __KERNEL__ 块内, 仅内核可见)
struct shmid_kernel /* private to the kernel */
{
    struct kern_ipc_perm shm_perm;  // IPC 权限/键值/锁
    struct file *shm_file;          // 关联的 shmem 文件, 承载页缓存
    int id;                         // 内部 ID (用于 IPC 表索引)
    unsigned long shm_nattch;       // 当前附加次数(映射该段的进程数)
    unsigned long shm_segsz;        // 段大小(字节)
    time_t shm_atim;                // 最后 attach 时间
    time_t shm_dtim;                // 最后 detach 时间
    time_t shm_ctim;                // 最后变更时间
    pid_t shm_cprid;                // 创建者 PID
    pid_t shm_lprid;                // 最后操作 PID
    struct user_struct *mlock_user; // 锁定内存的用户结构(用于 SHM_LOCK)
};

shmem_inode_info

  • 来自 tmpfs/shmem,作为 shm_file 的支撑,负责页缓存与换入换出(除非 SHM_LOCK 锁定)。

系统调用路径(x86_64 类推)

  • sys_shmgetipc/shm.c:sys_shmgetnewseg() 创建段(分配 shmid_kernel + shmem 文件)并通过 ipc_addid 安装到 IPC 表
  • sys_shmatipc/shm.c:sys_shmatdo_shmat() 检查权限/地址 → 通过 do_mmap_pgoff()shm_file 映射到进程用户空间
  • sys_shmdtipc/shm.c:sys_shmdtdo_shmdt() 卸载映射,递减 shm_nattch
  • sys_shmctlipc/shm.c:sys_shmctl 处理 IPC_RMID/IPC_SET/IPC_STAT/SHM_LOCK/SHM_UNLOCK

关键流程

创建段(shmget → newseg)

  1. 校验大小不超过 shm_ctlmax,对齐页(向上按页或 SHMLBA)。
  2. 分配 shmid_kernelipc_rcu_alloc),初始化 shm_perm、大小、时间戳、计数。
  3. 创建支撑文件:调用 shmem_file_setup() 创建匿名 shmem 文件,关联到 shm_file,用于后续映射与页缓存管理。
  4. 通过 ipc_addid(&shm_ids, …) 安装到 IPC ID 表,返回 shmid(序号+序列)。

简要实现(ipc/shm.c:sys_shmget/newseg

asmlinkage long sys_shmget(key_t key, size_t size, int shmflg)
{
    struct shmid_kernel *shp;
    int err, id;

    // 尺寸检查: 不得超过 shm_ctlmax
    if (size > shm_ctlmax)
        return -EINVAL;

    // 对齐到页面/SHMLBA (VIPT/TLB 需求)
    size = ALIGN(size, SHMLBA);

    // 如果指定 IPC_PRIVATE 或不存在且 IPC_CREAT,则创建
    id = newseg(key, shmflg, size);
    return id; // 返回 shmid (序号+序列)
}

static int newseg(key_t key, int shmflg, size_t size)
{
    struct shmid_kernel *shp;
    int id, err;

    // 分配内核描述符
    shp = ipc_rcu_alloc(sizeof(*shp));
    if (!shp)
        return -ENOMEM;

    shp->shm_perm.key  = key;
    shp->shm_perm.mode = (shmflg & S_IRWXUGO);
    shp->shm_perm.security = NULL;
    shp->shm_segsz = size;
    shp->shm_atim = shp->shm_dtim = 0; // 初始未映射/未解绑
    shp->shm_ctim = get_seconds();     // 创建时间
    shp->shm_cprid = shp->shm_lprid = current->pid;
    shp->shm_nattch = 0;               // 尚无进程映射

    // 创建支撑文件 (基于 shmem)
    shp->shm_file = shmem_file_setup("SYSV0000", size, 0);
    if (IS_ERR(shp->shm_file)) {
        err = PTR_ERR(shp->shm_file);
        ipc_rcu_putref(shp);
        return err;
    }

    // 安装到 IPC ID 表
    id = ipc_addid(&shm_ids, &shp->shm_perm, shmmni);
    if (id == -1) {
        fput(shp->shm_file);
        ipc_rcu_putref(shp);
        return -ENOSPC;
    }

    return shm_buildid(id, shp->shm_perm.seq); // 返回 shmid
}

映射(shmat → do_shmat)

  1. 权限检查与 ID 校验 (ipc_lock/ipc_checkid/ipcperms)。
  2. 解析 shmaddr / shmflg
    • SHM_RDONLY 只读映射
    • SHM_RND 地址向下按 SHMLBA 对齐
    • 若指定地址且违反对齐/冲突,返回 EINVAL
  3. 增加 shm_nattch,更新时间 shm_atimshm_lprid
  4. 通过 do_mmap_pgoff()shm_file 映射到进程用户空间(用户态虚拟地址因进程不同而异,但指向同一物理页集合)。

简要实现(ipc/shm.c:sys_shmat/do_shmat

asmlinkage long sys_shmat(int shmid, char __user *shmaddr, int shmflg)
{
    unsigned long addr;
    int err;

    // do_shmat 负责权限/地址检查与实际映射
    err = do_shmat(shmid, shmaddr, shmflg, &addr);
    if (err)
        return err;
    return addr; // 用户态虚拟地址
}

static int do_shmat(int shmid, char __user *shmaddr, int shmflg,
                    unsigned long *raddr)
{
    struct shmid_kernel *shp;
    struct file *file;
    unsigned long addr, flags;

    // 获取并锁定段
    shp = shm_lock(shmid);
    if (!shp)
        return -EINVAL;
    if (ipc_checkid(&shp->shm_perm, shmid)) {
        shm_unlock(shmid);
        return -EIDRM;
    }
    if (ipcperms(&shp->shm_perm, (shmflg & SHM_RDONLY) ? S_IRUGO : S_IRUGO|S_IWUGO)) {
        shm_unlock(shmid);
        return -EACCES;
    }

    file = shp->shm_file;
    get_file(file); // 引用计数+1, 防止映射期间文件被释放

    // 处理地址/对齐
    addr = (unsigned long)shmaddr;
    if (shmflg & SHM_RND)
        addr &= ~(SHMLBA - 1);

    flags = MAP_SHARED;
    if (shmflg & SHM_RDONLY)
        flags |= PROT_READ;
    else
        flags |= PROT_READ | PROT_WRITE;

    // 实际映射: do_mmap_pgoff 返回用户态虚拟地址
    addr = do_mmap_pgoff(file, addr, shp->shm_segsz, flags, 0, 0);
    if (IS_ERR_VALUE(addr)) {
        fput(file);
        shm_unlock(shmid);
        return addr;
    }

    // 更新状态
    shp->shm_nattch++;              // 映射计数+1
    shp->shm_atim = get_seconds();  // 记录最后 attach 时间
    shp->shm_lprid = current->pid;  // 记录最后操作 PID

    shm_unlock(shmid);
    *raddr = addr;
    return 0;
}

解除映射(shmdt → do_shmdt)

  1. 查找并拆除进程中对应的 VMA 区域(按基地址与长度)。
  2. 递减 shm_nattch,更新时间 shm_dtimshm_lprid
  3. 如果段已被标记删除且 shm_nattch==0,释放资源。

简要实现(ipc/shm.c:sys_shmdt/do_shmdt

asmlinkage long sys_shmdt(char __user *shmaddr)
{
    return do_shmdt((unsigned long)shmaddr);
}

static int do_shmdt(unsigned long addr)
{
    struct shmid_kernel *shp;
    int retval;

    // 通过 VMA 找到对应的 shm 段, 执行 unmap
    retval = shm_unmap(addr); // 内部执行 find_vma + do_munmap
    if (retval)
        return retval;

    // shm_unmap 中会递减 shm_nattch,并在必要时触发销毁
    return 0;
}

控制(shmctl)

  • IPC_RMID: 标记段删除,若 shm_nattch==0 立即释放(shm_destroy),否则等待最后一个 detach 后释放。
  • IPC_SET: 更新权限和 shm_perm.uid/gid/mode,更新时间戳。
  • IPC_STAT: 填充 shmid_ds 返回用户态。
  • SHM_LOCK/SHM_UNLOCK: 锁定/解锁页以防换出(需要 CAP_IPC_LOCK),内部通过 shm_lock/shm_unlock 配合 shmem。

简要实现(ipc/shm.c:sys_shmctl,聚焦 IPC_RMID)

asmlinkage long sys_shmctl(int shmid, int cmd, struct shmid_ds __user *buf)
{
    struct shmid_kernel *shp;
    int err;

    down(&shm_ids.sem);              // IPC 表级锁
    shp = shm_lock(shmid);           // 对象锁 + 获取
    if (!shp) {
        err = -EINVAL;
        goto out_up;
    }
    if (ipc_checkid(&shp->shm_perm, shmid)) {
        err = -EIDRM;
        goto out_unlock_up;
    }

    switch (cmd) {
    case IPC_RMID:
        // 从 IPC 表移除,标记删除
        shm_destroy(shp);
        err = 0;
        break;
    case IPC_SET:
        // 更新权限/属主/模式
        err = shmctl_down(shp, cmd, buf);
        break;
    case IPC_STAT:
        // 拷贝状态给用户
        err = shmctl_stat(shp, buf);
        break;
    case SHM_LOCK:
    case SHM_UNLOCK:
        err = shmctl_do_lock(shp, cmd);
        break;
    default:
        err = -EINVAL;
        break;
    }

out_unlock_up:
    shm_unlock(shmid);
out_up:
    up(&shm_ids.sem);
    return err;
}

释放与回收

  • shm_destroy:从 IPC 表移除 (ipc_rmid),关闭 shm_file,释放 shmid_kernel
  • 如果段被 IPC_RMID 标记,最后一个 shmdt 完成后触发真正释放。

删除与回收(ipc/shm.c:shm_destroy

static void shm_destroy(struct shmid_kernel *shp)
{
    // 从 IPC ID 表移除并标记删除
    ipc_rmid(&shm_ids, &shp->shm_perm);

    // 关闭支撑文件,释放引用
    fput(shp->shm_file);

    // 释放描述符 (RCU)
    ipc_rcu_putref(shp);
}

并发与锁

  • IPC 表锁:shm_ids.sem 序列化添加/删除。
  • 对象锁:ipc_lock 保护单个段的元数据(权限、计数、状态)。
  • RCU/引用计数:ipc_rcu_alloc/putref 管理对象生命周期;shm_nattch 计数配合删除判定。

相关限制(2.6.12)

  • shmmax:单段最大字节数 (/proc/sys/kernel/shmmax)
  • shmmni:系统最大段数 (/proc/sys/kernel/shmmni)
  • shmall:可用页框总数上限 (/proc/sys/kernel/shmall)
  • SHMLBA:映射对齐粒度(通常为页或更大,对齐到大页边界以兼容 VIPT/TLB 需求)

重要特性/行为

  • 段由 shmem(tmpfs)文件承载页缓存,支持按需分配与换出(除非 SHM_LOCK)。
  • shmat 映射地址位于用户态虚拟地址空间,各进程地址可不同但物理页共享。
  • shmdt 仅拆映射并递减计数,不一定释放物理页;IPC_RMID + shm_nattch==0 才真正销毁。
  • SEM_UNDO 不涉及共享内存;共享内存的同步需用户态自管(信号量/互斥锁等)。

参考函数/符号(2.6.12)

  • sys_shmget / sys_shmat / sys_shmdt / sys_shmctl
  • newseg / do_shmat / do_shmdt / shm_destroy
  • shmem_file_setup(支撑文件)/ do_mmap_pgoff(映射)
  • ipc_addid / ipc_rmid / ipc_lock / ipc_checkid
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值