1.共享内存
1.1 概念和原理
共享内存是进程间通信的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容,其他进程都会察觉到这个更改。
基本原理:共享内存允许多个进程将同一块物理内存映射到各自的虚拟地址空间中,从而实现数据的直接读写和共享。各进程可以直接访问这块内存区域,无需内核反复拷贝数据,通信速度快。
举个例子:就像多个人(进程)在同一间会议室(物理内存),每个人都能看到和改动会议室中的白板(共享内存)。
1.2 共享内存的特点
- 共享内存是进程间共享数据的一种最快方式。一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
- 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。若一个进程正在向共享内存区数据,则在它做完这一步操作之前,别的进程不应当去读、写这些数据。
1.3 核心机制
共享内存的使用包括 :
- 调用 shmget()创建共享内存
- 调用 shmat() 映射共享内存至进程虚拟空间
- 调用 shmdt()接触映射关系
- 操作系统内核分配内存区
- 进程通过系统调用(如 shmget, mmap等)请求一块共享内存区域
- 内核分配一块实际的物理内存,并为其分配一个唯一的标识符(如 shmid)
- 进程映射到自己的地址空间
- 进程通过 shmat、mmap等系统调用将这块物理内存映射到自己的虚拟地址空间
- 同一块物理内存可以被多个进程映射,各自获得一个指针,指向"同样的数据"
- 读写共享
- 进程直接读取/写入这块共享内存,所有映射到此内存的进程都能“看到“数据的变化
- 无需内核缓冲区的拷贝,效率高
1.4 创建共享内存
共享内存的创建通过shmget()实现。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
- key: 共享内存的键值,用于标识共享内存段。通常使用 ftok 函数生成键值。
- size: 共享内存段的大小,以字节为单位
- shmflg: 共享内存的标志位,用于指定创建共享内存的权限和行为
返回值:
- 成功时,返回共享内存的标识符(即共享内内存的ID)
- 失败时,返回-1,并设置相应的错误码
int
shmget (key_t key, size_t size, int shmflg)
{
#ifdef __ASSUME_DIRECT_SYSVIPC_SYSCALLS
return INLINE_SYSCALL_CALL (shmget, key, size, shmflg, NULL);
#else
return INLINE_SYSCALL_CALL (ipc, IPCOP_shmget, key, size, shmflg, NULL);
#endif
}
其中,INLINE_SYSCALL_CALL是封装系统调用的宏。展开看,其实就是最终会调用Linux系统调用接口(实际会转为 syscall : 是对底层内核系统调用的直接封装,会触发软中断进入内核)。
在Linux内核 ipc/shm.c 中给出了syscall函数实现:
SYSCALL_DEFINE3(shmget, key_t, key, size_t, size, int, shmflg)
{
return ksys_shmget(key, size, shmflg);
}
long ksys_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);
}
该函数创建对应的 ipc_namespace 指针并指向对应的 ipc_ns, 初始化共享内存对应的操作shm_ops,并将传参key, size, shmflg封装为传参shm_params,最终调用 ipcget()。
/**
* ipcget - Common sys_*get() code
* @ns: namespace
* @ids: ipc identifier set
* @ops: operations to be called on ipc object creation, permission checks
* and further checks
* @params: the parameters needed by the previous operations.
*
* Common routine called by sys_msgget(), sys_semget() and sys_shmget().
*/
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
if (params->key == IPC_PRIVATE)
return ipcget_new(ns, ids, ops, params);
else
return ipcget_public(ns, ids, ops, params);
}
参数:
- ns: 命名空间指针,支持容器/多实例的IPC资源隔离
- ids: 当前IPC 对象(如共享内存/信号量消息队列)集合的数据结构
- ops: 用于对象创建、权限检查等操作函数集合
- params: 本次操作的参数 (如:key、flags、size等)
ipc_get根据传入参数params->key进行不同的操作
/**
* ipcget_new - create a new ipc object
* @ns: ipc namespace
* @ids: ipc identifier set
* @ops: the actual creation routine to call
* @params: its parameters
*
* This routine is called by sys_msgget, sys_semget() and sys_shmget()
* when the key is IPC_PRIVATE.
*/
static int ipcget_new(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
int err;
down_write(&ids->rwsem);
err = ops->getnew(ns, params);
up_write(&ids->rwsem);
return err;
}
/**
* ipcget_public - get an ipc object or create a new one
* @ns: ipc namespace
* @ids: ipc identifier set
* @ops: the actual creation routine to call
* @params: its parameters
*
* This routine is called by sys_msgget, sys_semget() and sys_shmget()
* when the key is not IPC_PRIVATE.
* It adds a new entry if the key is not found and does some permission
* / security checkings if the key is found.
*
* On success, the ipc id is returned.
*/
static int ipcget_public(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
struct kern_ipc_perm *ipcp;
int flg = params->flg;
int err;
/*
* Take the lock as a writer since we are potentially going to add
* a new entry + read locks are not "upgradable"
*/
down_write(&ids->rwsem);
ipcp = ipc_findkey(ids, params->key);
if (ipcp == NULL) {
/* key not used */
if (!(flg & IPC_CREAT))
err = -ENOENT;
else
err = ops->getnew(ns, params);
} else {
/* ipc object has been locked by ipc_findkey() */
if (flg & IPC_CREAT && flg & IPC_EXCL)
err = -EEXIST;
else {
err = 0;
if (ops->more_checks)
err = ops->more_checks(ipcp, params);
if (!err)
/*
* ipc_check_perms returns the IPC id on
* success
*/
err = ipc_check_perms(ns, ipcp, ops, params);
}
ipc_unlock(ipcp);
}
up_write(&ids->rwsem);
return err;
}
当key为 IPC_PRIVATE时调用 ipcget_new() 函数,首先通过 down_write(&ids->rwsem)为写加锁,保证IPC对象集合的并发安全(防止并发创建/修改),再 ops->getnew(ns, params)根据具体IPC对象类型创建新的对象,最后 up_write()释放写锁,返回新对象的IPC id或错误码。
而不为 IPC_PRIVATE时调用的 ipcget_public: 同样先对写加锁,然后通过 ipc_findkey()函数按照key查找 struct kern_ipc_perm:
(1)如果没有找到ipcp == NULL此时应检查是否存在 IPC_CREATE标志
1)如果没有,说明用户只想查找不想新建,则 err = -ENOENT(不存在)
2)如果存在,则通过 ops->getnew 新建对象(和 ipcget_new逻辑一致)
(2)如果对象存在
1)如果同时存在 IPC_CREATE和IPC_EXCL,说明”只要存在会报错“(独占创建),返回 err = -EEXIST
2)否则:执行 ops->more_checks(进一步检查)
再通过 ipc_check_perms检查当前进程对该对象是否具有足够权限
所以新的创建最后都会走到注册的newseg()函数。该函数主要逻辑为
- 通过 kvmalloc() 在直接映射区分配一个 struct shmid_kernel 结构体,该结构体用于描述共享内存。
- 调用hugetlb_file_setup()或shmem_kernel_file_setup()关联文件。虚拟地址空间可以和物理内存关联,但是页表的申请条件中会避开已分配的映射,即物理内存是某个进程独享的。所以如何实现物理内存向多个进程的虚拟内存映射呢?这里就要靠文件来实现了:虚拟地址空间也可以映射到一个文件,文件是可以跨进程共享的。这里我们并不是映射到硬盘上存储的文件,而是映射到内存文件系统上的文件。这里定要注意区分 shmem 和 shm ,前者是一个文件系统,后者是进程通信机制。
- 通过 ipc_addid() 将新创建的 struct shmid_kernel 结构挂到 shm_ids 里面的基数树上,返回相应的 id,并且将 struct shmid_kernel 挂到当前进程的 sysvshm 队列中。
/** * newseg - Create a new shared memory segment * @ns: namespace * @params: ptr to the structure that contains key, size and shmflg * * Called with shm_ids.rwsem held as a writer. */ 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_SHIFT; struct file *file; char name[13]; ...... shp = kvmalloc(sizeof(*shp), GFP_KERNEL); ...... shp->shm_perm.key = key; shp->shm_perm.mode = (shmflg & S_IRWXUGO); shp->mlock_user = NULL; shp->shm_perm.security = NULL; ...... if (shmflg & SHM_HUGETLB) { ...... file = hugetlb_file_setup(name, hugesize, acctflag, &shp->mlock_user, HUGETLB_SHMFS_INODE, (shmflg >> SHM_HUGE_SHIFT) & SHM_HUGE_MASK); } else { ...... file = shmem_kernel_file_setup(name, size, acctflag); } ...... shp->shm_cprid = get_pid(task_tgid(current)); shp->shm_lprid = NULL; shp->shm_atim = shp->shm_dtim = 0; shp->shm_ctim = ktime_get_real_seconds(); shp->shm_segsz = size; shp->shm_nattch = 0; shp->shm_file = file; shp->shm_creator = current; /* ipc_addid() locks shp upon success. */ error = ipc_addid(&shm_ids(ns), &shp->shm_perm, ns->shm_ctlmni); ...... list_add(&shp->shm_clist, ¤t->sysvshm.shm_clist); /* * shmid gets reported as "inode#" in /proc/pid/maps. * proc-ps tools use this. Changing this will break them. */ file_inode(file)->i_ino = shp->shm_perm.id; ns->shm_tot += numpages; error = shp->shm_perm.id; ...... }
实际上
shmem_kernel_file_setup()
会在shmem文件系统里面创建一个文件:__shmem_file_setup()
会创建新的 shmem 文件对应的 dentry 和 inode,并将它们两个关联起来,然后分配一个 struct file 结构来表示新的 shmem 文件,并且指向独特的 shmem_file_operations。/** * shmem_kernel_file_setup - get an unlinked file living in tmpfs which must be kernel internal. * @name: name for dentry (to be seen in /proc/<pid>/maps * @size: size to be set for the file * @flags: VM_NORESERVE suppresses pre-accounting of the entire object size */ struct file *shmem_kernel_file_setup(const char *name, loff_t size, unsigned long flags) { return __shmem_file_setup(name, size, flags, S_PRIVATE); } static struct file *__shmem_file_setup(const char *name, loff_t size, unsigned long flags, unsigned int i_flags) { struct file *res; struct inode *inode; struct path path; struct super_block *sb; struct qstr this; ...... this.name = name; this.len = strlen(name); this.hash = 0; /* will go */ sb = shm_mnt->mnt_sb; path.mnt = mntget(shm_mnt); path.dentry = d_alloc_pseudo(sb, &this); d_set_d_op(path.dentry, &anon_ops); ...... inode = shmem_get_inode(sb, NULL, S_IFREG | S_IRWXUGO, 0, flags); inode->i_flags |= i_flags; d_instantiate(path.dentry, inode); inode->i_size = size; ...... res = alloc_file(&path, FMODE_WRITE | FMODE_READ, &shmem_file_operations); return res; }
1.5 共享内存的映射
从上述代码详解中可知,共享内存的数据结构 struct shmid_kernel通过成员 struct file* shm_file来管理内存文件系统 shmem上的内存文件。无论这个共享内存是否被映射,shm_file都是存在的。
对于用户来说,共享内存的映射是通过调用 shmat()完成的。
void *
shmat (int shmid, const void *shmaddr, int shmflg)
{
#ifdef __ASSUME_DIRECT_SYSVIPC_SYSCALLS
return (void*) INLINE_SYSCALL_CALL (shmat, shmid, shmaddr, shmflg);
#else
unsigned long resultvar;
void *raddr;
resultvar = INTERNAL_SYSCALL_CALL (ipc, IPCOP_shmat, shmid, shmflg,
&raddr, shmaddr);
if (INTERNAL_SYSCALL_ERROR_P (resultvar))
return (void *) INLINE_SYSCALL_ERROR_RETURN_VALUE (INTERNAL_SYSCALL_ERRNO (resultvar));
return raddr;
#endif
}
参数:
- shmid:共享内存段ID(由shmget()返回)
- shmaddr:建议映射到的地址(通常为NULL,让内核自动分配)
- shmflg:标志,如:SHM_RDONLY(只读映射)、SHM_RND(地址对齐)
—— 直接系统调用
#ifdef __ASSUME_DIRECT_SYSVIPC_SYSCALLS
return (void*) INLINE_SYSCALL_CALL (shmat, shmid, shmaddr, shmflg);
- 直接通过 glibc 的
INLINE_SYSCALL_CALL
宏,发起shmat
系统调用。 - 该宏最终转为一个 syscall 指令,进入内核态执行真正的映射(相当于
syscall(SYS_shmat, shmid, shmaddr, shmflg)
)。
——ipc多路系统调用
#else
unsigned long resultvar;
void *raddr;
resultvar = INTERNAL_SYSCALL_CALL (ipc, IPCOP_shmat, shmid, shmflg,
&raddr, shmaddr);
if (INTERNAL_SYSCALL_ERROR_P (resultvar))
return (void *) INLINE_SYSCALL_ERROR_RETURN_VALUE (INTERNAL_SYSCALL_ERRNO (resultvar));
return raddr;
#endif
- 兼容老内核或特殊平台:通过
ipc
这个多路系统调用(用第一个参数区分功能)。 INTERNAL_SYSCALL_CALL
实际会做:syscall(SYS_ipc, IPCOP_shmat, shmid, shmflg, &raddr, shmaddr)
- 其中
IPCOP_shmat
表示这是shmat
子功能。
- 返回值处理:
- 如果调用失败,返回一个错误指针(
(void*) -1
),并设置 errno。 - 成功则返回映射到进程空间的实际地址
raddr
。
- 如果调用失败,返回一个错误指针(
在 Linux中 /ipc/shm.c 中系统调用的实现为:
SYSCALL_DEFINE3(shmat, int, shmid, char __user *, shmaddr, int, shmflg)
{
unsigned long ret;
long err;
err = do_shmat(shmid, shmaddr, shmflg, &ret, SHMLBA);
if (err)
return err;
force_successful_syscall_return();
return (long)ret;
}
该syscall调用 do_shmat() 函数
/*
* Fix shmaddr, allocate descriptor, map shm, add attach descriptor to lists.
*
* NOTE! Despite the name, this is NOT a direct system call entrypoint. The
* "raddr" thing points to kernel space, and there has to be a wrapper around
* this.
*/
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, *base;
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;
err = -EINVAL;
if (shmid < 0)
goto out;
if (addr) {
if (addr & (shmlba - 1)) {
if (shmflg & SHM_RND) {
addr &= ~(shmlba - 1); /* round down */
/*
* Ensure that the round-down is non-nil
* when remapping. This can happen for
* cases when addr < shmlba.
*/
if (!addr && (shmflg & SHM_REMAP))
goto out;
} else
#ifndef __ARCH_FORCE_SHMLBA
if (addr & ~PAGE_MASK)
#endif
goto out;
}
flags |= MAP_FIXED;
} else if ((shmflg & SHM_REMAP))
goto out;
if (shmflg & SHM_RDONLY) {
prot = PROT_READ;
acc_mode = S_IRUGO;
f_flags = O_RDONLY;
} else {
prot = PROT_READ | PROT_WRITE;
acc_mode = S_IRUGO | S_IWUGO;
f_flags = O_RDWR;
}
if (shmflg & SHM_EXEC) {
prot |= PROT_EXEC;
acc_mode |= S_IXUGO;
}
/*
* We cannot rely on the fs check since SYSV IPC does have an
* additional creator id...
*/
ns = current->nsproxy->ipc_ns;
rcu_read_lock();
shp = shm_obtain_object_check(ns, shmid);
if (IS_ERR(shp)) {
err = PTR_ERR(shp);
goto out_unlock;
}
err = -EACCES;
if (ipcperms(ns, &shp->shm_perm, acc_mode))
goto out_unlock;
err = security_shm_shmat(&shp->shm_perm, shmaddr, shmflg);
if (err)
goto out_unlock;
ipc_lock_object(&shp->shm_perm);
/* check if shm_destroy() is tearing down shp */
if (!ipc_valid_object(&shp->shm_perm)) {
ipc_unlock_object(&shp->shm_perm);
err = -EIDRM;
goto out_unlock;
}
/*
* We need to take a reference to the real shm file to prevent the
* pointer from becoming stale in cases where the lifetime of the outer
* file extends beyond that of the shm segment. It's not usually
* possible, but it can happen during remap_file_pages() emulation as
* that unmaps the memory, then does ->mmap() via file reference only.
* We'll deny the ->mmap() if the shm segment was since removed, but to
* detect shm ID reuse we need to compare the file pointers.
*/
base = get_file(shp->shm_file);
shp->shm_nattch++;
size = i_size_read(file_inode(base));
ipc_unlock_object(&shp->shm_perm);
rcu_read_unlock();
err = -ENOMEM;
sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
if (!sfd) {
fput(base);
goto out_nattch;
}
file = alloc_file_clone(base, f_flags,
is_file_hugepages(base) ?
&shm_file_operations_huge :
&shm_file_operations);
err = PTR_ERR(file);
if (IS_ERR(file)) {
kfree(sfd);
fput(base);
goto out_nattch;
}
sfd->id = shp->shm_perm.id;
sfd->ns = get_ipc_ns(ns);
sfd->file = base;
sfd->vm_ops = NULL;
file->private_data = sfd;
err = security_mmap_file(file, prot, flags);
if (err)
goto out_fput;
if (mmap_write_lock_killable(current->mm)) {
err = -EINTR;
goto out_fput;
}
if (addr && !(shmflg & SHM_REMAP)) {
err = -EINVAL;
if (addr + size < addr)
goto invalid;
if (find_vma_intersection(current->mm, addr, addr + size))
goto invalid;
}
addr = do_mmap(file, addr, size, prot, flags, 0, 0, &populate, NULL);
*raddr = addr;
err = 0;
if (IS_ERR_VALUE(addr))
err = (long)addr;
invalid:
mmap_write_unlock(current->mm);
if (populate)
mm_populate(addr, populate);
out_fput:
fput(file);
out_nattch:
down_write(&shm_ids(ns).rwsem);
shp = shm_lock(ns, shmid);
shp->shm_nattch--;
if (shm_may_destroy(shp))
shm_destroy(ns, shp);
else
shm_unlock(shp);
up_write(&shm_ids(ns).rwsem);
return err;
out_unlock:
rcu_read_unlock();
out:
return err;
}
该函数负责将已存在的共享内存段(由 shmid 标识)映射到当前进程的虚拟地址空间,并处理各种权限、参数、内存分配和管理细节。主要逻辑为:
(1)参数与地址合法性检查
- 检查 shmid 是否为负,若为负则非法。
- 如果用户指定
shmaddr
(希望映射到某处),需检查地址是否满足对齐要求(shmlba,系统页面/大页对齐)。SHM_RND
标志:自动向下对齐到 shmlba 的整数倍。MAP_FIXED
标志:用户要求必须映射到指定地址。(2)访问权限与 flag 处理
SHM_RDONLY
→ 映射为只读,权限检查 S_IRUGO,文件 O_RDONLY。- 否则读写,权限 S_IRUGO | S_IWUGO,O_RDWR。
SHM_EXEC
→ 增加可执行权限。- 设定 mmap 的 prot、flags、acc_mode、f_flags。
(3)查找并验证共享内存对象
- 获取当前命名空间指针。
shm_obtain_object_check()
获取 shmid 对应的内核对象shmid_kernel *shp
,并做有效性检查。ipcperms()
检查权限。security_shm_shmat()
进一步 LSM 安全检查。(4)对象锁定和有效性/销毁检查
- 锁定 shm_perm,防止并发操作。
- 检查对象是否正在销毁(已被删除但还有引用)。
- 增加引用计数(shm_nattch++)。
(5)文件结构准备
- 为内存文件分配一个 file 结构,分配跟踪数据(
struct shm_file_data
)。alloc_file_clone()
创建 file 结构体,用于 do_mmap。- 设置 file->private_data、权限等。
(6)安全检查&加锁虚拟内存空间
security_mmap_file()
LSM 检查。- 对当前进程
mm
加写锁,防止并发更改。(7)虚拟内存区域检查与 do_mmap
- 若指定了地址且不重映射,需确保虚拟区间未被占用。
do_mmap()
真正执行mmap,将共享内存文件映射到进程虚拟空间(核心操作)。- 返回实际映射地址给用户层。
(8)后处理与资源回收
- 解锁、减少引用计数。
- 如果引用计数为0且已被删除,则销毁(shm_destroy)。
- 释放所有临时结构和文件。
- 错误处理分支,保证所有引用和锁都能正确释放。
共享内存的映射完整流程:
- 用户进程调用
shmat()
,通过系统调用进入内核。 - 内核层
do_shmat()
完成参数校验、权限检查、内核对象查找与锁定。 - 增加引用计数,准备文件结构,安全校验。
- 通过
do_mmap()
将共享内存的物理页映射到进程虚拟空间。 - 返回虚拟地址给用户进程,进程即可直接访问共享数据。
- 进程用完后
shmdt()
解除映射,引用计数递减,最终安全释放物理资源。 - 整个流程确保资源安全、权限合规、并发可控,是高效进程间通信的基础。