十一假期有點墮落,無限火力有點上癮,謹戒、謹戒
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
,如果Service
4分鐘內重新啟動超過4次,系統將自動重新啟動並進入recovery
模式console [<console>]
:表示該服務需要一個控制檯
/dev/console
/dev/
部分,如/dev/tty0
需寫成console tty0
disabled
:表示服務不會通過class_start
啟動。它必須以命令start service_name
的方式指定來啟動setenv <name> <value>
:在Service
啟動時將環境變數name
設定為value
socket <name> <type> <perm> [ <user> [ <group> [ <seclabel> ] ] ]
:建立一個名為/dev/socket/<name>
的通訊端,並把檔案描述符傳遞給要啟動的程序
type
的值必須是dgram
、stream
、seqpacket
user
和group
預設為0seclabel
是這個socket
的SElinux
安全上下文,預設為當前service
的上下文user <username>
:在執行此服務之前切換使用者名稱,預設的是root。如果程序沒有相應的許可權,則不能使用該命令oneshot
:Service
退出後不再重新啟動class <name>
:給Service
指定一個名字。所有同名字的服務可以同時啟動和停止。如果不通過class
顯示指定,預設為default
onrestart
:當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
程序了,不過這種模式還是很不錯、值得借鑑的