換個角度看init (Android P 9.0)
換個角度看init
作者: WertherZhang
主頁: ofollow,noindex">https://wertherzhang.com
連結: https://www.jianshu.com/p/220cab3652d1
來源: 簡書
轉載請註明來源
本篇雖然歸屬於跟我讀原始碼系列, 但實際是作者嘗試從非原始碼的角度來進入init, 過程中作者會擷取儘量少的原始碼來輔助理解(所有的程式碼連結指向 http://androidxref.com/9.0.0_r3/ ).
所有的UNIX系統都有一個特殊的程序, 它是核心啟動完畢後, 使用者態中啟動的第一個程序, 它負責啟動系統, 病作為系統中其他程序的祖先程序. 傳統上, 這個程序被稱為init. Android 也沿用了這個約定俗成的規矩.
不過Android中的init和UNIX或Linux中的init還是很大區別的, 其中最重要的不同之處在於, Android中的init支援系統屬性, 病使用一些指定的rc檔案(來規定它的行為). 在介紹這兩個特性之後, 我們將拼湊出一副init執行流程的全景圖:它的初始化過程和主迴圈(run-loop)中要做的操作.
另外, init還扮演著其他角色 -- 它還要花生為ueventd, watchdogd 和 subcontext. 這三個重要的核心服務也是由init這個二進位制可執行檔案來實現的 -- 通過符號連結的方式載入.
init的角色和任務
和大多數unix核心一樣, linux核心會去尋找一個路徑和檔名預先規定好的二進位制可執行檔案, 並把它作為第一個使用者態程序執行. 桌面linux系統中, 這個可執行檔案一般是 /sbin/init, 它會去讀取 /etc/inittab 檔案的內容, 以獲取所支援的 "執行級別"(run-levels), 執行時配置資訊(單使用者,多使用者,網路檔案系統等), 需隨機啟動的程序以及當用戶按下Ctrl+Alt+Del 組合鍵時該做出何種反應等資訊. Android 也使用這樣一個init程式, 但是Android中的init程式和傳統linux中的init程式的相似之處也就僅止於這個檔名了, 它們之間的區別如下表 所示
Linux的/sbin/init | Android的 init | |
---|---|---|
配置檔案 | /etc/inittab | ro.boot.init_rc 指向的檔案或者 /init.rc , /system/etc/init,/product/etc/init, /odm/etc/init,/vendor/etc/init 中指向的所有.rc 檔案以及匯入的檔案 |
多種配置 | 支援"執行級別"概念, 每個執行級別都會從 /etc/底下載入不同的指令碼 |
沒有執行級概念, 但是有觸發器(trigger)和系統屬性提供了配置選項 |
看門狗 | 支援通過respawn關鍵字定義過的守護程序會在退出時重啟 -- 除非該程序反覆崩潰, 在這種情況下, 反覆崩潰的程序會被掛起幾分鐘 | 服務預設是自動重啟 除非啟動指令碼中顯示使用了 oneshot 引數. 服務還可以使用critical 引數, 這會使系統在該服務連續崩潰時重啟系統. |
收容孤兒程序 | 支援: init會呼叫wait4()系統呼叫獲取返回碼, 並避免出現殭屍程序 | 支援: init註冊了一個SIGCHLD訊號處理函式. SIGCHLD 訊號是在子程序退出後由核心自動傳送的.大多數程序會默默第呼叫wait(NULL)清理掉已退出程序, 而不去管退出碼是什麼 |
系統屬性 | 不支援 | init 通過共享一塊記憶體區域的方式, 讓系統中的所有程序都能讀取系統屬性(通過 getprop), 病通過一個名為 "property_service" 的 socket 讓有許可權的程序能夠(通過 setprop) 寫相關的屬性 |
分配socket | 不支援 | init 會繫結一個UNIX domain socket (支援dgram, stream 和 seqpacket)提供給子程序, 子程序可以通過 android_get_control_socket 函式獲取到它 |
觸發操作 | 不支援: linux只支援非常特殊的觸發操作, 比如 ctrl+alt+del 和 UPS 電源事件, 但是不允許任意的出發操作 | 支援: init 可以在任何一個系統屬性被修改時, 執行記錄在trigger語句塊中的指令 |
uevent事件 | linux依靠的是hotplug守護程序(通常為 udevd) | init 會化身為 ueventd, 用專門的配置檔案來指導其行為. |
init 是靜態連結的二進位制可執行檔案(可通過file命令檢視). 也就是說在編譯時, 它的所有依賴庫都已經被合併到這個二進位制可執行檔案中了. 這樣做是為了防止僅僅因為缺少某個庫或者某個庫被破壞而造成系統無法啟動的情況發生. /init 在剛被執行時, 只有和核心一起被打包放在boot分割槽上的RAM disk(單分割槽, AB分割槽是system, 此處不討論)被mount了上來, 換句話說, 系統中只有 / 和 /sbin, 沒有動態庫.
系統屬性
Android 的系統屬性提供了一個可全域性訪問的配置設定倉庫(類似windows登錄檔). 與init相關的原始碼中的 property_service.c 中的程式碼, 或按下表中給出的順序, 從多個檔案中載入屬性.
檔案 | 載入時機 | 內容 |
---|---|---|
$device_tree | process_kernel_dt | device tree 中定義的property, 全部轉換為 ro.boot. 字首的property |
cmdline | process_kernel_cmdline | cmdline中定義的property, 全部轉換為 ro.boot. 字首的property |
/system/etc/prop.default 或者 /default.prop , /product/build.prop, /odm/default.prop 和 /vendor/default.prop | 在init的函式中載入.除了從根讀取, 其餘分割槽必須在device-tree中定義並在 DoFirstStageMount 階段被掛載, 否則property載入失敗. | 初始設定. |
/system/build.prop, /odm/build.prop, /vendor/build.prop, factory/factory.prop | init.rc 中 post-fs 階段 | 除了factory.prop 是隻載入ro字首, 其餘全載入 |
/data/property/persist.* | late-init 或者 data解密完成 | 重啟後不會丟失的property, 必須加上字首persist |
注意: 各個檔案的載入順序是非常重要的, ro字首的property早載入的生效, 而非ro字首的property晚載入的生效.
因為init程序是系統中所有程序的祖先, 所以只有它才天生適合實現系統屬性的初始化. 在它剛開始初始化的時候, init中的程式碼會呼叫 property_init() 去安裝系統屬性. 這個函式(最終)會呼叫 map_prop_area_rw() , 開啟 PROP_FILENAME
( 這個定義是 /dev/__properties__
) 底下的所有檔案, 在關閉檔案之前, 呼叫mmap(2) 以讀寫的方式對映到記憶體. 而需要讀的程序, 則是呼叫__system_properties_init, 在 bionic 中被呼叫(). 下面是模擬器中的輸出.

1537789826206.png

1537789889464.png
上面是基於共享記憶體實現了讀操作, 下面我看下寫操作. init 專門打開了一個專用的unix domain socket -- /dev/socket/property_service. 只要能連上這個socket, 任何人都能對它進行寫操作(0666, u:object_r:property_socket:s0). 這些寫入的命令會直接送給init, init會先檢查 socket 呼叫者有沒有設定屬性的許可權(selinux/uid/gid), selinux的相關許可權在 /property_contexts 中.

1538381595267.png
下面我們看下通過socket執行proeprty set 的strace輸出, 其中涉及通訊協議部分不過多介紹.

1538382197761.png
最終寫入的檔案如下:

1538382637099.png
上面我們看到, persist屬性是寫入 /data/property, 這也是為什麼persist屬性在重啟後依然生效. persist 這個關鍵字, 我們可以稱其為偽字首, 真正property的字首是 sys 這個關鍵字, 除了sys, 還有usb, radio等字首關鍵字, 用來區分功能和許可權的(比如前圖的selinux許可權). 具體的字首關鍵字此處不細說, 讀者可自行閱讀程式碼研究, 下面介紹下幾個偽字首.
- persist 字首. 儲存在 /data/property 目錄, 重啟後依然生效.在data解密後或者late-init階段載入.
- ro 字首. 只讀屬性, 類似C/CPP中的常量定義, 它能且只能被設定1次, 這類屬性一般儘可能早設定. 依據前文的載入順序, 在有兩個相同的ro字首property時, 前面載入的生效, 而非ro字首的property, 後面載入的生效.
- ctl 字首. 這是方便控制init中的服務而設立的 -- 通過把相關服務的名字設定為 ctl.start 和 ctl.stop的值, 就能方便地啟動和停止指定服務. (命令列中的start和stop實際上就是通過該property來設定.) 這種能力是受selinux控制的具體的許可權定義在, 有興趣可以看下, 所有的property相關許可權定義都在這個檔案中.
這就是為啥前面說的, persist屬性的property必須在data分割槽解密完成後才能載入. 同樣帶來的一個問題是, core類別的服務, 最好不要使用persist屬性控制其邏輯, 如果真有需要, 得考慮data解密的問題和property重新載入更新的情況.
下面, 我們依然用strace, 來跟一下 ctl.start 的strace

1538384398111.png
rc 檔案
init的主要操作是載入它的配置檔案, 並執行配置檔案中的相關命令. 傳統上的主要配置檔案 /init.rc . 但是在 最新版本的系統上, 其配置檔案的路徑還包含 /system/etc/init , /vendor/etc/init 和 /odm/etc/init, 配置檔案模組化了, 不同服務有單獨的rc配置檔案.也就是說, 前面的三個路徑是init中寫死的匯入路徑, 其餘的rc檔案可以通過關鍵字import匯入.

1538460582540.png
從上面的截圖, 我們可以看到, 有引數有兩個物件, 分別是 ActionManager 和 ServiceList, 分別對應init需要執行的命令列表和管理的服務列表.
import trigger(action) and service
這裡我們將trigger和action合併的原因是, trigger會觸發action, 本質相同, 而且從程式碼層面看, rc檔案就是分為三大語句 塊, import, trigger 和 service.

1538460863080.png
import 關鍵字修飾的就是import語句塊, 其作用是匯入其他rc並解析執行.
on 關鍵字修飾的就是trigger語句塊, 後面跟著一個引數 -- 這個引數既可以是預先規定的各個啟動階段(boot stage) 的名稱, 也可以是property的關鍵字, 後面跟冒號加 "屬性名稱=屬性值"這樣的表示式(這種情況的意思是, 如果property被設定為當前值, 則觸發trigger定義的action). 預先定義的啟動階段如下, 此處只給出AOSP的標準流程, 各個廠商可能會改動. 後面會提供方法檢視改動後的流程.
service 關鍵字, 定義的是由init啟動的各個程序服務名字, init會根據相關的命令啟動相關服務程序, 也可以根據引數(OPTIONS) 修改服務的狀態. 前文實驗的bootanim服務, 就是這裡定義的.
下面我們對以上三個section, 簡單展開介紹下, 基本上了解了這三個, rc檔案的結構就瞭解了.
我們先看一下, import關鍵字解析rc檔案的順序.

1538534200435.png
從上面程式碼看, 每解析完成一個rc檔案, 就會按照import順序解析其import進來的rc檔案. 下面看個例子
# A.rc import b.rc import d.rc # b.rc import c.rc
其真實的載入順序是, a.rc -> b.rc -> c.rc -> d.rc
我們來看下init的README.md中關於import順序的虛擬碼:
fn Import(file) Parse(file) for (import : file.imports) Import(import) Import(/init.rc) Directories = [/system/etc/init, /vendor/etc/init, /odm/etc/init] for (directory : Directories) files = <Alphabetical order of directory's contents> for (file : files) Import(file)
下面看下 on 關鍵字, 也就是trigger. 首先看下aosp定義的啟動階段(基本的), 除了charger的階段, 其餘階段存在先後順序:
啟動階段 | 內容 |
---|---|
early-init | 初始化的第一個階段,用於設定selinux 和 OOM |
init | 建立檔案系統, mount節點以及寫核心變數 |
late-init | 觸發各種trigger |
early-fs | 檔案系統準備被mount前需要完成的工作 |
fs | 專門用於載入各個分割槽 |
post-fs | 在各個檔案系統(data除外)mount完畢後執行的命令 |
post-fs-data | 解密/data分割槽(如果需要), 並掛載 |
early-boot | 在屬性服務(property service)初始化之後, 啟動剩餘內容之前需要完成的工作 |
boot | 正常的啟動命令 |
charger | 當手機處於充電模式時(關機情況下充電), 需要執行的命令 |
我們單獨把 late-init 拎出來看下, 有興趣的可以更深入看下每個trigger所執行的程式碼.

1538536421223.png
相信如果看過rc檔案, 都會知道每個rc檔案中都有trigger, 那麼同名trigger的執行先後順序是如何的呢? 下面給個結論, 和不需要看程式碼就知道順序的方法.
結論: 按照rc檔案的載入順序, 執行同名trigger底下的命令.
檢視方法: 通過命令 dmesg -w 監控啟動流程, 然後我們過濾關鍵字 "processing action", 這個關鍵字即展示了各個trigger(啟動階段)的先後順序, 也展示了各個rc檔案中同名trigger的執行順序.
下面看下 proeprty的trigger, 一個典型的例子就是 usb config, 語法很簡單, 冒號後面的property同時為true, 觸發執行命令.

1538537060554.png
看到上面的例子, 讀者一定很想問, 既然有 &&
操作符, 是否有 ||
操作符呢? 很抱歉, 沒有呢.

1538537397986.png
關於trigger下面掛的action (命令集), 我們在後文再看, 我們先把最後一個語句塊 service 過一遍.
還是以 bootanim 為例子:
# /system/etc/init/bootanim.rc service bootanim /system/bin/bootanimation class core animation user graphics group graphics audio disabled oneshot writepid /dev/stune/top-app/tasks
其格式如下:
service namebinary_path options
其中 name, 就是 init 維護的服務列表, 可以通過 start 和 stop 啟動和停止服務.
binary_path 就是真實的可執行檔案路徑.
optinos 主要是 class, user, group 等關鍵字, 會在後文對這些關鍵字做介紹.
所有 定義了的 service, 都會被init管理, 也就是說, init會監聽其退出的訊息, 並根據options, 執行對應的異常處理邏輯, 此處邏輯不細講, 讀者有興趣可以自行閱讀程式碼.
trigger 命令集
trigger 命令集也叫init命令集, 其實就是init.rc中所有可執行命令. 我們重點關注下, 定義了哪些命令, 並且命令是如何被使用的.
命令列表定義在BuiltinFunctionMap::map, 下面看個例子, 具體命令列表大家自己跳轉到原始碼:

1538539567761.png
如果想追尋各個引數的含義, 可以檢視程式碼和.
後面如果遇到對應的命令, 會進行簡單的介紹. 具體各個命令的用法, 請參考init的 README.md.
service 的 option 集
option 合集定義在Service::OptionParserMap::map, 我們重點關注下幾乎所有service都會用到的option:
- class . 該service所屬的類別, 字串, 如果未設定, 則預設為 default. 可通過
class_start
來啟動某個類別的服務. - user. 指定該程序執行時所屬使用者
- group. 指定該程序執行時所屬的組.
- critical. 指定該程序屬於critical型別, 該型別的服務連續4次崩潰會導致系統重啟.
- disabled. 指定該服務預設不啟動. 如果不設定該屬性, 服務都會預設啟動. 原理是, 前文的命令集中有命令
class_start
, 通過class_start core
,class_start main
等方法啟動服務, 而命令無法啟動 disabled 服務, 這是該命令區別於普通的start
命令的地方. - oneshot. 指定該服務如果退出後不自動重啟(觸發位置為子程序退出的訊號處理函式). 其他情況下(比如 start 或者 class_start 命令), 依然可以再次啟動該服務.
- onrestart. 該flag表示如果服務重啟, 則重啟onrestart後面的服務. 但是通過
{class_}stop
或者{class_}reset
命令導致的重啟, 不會重啟onrestart定義的相關服務.

1538546376921.png
額外介紹下 shutdown
選項, 目前只有引數 critical, 且如果被置上該選項, 在關機重啟的流程中, 最後被結束. 按理說, servicemanager 重啟, 所有註冊的binder服務都需要重啟, 但是在模擬器上測試發現, 重啟 servicemanager, 系統就無法啟動了. 應該是跟部分native程序通訊方式從socket切換到binder有關.
組合鍵 keychords

1538548688289.png
keychords 也是 service 的option 欄位, 這裡單獨拎出來講的原因是, 這是類似後門一樣的神奇功能, 它允許使用者在按下某幾個組合鍵時, 啟動某些服務. "組合鍵" 當前被定義為在的情況下同時按下某些按鍵. 每個按鍵在linux的輸入子系統上都有對應的掃描碼, 此處 114, 115, 116 就對應的 VOLUME_DOWN, VOLUME_UP 和 POWER 鍵的掃描碼(掃描碼和framework KEYCODE對應表).
要支援組合鍵, 則必須存在 /dev/keychord , 且adbd程序正在執行. 如果需要可以通過改程式碼, 強制該後門全域性啟用.
mount 檔案系統
檔案系統掛載順序
本文忽略AB系統, 如果有興趣瞭解, 請閱讀作者的另一篇文章, 關於AB升級和AB分割槽.
我們先來看一下模擬器中的分割槽掛載.

1538552703362.png
mount 命令實際讀取的是 /proc/mounts
system 分割槽之前的掛載, 在init的第一階段執行, 不詳細說明了.
而system/vendor/data分割槽的掛載, 由於階段的不同, 掛載資訊的來源不同, 此處僅針對模擬器中的情況來進行秒數, 讀者可根據相關程式碼在實際的裝置中嘗試研究.
system 和 vendor 的分割槽掛載資訊儲存在 /sys/bus/platform/devices/ANDR0001:00/properties/android/fstab .

1538553017765.png
從上圖可以看到, 掛載的裝置路徑, 資訊和檔案系統型別.
那麼這些資訊的路徑資料從哪裡來的?

1538553102865.png
準確地說, 是傳遞給kernel的引數中, 帶了該資訊學.
generic_x86_64:/ # cat /proc/cmdline qemu=1 androidboot.hardware=ranchu clocksource=pit android.qemud=1 console=0 android.checkjni=1 qemu.gles=1 qemu.encrypt=1 qemu.opengles.version=196608 cma=262M androidboot.android_dt_dir=/sys/bus/platform/devices/ANDR0001:00/properties/android/ ramoops.mem_address=0xff018000 ramoops.mem_size=0x10000 memmap=0x10000$0xff018000
也就是說 system 和 vendor 是在時掛載, 也是最早掛載的.
cache 和 data 分割槽是定義在 fstab.{hardware} 中

1538553420032.png
此處忽略帶有voldmanaged關鍵字, 因為該關鍵字是指這條掛載資訊由vold管理並掛載, 而不包含該關鍵字的都是由init管理並掛載.
那麼fstab是什麼時候被解析和掛載的呢? 在 init.{hardware}.rc 中 on fs 階段的命令掛載的.
如果data分割槽塊加密了呢? 剛開機的時候, 會先輸入密碼, 然後再出現開機動畫, 最後出來真實系統. 也就是說, 如果data分割槽加密了, data分割槽就是最後被掛載的. 我們在下節介紹該過程中, 分割槽掛載的一些變化.
為什麼無法掛著data會導致安全啟動
這個問題, 其實也是觸發安全啟動的條件.我們先來看下輸密碼介面的掛載資訊.

1538554098516.png
關注下data分割槽, 當前是掛載成tmpfs, 也就是記憶體檔案系統. 換言之, 安全啟動過程中, 無法掛載data分割槽, 然後掛載了記憶體分割槽, 用於啟動一個小的ui系統輸入密碼. 那麼什麼條件下切換到安全啟動介面呢?正常情況下, 在檢查到fstab中有加密選項, 並且分區別檢測到加密, 就會進入安全啟動邏輯. 但是還有一種異常情況, fstab中有可加密選項, 但data掛載失敗.

1538554481738.png
此處, 如果data分割槽無法被掛載, 並且fstab中有標記data分割槽是可以被加密的, 那麼就會掛載tmpfs, 並嘗試解密處理.
解密和掛載data分割槽的流程在cryptfs_restart_internal, 其中, 先停止main型別的服務, 然後掛載data分割槽, 最後再重新啟動main型別的服務, 全程通過property vold.decrypt
來控制服務的啟動和配置. 我們簡單看下property的變化.
在解密分割槽前, 設定property為 trigger_reset_main

1538555188795.png
在解密掛載data分割槽

1538555207271.png
在解密掛載完成後, 設定property

1538555271446.png
這裡可以對應上前文的data分割槽掛載後, init程式會載入 persist 屬性的property.
感興趣的同學可自行閱讀下該property的變化和 rc檔案中相關的trigger, 結合前文來回答下面的問題:
- 在掛載data分割槽之前是如何關閉所有需要用到data分割槽的服務的?
- 解密掛載分割槽後, 又是如何重啟服務的?
- 是會重啟所有的main型別服務嗎? 閱讀rc檔案, 找出你覺得在解密後無法被重啟的服務(假設data解密前已經啟動)
- onrestart 標記的服務是否會被觸發重啟?
總結
作為大多數守護程序的樣板, init程式碼的執行流程完全遵循建立服務的經典模式: 初始化, 然後陷入一個迴圈中, 而且永遠不退出.
- 檢查自身的可執行程式是不是被當初ueventd, watchdogd 或者 subcontext, 如果是, 則轉到對應守護程序的主迴圈函式, 後續程式碼流程不執行
- 建立/dev, /proc, /sys 目錄, 並掛載
- 掛載裝置樹中定義的fstab資訊, 也就是前文提到的, 掛載system分割槽和vendor分割槽
- selinux初始化
- 建立 /dev/.booting 檔案, 在啟動完畢後, 該檔案會被刪除(在 firmware_mounts_complete, 也就是在 early-boot之前)
- property_init . 初始化 property service
- 將 dt 和 kernel cmdline的資訊匯出到proeprty, 規則是將所有的 androidboot.xxxx 匯出成 ro.boot.xxx
- 再次初始化 selinux
- 初始化訊號處理函式, 用於處理子程序退出訊號
- 載入 一堆 default.prop 檔案
- 解析所有rc檔案
- 將early-init的trigger新增到trigger佇列中
- 將 init 的trigger 新增到 trigger佇列中
- 根據 ro.boot.mode (androidboot.mode) 來判斷是不是處於關機充電模式, 如果是, 則不掛載檔案系統且只啟動charger服務.
- 進入主迴圈開始執行trigger的命令
下面看下主迴圈, 會迴圈依次執行下面的程式碼
- 執行HandlePowerctlMessage, 處理 property sys.powerctl, 比如shutdown 或者 reboot
- am.ExecuteOneCommand() , 處理前文新增的trigger的action
- RestartProcesses() , 檢查所有已經註冊過的服務, 必要時重啟服務
- 輪詢如下三個fd:
- property_set_fd : 也就是前文的 /dev/socket/proeprty_service, 這個socket是用來讓想要設定某個屬性的客戶端連線併發送對應的key/value 給 init 程序.
- keychord_fd : 用來處理啟動服務的組合鍵
- signal_read_fd: 它是socketpair的一端, 另一端在訊號處理函式裡, 在收到子程序退出訊號後, 寫入一端, 則epoll等待就會返回處理子程序退出邏輯.
init 的其他角色
多個角色共用一套程式碼, 利用的是軟連結的機制, 跟busybox一樣, 通過 argv[0] 可以獲取到執行的檔名字(也就是軟連結的檔名), 從而知道需要執行的模組程式碼.
ueventd
作為ueventd執行時, init這個程式是用來管理硬體裝置的. 它需要響應核心的通知(netlink), 管理/sys偽檔案系統中與各個裝置對應的檔案並負責讓各個程序通過它在/dev/底下建立符號連結指向到這些檔案. 為了完成這些操作, uevnetd使用另外的初始化指令碼 uevnetd.rc 和 ueventd.{hardware}.rc .這些配置檔案比init的配置檔案簡單多了, 只記錄了哪些檔案被配置成哪些許可權. ueventd 會逐條處理rc檔案, 並呼叫. uevent, 還負責處理fireware的載入, 在kernel 傳送 add 訊息後, uevent 呼叫確認子系統是 firmware, 載入資料併發送.

1538567103901.png
watchdogd
和ueventd一樣, watchdogd也是init的另一面. 這時, 它被用作硬體watchdog定時器(timer) (/dev/watchdog,如果有的話)在使用者態中的介面. 設定一個超時((timeout)變數, 每隔一段時 間就傳送一個keepalive訊號(一個""位元組)。如果超過了超時變數規定的時間,watchdogd 還沒能及時傳送keepalive訊號,硬體watchdog定時器就會發出一箇中斷,要求核心重啟。盡 管有那麼點極端的感覺,但我們要知道唯一能讓watchdogd不能及時傳送keepalive訊號的原因 只有系統已經掛機(hang)了―這時系統很可能已經無法從掛機的狀態恢復過來了,重啟裝置反而算是個比較簡單的解決方案. 作為watchdogd時,這個守護程序只接受兩個命令列引數:interval和margin―這兩個參 數的初始值都是10,單位是“秒”。裝置的總超時時間((timeout變數的值)是這兩個引數之和 (即,預設值為20秒).不過在超時沒有接收到資訊後,系統並不會馬上重啟,而是會再稍微等 上一小會(給系統最後一次補救的機會).
init subcontext
該模組是在android P(9.0) 才引入的, 是為了完全隔離 system 和 vendor. 相信使用P的童鞋應該都有經歷過, 某系property 無法設定或者無法讀取了 (selinux許可權限制), 這些都是這個模組乾的.

1538578048683.png
紅色框內的就是該模組.
從上圖看, 該模組是由init建立的, 我們來看下該模組是如何建立的和它的引數含義.
在init的main函式中, 有如下的程式碼, 用於初始化 subcontext
subcontexts = InitializeSubcontexts();
該函式實現如下:

1538576209865.png
依據下面的路徑和資訊, 建立對應的 subcontexts, 此處會建立2個subcontexts程序, 分別用於處理 vendor 下的rc指令碼和 odm 下的rc指令碼. 其主要原理就是, 如果rc檔案是在vendor/odm底下, 且命令需要subcontexts, 就將命令轉交給 對應的 subcontexts 模組處理.

1538576247139.png
我們看下subcontexts的Fork程式碼.

1538576396389.png
通過socketpair, 建立init和 subcontexts 模組間的通訊.
下面我們看使用的地方, 按照我們的理解, 是要完全隔離system和vendor的許可權, 那麼, 主要動的是trigger.

1538576540373.png
這裡是建立parser的地方, 我們看到, 主要動的其實是trigger和 service.
我們先看下 service , 主要控制的是哪些許可權.

1538576693615.png
控制service重啟時, 所需要執行的命令的許可權.
我們再看看 ActionParser.

1538577672410.png
最終與service一樣, 如果是在vendor/odm 底下的rc中的action, 都會被指定 action_subcontext .
我們看了subcontexts 的啟動, 和 區分是普通init還是subcontexts執行的標準後, 我們看下, 一個vendor底下的action, 如何被執行的.

1538577857424.png
呼叫subcontexts 的 Execute 函式, 其實就是通過socketpair, 通知 另一個程序幫忙執行, 然後將結果返回.
而 execute_in_subcontext_
該變數就是前文 [trigger 命令集] 中引數的第三列, true 為該命令需要通過subcontext執行, false 為不用.


這就是為啥 剛從 O 升級 到 P後, property 包括 rc中的一些其他命令, 會出現selinux許可權問題
type=1400 audit(1511821362.996:9): avc: denied { search } for pid=540 comm="init" name="nfc" dev="sda45" ino=1310721 scontext=u:r:vendor_init:s0 tcontext=u:object_r:nfc_data_file:s0 tclass=dir permissive=0 init: Command 'write /data/nfc/bad_file_access 1234' action=boot (/vendor/etc/init/hw/init.walleye.rc:422) took 2ms and failed: Unable to write to file '/data/nfc/bad_file_access': open() failed: Permission denied