人类社会进步的根源力量是那些头脑卓越的天才,大至推动社会革新的发明创造,微至影响某一行业发展方向的技术创造,比如Android操作系统,小部分天才的发明创造(android系统的核心设计者),才有无数为之修边幅的工作机会,此篇不敢妄谈android太多内容,只将本人对android系统设计中一个巧妙无比的binder机制的浅薄认知做一个分析:
涉及的要义:
A)Binder IPC
B)Binder通信模型
C)Android为何引入Binder
A)Binder IPC
Binder机制符合Client-Server通信机制,总体来说系统中一个进程将android上层能提供的service综合管理起来,比如(电源管理服务,多媒体编解码,输入子系统服务
等);其他的用户进程作为客户端向该进程提出要求服务申请。通过该过程我们分析,客户端如何获取所需服务的入口呢?这是第一步需求;其二,Client与Server间如何进行
通信和数据交互,好比李四去人事局找局长办事,这就相当于客户端的需求,而局长相当于服务端,门岗则相当于binder守护进程,李四需通过门岗获取局长办公室的具体位
置,而该位置是局长办公室挂牌的第一天就在门岗做了注册记录的,此举相当于android系统的service向binder进程注册操作,李四从门岗那里拿到的并不是局长办公室,而是
办公室的位置信息说明,姑且称之为局长办公室物理位置的引用,李四最终通过该“引用”就可以找到局长办公室(binder实体),而后就可以跟局长交流所需服务,这当中肯定
涉及双方语种一致无交流障碍,即binder通信协议。
面向对象思想的引入将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。
B)Binder通信模型
Binder框架有四个组成部分:Client,Server,binder驱动,以及ServiceManager;其中binder驱动运行于内核空间,其余三者运行在用户空间。套用互联网传输的例子,Server为服务器,Client为终端设备,ServiceManager为DNS(域名服务器),驱动为硬件通道功能的路由器。
B.1) Binder驱动
所谓的驱动自然是底层,少有体现在上层,而此驱动也有别于具体的硬件设备(I2c设备等)驱动,跟具体硬件设备毫无关联,只是套用了硬件设备的驱动模型而已,我们看kenel中的binder.c文件,可知binder注册为misc节点框架下的设备(/dev/binder),并向用户空间提供open(), mmap(), ioctl(), poll()等调用入口,用户可通过其注册生成的设备节点从用户空间实现调用,驱动负责进程之间Binder通信的建立,进程间Binder的传递,Binder的引用计数管理,数据包在进程间的传递和交互等系列支持。
B.2) Binder实体与ServiceManager
ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder 名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外都有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给 SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及 SMgr对实体的引用,将名字及新建的引用传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。
ServiceManager的代码部分:frameworks/native/cmds/servicemanager
int main(int argc, char **argv)
{
struct binder_state *bs;
void *svcmgr = BINDER_SERVICE_MANAGER;
bs = binder_open(128*1024);
if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
klog_init();
svcmgr_handle = svcmgr;
binder_loop(bs, svcmgr_handler);
return 0;
}
binder_open定义在frameworks/native/cmds/servicemanager/binder.c中,
struct binder_state *binder_open(unsigned mapsize)
{
struct binder_state *bs;
bs = malloc(sizeof(*bs));
if (!bs) {
errno = ENOMEM;
return 0;
}
bs->fd = open("/dev/binder", O_RDWR);
if (bs->fd < 0) {
fprintf(stderr,"binder: cannot open device (%s)\n",
strerror(errno));
goto fail_open;
}
bs->mapsize = mapsize;
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
if (bs->mapped == MAP_FAILED) {
fprintf(stderr,"binder: cannot map device (%s)\n",
strerror(errno));
goto fail_map;
}
/* TODO: check version */
return bs;
fail_map:
close(bs->fd);
fail_open:
free(bs);
return 0;
}
实际是调用到了binder驱动中的binder_open方法:
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);
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
get_task_struct(current);
proc->tsk = current;
INIT_LIST_HEAD(&proc->todo);
init_waitqueue_head(&proc->wait);
proc->default_priority = task_nice(current);
binder_lock(__func__);
binder_stats_created(BINDER_STAT_PROC);
hlist_add_head(&proc->proc_node, &binder_procs);
proc->pid = current->group_leader->pid;
INIT_LIST_HEAD(&proc->delivered_death);
filp->private_data = proc;
binder_unlock(__func__);
if (binder_debugfs_dir_entry_proc) {
char strbuf[11];
snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);
}
return 0;
}
这个函数的主要作用就是创建一个binder_proc 结构来保存打开/dev/binder设备节点的进程的上下文信息,并且将进程上下文信息保存在结构体file的private_data变量中,如此一来,在执行其他该设备节点的相关操作的时候便可以通过此结构体来获取进程的上下文信息,该进程上下文信息还会保存在一个全局的哈希表binder_procs中,供驱动内部使用。
binder_proc乃binder驱动层面非常重要的一个结构体,定义在驱动目录的binder.c中,
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;
struct rb_root allocated_buffers;
size_t free_async_space;
struct page **pages;
size_t buffer_size;
uint32_t buffer_free;
struct list_head todo;
wait_queue_head_t wait;
struct binder_stats stats;
struct list_head delivered_death;
int max_threads;//设置threads数目阀值</span>
int requested_threads;
int requested_threads_started;
int ready_threads;
long default_priority;
struct dentry *debugfs_entry;
};
其中红色标识的四个结构体是主旋律,这四个成员是四个红黑树节点,即binder_proc分别挂载4颗红黑树上,threads树用来保存proc进程内处理用户请求的线程;nodes树用来保存binder_proc进程内的binder实体;refs_by_desc树和refs_by_node树用来保存binder_proc进程内的Binder引用,即引用的其它进程的Binder实体,它分别用两种方式来组织红黑树,一种是以句柄作来key值来组织,一种是以引用的实体节点的地址值作来key值来组织,它们都是表示同一样东西,只不过是为了内部查找方便而用两个红黑树来表示。在一个进程中,有多少“被其他进程进行跨进程调用的”binder实体,就会在该进程对应的nodes树中生成多少个红黑树节点。另外一方面,一个进程若要访问其他进程的binder实体,则必须在其refs_by_desc树中拥有对应的引用节点。
之后是mmap方法,同样间接调用到内核的binder驱动中,
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 (proc->tsk != current)
return -EINVAL;
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
binder_debug(BINDER_DEBUG_OPEN_CLOSE,
"binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n",
proc->pid, vma->vm_start, vma->vm_end,
(vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
(unsigned long)pgprot_val(vma->vm_page_prot));
if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
ret = -EPERM;
failure_string = "bad vm_flags";
goto err_bad_arg;
}
vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
mutex_lock(&binder_mmap_lock);
if (proc->buffer) {
ret = -EBUSY;
failure_string = "already mapped";
goto err_already_mapped;
}
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
if (area == NULL) {
ret = -ENOMEM;
failure_string = "get_vm_area";
goto err_get_vm_area_failed;
}
proc->buffer = area->addr;
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
mutex_unlock(&binder_mmap_lock);
#ifdef CONFIG_CPU_CACHE_VIPT
if (cache_is_vipt_aliasing()) {
while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
pr_info("binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
vma->vm_start += PAGE_SIZE;
}
}
#endif
proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
if (proc->pages == NULL) {
ret = -ENOMEM;
failure_string = "alloc page array";
goto err_alloc_pages_failed;
}
proc->buffer_size = vma->vm_end - vma->vm_start;
vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
ret = -ENOMEM;
failure_string = "alloc small buf";
goto err_alloc_small_buf_failed;
}
buffer = proc->buffer;
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
binder_insert_free_buffer(proc, buffer);
proc->free_async_space = proc->buffer_size / 2;
barrier();
proc->files = get_files_struct(current);
proc->vma = vma;
proc->vma_vm_mm = vma->vm_mm;
/*pr_info("binder_mmap: %d %lx-%lx maps %p\n",
proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/
return 0;
err_alloc_small_buf_failed:
kfree(proc->pages);
proc->pages = NULL;
err_alloc_pages_failed:
mutex_lock(&binder_mmap_lock);
vfree(proc->buffer);
proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
mutex_unlock(&binder_mmap_lock);
err_bad_arg:
pr_err("binder_mmap: %d %lx-%lx %s failed %d\n",
proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
return ret;
}
函数首先通过filp->private_data得到在打开设备文件/dev/binder时创建的binder_proc结构。内存映射信息放在vma参数中,注意,此时的vma数据类型是struct vm_area_struct,它表示连续的一块虚拟内存区域,在函数变量声明的地方,我们还可以看到一个struct vm_struct结构,此结构体也表示一块连续的虚拟内存区域,那么两者的区别在哪里呢?在linux中,vm_area_struct表示的虚拟地址是给进程使用的,而vm_struct表示的虚拟地址是给内核使用的,它们对应的物理页面都可以是不连续的,vm_area_struct表示的虚拟地址范围为0~3G,而vm_struct表示的范围为(3G + 896M + 8M)~4G,我们有一个疑问,为何vm_struct地址空间范围不是3G~4G呢?原来,3G~(3G + 896M)和实际的连续的物理内存存在简单的对应关系,即对应0~896M的物理内存,而(3G + 896M) ~ (3G + 896M + 8M)是安全保护区域(所有指向该地址段的指针都是非法的),因此vm_struct使用(3G + 896M + 8M)~4G地址空间来映射非连续的物理页面;理解此函数需要一定的内核内存映射及管理的功底,敝人自认为功力有些许不足,只做大体功能的说明,binder通信的数据传输涉及两个系统空间,其一是用户进程,运行于用户空间;其二是内核进程,运行于内核空间;而客户端通过binder驱动与另一个用户空间进程的数据进行交互,则同样的数据需要Client将数据copy至内核虚拟内存空间,binder驱动再将内核虚拟内存空间数据copy至Server的用户空间进程的虚拟存储区域,即需要至少两次copy;而通过mmap映射的原理就是将一个物理页分别映射到进程虚拟空间及内核虚拟空间,则交互的数据只需从进程空间copy至内核空间一次,前文说到物理页多重映射可实现内核空间与server间的数据共享,即少copy一次,进而提升效率。
这样,frameworks/base/cmds/servicemanager/binder.c文件中的binder_open函数就描述完了,回到frameworks/base/cmds/servicemanager/service_manager.c文件中的main函数,下一步就是调用binder_become_context_manager来通知Binder驱动程序自己是Binder机制的上下文管理者,即守护进程。binder_become_context_manager函数位于frameworks/base/cmds/servicemanager/binder.c文件中:
int binder_become_context_manager(struct binder_state *bs)
{
return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}
这个功能是通过ioctl实现的,我们可以看一下内核binder驱动中对BINDER_SET_CONTEXT_MGR的处理:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
....
switch (cmd) {
...
case BINDER_SET_CONTEXT_MGR:
if (binder_context_mgr_node != NULL) {
pr_err("BINDER_SET_CONTEXT_MGR already set\n");
ret = -EBUSY;
goto err;
}
ret = security_binder_set_context_mgr(proc->tsk);
if (ret < 0)
goto err;
if (uid_valid(binder_context_mgr_uid)) {
if (!uid_eq(binder_context_mgr_uid, current->cred->euid)) {
pr_err("BINDER_SET_CONTEXT_MGR bad uid %d != %d\n",
from_kuid(&init_user_ns, current->cred->euid),
from_kuid(&init_user_ns, binder_context_mgr_uid));
ret = -EPERM;
goto err;
}
} else
binder_context_mgr_uid = current->cred->euid;
binder_context_mgr_node = binder_new_node(proc, 0, 0);
if (binder_context_mgr_node == NULL) {
ret = -ENOMEM;
goto err;
}
binder_context_mgr_node->local_weak_refs++;
binder_context_mgr_node->local_strong_refs++;
binder_context_mgr_node->has_strong_ref = 1;
binder_context_mgr_node->has_weak_ref = 1;
break;
...
}
}
binder_context_mgr_uid初始值为INVALID_UID(-1), 那么第一次调用会走else下边的红色代码行,
于是初始化binder_context_mgr_uid为current->cred->euid,这样,当前线程就成为Binder机制的守护进程了,并且通过binder_new_node为Service Manager创建Binder实体:
static struct binder_node *binder_new_node(struct binder_proc *proc,
binder_uintptr_t ptr,
binder_uintptr_t cookie)
{
struct rb_node **p = &proc->nodes.rb_node;
struct rb_node *parent = NULL;
struct binder_node *node;
while (*p) {
parent = *p;
node = rb_entry(parent, struct binder_node, rb_node);
if (ptr < node->ptr)
p = &(*p)->rb_left;
else if (ptr > node->ptr)
p = &(*p)->rb_right;
else
return NULL;
}
node = kzalloc(sizeof(*node), GFP_KERNEL);
if (node == NULL)
return NULL;
binder_stats_created(BINDER_STAT_NODE);
rb_link_node(&node->rb_node, parent, p);
rb_insert_color(&node->rb_node, &proc->nodes);
node->debug_id = ++binder_last_id;
node->proc = proc;
node->ptr = ptr;
node->cookie = cookie;
node->work.type = BINDER_WORK_NODE;
INIT_LIST_HEAD(&node->work.entry);
INIT_LIST_HEAD(&node->async_todo);
binder_debug(BINDER_DEBUG_INTERNAL_REFS,
"%d:%d node %d u%016llx c%016llx created\n",
proc->pid, current->pid, node->debug_id,
(u64)node->ptr, (u64)node->cookie);
return node;
}
注意,这里传进来的ptr和cookie均为NULL。函数首先检查proc->nodes红黑树中是否已经存在以ptr为键值的node,如果已经存在,就返回NULL。在这个场景下,由于当前线程是第一次进入到这里,所以肯定不存在,于是就新建了一个ptr为NULL的binder_node,并且初始化其它成员变量,并插入到proc->nodes红黑树中去。
binder_new_node返回到binder_ioctl函数后,就把新建的binder_node指针保存在binder_context_mgr_node中了,紧接着,又初始化了binder_context_mgr_node的引用计数值。
最后回到ServiceManager.c的main函数继续走,binder_loop; 则binder系统初始化完成,进入到loop中,等候Client与Server的交互请求了。
讲到这里,我们回到问题三:
C)Android为何引入Binder
从binder的框架模型中我们得知,数据交互的活跃方为Client-Server之间资源的调度和数据交互,内核中传统的IPC机制有管道,消息队列,共享内存,信号量,socket,这其中支持上述交互方式的只有socket;其次从传输性能看,System V IPC(消息队列,共享内存,信号量)传输数据均需要copy两次,第一次将数据源copy至内核开辟的缓冲区,第二次从内核开辟的缓冲区copy至接收方缓冲区,共享内存不需copy,但是很难控制,binder只需要一次copy,socket适合不同终端进程在网络中传输数据,效率低,开销大;最后就是安全性层面,android给应用程序都分配了相应的UID/PID,这一机制在底层有赖于linux内核的安全机制,如果用其他的通讯方式则UID/PID 只能自己设置,但是这就给网络应用程序以可乘之机,危害系统安全。
基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于 Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。
后续可能会有对kernel binder驱动部分细化的分析,欢迎诸位留言交流。