【安卓IPC-Binder通信】第三篇:binder驱动内核解析

本章主要介绍binder驱动的三个重要函数,binder_open()、binder_mmap()以及binder_ioctl()。在介绍这三个函数的过程中,简单提binder中的数据结构。

在上一篇《ServiceManager启动过程》中,在servicemanager.c的main方法中,servicemanager进程调用了自己的binder_open()方法,申请了128K大小的内存空间,

我们从这里开始说起。

在servicemanager自己实现的binder_open()中,它分别调用了open(),mmap()以及ioctl()三个系统调用。这三个方法最终会调用到binder驱动层的binder_open(),binder_mmap()以及binder_ioctl()方法。

之所以会这样,是因为在binder驱动的调用binder_init()->init_binder_device()初始化过程中,在binder_device结构体中,已经对这些驱动层的方法做了映射。

kernel/msm-5.4/drivers/android/binder.c以及binder_internal.h
/**
 * struct binder_device - information about a binder device node(binder设备的节点信息)
 * @hlist:          list of binder devices (only used for devices requested via
 *                  CONFIG_ANDROID_BINDER_DEVICES)
 * @miscdev:        information about a binder character device node
 * @context:        binder context information
 * @binderfs_inode: This is the inode of the root dentry of the super block
 *                  belonging to a binderfs mount.
 */
struct binder_device {
    struct hlist_node hlist;
    struct miscdevice miscdev;
    struct binder_context context;
    struct inode *binderfs_inode;
    refcount_t ref;
};
 
//映射的结构体
const struct file_operations binder_fops = {
    .owner = THIS_MODULE,
    .poll = binder_poll,
    .unlocked_ioctl = binder_ioctl,
    .compat_ioctl = binder_ioctl,
    .mmap = binder_mmap,
    .open = binder_open,
    .flush = binder_flush,
    .release = binder_release,
};
 
//init_binder_device方法
static int __init init_binder_device(const char *name)
{
    int ret;
    struct binder_device *binder_device;
    //在内核空间为binder_device申请内存
    binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);
 
    //在这里做的映射
    binder_device->miscdev.fops = &binder_fops;
    binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;
    binder_device->miscdev.name = name;
 
    refcount_set(&binder_device->ref, 1);
    binder_device->context.binder_context_mgr_uid = INVALID_UID;
    binder_device->context.name = name;
    mutex_init(&binder_device->context.context_mgr_node_lock);
     
    //挂载binder驱动
    ret = misc_register(&binder_device->miscdev);
 
    hlist_add_head(&binder_device->hlist, &binder_devices);
 
    return ret;
}

1: binder_open()

任何一个进程想要使用binder驱动进行通信时,都需要执行binder_open方法,打开binder驱动。

为的是,创建初始化binder_proc,并将当前进程的binder_proc保存在binder驱动管理的全局链表binder_procs中。

在binder驱动中,binder_procs链表管理着所有进程执行完binder_open()创建的binder_proc。


在这里插入图片描述
binder_open()
static int binder_open(struct inode *nodp, struct file *filp)
{
//binder_proc结构体,每一个进程在binder驱动中都对应着一个此结构体。binder_proc是进程在使用binder通信时在内核中的数据结构。
    struct binder_proc *proc;
//为binder_proc结构体申请内存,并初始化为0,并返回一个指向分配的内存块起始地址的地址指针。
    proc = kzalloc(sizeof(*proc), GFP_KERNEL);
 
//获取一个当前线程的task_struct(PCB进程控制块),在linux中,无论是进程还是线程都是有PCB进程控制块来表示的。
    get_task_struct(current);
 
//将当前线程的task保存到binder进程的tsk
    proc->tsk = current;
//初始化todo队列(也叫做工作队列),用于存放待处理的请求(server端),由于服务进程不会是一个一个处理请求的,在todo队列中的请求会多线程处理。
    INIT_LIST_HEAD(&proc->todo);
//初始化wait队列,当某个服务进程被请多的次数过多时,多余的任务会放在wait队列中。
    init_waitqueue_head(&proc->wait);
//将当前线程线程优先级保存在binder进程的default_priority中
    proc->default_priority = task_nice(current); /*获取当前进程的优先级*/
 
//持有锁
    binder_lock(__func__);
 
//将当前的binder_proc对象加入到binder驱动管理的全局binder_procs链表中
    hlist_add_head(&proc->proc_node, &binder_procs);
//将当前线程所处进程的pid记录在binder进程的pid中
    proc->pid = current->group_leader->pid;
 
    INIT_LIST_HEAD(&proc->delivered_death);
//将proc记录到文件对象(传到用户空间的desc描述符就是描述此文件对象的)的私有数据当中,为的是在其它函数中也可以访问到此binder_proc对象
    filp->private_data = proc;
 
//释放锁
    binder_unlock(__func__);
 
    return 0;
}

2: binder_mmap()

以下为binder驱动一次拷贝原理图,binder_mmap主要是做了以下的事情。


在这里插入图片描述
binder_mmap
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
//这里先申请一个内核虚拟内存的指针地址
    struct vm_struct *area;
//从filp中拿出私有数据,在binder_open的时候就把本进程的binder_proc保存进去了。
    struct binder_proc *proc = filp->private_data;
    const char *failure_string;
//申请一个binder_buffer的地址,此地址是用来进程间传递数据的内核缓冲区
    struct binder_buffer *buffer;
 
//从这里可以看出,用户空间一次申请内存的大小不能大于4M,如果大于4M就再加4M。在用户空间的时候可以看到serviemanager申请了128*1024(128K)的大小。
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        vma->vm_end = vma->vm_start + SZ_4M;
 
    mutex_lock(&binder_mmap_lock);
//一个进程只能映射一次
    if (proc->buffer) {
        ret = -EBUSY;
        failure_string = "already mapped";
        goto err_already_mapped;
    }
 
//使用IOREMAP的方式,在内核虚拟内存申请大小为vma->vm_end - vma->vm_start的空间。
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
 
//把申请到的area的首地址,保存到pro->buffer。这样,*buffer就指向刚刚申请到的内核空间的首地址了。
//其中proc->buffer为当前进程buffer的首地址,后面还会新增其他buffer
    proc->buffer = area->addr;
     
//这里是在计算用户空间vm_area_struct vam的首地址与内核空间vm_struct area首地址的差值(偏移量),并保存至proc->user_buffer_offset
//由于虚拟内存是连续的(32位应用为0~4G),通过计算偏移量,就可以在用户空间拿到内核空间的地址。
    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
 
 
    mutex_unlock(&binder_mmap_lock);
 
// 在物理内存上申请对应页数总大小的物理内存。这里这样算一下是为了取整,因为待会分配物理内存时会按照page进行分配。“sizeof(proc->pages[0]) * ((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);
//设置内核空间的buffer_size。
    proc->buffer_size = vma->vm_end - vma->vm_start;
 
    vma->vm_ops = &binder_vm_ops;
    vma->vm_private_data = proc;
 
//分配物理页面,同时映射到内核空间和进程空间,先分配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;
    }
 
//让binder_buffer 对象指向binder_proc的buffer地址,也就是binder_buffer指向了area的首地址。
    buffer = proc->buffer;
 
// 初始化binder_proc的buffers链表头
    INIT_LIST_HEAD(&proc->buffers);
 
//将当前binder_proc拥有的binder_buffer链接到binder_proc当中,注意这个时候还只有一个binder_buffer
    list_add(&buffer->entry, &proc->buffers);
 
    buffer->free = 1; /*表明当前buffer是空闲buffer*/
    binder_insert_free_buffer(proc, buffer); /*将当前的binder_buffer插入到binder_proc空闲链表中*/
    proc->free_async_space = proc->buffer_size / 2;
    barrier();
    proc->files = get_files_struct(current);
    proc->vma = vma; /*保留这128K在用户空间地址空间*/
    proc->vma_vm_mm = vma->vm_mm; /*保留这128在kernel的地址空间*/
 
    return 0;
 }

然后是虚拟地址与物理地址的映射,包括用户空间与内核空间的两次映射。(这里主要是linux内核的知识,暂不做深究)

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 page **page;
  // mm_struct 是 task_struct 结构体中虚拟地址管理的结构体
  struct mm_struct *mm;
 
  if (vma)
        // binder_mmap 过程 vma 不为空,其他情况都为空
        mm = NULL;
    else
        // 获取 mm 结构体,从 tsk 中获取
        mm = get_task_mm(proc->tsk);
       
  if (mm) {
    // 获取 mm_struct 的写信号量
    down_write(&mm->mmap_sem);
    vma = proc->vma;
  }
 
  // 此处 allocate 为 1,代表分配过程。如果为 0 则代表释放过程
  if (allocate == 0)
    goto free_range;
 
  for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
    int ret;binder_update_page_range()
    page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
    // 分配一个 page 的物理内存
    *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
    // 物理空间映射到虚拟内核空间
    ret = map_kernel_range_noflush((unsigned long)page_addr,
          PAGE_SIZE, PAGE_KERNEL, page);
    //  用户空间地址 = 内核空间地址 + 偏移量
    user_page_addr = (uintptr_t)page_addr + proc->user_buffer_offset;
    //物理空间映射到虚拟进程空间
    ret = vm_insert_page(vma, user_page_addr, page[0]);
  }
   
  if (mm) {
    // 释放内存的写信号量
    up_write(&mm->mmap_sem);
     // 减少 mm->mm_users 计数
    mmput(mm);
  }
  return 0;
 
  //释放内存的流程
  free_range:
  ...
  return -ENOMEM;
}

3、linux内核分配内存函数

此部分为赋值了他们的文档,为的是看内核内存分配函数时可以查询。

1.kmalloc()
文件包含:#include
功能描述:kmalloc()用于分配在物理上连续的内存,虚拟地址也是连续的,是基于slab分配实际上存在的内存。
但是kmalloc最多只能开辟大小为32XPAGE_SIZE的内存,一般的PAGE_SIZE=4kB,也就是128kB的大小的内存。
函数定义:static __always_inline void * kmalloc (size_t size, gfp_t flags);
输入参数:size:分配内存的字节数
flags:分配标志,要分配内存的类型
返回参数:返回一个指向分配的内存块起始地址的地址指针
释放函数:kfree()

2.kcalloc()
文件包含:#include
功能描述:kcalloc()是基于slab分配实际上存在的内存,并且在分配后将内存中的内容都初始化为0,但kcalloc()主要为一个数组分配
内存空间,数组中的一个元素对应一个内存对象
函数定义:static inline void * kcalloc (size_t n, size_t size, gfp_t flags);
输入参数:n:数组中的元素个数
size:分配数组中每个元素所对应的内存对象的字节数
flags:分配标志,要分配内存的类型
返回参数:返回一个对所分配的内存对象数组的引用
释放函数:kfree()

3.kzalloc()
文件包含:#include
功能描述:kzalloc()是基于slab分配实际上存在的内存,在分配内存后将内存中的内容都初始化为0,和kcalloc()有些相似,
但不针对数组。
函数定义:static inline void * kzalloc (size_t size, gfp_t flags);
输入参数:size:分配内存的字节数
flags:分配标志,要分配内存的类型
返回参数:返回一个指向分配的内存块起始地址的地址指针
释放函数:kfree()

4.krealloc()
文件包含:#include
功能描述:重新分配内存,但不改变原地址空间中的内容
函数定义:void * __must_check krealloc (const void *p, size_t new_size, gfp_t flags);
输入参数:p:重新分配的原内存空间的起始地址
new_size:重新分配内存的字节数
flags:分配标志,要分配内存的类型
返回参数:重新分配的内存空间的起始地址
释放函数:遵从原分配内存函数的分配函数

5.vmalloc()
文件包含:#include
功能描述:用于分配一块非连续的地址空间,物理地址一般是非连续的,但是虚拟地址是连续的,分配的内存空间
被映射进入内核数据段中,从用户空间是不可见的。vmalloc比kmalloc要慢。
函数定义:void *vmalloc(unsigned long size);
输入参数:size:分配内存的字节数
返回参数:返回创建的地址区间的虚拟地址,如果分配失败返回NULL。
释放函数:vfree()

6.vmalloc_to_page()
文件包含:#include
功能描述:找到vmalloc()所分配内存的虚拟地址所映射的物理页,并返回该页的指针描述符
函数定义:struct page *vmalloc_to_page(const void *addr);
输入参数:addr:一般是vmalloc()的返回地址,也是一个虚拟地址
返回参数:addr映射的物理页的指针描述符

7.vmalloc_user()
文件包含:#include
功能描述:功能类似于vmalloc(),在vmalloc()的基础上将地址空间清零,这样该地址空间被映射到用户空间不会发生数据泄露
函数定义:void *vmalloc_user(unsigned long size);
输入参数:size:分配内存的字节数
返回参数:返回创建的地址区间的虚拟地址,如果分配失败返回NULL。
释放函数:vfree()

你可能感兴趣的:(【安卓IPC-Binder通信】第三篇:binder驱动内核解析)