Binder驱动分析
由于时间和精力有限,驱动的分析可能会存在不准确或者不全的地方。。。
准备从 Binder 内存管理、多线程和引用计数三个方面介绍驱动。
进而回答之前留下的问题。
数据结构介绍
我们即将见到Binder
驱动的第一个数据结构!!!!!!!有请,,,,
struct binder_alloc
每个进程对应一个binder_alloc
,通过它,我们就能管理Binder进程的内存了。。。
定义
struct binder_alloc {
struct mutex mutex;
struct vm_area_struct *vma;
struct mm_struct *vma_vm_mm;
void *buffer;
ptrdiff_t user_buffer_offset;
struct list_head buffers;
struct rb_root free_buffers;
struct rb_root allocated_buffers;
size_t free_async_space;
struct binder_lru_page *pages;
size_t buffer_size;
uint32_t buffer_free;
int pid;
size_t pages_high;
};
-
vma
表示
mmap
映射的那一段用户空间的虚拟地址空间。 -
buffer
mmap
的系统调用实现会为进程分配一段虚拟地址(vma
),然后会调用到binder_mmap
,由binder完成内存映射工作。binder的做法就是将创建一段和vma
大小一样的内核虚拟地址和vma
关联。buffer
记录的就是内核虚拟地址的起始地址。//为内核分配虚拟地址空间 area = get_vm_area(vma->vm_end - vma->vm_start, VM_ALLOC); alloc->buffer = area->addr; //保存起始地址 alloc->buffer_size = vma->vm_end - vma->vm_start; // 我们映射的大小
-
user_buffer_offset
vma
和vma_vm_mm
的固定偏移。// 负数?? alloc->user_buffer_offset = vma->vm_start - (uintptr_t)alloc->buffer;
-
pages
mmap
时,先为映射的虚拟地址段分配好页面。alloc->pages = kzalloc(sizeof(alloc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE),GFP_KERNEL);
虽然建立了虚拟地址的映射,但是还是没有看到为我们mmap
的虚拟地址分配物理内存??别急,再来认识第二个数据结构,他能帮助我们更好的理解binder_alloc
。她就是struct binder_buffer
。
struct binder_buffer
后面,我们传输数据时,一次传输的数据就对应一个binder_buffer
。
定义如下:
struct binder_buffer {
struct list_head entry; /* free and allocated entries by address */
struct rb_node rb_node; /* free entry by size or allocated entry */
/* by address */
unsigned free:1;
unsigned allow_user_free:1;
unsigned async_transaction:1;
unsigned debug_id:29;
struct binder_transaction *transaction;
struct binder_node *target_node;
size_t data_size;
size_t offsets_size;
size_t extra_buffers_size;
void *data;
};
-
entry
和rb_node
回到
binder_alloc
://`binder_alloc` struct list_head buffers; // 当前进程所有的 binder_buffer 链表 struct rb_root free_buffers; // 空闲状态的 binder_buffer 红黑树 struct rb_root allocated_buffers; //正在使用的 binder_buffer 红黑树
binder_alloc::buffers
就是是一个链表,当前进程所有的binder_buffer
都会挂载到这个链表(通过entry
)。此外,
binder_buffer
一旦创建,就不会被轻易回收内存,所以,他有两种状态,被使用和空闲。根据其状态,binder驱动将其挂载到binder_alloc
的两棵红黑树中(通过rb_node
)。 -
其余的先不说,说多了很懵逼。
我们再回到binder_mmap
中,前面Binder驱动已经完成了 内核和用户空间虚拟地址的映射,并且创建了一堆page。接下来,就是先创建一个binder_buffer
,然后将其添加到binde_alloc::buffers
和binder_alloc::free_buffers
中。
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma) {
...
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
buffer->data = alloc->buffer;
//添加到链表中
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1; //空闲buffer
//添加到空闲buffer红黑树中
binder_insert_free_buffer(alloc, buffer);
alloc->free_async_space = alloc->buffer_size / 2;
...
}
到这里,mmap
就结束了,除了为这些数据结构分配内存,没有看到为mmap
的虚拟地址分配物理内存的操作。
内存管理
32bit系统
这一节我们会知道,Binder一次传输的数据最大是多少!Binder的一次内存拷贝发生在什么时候。
mmap
我们知道,进程在使用binder时需要用到ProcessState
类,其对象在构造的时候就会打开binder确定并执行mmap
。
//#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
if (mVMStart == MAP_FAILED) {
close(mDriverFD);
mDriverFD = -1;
}
-
PROT_READ
,只读映射。简单的理解,就是
mmap
出来的虚拟地址空间是作为 数据接收缓冲区用的 -
mmap
映射的虚拟地址空间大小是1M - 8K
。
对于由Zygote
孵化的进程,都默认创建了ProcessState
对象,所以其传输的最大数据就是1M-8K
,当然,这只是理论值。
为了区分要发送的数据,还有一些辅助数据结构,这些也是要占用大小的。
此外,还要内存碎片问题。。。。
那是不是我们自己修改ProcessState
中mmap
的映射大小就能够传输大数据呢?
是滴,但是驱动对此也做了限制,最大为4M。
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
...
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
...
}
对于大数据的传输,我们可以使用
Ashmem
,然后使用Binder传输 文件描述符实现。
对于mmap
具体做了哪些工作,我们用下面的一张图来总结。
-
在内核中申请一段连续的 虚拟地址空间。将其起始地址保存到
binder_alloc->buffer
,将地址空间大小保存到binder_alloc->pages
。 -
将用户空间的虚拟地址和内核虚拟地址之间的偏移保存到
binder_alloc->use_buffer_offset
。这个偏移是固定的,通过这个偏移,binder驱动能轻易的根据内核虚拟地址计算出 用户空间虚拟地址。反之亦然。
-
将内核虚拟地址分段,并将页面信息保存到
binder_alloc->pages
中。这里说明一下,虽然名为
pages
,但是实际上就是binder驱动 前面拿到的虚拟地址分成了若干段,然后每一段的大小都是一个page的大小。一开始
SEG
是不会关联page的,用到的时候才会关联page。 -
创建一个空的
binder_buffer
,并将其加入到binder_alloc->free_buffers
和binder_alloc->buffers
中。这个在图中没有体现出来,binder驱动的内存管理更多的就是对
binder_buffer
的管理。后面会详细介绍。
binder_buffer
该结构体用于缓存每次 transaction 时的数据,也就是我们通过IPCThreadState::transact
发送的数据(用结构体struct binder_transaction_data
表示)。
创建binder_buffer
通过函数binder_alloc_new_buf
能够获取到一个满足我们需求的binder_buffer
。函数原型如下:
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);
data_size
: 同binder_transaction_data.data_size
,对应的是我们实际发送数据(用户空间的有效数据)的大小。
offsets_size
: 同binder_transaction_data.offsets_size
,对应的是实际发送数据中binder对象的偏移数组的大小。
extra_buffers_size
: 0
is_aync
: 本次申请的binder_buffer
是否用于异步传输。
基本流程:
-
计算需要的buffer大小。就是
data_size
和data_offset_size
四字节对齐后之和。data_offsets_size = ALIGN(data_size, sizeof(void *)) + ALIGN(offsets_size, sizeof(void *)); if (data_offsets_size < data_size || data_offsets_size < offsets_size) { return ERR_PTR(-EINVAL); } //extra_buffers_size == 0 size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *)); if (size < data_offsets_size || size < extra_buffers_size) { return ERR_PTR(-EINVAL); } if (is_async && alloc->free_async_space < size + sizeof(struct binder_buffer)) { return ERR_PTR(-ENOSPC); } /* Pad 0-size buffers so they get assigned unique addresses */ size = max(size, sizeof(void *));
-
在
binder_alloc->free_buffers
中找一个合适的buffer。mmap
中,我们创建了一个空的binder_buffer
,将其加入到了free_buffers
中。代码如下:buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); buffer->data = alloc->buffer; list_add(&buffer->entry, &alloc->buffers); buffer->free = 1; binder_insert_free_buffer(alloc, buffer); alloc->free_async_space = alloc->buffer_size / 2;
补充一下,在这里可以看到,用于异步传输的缓冲区最大值是我们mmap的size的一半。
此时的内存关系大概如下:
mmap
时,创建的第一个binder_buffer
,其data
指向binder_alloc->buffer
,也就是当前进程映射的内核虚拟地址空间的首地址。binder驱动计算
binder_buffer
大小并不会根据其传输的实际数据来计算的,而是根据前后两个binder_buffer
的距离来计算的。static size_t binder_alloc_buffer_size(struct binder_alloc *alloc, struct binder_buffer *buffer) { //如果是buffer_alloc->buffers中的最后一个buffer,其大小就是 最后一个buffer起始地址到 虚拟地址空间末尾。 if (list_is_last(&buffer->entry, &alloc->buffers)) return (u8 *)alloc->buffer + alloc->buffer_size - (u8 *)buffer->data; // 后一个buffer的起始地址减去当前buffer的起始地址。 return (u8 *)binder_buffer_next(buffer)->data - (u8 *)buffer->data; }
我们假设,我们是第一次创建
binder_buffer
。我们从binder_alloc->free_buffers
中拿到的free_buffer
就是mmap
中创建的。其大小就是binder_alloc->buffer_size
(通常就是1M - 8K
)。很明显,这个free_buffer
太大了。 -
分配物理内存。
分配物理内存的基本单位是page。 这里不是按照
binder_buffer
的大小来分配,而是按照实际传输的数据大小来分配。分配工作由
binder_update_page_range
完成。// free_buffer的 最后一个 page的首地址 has_page_addr = (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK); //我们实际需要的 数据 对应的 page的首地址 end_page_addr = (void *)PAGE_ALIGN((uintptr_t)buffer->data + size); // binder_buffer size > actual size if (end_page_addr > has_page_addr) end_page_addr = has_page_addr; ret = binder_update_page_range(alloc, 1, (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr);
binder_update_page_range
的主要工作就是通过alloc_page
申请page
,然后建立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) { //直接复用之前申请过的 page on_lru = list_lru_del(&binder_alloc_lru, &page->lru); continue; } //申请 page page->page_ptr = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO); page->alloc = alloc; INIT_LIST_HEAD(&page->lru); // 建立内核虚拟地址和page的映射 ret = map_kernel_range_noflush((unsigned long)page_addr, PAGE_SIZE, PAGE_KERNEL, &page->page_ptr); flush_cache_vmap((unsigned long)page_addr, (unsigned long)page_addr + PAGE_SIZE); // 建立 用户空间和page的映射 user_page_addr = (uintptr_t)page_addr + alloc->user_buffer_offset; ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr); if (index + 1 > alloc->pages_high) alloc->pages_high = index + 1; }
-
裁剪一个新的
free binder_buffer
既然前面的
free_buffer
过大,我们就可以将其裁剪为两个,一个够用,另一个就是剩下的全部,然后将其加入到binder_alloc->free_buffer
.// buffer_size 就是前面提到的`1M - 8K` // size 就是我们需要的大小。 if (buffer_size != size) { struct binder_buffer *new_buffer; new_buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); new_buffer->data = (u8 *)buffer->data + size; //将剩余空间都给这个新的free buffer list_add(&new_buffer->entry, &buffer->entry); new_buffer->free = 1; binder_insert_free_buffer(alloc, new_buffer); }
-
将申请到的
binder_buffer
加入到binder_alloc->allocated
中。通过函数
binder_insert_allocated_buffer_locked
完成。
归还binder_buffer
当binder_buffer
完成了传输任务后,就需要将其归还,以便其他传输任务使用。归还操作对应的函数是binder_free_buf_locked
。
归还操作如下:
-
归还 申请的
page
, 也是通过binder_update_page_range
完成。归还操作代码大致如下:for (page_addr = end - PAGE_SIZE; page_addr >= start; page_addr -= PAGE_SIZE) { bool ret; size_t index; index = (page_addr - alloc->buffer) / PAGE_SIZE; page = &alloc->pages[index]; ret = list_lru_add(&binder_alloc_lru, &page->lru); }
如上,将
binder_buffer
占用的page
页添加到binder_alloc_lru
中,值得注意的时,归还page
并不是将其归还给操作系统,而是归还给binder_alloc
,其还是占据实际的物理内存的,这样做的目的是避免频繁的alloc_page
和free_page
的调用。再来看一下调用代码:
binder_update_page_range(alloc, 0, (void *)PAGE_ALIGN((uintptr_t)buffer->data), (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK));
PAGE_ALIGN
:如果(uintptr_t)buffer->data
的地址不是page
的其实地址,其返回值就是下一个page的其实地址。(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK)
,实际上就是向下取整。用图片来解释一下。由于
PAGE1
和PAGE3
同时被其他的binder_buffer
所引用,我们能回收的只有PAGE2
。图片画的可能有点歧义,橙色部分应该 binder_buffer中数据占用占用的内存对应的page。
-
判断是否需要合并
binder_buffer
。如上图所示,
curr
表示我们当前需要归还的binder_buffer
,它的下一个指向next
,也是free
状态的,所以要将这两个binder_buffer
合并。同时,它的上一个
prev
也是free
状态,所以也要合并,最终这三个binder_buffer
会合并成一个。// 后面还有一个空闲 binder_bufer, 删除它 if (!list_is_last(&buffer->entry, &alloc->buffers)) { struct binder_buffer *next = binder_buffer_next(buffer); if (next->free) { rb_erase(&next->rb_node, &alloc->free_buffers); // binder_delete_free_buffer(alloc, next); } } // 前面还有一个空闲 binder_buffer, 删除当前buffer if (alloc->buffers.next != &buffer->entry) { struct binder_buffer *prev = binder_buffer_prev(buffer); if (prev->free) { binder_delete_free_buffer(alloc, buffer); rb_erase(&prev->rb_node, &alloc->free_buffers); buffer = prev; } }
合并操作的主要工作由
binder_delete_free_buffer
完成。按理说只要释放掉binder_buffer
的内存就可以了,反正page
前面已经被binder_alloc
处理了。但有一种特殊情况。就是两个binder_buffer
处于同一个page
。static void binder_delete_free_buffer(struct binder_alloc *alloc, struct binder_buffer *buffer) { struct binder_buffer *prev, *next = NULL; bool to_free = true; BUG_ON(alloc->buffers.next == &buffer->entry); prev = binder_buffer_prev(buffer); BUG_ON(!prev->free); // buffer->prev 的尾部 和 buffer的首部 在同一个 page中。 if (prev_buffer_end_page(prev) == buffer_start_page(buffer)) { // 那就不能回收当前page to_free = false; } if (!list_is_last(&buffer->entry, &alloc->buffers)) { next = binder_buffer_next(buffer); // buffer->next 的首部和 buffer 的尾部 在同一个page中 if (buffer_start_page(next) == buffer_start_page(buffer)) { // 那也能回收当前page to_free = false; } } // buffer的首刚好是page 的首地址 if (PAGE_ALIGNED(buffer->data)) { // 也不能回收当前page to_free = false; } if (to_free) { // 为什么只回收一个page呢? binder_update_page_range(alloc, 0, buffer_start_page(buffer), buffer_start_page(buffer) + PAGE_SIZE); } list_del(&buffer->entry); //释放 binder_buffer占用的内存 kfree(buffer); }
为什么
PAGE_ALIGNED(buffer->data)
成立是,不用回收当前的page?为什么后面只回收只用回收一个PAGE
呢?回到我们刚刚的图片。我们回收中间的B(
binder_buffer
)后,PAGE2
已经被回收(添加到binder_alloc_lru
中)了(此时B已经是空闲binder_buffer
)。由于A
和C
的存在,我们没有合并操作。现在我们要回收A
。首先执行的是下面的代码://binder_free_buf_locked binder_update_page_range(alloc, 0, (void *)PAGE_ALIGN((uintptr_t)buffer->data), (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK));
在回收
binder_buffer A
时,以这段代码而言,PAGE_ALIGN((uintptr_t)buffer->data)
的值是PAGE2
的起始地址,(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK)
的值是PAGE1
的起始地址。所以这段代码不会执行回收操作。然后执行的是
binder_delete_free_buffer
。由于A
后面的B
是空闲状态,所以要合并A
和B
。合并后的状态如下:在回到
binder_delete_free_buffer
中,按照该函数的流程走下来,我们只要回收PAGE1
就行。这也是为什么binder_delete_free_buffer
中的binder_update_page_range
只会回收一个页面。同理,如果
A
刚好是在PAGE1
的首地址,那么在进入binder_free_buf_locked
的第一个binder_update_page_range
时,PAGE1
就会被回收。所以binder_delete_free_buffer
也就不需要回收PAGE
。最后一步两个
binder_buffer
合并后,多出一个binder_buffer
,binder驱动要会后这个数据结构占用的内存。
总结
关于Binder驱动的内存管理就介绍到这里,回顾一下重点。
-
一次内存拷贝的原理。
mmap
在用户空间和内核空间 各分配了一个连续的虚拟地址空间。当我们创建binder_buffer
时,驱动会将这两个虚拟地址(大小就是传输数据的实际大小)映射到相同的物理内存上。就避免了 一次从内核往用户空间拷贝的操作。 -
binder一次传输最大的size是多少
-
zygote孵化的进程理论上最大是
1M-8K
(有ProcessState
限制),对于异步传输是1M-8K
的一半 -
对于一般的native进程,最大是
4M
,由Binder驱动限制。 -
由前面的分析可以知道,当前能够传输的数据最大size取决于
binder_alloc->free_buffers
中最大的空闲buffer的size
.
-
数据传输原理
之前已经介绍过,数据从进程A到进程B的 native 层的实现,这一章节主要就是介绍Binder
驱动是如何传输数据的。
不同于TCP的流式传输,Binder
更像UDP的报文传输,binder驱动需要明确知道传输数据的size(从前面的内存管理一节也可以看出这一点)。
首先看一下数据传输的基本调用流程(BC_TRANSACTION
):
ioctl(fd, BINDER_WRITE_READ, &bwr);
---------进入binder驱动----------
binder_ioctl
binder_ioctl_write_read
binder_thread_write
binder_transaction //(分析重点)
binder_thread_read //(分析重点)
前面我们虽然提到了一次内存拷贝的本质,但是这个一次内存拷贝发生在什么时候???? 还有 传输 binder 对象和文件描述符的时候,binder驱动做了什么事情??
大多数情况下,binder数据传输都是类似于C/S
模式,有客户端发起请求,服务端响应客户端的请求。接下来我们也以着重分析请求和响应两个过程。
数据从Client到Service
当我们通过mRemote()->transaction
发送数据时,最终执行的是ioctl(fd, BINDER_WRITE_READ, &bwr)
。该调用会导致当前进程进入内核态。最终调用到binder驱动的binder_ioctl
函数。