如何轻松形象的理解Binder (1)?

1,289 阅读12分钟

前言

在做Framework之前呢,从事应用层开发的时候,有时候也会用到AIDL,了解到底层是用Binder实现,看了网上关于Binder的文章之后,整个人陷入了一个懵圈的状态,Binder到底是个啥?首先确定的一点,我们知道Binder是用来做跨进程通信使用的.那么我们知道跨线程通信用Handle,跨进程通信的特点是什么呢?

跨进程通信 (Inter Process Communication)

Linux中,是进程隔离的(如果对这个概念不理解,需要先了解一下进程隔离的相关知识),还需要了解内核空间和用户空间的相关概念,了解了这些特性,就理解了跨进程通信在Linux系统中重要性.

Linux系统编程---用户空间与内核空间

场景设定

很多文章首先比较了其他跨进程通信方式与Binder的优劣.这里就不描述了.直接描述一下Binder要解决的问题, 我们根据系统的状况设定一个场景.

前提

XX酒店为一个简单的系统.

  • 1 客房为用户空间.
  • 2 公共空间是内核空间.
  • 3 住在客房之中的游客就是我们的应用.(严谨一些就是每一个进程都住着一件客房),为了对应进程隔离,居住在不同客房的用户不能在各自的客房中直接交流.需要通过一些工具.

需求

为了解决上述交流的问题,酒店向市面征求解决方案,解决各个客房之间可能存在的沟通问题,但是也做了以下的限制:

  • 1 为了保证用户之间的安全,用户之间不能在各自的客房之中进行交流,在公共空间进行交流.
  • 2 为了保证便捷,给需要交流的用户尽可能的方便,不需要用户在客房和公共空间之间来回奔波.
  • 3 为了保证用户的隐私,不需要的用户不能被打扰.

我们的方案

我们看到了酒店的需求,就提出了我们自己的一套方案,大致方案如下

  • 1 在公共区域架设一个大的交换机.
  • 2 有通话需求的客户只需要告诉客服一声,我们就会将就会记录下来用户的信息,只要其他客户报出了该用户的信息就可以进行通话请求了.
  • 3 不需要的用户只要没有主动告知,我们将不对其进行被动沟通需求,只保留他的主动沟通需求.

酒店看了我们方案觉得还行,就让我们提供一个具体的技术方案,让酒店领导观看,下面我们就去写一下这个具体的需求.

放在公共区域的交换机

首先我们要确定方案并不是实时通话需求,那么就意味着我们需要一个空间来存储不同的客户之间交换的信息.

  • 1:提供一个空间(/dev/binder).
  • 2:提供一个打开这个空间的方式 binder_open.
  • 3:因为用户是需要将信息存储在这里,我们在空间中在分配一块区域去存储信息binder_mmap

准备工作(binder_init)

static int __init binder_init(void)
{
   int ret;
   //首先需要一个组来运行整个工作,使用了一个单线程的工作组 Linux
   binder_deferred_workqueue = create_singlethread_workqueue("binder");
   if (!binder_deferred_workqueue)
      return -ENOMEM;
   //内核调试相关, 文件目录在/sys/kernel/debug/ 
   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);
   //注册binder驱动设备,misc是miscellaneous的意思,表示杂项设备注册.
   ret = misc_register(&binder_miscdev);
   //binder目录存在,在创建下面这些子目录用作调试
   if (binder_debugfs_dir_entry_root) {
      debugfs_create_file("state",
                S_IRUGO,
                binder_debugfs_dir_entry_root,
                NULL,
                &binder_state_fops);
      debugfs_create_file("stats",
                S_IRUGO,
                binder_debugfs_dir_entry_root,
                NULL,
                &binder_stats_fops);
      debugfs_create_file("transactions",
                S_IRUGO,
                binder_debugfs_dir_entry_root,
                NULL,
                &binder_transactions_fops);
      debugfs_create_file("transaction_log",
                S_IRUGO,
                binder_debugfs_dir_entry_root,
                &binder_transaction_log,
                &binder_transaction_log_fops);
      debugfs_create_file("failed_transaction_log",
                S_IRUGO,
                binder_debugfs_dir_entry_root,
                &binder_transaction_log_failed,
                &binder_transaction_log_fops);
   }
   return ret;
}

下面是注册binder驱动设备时候传入的结构体.

//定义了不同文件操作,对应的函数
static 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,
};

static struct miscdevice binder_miscdev = {
   .minor = MISC_DYNAMIC_MINOR,// 次设备号
   .name = "binder",   // 设备名称
   .fops = &binder_fops //文件操作定义的集合,上面的自定义结构体
};

这一段我们可以理解为,我们设计了我们整个通信系统的最基本的设计,填写了一些报表,在酒店注册了一些信息和必须的准备.

申请空间(binder_open)

做好了上面一系列的准备工作,我们在酒店中也注册了我们的通信系统,也申请了一个工作组来做具体的维护工作,就提供了一个最进本

static int binder_open(struct inode *nodp, struct file *filp)
{
   //创建一个binder进程 ,binder_proc 是一个自定义结构体
   struct binder_proc *proc;
   //宏定义函数binder_debug ,最终调用了pr_info去输出内核调试日志
   binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
           current->group_leader->pid, current->pid);
    //给上面定义的binder进程申请一块内存,GFP_KERNEL是内核申请内存的一种方式.
   proc = kzalloc(sizeof(*proc), GFP_KERNEL);
   if (proc == NULL)
      return -ENOMEM;
   /*current这个变量是一个很突兀的变量,查了一下,这个是内核定义的一个宏函数get_current().
   获取的是当前进程结构的指针.*/
   //获取当前进程的task_struct ,Linux使用task_struct结构体中来管理进程,简单了解即可
   get_task_struct(current);
   //将当前线程的task_struct赋值给binder进程中的tsk
   proc->tsk = current;
   //内核宏函数,todo是一个list_head,初始化列表
   INIT_LIST_HEAD(&proc->todo);
   /*初始化等待队列,void init_waitqueue_head(wait_queue_head_t *q) 函数原型,&proc->wait的数据结
   构是一个wait_queue_head_t,*/
   init_waitqueue_head(&proc->wait);
   //将当前进程的优先级设置为binder进程的优先级
   proc->default_priority = task_nice(current);
   //设置一个同步锁
   binder_lock(__func__);
   // BINDER_STAT_PROC类型+1,计数器,BINDER_STAT_PROC是一个枚举对象中的值
   binder_stats_created(BINDER_STAT_PROC);
   // 设置proc_node为链表的头部
   hlist_add_head(&proc->proc_node, &binder_procs);
   // 设置pid
   proc->pid = current->group_leader->pid;
   //初始化已经发送的死亡通知列表 
   INIT_LIST_HEAD(&proc->delivered_death);
   //file文件的private_data指针指向binder进程.
   filp->private_data = proc;
   //释放锁
   binder_unlock(__func__);
   // 调试相关 sys/kernel/debug/binder目录存在
   if (binder_debugfs_dir_entry_proc) {
      char strbuf[11];
       //打印信息
      snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
      //将binder进程的debugfs_entry指针指向建的测试文件
      proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
         binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);
   }

   return 0;
}

上面的代码我们可以理解为,我们正式在酒店的公共空间中申请了一块地盘来,在在里面我们做了基本的初始化工作.

给用户分配空间(binder_mmap)

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;
    // current 前面介绍过,是当前进程的指针,判断传入的进程信息是否一致,不一致,就return
   if (proc->tsk != current)
      return -EINVAL;
    //Linux内核使用vm_area_struct结构表示一个独立的虚拟内存区域, 虚拟内存大小小于4M,就设置为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));
    // 设置权限,禁止写入.下面会将vm_area_struct的结构代码贴出来,
   if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
      ret = -EPERM;
      failure_string = "bad vm_flags";
      goto err_bad_arg;
   }
   // 禁止copy,禁止写入
   vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
   // 同步锁
   mutex_lock(&binder_mmap_lock);
   // binder进程的buffer为null,就转错误处理
   if (proc->buffer) {
      ret = -EBUSY;
      failure_string = "already mapped";
      goto err_already_mapped;
   }
   // 申请一个与进程虚拟地址大小的内核虚拟空间,采用IOREMAP,可以映射
   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);
// 内核配置,CPU CACHE是否支持VIPT
#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 %pK bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
         vma->vm_start += PAGE_SIZE;
      }
   }
#endif
   // 内存虚拟空间大小是否小于 binder最小分配限制
   if (vma->vm_end - vma->vm_start < BINDER_MIN_ALLOC) {
      ret = -EINVAL;
      failure_string = "VMA size < BINDER_MIN_ALLOC";
      goto err_vma_too_small;
   }
   //分配物理页的指针数组,数组大小为vma的等效page个数;
   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长度
   proc->buffer_size = vma->vm_end - vma->vm_start;
   //设置文件映射函数合集 , binder_vm_ops 定义了open close fault
   vma->vm_ops = & binder_vm_ops;
   //将 vm_private_data指向 binder进程,vm_private_data,用于指向进程的 用户虚拟地址空间中的私有数据, 查到的内容,并不一定准确.
   vma->vm_private_data = proc;
    // 分配物理页面,1是申请,0是释放
   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 指向 proc中的buffer
   buffer = proc->buffer;
   // 前文有出现 ,初始化列表
   INIT_LIST_HEAD(&proc->buffers);
   //在buffers中 插入buffer
   list_add(&buffer->entry, &proc->buffers);
   //标记binder_buffer为空闲的
   buffer->free = 1;
   //将binder_buffer添加到proc
   binder_insert_free_buffer(proc, buffer);
   //异步可用空间大小为buffer总大小的一半。
   proc->free_async_space = proc->buffer_size / 2;
   //内存屏障,防止乱序
   barrier();
   //将binder_proc的文件结构指向当前进程的文件结构
   proc->files = get_files_struct(current);
   //设置binder_proc的虚拟地址指向
   proc->vma = vma;
   //设置binder_proc的虚拟内存结构
   proc->vma_vm_mm = vma->vm_mm;
   /*pr_info("binder_mmap: %d %lx-%lx maps %pK\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:
err_vma_too_small:
   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;
}

上面整个过程中出现了2个非常重要的结构体和一个非常重要的函数,binder_proc,binder_bufferbinder_update_page_range()函数,binder_procopen中就已经存在,只是binder_procbinder_buffer有比较强的关联,我们将它们放在一起进行讲述.

binder_proc

从名称也能猜到,这个是用来描述binder进程的,它的第一次出现是在binder_open函数中,也就是当每一个进程去打开binder驱动时,就会创建一个对应的binder_proc来存储当前的进程的进程信息.

struct binder_proc {
    //存储进程节点
   struct hlist_node proc_node;
   //关联binder_threads中的 rb_node中的红黑树节点
   struct rb_root threads;
   //关联binder_node中的rb_node中的红黑树节点
   struct rb_root nodes;
   //使用句柄来排序的红黑树
   struct rb_root refs_by_desc;
   //使用binder实体的地址来排序
   struct rb_root refs_by_node;
   // 进程ID
   int pid;
   // 进程的虚拟地址空间
   struct vm_area_struct *vma;
   // 进程的内存结构体
   struct mm_struct *vma_vm_mm;
   // 进程的task结构体
   struct task_struct *tsk;
   // 进程的文件结构体
   struct files_struct *files;
   struct hlist_node deferred_work_node;
   int deferred_work;
   //进程内核虚拟内存的起始地址,和进程的虚拟地址映射在驱动中的同一物理页
   void *buffer;
   //和进程虚拟地址的差值
   ptrdiff_t user_buffer_offset;
   //关联binder_buffer
   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;
   // binder统计信息
   struct binder_stats stats;
   // 已分发的死亡通知
   struct list_head delivered_death;
   // 最大线程数
   int max_threads;
   // 请求的线程数
   int requested_threads;
   // 已启动的请求线程数
   int requested_threads_started;
   // 准备就绪的线程个数
   int ready_threads;
   // 默认优先级
   long default_priority;
   struct dentry *debugfs_entry;
};

binder_buffer

通信中binder驱动中每一段的内存使用binder_buffer用来描述.也就是binder_proc中的buffer关联.

struct binder_buffer {
   struct list_head entry; /* free and allocated entries by address */
   struct rb_node rb_node; /* free entry by size or allocated entry */
            /* by address */
   //是否空闲
   unsigned free:1;
   //是否允许释放
   unsigned allow_user_free:1;
   unsigned async_transaction:1;
   unsigned debug_id:29;

   struct binder_transaction *transaction;
   //该缓存区所需处理的Binder实体 
   struct binder_node *target_node;
   //数据大小
   size_t data_size;
   //数据偏移量
   size_t offsets_size;
   //数据地址
   uint8_t data[0];
};

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;
   struct page **page;
   struct mm_struct *mm;

   binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
           "%d: %s pages %pK-%pK\n", proc->pid,
           allocate ? "allocate" : "free", start, end);
   // 申请的结束地址小于开始地址,直接return 
   if (end <= start)
      return 0;
    
   trace_binder_update_page_range(proc, allocate, start, end);
   // binder_mmap vma 不为空 
   if (vma)
      mm = NULL;
   else
      mm = get_task_mm(proc->tsk); //读取进程的内存描述符

   if (mm) {
       //获取写锁
      down_write(&mm->mmap_sem);
      vma = proc->vma;
      if (vma && mm != proc->vma_vm_mm) {
         pr_err("%d: vma mm and task mm mismatch\n",
            proc->pid);
         vma = NULL;
      }
   }
   // 0代表释放,1代表申请
   if (allocate == 0)
      goto free_range;

   if (vma == NULL) {
      pr_err("%d: binder_alloc_buf failed to map pages in userspace, no vma\n",
         proc->pid);
      goto err_no_vma;
   }
    // 开始循环按照PAGE_SIZE 分配实际的物理页内存,每次分配一个
   for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
      int ret;

      page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];

      BUG_ON(*page);
      // alloc_page() linux分配物理页
      *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
      if (*page == NULL) {
         pr_err("%d: binder_alloc_buf failed for page at %pK\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);
      if (ret) {
         pr_err("%d: binder_alloc_buf failed to map page at %pK in kernel\n",
                proc->pid, page_addr);
         goto err_map_kernel_failed;
      }
      user_page_addr =
         (uintptr_t)page_addr + proc->user_buffer_offset;
      //将用户虚拟地址与该物理页建立映射关系
      ret = vm_insert_page(vma, user_page_addr, page[0]);
      if (ret) {
         pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n",
                proc->pid, user_page_addr);
         goto err_vm_insert_page_failed;
      }
      /* vm_insert_page does not seem to increment the refcount */
   }
   if (mm) {
      // 释放写锁
      up_write(&mm->mmap_sem);
      mmput(mm);
   }
   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;
}

小结

到这里,我们先分析上面所有的代码都做了什么,

  • 1: 首先初始化了binder驱动设备.初始化一个名称为binderworkqueue.驱动设备中的fops 指向了binder_fops
// 定义了对应的操作的函数.其中open和mmap前面已经介绍过了.
static 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,
};
  • 2: 假如当有外部进程使用binder进行通信,那么首先要调用open函数,创建了一个binder_proc实例.并将其加入到队列之中.前面也说过binder_proc就是一个binder进程.

  • 3: 给对应的通信进程赋予一定的空间,这里mmap是内存映射.这里申请了一块物理内存,然后将内核虚拟地址和用户虚拟地址映射到同一块物理内存上(最大申请4M)

  • 内存映射: 举个例子描述一下内存映射,酒店的每一个客房都是一个实际存在的实体.对应着计算机中存储空间中的实际物理内存,那么我们假如通过不同的网络平台定出去20个房间,对应的房间都有一个编码(对于着我们的常用的虚拟的内存地址),那么这时候一个虚拟的房间编码正常来说就对应这一个具体的房间.这个就是一个简单的内存映射.

  • 4: 经过上面的全部准备,接下来就是使用我们的系统进行通信了,在驱动设备的fops中也看到了不同的ioctl方式对于的函数都是binder_ioctl.那么下一章节就来具体描述对于的通信细节.

  • 感受: 在读整个代码的时候有大量的linux知识,查找对应的细节花费的时间比看binder其本身的代码花费了更多的时间.不过也为了避免陷入细节无法自拔,基本上通过man查看了对于的linux函数之后,并没有继续深入linux的实现细节.

参考

这个是一个私人博客,里面有大量的关于Android相关的知识

这个从linux的角度去分析了binder,看了之后对整个binder理解会更通透一下