很多BAT也不一定能懂的binder机制!
因为搞懂binder需要会c,linux内核知识。看java根本就看不懂!
我同事从小米跳槽过来,干安卓framework层10年,是小米的专家级别
然后他把binder驱动层全部和我讲解了一遍,然后我这边做个笔记分享给大家。
分6篇文字讲解:
01. Android Binder图解 小米系统专家 解析Service 的addService注册过程 (安卓12)
02. Android Binder图解 小米系统专家 解析 ServiceManager和binder通信 (安卓12)
03. Android Binder图解 小米系统专家 解析binder驱动层解析binder通信过程 (安卓12)
04. Android Binder图解 小米系统专家 从binder java层解析binder整个流程(安卓12)
05. Android Binder图解 小米系统专家 解析binder总结调用流程 (安卓12)
06. Android Binder图解 小米系统专家 解析binder面试一网打尽(安卓12)
问题:
1. binder 驱动层的几个方法
2. binder 驱动是怎么帮我们找到 ServiceManager 进程的
3. ServiceManager 进程是怎么进入等待怎么被唤醒的
4. 数据是怎么传递的,handle 和 type 是怎么被计算和管理的
相关源码文件:
/drivers/android/binder.c
/drivers/staging/android/binder.c
4个重要的方法分析:
open 是打开驱动、mmap 是映射驱动、ioctl 是操作驱动、close 是关闭驱动,
分别对应驱动层的 binder_open、binder_mmap、binder_ioctl 和 binder_colse 方法
最终通过ioctl函数和Binder驱动进行通信,最后是交给了binder 驱动的 ioctl 方法,这一部分涉及到Kernel Binder的内容
初始化 binder_init()
内核初始化时,会调用到device_initcall()进行初始化,从而启动binder_init。
binder_init()主要负责注册misc设备,通过调用misc_register()来实现。
在Android8.0之后,现在Binder驱动有三个:/dev/binder; /dev/hwbinder; /dev/vndbinder.
————————————————
device_initcall(binder_init);
static HLIST_HEAD(binder_devices); static int __init binder_init(void)
{
int ret;
char *device_name, *device_names, *device_tmp;
struct binder_device *device;
struct hlist_node *tmp; ret = binder_alloc_shrinker_init();
if (ret)
return ret; atomic_set(&binder_transaction_log.cur, ~0U);
atomic_set(&binder_transaction_log_failed.cur, ~0U); //在debugfs文件系统中创建一个目录,返回值是指向dentry的指针
//在手机对应的目录:/sys/kernel/debug/binder,里面创建了几个文件,用来记录binder操作过程中的信息和日志: //failed_transaction_log、state、stats、transaction_log、transactions binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root)
//创建目录:/sys/kernel/debug/binder/proc
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
...device_names = kzalloc(strlen(binder_devices_param) + 1, GFP_KERNEL);
if (!device_names) {
ret = -ENOMEM;
goto err_alloc_device_names_failed;
}
strcpy(device_names, binder_devices_param); device_tmp = device_names;
//Android8.0 中引入了hwbinder,vndbinder,所以现在有三个binder,分别需要创建三个binder device:
// /dev/binder、/dev/hwbinder、/dev/vndbinder //循环注册binder 的三个设备:/dev/binder、/dev/hwbinder、/dev/vndbinder while ((device_name = strsep(&device_tmp, ","))) {
ret = init_binder_device(device_name);
if (ret)
goto err_init_binder_device_failed; }return ret; err_init_binder_device_failed: hlist_for_each_entry_safe(device, tmp, &binder_devices, hlist) { misc_deregister(&device->miscdev);
hlist_del(&device->hlist);
kfree(device); }kfree(device_names); err_alloc_device_names_failed: debugfs_remove_recursive(binder_debugfs_dir_entry_root); return ret; } static int __init init_binder_device(const char *name)
{
int ret;
struct binder_device binder_device; //申请内存空间,
binder_device = kzalloc(sizeof(binder_device), GFP_KERNEL);
if (!binder_device)
return -ENOMEM; binder_device->miscdev.fops = &binder_fops;
binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;
binder_device->miscdev.name = name; binder_device->context.binder_context_mgr_uid = INVALID_UID;
binder_device->context.name = name;
mutex_init(&binder_device->context.context_mgr_node_lock); ret = misc_register(&binder_device->miscdev);
if (ret < 0) {
kfree(binder_device);
return ret;
}hlist_add_head(&binder_device->hlist, &binder_devices); return ret; }
1. biner_open 源码分析
这里我们只需要了解 open 是打开驱动、 分别对应驱动层的 binder_open、
创建 binder_proc 对象,并把当前进程等信息保存到 binder_proc 对象,该对象管理 IPC 所需的各种信息并拥有其他结构体的根结构体;再把 binder_proc 对象保存到文件指针 filp,以及把 binder_proc 加入到全局链表 binder_procs。
binder_proc 就是用来存放 binder 相关数据的结构体,每个进程独有一份。
总结:****biner_open**** 里面创建了一个****binder_proc 对象。存放4个红黑树!
红黑树结构:
特点:具有二叉树的特点;根节点是黑色的;叶子节点不存数据;任何相邻的节点都不能同时为红色;每个节点,从该节点到达其可达的叶子节点是所路径,都包含相同数目的黑色节点
优点:具有二叉树所有特点,与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,减少性能消耗;
缺点:代码复杂,查找效率比平衡二叉树低
————————————————
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
current->group_leader->pid, current->pid);
// 在内核空间开辟一块连续内存,而且大小不能超过 128K ,初始化为 0
proc = kvalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
// 获取当前线程的 task_struct
get_task_struct(current);
proc->tsk = current;
// 初始化 todo 队列列表 (工作列表)
INIT_LIST_HEAD(&proc->todo);
// 初始化等待队列
init_waitqueue_head(&proc->wait);
// 把 binder_proc 加入到 binder 驱动的列表中
hlist_add_head(&proc->proc_node, &binder_procs);
// 文件对象的私有数据 = binder_proc
filp->private_data = proc;
return 0;
}
struct binder_proc {
struct rb_root threads; // 线程处理的红黑树
struct rb_root nodes; // 内部 binder 对象的红黑树
struct rb_root refs_by_desc;// 外部对应的 binder 对象的红黑树,以 handle 做key
struct rb_root refs_by_node;// 外部对应的 binder 对象的红黑树,以 地址 做key
int pid; // 进程id
struct vm_area_struct *vma;
struct mm_struct *vma_vm_mm;
struct task_struct *tsk;
struct files_struct *files;
void *buffer;
struct page **pages;
size_t buffer_size;
uint32_t buffer_free;
struct list_head todo;
wait_queue_head_t wait;
int max_threads;
};
非常重要的数据结构:binder_proc
struct binder_proc {
struct hlist_node proc_node;
// 四棵比较重要的树
struct rb_root threads;
struct rb_root nodes;
struct rb_root refs_by_desc;
struct rb_root refs_by_node;
int pid;
struct vm_area_struct *vma; //虚拟地址空间,用户控件传过来
struct mm_struct *vma_vm_mm;
struct task_struct *tsk;
struct files_struct *files;
struct hlist_node deferred_work_node;
int deferred_work;
void *buffer; //初始地址
ptrdiff_t user_buffer_offset; //这里是偏移
struct list_head buffers;//这个列表连接所有的内存块,以地址的大小为顺序,各内存块首尾相连
struct rb_root free_buffers;//连接所有的已建立映射的虚拟内存块,以内存的大小为index组织在以该节点为根的红黑树下
struct rb_root allocated_buffers;//连接所有已经分配的虚拟内存块,以内存块的开始地址为index组织在以该节点为根的红黑树下
}
2. binder_mmap 源码分析
mmap 是映射驱动,binder_mmap、
(binder_mmap 的主要作用就是开辟一块连续的内核空间,并且开辟一个物理页的地址空间,同时映射到用户空间和内核空间。)
总结:binder_mmap,通过内存映射,使用进程虚拟地址空间和内核虚拟地址空间来映射同一个物理页面
//mmap把设备内存(内核空间)映射到用户空间,从而用户程序可以在用户空间操作内核空间的地址;
//binder_mmap能映射的最大内存空间为4M,而且不能映射具有写权限的内存区域;
//binder_mmap现在内核虚拟映射表上获取一块可以使用的区域,然后分配物理页,再把物理页映射到虚拟映射表上;
内核空间的数据映射到用户空间其实就是添加一个偏移地址,并且将数据的首地址、数据的大小都复制到一个用户空间的Parcel结构体
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
struct vm_struct *area;
// 获取 binder_proc
struct binder_proc *proc = filp->private_data;
const char *failure_string;
struct binder_buffer *buffer;
if (proc->tsk != current)
return -EINVAL;
// 普通进程 1M - 8k ,ServiceManager 进程是 128K
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
// 内核空间的开辟的首地址
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);
// 计算大小 1M - 8K
proc->buffer_size = vma->vm_end - vma->vm_start;
// 开辟映射物理页,一次拷贝的关键在这个方法
if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
}
buffer->free = 1;
proc->files = get_files_struct(current);
proc->vma = vma;
proc->vma_vm_mm = vma->vm_mm;
return ret;
}
static int binder_update_page_range(struct binder_proc *proc, int allocate,
void *start, void *end,
struct vm_area_struct *vma)
{
void *page_addr;
unsigned long user_page_addr;
struct vm_struct tmp_area;
struct page **page;
struct mm_struct *mm;
if (end <= start)
return 0;
if (vma)
mm = NULL;
else
mm = get_task_mm(proc->tsk);
// 判断是不是释放 1 ,开辟(物理内存)
if (allocate == 0)
goto free_range;
// 这里只有一个物理页
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
// 在物理内存上开辟一个页
*page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
if (*page == NULL) {
pr_err("%d: binder_alloc_buf failed for page at %p\n",
proc->pid, page_addr);
goto err_alloc_page_failed;
}
tmp_area.addr = page_addr;
tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
// 把开辟好的这一个页映射到内核空间
ret = map_vm_area(&tmp_area, PAGE_KERNEL, page);
// 根据偏移量计算用户空间的首地址
user_page_addr =
(uintptr_t)page_addr + proc->user_buffer_offset;
// 把开辟好的这一个页映射到用户空间
ret = vm_insert_page(vma, user_page_addr, page[0]);
}
return 0;
// 释放的逻辑
free_range:
for (page_addr = end - PAGE_SIZE; page_addr >= start;
page_addr -= PAGE_SIZE) {
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
if (vma)
zap_page_range(vma, (uintptr_t)page_addr +
proc->user_buffer_offset, PAGE_SIZE, NULL);
err_vm_insert_page_failed:
unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);
err_map_kernel_failed:
__free_page(*page);
*page = NULL;
err_alloc_page_failed:
;
}
err_no_vma:
if (mm) {
up_write(&mm->mmap_sem);
mmput(mm);
}
return -ENOMEM;
}
为啥只映射一个页呢?传 20K 数据怎么办?
如何避免内存的浪费?
问题:mmap是在哪个过程中调用的???
一次拷贝的原理:和linux有关,内存,页,物理地址和虚拟地址有关!
开辟一个页映射到用户空间
开辟一个页映射到内核空间
[图片上传失败...(image-294baf-1642735792543)]
[图片上传失败...(image-ef40c2-1642735792543)]
[图片上传失败...(image-51a969-1642735792543)]
[图片上传失败...(image-b2c581-1642735792543)]
一个用户进程包含:用户空间和内核空间????
问题: 为什么会同时使用进程虚拟地址空间和内核虚拟地址空间来映射同一个物理页面呢?
同一个物理页面,一方映射到进程虚拟地址空间,一方映射到内核虚拟地址空间,这样,进程和内核之间就可以减少一次内存拷贝了,提到了进程间通信效率
问题:一页是多少?
4k
问题:这里用户空间mmap (1M-8K)的空间,为什么要减去8K,而不是直接用1M?
系统定义:BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) = (1M- sysconf(_SC_PAGE_SIZE) * 2)
这里的8K,其实就是两个PAGE的SIZE, 物理内存的划分是按PAGE(页)来划分的,一般情况下,一个Page的大小为4K。
————————————————
3.binder_ioctl 源码分析 (代码异常复杂)--->发命令
binder_ioctl()函数负责在两个进程间收发IPC数据和IPC reply数据,Native C\C++ 层传入不同的cmd和数据,根据cmd的值,进行相应的处理并返回
参数:
filp:文件描述符
cmd:ioctl命令
arg:数据类型
binder 驱动并不提供常规的 read()、write() 等文件操作!
Binder主要通过ioctl命令对底层进行调用,不会直接用read和write对其进行操作
ioctl 是操作驱, binder_ioctl
ioctl 命令有 BINDER_WRITE_READ (binder 读写交互)、BINDER_SET_CONTEXT_MGR(servicemanager进程成为上下文管理者)、BINDER_SET_MAX_THREADS(设置最大线程数)、BINDER_VERSION(获取 binder 版本)。有两个核心复杂方法 binder_thread_write 和 binder_thread_read
binder 驱动作为接收方 binder_ioctl() 方法接收的命令,还有一些与之对应的 BR_ 开头的命令,由 binder 驱动主动发出,比如 BR_TRANSACTION、BR_REPLY,在一次 IPC 调用中是这样应用的
记住流程图
[图片上传失败...(image-44110d-1642735792543)]
[图片上传失败...(image-1d7486-1642735792543)]
Binder协议中BC与BR的区别
BC与BR主要是标志数据及Transaction流向,其中BC是从用户空间流向内核,而BR是从内核流线用户空间,比如Client向Server发送请求的时候,用的是BC_TRANSACTION,当数据被写入到目标进程后,target_proc所在的进程被唤醒,在内核空间中,会将BC转换为BR,并将数据与操作传递该用户空间。
4).close 是关闭驱动, 和 binder_colse 方法就可以了
问题 2.binder驱动是怎么帮我们找到servermanager进程的
handle = 0 , 驱动层会创建一个静态的变量 binder_context_mgr_node
里面有4给红黑树,存放了很多信息!
问题3.servermanager进程是怎进入等待,怎么被唤醒的?
我自己的理解:开始servermanager在等待,然后因为客户端往binder写入了数据,然后binder驱动往serverManager发送了
东西,之后就把它唤醒了!
等待是在 wait 队列上等,条件是 todo 队列是否为空,
客户端找到 tagert_prco 了之后把数据拷贝到目标进程,什么时候去找?这个代码在哪里?
找到了tagert_prco之后就能找到等待队列!****
同时把数据拷贝到目标进程往目标进程写入处理的命令,
然后往自己进程写入一个接受请求的命令(让自己进入等待),
最后唤醒服务进程的wait队列,这个时候服务进程被唤醒就会继续往下处理todo队列请求。
问题4.数据是如何传递的,handle和type是怎么被计算和管理的
binder 驱动会判断 type 是什么,然后呢会往自己的进程挂上一个 binder_node ,
会往目标进程挂上两个 binder_node 一个挂在以 handle 值为 key 的红黑树上,一个挂在以 cookie 为 key 的红黑树上,handle 值是根据红黑树的数据了累加的。
主要在: 驱动层有两个核心复杂方法 binder_thread_write 和 binder_thread_read
// ServiceManager 进程:获取判断 binder 驱动版本号是否一致
if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) || (vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {
goto fail_open;
}
// ServiceManager 进程:让 ServiceManager 进程成为管理者
int binder_become_context_manager(struct binder_state *bs)
{
return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}
// ServiceManager 进程:binder 线程进入循环等待
readbuf[0] = BC_ENTER_LOOPER;
binder_write(bs, readbuf, sizeof(uint32_t));
// ServiceManager 进程:进入循环不断的读写 binder 内容
for (;;) {
bwr.read_size = sizeof(readbuf);
bwr.read_consumed = 0;
bwr.read_buffer = (uintptr_t) readbuf;
res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
...
}
// media 进程:添加 MediaPlayerService 服务
do {
// 通过 ioctl 不停的读写操作,跟 Binder Driver 进行通信,转发给 ServiceManager 进程
if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
err = NO_ERROR;
...
} while (err == -EINTR); //当被中断,则继续执行
**********整体的一个过程:**
1. 获取 Binder 驱动版本
2. 成为 Binder 驱动管理者
3. ServiceManager 进程进入循环等待
4. Binder 驱动添加系统服务
5. ServiceManager 进程处理添加请求
[图片上传失败...(image-acea45-1642735792536)]
ServiceManager需要查询吗?
- ServiceManager是比较特殊的服务,所有应用都能直接使用,因为ServiceManager对于Client端来说Handle句柄是固定的,都是0,所以ServiceManager服务并不需要查询,可以直接使用。
2. app 客户请求服务过程分析
[图片上传失败...(image-1af274-1642735792535)]
服务端查询返回结果分析
[图片上传失败...(image-92d60e-1642735792536)]
3.1. 驱动层的客户端数据是怎么被回复的
[图片上传失败...(image-b58b2a-1642735792536)]
服务端之所以能回复客户端是因为 之前在请求的时候就已经记录 from 从哪里来,也就是已经记录了客户端的 binder_proc 对象,这里 getService 返回给客户端的是一个 flat_binder_object ,
[图片上传失败...(image-55460d-1642735792536)]
3.2. 客户端是怎么进入等待的
客户端是因为在请求服务端的时候驱动层会写入一个 BINDER_WORK_TRANSACTION_COMPLETE ,
然后会清空 writeData , 从新进入驱动层,进入读方法,因为todo队列里面没东西进入等待,等待服务端执行完毕唤醒客户端
4. 客户端等待响应过程分析
内核分析binder:
https://cloud.tencent.com/developer/article/1689220(一系列的文章 )