匿名内存映射

java中的MemoryFile,c++中的MemoryHeapBase都是使用了匿名内存映射,才可以进程间通信。

但是,它能进程间通信,首先是基于binder通信之上,为什么?后面再讲。

它的原理是,先注册一个设备路径为“/dev/ashmem”的混杂设备,无论哪个进程,只open一次后获得一个fd,然后把这个fd通过binder驱动传递给另一方进程,另一方进程获得自己的fd之后,双方都mmap一次,此时双方mmap的这块内存互相共享。

老罗的博客里讲的就是这个事。

我考虑的是,这里面的binder传递fd,其实可以不用传递,既然设备已注册,那么双方的进程,甚至多方进程同时打开这个设备,然后各自mmap不也可以共享嘛,当然内存共享不止linux内核的匿名内存共享,我想这是可以实现的,只要大神熟悉内核,熟悉内核内存分配与内存共享方面的知识,这个考虑应该不足为奇,麻烦的就是mmap应该怎么样设计,使用完的内存应该怎样回收?好吧,这样的考虑,留给内核牛人。

我们只谈第一段中所讲的匿名内存映射。

open在驱动层初始化了一个数据结构ashmem_area:

/*
 * ashmem_area - anonymous shared memory area
 * Lifecycle: From our parent file's open() until its release()
 * Locking: Protected by `ashmem_mutex'
 * Big Note: Mappings do NOT pin this structure; it dies on close()
 */
struct ashmem_area {
	char name[ASHMEM_FULL_NAME_LEN];/* optional name for /proc/pid/maps */
	struct list_head unpinned_list;	/* list of all ashmem areas */
	struct file *file;		/* the shmem-based backing file */
	size_t size;			/* size of the mapping, in bytes */
	unsigned long prot_mask;	/* allowed prot bits, as vm_flags */
};

 域name表示这块共享内存的名字,这个名字会显示/proc//maps文件中,表示打开这个共享内存文件的进程ID;域file表示这个共享内存在临时文件系统tmpfs中对应的文件,在内核决定要把这块共享内存对应的物理页面回收时,就会把它的内容交换到这个临时文件中去;域size表示这块共享内存的大小;域prot_mask表示这块共享内存的访问保护位;域unpinned_list是一个列表头,它把这块共享内存中所有被解锁的内存块连接在一起,其中被解锁的内存块,也有一个数据结构来描述:

/*
 * ashmem_range - represents an interval of unpinned (evictable) pages
 * Lifecycle: From unpin to pin
 * Locking: Protected by `ashmem_mutex'
 */
struct ashmem_range {
	struct list_head lru;		/* entry in LRU list */
	struct list_head unpinned;	/* entry in its area's unpinned list */
	struct ashmem_area *asma;	/* associated area */
	size_t pgstart;			/* starting page, inclusive */
	size_t pgend;			/* ending page, inclusive */
	unsigned int purged;		/* ASHMEM_NOT or ASHMEM_WAS_PURGED */
};

不讲对被解锁的内存块的管理,老罗的博客讲的很丰富。

驱动层open的设计看样子不支持进程间通信,只能调用一次,无论在哪个进程,只要确保open之后再mmap:

static int ashmem_open(struct inode *inode, struct file *file)
{
	struct ashmem_area *asma;
	int ret;
 
	ret = nonseekable_open(inode, file);
	if (unlikely(ret))
		return ret;
 
	asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
	if (unlikely(!asma))
		return -ENOMEM;
 
	INIT_LIST_HEAD(&asma->unpinned_list);
	memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);
	asma->prot_mask = PROT_MASK;
	file->private_data = asma;
 
	return 0;
}

初次mmap的进程分配并映射了匿名的共享内存:

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct ashmem_area *asma = file->private_data;
	int ret = 0;
 
	mutex_lock(&ashmem_mutex);
 
	/* user needs to SET_SIZE before mapping */
	if (unlikely(!asma->size)) {
		ret = -EINVAL;
		goto out;
	}
 
	/* requested protection bits must match our allowed protection mask */
	if (unlikely((vma->vm_flags & ~asma->prot_mask) & PROT_MASK)) {
		ret = -EPERM;
		goto out;
	}
 
	if (!asma->file) {
		char *name = ASHMEM_NAME_DEF;
		struct file *vmfile;
 
		if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')
			name = asma->name;
 
		/* ... and allocate the backing shmem file */
		vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
		if (unlikely(IS_ERR(vmfile))) {
			ret = PTR_ERR(vmfile);
			goto out;
		}
		asma->file = vmfile;
	}
	get_file(asma->file);
 
	if (vma->vm_flags & VM_SHARED)
		shmem_set_file(vma, asma->file);
	else {
		if (vma->vm_file)
			fput(vma->vm_file);
		vma->vm_file = asma->file;
	}
	vma->vm_flags |= VM_CAN_NONLINEAR;
 
out:
	mutex_unlock(&ashmem_mutex);
	return ret;
}

第二次mmap的进程只是映射了这块匿名共享内存,匿名共享内存不像android的binder驱动一样,它不是android独创,它是linux独创。为什么第二次不走下面这节代码:

if (!asma->file) {
......
	}

因为不同进程打开同一设备文件的Struct file*是同一个,初次进程在mutex_lock锁的加持下,已经把匿名共享文件赋值给了asma->file,并且asma作为了file的私有数据,当下一个打开相同设备文件的进程进来获取到的私有数据是同一个asma,自然它的file字段也就是上一个进程已经赋过值的file。

文件描述符是如何在binder驱动里传递的?

    //java层:
    Parcel out = Parcel.obtin();
    。。。。。。
    out.writeFileDescriptor(mFd);
    。。。。。。

    //c++层
    binder_transaction_data tr;
    tr.target.handle = handle;	
    tr.code = code;	
    tr.flags = binderFlags;	
    const status_t err = data.errorCheck();	
    if (err == NO_ERROR) {	
        tr.data_size = data.ipcDataSize();	
        tr.data.ptr.buffer = data.ipcData();//fd在这里面	
        tr.offsets_size = data.ipcObjectsCount()*sizeof(size_t);	
        tr.data.ptr.offsets = data.ipcObjects();//fd的偏移值在这个偏移数组里
    } 
    。。。。。。	
    //把tr传给驱动

    //驱动层
	offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
	......
	off_end = (void *)offp + tr->offsets_size;
	for (; offp < off_end; offp++) {
		struct flat_binder_object *fp;
		......
		fp = (struct flat_binder_object *)(t->buffer->data + *offp);
		switch (fp->type) {
		......
		case BINDER_TYPE_FD: {
			int target_fd;
			struct file *file;
			......
			//文件描述符的值就保存在fp->handle中,通过fget函数取回这个文件描述符所对应的打开文件结构
			file = fget(fp->handle);
			......
			//在目标进程中获得一个空闲的文件描述符
			target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
			......
			//在目标进程中,打开文件结构有了,文件描述符也有了,接下来就可以把这个文件描述符和这
			//个打开文件结构关联起来就可以了
			task_fd_install(target_proc, target_fd, file);
			......
			// 由于这个Binder对象最终是要返回给目标进程的,所以还要修改fp->handle的值,它原来表示的是在源进程中的
			//文件描述符,现在要改成目标进程的文件描述符
			fp->handle = target_fd;
		} break;
			......
	  }


    //另一方java层:
    Parcel reply = Parcel.obtin();
    。。。。。。
    mFd = reply.writeFileDescriptor();

所以文件描述符fd在在驱动层传递的时候,为另一方进程创建了一个对应file的描述符,这样另一方进程拿到的描述符就和对方进程的描述符“描述”的是同一个file结构,那么mmap的时候调用到了同一个设备文件的文件操作mmap,进而都映射到了同一块匿名共享内存。

参考:老罗博客

你可能感兴趣的:(匿名内存映射)