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/<pid>/maps文件中,<pid>表示打开这个共享内存文件的进程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,进而都映射到了同一块匿名共享内存。
参考:老罗博客