Binder机制
Android系统中进程间通讯(IPC)的一种方式,Android中ContentProvider、Intent、aidl都是基于Binder
内存管理
Binder最大只能传1M的数据,因为Binder驱动只预留了一段1M大小的虚拟地址
mmap中定义BINDER_VM_SIZE为1M,Binder数据需要跨进程传递,需要在内核上开辟空间,所以允许在Binder上传递的数据不是无限大的
Binder 架构
通信采用C/S架构,包含Client、 Server、 ServiceManager 以及 Binder 驱动
在 framework 层进行了封装,通过 JNI 技术调用 Native(C/C++)层的 Binder 架构
在 Native 层以 ioctl 的方式与 Binder 驱动通讯
Binder 机制
注册服务端
通过 ServiceManager 注册服务。向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息,然后向ServiceManager的svcinfo列表中缓存注册的服务
获取服务端
通过 ServiceManager 向 svcinfo 列表中查询,返回服务端的代理
发送请求
通过BinderProxy将请求参数发送给ServiceManager
通过共享内存的方式使用内核方法copy_from_user()将参数拷贝到内核空间
客户端进入等待状态
Binder驱动向服务端的todo队列里面插入一条事务
事务执行完之后把执行结果通过 copy_to_user()将内核的结果拷贝到用户空间(只执行拷贝命令,不拷贝数据,binder只进行一次拷贝)
唤醒客户端并响应结果
Binder 驱动
运行在内核空间,负责各个用户进程通过 Binder 通信的内核模块
用户空间
用户程序的运行空间
只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令
用户空间访问内核空间的唯一方式就是系统调用,如访问文件,网络等。通过统一接口,资源访问均在内核控制下执行,以免用户程序对系统资源的越权访问。
内核空间
Linux 内核的运行空间
可以执行任意命令,调用系统的一切资源
他们之间考虑到安全因素是隔离的,即使用户程序崩溃,内核也不受影响
内核状态
当一个任务(进程)执行系统调用而陷入内核代码中执行时的状态,此时处理器处于特权级最高的(0级)内核代码中执行
用户运行态
当进程在执行用户自己的代码时,此时处理器在特权级最低的(3级)用户代码中运行
Binder 进程与线程
对于底层Binder驱动,通过 binder_procs 链表记录所有创建的 binder_proc 结构体,binder 驱动层的每一个 binder_proc 结构体都与用户空间的一个用于 binder 通信的进程一一对应,且每个进程有且只有一个 ProcessState 对象,这是通过单例模式来保证的。在每个进程中可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式,即一个线程对应一个 IPCThreadState 对象,在 Binder 驱动层也有与之相对应的结构,那就是 Binder_thread 结构体。在 binder_proc 结构体中通过成员变量 rb_root threads,来记录当前进程内所有的 binder_thread。
Binder 线程池:每个 Server 进程在启动时创建一个 binder 线程池,并向其中注册一个 Binder 线程;之后 Server 进程也可以向 binder 线程池注册新的线程,或者 Binder 驱动在探测到没有空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对于一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,例如 Android 的 system_server 进程就存在16个线程。对于所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。
ServiceManager 启动
分为 framework 层和 native 层,framework 层只是对 native 层进行了封装方便调用
启动是系统在开机时,init 进程解析 init.rc 文件调用 service_manager.c 中的 main() 入口启动的
启动过程
1、打开驱动创建全局链表 binder_procs
2、将自己当前进程信息保存到 binder_procs 链表
3、开启 loop 不断的处理共享内存中的数据,并处理响应
ServiceManager 注册服务
通过 ServiceManager 的 addService() 方法来注册服务
注册过程
1、ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令,携带 ADD_SERVICE_TRANSACTION 命令
2、注册服务的线程进入等待状态。waitForResponse()
3、Binder 驱动收到请求命令向 todo 队列里面添加一条注册服务的事务
4、事务处理完之后发送 BR_TRANSACTION 命令
5、ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务
6、最后发送 BR_REPLY 命令唤醒等待的线程,通知注册成功
ServiceManager 获取服务
获取过程
1、ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令携带 CHECK_SERVICE_TRANSACTION 命令
2、获取服务的线程进入等待状态 waitForResponse()
3、Binder 驱动收到请求命令向 todo 队列里面添加一条查询服务的事务
4、如果查询到,事务发送BC_TRANSACTION命令唤醒
5、ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务
6、最后发送 BR_REPLY 命令唤醒等待的线程,通知查询成功
7、如果未查询到,与 binder_procs 链表中的服务进行一次通讯再响应
完整通讯
步骤
1、通过 ServiceManager 获取到服务端的 BinderProxy 代理对象
2、调用 BinderProxy 将参数,方法标识(如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager
3、客户端线程进入等待状态
4、ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务
5、事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间
6、唤醒等待的线程,响应结果
问题
传统的 Linux 通信机制,比如 Socket,管道等都是内核支持的;但是 Binder 并不是 Linux 内核的一部分,它是怎么做到访问内核空间的呢
Linux 的动态可加载内核模块(Loadable Kernel Module,LKM)。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行
它在运行时被链接到内核作为内核的一部分在内核空间运行
Binder一次通信中进行了几次有效数据拷贝
一次。
Binder驱动程序位于内存的内核空间,Binder服务程序位于位于内存的用户空间,为Binder驱动和Binder服务分配了同一块物理内存地址
ServiceManager通过内核方法 copy_from_user()将数据拷贝到内核空间,这个过程中数据的物理地址发生了变化
事务将执行结果通过copy_to_user() 将结果拷贝到用户空间,这个过程中只传递了数据的虚拟地址,数据的物理地址没有变化
还有那些IPC通讯方式
linux中消息队列、共享内存、信号量、socket
socket传输效率低,开销大,用于网络和进程间低速通信,两次数据拷贝
消息队列、信号量采用存储-转发方式,两次拷贝
共享内存控制复杂,0次拷贝