Android 存儲系統解析之架構篇(上)
基於 Android 6.0 的源碼,剖析存儲架構的設計
引言:筆者最近遇到不少存儲相關 bug 導致手機發生 ANR,經過分析後,瓶頸主要在於主線程被 blocked 在 vold 進程. 大抵地一看 MountService/vold/kernel 的整個交互過程涉及多線程通信,IO 相關的操作都是在子線程完成,不應該阻塞 system_server 主線程啊?既然在子線程的工作都阻塞主線程, 那該如何修復這個 bug?爲了回答上面這些疑問,深入研究 Android 的整個存儲系統的架構設計。
一、概述
本文講述 Android 存儲系統的架構與設計,基於 Android 6.0 的源碼,涉及到最爲核心的便是 MountService 和 Vold 這兩個模塊以及之間的交互。爲了縮減篇幅,只展示部分核心代碼。
MountService:Android Binder 服務端,運行在 system_server 進程,用於跟 Vold 進行消息通信,比如MountService
向Vold
發送掛載 SD 卡的命令, 或者接收到來自Vold
的外設熱插拔事件。MountService 作爲 Binder 服務端,那麼相應的 Binder 客戶端便是 StorageManager,通過 binder IPC 與 MountService 交互。
Vold:全稱爲 Volume Daemon,用於管理外部存儲設備的 Native daemon 進程,這是一個非常重要的守護進程,主要由 NetlinkManager,VolumeManager,CommandListener 這 3 部分組成。
1.1 模塊架構
從模塊地角度劃分 Android 整個存儲架構:
圖解:
-
Linux Kernel:通過
uevent
向 Vold 的 NetlinkManager 發送 Uevent 事件; -
NetlinkManager:接收來自 Kernel 的
Uevent
事件,再轉發給 VolumeManager; -
VolumeManager:接收來自 NetlinkManager 的事件,再轉發給 CommandListener 進行處理;
-
CommandListener:接收來自 VolumeManager 的事件,通過
socket
通信方式發送給 MountService; -
MountService:接收來自 CommandListener 的事件。
1.2 進程架構
(1) 先看看 Java framework 層的線程:
MountService 運行在 system_server 進程,這裏查詢的便是 system_server 進程的所有子線程,system_server 進程承載整個 framework 所有核心服務,子線程數有很多,這裏只列舉與 MountService 模塊相關的子線程。
(2) 再看看 Native 層的線程:
Vold 作爲 native 守護進程,進程名爲 "/system/bin/vold",pid=387,通過ps -t
可查詢到該進程下所有的子進程 / 線程。
小技巧:有讀者可能會好奇,爲什麼/system/bin/sdcard
是子進程,而非子線程呢?要回答這個問題,有兩個方法,其一就是直接看擼源碼,會發現這是通過fork
方式創建的,而其他子線程都是通過pthread_create
方式創建的。當然其實還有個更快捷的小技巧,就是直接看上圖中的第 4 列,這一列的含義是VSIZE
,代表的是進程虛擬地址空間大小,是否共享地址空間,這是進程與線程最大的區別,再來看看 / sdcard 的 VSIZE 大小跟父進程不一樣,基本可以確實 / sdcard 是子進程。
(3) 從進程 / 線程視角來看 Android 存儲架構:
-
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.fg
和android.io
。
Tips: 同一個模塊可以運行在各個不同的進程 / 線程, 同一個進程可以運行不同模塊的代碼, 所以從進程角度和模塊角度劃分看到的有所不同的.
爲了闡述清楚存儲系統的通信架構,主要分爲以下 4 個過程:
-
MountService 發送消息:MountService 是如何從向 vold 守護進程通信;
-
MountService 接收消息:MountService 接收到 vold 發送過來的消息又是如何處理;
-
Kernel 上報事件:當存儲設備發生熱插拔等事件,kernel 是如何通知用戶空間的 vold;
-
不請自來的廣播:對於事件往往都是 MountService 下發,然後再收到底層的迴應,但對於有些廣播卻非如此,而是由底層直接觸發,對於 MountService 來說卻是 “不請自來” 的消息。
限於篇幅過長,本文先講述前兩個過程,下一篇文章再來說說後兩個過程。
1.3 類關係圖
上圖中 4 個藍色塊便是前面談到的核心模塊。
二、 通信架構
Android 存儲系統中涉及各個進程間通信,這個架構採用的 socket,並沒有採用 Android binder IPC 機制。這樣的架構代碼大量更少,整體架構邏輯也相對簡單,在介紹通信過程前,先來看看 MountService 對象的實例化過程,那麼也就基本明白進程架構中 system_sever 進程爲了 MountService 服務而單獨創建與共享使用到線程情況。
首先,MountService 對象實例化的過程中完成是:
-
創建 ICallbacks 回調方法, FgThread 線程名爲 "android.fg",此處用到的 Looper 便是線程 "android.fg" 中的 Looper;
-
創建並啓動線程名爲 "MountService" 的 handlerThread;
-
創建 OBB 操作的 handler,IoThread 線程名爲 "android.io",此處用到的的 Looper 便是線程 "android.io" 中的 Looper;
-
創建 NativeDaemonConnector 對象
-
創建並啓動線程名爲 "VoldConnector" 的線程;
-
創建並啓動線程名爲 "CryptdConnector" 的線程;
-
註冊監聽用戶添加、刪除的廣播;
從這裏便可知道共創建了 3 個線程:MountService
,VoldConnector
,CryptdConnector
,另外還會使用到系統進程中的兩個線程android.fg
和android.io
. 這便是在文章開頭進程架構圖中 Java framework 層進程的創建情況.
2.1 MountService 發送消息
system_server 進程與 vold 守護進程間採用 socket 進行通信,這個通信過程是由 MountService 線程向 vold 線程發送消息。這裏以執行 mount 調用爲例:
2.1.1 MountService.mount
public void mount(String volId) {
//【見小節 2.1.2】
mConnector.execute("volume", "mount", vol.id, vol.mountFlags, vol.mountUserId);
}
2.1.2 NDC.execute
execute() 經過層層調用到 executeForList()
-
首先,將帶執行的命令 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() 方法
2.1.4 FL.dispatchCommand
這是用於分發從 MountService 發送過來的命令,針對不同的命令調用不同的類。在處理過程中遇到下面情況,則會直接發送響應嗎 500 的應答消息給 MountService
-
當無法找到匹配的類,則會直接向 MountService 返回響應碼 500,內容 "Command not recognized" 的應答消息;
-
命令參數過長導致 socket 管道溢出,則會發送響應碼 500,內容 "Command too long" 的應答消息。
2.1.5 CL.runCommand
例如前面發送過來的是volume mount
,則會調用到 CommandListener 的內部類 VolumeCmd 的 runCommand 來處理該消息,並進入 mount 分支。
2.1.6 小節
MountService 向 vold 發送消息後,便阻塞在圖中的 MountService 線程的 NDC.execute()方法,那麼何時纔會退出呢?圖的後半段 MonutService 接收消息的過程會有答案,那便是在收到消息,並且消息的響應嗎不屬於區間 [600,700) 則添加事件到 ResponseQueue,從而喚醒阻塞的 MountService 繼續執行。關於上圖的後半段介紹的便是 MountService 接收消息的流程。
2.2 MountService 接收消息
當 Vold 在處理完完 MountService 發送過來的消息後,會通過 sendGenericOkFail 發送應答消息給上層的 MountService。
2.2.1 響應碼
-
當執行成功,則發送響應碼爲 500 的成功應答消息;
-
當執行失敗,則發送響應碼爲 400 的失敗應答消息。
不同的響應碼 (VoldResponseCode),代表着系統不同的處理結果,主要分爲下面幾大類:
例如當操作執行成功,VoldConnector 線程能收到類似 `RCV <- {200 3 Command succeeded}的響應事件。其中對於 [600,700) 響應碼是由 Vold 進程 "不請自來" 的事件,主要是針對 disk,volume 的一系列操作,比如設備創建,狀態、路徑改變,以及文件類型、uid、標籤改變等事件都是底層直接觸發,後面再會詳細講。介紹完響應碼,接着繼續來說說發送應答消息的過程:
2.2.2 SC.sendMsg
sendMsg 經過層層調用,進入 sendDataLockedv 方法
2.2.3 NDC.listenToSocket
應答消息寫入 socket 管道後,在 MountService 的另個線程 "VoldConnector" 中建立了名爲vold
的 socket 的客戶端,通過循環方式不斷監聽 Vold 服務端發送過來的消息。
監聽也是阻塞的過程,當收到不同的消息相應碼,採用不同的行爲:
-
當響應嗎不屬於區間 [600,700):則將該事件添加到 mResponseQueue,並且觸發響應事件所對應的請求事件不再阻塞到 ResponseQueue.poll,那麼線程繼續往下執行,即前面小節 [2.1.2] NDC.execute 的過程。
-
當響應碼區間爲 [600,700):則發送消息交由 mCallbackHandler 處理,向線程
android.fg
發送 Handler 消息,該線程收到後回調 NativeDaemonConnector 的handleMessage
來處理。
2.2.4 小節
三、總結
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 需要創建目錄時,比如getExternalFilesDirs
,getExternalCacheDirs
等當目錄不存在時都需向存儲系統發出 mkdirs 的命令。另外,MountService 作爲 Binder 服務端,那自然而然會有 Binder 客戶端,那就是StorageManager
,這個比較簡單就不再細說了,歡迎大家與 Gityuan。
3.2 架構的思考
以 Google 原生的 Android 存儲系統的架構設計主要採用 Socket 阻塞式通信方式,雖然 vold 的 native 層面有多個子線程幹活,但各司其職,真正處理上層發送過來的命令,仍然是單通道的模式。
目前外置存儲設備比如 sdcard 或者 otg 的硬件質量參差不齊,且隨使用時間碎片化程度也越來越嚴重,對於存儲設備掛載的過程中往往會有磁盤檢測 fsck_msdos 或者整理 fstrim 的動作,那麼勢必會阻塞多線程併發訪問,影響系統穩定性,從而造成系統 ANR。
例如系統剛啓動過程中 reset 操作需要重新掛載外置存儲設備,而緊接着 system_server 主線程需要執行的 volume user_started 操作便會被阻塞,阻塞超過 20s 則系統會拋出 Service Timeout 的 ANR。
最後,本文主要詳細闡述 MountService 與 vold 之間的通信過程,關注《架構師》,繼續瞭解下一篇文章繼續說說 vold 與 kernel 是如何通信,底層是如何主動觸發事件通知 MountService.
作者:袁輝輝
來源:http://gityuan.com/2016/07/23/android-io-arch/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/T2IuW-9oUtAR4GQJXIwrXw