本文主要解析两个关键点:
- ServiceManger 如何成为所有Service的管理进程
- ServiceManager 与 Binder Driver 共享 128K 内存
我们从ServiceManager的 main函数开始(service_manager.c):
int main()
{
struct binder_state *bs;
bs = binder_open(128*1024);
...
binder_become_context_manager(bs);
...
binder_loop(bs, svcmgr_handler);
return 0;
}
从其main函数可以看到,ServiceManager进程主要通过这三个函数的执行,使得其成为 Service的管理进程,我们依次来说;
1.binder_open
我们看下binder_open函数(代码在 /frameworks/native/cmds/servicemanager/binder.c), 这里摘取了函数的主要逻辑:
struct binder_state *binder_open(size_t mapsize)
{
struct binder_state *bs;
struct binder_version vers;
bs = malloc(sizeof(*bs));
...
bs->fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
...
if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
(vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {
goto fail_open;
}
...
bs->mapsize = mapsize;
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
...
return bs;
}
其中 bs 是一个struct:
struct binder_state
{
int fd;
void *mapped;
size_t mapsize;
};
bs->fd : 保存着打开的 binder driver节点 "/dev/binder"对应的文件描述符, 待 ServiceManager 进程与 binder driver通信时,
会使用到这个fd;
bs->mmaped : 保存着 mmap 返回的用户态虚拟地址;
bs->mapsize :128 KB
函数首先调用 malloc 给 bs分配一块内存,这个没什么可说的;
接下来 open 函数会打开 binder driver对应的字符设备节点 "/dev/binder",对于用户态来说,目的就是返回了一个 fd;
而由于 binder driver 注册,而对于这个节点的 open操作,最终经过 open 系统调用后,会调用 binder driver的 binder_open函数,binder driver的 binder_open 函数 (kernel/drivers/android/binder.c):
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc; //每个要进行binder通信的进程都有一个对应的 binder_proc
...
proc = kzalloc(sizeof(*proc), GFP_KERNEL); //在kernel为调用open的进程分配binder_proc
...
get_task_struct(current);
proc->tsk = current; //设置该 binder_proc的 task指向调用 open的进程
INIT_LIST_HEAD(&proc->todo); //初始化todo list,这个list用来保存将要进行的通信工作
init_waitqueue_head(&proc->wait);//初始化 wait,即正在睡眠的 binder 线程list
proc->default_priority = task_nice(current);//进程优先级
hlist_add_head(&proc->proc_node, &binder_procs);//把 proc添加到binder_procs list中
proc->pid = current->group_leader->pid; //记录当前进程 pid
INIT_LIST_HEAD(&proc->delivered_death); //death list
filp->private_data = proc; //把 proc记录到 file的 private_data
return 0;
}
这里面的操作主要是 创建一个 binder_proc,初始化一些数据,并把 binder_proc保存到service_manager进程打开 "/dev/binder"时,
创建的 filp->private_data中。binder_proc是每个要进行 binder通信都需要的一个数据结构,与进程关系是一对一 ,其保存进程binder通信时使用到的一些数据。
下面我们看一下 binder_proc:
struct binder_proc {
// 进程打开设备文件/dev/binder时,Binder驱动会为它创建一个binder_proc结构体,并将它
// 保存在全局hash列表中,proc_node是该hash列表的节点。
struct hlist_node proc_node;
// 每个使用了Binder机制的进程都有一个Binder线程池,用来处理进程间通信请求。threads以
// 线程ID作为key来组织进程的Binder线程池。进程可以调用ioctl将线程注册到Binder驱动中
// 当没有足够的空闲线程处理进程间通信请求时,驱动可以要求进程注册更多的线程到Binder线程
// 池中
struct rb_root threads;
struct rb_root nodes; // 组织Binder实体对象,它以成员ptr作为key
struct rb_root refs_by_desc; // 组织Binder引用对象,它以成员desc作为key
struct rb_root refs_by_node; // 组织Binder引用对象,它以成员node作为key
int pid; // 指向进程组ID
struct vm_area_struct *vma; // 内核缓冲区的用户空间地址,供应用程序使用
struct mm_struct *vma_vm_mm;
struct task_struct *tsk; // 指向进程任务控制块
struct files_struct *files; // 指向进程打开文件结构体数组
// 一个hash表,保存进程可以延迟执行的工作项,这些延迟工作有三种类型
// BINDER_DEFERRED_PUT_FILES、BINDER_DEFERRED_FLUSH、BINDER_DEFERRED_RELEASE
// 驱动为进程分配内核缓冲区时,会为该缓冲区创建一个文件描述符,进程可以通过该描述符将该内
// 核缓冲区映射到自己的地址空间。当进程不再需要使用Binder机制时,就会通知驱动关闭该文件
// 描述符并释放之前所分配的内核缓冲区。由于这不是一个马上就要完成的操作,因此驱动会创建一
// 个BINDER_DEFERRED_PUT_FILES类型的工作来延迟执行;
// Binder线程池中的空闲Binder线程是睡眠在一个等待队列中的,进程可以通过调用函数flush
// 来唤醒这些线程,以便它们可以检查进程是否有新的工作项需要处理。此时驱动会创建一个
// BINDER_DEFERRED_FLUSH类型的工作项,以便延迟执行唤醒空闲Binder线程的操作;
// 当进程不再使用Binder机制时,会调用函数close关闭文件/dev/binder,此时驱动会释放之
// 前为它分配的资源,由于资源释放是个比较耗时的操作,驱动会创建一个
// BINDER_DEFERRED_RELEASE类型的事务来延迟执行
struct hlist_node deferred_work_node;
int deferred_work; // 描述该延迟工作项的具体类型
void *buffer; // 内核缓冲区的内核空间地址,供驱动程序使用
ptrdiff_t user_buffer_offset; // vma和buffer之间的差值
// buffer指向一块大的内核缓冲区,驱动程序为方便管理,将它划分成若干小块,这些小块的内核缓
// 冲区用binder_buffer描述保存在列表中,按地址从小到大排列。buffers指向该列表的头部。
struct list_head buffers;
struct rb_root free_buffers; // buffers中的小块有的正在使用,被保存在此红黑树
struct rb_root allocated_buffers; // buffers中的空闲小块被保存在此红黑树
size_t free_async_space; // 当前可用的保存异步事务数据的内核缓冲区的大小
struct page **pages; // buffer和vma都是虚拟地址,它们对应的物理页面保存在pages
// 中,这是一个数组,每个元素指向一个物理页面
size_t buffer_size; // 进程调用mmap将它映射到进程地址空间,实际上是请求驱动为它
// 分配一块内核缓冲区,缓冲区大小保存在该成员中
uint32_t buffer_free; // 空闲内核缓冲区的大小
struct list_head todo; // 当进程接收到一个进程间通信请求时,Binder驱动就将该请求封
// 装成一个工作项,并且加入到进程的待处理工作向队列中,该队列
// 使用成员变量todo来描述。
wait_queue_head_t wait; // 线程池中空闲Binder线程会睡眠在由该成员所描述的等待队列中
// 当宿主进程的待处理工作项队列增加新工作项后,驱动会唤醒这
// 些线程,以便处理新的工作项
struct binder_stats stats; // 用来统计进程数据
// 当进程所引用的Service组件死亡时,驱动会向该进程发送一个死亡通知。这个正在发出的通知被
// 封装成一个类型为BINDER_WORK_DEAD_BINDER或BINDER_WORK_DEAD_BINDER_AND_CLEAR
// 的工作项,并保存在由该成员描述的队列中删除
struct list_head delivered_death;
int max_threads; // 驱动程序最多可以主动请求进程注册的线程数
int requested_threads;
int requested_threads_started;
int ready_threads; // 进程当前的空闲Binder线程数
long default_priority; // 进程的优先级,当线程处理一个工作项时,线程优先级可能被
// 设置为宿主进程的优先级
struct dentry *debugfs_entry;
};
总结:用户进程调用open("/dev/binder"),其实做了2个工作:
- 记录一个文件描述符 fd到用户进程,以便后面与 binder driver 通信
- 内核中执行内核的 binder_open 函数,为该用户进程创建一个对应的 binder_proc,以便通信使用
接下来,ServiceManager中的 binder_open函数会依次执行 version校验 和 mmap操作,分到下面两节来说。
2.version校验
对应代码是:
if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
(vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {
goto fail_open;
}
就是从获取一下binder driver中的binder的版本,比较是否与 service manger使用 binder 版本匹配;
主要工作在 binder version的获取,是通过 ioctl来获取的。
ioctl(bs->fd, BINDER_VERSION, &vers);
刚才记录的 bs->fd就是用来做 ioctl通信的,发的命令是 BINDER_VERSION,得到的version记录在 vers中。
ioctl经过系统调用后,会调用到 binder driver的 binder_ioctl 函数,这里只摘录跟本次调用 相关的代码:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int ret;
struct binder_proc *proc = filp->private_data;//这个filp就是servicemanager进程打开 "/dev/binder"节点是对应的进程,之前把 proc记录到了其 private_data中
struct binder_thread *thread;
unsigned int size = _IOC_SIZE(cmd); // cmd, BINDER_VERSION
void __user *ubuf = (void __user *)arg;//对应上面的 vers
...
thread = binder_get_thread(proc);//从proc->threads.rb_node中找,找不到则创建一个,并记录
switch (cmd) {
...
case BINDER_VERSION: {
struct binder_version __user *ver = ubuf;
if (size != sizeof(struct binder_version)) {
ret = -EINVAL;
goto err;
}
if (put_user_preempt_disabled(BINDER_CURRENT_PROTOCOL_VERSION,
&ver->protocol_version)) {//把 binder driver version写入到 vers中,这样返回后,Servicemanger中 binder_open函数的局部变量 vers中就有数据了
ret = -EINVAL;
goto err;
}
break;
}
}
ret = 0;
if (thread)
thread->looper &= ~BINDER_LOOPER_STATE_NEED_RETURN;
return ret; //正常返回
}
完成binder dirver的vers获取,即可进行对比验证。
接下来是: bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
3.binder_mmap
service manager 的 version校验办成后,接下来就是mmap,
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
这个函数主要完成两件事情:
- 把 mmap 128KB 返回的service manager进程的用户空间地址记录到 bs->mmaped
- 在内核调用 binder driver的 binder_mmap函数,为进程在内核空间申请 128K虚拟内存,在用户空间也申请 128K虚拟内存;并先 alloc一个 PAGE的物理内存以便使用,后续需要的的时候,再真正分配物理页面
实际上,kernel空间的 128K 虚拟内存,和用户空间的 128K虚拟内存,在使用时,是会对应到相同的物理内存的。代码如下:
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
struct vm_struct *area;
struct binder_proc *proc = filp->private_data;
const char *failure_string;
struct binder_buffer *buffer;
...
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;//每个进程的binder通信内存最大不能超过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;//记录 offset
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;//buffer size
vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
ret = binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma); //分配一个 PAGE 的实际物理页面,并通过vma与用户空间虚拟地址绑定,通过 proc->pages与内核空间虚拟地址绑定起来
buffer = proc->buffer;
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
binder_insert_free_buffer(proc, buffer);
proc->fre