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__ 块内, 仅内核可见)structshmid_kernel/* private to the kernel */{structkern_ipc_permshm_perm;// IPC 权限/键值/锁structfile*shm_file;// 关联的 shmem 文件, 承载页缓存intid;// 内部 ID (用于 IPC 表索引)unsignedlongshm_nattch;// 当前附加次数(映射该段的进程数)unsignedlongshm_segsz;// 段大小(字节)time_tshm_atim;// 最后 attach 时间time_tshm_dtim;// 最后 detach 时间time_tshm_ctim;// 最后变更时间pid_tshm_cprid;// 创建者 PIDpid_tshm_lprid;// 最后操作 PIDstructuser_struct*mlock_user;// 锁定内存的用户结构(用于 SHM_LOCK)};shmem_inode_info
- 来自 tmpfs/shmem,作为
shm_file的支撑,负责页缓存与换入换出(除非 SHM_LOCK 锁定)。
系统调用路径(x86_64 类推)
sys_shmget→ipc/shm.c:sys_shmget→newseg()创建段(分配 shmid_kernel + shmem 文件)并通过ipc_addid安装到 IPC 表sys_shmat→ipc/shm.c:sys_shmat→do_shmat()检查权限/地址 → 通过do_mmap_pgoff()把shm_file映射到进程用户空间sys_shmdt→ipc/shm.c:sys_shmdt→do_shmdt()卸载映射,递减shm_nattchsys_shmctl→ipc/shm.c:sys_shmctl处理IPC_RMID/IPC_SET/IPC_STAT/SHM_LOCK/SHM_UNLOCK
关键流程
创建段(shmget → newseg)
- 校验大小不超过
shm_ctlmax,对齐页(向上按页或 SHMLBA)。 - 分配
shmid_kernel(ipc_rcu_alloc),初始化shm_perm、大小、时间戳、计数。 - 创建支撑文件:调用
shmem_file_setup()创建匿名 shmem 文件,关联到shm_file,用于后续映射与页缓存管理。 - 通过
ipc_addid(&shm_ids, …)安装到 IPC ID 表,返回 shmid(序号+序列)。
简要实现(ipc/shm.c:sys_shmget/newseg)
asmlinkagelongsys_shmget(key_tkey,size_tsize,intshmflg){structshmid_kernel*shp;interr,id;// 尺寸检查: 不得超过 shm_ctlmaxif(size>shm_ctlmax)return-EINVAL;// 对齐到页面/SHMLBA (VIPT/TLB 需求)size=ALIGN(size,SHMLBA);// 如果指定 IPC_PRIVATE 或不存在且 IPC_CREAT,则创建id=newseg(key,shmflg,size);returnid;// 返回 shmid (序号+序列)}staticintnewseg(key_tkey,intshmflg,size_tsize){structshmid_kernel*shp;intid,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);returnerr;}// 安装到 IPC ID 表id=ipc_addid(&shm_ids,&shp->shm_perm,shmmni);if(id==-1){fput(shp->shm_file);ipc_rcu_putref(shp);return-ENOSPC;}returnshm_buildid(id,shp->shm_perm.seq);// 返回 shmid}映射(shmat → do_shmat)
- 权限检查与 ID 校验 (
ipc_lock/ipc_checkid/ipcperms)。 - 解析
shmaddr/shmflg:SHM_RDONLY只读映射SHM_RND地址向下按SHMLBA对齐- 若指定地址且违反对齐/冲突,返回
EINVAL
- 增加
shm_nattch,更新时间shm_atim、shm_lprid。 - 通过
do_mmap_pgoff()将shm_file映射到进程用户空间(用户态虚拟地址因进程不同而异,但指向同一物理页集合)。
简要实现(ipc/shm.c:sys_shmat/do_shmat)
asmlinkagelongsys_shmat(intshmid,char__user*shmaddr,intshmflg){unsignedlongaddr;interr;// do_shmat 负责权限/地址检查与实际映射err=do_shmat(shmid,shmaddr,shmflg,&addr);if(err)returnerr;returnaddr;// 用户态虚拟地址}staticintdo_shmat(intshmid,char__user*shmaddr,intshmflg,unsignedlong*raddr){structshmid_kernel*shp;structfile*file;unsignedlongaddr,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=(unsignedlong)shmaddr;if(shmflg&SHM_RND)addr&=~(SHMLBA-1);flags=MAP_SHARED;if(shmflg&SHM_RDONLY)flags|=PROT_READ;elseflags|=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);returnaddr;}// 更新状态shp->shm_nattch++;// 映射计数+1shp->shm_atim=get_seconds();// 记录最后 attach 时间shp->shm_lprid=current->pid;// 记录最后操作 PIDshm_unlock(shmid);*raddr=addr;return0;}解除映射(shmdt → do_shmdt)
- 查找并拆除进程中对应的 VMA 区域(按基地址与长度)。
- 递减
shm_nattch,更新时间shm_dtim、shm_lprid。 - 如果段已被标记删除且
shm_nattch==0,释放资源。
简要实现(ipc/shm.c:sys_shmdt/do_shmdt)
asmlinkagelongsys_shmdt(char__user*shmaddr){returndo_shmdt((unsignedlong)shmaddr);}staticintdo_shmdt(unsignedlongaddr){structshmid_kernel*shp;intretval;// 通过 VMA 找到对应的 shm 段, 执行 unmapretval=shm_unmap(addr);// 内部执行 find_vma + do_munmapif(retval)returnretval;// shm_unmap 中会递减 shm_nattch,并在必要时触发销毁return0;}控制(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)
asmlinkagelongsys_shmctl(intshmid,intcmd,structshmid_ds__user*buf){structshmid_kernel*shp;interr;down(&shm_ids.sem);// IPC 表级锁shp=shm_lock(shmid);// 对象锁 + 获取if(!shp){err=-EINVAL;gotoout_up;}if(ipc_checkid(&shp->shm_perm,shmid)){err=-EIDRM;gotoout_unlock_up;}switch(cmd){caseIPC_RMID:// 从 IPC 表移除,标记删除shm_destroy(shp);err=0;break;caseIPC_SET:// 更新权限/属主/模式err=shmctl_down(shp,cmd,buf);break;caseIPC_STAT:// 拷贝状态给用户err=shmctl_stat(shp,buf);break;caseSHM_LOCK:caseSHM_UNLOCK:err=shmctl_do_lock(shp,cmd);break;default:err=-EINVAL;break;}out_unlock_up:shm_unlock(shmid);out_up:up(&shm_ids.sem);returnerr;}释放与回收
shm_destroy:从 IPC 表移除 (ipc_rmid),关闭shm_file,释放shmid_kernel。- 如果段被 IPC_RMID 标记,最后一个
shmdt完成后触发真正释放。
删除与回收(ipc/shm.c:shm_destroy)
staticvoidshm_destroy(structshmid_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_shmctlnewseg/do_shmat/do_shmdt/shm_destroyshmem_file_setup(支撑文件)/do_mmap_pgoff(映射)ipc_addid/ipc_rmid/ipc_lock/ipc_checkid