进程间通信(2)共享内存

http://blog.csdn.net/u013686019/article/details/78002538

一、共享内存实现进程间通信的基础

以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数据)起始地址。


proc->user_buffer_offset,保存用户空间Buffer地址和内核空间Buffer地址的偏移,由先前知识可知,偏移量为一个负值。

有了这个偏移量,就把这两部分的映射区域联系在了一起。


以上说的都是虚(拟内存)的,要干事毕竟还得来实(物理内存)的,接下来就是把上述Buffer映射到物理内存上。
((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

猜你喜欢

转载自blog.csdn.net/u013686019/article/details/78002538
今日推荐