Linux进程间通讯(四)共享内存

Linux进程间通讯

Linux进程间通讯(一)信号(上)

Linux进程间通讯(二)信号(下)

Linux进程间通讯(三)管道

Linux进程间通讯(四)共享内存

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, &current->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 中找到一个空闲页,或者从伙伴系统中分配空闲页,最后建立页表
发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102670752