Android 儲存系統之架構篇

Gityuan發表於2016-08-02

基於Android 6.0的原始碼,剖析儲存架構的設計

Android 儲存系統之原始碼篇

Android 儲存系統之架構篇

一、概述

本文講述Android儲存系統的架構與設計,涉及到最為核心的便是MountService和Vold這兩個模組以及之間的互動。上一篇文章Android儲存系統之原始碼篇從原始碼角度介紹相關模組的建立與啟動過程,那麼本文主要從全域性角度把握和剖析Android的儲存系統。

MountService:Android Binder服務端,執行在system_server程式,用於跟Vold進行訊息通訊,比如MountServiceVold傳送掛載SD卡的命令,或者接收到來自Vold的外設熱插拔事件。MountService作為Binder服務端,那麼相應的Binder客戶端便是StorageManager,通過binder IPC與MountService互動。

Vold:全稱為Volume Daemon,用於管理外部儲存裝置的Native daemon程式,這是一個非常重要的守護程式,主要由NetlinkManager,VolumeManager,CommandListener這3部分組成。

1.1 模組架構

從模組地角度劃分Android整個儲存架構:

arch-vold-mount

圖解:

  • Linux Kernel:通過uevent向Vold的NetlinkManager傳送Uevent事件;
  • NetlinkManager:接收來自Kernel的Uevent事件,再轉發給VolumeManager;
  • VolumeManager:接收來自NetlinkManager的事件,再轉發給CommandListener進行處理;
  • CommandListener:接收來自VolumeManager的事件,通過socket通訊方式傳送給MountService;
  • MountService:接收來自CommandListener的事件。

1.2 程式架構

(1)先看看Java framework層的執行緒:

root@gityuan:/ # ps -t | grep 1212
system    1212  557   2334024 160340 SyS_epoll_ 7faedddbe4 S system_server
system    2662  1212  2334024 160340 SyS_epoll_ 7faedddbe4 S MountService
system    2663  1212  2334024 160340 unix_strea 7faedde73c S VoldConnector
system    2664  1212  2334024 160340 unix_strea 7faedde73c S CryptdConnector
...

MountService執行在system_server程式,這裡查詢的便是system_server程式的所有子執行緒,system_server程式承載整個framework所有核心服務,子執行緒數有很多,這裡只列舉與MountService模組相關的子執行緒。

(2)再看看Native層的執行緒:

root@gityuan:/ # ps -t | grep " 387 "
USER      PID   PPID  VSIZE  RSS   WCHAN              PC  NAME
root      387   1     13572  2912  hrtimer_na 7fa34755d4 S /system/bin/vold
root      397   387   13572  2912  poll_sched 7fa3474d1c S vold
root      399   387   13572  2912  poll_sched 7fa3474d1c S vold
root      400   387   13572  2912  poll_sched 7fa3474d1c S vold
media_rw  2702  387   7140   2036  inotify_re 7f84b1d6ac S /system/bin/sdcard

Vold作為native守護程式,程式名為”/system/bin/vold”,pid=387,通過ps -t可查詢到該程式下所有的子程式/執行緒。

小技巧:有讀者可能會好奇,為什麼/system/bin/sdcard是子程式,而非子執行緒呢?要回答這個問題,有兩個方法,其一就是直接看擼原始碼,會發現這是通過fork方式建立的,而其他子執行緒都是通過pthread_create方式建立的。當然其實還有個更快捷的小技巧,就是直接看上圖中的第4列,這一列的含義是VSIZE,代表的是程式虛擬地址空間大小,是否共享地址空間,這是程式與執行緒最大的區別,再來看看/sdcard的VSIZE大小跟父程式不一樣,基本可以確實/sdcard是子程式。

(3) 從程式/執行緒視角來看Android儲存架構:

arch-io-process

  • Java層:採用 1個主執行緒(system_server) + 3個子執行緒(VoldConnector, MountService, CryptdConnector);
  • Native層:採用 1個主執行緒(/system/bin/vold) + 3個子執行緒(vold) + 1子程式(/system/bin/sdcard);

注:圖中紅色字代表的程式/執行緒名,vold程式通過pthread_create的方式建立的3個子執行緒名都為vold,圖中只是為了便於區別才標註為vold1, vold2, volD3,其實名稱都為vold。

Android還可劃分為核心空間(Kernel Space)和使用者空間(User space),從上圖可看出,Android儲存系統在User space總共採用9個程式/執行緒的架構模型。當然,除了這9個進/執行緒,另外還會在handler訊息處理過程中使用到system_server的兩個子執行緒:android.fgandroid.io

Tips: 同一個模組可以執行在各個不同的程式/執行緒, 同一個程式可以執行不同模組的程式碼,所以從程式角度和模組角度劃分看到的有所不同的.

1.3 類關係圖

vold

volume

上圖中4個藍色塊便是前面談到的核心模組。

二、 通訊架構

Android儲存系統中涉及各個程式間通訊,這個架構採用的socket,並沒有採用Android binder IPC機制。這樣的架構程式碼大量更少,整體架構邏輯也相對簡單,在介紹通訊過程前,先來看看MountService物件的例項化過程,那麼也就基本明白程式架構中system_sever程式為了MountService服務而單獨建立與共享使用到執行緒情況。

public MountService(Context context) {
    sSelf = this;

    mContext = context;
    //FgThread執行緒名為“"android.fg",建立IMountServiceListener回撥方法
    mCallbacks = new Callbacks(FgThread.get().getLooper());
    //獲取PKMS的Client端物件
    mPms = (PackageManagerService) ServiceManager.getService("package");
    //建立“MountService”執行緒
    HandlerThread hthread = new HandlerThread(TAG);
    hthread.start();

    mHandler = new MountServiceHandler(hthread.getLooper());
    //IoThread執行緒名為"android.io",建立OBB操作的handler
    mObbActionHandler = new ObbActionHandler(IoThread.get().getLooper());

    File dataDir = Environment.getDataDirectory();
    File systemDir = new File(dataDir, "system");
    mLastMaintenanceFile = new File(systemDir, LAST_FSTRIM_FILE);
    //判斷/data/system/last-fstrim檔案,不存在則建立,存在則更新最後修改時間
    if (!mLastMaintenanceFile.exists()) {
        (new FileOutputStream(mLastMaintenanceFile)).close();
        ...
    } else {
        mLastMaintenance = mLastMaintenanceFile.lastModified();
    }
    ...
    //將MountServiceInternalImpl登記到sLocalServiceObjects
    LocalServices.addService(MountServiceInternal.class, mMountServiceInternal);
    //建立用於VoldConnector的NDC物件
    mConnector = new NativeDaemonConnector(this, "vold", MAX_CONTAINERS * 2, VOLD_TAG, 25,
            null);
    mConnector.setDebug(true);
    //建立執行緒名為"VoldConnector"的執行緒,用於跟vold通訊
    Thread thread = new Thread(mConnector, VOLD_TAG);
    thread.start();

    //建立用於CryptdConnector工作的NDC物件
    mCryptConnector = new NativeDaemonConnector(this, "cryptd",
            MAX_CONTAINERS * 2, CRYPTD_TAG, 25, null);
    mCryptConnector.setDebug(true);
    //建立執行緒名為"CryptdConnector"的執行緒,用於加密
    Thread crypt_thread = new Thread(mCryptConnector, CRYPTD_TAG);
    crypt_thread.start();

    //註冊監聽使用者新增、刪除的廣播
    final IntentFilter userFilter = new IntentFilter();
    userFilter.addAction(Intent.ACTION_USER_ADDED);
    userFilter.addAction(Intent.ACTION_USER_REMOVED);
    mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler);

    //內部私有volume的路徑為/data,該volume通過dumpsys mount是不會顯示的
    addInternalVolume();

    //預設為false
    if (WATCHDOG_ENABLE) {
        Watchdog.getInstance().addMonitor(this);
    }
}

其主要功能依次是:

  1. 建立ICallbacks回撥方法,FgThread執行緒名為”android.fg”,此處用到的Looper便是執行緒”android.fg”中的Looper;
  2. 建立並啟動執行緒名為”MountService”的handlerThread;
  3. 建立OBB操作的handler,IoThread執行緒名為”android.io”,此處用到的的Looper便是執行緒”android.io”中的Looper;
  4. 建立NativeDaemonConnector物件
  5. 建立並啟動執行緒名為”VoldConnector”的執行緒;
  6. 建立並啟動執行緒名為”CryptdConnector”的執行緒;
  7. 註冊監聽使用者新增、刪除的廣播;

從這裡便可知道共建立了3個執行緒:MountService,VoldConnector,CryptdConnector,另外還會使用到系統程式中的兩個執行緒android.fgandroid.io. 這便是在文章開頭程式架構圖中Java framework層程式的建立情況.

接下來,我們分別從MountService向vold傳送訊息和接收訊息兩個方面,以及Kernel向vold上報事件3個方面展開。

2.1 MountService傳送訊息

system_server程式與vold守護程式間採用socket進行通訊,這個通訊過程是由MountService執行緒向vold執行緒傳送訊息。這裡以執行mount呼叫為例:

2.1.1 MS.mount

class MountService extends IMountService.Stub
        implements INativeDaemonConnectorCallbacks, Watchdog.Monitor {

    public void mount(String volId) {
        ...
        try {
            //【見小節2.1.2】
            mConnector.execute("volume", "mount", vol.id, vol.mountFlags, vol.mountUserId);
        } catch (NativeDaemonConnectorException e) {
            throw e.rethrowAsParcelableException();
        }
    }
}

2.1.2 NDC.execute

[-> NativeDaemonConnector.java]

public NativeDaemonEvent execute(String cmd, Object... args)
    throws NativeDaemonConnectorException {
    return execute(DEFAULT_TIMEOUT, cmd, args);
}

其中DEFAULT_TIMEOUT=1min,即命令執行超時時長為1分鐘。經過層層呼叫到executeForList()

public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args)
        throws NativeDaemonConnectorException {
    final long startTime = SystemClock.elapsedRealtime();

    final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();

    final StringBuilder rawBuilder = new StringBuilder();
    final StringBuilder logBuilder = new StringBuilder();

    //mSequenceNumber初始化值為0,每執行一次該方法則進行加1操作
    final int sequenceNumber = mSequenceNumber.incrementAndGet();

    makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);

    //例如:“3 volume reset”
    final String rawCmd = rawBuilder.toString();
    final String logCmd = logBuilder.toString();

    log("SND -> {" + logCmd + "}");

    synchronized (mDaemonLock) {
        //將cmd寫入到socket的輸出流
        mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));
        ...
    }

    NativeDaemonEvent event = null;
    do {
        //阻塞等待,直到收到相應指令的響應碼
        event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd);
        events.add(event);
    //當收到的事件響應碼屬於[100,200)區間,則繼續等待後續事件上報
    } while (event.isClassContinue());

    final long endTime = SystemClock.elapsedRealtime();
    //對於執行時間超過500ms則會記錄到log
    if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
        loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
    }
    ...
    return events.toArray(new NativeDaemonEvent[events.size()]);
}
  • 首先,將帶執行的命令mSequenceNumber執行加1操作;
  • 再將cmd(例如3 volume reset)寫入到socket的輸出流;
  • 通過迴圈與poll機制阻塞等待底層響應該操作完成的結果;
  • 有兩個情況會跳出迴圈:
    • 當超過1分鐘未收到vold相應事件的響應碼,則跳出阻塞等待;
    • 當收到底層的響應碼,且響應碼不屬於[100,200)區間,則跳出迴圈。
  • 對於執行時間超過500ms的時間,則額外輸出以NDC Command開頭的log資訊,提示可能存在優化之處。

2.1.3 FL.onDataAvailable

MountService執行緒通過socket傳送cmd事件給vold,對於vold守護程式在啟動的過程,初始化CommandListener時通過pthread_create建立子執行緒vold來專門監聽MountService傳送過來的訊息,當該執行緒接收到socket訊息時,便會呼叫onDataAvailable()方法

[-> FrameworkListener.cpp]

bool FrameworkListener::onDataAvailable(SocketClient *c) {
    char buffer[CMD_BUF_SIZE];
    int len;
    // 多次嘗試從socket管道讀取資料
    len = TEMP_FAILURE_RETRY(read(c->getSocket(), buffer, sizeof(buffer)));
    ...

    for (i = 0; i < len; i++) {
        if (buffer[i] == '\0') {
            //分發該命令【見小節2.1.4】
            dispatchCommand(c, buffer + offset);
            ...
        }
    }
    return true;
}

2.1.4 FL.dispatchCommand

[-> FrameworkListener.cpp]

void FrameworkListener::dispatchCommand(SocketClient *cli, char *data) {
    ...
    for (i = mCommands->begin(); i != mCommands->end(); ++i) {
        FrameworkCommand *c = *i;

        if (!strcmp(argv[0], c->getCommand())) {
            //找到相應的類處理該命令
            if (c->runCommand(cli, argc, argv)) {
                SLOGW("Handler '%s' error (%s)", c->getCommand(), strerror(errno));
            }
            goto out;
        }
    }
    ...
}

這是用於分發從MountService傳送過來的命令,針對不同的命令呼叫不同的類,總共有以下6類:

  • DumpCmd
  • VolumeCmd
  • AsecCmd
  • ObbCmd
  • StorageCmd
  • FstrimCmd

另外,在處理過程中遇到下面情況,則會直接傳送響應嗎500的應答訊息給MountService

  • 當無法找到匹配的類,則會直接向MountService返回響應碼500,內容”Command not recognized”的應答訊息;
  • 命令引數過長導致socket管道溢位,則會傳送響應碼500,內容”Command too long”的應答訊息。

2.1.5 CL.runCommand

例如前面傳送過來的是volume mount,則會呼叫到CommandListener的內部類VolumeCmd的runCommand來處理該訊息,並進入mount分支。

int CommandListener::VolumeCmd::runCommand(SocketClient *cli,
                                           int argc, char **argv) {
    VolumeManager *vm = VolumeManager::Instance();
    std::lock_guard<std::mutex> lock(vm->getLock());
    ...
    std::string cmd(argv[1]);
    if (cmd == "reset") {
           return sendGenericOkFail(cli, vm->reset());
    }else if (cmd == "mount" && argc > 2) {
        // mount [volId] [flags] [user]
        std::string id(argv[2]);
        auto vol = vm->findVolume(id);
        if (vol == nullptr) {
            return cli->sendMsg(ResponseCode::CommandSyntaxError, "Unknown volume", false);
        }

        int mountFlags = (argc > 3) ? atoi(argv[3]) : 0;
        userid_t mountUserId = (argc > 4) ? atoi(argv[4]) : -1;

        vol->setMountFlags(mountFlags);
        vol->setMountUserId(mountUserId);
        //真正的掛載操作【見2.1.6】
        int res = vol->mount();
        if (mountFlags & android::vold::VolumeBase::MountFlags::kPrimary) {
            vm->setPrimary(vol);
        }
        //傳送應答訊息給MountService【見2.2.1】
        return sendGenericOkFail(cli, res);
    }
    // 省略其他的else if
    ...
}

2.1.6 mount

這裡便進入了VolumeManager模組,執行volume裝置真正的掛載操作。對於掛載內建儲存和外接儲存流程是有所不同的,這裡就不再細說,簡單的呼叫流程:

VolumeCmd.runCommand
    VolumeBase.mount
        EmulatedVolume.doMount(內建)
        PublicVolume.doMount(外接)
            vfat::Check
            vfat::Mount
            fork (/sdcard)

2.1.7 小節

mountservice_socket

MountService向vold傳送訊息後,便阻塞在圖中的MountService執行緒的NDC.execute()方法,那麼何時才會退出呢?圖的後半段MonutService接收訊息的過程會有答案,那便是在收到訊息,並且訊息的響應嗎不屬於區間[600,700)則新增事件到ResponseQueue,從而喚醒阻塞的MountService繼續執行。關於上圖的後半段介紹的便是MountService接收訊息的流程。

2.2 MountService接收訊息

當Vold在處理完完MountService傳送過來的訊息後,會通過sendGenericOkFail傳送應答訊息給上層的MountService。

2.2.1 響應碼

[-> CommandListener.cpp]

int CommandListener::sendGenericOkFail(SocketClient *cli, int cond) {
    if (!cond) {
        //【見小節2.2.2】
        return cli->sendMsg(ResponseCode::CommandOkay, "Command succeeded", false);
    } else {
        return cli->sendMsg(ResponseCode::OperationFailed, "Command failed", false);
    }
}
  • 當執行成功,則傳送響應碼為500的成功應答訊息;
  • 當執行失敗,則傳送響應碼為400的失敗應答訊息。

不同的響應碼(VoldResponseCode),代表著系統不同的處理結果,主要分為下面幾大類:

響應碼 事件類別 對應方法
[100, 200) 部分響應,隨後繼續產生事件 isClassContinue
[200, 300) 成功響應 isClassOk
[400, 500) 遠端服務端錯誤 isClassServerError
[500, 600) 本地客戶端錯誤 isClassClientError
[600, 700) 遠端Vold程式自觸發的事件 isClassUnsolicited

例如當操作執行成功,VoldConnector執行緒能收到類似`RCV <- {200 3 Command succeeded}的響應事件。

其中對於[600,700)響應碼是由Vold程式”不請自來”的事件,主要是針對disk,volume的一系列操作,比如裝置建立,狀態、路徑改變,以及檔案型別、uid、標籤改變等事件都是底層直接觸發。

命令 響應嗎
DISK_CREATED 640
DISK_SIZE_CHANGED 641
DISK_LABEL_CHANGED 642
DISK_SCANNED 643
DISK_SYS_PATH_CHANGED 644
DISK_DESTROYED 649
VOLUME_CREATED 650
VOLUME_STATE_CHANGED 651
VOLUME_FS_TYPE_CHANGED 652
VOLUME_FS_UUID_CHANGED 653
VOLUME_FS_LABEL_CHANGED 654
VOLUME_PATH_CHANGED 655
VOLUME_INTERNAL_PATH_CHANGED 656
VOLUME_DESTROYED 659
MOVE_STATUS 660
BENCHMARK_RESULT 661
TRIM_RESULT 662

介紹完響應碼,接著繼續來說說傳送應答訊息的過程:

2.2.2 SC.sendMsg

[-> SocketClient.cpp]

int SocketClient::sendMsg(int code, const char *msg, bool addErrno) {
    return sendMsg(code, msg, addErrno, mUseCmdNum);
}

sendMsg經過層層呼叫,進入sendDataLockedv方法

int SocketClient::sendDataLockedv(struct iovec *iov, int iovcnt) {
    ...
    struct sigaction new_action, old_action;
    memset(&new_action, 0, sizeof(new_action));
    new_action.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &new_action, &old_action);

    //將應答訊息寫入socket管道
    for (;;) {
        ssize_t rc = TEMP_FAILURE_RETRY(
            writev(mSocket, iov + current, iovcnt - current));

        if (rc > 0) {
            size_t written = rc;
            while ((current < iovcnt) && (written >= iov[current].iov_len)) {
                written -= iov[current].iov_len;
                current++;
            }
            if (current == iovcnt) {
                break;
            }
            iov[current].iov_base = (char *)iov[current].iov_base + written;
            iov[current].iov_len -= written;
            continue;
        }
        ...
        break;
    }

    sigaction(SIGPIPE, &old_action, &new_action);
    ...
    return ret;
}

2.2.3 NDC.listenToSocket

應答訊息寫入socket管道後,在MountService的另個執行緒”VoldConnector”中建立了名為vold的socket的客戶端,通過迴圈方式不斷監聽Vold服務端傳送過來的訊息。

[-> NativeDaemonConnector.java]

private void listenToSocket() throws IOException {
    LocalSocket socket = null;
    try {
        socket = new LocalSocket();
        LocalSocketAddress address = determineSocketAddress();
        //建立與"/dev/socket/vold"的socket連線
        socket.connect(address);
        InputStream inputStream = socket.getInputStream();
        synchronized (mDaemonLock) {
            mOutputStream = socket.getOutputStream();
        }
        ...
        while (true) {
            int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
            ...
            for (int i = 0; i < count; i++) {
                if (buffer[i] == 0) {
                    final String rawEvent = new String(
                            buffer, start, i - start, StandardCharsets.UTF_8);
                    //解析socket服務端傳送的event
                    final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(
                            rawEvent);
                    log("RCV <- {" + event + "}");

                    if (event.isClassUnsolicited()) {
                        ...
                        //當響應碼區間為[600,700),則傳送訊息交由mCallbackHandler處理
                        if (mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(
                                event.getCode(), event.getRawEvent()))) {
                            releaseWl = false;
                        }
                    } else {
                        //對於其他響應碼則新增到mResponseQueue佇列
                        mResponseQueue.add(event.getCmdNumber(), event);
                    }
                }
            }
        }
    } finally {
        //收尾清理類工作
        ...
    }
}

監聽也是阻塞的過程,當收到不同的訊息相應碼,採用不同的行為:

  • 當響應嗎不屬於區間[600,700):則將該事件新增到mResponseQueue,並且觸發響應事件所對應的請求事件不再阻塞到ResponseQueue.poll,那麼執行緒繼續往下執行,即前面小節[2.1.2] NDC.execute的過程。
  • 當響應碼區間為[600,700):則傳送訊息交由mCallbackHandler處理,向執行緒android.fg傳送Handler訊息,該執行緒收到後回撥NativeDaemonConnector的handleMessage來處理。

2.2.4 小節

volume_reset

2.3 Kernel上報事件

介紹完MonutService與vold之間的互動通訊,那麼再來看看Kernel是如何上報事件到vold的流程。再介紹這個之前,先簡單看看vold啟動時都建立了哪些物件。

[-> system/vold/Main.cpp]

int main(int argc, char** argv) {
    setenv("ANDROID_LOG_TAGS", "*:v", 1);
    android::base::InitLogging(argv, android::base::LogdLogger(android::base::SYSTEM));

    VolumeManager *vm;
    CommandListener *cl;
    CryptCommandListener *ccl;
    NetlinkManager *nm;

    mkdir("/dev/block/vold", 0755);

    //用於cryptfs檢查,並mount加密的檔案系統
    klog_set_level(6);

    //建立單例物件VolumeManager
    if (!(vm = VolumeManager::Instance())) {
        exit(1);
    }

    //建立單例物件NetlinkManager
    if (!(nm = NetlinkManager::Instance())) {
        exit(1);
    }

    if (property_get_bool("vold.debug", false)) {
        vm->setDebug(true);
    }

    // 建立CommandListener物件
    cl = new CommandListener();
    // 建立CryptCommandListener物件
    ccl = new CryptCommandListener();

    //給vm設定socket監聽物件
    vm->setBroadcaster((SocketListener *) cl);
    //給nm設定socket監聽物件
    nm->setBroadcaster((SocketListener *) cl);

    if (vm->start()) { //啟動vm
        exit(1);
    }

    process_config(vm); //解析config引數

    if (nm->start()) {  //啟動nm
        exit(1);
    }

    coldboot("/sys/block");

    //啟動響應命令的監聽器
    if (cl->startListener()) {
        exit(1);
    }

    if (ccl->startListener()) {
        exit(1);
    }

    //Vold成為監聽執行緒
    while(1) {
        sleep(1000);
    }

    exit(0);
}

該方法的主要功能是建立並啟動:VolumeManager,NetlinkManager ,NetlinkHandler,CommandListener,CryptCommandListener。

Kernel上報事件給使用者空間採用了Netlink方式,Netlink是一種特殊的socket,它是Linux所特有的。傳送的訊息是暫存在socket接收快取中,並不被接收者立即處理,所以netlink是一種非同步通訊機制。而對於syscall和ioctl則都是同步通訊機制。

Linux系統中大量採用Netlink機制來進行使用者空間程式與kernel的通訊。例如裝置熱外掛,這會產生Uevent(User Space event,使用者空間事件)是Linux系統中使用者空間與核心空間之間通訊的訊息內容,主要用於裝置驅動的事件通知。Uevent是Kobject的一部分,當Kobject狀態改變時通知使用者空間程式。對於kobject_action包括KOBJ_ADD,KOBJ_REMOVE,KOBJ_CHANGE,KOBJ_MOVE,KOBJ_ONLINE,KOBJ_OFFLINE,當傳送任何一種action都會引發Kernel傳送Uevent訊息。

vold早已準備就緒等待著Kernel上報Uevent事件,接下來看看vold是如何接收Uevent事件,這就從NetlinkManager啟動開始說起。

2.3.2 NM.start

[-> NetlinkManager.java]

int NetlinkManager::start() {
    struct sockaddr_nl nladdr;
    int sz = 64 * 1024;
    int on = 1;

    memset(&nladdr, 0, sizeof(nladdr));
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_pid = getpid(); //記錄當前程式的pid
    nladdr.nl_groups = 0xffffffff;

    //PF_NETLINK代表建立的是Netlink通訊的socket
    if ((mSock = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC,
            NETLINK_KOBJECT_UEVENT)) < 0) {
        return -1;
    }

    //設定uevent的SO_RCVBUFFORCE選項
    if (setsockopt(mSock, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)) < 0) {
        goto out;
    }

    //設定uevent的SO_PASSCRED選項
    if (setsockopt(mSock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) {
        goto out;
    }
    //繫結uevent socket
    if (bind(mSock, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) {
        goto out;
    }

    //建立NetlinkHandler
    mHandler = new NetlinkHandler(mSock);
    //啟動NetlinkHandler
    if (mHandler->start()) {
        goto out;
    }
    return 0;

out:
    close(mSock);
    return -1;
}

NetlinkManager啟動的過程中,會建立並啟動NetlinkHandler,在該過程會通過pthrea_create建立子執行緒專門用於接收Kernel傳送過程的Uevent事件,當收到資料時會呼叫NetlinkListener的onDataAvailable方法。

2.3.3 NL.onDataAvailable

[-> NetlinkListener.cpp]

bool NetlinkListener::onDataAvailable(SocketClient *cli)
{
    int socket = cli->getSocket();
    ...
    //多次嘗試獲取socket資料
    count = TEMP_FAILURE_RETRY(uevent_kernel_recv(socket,
            mBuffer, sizeof(mBuffer), require_group, &uid));
    ...

    NetlinkEvent *evt = new NetlinkEvent();
    //解析訊息並封裝成NetlinkEvent
    if (evt->decode(mBuffer, count, mFormat)) {
        //事件處理【見小節2.3.4】
        onEvent(evt);
    } else if (mFormat != NETLINK_FORMAT_BINARY) {
        ...
    }

    delete evt;
    return true;
}

2.3.4 NH.onEvent

[-> NetlinkHandler.cpp]

void NetlinkHandler::onEvent(NetlinkEvent *evt) {
    VolumeManager *vm = VolumeManager::Instance();
    const char *subsys = evt->getSubsystem();

    if (!strcmp(subsys, "block")) {
        //對於塊裝置的處理過程
        vm->handleBlockEvent(evt);
    }
}

驅動裝置分為字元裝置、塊裝置、網路裝置。對於字元裝置按照字元流的方式被有序訪問,字元裝置也稱為裸裝置,可以直接讀取物理磁碟,不經過系統快取,例如鍵盤直接產生中斷。而塊裝置是指系統中能夠隨機(不需要按順序)訪問固定大小資料片(chunks)的裝置,例如硬碟;塊裝置則是通過系統快取進行讀取。

2.3.5 VM.handleBlockEvent

[-> VolumeManager.cpp]

void VolumeManager::handleBlockEvent(NetlinkEvent *evt) {
    std::lock_guard<std::mutex> lock(mLock);

    std::string eventPath(evt->findParam("DEVPATH")?evt->findParam("DEVPATH"):"");
    std::string devType(evt->findParam("DEVTYPE")?evt->findParam("DEVTYPE"):"");
    if (devType != "disk") return;

    int major = atoi(evt->findParam("MAJOR"));
    int minor = atoi(evt->findParam("MINOR"));
    dev_t device = makedev(major, minor);

    switch (evt->getAction()) {
    case NetlinkEvent::Action::kAdd: {
        for (auto source : mDiskSources) {
            if (source->matches(eventPath)) {
                int flags = source->getFlags();
                if (major == kMajorBlockMmc) {
                    flags |= android::vold::Disk::Flags::kSd;
                } else {
                    flags |= android::vold::Disk::Flags::kUsb;
                }

                auto disk = new android::vold::Disk(eventPath, device,
                        source->getNickname(), flags);
                //建立
                disk->create();
                mDisks.push_back(std::shared_ptr<android::vold::Disk>(disk));
                break;
            }
        }
        break;
    }
    case NetlinkEvent::Action::kChange: {
        ...
        break;
    }
    case NetlinkEvent::Action::kRemove: {
        ...
        break;
    }
    ...
    }
}

2.3.6 小節

此處,我們以裝置插入為例,來描繪一下整個流程圖:

kernel_process

2.4 不請自來的廣播

執行緒VoldConnector通過socket不斷監聽來自vold傳送過來的響應訊息:

  • 情況一:響應碼不屬於區間[600, 700),則直接將響應訊息新增到響應佇列ResponseQueue,當響應佇列有資料到來,便會喚醒另個執行緒MountService阻塞操作poll輪詢操作。
  • 情況二:響應碼屬於區間[600, 700),則便是Unsolicited broadcasts,即不請自來的廣播,當收到這類事件,則處理流程較第一種情況更復雜。

接下來說說第二種情況,對於不清自來的廣播,這裡的廣播並非四大元件的廣播,而是vold通過socket傳送過來的訊息。還記得還文章的開頭講到程式架構時,提到會涉及system_server的執行緒android.fg,那麼這個過程就會講到該執行緒的作用。回到NDC的監聽socket過程。

2.4.1 NDC.listenToSocket

[-> NativeDaemonConnector.java]

private void listenToSocket() throws IOException {
    LocalSocket socket = null;
    try {
        socket = new LocalSocket();
        LocalSocketAddress address = determineSocketAddress();
        //建立與"/dev/socket/vold"的socket連線
        socket.connect(address);
        InputStream inputStream = socket.getInputStream();
        synchronized (mDaemonLock) {
            mOutputStream = socket.getOutputStream();
        }
        ...
        while (true) {
            int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
            ...
            for (int i = 0; i < count; i++) {
                if (buffer[i] == 0) {
                    final String rawEvent = new String(
                            buffer, start, i - start, StandardCharsets.UTF_8);
                    //解析socket服務端傳送的event
                    final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(
                            rawEvent);
                    log("RCV <- {" + event + "}");

                    if (event.isClassUnsolicited()) {
                        ...
                        //當響應碼區間為[600,700),則傳送訊息交由mCallbackHandler處理【2.4.2】
                        if (mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(
                                event.getCode(), event.getRawEvent()))) {
                            releaseWl = false;
                        }
                    } else {
                        //對於其他響應碼則新增到mResponseQueue佇列
                        mResponseQueue.add(event.getCmdNumber(), event);
                    }
                }
            }
        }
    } finally {
        //收尾清理類工作
        ...
    }
}

通過handler訊息機制,由mCallbackHandler處理,先來看看其初始化過程:

mCallbackHandler = new Handler(mLooper, this);
Looper=`FgThread.get().getLooper();

可以看出Looper採用的是執行緒android.fg的Looper,訊息回撥處理方法為NativeDaemonConnector的handleMessage來處理。那麼這個過程就等價於向執行緒android.fg傳送Handler訊息,該執行緒收到訊息後回撥NativeDaemonConnector的handleMessage來處理。

2.4.2 NDC.handleMessage

[-> NativeDaemonConnector.java]

public boolean handleMessage(Message msg) {
    String event = (String) msg.obj;
    ...
    mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))
            log(String.format("Unhandled event '%s'", event));
    ...
    return true;
}

此處的mCallbacks,是由例項化NativeDaemonConnector物件時傳遞進來的,在這裡是指MountService。轉了一圈,又回到MountService。

2.4.3 MS.onEvent

[-> MountService.java]

public boolean onEvent(int code, String raw, String[] cooked) {
    synchronized (mLock) {
        return onEventLocked(code, raw, cooked);
    }
}

onEventLocked增加同步鎖,用於多執行緒併發訪問的控制。根據vold傳送過來的不同響應碼將採取不同的處理流程。

2.4.4 MS.onEventLocked

這裡以收到vold傳送過來的RCV <- {650 public ...}為例,即掛載外接sdcard/otg外接儲存的流程:

[-> MountService.java]

private boolean onEventLocked(int code, String raw, String[] cooked) {
    switch (code) {
        case VoldResponseCode.VOLUME_CREATED: {
            final String id = cooked[1];
            final int type = Integer.parseInt(cooked[2]);
            final String diskId = TextUtils.nullIfEmpty(cooked[3]);
            final String partGuid = TextUtils.nullIfEmpty(cooked[4]);

            final DiskInfo disk = mDisks.get(diskId);
            final VolumeInfo vol = new VolumeInfo(id, type, disk, partGuid);
            mVolumes.put(id, vol);
            //【見小節2.4.5】
            onVolumeCreatedLocked(vol);
            break;
        }
        ...
    }
    return true;
}

2.4.5 MS.onVolumeCreatedLocked

[-> MountService.java]

private void onVolumeCreatedLocked(VolumeInfo vol) {
    if (vol.type == VolumeInfo.TYPE_EMULATED) {
        ...

    } else if (vol.type == VolumeInfo.TYPE_PUBLIC) {
        if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
                && vol.disk.isDefaultPrimary()) {
            vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
            vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
        }

        if (vol.disk.isAdoptable()) {
            vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
        }

        vol.mountUserId = UserHandle.USER_OWNER;
        //【見小節2.4.6】
        mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();

    }
}

這裡又遇到一個Handler型別的物件mHandler,再來看看其定義:

private static final String TAG = "MountService";
HandlerThread hthread = new HandlerThread(TAG);
hthread.start();
mHandler = new MountServiceHandler(hthread.getLooper());

該Handler用到Looper便是執行緒MountService中的Looper,回撥方法handleMessage位於MountServiceHandler類:

2.4.6 MSH.handleMessage

[-> MountService]

class MountServiceHandler extends Handler {
    public void handleMessage(Message msg) {
       switch (msg.what) {
           case H_VOLUME_MOUNT: {
               final VolumeInfo vol = (VolumeInfo) msg.obj;
               try {
                   //傳送mount操作
                   mConnector.execute("volume", "mount", vol.id, vol.mountFlags,
                           vol.mountUserId);
               } catch (NativeDaemonConnectorException ignored) {
               }
               break;
            }
            ...
        }
    }
}

當收到H_VOLUME_MOUNT訊息後,執行緒MountService便開始向vold傳送mount操作事件,再接下來的流程在前面小節【2.1】已經介紹過

2.4.7 小結

unsolicited_broadcasts

三、總結

3.1 概括

本文首先從模組化和程式的視角來整體上描述了Android儲存系統的架構,並分別展開對MountService, vold, kernel這三者之間的通訊流程的剖析。

{1}Java framework層:採用 1個主執行緒(system_server) + 3個子執行緒(VoldConnector, MountService, CryptdConnector);MountService執行緒不斷向vold下發儲存相關的命令,比如mount, mkdirs等操作;而執行緒VoldConnector一直處於等待接收vold傳送過來的應答事件;CryptdConnector通訊原理和VoldConnector大抵相同,有興趣地讀者可自行閱讀。

(2)Native層:採用 1個主執行緒(/system/bin/vold) + 3個子執行緒(vold) + 1子程式(/system/bin/sdcard);vold程式中會通過pthread_create方式來生成3個vold子執行緒,其中兩個vold執行緒分別跟上層system_server程式中的執行緒VoldConnector和CryptdConnector通訊,第3個vold執行緒用於與kernel進行netlink方式通訊。

本文更多的是以系統的角度來分析儲存系統,那麼對於app來說,那麼地方會直接用到的呢?其實用到的地方很多,例如儲存裝置掛載成功會傳送廣播讓app知曉當前儲存掛載情況;其次當app需要建立目錄時,比如getExternalFilesDirsgetExternalCacheDirs等當目錄不存在時都需向儲存系統發出mkdirs的命令。另外,MountService作為Binder服務端,那自然而然會有Binder客戶端,那就是StorageManager,這個比較簡單就不再細說了。

3.2 架構的思考

以Google原生的Android儲存系統的架構設計主要採用Socket阻塞式通訊方式,雖然vold的native層面有多個子執行緒幹活,但各司其職,真正處理上層傳送過來的命令,仍然是單通道的模式。

目前外接儲存裝置比如sdcard或者otg的硬體質量參差不齊,且隨使用時間碎片化程度也越來越嚴重,對於儲存裝置掛載的過程中往往會有磁碟檢測fsck_msdos或者整理fstrim的動作,那麼勢必會阻塞多執行緒併發訪問,影響系統穩定性,從而造成系統ANR。

例如系統剛啟動過程中reset操作需要重新掛載外接儲存裝置,而緊接著system_server主執行緒需要執行的volume user_started操作便會被阻塞,阻塞超過20s則系統會丟擲Service Timeout的ANR。

相關文章