[023]你真的懂AIDL的oneway嘛?

1 前言

用AIDL的人应该都知道下面代码中start和stop方法定义成oneway代表这个Binder接口是异步调用。

interface IPlayer {
    oneway void start();//异步,假设执行2秒
    oneway void stop();//异步,假设执行2秒
    int getVolume();// 同步,假设执行1秒
}

1.1 什么是异步调用?

举个例子:假如Client端调用IPlayer.start(),而且Server端的start需要执行2秒,由于定义的接口是异步的,Client端可以快速的执行IPlayer.start(),不会被Server端block住2秒。

1.2 什么是同步调用?

举个例子:假如Client端调用IPlayer. getVolume(),而且Server端的getVolume需要执行1秒,由于定义的接口是同步的,Client端在执行IPlayer. getVolume()的时候,会被Server端block住1秒。

1.3 为什么会有同步调用和异步调用?

细心的读者已经发现了,其实一般使用异步调用的时候,Client并不需要得到Server端的执行Binder服务的状态或者返回值,这时候使用异步调用,可以有效的提高Client执行的效率。

2 提问

好像很多人都明白前面讲的意思,我就出几个问题考考大家

假设进程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秒
}

2.1 问题1

如果进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer2.start(),请问进程A能否同时响应这两次Binder调用并执行?

正确答案:可以同时执行。

2.2 问题2

如果进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer1.start(),请问进程A能否同时响应这两次Binder调用并执行?

正确答案:不能同时执行,需要一个一个排队执行。

2.3 问题3

如果进程B和进程C同一时刻分别调用IPlayer1.start()和IPlayer1.end(),请问进程A能否同时响应这两次Binder调用并执行?

正确答案:不能同时执行,需要一个一个排队执行。

如果回答正确并且知道原因的朋友,这个文章就可以不看了。如果回答错误或者蒙对了不清楚原因的朋友,请继续阅读文章帮你理解这些问题。

3 代码分析

话不多说,先看源码,我们首先来看看oneway的Binder调用在Binder Driver中的逻辑

/**
 * binder_proc_transaction() - sends a transaction to a process and wakes it up
 * @t:      transaction to send
 * @proc:   process to send the transaction to
 * @thread: thread in @proc to send the transaction to (may be NULL)
 */
static bool binder_proc_transaction(struct binder_transaction *t,
                    struct binder_proc *proc,
                    struct binder_thread *thread)
{
    //找到Server端的对应Binder服务在Binder驱动中对应的对象binder_node
    struct binder_node *node = t->buffer->target_node;
    //判断这次Binder调用是不是oneway
    bool oneway = !!(t->flags & TF_ONE_WAY);
    //初始化为false,用于标记当前Server端的对应Binder服务是否正在执行oneway的方法
    bool pending_async = false;

    binder_node_lock(node);
    //oneway == true
    if (oneway) {
        if (node->has_async_transaction) {
            //第2次oneway调用执行这里
            //发现对应Binder服务正在执行oneway的方法,设置pending_async为true
            pending_async = true;
        } else {
            //第1次oneway调用执行这里
            //发现对应Binder服务没有执行oneway的方法,设置has_async_transaction为1
            node->has_async_transaction = 1;
        }
    }

    binder_inner_proc_lock(proc);

    //如果发现Server端已经死亡,就直接返回了,正常不会执行
    if (proc->is_dead || (thread && thread->is_dead)) {
        binder_inner_proc_unlock(proc);
        binder_node_unlock(node);
        return false;
    }

    //oneway的调用thread为空,第1次oneway调用,pending_async为false
    if (!thread && !pending_async)
        //第1次oneway调用会找到一个空闲的Server端线程,用于响应这次oneway调用
        thread = binder_select_thread_ilocked(proc);

    if (thread) {
        //第1次oneway调用,thread不为空,直接把这次Binder work放到thread的工作队列去执行
        binder_enqueue_thread_work_ilocked(thread, &t->work);
    } else if (!pending_async) {
        binder_enqueue_work_ilocked(&t->work, &proc->todo);
    } else {
        //第2次oneway调用,thread为空,pending_async为true,
        //这次Binder work放到Binder Node的async_todo队列中,不会立刻执行
        binder_enqueue_work_ilocked(&t->work, &node->async_todo);
    }

    if (!pending_async)
        //第1次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,不懂的可以看[007]一次Binder通信最大可以传输多大的数据?这个博客,假设一次IPlayer1.start(),需要申请1KB的buffer,也就意味这在第509次IPlayer1.start()的时候,无法申请到buffer从而导致IPlayer1.start()的Binder调用失败。

[011]一个看似是系统问题的应用问题的解决过程中解决的就是这个问题。

5 小结

Binder机制是一个非常牛逼的机制,里面有很多小的细节值得我们去深挖,只有完全理解Binder驱动,才能从微观的角度去解决宏观的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342