Android 8.0 系統啟動流程之init程序--第二階段(五)
1、概述
上一篇中講了init程序的第一階段,我們接著講第二階段,主要有以下內容
- 建立程序會話金鑰並初始化屬性系統
- 進行SELinux第二階段並恢復一些檔案安全上下文
- 新建epoll並初始化子程序終止訊號處理函式
- 設定其他系統屬性並開啟系統屬性服務
2、建立程序會話金鑰並初始化屬性系統
由於之前第一階段最後有設定INIT_SECOND_STAGE,所以在第二階段is_first_stage條件不成立直接跳過該段程式碼。從keyctl開始才是重點內容,我們一一展開來看
int main(int argc, char** argv) {
//同樣進行ueventd/watchdogd跳轉及環境變數設定
...
//之前準備工作時將INIT_SECOND_STAGE設定為true,已經不為nullptr,所以is_first_stage為false
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
//is_first_stage為false,直接跳過
if (is_first_stage) {
...
}
// At this point we're in the second stage of init.
InitKernelLogging(argv); //上一節有講,初始化日誌輸出
LOG(INFO) << "init second stage started!";
// Set up a session keyring that all processes will have access to. It
// will hold things like FBE encryption keys. No process should override
// its session keyring.
keyctl(KEYCTL_GET_KEYRING_ID, KEY_SPEC_SESSION_KEYRING, 1); //初始化程序會話金鑰
// Indicate that booting is in progress to background fw loaders, etc.
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));//建立 /dev/.booting 檔案,就是個標記,表示booting進行中
property_init();//初始化屬性系統,並從指定檔案讀取屬性
//接下來的一系列操作都是從各個檔案讀取一些屬性,然後通過property_set設定系統屬性
// If arguments are passed both on the command line and in DT,
// properties set in DT always have priority over the command-line ones.
/*
* 1.這句英文的大概意思是,如果引數同時從命令列和DT傳過來,DT的優先順序總是大於命令列的
* 2.DT即device-tree,中文意思是裝置樹,這裡面記錄自己的硬體配置和系統執行引數
*/
process_kernel_dt();//處理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);
// Clean up our environment.
unsetenv("INIT_SECOND_STAGE"); //清空這些環境變數,因為之前都已經存入到系統屬性中去了
unsetenv("INIT_STARTED_AT");
unsetenv("INIT_SELINUX_TOOK");
unsetenv("INIT_AVB_VERSION");
...
}
2.1 keyctl
定義在platform/system/core/libkeyutils/Keyutils.cpp
keyctl將主要的工作交給__NR_keyctl這個系統呼叫,keyctl是Linux系統操縱核心的通訊金鑰管理工具
static long keyctl(int cmd, ...) {
va_list va;
//va_start,va_arg,va_end是配合使用的,用於將可變引數從堆疊中讀取出來
va_start(va, cmd);//va_start是獲取第一個引數地址
unsigned long arg2 = va_arg(va, unsigned long);//va_arg 遍歷引數
unsigned long arg3 = va_arg(va, unsigned long);
unsigned long arg4 = va_arg(va, unsigned long);
unsigned long arg5 = va_arg(va, unsigned long);
va_end(va);
return syscall(__NR_keyctl, cmd, arg2, arg3, arg4, arg5);//系統呼叫
}
key_serial_t keyctl_get_keyring_ID(key_serial_t id, int create) {
return keyctl(KEYCTL_GET_KEYRING_ID, id, create);
}
2.2 property_init
定義在 platform/system/core/init/property_service.cpp
直接交給 __system_property_area_init 處理
void property_init() {
if (__system_property_area_init()) {
LOG(ERROR) << "Failed to initialize property area";
exit(1);
}
}
_system_property_area_init 定義在/bionic/libc/bionic/system_properties.cpp看名字大概知道是用來初始化屬性系統區域的,應該是分門別類更準確些,首先清除快取,這裡主要是清除幾個連結串列以及在記憶體中的對映,新建property_filename目錄,這個目錄的值為 /dev/_properties;然後就是呼叫initialize_properties載入一些系統屬性的類別資訊,最後將載入的連結串列寫入檔案並對映到記憶體
2.3 process_kernel_dt
定義在platform/system/core/init/init.cpp
讀取DT(裝置樹)的屬性資訊,然後通過 property_set 設定系統屬性
static void process_kernel_dt() {
if (!is_android_dt_value_expected("compatible", "android,firmware")) {
//判斷 /proc/device-tree/firmware/android/compatible 檔案中的值是否為 android,firmware
return;
}
// get_android_dt_dir()的值為/proc/device-tree/firmware/android
std::unique_ptr<DIR, int (*)(DIR*)> dir(opendir(get_android_dt_dir().c_str()), closedir);
if (!dir) return;
std::string dt_file;
struct dirent *dp;
while ((dp = readdir(dir.get())) != NULL) {//遍歷dir中的檔案
if (dp->d_type != DT_REG || !strcmp(dp->d_name, "compatible") || !strcmp(dp->d_name, "name")) {
//跳過 compatible和name檔案
continue;
}
std::string file_name = get_android_dt_dir() + dp->d_name;
android::base::ReadFileToString(file_name, &dt_file);//讀取檔案內容
std::replace(dt_file.begin(), dt_file.end(), ',', '.');//替換 , 為 .
property_set("ro.boot."s + dp->d_name, dt_file);// 將 ro.boot.檔名 作為key,檔案內容為value,設定進屬性
}
}
2.4 process_kernel_cmdline
static void process_kernel_cmdline() {
// The first pass does the common stuff, and finds if we are in qemu.
// The second pass is only necessary for qemu to export all kernel params
// as properties.
import_kernel_cmdline(false, import_kernel_nv);
if (qemu[0]) import_kernel_cmdline(true, import_kernel_nv);
}
static void import_kernel_nv(const std::string& key, const std::string& value, bool for_emulator) {
if (key.empty()) return;
if (for_emulator) {
// In the emulator, export any kernel option with the "ro.kernel." prefix.
property_set("ro.kernel." + key, value);
return;
}
if (key == "qemu") {
strlcpy(qemu, value.c_str(), sizeof(qemu));
} else if (android::base::StartsWith(key, "androidboot.")) {
property_set("ro.boot." + key.substr(12), value);
}
}
2.5 export_kernel_boot_props
static void export_kernel_boot_props() {
struct {
const char *src_prop;
const char *dst_prop;
const char *default_value;
} prop_map[] = {
{ "ro.boot.serialno", "ro.serialno", "", },
{ "ro.boot.mode", "ro.bootmode", "unknown", },
{ "ro.boot.baseband", "ro.baseband", "unknown", },
{ "ro.boot.bootloader", "ro.bootloader", "unknown", },
{ "ro.boot.hardware", "ro.hardware", "unknown", },
{ "ro.boot.revision", "ro.revision", "0", },
};
for (size_t i = 0; i < arraysize(prop_map); i++) {
std::string value = GetProperty(prop_map[i].src_prop, "");
property_set(prop_map[i].dst_prop, (!value.empty()) ? value : prop_map[i].default_value);
}
}
3、進行SELinux第二階段並恢復一些檔案安全上下文
3.1 selinux_initialize
定義在platform/system/core/init/init.cpp
第二階段只是執行 selinux_init_all_handles
selinux_initialize(false);//第二階段初始化SELinux policy
static void selinux_initialize(bool in_kernel_domain) {
... //和之前一樣設定回撥函式
if (in_kernel_domain) {//第二階段跳過 in_kernel_domain為false
...
} else {
selinux_init_all_handles();
}
}
static void selinux_init_all_handles(void)
{
sehandle = selinux_android_file_context_handle();//建立context的處理函式
selinux_android_set_sehandle(sehandle);//將剛剛新建的處理賦值給fc_sehandle
sehandle_prop = selinux_android_prop_context_handle();//建立prop的處理函式
}
4、新建epoll並初始化子程序終止訊號處理函式
int main(){
......
epoll_fd = epoll_create1(EPOLL_CLOEXEC);//建立epoll例項,並返回epoll的檔案描述符
if (epoll_fd == -1) {
PLOG(ERROR) << "epoll_create1 failed";
exit(1);
}
......
}
4.1 epoll_create1
定義在platform/system/core/init/init.cpp
EPOLL類似於POLL,是Linux中用來做事件觸發的,linux很長的時間都在使用select來做事件觸發,它是通過輪詢來處理的,輪詢的fd數目越多,自然耗時越多,對於大量的描述符處理,EPOLL更有優勢。epoll_create1是epoll_create的升級版,可以動態調整epoll例項中檔案描述符的個數
EPOLL_CLOEXEC這個引數是為檔案描述符新增O_CLOEXEC屬性
4.2 signal_handler_init
定義在platform/system/core/init/signal_handler.cpp
這個函式主要的作用是註冊SIGCHLD訊號的處理函式。init是一個守護程序,為了防止init的子程序成為殭屍程序(zombie process),需要init在子程序在結束時獲取子程序的結束碼,通過結束碼將程式表中的子程序移除,防止成為殭屍程序的子程序佔用程式表的空間(程式表的空間達到上限時,系統就不能再啟動新的程序了,會引起嚴重的系統問題)
在linux當中,父程序是通過捕捉SIGCHLD訊號來得知子程序執行結束的情況,SIGCHLD訊號會在子程序終止的時候發出,瞭解這些背景後,我們來看看init程序如何處理這個訊號
首先,呼叫socketpair,這個方法會返回一對檔案描述符,這樣當一端寫入時,另一端就能被通知到,
socketpair兩端既可以寫也可以讀,這裡只是單向的讓s[0]寫,s[1]讀
然後,新建一個sigaction結構體,sa_handler是訊號處理函式,指向SIGCHLD_handler,
SIGCHLD_handler做的事情就是往s[0]裡寫個”1”,這樣s1就會收到通知,SA_NOCLDSTOP表示只在子程序終止時處理,
因為子程序在暫停時也會發出SIGCHLD訊號
sigaction(SIGCHLD, &act, 0) 這個是建立訊號繫結關係,也就是說當監聽到SIGCHLD訊號時,由act這個sigaction結構體處理
ReapAnyOutstandingChildren 這個後文講
最後,register_epoll_handler的作用就是註冊一個監聽,當signal_read_fd(之前的s[1])收到訊號,觸發handle_signal
終上所述,signal_handler_init函式的作用就是,接收到SIGCHLD訊號時觸發handle_signal
void signal_handler_init() {
// Create a signalling mechanism for SIGCHLD.
int s[2];
//建立socket並返回檔案描述符
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {
PLOG(ERROR) << "socketpair failed";
exit(1);
}
signal_write_fd = s[0];
signal_read_fd = s[1];
// Write to signal_write_fd if we catch SIGCHLD.
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIGCHLD_handler;//act處理函式
act.sa_flags = SA_NOCLDSTOP;
sigaction(SIGCHLD, &act, 0);
ServiceManager::GetInstance().ReapAnyOutstandingChildren();//具體處理子程序終止訊號
register_epoll_handler(signal_read_fd, handle_signal);//註冊signal_read_fd到epoll中
}
void register_epoll_handler(int fd, void (*fn)()) {
epoll_event ev;
ev.events = EPOLLIN; //監聽事件型別,EPOLLIN表示fd中有資料可讀
ev.data.ptr = reinterpret_cast<void*>(fn);//回撥函式賦值給ptr
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {//註冊事件
PLOG(ERROR) << "epoll_ctl failed";
}
}
4.2.1 handle_signal
定義在platform/system/core/init/signal_handler.cpp
首先清空signal_read_fd中的資料,然後呼叫ReapAnyOutstandingChildren,ReapAnyOutstandingChildren是定義在system/core/init/service.cpp中,之前在signal_handler_init中呼叫過一次,它其實是呼叫ReapOneProcess
static void handle_signal() {
// Clear outstanding requests.
char buf[32];
read(signal_read_fd, buf, sizeof(buf));
ServiceManager::GetInstance().ReapAnyOutstandingChildren();
}
4.2.2 ReapOneProcess
定義在platform/system/core/init/service.cpp
這是最終的處理函數了,這個函式先用waitpid找出掛掉程序的pid,然後根據pid找到對應Service,最後呼叫Service的Reap方法清除資源,根據程序對應的型別,決定是否重啟機器或重啟程序
bool ServiceManager::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.
//用waitpid函式獲取狀態發生變化的子程序pid
//waitpid的標記為WNOHANG,即非阻塞,返回為正值就說明有程序掛掉了
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 (PropertyChildReap(pid)) {
return true;
}
Service* svc = FindServiceByPid(pid);//通過pid找到對應的Service
std::string name;
std::string wait_string;
if (svc) {
name = StringPrintf("Service '%s' (pid %d)", svc->name().c_str(), pid);
if (svc->flags() & SVC_EXEC) {
wait_string = StringPrintf(" waiting took %f seconds",
exec_waiter_->duration().count() / 1000.0f);
}
} else {
name = StringPrintf("Untracked pid %d", pid);
}
auto status = siginfo.si_status;
if (WIFEXITED(status)) {
LOG(INFO) << name << " exited with status " << WEXITSTATUS(status) << wait_string;
} else if (WIFSIGNALED(status)) {
LOG(INFO) << name << " killed by signal " << WTERMSIG(status) << wait_string;
}
if (!svc) {//沒有找到,說明已經結束了
return true;
}
svc->Reap();//清除子程序相關的資源
if (svc->flags() & SVC_EXEC) {
exec_waiter_.reset();
}
if (svc->flags() & SVC_TEMPORARY) {
RemoveService(*svc);
}
return true;
}
5、設定其他系統屬性並開啟系統屬性服務
...
property_load_boot_defaults();//從檔案中載入一些屬性,讀取usb配置
export_oem_lock_status();//設定ro.boot.flash.locked 屬性
start_property_service();//開啟一個socket監聽系統屬性的設定
set_usb_controller();//設定sys.usb.controller 屬性
...
5.1 設定其他系統屬性
property_load_boot_defaults,export_oem_lock_status,set_usb_controller這三個函式都是呼叫property_set設定一些系統屬性
void property_load_boot_defaults() {
if (!load_properties_from_file("/system/etc/prop.default", NULL)) {//從檔案中讀取屬性
// Try recovery path
if (!load_properties_from_file("/prop.default", NULL)) {
// Try legacy path
load_properties_from_file("/default.prop", NULL);
}
}
load_properties_from_file("/odm/default.prop", NULL);
load_properties_from_file("/vendor/default.prop", NULL);
update_sys_usb_config();
}
static void export_oem_lock_status() {
if (!android::base::GetBoolProperty("ro.oem_unlock_supported", false)) {
return;
}
std::string value = GetProperty("ro.boot.verifiedbootstate", "");
if (!value.empty()) {
property_set("ro.boot.flash.locked", value == "orange" ? "0" : "1");
}
}
static void set_usb_controller() {
std::unique_ptr<DIR, decltype(&closedir)>dir(opendir("/sys/class/udc"), closedir);
if (!dir) return;
dirent* dp;
while ((dp = readdir(dir.get())) != nullptr) {
if (dp->d_name[0] == '.') continue;
property_set("sys.usb.controller", dp->d_name);
break;
}
}
5.2 start_property_service
定義在platform/system/core/init/property_service.cpp
之前我們看到通過property_set可以輕鬆設定系統屬性,那幹嘛這裡還要啟動一個屬性服務呢?這裡其實涉及到一些許可權的問題,不是所有程序都可以隨意修改任何的系統屬性,
Android將屬性的設定統一交由init程序管理,其他程序不能直接修改屬性,而只能通知init程序來修改,而在這過程中,init程序可以進行許可權控制,我們來看看這些是如何實現的
首先建立一個socket並返回檔案描述符,然後設定最大併發數為8,其他程序可以通過這個socket通知init程序修改系統屬性,最後註冊epoll事件,也就是當監聽到property_set_fd改變時呼叫handle_property_set_fd
void start_property_service() {
property_set("ro.property_service.version", "2");
//建立socket用於通訊
property_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
false, 0666, 0, 0, nullptr, sehandle);
if (property_set_fd == -1) {
PLOG(ERROR) << "start_property_service socket creation failed";
exit(1);
}
//監聽property_set_fd,設定最大併發數為8
listen(property_set_fd, 8);
register_epoll_handler(property_set_fd, handle_property_set_fd);//註冊epoll事件
}
static void handle_property_set_fd() {
static constexpr uint32_t kDefaultSocketTimeout = 2000; /* ms */
//等待客戶端連線
int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
if (s == -1) {
return;
}
struct ucred cr;
socklen_t cr_size = sizeof(cr);
//獲取連線到此socket的程序的憑據
if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
close(s);
PLOG(ERROR) << "sys_prop: unable to get SO_PEERCRED";
return;
}
SocketConnection socket(s, cr);// 建立socket連線
uint32_t timeout_ms = kDefaultSocketTimeout;
uint32_t cmd = 0;
if (!socket.RecvUint32(&cmd, &timeout_ms)) {//讀取socket中的操作資訊
PLOG(ERROR) << "sys_prop: error while reading command from the socket";
socket.SendUint32(PROP_ERROR_READ_CMD);
return;
}
switch (cmd) {//根據操作資訊,執行對應處理,兩者區別一個是以char形式讀取,一個以String形式讀取
case PROP_MSG_SETPROP: {
char prop_name[PROP_NAME_MAX];
char prop_value[PROP_VALUE_MAX];
if (!socket.RecvChars(prop_name, PROP_NAME_MAX, &timeout_ms) ||
!socket.RecvChars(prop_value, PROP_VALUE_MAX, &timeout_ms)) {
PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP): error while reading name/value from the socket";
return;
}
prop_name[PROP_NAME_MAX-1] = 0;
prop_value[PROP_VALUE_MAX-1] = 0;
handle_property_set(socket, prop_value, prop_value, true);
break;
}
case PROP_MSG_SETPROP2: {
std::string name;
std::string value;
if (!socket.RecvString(&name, &timeout_ms) ||
!socket.RecvString(&value, &timeout_ms)) {
PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP2): error while reading name/value from the socket";
socket.SendUint32(PROP_ERROR_READ_DATA);
return;
}
handle_property_set(socket, name, value, false);
break;
}
default:
LOG(ERROR) << "sys_prop: invalid command " << cmd;
socket.SendUint32(PROP_ERROR_INVALID_CMD);
break;
}
}
static void handle_property_set(SocketConnection& socket,
const std::string& name,
const std::string& value,
bool legacy_protocol) {
const char* cmd_name = legacy_protocol ? "PROP_MSG_SETPROP" : "PROP_MSG_SETPROP2";
if (!is_legal_property_name(name)) {//檢查key的合法性
LOG(ERROR) << "sys_prop(" << cmd_name << "): illegal property name \"" << name << "\"";
socket.SendUint32(PROP_ERROR_INVALID_NAME);
return;
}
struct ucred cr = socket.cred(); //獲取操作程序的憑證
char* source_ctx = nullptr;
getpeercon(socket.socket(), &source_ctx);
if (android::base::StartsWith(name, "ctl.")) { //如果以ctl.開頭,就執行Service的一些控制操作
if (check_control_mac_perms(value.c_str(), source_ctx, &cr)) {//SELinux安全檢查,有許可權才進行操作
handle_control_message(name.c_str() + 4, value.c_str());
if (!legacy_protocol) {
socket.SendUint32(PROP_SUCCESS);
}
} else {
LOG(ERROR) << "sys_prop(" << cmd_name << "): Unable to " << (name.c_str() + 4)
<< " service ctl [" << value << "]"
<< " uid:" << cr.uid
<< " gid:" << cr.gid
<< " pid:" << cr.pid;
if (!legacy_protocol) {
socket.SendUint32(PROP_ERROR_HANDLE_CONTROL_MESSAGE);
}
}
} else {//其他的屬性呼叫property_set進行設定
if (check_mac_perms(name, source_ctx, &cr)) {//SELinux安全檢查,有許可權才進行操作
uint32_t result = property_set(name, value);
if (!legacy_protocol) {
socket.SendUint32(result);
}
} else {
LOG(ERROR) << "sys_prop(" << cmd_name << "): permission denied uid:" << cr.uid << " name:" << name;
if (!legacy_protocol) {
socket.SendUint32(PROP_ERROR_PERMISSION_DENIED);
}
}
}
freecon(source_ctx);
}
6、小結
init程序第二階段主要工作是初始化屬性系統,解析SELinux的匹配規則,處理子程序終止訊號,啟動系統屬性服務,可以說每一項都很關鍵,如果說第一階段是為屬性系統,SELinux做準備,那麼第二階段就是真正去把這些落實的,下一篇我們將講解.rc檔案的語法規則;未完待續。。。