一、共享内存实现进程间通信的基础
以32位系统为例,其可寻址的最大内存为4GiB(2^32),这4GiB内存就是常说的虚拟内存。
Linux内核将这4GiB的虚拟内存分为两部分:底部较大的部分用于用户进程,即用户空间(user space);顶部专用于内核,即内核空间(kernel space):
如果划分比例为3 : 1,则0~3GiB用于用户空间,1GiB用于内核空间。当然,这个分割比是可以修改的。
在创建进程的时候,每个进程分配独有的3GiB虚拟内存,且彼此无法访问对方的这部分内存内容(所以需要进程间通信)。
而1GiB的内核空间却是各个进程"共享",之所以加引号是因为进程无法直接访问,必须通过系统调用陷入内核进行访问。
共享内存的通信方式就是基于此实现。
二、经典实例
共享内存这种进程间通信方式的一个典型的实现就是Android中的IPC工具:Binder。
Binder利用Linux自身的内存管理技术,确保了数据在内核空间传递过程中的可靠性;其次,由于用户空间无法访问内核空间,这就解决了数据的安全性。
Binder实现为Linux的一个驱动,注册成功后创建"/dev/binder"节点,并提供以下操作接口:
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
自然,跟主题有关的就是binder_mmap()函数,用户空间通过
mmap()系统调用陷入到
binder_mmap()函数,完成用户空间和内核空间的内存映射:
struct binder_state *binder_open(size_t mapsize)
{
struct binder_state *bs;
bs = malloc(sizeof(*bs));
bs->fd = open("/dev/binder", O_RDWR);
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
return bs;
}
binder_mmap()函数:
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct vm_struct *area;
struct binder_proc *proc = filp->private_data;
struct binder_buffer *buffer;
// reserve a contiguous kernel virtual area
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
proc->buffer = area->addr;
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
proc->buffer_size = vma->vm_end - vma->vm_start;
binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma);
return 0;
}
file的private_data成员在调用open()的时候进行赋值,保存一个struct binder_proc对象指针,这里将其取出。
private_data的初始化过程:
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
.... // 省略proc其他成员的初始化
filp->private_data = proc;
binder_unlock(__func__);
return 0;
}
在用户空间调用mmap()时指定映射的内存的大小为mapsize,vma->vm_start和vma->vm_end分别指向用户空间映射的Buffer的起始地址和结束地址。
通过get_vm_area(),在内核的虚拟映射区寻找一个连续的、(vma->vm_end - vma->vm_start)大小的区域,返回给area。
area->addr保存着这部分区域的起始地址。
执行完proc->buffer = area->addr后,proc->buffer就有了内核空间映射的Buffer(用于接收IPC数据)的起始地址。
有了这个偏移量,就把这两部分的映射区域联系在了一起。
((vma->vm_end - vma->vm_start) / PAGE_SIZE)计算需要的页数。页是kernel管理物理内存的基本单位,由struct page表示。
proc->pages用于为管理内存的pages分配内存。
binder_update_page_range()用于分配物理页,kernel通过页表这一方式实现物理地址和虚拟地址的映射,这部分执行后:
之后,不同进程间通过Binder这种共享内存的方式就可进行IPC。
参考资料:
1、Linux内核设计与实现
Linux Kernel Development, Robert Love
2、深入Linux内核架构
Professional Linux Kernel Architecture, Wolfgang Mauerer
3、Android框架揭秘
Inside the Android Framework