Linux进程间通讯
Linux进程间通讯(四)共享内存
一、IPC总览
在内核中,对于共享内存、消息队列、信号量都是使用统一的机制管理起来的,都叫做 ipcxxx
为了维护这三种进程间通讯的机制,内核定义了一个三项的数组,如下
struct ipc_namespace {
......
struct ipc_ids ids[3];
......
}
#define IPC_SEM_IDS 0
#define IPC_MSG_IDS 1
#define IPC_SHM_IDS 2
#define sem_ids(ns) ((ns)->ids[IPC_SEM_IDS])
#define msg_ids(ns) ((ns)->ids[IPC_MSG_IDS])
#define shm_ids(ns) ((ns)->ids[IPC_SHM_IDS])
其中0号元素表示信号量、1号元素表示消息队列,2号元素表示共享内存
下面看一下 struct ipc_ids 的定义
struct ipc_ids {
int in_use;
unsigned short seq;
struct rw_semaphore rwsem;
struct idr ipcs_idr;
int next_id;
};
struct idr {
struct radix_tree_root idr_rt;
unsigned int idr_next;
};
- in_use 表示有多少个 ipc
- seq 和 next_id 用于一起生成 ipc 唯一的 id
- ipcs_idr 是一棵基数树,每中 ipc 都维护着这样的一棵树,用于快速通过 id 找到对应的 ipc 对象
下面来讨论共享内存
二、创建内存
创建共享内存通过系统调用 shmget,下面来看一看它的定义
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 = shm_security,
.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);
}
shmget 中调用了 ipcget,这个函数用来创建一个 ipc 对象。这里的参数为 shm_ids(表明了创建的是共享内存对象),对应的操作 shm_ops,以及对应的参数
ipcget 的定义如下
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);
}
如果 key 是 IPC_PRIVATE,那么就调用 ipcget_new 永远创建新的;不然的话,就会调用 ipcget_public
ipcget_public 的定义如下
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;
ipcp = ipc_findkey(ids, params->key);
if (ipcp == NULL) {
if (!(flg & IPC_CREAT))
err = -ENOENT;
else
err = ops->getnew(ns, params);
} else {
if (flg & IPC_CREAT && flg & IPC_EXCL)
err = -EEXIST;
else {
err = 0;
if (ops->more_checks)
err = ops->more_checks(ipcp, params);
......
}
}
return err;
}
首先会根据 key 在信号量的 ids 中查找指定的 ipc 对象,如果没有找到,并且设置了 IPC_CREAT,那么就通过 ops->getnew 来创建一个新的。这个 ops 是通过一开始的函数传参传递过来的,其对应 shm_ops,这里面的 getnew 函数对应的就是 newseg,定义如下
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];
vm_flags_t acctflag = 0;
......
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;
......
file = shmem_kernel_file_setup(name, size, acctflag);
......
shp->shm_cprid = task_tgid_vnr(current);
shp->shm_lprid = 0;
shp->shm_atim = shp->shm_dtim = 0;
shp->shm_ctim = get_seconds();
shp->shm_segsz = size;
shp->shm_nattch = 0;
shp->shm_file = file;
shp->shm_creator = current;
error = ipc_addid(&shm_ids(ns), &shp->shm_perm, ns->shm_ctlmni);
......
list_add(&shp->shm_clist, ¤t->sysvshm.shm_clist);
......
file_inode(file)->i_ino = shp->shm_perm.id;
ns->shm_tot += numpages;
error = shp->shm_perm.id;
......
return error;
}
-
第一步是通过 kvmalloc 在直接映射区分配一个 shmid_kernel 对象,这个结构是用来描述共享内存的
-
第二步是将共享内存和文件进行关联,为什么要做这个操作呢?因为虚拟空间可以映射到物理内存,也可以映射到文件,而物理内存是某个进程共享的,而文件则可以跨多个进程共享
这里的共享内存需要跨进程共享,可以借鉴文件映射的思想。只不过这里不是映射到磁盘的文件,而是映射到内存文件系统上特殊的文件。在 mm/shmem.c 中定义了这样的一个基于内存的文件系统,这里的 shmem 要和 shm 区分,shmem 是一种文件系统,而 shm 是进程间通讯的一种方式
系统初始化的时候,会注册这样的一个文件系统,如下定义
int __init shmem_init(void) { int error; error = shmem_init_inodecache(); error = register_filesystem(&shmem_fs_type); shm_mnt = kern_mount(&shmem_fs_type); ...... return 0; } static struct file_system_type shmem_fs_type = { .owner = THIS_MODULE, .name = "tmpfs", .mount = shmem_mount, .kill_sb = kill_litter_super, .fs_flags = FS_USERNS_MOUNT, };
newseg 会调用 shmem_kernel_file_setup,在 shmem 文件系统中创建文件,其定义如下
/** * 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; }
__shmem_file_setup 会创建 dentry 和 inode,然后将它们关联起来,之后会创建 struct file,然后指定操作为 shmem_file_operations。注意,这里的 struct file 并不属于某个具体的进程,它是属于这个共享内存对象的,独立于具体进程而存在
shmem_file_operations 的定义如下
static const struct file_operations shmem_file_operations = { .mmap = shmem_mmap, .get_unmapped_area = shmem_get_unmapped_area, #ifdef CONFIG_TMPFS .llseek = shmem_file_llseek, .read_iter = shmem_file_read_iter, .write_iter = generic_file_write_iter, .fsync = noop_fsync, .splice_read = generic_file_splice_read, .splice_write = iter_file_splice_write, .fallocate = shmem_fallocate, #endif };
-
第三步就是通过 ipc_addid,将 这个共享内存对象挂到共享内存对应的 shm_ids上,然后将共享内存对象挂到当前进程的 sysvshm 队列中
至此,共享内存的创建已经完成了
三、将共享内存映射到进程虚拟地址空间
从上面的代码中,我们可以知道,共享内存的数据结构叫做 struct shmid_kernel,通过它的成员 struct file* shm_file,来管理内存文件系统 shmem 上的内存文件,无论这个共享内存是否被映射,这个 shm_file 都会存在
接下来,要将共享内存映射到虚拟地址空间上,使用的是 shmat,其定义如下
SYSCALL_DEFINE3(shmat, int, shmid, char __user *, shmaddr, int, shmflg)
{
unsigned long ret;
long err;
err = do_shmat(shmid, shmaddr, shmflg, &ret, SHMLBA);
force_successful_syscall_return();
return (long)ret;
}
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;
struct path path;
fmode_t f_mode;
unsigned long populate = 0;
......
prot = PROT_READ | PROT_WRITE;
acc_mode = S_IRUGO | S_IWUGO;
f_mode = FMODE_READ | FMODE_WRITE;
......
ns = current->nsproxy->ipc_ns;
shp = shm_obtain_object_check(ns, shmid);
......
path = shp->shm_file->f_path;
path_get(&path);
shp->shm_nattch++;
size = i_size_read(d_inode(path.dentry));
......
sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
......
file = alloc_file(&path, f_mode,
is_file_hugepages(shp->shm_file) ?
&shm_file_operations_huge :
&shm_file_operations);
......
file->private_data = sfd;
file->f_mapping = shp->shm_file->f_mapping;
sfd->id = shp->shm_perm.id;
sfd->ns = get_ipc_ns(ns);
sfd->file = shp->shm_file;
sfd->vm_ops = NULL;
......
addr = do_mmap_pgoff(file, addr, size, prot, flags, 0, &populate, NULL);
*raddr = addr;
err = 0;
......
return err;
}
首先通过 shm_obtain_object_check,利用共享内存的id,找到了相应的共享内存对象 struct shmid_kernel,然后可以通过它来找到管理共享内存文件的 struct file* shm_file
接着分配一个 struct shm_file_data 对象,用来保存共享内存的信息
然后为进程分配一个 struct file 对象,并将 shm_file_data 设置到 file 的 private_data 中,这样我们就可以通过这个 struct file 来访问到真正的共享内存文件了
然后指定的操作为 shm_file_operations,其定义如下
static const struct file_operations shm_file_operations = {
.mmap = shm_mmap,
.fsync = shm_fsync,
.release = shm_release,
.get_unmapped_area = shm_get_unmapped_area,
.llseek = noop_llseek,
.fallocate = shm_fallocate,
};
为什么要再创建一个 struct file 对象呢?
shmem 中的 struct file 是用来管理共享内存文件的;这个为进程创建的struct file 是用来做内存映射的
在为进程分配地址空间的时候,会使用 vm_area_struct 来管理一块区域,当这个对象里面的 struct file* vm_file 为 NULL 时,表示这块虚拟内存是直接映射到物理内存中的,当有指向某个 struct file 的时候 ,表示虚拟内存是映射到文件的。而我们前面说过,只有通过文件映射才能实现跨进程间共享内存,所以这个 vm_area_struct 中的 struct file* vm_file 必须指向一个文件
接下俩继续调用的是 do_mmap_pgoff,这个函数是用来映射内存的。它会分配一个 vm_area_struct 指向虚拟地址空间中未被使用的部分,然后其 struct file* vm_file 指向为新创建的 struct file,然后回调用 shm_file_operations 的 mmap 函数,也就是 shm_mmap 来映射,定义如下
static int shm_mmap(struct file *file, struct vm_area_struct *vma)
{
struct shm_file_data *sfd = shm_file_data(file);
int ret;
ret = __shm_open(vma);
ret = call_mmap(sfd->file, vma);
sfd->vm_ops = vma->vm_ops;
vma->vm_ops = &shm_vm_ops;
return 0;
}
shm_mmap 中调用的是 struct shm_file_data 中的 file 的 mmap 函数,这个 file 对应的就是 shmem 文件系统上真正的文件。这里的 mmap 对应的是 shmem_mmap,其定义如下
static int shmem_mmap(struct file *file, struct vm_area_struct *vma)
{
file_accessed(file);
vma->vm_ops = &shmem_vm_ops;
return 0;
}
回到 shm_mmap,执行到最后 sfd->vm_ops 对应的是 shemem_vm_ops,vma->vm_ops 对应的是 shm_vm_ops
下面看一看这两个操作集的定义
static const struct vm_operations_struct shm_vm_ops = {
.open = shm_open, /* callback for a new vm-area open */
.close = shm_close, /* callback for when the vm-area is released */
.fault = shm_fault,
};
static const struct vm_operations_struct shmem_vm_ops = {
.fault = shmem_fault,
.map_pages = filemap_map_pages,
};
这里面最重要的就是 fault 函数,当访问虚拟内存未经映射的时候,就会触发缺页异常,最后调用到 vm_operations_struct 的 fault 函数
关于映射共享内存到此为止,接下来就是访问共享内存
四、访问共享内存
其实到现在还未真正的分配物理内存,只是分配好虚拟地址空间,上面说了当访问虚拟内存未经映射的时候,就会触发缺页异常,最后调用到 vm_operations_struct 的 fault 函数,也就是 shm_faullt,其定义如下
static int shm_fault(struct vm_fault *vmf)
{
struct file *file = vmf->vma->vm_file;
struct shm_file_data *sfd = shm_file_data(file);
return sfd->vm_ops->fault(vmf);
}
可以看到,它会通过 shm_file_data 的 vm_ops 调用 fault,也就是对应 shmem_vm_ops 的 falut,也就是 shmem_fault 函数,其定义如下
static int shmem_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct inode *inode = file_inode(vma->vm_file);
gfp_t gfp = mapping_gfp_mask(inode->i_mapping);
......
error = shmem_getpage_gfp(inode, vmf->pgoff, &vmf->page, sgp,
gfp, vma, vmf, &ret);
......
}
/*
* shmem_getpage_gfp - find page in cache, or get from swap, or allocate
*
* If we allocate a new one we do not mark it dirty. That's up to the
* vm. If we swap it in we mark it dirty since we also free the swap
* entry since a page cannot live in both the swap and page cache.
*
* fault_mm and fault_type are only supplied by shmem_fault:
* otherwise they are NULL.
*/
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, int *fault_type)
{
......
page = shmem_alloc_and_acct_page(gfp, info, sbinfo,
index, false);
......
}
shmem_fault 会调用 shmem_getpage_gfp,在 page cache 和 swap 中找到一个空闲页。如果找不到,它会通过
shmem_alloc_and_acct_page 来获取,最终会通过伙伴系统在物理内存中分配页
最终建立页表,至此共享内存才算真正的映射到虚拟地址空间中,进程可以对其进程访问了
五、总结
下面来总结一下共享内存的创建和映射过程
- 1.调用 shmget 创建一个共享内存对象
- 2.通过id来查找共享内存是否被创建,如果没有被创建,那么就创建一个 struct shmid_kernel
- 3.在内存文件系统 shmem 上创建一个文件,然后用 struct shmid_kernel 来指向这个文件,这个文件用 struct file 来表示,当然它也有对应的 inode 和 dentry
- 4.调用 shmat 来讲共享内存映射到虚拟地址空间
- 5.通过 id 找到共享内存对象 struct shmid_kernel,
- 6.创建用于内存映射到文件的 struct file 和 struct shm_file_data,这个 file 是为进程创建的用来映射文件用的,这里面的 shm_file_data 保存着 shmem 文件系统中的共享内存文件的信息
- 7.分配虚拟地址空间,使用 vm_area_struct 来管理,并叫 struct file* vm_file 指向新创建的 file,然后调用这个 file 对应的mmap
- 8.这个 file 对应的 mmap 会转而调用 shmem 文件系统中的 file 对应的 mmap,shmem_mmap 函数
- 9.这个 mmap 会设置 vm_area_struct 的 vm_ops 和 shm_file_data 的 vm_ops,其中最重要的函数时 fault,用于处理缺页异常,此时内存映射已经完成,但是还没有分配物理内存
- 10.当进程访问共享内存的时候,会触发缺页异常,最终调用到 vm_area_struct 中的 fault 函数,然后转而调用 shm_file_data 中的 fault 函数
- 11.最终在 page cache 中找到一个空闲页,或者从伙伴系统中分配空闲页,最后建立页表