十一假期有點墮落,無限火力有點上癮,謹戒、謹戒
Init行程是Linux 內核啟動后創建的第一個用戶行程,地位非常重要,
Init行程在初始化程序中會啟動很多重要的守護行程,因此,了解Init行程的啟動程序有助于我們更好的理解Android系統,
在介紹Init行程前,我們先簡單介紹下Android的啟動程序,從系統角度看,Android的啟動程序可分為3個大的階段:
bootloader引導裝載和啟動Linux內核啟動Android系統,可分為- 啟動
Init行程 - 啟動
Zygote - 啟動
SystemService - 啟動
SystemServer - 啟動
Home - 等等…
- 啟動
我們看下啟動程序圖:

下面簡單介紹下啟動程序:
Bootloader引導
當按下電源鍵開機時,最先運行的就是
Bootloader
Bootloader的主要作用是初始化基本的硬體設備(如 CPU、記憶體、Flash等)并且建立記憶體空間映射,為裝載Linux內核準備好合適的運行環境,- 一旦
Linux內核裝載完畢,Bootloader將會從記憶體中清除掉 - 如果在
Bootloader運行期間,按下預定義的的組合鍵,可以進入系統的更新模塊,Android的下載更新可以選擇進入Fastboot模式或者Recovery模式:Fastboot是Android設計的一套通過USB來更新Android磁區映像的協議,方便開發人員快速更新指定磁區,Recovery是Android特有的升級系統,利用Recovery模式可以進行恢復出廠設定,或者執行OTA、補丁和韌體升級,進入Recovery模式實際上是啟動了一個文本模式的Linux
- 裝載和啟動
Linux內核
Android 的
boot.img存放的就是Linux內核和一個根檔案系統
Bootloader會把boot.img映像裝載進記憶體- 然后
Linux內核會執行整個系統的初始化 - 然后裝載
根檔案系統 - 最后啟動
Init行程
- 啟動
Init行程
Linux內核加載完畢后,會首先啟動Init行程,Init行程是系統的第一個行程
Init行程啟動程序中,會決議Linux的配置腳本init.rc檔案,根據init.rc檔案的內容,Init行程會:- 裝載
Android的檔案系統 - 創建系統目錄
- 初始化屬性系統
- 啟動
Android系統重要的守護行程,像USB守護行程、adb守護行程、vold守護行程、rild守護行程等
- 裝載
- 最后,
Init行程也會作為守護行程來執行修改屬性請求,重啟崩潰的行程等操作
- 啟動
ServiceManager
ServiceManager由Init行程啟動,在Binder章節已經講過,它的主要作用是管理Binder服務,負責Binder服務的注冊與查找
- 啟動
Zygote行程
Init行程初始化結束時,會啟動Zygote行程,Zygote行程負責fork出應用行程,是所有應用行程的父行程
Zygote行程初始化時會創建Android 虛擬機、預裝載系統的資源檔案和Java類- 所有從
Zygote行程fork出的用戶行程都將繼承和共享這些預加載的資源,不用浪費時間重新加載,加快的應用程式的啟動程序 - 啟動結束后,
Zygote行程也將變為守護行程,負責回應啟動APK的請求
- 啟動
SystemServer
SystemServer是Zygote行程fork出的第一個行程,也是整個Android系統的核心行程
SystemServer中運行著Android系統大部分的Binder服務SystemServer首先啟動本地服務SensorManager- 接著啟動包括
ActivityManagerService、WindowsManagerService、PackageManagerService在內的所有Java服務
- 啟動
MediaServer
MediaServer由Init行程啟動,它包含了一些多媒體相關的本地Binder服務,包括:CameraService、AudioFlingerService、MediaPlayerService、AudioPolicyService
- 啟動
Launcher
SystemServer加載完所有的Java服務后,最后會呼叫ActivityManagerService的SystemReady()方法- 在
SystemReady()方法中,會發出Intent<android.intent,category.HOME> - 凡是回應這個
Intent的apk都會運行起來,一般Launcher應用才回去回應這個Intent
Init行程的初始化程序
Init行程的原始碼目錄在system/core/init下,程式的入口函式main()位于檔案init.c中
main()函式的流程
書中使用的是
Android 5.0原始碼,相比Android 9.0這部分已經有很多改動,不過大的方向是一致的,只能對比著學習了,,,
main()函式比較長,整個Init行程的啟動流程都在這個函式中,由于涉及的點比較多,這里我們先了解整體流程,細節后面補充,一點一點來哈
Init行程的main()函式的結構是這樣的:
int main(int argc, char** argv) {
//啟動引數判斷部分
if (is_first_stage) {
//初始化第一階段部分
}
//初始化第二階段部分
while (true) {
//一個無限回圈部分
}
啟動程式引數判斷
進入main()函式后,首先檢查啟動程式的檔案名
函式原始碼:
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) {
return watchdogd_main(argc, argv);
}
- 如果檔案名是
ueventd,執行ueventd守護行程的主函式ueventd_main - 如果檔案名是
watchdogd,執行watchdogd守護行程的主函式watchdogd_main - 都不是,則繼續執行
才開始是不是就已經有些奇怪了,Init行程中還包含了另外兩個守護行程的啟動,這主要是因為這幾個守護行程的代碼重合度高,開發人員干脆都放在一起了,
我們看一下Android.mk中的片段:
# Create symlinks.
LOCAL_POST_INSTALL_CMD := $(hide) mkdir -p $(TARGET_ROOT_OUT)/sbin; \
ln -sf ../init $(TARGET_ROOT_OUT)/sbin/ueventd; \
ln -sf ../init $(TARGET_ROOT_OUT)/sbin/watchdogd
- 在編譯時,
Android生成了兩個指向init檔案的符號鏈接ueventd和watchdogd - 這樣,啟動時如果執行的是這兩個符號鏈接,
main()函式就可以根據名稱判斷到底啟動哪一個
初始化的第一階段
設定檔案屬性掩碼
函式原始碼:
// Clear the umask.
umask(0);
默認情況下一個行程創建出的檔案合檔案夾的屬性是022,使用umask(0)意味著行程創建的屬性是0777
mount相應的檔案系統
函式原始碼:
// Get the basic filesystem setup we need put together in the initramdisk
// on / and then we'll let the rc file figure out the rest.
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
//......
mount("sysfs", "/sys", "sysfs", 0, NULL);
mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
//......
// Mount staging areas for devices managed by vold
// See storage config details at http://source.android.com/devices/storage/
mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=1000");
// /mnt/vendor is used to mount vendor-specific partitions that can not be
// part of the vendor partition, e.g. because they are mounted read-write.
mkdir("/mnt/vendor", 0755);
InitKernelLogging(argv);
創建一些基本的目錄,包括/dev、/proc、/sys等,同時把一些檔案系統,如tmpfs、devpt、proc、sysfs等mount到相應的目錄
tmpfs是一種基于記憶體的檔案系統,mount后就可以使用,tmpfs檔案系統下的檔案都放在記憶體中,訪問速度快,但是掉電丟失,因此適合存放一些臨時性的檔案tmpfs檔案系統的大小是動態變化的,剛開始占用空間很小,隨著檔案的增多會隨之變大Android將tmpfs檔案系統mount到/dev目錄,/dev目錄用來存放系統創造的設備節點
devpts是虛擬終端檔案系統,通常mount到/dev/pts目錄下proc也是一種基于記憶體的虛擬檔案系統,它可以看作是內核內部資料結構的介面- 通過它可以獲得系統的資訊
- 同時能夠在運行時修改特定的內核引數
sysfs檔案系統和proc檔案系統類似,它是在Linux 2.6內核引入的,作用是把系統設備和總線按層次組織起來,使得他們可以在用戶空間存取
初始化kernel的Log系統
通過InitKernelLogging()函式進行初始化,由于此時Android的log系統還沒有啟動,所以Init只能使用kernel的log系統
// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
// talk to the outside world...
InitKernelLogging(argv);
初始化SELinux
// Set up SELinux, loading the SELinux policy.
SelinuxSetupKernelLogging();
SelinuxInitialize();
SELinux是在Android 4.3加入的安全內核,后面詳細介紹
初始化的第二階段
創建.booting空檔案
在/dev目錄下創建一個空檔案.booting表示初始化正在進行
// At this point we're in the second stage of init.
InitKernelLogging(argv);
LOG(INFO) << "init second stage started!";
//......
// Indicate that booting is in progress to background fw loaders, etc.
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
大家留心注釋,我們已經處于初始化的第二階段了
is_booting()函式會依靠空檔案.booting來判斷是否行程處于初始化中- 初始化結束后這個檔案將被洗掉
初始化Android的屬性系統
property_init();
property_init()函式主要作用是創建一個共享區域來儲存屬性值,后面會詳細介紹
決議kernel引數并進行相關設定
// If arguments are passed both on the command line and in DT,
// properties set in DT always have priority over the command-line ones.
process_kernel_dt();
process_kernel_cmdline();
// Propagate the kernel variables to internal variables
// used by init as well as the current required properties.
export_kernel_boot_props();
// Make the time that init started available for bootstat to log.
property_set("ro.boottime.init", getenv("INIT_STARTED_AT"));
property_set("ro.boottime.init.selinux", getenv("INIT_SELINUX_TOOK"));
// Set libavb version for Framework-only OTA match in Treble build.
const char* avb_version = getenv("INIT_AVB_VERSION");
if (avb_version) property_set("ro.boot.avb_version", avb_version);
這部分進行的是屬性的設定,我們看下幾個重點方法:
process_kernel_dt()函式:讀取設備樹(DT)上的屬性設定資訊,查找系統屬性,然后通過property_set設定系統屬性process_kernel_cmdline()函式:決議kernel的cmdline檔案提取以androidboot.字串打頭的字串,通過property_set設定該系統屬性export_kernel_boot_props()函式:額外設定一些屬性,這個函式中定義了一個集合,集合中定義的屬性都會從kernel中讀取并記錄下來
進行第二階段的SELinux設定
進行第二階段的SELinux設定并恢復一些檔案安全背景關系
// Now set up SELinux for second stage.
SelinuxSetupKernelLogging();
SelabelInitialize();
SelinuxRestoreContext();
初始化子行程終止信號處理函式
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
PLOG(FATAL) << "epoll_create1 failed";
}
sigchld_handler_init();
在
linux當中,父行程是通過捕捉SIGCHLD信號來得知子行程運行結束的情況,SIGCHLD信號會在子行程終止的時候發出,
- 為了防止
init的子行程成為僵尸行程(zombie process),init在子行程結束時獲取子行程的結束碼 - 通過結束碼將程式表中的子行程移除,防止成為僵尸行程的子行程占用程式表的空間
- 程式表的空間達到上限時,系統就不能再啟動新的行程了,這樣會引起嚴重的系統問題
設定系統屬性并開啟屬性服務
property_load_boot_defaults();
export_oem_lock_status();
start_property_service();
set_usb_controller();
property_load_boot_defaults()、export_oem_lock_status()、set_usb_controller()這三個函式都是呼叫、設定一些系統屬性start_property_service():開啟系統屬性服務
加載init.rc檔案
init.rc是一個可配置的初始化檔案,在Android中被用作程式的啟動腳本,它是run commands運行命令的縮寫
通常第三方定制廠商可以配置額外的初始化配置:
init.%PRODUCT%.rc,在init的初始化程序中會決議該組態檔,完成定制化的配置程序,
init.rc檔案的規則和具體決議邏輯后面詳解,先看下它在main函式中的相關流程,
函式代碼:
const BuiltinFunctionMap function_map;
Action::set_function_map(&function_map);
subcontexts = InitializeSubcontexts();
// 創建 action 相關物件
ActionManager& am = ActionManager::GetInstance();
// 創建 service 相關物件
ServiceList& sm = ServiceList::GetInstance();
// 加載并決議init.rc檔案到對應的物件中
LoadBootScripts(am, sm);
決議完成后,會執行
am.QueueEventTrigger("early-init");
// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
// 等待冷插拔設備初始化完成
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
// ... so that we can start queuing up actions that require stuff from /dev.
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
// 初始化組合鍵監聽模塊
am.QueueBuiltinAction(keychord_init_action, "keychord_init");
// 在螢屏上顯示 Android 字樣的Logo
am.QueueBuiltinAction(console_init_action, "console_init");
// Trigger all the boot actions to get us started.
am.QueueEventTrigger("init");
// Starting the BoringSSL self test, for NIAP certification compliance.
am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");
// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
// wasn't ready immediately after wait_for_coldboot_done
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
// Don't mount filesystems or start core system services in charger mode.
std::string bootmode = GetProperty("ro.bootmode", "");
if (bootmode == "charger") {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}
// Run all property triggers based on current state of the properties.
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");
am.QueueEventTrigger函式即表明到達了某個所需的某個時間條件- 如
am.QueueEventTrigger("early-init")表明early-init條件觸發,對應的動作可以開始執行 - 需要注意的是這個函式只是將時間點(如:
early-init)填充進event_queue_運行佇列 - 后面的
while(true)回圈才會真正的去按順序取出,并觸發相應的操作
- 如
到這里,
init.rc相關的action和service已經決議完成- 對應的串列也已經準備就緒
- 對應的
Trigger也已經添加完成
接下來就是執行階段了:
while (true) {
// 執行命令串列中的 Action
if (!(waiting_for_prop || Service::is_exec_service_running())) {
am.ExecuteOneCommand();
}
if (!(waiting_for_prop || Service::is_exec_service_running())) {
if (!shutting_down) {
// 啟動服務串列中的服務
auto next_process_restart_time = RestartProcesses();
//......
}
//......
}
// 監聽子行程的死亡通知信號
epoll_event ev;
int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));
if (nr == -1) {
PLOG(ERROR) << "epoll_wait failed";
} else if (nr == 1) {
((void (*)()) ev.data.ptr)();
}
}
到這里,main函式的整體流程就分析完了,分析程序中我們舍棄了很多細節,接下來就是填補細節的時候了,
啟動Service行程
在main函式的while()回圈中會呼叫RestartProcesses()來啟動服務串列中的服務行程,我們看下函式原始碼:
static std::optional<boot_clock::time_point> RestartProcesses() {
//......
//回圈檢查每個服務
for (const auto& s : ServiceList::GetInstance()) {
// 判斷標志位是否為 SVC_RESTARTING
if (!(s->flags() & SVC_RESTARTING)) continue;
// ......省略時間相關的判斷
// 啟動服務行程
auto result = s->Start();
}
//......
}
RestartProcesses()會檢查每個服務:凡是帶有SVC_RESTARTING標志的,才會執行服務的啟動s->Start();
其實重點在s->Start();方法,我們具體來看下(刪減版):
Result<Success> Service::Start() {
//......省略部分
// 清空service相關標記
// 判斷service狀態,如果已運行直接回傳
// 判斷service二進制檔案是否存在
// 初始化console、scon(安全背景關系)等
//......省略部分
pid_t pid = -1;
if (namespace_flags_) {// 當service定義了namespace時會賦值為CLONE_NEWPID|CLONE_NEWNS
pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr);
} else {
pid = fork();
}
if (pid == 0) {// 子行程創建成功
//......省略部分
// setenv、writepid、重定向標準IO
//......省略部分
// As requested, set our gid, supplemental gids, uid, context, and
// priority. Aborts on failure.
SetProcessAttributes();
if (!ExpandArgsAndExecv(args_)) {// 決議引數并啟動service
PLOG(ERROR) << "cannot execve('" << args_[0] << "')";
}
// 不太懂這里退出的目的是干啥
_exit(127);
}
if (pid < 0) {
// 子行程創建失敗
pid_ = 0;
return ErrnoError() << "Failed to fork";
}
//......省略部分
// 執行service其他引數的設定,如oom_score_adj、創建并設定ProcessGroup相關的引數
//......省略部分
NotifyStateChange("running");
return Success();
}
Service行程的啟動流程還有很多細節,這部分只是簡單介紹下流程,涉及的scon、PID、SID、PGID等東西還很多,
偷懶啦!能力、時間有限,先往下學習,待用到時再來啃吧
決議啟動腳本init.rc
Init行程啟動時最重要的作業就是決議并執行啟動檔案init.rc,官方說明檔案下載鏈接
init.rc檔案格式
init.rc檔案是以section為單位組織的
-
section分為兩大類:action:以關鍵字on開始,表示一堆命令的集合service:以關鍵字service開始,表示啟動某個行程的方式和引數
-
section以關鍵字on或service開始,直到下一個on或service結束 -
section中的注釋以#開始
打個樣兒:
import /init.usb.rc
import /init.${ro.hardware}.rc
on early-init
mkdir /dev/memcg/system 0550 system system
start ueventd
on init
symlink /system/bin /bin
symlink /system/etc /etc
on nonencrypted
class_start main
class_start late_start
on property:sys.boot_from_charger_mode=1
class_stop charger
trigger late-init
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
service flash_recovery /system/bin/install-recovery.sh
class main
oneshot
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
無論是action還是service,并不是按照檔案中的書寫順序執行的,執行與否以及何時執行要由Init行程在運行時決定,
對于init.rc的action:
- 關鍵字
on后面跟的字串稱為trigger,如上面的early-init、init等, trigger后面是命令串列,命令串列中的每一行就是一條命令,
對于init.rc的service:
- 關鍵字
service后面是服務名稱,可以使用start加服務名稱來啟動一個服務,如start ueventd 服務名稱后面是行程的可執行檔案的路徑和啟動引數service下面的行稱為option,每個option占一行- 例如:
class main中的class表示服務所屬的類別,可以通過class_start來啟動一組服務,像class_start main
- 例如:
想要了解更多,可以參考原始碼中的README檔案,路徑是system/core/init/README.md
init.rc的關鍵字
這部分是對
system/core/init/README.md檔案的整理,挑重點記錄哈
Android的rc腳本包含了4中型別的宣告:Action、Commands、Services、Options
- 所有的指令都以行為單位,各種符號則由空格隔開,
c語言風格的反斜杠\可用于在符號間插入空格- 雙引號
""可用于防止字串被空格分割成多個記號 - 行末的反斜杠
\可用于折行 - 注釋以
#開頭 Action和Services用來申明一個分組- 所有的
Commands和Options都屬于最近宣告的分組 - 位于第一個分組之前的
Commands或Options將被忽略
- 所有的
Actions
Actions是一組Commands的集合,每個Action都有一個trigger用來決定何時執行,當觸發條件與Action的trigger匹配一致時,此Action會被加入到執行佇列的尾部
每個Action都會依次從佇列中取出,此Action的每個Command都將依次執行,
Actions格式如下:
on < trigger >
< command >
< command >
< command >
Services
通過Services定義的程式,會在Init中啟動,如果退出了會被重啟,
Services的格式如下:
service <name> <pathname> [ <argument> ]*
<option>
<option>
...
Options
Options是Services的修訂項,它們決定了一個Service何時以及如何運行,
critical:表示這是一個關鍵的Service,如果Service4分鐘內重新啟動超過4次,系統將自動重啟并進入recovery模式console [<console>]:表示該服務需要一個控制臺- 第二個引數用來指定特定控制臺名稱,默認為
/dev/console - 指定時需省略掉
/dev/部分,如/dev/tty0需寫成console tty0
- 第二個引數用來指定特定控制臺名稱,默認為
disabled:表示服務不會通過class_start啟動,它必須以命令start service_name的方式指定來啟動setenv <name> <value>:在Service啟動時將環境變數name設定為valuesocket <name> <type> <perm> [ <user> [ <group> [ <seclabel> ] ] ]:創建一個名為/dev/socket/<name>的套接字,并把檔案描述符傳遞給要啟動的行程type的值必須是dgram、stream、seqpacketuser和group默認為0seclabel是這個socket的SElinux安全背景關系,默認為當前service的背景關系
user <username>:在執行此服務之前切換用戶名,默認的是root,如果行程沒有相應的權限,則不能使用該命令oneshot:Service退出后不再重啟class <name>:給Service指定一個名字,所有同名字的服務可以同時啟動和停止,如果不通過class顯示指定,默認為defaultonrestart:當Service重啟時,執行一條命令
還有很多哈,就不一一介紹了,像shutdown <shutdown_behavior>這種,參照官方說明就好啦
Triggers
trigger本質上是一個字串,能夠匹配某種包含該字串的事件,trigger又被細分為事件觸發器(event trigger)和屬性觸發器(property trigger)
-
事件觸發器可由trigger命令或初始化程序中通過QueueEventTrigger()觸發- 通常是一些事先定義的簡單字串,例如:
boot,late-init等
- 通常是一些事先定義的簡單字串,例如:
-
屬性觸發器是當指定屬性的變數值變成指定值時觸發- 其格式為
property:<name>=*
- 其格式為
請注意,一個Action可以有多個屬性觸發器,但是最多有一個事件觸發器,看下官方的例子:
on boot && property:a=b
上面的Action只有在boot事件發生時,并且屬性a和數值b相等的情況下才會被觸發,
而對于下面的Action
on property:a=b && property:c=d
存在三種觸發情況:
- 在啟動時,如果
屬性a的值等于b并且屬性c的值等于d - 在
屬性c的值已經是d的情況下,屬性a的值被更新為b - 在
屬性a的值已經是b的情況下,屬性c的值被更新為d
對于事件觸發器,大體包括:
| 型別 | 說明 |
|---|---|
boot | init.rc被裝載后觸發 |
device-added-<path> | 指定設備被添加時觸發 |
device-removed-<path> | 指定設備被移除時觸發 |
service-exited-<name> | 在特定服務退出時觸發 |
early-init | 初始化之前觸發 |
late-init | 初始化之后觸發 |
init | 初始化時觸發 |
Commands
Command是用于Action的命令串列或者Service的Option<onrestart>中,在原始碼中是這樣的:
static const Map builtin_functions = {
{"chmod", {2, 2, {true, do_chmod}}},
{"chown", {2, 3, {true, do_chown}}},
{"class_start", {1, 1, {false, do_class_start}}},
{"class_stop", {1, 1, {false, do_class_stop}}},
......
};
看幾個常用的吧
bootchart [start|stop]:開啟或關閉行程啟動時間記錄工具
//init.rc file
mkdir /data/bootchart 0755 shell shell
bootchart start
- 在
Init行程中會啟動bootchart,默認不會執行時間采集 - 當我們需要采集啟動時間時,需創建一個
/data/bootchart/enabled檔案
chmod <octal-mode> <path>:更改檔案權限
chmode 0755 /metadata/keystone
chown <owner> <group> <path>:更改檔案的所有者和組
chown system system /metadata/keystone
mkdir <path> [mode] [owner] [group]:創建指定目錄
mkdir /data/bootchart 0755 shell shell
trigger <event>:觸發某個事件(Action),用于將該事件排在某個事件之后
on late-init
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs
trigger post-fs-data
trigger zygote-start
trigger load_persist_props_action
trigger early-boot
trigger boot
class_start <serviceclass>:啟動所有指定服務class下的未運行服務
class_start main
class_start late_start
class_stop <serviceclass>:停止所有指定服務class下的已運行服務
class_stop charger
exec [ <seclabel> [ <user> [ <group>\* ] ] ] -- <command> [ <argument>\* ]:通過給定的引數fork和啟動一個命令,
- 具體的命令在
--后開始 - 引數包括
seclable(默認的話使用-)、user、group exec為阻塞式,在當前命令完成前,不會運行其它命令,此時Init行程暫停執行,
exec - system system -- /system/bin/tzdatacheck /system/usr/share/zoneinfo /data/misc/zoneinfo
還有很多指令就不一一介紹了,參考官方檔案和原始碼就好啦
init腳本的決議
上面我們知道了,在init.cpp的main函式中通過LoadBootScripts()來加載rc腳本,我們簡單看下決議流程(注釋比較詳細啦)
/**
* 7.0后,init.rc進行了拆分,每個服務都有自己的rc檔案
* 他們基本上都被加載到/system/etc/init,/vendor/etc/init, /odm/etc/init等目錄
* 等init.rc決議完成后,會來決議這些目錄中的rc檔案,用來執行相關的動作
* ===============================================================================
* 對于自定義的服務來說,我們只需要通過 LOCAL_INIT_RC 指定自己的rc檔案即可
* 編譯時會根據磁區標簽將rc檔案拷貝到指定的partition/etc/init目錄
*/
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
// 創建決議器
Parser parser = CreateParser(action_manager, service_list);
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
// 如果沒有特殊配置ro.boot.init_rc,先決議 init.rc 檔案
parser.ParseConfig("/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
} else {
// 直接決議 ro.boot.init_rc 中的資料
parser.ParseConfig(bootscript);
}
}
/**
* 創建決議器,目前只有三種section:service、on、import
* 與之對應的,函式中也出現了三種決議器:ServiceParser、ActionParser、ImportParser
*/
Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
Parser parser;
parser.AddSectionParser("service", std::make_unique<ServiceParser>(&service_list, subcontexts));
parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, subcontexts));
parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));
return parser;
}
init中啟動的守護行程
在init.rc中定義了很多守護行程,9我們來看下相關內容:
# adb 守護行程放在了這里
import /init.usb.rc
# service adbd /system/bin/adbd --root_seclabel=u:r:su:s0
# class core
# ......
on boot
# Start standard binderized HAL daemons
class_start hal
# 啟動所有class core的服務
# 像adb、console等
class_start core
on eraly-init
start ueventd
on post-fs
# Load properties from
# /system/build.prop,
# /odm/build.prop,
# /vendor/build.prop and
# /factory/factory.prop
load_system_props
# start essential services
# 這幾個 service 的.rc檔案都在對應專案中,通過LOCAL_INIT_RC來指定
start logd
# 啟動三個Binder服務管理相關的service
# servicemanager用于框架/應用行程之間的 IPC,使用 AIDL 介面
start servicemanager
# hwservicemanager用于框架/供應商行程之間的 IPC,使用 HIDL 介面
# 也可用于供應商行程之間的 IPC,使用 HIDL 介面
start hwservicemanager
# vndservicemanager用于供應商/供應商行程之間的 IPC,使用 AIDL 介面
start vndservicemanager
on post-fs-data
# 啟動 vold(Volume守護行程),負責系統擴展儲存的自動掛載
# 這個行程后面詳解
start vold
# 負責回應 uevent 事件,創建對應的設備節點
service ueventd /sbin/ueventd
class core
......
# 包含常用的shell命令,如ls、cd等
service console /system/bin/sh
class core
......
# Zygote 行程在這里匯入,現在支持32位和64位
import /init.${ro.zygote}.rc
# zygote中會啟動一些相關service,像media、netd、wificond等
# service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
# onrestart restart audioserver
# onrestart restart cameraserver
# onrestart restart media
# onrestart restart netd
# onrestart restart wificond
# 啟動 Zygote 行程,觸發這個action的位置在 on late-init 中
on zygote-start && property:ro.crypto.state=unencrypted
start netd
start zygote
start zygote_secondary
啟動流程和需要啟動的service通過init.rc基本上就可以完成定制,
感覺Init行程通過決議*.rc的方式大大簡化了開發,真的是6啊,設計這一套AIL的人是真滴猛,,,,,,,
Init行程對信號的處理
Init行程是系統的一號行程,系統中的其他行程都是Init行程的后代.
按照Linux的設計,Init行程需要在這些后代死亡時負責清理它們,以防止它們變成僵尸行程,
僵尸行程簡介
關于
僵尸行程可以參考Wiki百科-僵尸行程哈
在類UNIX系統中,僵尸行程是指完成執行(通過exit系統呼叫,或運行時發生致命錯誤或收到終止信號所致),但在作業系統的行程表中仍然存在其行程控制塊,處于終止狀態的行程,
這發生于子行程需要保留表項以允許其父行程讀取子行程的退出狀態:一旦退出態通過wait系統呼叫讀取,僵尸行程條目就從行程表中洗掉,稱之為回收(reaped),
正常情況下,行程直接被其父行程wait并由系統回收,
僵尸行程的避免
父行程通過wait和waitpid等函式等待子行程結束,這會導致父行程掛起- 如果
父行程很忙,那么可以用signal函式為SIGCHLD安裝handler,因為子行程結束后,父行程會收到該信號,可以在handler中呼叫wait回收 - 如果
父行程不關心子行程什么時候結束,那么可以用signal(SIGCHLD,SIG_IGN)通知內核,自己對子行程的結束不感興趣,那么子行程結束后,內核會回收,并不再給父行程發送信號 - 還有一些技巧,就是
fork兩次,父行程先fork一個子行程,然后繼續作業,子行程fork一個孫行程后退出,那么孫行程被init接管,孫行程結束后,Init會回收,不過子行程的回收 還要自己做
我們下面來看下Init行程怎么處理的
初始化SIGCHLD信號
Init行程的main函式中,在初始化第二階段,有這么一個方法sigchld_handler_init():
// file : init.cpp
int main(int argc, char** argv) {
......
sigchld_handler_init();
......
}
// file : sigchld_handler.cpp
void sigchld_handler_init() {
// Create a signalling mechanism for SIGCHLD.
// 創建一個socketpair,往一個socket中寫,就可以從另外一個套接字中讀取到資料
int s[2];
socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s);
signal_write_fd = s[0];
signal_read_fd = s[1];
// Write to signal_write_fd if we catch SIGCHLD.
// 信號初始化相關引數設定
// 設定SIGCHLD_handler信號處理函式
// 設定SA_NOCLDSTOP標志
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIGCHLD_handler;
act.sa_flags = SA_NOCLDSTOP;
// 注冊SIGCHLD信號
sigaction(SIGCHLD, &act, 0);
ReapAnyOutstandingChildren();
// 注冊signal_read_fd到epoll_fd
register_epoll_handler(signal_read_fd, handle_signal);
}
// file : sigchld_handler.cpp
/**
* SIGCHLD_handler的作用是當init行程接收到SIGCHLD信號時,往signal_write_fd中寫入資料
* 這個時候套接字對中的另外一個signal_read_fd就可讀了,
*/
static void SIGCHLD_handler(int) {
if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {
PLOG(ERROR) << "write(signal_write_fd) failed";
}
}
// file : sigchld_handler.cpp
/**
* register_epoll_handler函式主要的作用是注冊屬性socket檔案描述符到輪詢描述符epoll_fd
*/
void register_epoll_handler(int fd, void (*fn)()) {
epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = reinterpret_cast<void*>(fn);
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
PLOG(ERROR) << "epoll_ctl failed";
}
}
信號的初始化是通過系統呼叫sigaction()來完成的
- 引數
act中:sa_handler用來指定信號的處理函式sa_flags用來指定觸發標志,SA_NOCLDSTOP標志意味著當子行程終止時才接收SIGCHLD信號
在Linux系統中,信號又稱為軟中斷,信號的到來會中斷行程正在處理的作業,因此在信號處理函式中不要去呼叫一些不可重入的函式,而且Linux不會對信號排隊,不管是在信號的處理期間再來多少個信號,當前的信號處理函式執行完后,內核只會再發送一個信號給行程,因此,為了不丟失信號,我們的信號處理函式執行得越快越好
而對于SIGCHLD信號,父行程需要執行等待操作,這樣的話時間就比較長了,因此需要有辦法解決這個矛盾
- 上面的代碼中創建了一對本地
socket用于行程間通信 - 當信號到來時,
SIGCHLD_handler處理函式只要向socket中的signal_write_fd寫入資料就可以 - 這樣,信號的處理就轉變到了
socket的處理上了
此時,我們需要監聽signal_read_fd,并提供一個回呼函式,這就是register_epoll_handler()函式的作用
- 函式中的
EPOLLIN表示當檔案描述符可讀時才會觸發 *fn就是觸發后的回呼函式指標,賦值給了ev.data.ptr,請留意下這個指標變數,后面會用到- 提供的回呼函式就是
handle_signal()
回應子行程的死亡事件
Init行程啟動完畢后,會監聽創建的socket,如果有資料到來,主執行緒會喚醒并呼叫處理函式handle_signal()
static void handle_signal() {
// Clear outstanding requests.
// 清空 signal_read_fd 中的資料
char buf[32];
read(signal_read_fd, buf, sizeof(buf));
ReapAnyOutstandingChildren();
}
void ReapAnyOutstandingChildren() {
while (ReapOneProcess()) {
}
}
static bool ReapOneProcess() {
siginfo_t siginfo = {};
// This returns a zombie pid or informs us that there are no zombies left to be reaped.
// It does NOT reap the pid; that is done below.
// 查詢是否存在僵尸行程
if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {
PLOG(ERROR) << "waitid failed";
return false;
}
// 沒有僵尸行程直接回傳
auto pid = siginfo.si_pid;
if (pid == 0) return false;
// At this point we know we have a zombie pid, so we use this scopeguard to reap the pid
// whenever the function returns from this point forward.
// We do NOT want to reap the zombie earlier as in Service::Reap(), we kill(-pid, ...) and we
// want the pid to remain valid throughout that (and potentially future) usages.
// 等待子行程終止
auto reaper = make_scope_guard([pid] { TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG)); });
......
if (!service) return true;
// 進行退出的其他操作
service->Reap(siginfo);
......
}
當接收到子行程的SIGCHLD信號后,會找出該行程對應的Service物件,然后呼叫Reap函式,我們看下函式內容:
void Service::Reap(const siginfo_t& siginfo) {
// 如果 不是oneshot 或者 是重啟的子行程
// 殺掉整個行程組,思考了下,先殺掉為重啟做準備吧
// 這樣當重啟的時候,就不會因為子行程已經存在而導致錯誤了
if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {
KillProcessGroup(SIGKILL);
}
// 做一些當前行程的清理作業
......
// Oneshot processes go into the disabled state on exit,
// except when manually restarted.
// 如果 是oneshot 或者 不是重啟的子行程,設定為SVC_DISABLED
if ((flags_ & SVC_ONESHOT) && !(flags_ & SVC_RESTART)) {
flags_ |= SVC_DISABLED;
}
// Disabled and reset processes do not get restarted automatically.
// 如果是SVC_DISABLED或者SVC_RESET
// 設定行程狀態為stopped,然后回傳
if (flags_ & (SVC_DISABLED | SVC_RESET)) {
NotifyStateChange("stopped");
return;
}
// If we crash > 4 times in 4 minutes, reboot into recovery.
// 省略 crash 次數檢測
......
// 省略一些行程狀態的設定,都是和重啟相關的
// Execute all onrestart commands for this service.
// 執行onrestart的指令
onrestart_.ExecuteAllCommands();
// 設定狀態為重啟中
NotifyStateChange("restarting");
return;
}
在Reap()函式中
- 會根據對應行程
Service物件的flags_標志位來判斷該行程能不能重啟 - 如果需要重啟,就給
flags_標志位添加SVC_RESTARTING標志位
到這里,我們清楚了handle_signal()函式的內部流程,那么它又是從哪里被呼叫的呢?
我們再回到init.cpp的main()方法中看看:
while (true) {
......
epoll_event ev;
int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));
if (nr == -1) {
PLOG(ERROR) << "epoll_wait failed";
} else if (nr == 1) {
((void (*)()) ev.data.ptr)();
}
}
請注意ev.data.ptr,還記得register_epoll_handler()函式不
void register_epoll_handler(int fd, void (*fn)()) {
......
ev.data.ptr = reinterpret_cast<void*>(fn);
......
}
當epoll_wait有資料接收到時,就會執行((void (*)()) ev.data.ptr)();,也就是我們的回呼函式handle_signal()了
咳咳咳,到這里就把
Init行程對子行程死亡通知的邏輯給梳理完了,原始碼一直在變,好在核心的邏輯沒有變化,且看且珍惜吧,哈哈哈~
屬性系統
簡介
屬性在Android系統中大量使用,用來保存系統設定或在行程間傳遞一些簡單的資訊
- 每個
屬性由屬性名和屬性值組成 屬性名通常一長串以.分割的字串,這些名稱的前綴有特定含義,不能隨便改動屬性值只能是字串
Java層可以通過如下方法來獲取和設定屬性:
//class android.os.SystemProperties
@SystemApi
public static String get(String key);
@SystemApi
public static String get(String key, String def);
@hide
public static void set(String key, String val);
native層可以使用:
android::base::GetProperty(key, "");
android::base::SetProperty(key, val);
對于系統中的每個行程來說:
讀取屬性值對任何行程都是沒有限制的,直接由本行程從共享區域中讀取修改屬性值則必須通過Init行程完成,同時Init行程還需要檢查發起請求的行程是否具有相應的權限
屬性值修改成功后,Init行程會檢查init.rc檔案中是否已經定義了和該屬性值匹配的trigger,如果有定義,則執行trigger下的命令,如:
on property:ro.debuggable=1
start console
這個trigger的含義是:當屬性ro.debuggable被設定為1,則執行命令start console,啟動console
Android系統級應用和底層模塊非常依賴屬性系統,常常依靠屬性值來決定它們的行為,
Android的系統設定程式中,很多功能的打開和關閉都是通過某個特定的系統屬性值來控制,這也意味著隨便改變屬性值將會嚴重影響系統的運行,因此,對于屬性值的修改必須要有特定的權限,對于權限的設定,現在統一由SELinux來控制,
屬性服務的啟動流程
我們先看下屬性服務啟動的整體流程:
int main(int argc, char** argv) {
......
// 屬性服務初始化
property_init();
......
// 啟動屬性服務
start_property_service();
......
}
在init.cpp的main()函式中,通過property_init();來對屬性服務進行初始化
void property_init() {
// 創建一個檔案夾,權限711,只有owner才可以設定
mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
// 讀取一些屬性檔案,將屬性值存盤在一個集合中
CreateSerializedPropertyInfo();
if (__system_property_area_init()) {// 創建屬性共享記憶體空間(這個函式是libc庫的部分)
LOG(FATAL) << "Failed to initialize property area";
}
if (!property_info_area.LoadDefaultPath()) {// 加載默認路徑上的屬性到共享區域
LOG(FATAL) << "Failed to load serialized property info file";
}
}
然后通過start_property_service()函式啟動服務:
void start_property_service() {
// 省略SELinux相關操作
......
property_set("ro.property_service.version", "2");
// 創建prop service對應的socket,并回傳socket fd
property_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, false, 0666, 0, 0, nullptr);
// 省略創建失敗的例外判斷
......
// 設定最大連接數量為8
listen(property_set_fd, 8);
// 注冊epolls事件監聽property_set_fd
// 當監聽到資料變化時,呼叫handle_property_set_fd函式
register_epoll_handler(property_set_fd, handle_property_set_fd);
}
socket描述符property_set_fd被創建后,用epoll來監聽property_set_fd- 當
property_set_fd有資料到來時,init行程將呼叫handle_property_set_fd()函式來進行處理
我們再來看下handle_property_set_fd()函式
static void handle_property_set_fd() {
static constexpr uint32_t kDefaultSocketTimeout = 2000; /* ms */
// 省略一些例外判斷
......
uint32_t cmd = 0;
// 省略cmd讀取操作和一些例外判斷
......
switch (cmd) {
case PROP_MSG_SETPROP: {
char prop_name[PROP_NAME_MAX];
char prop_value[PROP_VALUE_MAX];
// 省略字符資料的讀取組裝操作
......
uint32_t result =
HandlePropertySet(prop_name, prop_value, socket.source_context(), cr, &error);
// 省略例外情況處理
......
break;
}
case PROP_MSG_SETPROP2: {
std::string name;
std::string value;
// 省略字串資料的讀取操作
......
uint32_t result = HandlePropertySet(name, value, socket.source_context(), cr, &error);
// 省略例外情況處理
......
break;
}
default:
LOG(ERROR) << "sys_prop: invalid command " << cmd;
socket.SendUint32(PROP_ERROR_INVALID_CMD);
break;
}
}
Init行程在接收到設定屬性的cmd后,會執行處理函式HandlePropertySet()
uint32_t HandlePropertySet(const std::string& name, const std::string& value,
const std::string& source_context,
const ucred& cr, std::string* error) {
// 判斷要設定的屬性名稱是否合法
// 相當于命名規則檢查
if (!IsLegalPropertyName(name)) {
// 不合法直接回傳
return PROP_ERROR_INVALID_NAME;
}
// 如果是ctl開頭,說明是控制類屬性
if (StartsWith(name, "ctl.")) {
// 檢查是否具有對應的控制權限
......
// 權限通過后執行對應的控制指令
// 其實控制指令就簡單的幾個start/stop等,大家可以在深入閱讀下這個函式
HandleControlMessage(name.c_str() + 4, value, cr.pid);
return PROP_SUCCESS;
}
const char* target_context = nullptr;
const char* type = nullptr;
// 獲取要設定的屬性的背景關系和資料型別
// 后面會對target_context和type進行比較判斷
property_info_area->GetPropertyInfo(name.c_str(), &target_context, &type);
// 檢查是否具有當前屬性的set權限
if (!CheckMacPerms(name, target_context, source_context.c_str(), cr)) {
// 沒有直接回傳
return PROP_ERROR_PERMISSION_DENIED;
}
// 對屬性的型別和要寫入資料的型別進行判斷
// 大家看看CheckType函式就明白了,其實只有一個string型別,,,,,
if (type == nullptr || !CheckType(type, value)) {
// 不合法,直接回傳
return PROP_ERROR_INVALID_VALUE;
}
// 如果是sys.powerctl屬性,需要做一些特殊處理
if (name == "sys.powerctl") {
// 增加一些額外列印
......
}
if (name == "selinux.restorecon_recursive") {
// 特殊屬性,特殊處理
return PropertySetAsync(name, value, RestoreconRecursiveAsync, error);
}
return PropertySet(name, value, error);
}
除了一些特殊的屬性外,真正設定屬性的函式是PropertySet
static uint32_t PropertySet(const std::string& name, const std::string& value, std::string* error) {
size_t valuelen = value.size();
// 判斷屬性名是否合法
if (!IsLegalPropertyName(name)) {
*error = "Illegal property name";
return PROP_ERROR_INVALID_NAME;
}
// 判斷寫入的資料長度是否合法
// 判斷屬性是否為只讀屬性(ro)
if (valuelen >= PROP_VALUE_MAX && !StartsWith(name, "ro.")) {
*error = "Property value too long";
return PROP_ERROR_INVALID_VALUE;
}
// 判斷要寫入資料的編碼格式
if (mbstowcs(nullptr, value.data(), 0) == static_cast<std::size_t>(-1)) {
*error = "Value is not a UTF8 encoded string";
return PROP_ERROR_INVALID_VALUE;
}
// 根據屬性名獲取系統中存放的屬性物件
prop_info* pi = (prop_info*) __system_property_find(name.c_str());
if (pi != nullptr) {
// ro.* properties are actually "write-once".
// ro開頭的屬性只允許寫入一次
if (StartsWith(name, "ro.")) {
*error = "Read-only property was already set";
return PROP_ERROR_READ_ONLY_PROPERTY;
}
// 如果已經存在,并且不是只讀屬性,執行屬性更新函式
__system_property_update(pi, value.c_str(), valuelen);
} else {
// 如果系統中不存在屬性,執行添加屬性添加函式
int rc = __system_property_add(name.c_str(), name.size(), value.c_str(), valuelen);
if (rc < 0) {
*error = "__system_property_add failed";
return PROP_ERROR_SET_FAILED;
}
}
// Don't write properties to disk until after we have read all default
// properties to prevent them from being overwritten by default values.
// 如果是持久化的屬性,進行持久化處理
if (persistent_properties_loaded && StartsWith(name, "persist.")) {
WritePersistentProperty(name, value);
}
// 將屬性的變更添加到Action佇列中
property_changed(name, value);
return PROP_SUCCESS;
}
Init行程epoll屬性的socket,等待和處理屬性請求,
- 如果有請求到來,則呼叫
handle_property_set_fd來處理這個請求 - 在
handle_property_set_fd函式里,首先檢查請求者的uid/gid看看是否有權限,如果有權限則調property_service.cpp中的PropertySet函式,
在PropertySet函式中
- 它先查找就沒有這個屬性,如果找到,更改屬性,如果找不到,則添加新屬性,
- 更改時還會判斷是不是
ro屬性,如果是,則不能更改, - 如果是
persist的話還會寫到/data/property/<name>中,
最后它會調property_changed函式,把事件掛到佇列里
- 如果有人注冊這個屬性的話(比如
init.rc中on property:ro.kernel.qemu=1),最侄訓觸發它
ueventd和watchdogd簡介
ueventd行程
守護行程
ueventd的主要作用是接收ueventd來創建和洗掉設備中dev目錄下的設備節點
ueventd行程和Init行程并不是一個行程,但是它們的二進制檔案是相同的,只不過啟動時引數不一樣導致程式的執行流程不一樣,
在init.rc檔案中
on early-init
start ueventd
## Daemon processes to be run by init.
##
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
這樣Init行程在執行action eraly-init是就會啟動ueventd行程,
watchdogd行程
watchdogd和ueventd型別,都是獨立于Init的行程,但是代碼和Init行程在一起,watchdogd是用來配合硬體看門狗的,
當一個硬體系統開啟了watchdog功能,那么運行在這個硬體系統之上的軟體必須在規定的時間間隔內向watchdog發送一個信號,這個行為簡稱為喂狗(feed dog),以免watchdog記時超時引發系統重起,
現在的系統中很少有看到watchdogd行程了,不過這種模式還是很不錯、值得借鑒的
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/192767.html
標籤:其他
