关于Binder (AIDL)的 oneway 机制
开篇总结Binder 是 Android 中的 IPC(进程间通信)的最要一环,它的作用就是:异步调用(单个binder请求)应用向 binder 驱动发送数据后不需要挂起线程等待 binder 驱动的回复,而是直接结束。串行化处理(多个binder请求)对于一个服务端的 AIDL 接口而言,所有的 oneway 方法不会同时执行,binder 驱动会将他们串行化处理,排队一个一个调用。像一些系统
本系列:
开篇总结
Binder 是 Android 中的 IPC(进程间通信)的最要一环,它的作用就是:
- 异步调用(单个binder请求)
应用向 binder 驱动发送数据后不需要挂起线程等待 binder 驱动的回复,而是直接结束。 - 串行化处理(多个binder请求)
对于一个服务端的 AIDL 接口而言,所有的 oneway 方法不会同时执行,binder 驱动会将他们串行化处理,排队一个一个调用。
像一些系统服务调用应用进程的时候就会使用 oneway,比如 AMS 调用应用进程启动 Activity,这样就算应用进程中做了耗时的任务,也不会阻塞系统服务的运行。
阻塞与否对比
- oneway:调用方非阻塞(non-block)
- 非 oneway:调用方阻塞(休眠)
首先是非 oneway 的情况:
这里的挂起相当于 Thread 的 sleep,是真正的"休眠",底层调用的是 waitEventInterruptible(), Linux 系统函数。
oneway 的情况,客户端就不需要挂起线程等待:
涉及到的 binder 命令也有规律:
- 由外部发送给 binder 驱动的都是 BC_ 开头;
- 由 binder 驱动发往外部的都是 BR_开头;
详细介绍
AIDL是Android Interface definition language的缩写,它是一种android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口。android提供了很多进程间通信的组件,像Activity、BroadcastReceiver和ContentProvider都可以实现进程间的通信。
为什么还要用AIDL这个东西呢?
Android开发coder务必都遇到过类似的情况:
WindowManager wm =(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
或者:
WifiManager wifiManager = (WifiManager)getSystemService(Context.WIFI_SERVICE);
应用层调用一些系统层的服务,处理我们想要的需求,有赖于android生态的强大,很多事情都像调api那样了。
那这个Manager到底做了什么工作呢?
其实应用层Manager只是一个管理类,真正干活的另有其人,是一个叫xxxManagerService的系统服务。在Android系统中有很多的Manager,wifi的管理类叫WifiManager,蓝牙的管理类叫BluetoothManager,但是,只要有xxxManager.java,就会有Ixxx.aidl,并且有xxxService.java。
AIDL的作用是什么?
aidl类是实现Manager和Service通信的桥梁。
我们知道了AIDL的作用,如果上面的讲解还不能让你知道AIDL是做什么的,之后我会再写(chao)一篇讲解binder/AIDL的,可以认为是中间接口,但是中间接口不只是单纯的作为传话筒的作用,比如HAL,你认为他只是传话筒吗?并不是,知道有层次概念后,对你的学习会帮助巨大,其实更多地oneway类似于代码里的一个变量判断,通过这个变量判断来决定binder的调用。
同步异步
说(抄)了一堆,现在实际操作看看:
用AIDL的人应该都知道下面代码中start和stop方法定义成oneway代表这个Binder接口是异步调用。
1,什么是异步调用?
举个例子:假如Client端调用IPlayer.start(),而且Server端的start需要执行2秒,由于定义的接口是异步的,Client端可以快速的执行IPlayer.start(),不会被Server端block住2秒。
举个例子:假如Client端调用IPlayer. getVolume(),而且Server端的getVolume需要执行1秒,由于定义的接口是同步的,Client端在执行IPlayer. getVolume()的时候,会被Server端block住1秒。
其实一般使用异步调用的时候,Client并不需要得到Server端执行Binder服务的状态或者返回值,这时候使用异步调用,可以有效的提高Client执行的效率。
提问
前面讲的好像没什么意思,这里简单思考一下
假设进程A中有如下两个Binder服务IPlayer1和IPlayer2,这两个服务都有两个异步的接口start和stop。
interface IPlayer1 {
oneway void start();//异步,执行2秒
oneway void stop();//异步,执行2秒
}
interface IPlayer2 {
oneway void start();//异步,执行2秒
oneway void stop();//异步,执行2秒
}
问题1
如果进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer2.start(),请问进程A能否同时响应这两次Binder调用并执行?
- 正确答案:可以同时执行。
如果进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer1.start(),请问进程A能否同时响应这两次Binder调用并执行?
- 正确答案:不能同时执行,需要一个一个排队执行。
如果进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer1.end(),请问进程A能否同时响应这两次Binder调用并执行?
- 正确答案:不能同时执行,需要一个一个排队执行。
如果回答正确并且知道原因的朋友,这个文章就可以不看了。如果回答错误或者蒙对了不清楚原因的朋友,请继续阅读文章帮你理解这些问题。反正我是没有做对。
代码分析
话不多说,先看源码,我们首先来看看oneway的Binder调用在Binder Driver中的逻辑
static bool binder_proc_transaction(struct binder_transaction *t,
struct binder_proc *proc,
struct binder_thread *thread)
{
struct binder_node *node = t->buffer->target_node;
bool oneway = !!(t->flags & TF_ONE_WAY);
bool pending_async = false;
binder_node_lock(node);
if (oneway) {
//不管是是进程B还是进程C,因为不是同一个binder_node,所以都是走false的逻辑
if (node->has_async_transaction) {
//不执行
} else {
node->has_async_transaction = 1;
}
}
binder_inner_proc_lock(proc);
if (!thread && !pending_async)
//oneway调用会找到一个空闲的Server端线程,用于响应这次oneway调用
thread = binder_select_thread_ilocked(proc);
if (thread) {
//oneway调用,thread不为空,直接把这次Binder work放到thread的工作队列去执行
binder_enqueue_thread_work_ilocked(thread, &t->work);
} else if (!pending_async) {
//不执行
} else {
//不执行
}
if (!pending_async)
//oneway调用,thread不为空,所以需要唤醒thread执行工作队列中的Binder work
binder_wakeup_thread_ilocked(proc, thread, !oneway /* sync */);
binder_inner_proc_unlock(proc);
binder_node_unlock(node);
return true;
}
对应到我们的三个问题,我们首先有这样子的前提,进程A中有两个Binder Server端IPlayer1和IPlayer2,也就是在Binder驱动中有两个binder node的结构体,并且进程A的Binder线程池处于空闲的状态。还有一点要明确的是,就算进程B和进程C同时发起Binder调用,但是在Binder驱动中还是有先后顺序,因为有一把锁binder_inner_proc_lock(proc)。
问题1解析:
因为进程B和进程C分别调用两个Binder服务,也就是两个binder node,所以进程B和进程C都会走如下的代码,也就是说进程A会有两个线程分别处理进程B的IPlayer1.start()和进程C的IPlayer2.start(),所以答案是同时执行:
static bool binder_proc_transaction(struct binder_transaction *t,
struct binder_proc *proc,
struct binder_thread *thread)
{
struct binder_node *node = t->buffer->target_node;
bool oneway = !!(t->flags & TF_ONE_WAY);
bool pending_async = false;
binder_node_lock(node);
if (oneway) {
//不管是是进程B还是进程C,因为不是同一个binder_node,所以都是走false的逻辑
if (node->has_async_transaction) {
//不执行
} else {
node->has_async_transaction = 1;
}
}
binder_inner_proc_lock(proc);
if (!thread && !pending_async)
//oneway调用会找到一个空闲的Server端线程,用于响应这次oneway调用
thread = binder_select_thread_ilocked(proc);
if (thread) {
//oneway调用,thread不为空,直接把这次Binder work放到thread的工作队列去执行
binder_enqueue_thread_work_ilocked(thread, &t->work);
} else if (!pending_async) {
//不执行
} else {
//不执行
}
if (!pending_async)
//oneway调用,thread不为空,所以需要唤醒thread执行工作队列中的Binder work
binder_wakeup_thread_ilocked(proc, thread, !oneway /* sync */);
binder_inner_proc_unlock(proc);
binder_node_unlock(node);
return true;
}
问题2解析:
我们假设先处理进程B的IPlayer1.start()的调用,进程B会执行和问题1中描述的代码一样的操作,唤醒进程A中的一个线程,处理这次进程B的IPlayer1.start()调用。
但是进程C的IPlayer1.start()调用逻辑就不一样了,应该是下面这个逻辑,也就是说进程A不会立刻处理进程C的IPlayer1.start()的调用。所以答案就是不能同时执行,需要一个一个排队执行。
static bool binder_proc_transaction(struct binder_transaction *t,
struct binder_proc *proc,
struct binder_thread *thread)
{
struct binder_node *node = t->buffer->target_node;
bool oneway = !!(t->flags & TF_ONE_WAY);
bool pending_async = false;
binder_node_lock(node);
//oneway == true
if (oneway) {
if (node->has_async_transaction) {
//因为是进程C和进程B是同一个binder_node,进程B已经将has_async_transaction设置true
pending_async = true;
} else {
//不执行
}
}
binder_inner_proc_lock(proc);
if (thread) {
//不执行
} else if (!pending_async) {
//不执行
} else {
//这次Binder work放到binder_node的async_todo队列中,不会立刻执行
binder_enqueue_work_ilocked(&t->work, &node->async_todo);
}
binder_inner_proc_unlock(proc);
binder_node_unlock(node);
return true;
}
那什么时候处理进程C的IPlayer1.start(),看下面代码,简单说就是会在处理完进程B的IPlayer1.start()之后,在释放进程B调用IPlayer1.start()申请的buffer的时候,处理进程C的IPlayer1.start()。
case BC_FREE_BUFFER: {
//准确释放进程B申请的buffer
if (buffer->async_transaction && buffer->target_node) {
struct binder_node *buf_node;
struct binder_work *w;
//先拿到这块buffer处理的binder node,也就是IPlayer1对应的binder node
buf_node = buffer->target_node;
binder_node_inner_lock(buf_node);
//检查一下buf_node是否有未处理的oneway的binder work
w = binder_dequeue_work_head_ilocked(
&buf_node->async_todo);
if (!w) {
//不执行
buf_node->has_async_transaction = 0;
} else {
//如果有未处理完的oneway的binder work,就将binder node保存的async_todo全部添加到进程A的todo。
binder_enqueue_work_ilocked(
w, &proc->todo);
//唤醒一个线程去处理todo中的binder work,也就是进程C的IPlayer1.start()
binder_wakeup_proc_ilocked(proc);
}
binder_node_inner_unlock(buf_node);
}
//释放进程B申请的buffer
trace_binder_transaction_buffer_release(buffer);
binder_transaction_buffer_release(proc, buffer, NULL);
binder_alloc_free_buf(&proc->alloc, buffer);
break;
}
问题3解析:
虽然进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer1.end()两个不同的方法,但是两个进程调用的Server端都是IPlayer1,也就是binder node是同一个,所以答案和问题2一样。
4 思考一个问题
假如一个进程B,在短时间内,例如一秒内,调用1000次进程A的IPlayer1.start()会发生什么。
第1次IPlayer1.start():唤醒进程A的一个线程处理IPlayer1.start(),两秒之后完成;
第2-1000次IPlayer1.start():发现IPlayer1对应的binder node正在处理一个oneway的方法,会把所有2~1000次的调用放到binder node的async_todo队列中,等第一次IPlayer1.start()执行完成之后,释放buffer的时候,才能去统一处理这些async_todo中保存的第2-1000次。
那么问题就来了,虽然第2-1000次的调用不会立刻执行,但是已经在进程A中申请了所有的2~1000次IPlayer1.start()所需要的buffer,一个zygote进程A,最大oneway请求的buffer上限为(1MB -8KB)/2 = 508KB,不懂的可以看 一次Binder通信最大可以传输多大的数据? 这个博客,假设一次IPlayer1.start(),需要申请1KB的buffer,也就意味这在第509次IPlayer1.start()的时候,无法申请到buffer从而导致IPlayer1.start()的Binder调用失败。
在[011]一个看似是系统问题的应用问题的解决过程中解决的就是这个问题。
5 小结
Binder机制是一个非常牛逼的机制,Android里很多跨进程通讯都用到了。里面有很多小的细节值得我们去深挖,只有完全理解Binder驱动,才能从微观的角度去解决宏观的问题。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)