Binder机制学习
Binder驱动
binder驱动在以misc设备进行注册,作为虚拟字符设备,没有直接操作硬件,只是对设备内存的处理。
主要是驱动设备的初始化(binder_init),打开 (binder_open),映射(binder_mmap),数据操作(binder_ioctl)。
用户态的程序调用Kernel层驱动是需要陷入内核态,进行系统调用(syscall),
比如打开Binder驱动方法的调用链为: open-> __open() -> binder_open()。
open()为用户空间的方法,__open()便是系统调用中相应的处理方法,通过查找,对应调用到内核binder驱动的binder_open()方法,至于其他的从用户态陷入内核态的流程也基本一致。
用户空间调用open()方法,最终会调用binder驱动的binder_open()方法;mmap()/ioctl()方法也是同理,在BInder系列的后续文章从用户态进入内核态,都依赖于系统调用过程。
Binder核心API
1.binder_init
该方法主要是为了注册misc设备,debugfs_create_dir是指在debugfs文件系统中创建一个目录,返回值是指向dentry的指针
注册misc设备,miscdevice结构体,便是前面注册misc设备时传递进去的参数
file_operations结构体,指定相应文件操作的方法,可以看到在上面用户空间的方法会最终调用到对应的方法,就是根据这个来定的
2.binder_open
创建binder_proc对象,并把当前进程等信息保存到binder_proc对象,该对象管理IPC所需的各种信息并拥有其他结构体的根结构体;再把binder_proc对象保存到文件指针filp,以及把binder_proc加入到全局链表binder_procs。
Binder驱动中通过static HLIST_HEAD(binder_procs);,创建了全局的哈希链表binder_procs,用于保存所有的binder_proc队列,每次新创建的binder_proc对象都会加入binder_procs链表中。
3.binder_mmap
主要功能:首先在内核虚拟地址空间,申请一块与用户虚拟内存相同大小的内存;然后再申请1个page大小的物理内存,再将同一块物理内存分别映射到内核虚拟地址空间和用户虚拟内存空间,从而实现了用户空间的Buffer和内核空间的Buffer同步操作的功能。
总结:
binder_init:初始化字符设备;
binder_open:打开驱动设备,过程需要持有binder_main_lock同步锁;
binder_mmap:申请内存空间,该过程需要持有binder_mmap_lock同步锁;
binder_ioctl:执行相应的ioctl操作,该过程需要持有binder_main_lock同步锁;
当处于binder_thread_read过程,read_buffer无数据则释放同步锁,并处于wait_event_freezable过程,等有数据到来则唤醒并尝试持有同步锁。
Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。
系统调用主要通过如下两个函数来实现:
copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间
Linux 下的传统 IPC 通信原理
这种传统的 IPC 通信方式有两个问题:
1.性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝;
2.接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,这个是 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;
LKM:模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。
内存映射就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。
Linux 下的传统 IPC 通信原理
Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。
比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘–>内核空间→用户空间);
通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。
而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。
Binder IPC 通信过程通常是这样:
1.首先 Binder 驱动在内核空间创建一个数据接收缓存区;
2.接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
3.发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。
其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。
其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。
Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。
Binder IPC通信至少是两个进程的交互:
client进程执行binder_thread_write,根据BC_XXX命令,生成相应的binder_work;
server进程执行binder_thread_read,根据binder_work.type类型,生成BR_XXX,发送到用户空间处理。
Binder通信过程
1.首先,一个进程使用 BINDER_SET_CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager
2.Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
3.Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
Binder通信的代理模式
当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。
当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。
ServiceManager进程启动
1.ServiceManager成为Binder管理者
首先在android系统开始启动时通过init.rc启动了servicemanager(Native进程)可执行文件进程,系统真正的ServieManager管理最终通过Native进程servicemanager来完成。接下来分析在main函数中做了那些事情:
1.binder_open(128*1024)通过打开/dev/binder设备节点返回fd文件描述符,通过mmap最终实现对binder驱动128K大小的内存映射
2.binder_become_context_manager(bs)通过ioctl往设备节点发送BINDER_SET_CONTEXT_MGR命令通知Binder驱动为servicemanager创建特殊不变的0句柄实体binder_node:binder_context_mgr_node
3.Binder_loop(bs,svcmgr_handler)通过iotcl发送BINDER_WRITE_READ命令并携带BC_ENTER_LOOPER请求命令告知Binder驱动创建了binder主线程并使binder驱动创建与之对应的binder_thread结构体存储到binder_proc中,并通过for循环进入循环读取binder驱动返回信息的循环流程
ServiceManager(native进程)最终通过获取binder驱动设备节点fd地址,与binder驱动内存映射(虚拟内存与物理内存映射),开启binder主体线程循环读取binder驱动返回的消息而成为服务端;
成为服务端后能够不断接收来自客户端(服务端 与客户端在此统称为客户端)的binder请求通信,ServiceManager(native进程)完成其他服务的注册并保存binder驱动创建的binder句柄值与服务名称字符串,保证客户端通过字符串获取其他服务引用句柄;因而ServiceManager(native进程)为binder管理者(服务注册与获取的桥梁)。
2.客户端通过ServiceManager获取服务
用户进程需要和ServiceManager(native进程)进程通信,ServiceManager进程接收到请求后去响应
1.用户进程第一步先实例化ServiceManager Binder Proxy代理
通过BinderInternal.getContextObject()调用获取到句柄值为0的BpBinder native对象,最终通过javaObjectForIbinder()函数的jni转换为BinderProxy类对象其实就是填充了这个类的mObject对象也就是ServiceManager Binder对象的引用封装对象,所以SMN才能通过asInterface完成代理Proxy的实例化
MMAP
虚拟进程地址空间(vm_area_struct)和虚拟内核地址空间(vm_struct)都映射到同一块物理内存空间。当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据copy_from_user拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。一般地做法,需要Client端进程空间拷贝到内核空间,再由内核空间拷贝到Server进程空间,会发生两次拷贝。
对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态。到此,为何不直接让发送端和接收端直接映射到同一个物理空间,那样就连一次复制的操作都不需要了,0次复制操作那就与Linux标准内核的共享内存的IPC机制没有区别了,对于共享内存虽然效率高,但是对于多进程的同步问题比较复杂,而管道/消息队列等IPC需要复制2两次,效率较低。
如下是大致流程图:
借鉴文章:
https://blog.csdn.net/universus/article/details/6211589
关于mmap:https://www.cnblogs.com/huxiao-tee/p/4660352.html