圖解 | 不得錯過的Binder淺析(一)

哈利迪發表於2020-11-02

Framework和Binder的內容挺深的,本文還是站在應用層開發者的角度來建立基本認知,能在遇到問題的時候有思路和方向即可。(本文將帶著關鍵問題和核心流程展開,不會面面俱到)

大綱:

  • 背景
    • 為什麼要多程式
    • 為什麼要Binder
    • Binder簡單架構
  • 簡單示例
  • 原始碼分析
    • 客戶端與驅動互動
    • 服務端與驅動互動
  • 總結
  • 細節補充
    • Binder為什麼高效
    • Binder為什麼不用shm
  • 提問
  • 參考資料

本文約4.0k字,閱讀大約17分鐘。

Android原始碼基於8.0。

背景

為什麼要多程式

Binder是Android系統的一種跨程式通訊(IPC)機制。

在Android系統中,單個程式被分配了有限的記憶體,多程式可以使用更多記憶體隔離崩潰風險等。

多程式在Android中常見的使用場景有獨立程式的WebView、推送、保活、系統服務等,既然是多程式場景,那麼就需要跨程式通訊了。

為什麼要Binder

Linux自帶了一些跨程式通訊方式:

  • 管道(pipe):管道描述符是半雙工,單向的,資料只能往一個方向流,想要讀寫需要兩個管道描述符。Linux提供了pipe(fds)來獲取一對描述符,一個讀一個寫。匿名管道只能用在具有親緣關係的父子程式間的通訊,有名管道無此限制。

  • Socket:全雙工,可讀可寫。如Zygote程式等待AMS系統服務發起socket請求來建立應用程式。

  • 共享記憶體(shm,Shared Memory):會對映一段能被多個程式訪問的記憶體,是最高效的IPC方式,他通常需要結合其他跨程式方式如訊號量來同步資訊。Android基於shm改進得到匿名共享記憶體Ashmem(Anonymous Shared Memory),因高效而適合處理較大的資料,如應用程式通過共享記憶體來讀取SurfaceFlinger程式合成的檢視資料,進行展示。

  • 記憶體對映(mmap):Linux通過將一個虛擬記憶體區域與一個磁碟上的檔案關聯起來,以初始化這個虛擬記憶體區域的內容。通過指標的方式讀寫記憶體,系統會同步進對應的磁碟檔案。Binder用到了mmap

  • 訊號(signal):單向的,發個訊號就完事,無返回結果。只能發訊號,帶不了引數。如子程式被殺掉後系統會發出SIGCHLD訊號,父程式會清理子程式在程式表的描述資訊防止殭屍程式的發生。

另外還有檔案共享、訊息佇列(Message)等跨程式通訊方式…

這些跨程式通訊方式都各有優劣,Android最終選擇了自建一套兼顧好用、高效、安全的Binder。

  • 好用:易用的C/S架構(藉助AIDL後只需編寫業務邏輯)
  • 高效:用mmap進行記憶體對映,只需一次拷貝
  • 安全:核心態管理身份標記,每個App有UID來校驗許可權,同時支援實名(系統服務)和匿名(自己建立的服務)

Binder簡單架構

Linux記憶體被分為使用者空間核心空間,使用者空間需要經過系統呼叫才能訪問到核心空間。

(圖片來源:「寫給Android應用工程師的Binder原理剖析」)

Binder整體基於C/S架構。執行在核心空間的Binder驅動程式,會為使用者空間暴露出一個裝置檔案/dev/binder,程式間通過該檔案來建立通訊通道。

Binder的啟動過程:

  1. 開啟binder驅動(open)
  2. 將驅動檔案的描述符(mDriverFD)進行記憶體對映(mmap),分配緩衝區
  3. 服務端執行binder執行緒,把執行緒註冊到binder驅動,進入迴圈等待客戶端的指令(兩端通過ioctl與驅動互動)

簡單示例

AIDL(Android介面定義語言)可以輔助生成Binder的Java類,減少重複工作,使用姿勢網上有很多,這裡就直接手寫吧,方便理解。

示例呼叫流程如下:

程式碼不多,大部分是log,重點看註釋就行。

客戶端Activity:

//NoAidlActivity.java

protected void onCreate(Bundle savedInstanceState) {
    Intent intent = new Intent(this, MyService.class);

    bindService(intent, new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //1. 從物件池拿到可複用的物件(享元模式)
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();

            Log.e("哈利迪", "--- 我是客戶端 NoAidlActivity , pid = "
                  + Process.myPid() + ", thread = "
                  + Thread.currentThread().getName());

            String str = "666";
            Log.e("哈利迪", "客戶端向服務端傳送:" + str);
            //2. 往data寫資料,作為請求引數
            data.writeString(str);

            //3. 拿到服務端的IBinder控制程式碼,呼叫transact
            //約定行為碼是1;需要服務端的返回值,所以flags傳0表示同步呼叫
            service.transact(1, data, reply, 0);

            Log.e("哈利迪", "--- 我是客戶端 NoAidlActivity , pid = "
                  + Process.myPid() + ", thread = "
                  + Thread.currentThread().getName());

            //4. 從reply讀取服務端的返回值
            Log.e("哈利迪", "客戶端接收服務端返回:" + reply.readString());
        }
    }, Context.BIND_AUTO_CREATE);
}

service.transact傳入了flags為0,表示同步呼叫,會阻塞等待服務端的返回值。如果服務端進行了耗時操作,此時使用者操作UI則會引起ANR。

flags的另一個值是1,表示非同步呼叫的one way不需要等待服務端的返回結果,先忽略。

來看服務端執行的Service,

class MyService extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        //返回服務端的IBinder控制程式碼
        return new MyBinder();
    }
}

註冊服務,讓服務端Service執行在:remote程式,來實現跨程式,

<service
         android:name=".binder.no_aidl.MyService"
         android:process=":remote" />

執行在服務端的Binder物件,

class MyBinder extends Binder {

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags){
        if (code == 1) {//如果是約定好的行為碼1
            Log.e("哈利迪", "--- 我是服務端 MyBinder , pid = "
                  + Process.myPid() + ", thread = "
                  + Thread.currentThread().getName());
            //1. 從data讀取客戶端引數
            Log.e("哈利迪", "服務端收到:" + data.readString());

            String str = "777";
            Log.e("哈利迪", "服務端返回:" + str);
            //2. 從reply向客戶端寫返回值
            reply.writeString(str);

            //3. 處理完成
            return true;
        }
        return super.onTransact(code, data, reply, flags);
    }
}

執行如下,7行日誌:

由於我們的flags傳入的是0同步呼叫,可以試著在服務端onTransact裡sleep幾秒,會發現客戶端需要幾秒後才能列印出返回值。所以如果服務端需要進行耗時操作,客戶端則需要在子執行緒裡進行binder呼叫。

延伸:從 IT網際網路大叔 的「android獲取程式名函式,如何優化到極致」一文可見,在使用系統API時,如果有更好的方案,還是建議將跨程式方案getSystemService放到最後作為兜底,因為他需要的binder呼叫本身有開銷,而且作為應用層開發者也很少會去關注遠方程式的內部實現,萬一對方有潛在的耗時操作呢?

通過這個例子,我們可以看出,Binder機制使用了Parcel來序列化資料,客戶端在主執行緒呼叫了transact來請求(Parcel data傳參),服務端在Binder執行緒呼叫onTransact來響應(Parcel reply回傳結果)。

原始碼分析

Binder的呼叫流程大致如下,native層BpBinder的Bp指的是Binder proxy

可見,需要經過如下呼叫才能完成一次通訊:

  1. 請求:客戶端Java層->客戶端native層->Binder驅動層->服務端native層->服務端Java層
  2. 響應:服務端Java層->服務端native層->Binder驅動層->客戶端native層->客戶端Java層

即Binder驅動層充當著一個中轉站的作用,有點像網路分層模型。

客戶端與驅動互動

先來看客戶端與驅動的互動。因為是跨程式呼叫(指定了:remote),示例裡onServiceConnected回撥回來的service物件是個BinderProxy代理例項(不跨程式的話會發生遠端轉本地,後面講),我們以service.transact(1, data, reply, 0)這行呼叫作為入口跟進。

BinderProxy類寫在Binder類檔案裡面:

//BinderProxy.java

public boolean transact(int code, Parcel data, Parcel reply, int flags){
    //呼叫了native方法
    return transactNative(code, data, reply, flags);
}

這個native方法在android_util_Binder.cpp裡註冊,

//android_util_Binder.cpp

//JNI註冊
static const JNINativeMethod gBinderProxyMethods[] = {
    { "transactNative",
     "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z",
     (void*)android_os_BinderProxy_transact},
};

//native方法具體實現
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags){
    //轉成native層的Parcel
    Parcel* data = parcelForJavaObject(env, dataObj);
    Parcel* reply = parcelForJavaObject(env, replyObj);
    //拿到native層的控制程式碼BpBinder
    IBinder* target = (IBinder*)
        env->GetLongField(obj, gBinderProxyOffsets.mObject);
    //呼叫BpBinder的transact
    status_t err = target->transact(code, *data, reply, flags);
}

繼續跟BpBinder.cpp

//BpBinder.cpp

status_t BpBinder::transact(...){
    //交給執行緒單例處理,驅動會根據mHandle值來找到對應的binder控制程式碼
    status_t status = IPCThreadState::self()->transact(
        mHandle, code, data, reply, flags);
}

IPCThreadState是一個執行緒單例,負責與binder驅動進行具體的指令通訊,跟進IPCThreadState.cpp

//IPCThreadState.cpp

status_t IPCThreadState::transact(...){
    //將資料寫入mOut,見1.1
    err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);

    //...先忽略one way非同步呼叫的程式碼,只看有返回值的同步呼叫
    //跟binder驅動互動,傳入reply接收返回資料,見1.2
    err = waitForResponse(reply);
}

//1.1 將資料寫入mOut
status_t IPCThreadState::writeTransactionData(...)
{
    binder_transaction_data tr;
    //...打包各種資料(data size、buffer、offsets)
    tr.sender_euid = 0;
    //將BC_TRANSACTION指令寫入mOut
    mOut.writeInt32(cmd);
    //將打包好的binder_transaction_data寫入mOut
    mOut.write(&tr, sizeof(tr));
}

//1.2 跟binder驅動互動,傳入reply接收返回資料
status_t IPCThreadState::waitForResponse(...){
    //這個迴圈很重要,客戶端就是在這裡休眠等待服務端返回結果的
    while (1) {
        //跟驅動進行資料互動,往驅動寫mOut,從驅動讀mIn,見1.3
        talkWithDriver();
        //讀取驅動回覆的指令
        cmd = (uint32_t)mIn.readInt32();
        switch (cmd) {
            case BR_TRANSACTION_COMPLETE:
                //表示驅動已經收到客戶端的transact請求
                //如果是one way非同步呼叫,到這就可以結束了
                if (!reply && !acquireResult) goto finish;
                break;
            case BR_REPLY:
                //表示客戶端收到服務端的返回結果
                binder_transaction_data tr;
                //把服務端的資料讀出來,打包進tr
                err = mIn.read(&tr, sizeof(tr));
                //再把tr的資料透傳進reply
                reply->ipcSetDataReference(...);
                //結束
                goto finish;
        }
    }
}

//1.3 跟驅動進行資料互動,往驅動寫mOut,從驅動讀mIn
status_t IPCThreadState::talkWithDriver(bool doReceive){
    binder_write_read bwr;
    //指定寫資料大小和寫緩衝區
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();

    //指定讀資料大小和讀緩衝區
    if (doReceive && needRead) {
        bwr.read_size = mIn.dataCapacity();
        bwr.read_buffer = (uintptr_t)mIn.data();
    } else {
        bwr.read_size = 0;
        bwr.read_buffer = 0;
    }

    //ioctl的呼叫進入了binder驅動層的binder_ioctl
    ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);

    if (bwr.write_consumed > 0) {
        //資料已經寫入驅動,從mOut移除
        if (bwr.write_consumed < mOut.dataSize())
            mOut.remove(0, bwr.write_consumed);
        else
            mOut.setDataSize(0);
    }
    if (bwr.read_consumed > 0) {
        //從驅動讀出資料存入mIn
        mIn.setDataSize(bwr.read_consumed);
        mIn.setDataPosition(0);
    }
}

ioctl的呼叫進入了binder驅動層的binder_ioctl,驅動層的程式碼先不跟。

服務端與驅動互動

從「一圖摸清Android應用程式的啟動」一文可知,服務端建立了一個執行緒註冊進binder驅動,即binder執行緒,在ProcessState.cpp

//ProcessState.cpp

virtual bool threadLoop()
{	//把binder執行緒註冊進binder驅動程式的執行緒池中
    IPCThreadState::self()->joinThreadPool(mIsMain);
    return false;
}

跟進IPCThreadState.cpp

//IPCThreadState.cpp

void IPCThreadState::joinThreadPool(bool isMain){
    //向binder驅動寫資料,表示當前執行緒需要註冊進binder驅動
    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
    status_t result;
    do {
        //進入死迴圈,等待指令的到來,見1.1
        result = getAndExecuteCommand();
    } while (result != -ECONNREFUSED && result != -EBADF);
    //向binder驅動寫資料(退出迴圈,執行緒結束)
    mOut.writeInt32(BC_EXIT_LOOPER);
}

//1.1 等待指令的到來
status_t IPCThreadState::getAndExecuteCommand(){
    //跟驅動進行資料互動,驅動會把指令寫進mIn
    talkWithDriver();
    //從mIn讀出指令
    cmd = mIn.readInt32();
    //執行指令,見1.2
    result = executeCommand(cmd);
    return result;
}

//1.2 執行指令
status_t IPCThreadState::executeCommand(int32_t cmd){
    //客戶端發請求到驅動,驅動轉發到服務端
    switch ((uint32_t)cmd) {
        case BR_TRANSACTION:{
            //服務端收到BR_TRANSACTION指令
            binder_transaction_data tr;
            //讀出客戶端請求的引數
            result = mIn.read(&tr, sizeof(tr));

            //準備資料,向上傳給Java層
            Parcel buffer; Parcel reply;
            buffer.ipcSetDataReference(...);

            //cookie儲存的是binder實體,對應服務端的native層物件就是BBinder
            reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                                                            &reply, tr.flags);
            //服務端向驅動寫返回值,讓驅動轉發給客戶端
            sendReply(reply, 0);
        }
    }
}

//1.3 服務端向驅動寫返回值,讓驅動轉發給客戶端
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags){
    err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);
    //服務端返回結果給客戶端就行,不用等待客戶端,所以傳NULL
    return waitForResponse(NULL, NULL);
}

然後看下BBinder的transact是怎麼向上傳遞到Java層的,在Binder.cpp中,

//Binder.cpp

status_t BBinder::transact(uint32_t code, const Parcel& data, 
                           Parcel* reply, uint32_t flags){
    switch (code) {
            //ping指令用來判斷連通性,即binder控制程式碼是否還活著
        case PING_TRANSACTION:
            reply->writeInt32(pingBinder());
            break;
        default:
            //看這,通過JNI呼叫到Java層的execTransact,見1.1
            err = onTransact(code, data, reply, flags);
            break;
    }
    return err;
}

//android_util_Binder.cpp

//1.1 通過JNI呼叫到Java層的execTransact
virtual status_t onTransact(...){
    JNIEnv* env = javavm_to_jnienv(mVM);
    jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, ...);
}

回到Java層,execTransact如下:

//android.os.Binder.java

private boolean execTransact(...) {
    res = onTransact(code, data, reply, flags);
}

至此就回撥到了示例程式碼中服務端MyBinder的onTransact了,我們在示例中處理請求引數data和返回值reply,最後由native層的sendReply(reply, 0)真正向驅動寫返回值,讓驅動轉發給客戶端。

將呼叫程式碼和流程圖結合起來:

然後是指令互動圖(非one way模式):

binder同步呼叫等到服務端的BR_REPLY指令後就真正結束,服務端則繼續迴圈,等待下一次請求。

總結

本文主要介紹了Binder的背景和呼叫流程,將留下3個疑問繼續探討。

  1. binder控制程式碼是怎麼傳輸和管理的(binder驅動和ServiceManager程式)
  2. binder控制程式碼的遠端轉本地
  3. one way非同步模式和他的序列呼叫(async_todo)、同步模式的並行呼叫

系列文章:

細節補充

Binder為什麼高效

Linux使用者空間是無法直接讀寫磁碟的,系統所有的資源管理(讀寫磁碟檔案、分配回收記憶體、從網路介面讀寫資料)都是在核心空間完成的,使用者空間需要通過系統呼叫讓核心空間完成這些功能。

傳統IPC傳輸資料:傳送程式需要copy_from_user從使用者到核心,接收程式再copy_to_uer從核心到使用者,兩次拷貝。

而Binder傳輸資料:用mmap將binder核心空間的虛擬記憶體和使用者空間的虛擬記憶體對映到同一塊實體記憶體copy_from_user將資料從傳送程式的使用者空間拷貝到接收程式的核心空間(一次拷貝),接收程式通過對映關係能直接在使用者空間讀取核心空間的資料

(圖片來源:「寫給Android應用工程師的Binder原理剖析」)

Binder為什麼不用shm

shm通常需要結合其他跨程式方式如訊號量來同步資訊,使用沒有mmap方便。

提問

  • 上期提問: SurfaceFlinger程式為什麼不是通過Zygote程式的fork建立,而是由init程式建立?

參考資料


更多性感文章,關注原創技術公眾號:哈利迪ei

相關文章