您的位置:首页 > 运维架构 > Linux

十指相扣:陪binderIPC度过的漫长岁月(3)

2016-02-04 00:03 489 查看
现在该分析一下最初的binder进程是怎么产生的了,首先sm是一个linux内核的守护进程,我们要知道她的来路,还是先看看她的入口函数:

int main(int argc, char **argv)
{
//这个结构体存放binder进程的状态信息(binder设备信息)
struct binder_state *bs;
void *svcmgr = BINDER_SERVICE_MANAGER;

//连接驱动,再完成128kb的内核空间到进程空间的映射
bs = binder_open(128*1024);

//下面将会调用ioctl()函数,操作驱动文件节点告诉binder驱动
//这里是binder进程上下文管理者
if (binder_become_context_manager(bs)) {
LOGE("cannot become context manager (%s)/n", strerror(errno));
return -1;
}

svcmgr_handle = svcmgr;
//进入一个死循环(消息循环)
binder_loop(bs, svcmgr_handler);
return 0;
}


main()函数中调用了binder_open()函数,这函数待会分析,主要功能就是打开binder驱动的文件节点“dev/binder”,再完成内存映射,向驱动申请成为上下文管理者后即进入消息循环,等待来自client端的请求数据包,一旦有请求就会被唤醒,然后处理请求。前面说了,sm是一个守护进程,会在init.rc中被定义,那么linux kernel在启动时会对init里面的守护进程进行启动,进入sm的入口函数,sm就是这样以一个binder进程的形式被启动。

上面说到binder_open()函数,这个函数到底干了什么呢,先看看代码:

struct binder_state *binder_open(size_t mapsize)
{
struct binder_state *bs;
struct binder_version vers;
//创建binder状态信息(/dev/binder的设备信息),分配空间
bs = malloc(sizeof(*bs));
if (!bs) {
errno = ENOMEM;
return NULL;
}
//连接设备文件节点
bs->fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
//错误处理
if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
(vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {
fprintf(stderr,
"binder: kernel driver version (%d) differs from user space version (%d)\n",
vers.protocol_version, BINDER_CURRENT_PROTOCOL_VERSION);
goto fail_open;
}
//开始设定内存映射的大小
bs->mapsize = mapsize;
//mmap()系统调用完成内存映射,映射128K的内存,只读模式
//MAP_PRIVATE参数为使用私有映射,即满足cow原则,建立一个
//写入时的私有映射,这个映射由内核来控制空间分配,当内核空间被
//写入时,即新建一个内存的拷贝作为用户进程的映射,第一个参数NULL
//表示由内核来选定映射的起始地址,最后作为mmap()的返回值,最后
//一个参数为相对于映射基址的偏移量。
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;
}
return bs;
fail_map:
close(bs->fd);
fail_open:
free(bs);
return NULL;
}


关于这个函数的代码有比较多的容错代码我们暂且不去关注,在我注释的那几行就可以看出这个函数的基本功能了,无非两件重要的事情:1.打开设备文件,2.完成内核空间到用户进程的映射。

我们先来看看第一件事情,这个函数通过open()函数打开binder设备文件,我们再来看看open()函数,然而作为文件节点,在这里这个open函数是作为结构体的一个成员,是一个函数指针,我们需要找到它实际指向的函数。

首先我们注意到在binder.c这个文件的后面部分,有一个静态加载一次的代码块:

static const struct file_operations binder_fops = {

//表示拥有者(该驱动本身)
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};


binder_fops 是一个
file_operations
的结构体类型,即描述一个文件节点类型,mmap,poll等属性表明对该文件节点进行mmap(),poll()等操作时调用的函数的函数指针,这样说来,open()函数的函数指针是binder_open,所以我们对节点的描述符调用open()实际上就是调用
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);
mutex_lock(&binder_lock);
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;
mutex_unlock(&binder_lock);
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进程就是这样被创建的,在连接设备文件时调用
kzalloc
()函数完成了
proc
内存的分配,其中
binder_proc
结构体存放的是proc的上下文,这里的上下文信息会被放进系统的全局哈希表,毕竟系统不可能总是顾着一个进程,让你先运行着,等需要调度你时,我在通过哈希表找到你的上下文信息
context
,这个函数做了很多初始化的工作,current指向当前线程,初始化时进程的tsk任务字段指向当前线程,
INIT_LIST_HEAD
这个宏初始化了进程的事务列表,一个全局等待队列(to-do queue,
&proc->todo
),接下来会设定进程优先级为当前线程的优先级,再通过
hlist_add_head
函数将上下文信息添加到系统全局哈希表,虽然是添加到了全局哈希表满足系统的规定,然而直接对文件节点的操作是有绿色通道的,因为
filp
这个字段的存在,它的
private_data
域存放了proc的信息,所以open()函数是可以通过filp直接访问到进程的上下文的,filp是一个
struct file *
类型,作为参数传递到binder_open()函数,所以把它传递进去就可以了。

ok,我们再来看第二件重要的事情,内存映射,也就是mmap()函数的调用,同样的道理,它指向binder_mmap()函数:

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
//当要进行内存映射时,内核会在用户空间的mmap内存段
//寻找一块可用的内存区,并且会创建一个vm_area_struct
//的结构体(虚拟地址描述符),然后修改页目录表项跟内核空间(设备的内存)
//进行对应。
int ret;
struct vm_struct *area;
struct binder_proc *proc = filp->private_data;
const char *failure_string;
//映射在进程空间的地址用binder_buffer结构体描述
struct binder_buffer *buffer;
//边界检测,内存映射的大小最多为4M
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;
if (proc->buffer) {
ret = -EBUSY;
failure_string = "already mapped";
goto err_already_mapped;
}
//获取内核空间中可用的内存空间,即用户的进程中的缓存池空间
//会映射到这里获取到的内核空间。
//get_vm_area()是一个内核函数,大小为vma->vm_end - vma->vm_start
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;
}
//将映射的内存地址记录在进程的buffer指针,即进程的上下文信息里面
proc->buffer = area->addr;
//vma为进程虚拟地址,内存分页后产生的虚拟地址描述,
//计算出内核空间地址相对于进程空间虚拟地址的偏移量。
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;

//省略条件编译部分

//所有页面信息的存储。(指针),为每一页的页结构的指针分配空间。
//二重指针。
//需要把内核空间的这段内存作为缓存池映射到用户进程空间,然而由于用户进程内存分页
//所以需要为所有的页面分配内存(vma->vm_end - vma->vm_start) / PAGE_SIZE为缓存池
//需要的内存页数。
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;
}
//buffer_size即为进程缓存池的大小
proc->buffer_size = vma->vm_end - vma->vm_start;
//binder_vm_ops为虚拟内存操作函数,对应两个属性open和close,作为
//函数指针指向对应的处理函数。
vma->vm_ops = &binder_vm_ops;
//vma的私有数据段记录进程的信息,可访问进程的上下文信息
vma->vm_private_data = proc;
//为虚拟地址空间proc->buffer ~ proc->buffer + PAGE_SIZE分配一个空闲的物理页面,第一个参数为进程的地址,第二个参数为1说明为分配内存操作。
//第三个参数为分页的起始地址,这里的的映射指的就仅仅是地址的数学变换了。
//第四个参数为分页的尾地址。
//这里可以看出,其实内存映射就是共享一块物理内存,在这里是一页空闲的物理内存。
//页面结构的重调整,对应到物理地址,完成地址变换
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;
/*printk(KERN_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:
vfree(proc->buffer);
proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
err_bad_arg:
printk(KERN_ERR "binder_mmap: %d %lx-%lx %s failed %d\n",
proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
return ret;
}


上面比较详细的分析了内存映射的过程,其实这里我们明白了所谓的内存映射本质上还是内存共享,我们在真实的物理内存上分配一块(内核刚开始只是分配了一页)的内存,然后分别通过地址的数学变换换算到内核空间的地址以及进程的虚拟地址,大概的原理如下图:



binder_update_page_range
()函数为我们分配了一个空闲的一页大小的物理内存,再来看看它的实现:

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;
//内存页以page结构体形式存放,这里的page指针为二重指针
//指向一个物理内存页的地址。
struct page **page;

// 开始分配物理页面,一页一页的分配
// 并将"内核空间"和"用户空间(进程的内存区域)"指向同一块物理内存。
//在这里这个for循环只会循环一次,因为只分配一页的内存
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
struct page **page_array_ptr;
//页面指针的指针
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];

// 在这里分配物理页面
*page = alloc_page(GFP_KERNEL | __GFP_ZERO);

tmp_area.addr = page_addr;
tmp_area.size = PAGE_SIZE + PAGE_SIZE;
page_array_ptr = page;
// 将物理页面映射到内核空间中
ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);

//进程虚拟内存的起始地址。
user_page_addr =
(uintptr_t)page_addr + proc->user_buffer_offset;
// 将物理页面映射插入到进程的虚拟内存中
ret = vm_insert_page(vma, user_page_addr, page[0]);
}

return 0;

//...
}


这里需要的内存页不一定只有一页,我们的系统为什么老是分配一页的内存呢,这里就是性能优化节约内存的一种做法,用时分配,先分配一页内存,用完不够了,系统会再分配一页内存,还是比较机智的~

ok,这里binder的驱动层还有其他比较复杂的机制,这里先不分析,毕竟我不是专门搞linux内核开发的,这里水太深。。。
binder_become_context_manager
()这个函数是使这个binder进程升级成为context manger的关键所在,本来是个普通的binder进程,一下子就成了守护进程sm,来看看这个函数:

int binder_become_context_manager(struct binder_state *bs)
{
return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}


代码出奇的简洁,其实调用ioctl()系统函数对驱动设备节点进行了操作,具体的操作就不说了,涉及到太多驱动的io,跟我这个关系不大,
BINDER_SET_CONTEXT_MGR
这个参数已经告诉了我们她干了什么。

最后再看看main()函数中的最后调用的
binder_loop
() 进入消息循环的函数:

void binder_loop(struct binder_state *bs, binder_handler func)
{
int res;
struct binder_write_read bwr;
uint32_t readbuf[32];
bwr.write_size = 0;
bwr.write_consumed = 0;
bwr.write_buffer = 0;
readbuf[0] = BC_ENTER_LOOPER;
//通知内核开始进入消息循环状态
binder_write(bs, readbuf, sizeof(uint32_t));
//消息循环起始就是一个死循环
for (;;) {
//开始读数据
bwr.read_size = sizeof(readbuf);
bwr.read_consumed = 0;
bwr.read_buffer = (uintptr_t) readbuf;
//ioctl()函数向内核(设备文件)发送消息,并获取回复
res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
//容错处理
if (res < 0) {
ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno));
break;
}
res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
if (res == 0) {
ALOGE("binder_loop: unexpected reply?!\n");
break;
}
if (res < 0) {
ALOGE("binder_loop: io error %d %s\n", res, strerror(errno));
break;
}
}
}


代码还是比较简单,通过一个死循环不断地读取内核驱动节点,读取再通过解析消息(binder框架协议),就可以检测到是否服务的操作,比如注册,删除,查询等等。

这样我们对sm的来路就基本摸清了,一个本质是binder进程的守护进程,我们分析了它从创建(空间分配)到连接设备驱动,完成内存映射以及注册为context manager到最后建立消息循环的过程。

补充说明一点,进程的上下文信息会保存她进行ipc的所有的binder的引用(不是实体),实体会有很多个引用,引用按照binder在内核中地址(不受相对偏址影响没有重合)作为唯一索引,储存在上下文信息的红黑树里面,上下文中保留红黑树的关联节点。

关于kernel对于驱动的加载,驱动程序在binder.c文件的后面定义了一个__init 标识的
binder_init
()函数,这个函数里面注册了设备信息,会将设备以文件节点的形式存在”dev/binder”下,__init这个在宏定义中会分配在对应不同优先级的段中,最后通过
device_initcall(binder_init);
完成函数的注册,kernel启动时就会自动加载驱动,触发这个函数(在设备初始化时被调用),所以整个binder驱动程序就被调用了,binder进程也就得以运行啦~~

还有这里没有对每一个驱动级的函数进行源代码分析,主要是因为设计太多linux驱动的操作,跟ipc的关系不太密切,我们研究ipc并不是为了剖析linux底层,只是借助linux内核更深入的认识binder ipc,以后或许会有更详细的分析,ok,ipc的分析就到这里了,本系列结束~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息