一、前言
众所周知,Binder之所以高效,是因为它只发生了一次内存拷贝,那么它的"一次拷贝"到底是怎么实现的呢?
我们在之前在分析binder_transaction的时候,提到了一个方法,这个方法是一次拷贝的核心,我们在这里来仔细分析一下
二、源码分析
2.1 入口-binder_transaction
Kernel\drivers\android\binder.c
c++
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply,
binder_size_t extra_buffers_size)
{
//申请内存
t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
tr->offsets_size, extra_buffers_size,
!reply && (t->flags & TF_ONE_WAY), current->tgid);
//拷贝数据
if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer, 0,
(const void __user *)
(uintptr_t)tr->data.ptr.buffer,
tr->data_size)) {
//error
}
}
在这个方法中,有两个函数对拷贝数据非常重要,一个是binder_alloc_new_buf
,另一个是binder_alloc_copy_user_to_buffer
,binder_alloc_new_buf
是用来申请内存用的,而binder_alloc_copy_user_to_buffer
是执行具体的数据拷贝。
可以看到这里两个方法传递的参数都是target_proc
的,代表我们此时在操纵接收端的内存。
我们分开来看一下,先看下binder_alloc_new_buf
:
2.2 buffer申请-binder_alloc_new_buf
c++
struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size,
int is_async,
int pid)
{
struct binder_buffer *buffer;
mutex_lock(&alloc->mutex);
buffer = binder_alloc_new_buf_locked(alloc, data_size, offsets_size,
extra_buffers_size, is_async, pid);
mutex_unlock(&alloc->mutex);
return buffer;
}
直接调用了binder_alloc_new_buf_locked
方法:
c++
static struct binder_buffer *binder_alloc_new_buf_locked(
struct binder_alloc *alloc,
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size,
int is_async,
int pid)
{
struct rb_node *n = alloc->free_buffers.rb_node;
struct binder_buffer *buffer;
size_t buffer_size;
struct rb_node *best_fit = NULL;
void __user *has_page_addr;
void __user *end_page_addr;
size_t size, data_offsets_size;
int ret;
//数据对齐
data_offsets_size = ALIGN(data_size, sizeof(void *)) +
ALIGN(offsets_size, sizeof(void *));
size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *));
/* Pad 0-size buffers so they get assigned unique addresses */
size = max(size, sizeof(void *));
//寻找有没有空闲的buffer
while (n) {
buffer = rb_entry(n, struct binder_buffer, rb_node);
buffer_size = binder_alloc_buffer_size(alloc, buffer);
if (size < buffer_size) {
best_fit = n;
n = n->rb_left;
} else if (size > buffer_size)
n = n->rb_right;
else {
best_fit = n;
break;
}
}
//包含该数据的页面
has_page_addr = (void __user *)
(((uintptr_t)buffer->user_data + buffer_size) & PAGE_MASK);
//user_data结尾对应的页面
end_page_addr =
(void __user *)PAGE_ALIGN((uintptr_t)buffer->user_data + size);
if (end_page_addr > has_page_addr)
end_page_addr = has_page_addr;
ret = binder_update_page_range(alloc, 1, (void __user *)
PAGE_ALIGN((uintptr_t)buffer->user_data), end_page_addr);
buffer->free = 0;
buffer->allow_user_free = 0;
//插入allocated_buffers红黑树中
binder_insert_allocated_buffer_locked(alloc, buffer);
buffer->data_size = data_size;
buffer->offsets_size = offsets_size;
return buffer;
}
这个方法还是很长的,总体来说,干了这么几件事:
-
1、数据对齐
-
2、寻找
binder_alloc
中有没有空闲的节点,如果有并且足够大,那么就可以用这个 -
3、计算我们内存对应的页面
-
4、执行
binder_update_page_range
方法 -
5、将使用的Buffer插入到
allocated_buffers
中
中间的这个binder_update_page_range
方法,看起来比较可疑,我们跳进去看下:
2.2.1 binder_update_page_range
c++
static int binder_update_page_range(struct binder_alloc *alloc, int allocate,
void __user *start, void __user *end)
{
void __user *page_addr;
unsigned long user_page_addr;
struct binder_lru_page *page;
struct vm_area_struct *vma = NULL;
struct mm_struct *mm = NULL;
bool need_mm = false;
//是否需要申请页面,我们这里肯定是需要的,所以need_mm为true
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
page = &alloc->pages[(page_addr - alloc->buffer) / PAGE_SIZE];
if (!page->page_ptr) {
need_mm = true;
break;
}
}
if (mm) {
mmap_read_lock(mm);
vma = alloc->vma;
}
//判断物理Page是否存在
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
bool on_lru;
size_t index;
index = (page_addr - alloc->buffer) / PAGE_SIZE;
page = &alloc->pages[index];
if (page->page_ptr) {
//页面存在
continue;
}
//申请物理Page
page->page_ptr = alloc_page(GFP_KERNEL |
__GFP_HIGHMEM |
__GFP_ZERO);
page->alloc = alloc;
INIT_LIST_HEAD(&page->lru);
user_page_addr = (uintptr_t)page_addr;
//将page插入到vma中
ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr);
}
if (mm) {
mmap_read_unlock(mm);
mmput(mm);
}
return 0;
}
哦,原来binder_update_page_range
是用来申请物理页面的,按需申请Page
并将其插入vma
中,且被binder_alloc
所持有,那么binder_alloc
是何许人也?
2.2.2 binder_alloc
是什么?
图2.1 - binder_alloc基础结构
通过上图可以看到,binder_alloc
持有vma
、binder_buffer
的空闲列表以及在使用中的列表,还有物理页面,它持有了申请内存相关的数据。
2.2.3 binder_buffer
是什么?
图2.2 - binder_buffer基础结构
binder_buffer
持有当次通信所需的数据地址以及数据大小,并且被binder_alloc
持有。
2.2.4 小结
到这里binder_alloc_new_buf
就分析的差不多了,最重要的是申请binder_buffer
,以及申请物理Page。
接下来我们看binder_alloc_copy_user_to_buffer
:
2.3 数据拷贝 - binder_alloc_copy_user_to_buffer
c++
unsigned long
binder_alloc_copy_user_to_buffer(struct binder_alloc *alloc,
struct binder_buffer *buffer,
binder_size_t buffer_offset,
const void __user *from,
size_t bytes)
{
//检查Buffer
if (!check_buffer(alloc, buffer, buffer_offset, bytes))
return bytes;
while (bytes) {
unsigned long size;
unsigned long ret;
struct page *page;
pgoff_t pgoff;
void *kptr;
//从binder_alloc中获得Page
page = binder_alloc_get_page(alloc, buffer,
buffer_offset, &pgoff);
size = min_t(size_t, bytes, PAGE_SIZE - pgoff);
//将该Page映射到Kernel地址空间
kptr = kmap(page) + pgoff;
//从用户空间拷贝数据到kptr
ret = copy_from_user(kptr, from, size);
//释放page
kunmap(page);
if (ret)
return bytes - size + ret;
bytes -= size;
from += size;
buffer_offset += size;
}
return 0;
}
这个方法可以说是重中之重,
-
1、首先是拿到我们之前申请到的物理页面
-
2、将该
Page
映射到Kernel的内存地址空间之中,得到地址kptr
,这样kptr
这个地址也就指向了我们申请到的Page
中,也就是说,kptr
和用户空间实际上指向同一个物理页面,如果修改kptr
的数据,也就是在修改用户空间的数据 -
3、执行数据拷贝
-
4、释放页面
我们回顾一下,刚才说kptr
和用户空间指向的同一个物理页面,而且只执行了一次从写入端用户空间到内核态的数据拷贝,就完成了数据的传递,这就是"一次拷贝"的真正实现。
三、总结
讲到这里其实就比较清楚了,"一次拷贝"的核心正是Binder驱动与接收进程同时映射到同一个物理页面,发送进程在发生调用时,只需要将其拷贝给内核层的地址中,就完成了数据的传递。
图3.1 - 数据传递示意图
四、疑问
既然Binder驱动和Server进程可以通过内存映射的形式映射到同一个页面,那么我们能不能把Client端的也给映射过去呢?这样甚至都不需要拷贝。
其实这种做法就跟共享内存一样了,存在一些问题:
-
1、存在一定的安全问题,多个客户端都可以访问该内容
-
2、数据同步很难处理,同时存在两个甚至多个进程对这段内存作出读写,很容易就出现数据同步问题
-
3、内存不好管理
所以IPC要想做到快速且安全,至少是需要发生一次拷贝的。