Android 8.1 源码_启动篇(一) -- 深入研究 init(转 Android 9.0 分析)
前言
init进程,它是一个由内核启动的用户级进程,当Linux内核启动之后,运行的第一个进程是init,这个进程是一个守护进程,确切的说,它是Linux系统中用户控件的第一个进程,所以它的进程号是1。它的生命周期贯穿整个linux 内核运行的始终, linux中所有其它的进程的共同始祖均为init进程。
开篇
核心源码
Android 版本 | 关键类 | 路径 |
---|---|---|
8.1 | init.rc | system/core/rootdir/init.rc |
8.1 | init.cpp | system/core/init/init.cpp |
8.1 | property_service.cpp | system/core/init/property_service.cpp |
8.1 | init_parser.h | system/core/init/init_parser.h |
8.1 | init_parser.cpp | system/core/init/init_parser.cpp |
8.1 | log.cpp | system/core/init/log.cpp |
8.1 | logging.cpp | system/core/base/logging.cpp |
8.1 | property_service.cpp | system/core/init/property_service.cpp |
8.1 | signal_handler.cpp | system/core/init/signal_handler.cpp |
8.1 | service.cpp | system/core/init/service.cpp |
8.1 | Action.cpp | system/core/init/Action.cpp |
8.1 | builtins.cpp | system/core/init/builtins.cpp |
Android系统启动过程
1. 按下电源系统启动
当电源按下时引导芯片代码开始从预定义的地方(固化在ROM)开始执行,加载引导程序Bootloader到RAM,然后执行。
2. 引导程序Bootloader
引导程序是在Android操作系统开始运行前的一个小程序,它的主要作用是把系统OS拉起来并运行。
3. linux内核启动
内核启动时,设置缓存、被保护存储器、计划列表,加载驱动。当内核完成系统设置,它首先在系统文件中寻找”init”文件,然后启动root进程或者系统的第一个进程。
4. init进程启动
✨ 这就是我们接下来要讨论的内容 ✨
Read The Fucking Code
Android init进程的入口文件在system/core/init/init.cpp中,由于init是命令行程序,所以分析init.cpp首先应从main函数开始:
第一阶段(内核态)
判断及增加环境变量
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
//根据参数,判断是否需要启动ueventd和watchdogd
if (!strcmp(basename(argv[0]), "ueventd")) { // 启动ueventd
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) { // 启动watchdogd
return watchdogd_main(argc, argv);
}
if (REBOOT_BOOTLOADER_ON_PANIC) {
InstallRebootSignalHandlers(); // 若紧急重启,则安装对应的消息处理器
}
add_environment("PATH", _PATH_DEFPATH); // 添加环境变量
... ...
}
创建并挂载相关的文件系统
int main(int argc, char** argv) {
/* 01. 判断及增加环境变量 */
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
if (is_first_stage) { // 判断是否是系统启动的第一阶段(第一次进入:true)
boot_clock::time_point start_time = boot_clock::now(); // 用于记录启动时间
// Clear the umask.
umask(0); // 清除屏蔽字(file mode creation mask),保证新建的目录的访问权限不受屏蔽字影响
// 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"); // 挂载tmpfs文件系统
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL); // 挂载devpts文件系统
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)); // 挂载proc文件系统
// Don't expose the raw commandline to unprivileged processes.
chmod("/proc/cmdline", 0440); // 8.0新增, 收紧了cmdline目录的权限
gid_t groups[] = { AID_READPROC }; // 8.0新增,增加了个用户组
setgroups(arraysize(groups), groups);
mount("sysfs", "/sys", "sysfs", 0, NULL); // 挂载sysfs文件系统
mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL); // 8.0新增
mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)); // 提前创建了kmsg设备节点文件,用于输出log信息
mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));
... ...
}
如上所示,该部分主要用于创建和挂载启动所需的文件目录。需要注意的是,在编译Android系统源码时,在生成的根文件系统中, 并不存在这些目录,它们是系统运行时的目录,即当系统终止时,就会消失。
四类文件系统:
tmpfs:一种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中,如果你将tmpfs文件系统卸载后,那么其下的所有的内容将不复存在。tmpfs既可以使用RAM,也可以使用交换分区,会根据你的实际需要而改变大小。tmpfs的速度非常惊人,毕竟它是驻留在RAM中的,即使用了交换分区,性能仍然非常卓越。由于tmpfs是驻留在RAM的,因此它的内容是不持久的。断电后,tmpfs的内容就消失了,这也是被称作tmpfs的根本原因。
devpts:为伪终端提供了一个标准接口,它的标准挂接点是/dev/ pts。只要pty的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态的创建一个新的pty设备文件。
proc:一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。
sysfs:与proc文件系统类似,也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在/sys目录下。sysfs文件系统是Linux2.6内核引入的,它把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。
重定向输入输出/内核Log系统
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建并挂载相关的文件系统 */
if (is_first_stage) {
... ...
// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
// talk to the outside world...
InitKernelLogging(argv);
... ...
}
... ...
屏蔽标准的输入输出
跟踪InitKernelLogging(): system/core/init/log.cpp
void InitKernelLogging(char* argv[]) {
// Make stdin/stdout/stderr all point to /dev/null.
int fd = open("/sys/fs/selinux/null", O_RDWR);
if (fd == -1) { // 若开启失败,则记录log
int saved_errno = errno;
android::base::InitLogging(argv, &android::base::KernelLogger);
errno = saved_errno;
PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
}
dup2(fd, 0); // dup2函数的作用是用来复制一个文件的描述符, 通常用来重定向进程的stdin、stdout和stderr
dup2(fd, 1); // 它的函数原形是:int dup2(int oldfd, int targetfd),该函数执行后,targetfd将变成oldfd的复制品
dup2(fd, 2); // 因此这边的过程其实就是:创建出__null__设备后,将0、1、2绑定到__null__设备上
if (fd > 2) close(fd); // 所以init进程调用InitKernelLogging函数后,通过标准的输入输出无法输出信息
android::base::InitLogging(argv, &android::base::KernelLogger);
}
设置kernel logger
跟踪InitLogging():system/core/base/logging.cpp
// 设置KernelLogger
void InitLogging(char* argv[], LogFunction&& logger, AbortFunction&& aborter) {
//设置logger
SetLogger(std::forward<LogFunction>(logger));
SetAborter(std::forward<AbortFunction>(aborter));
if (gInitialized) {
return;
}
gInitialized = true;
// Stash the command line for later use. We can use /proc/self/cmdline on
// Linux to recover this, but we don't have that luxury on the Mac/Windows,
// and there are a couple of argv[0] variants that are commonly used.
if (argv != nullptr) {
std::lock_guard<std::mutex> lock(LoggingLock());
ProgramInvocationName() = basename(argv[0]);
}
const char* tags = getenv("ANDROID_LOG_TAGS");
if (tags == nullptr) {
return;
}
// 根据TAG决定最小记录等级
std::vector<std::string> specs = Split(tags, " ");
for (size_t i = 0; i < specs.size(); ++i) {
// "tag-pattern:[vdiwefs]"
std::string spec(specs[i]);
if (spec.size() == 3 && StartsWith(spec, "*:")) {
switch (spec[2]) {
case 'v':
gMinimumLogSeverity = VERBOSE;
continue;
... ...
}
}
LOG(FATAL) << "unsupported '" << spec << "' in ANDROID_LOG_TAGS (" << tags
<< ")";
}
}
当需要输出日志时,KernelLogger函数就会被调用:
#if defined(__linux__)
void KernelLogger(android::base::LogId, android::base::LogSeverity severity,
const char* tag, const char*, unsigned int, const char* msg) {
... ...
// 打开log节点
static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
if (klog_fd == -1) return;
// 决定log等级
int level = kLogSeverityToKernelLogLevel[severity];
// The kernel's printk buffer is only 1024 bytes.
// TODO: should we automatically break up long lines into multiple lines?
// Or we could log but with something like "..." at the end?
char buf[1024];
size_t size = snprintf(buf, sizeof(buf), "<%d>%s: %s\n", level, tag, msg);
if (size > sizeof(buf)) {
size = snprintf(buf, sizeof(buf), "<%d>%s: %zu-byte message too long for printk\n",
level, tag, size);
}
iovec iov[1];
iov[0].iov_base = buf;
iov[0].iov_len = size;
// 通过iovec将log发送到dev/kmsg
TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));
}
#endif
挂在一些分区设备
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
if (is_first_stage) {
... ...
// 挂载特定的分区设备
if (!DoFirstStageMount()) {
LOG(ERROR) << "Failed to mount required partitions early ...";
panic(); // panic会尝试reboot
}
}
... ...
跟踪DoFirstStageMount():system/core/init/init_first_stage.cpp
// Mounts partitions specified by fstab in device tree.
bool DoFirstStageMount() {
// Skips first stage mount if we're in recovery mode.
if (IsRecoveryMode()) {
LOG(INFO) << "First stage mount skipped (recovery mode)";
return true;
}
// Firstly checks if device tree fstab entries are compatible.
if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {
LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
return true;
}
// 满足上述条件时,就会调用FirstStageMount的DoFirstStageMount函数
std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
if (!handle) {
LOG(ERROR) << "Failed to create FirstStageMount";
return false;
}
// 主要是初始化特定设备并挂载
return handle->DoFirstStageMount();
}
完成SELinux相关工作
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
if (is_first_stage) {
... ...
// 此处应该是初始化安全框架:Android Verified Boot
// AVB主要用于防止系统文件本身被篡改,还包含了防止系统回滚的功能,
// 以免有人试图回滚系统并利用以前的漏洞
SetInitAvbVersionInRecovery();
// Set up SELinux, loading the SELinux policy.
selinux_initialize(true); // 调用selinux_initialize启动SELinux
... ...
}
... ...
跟踪selinux_initialize():
static void selinux_initialize(bool in_kernel_domain) {
Timer t;
selinux_callback cb;
cb.func_log = selinux_klog_callback; // 用于打印log的回调函数
selinux_set_callback(SELINUX_CB_LOG, cb);
cb.func_audit = audit_callback; // 用于检查权限的回调函数
selinux_set_callback(SELINUX_CB_AUDIT, cb);
// init进程的运行是区分用户态和内核态的,first_stage运行在内核态
if (in_kernel_domain) {
LOG(INFO) << "Loading SELinux policy";
// 用于加载sepolicy文件
// 该函数最终将sepolicy文件传递给kernel,这样kernel就有了安全策略配置文件,后续的MAC才能开展起来。
if (!selinux_load_policy()) {
panic();
}
bool kernel_enforcing = (security_getenforce() == 1); // 内核中读取的信息
bool is_enforcing = selinux_is_enforcing(); // 命令行中得到的数据
// 用于设置selinux的工作模式。selinux有两种工作模式:
// 1、”permissive”,所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志
// 2、”enforcing”,所有操作都会进行权限检查。在一般的终端中,应该工作于enforing模式
if (kernel_enforcing != is_enforcing) {
if (security_setenforce(is_enforcing)) {
PLOG(ERROR) << "security_setenforce(%s) failed" << (is_enforcing ? "true" : "false");
security_failure(); // 将重启进入recovery mode
}
}
std::string err;
if (!WriteFile("/sys/fs/selinux/checkreqprot", "0", &err)) {
LOG(ERROR) << err;
security_failure();
}
// init's first stage can't set properties, so pass the time to the second stage.
setenv("INIT_SELINUX_TOOK", std::to_string(t.duration().count()).c_str(), 1);
} else {
selinux_init_all_handles(); // 在second stage调用时,初始化所有的handle
}
}
is_first_stage 收尾
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
if (is_first_stage) {
... ...
// We're in the kernel domain, so re-exec init to transition to the init domain now
// that the SELinux policy has been loaded.
if (selinux_android_restorecon("/init", 0) == -1) { // 按selinux policy要求,重新设置init文件属性
PLOG(ERROR) << "restorecon failed";
security_failure(); // 失败的话会reboot
}
static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1); // 记录初始化时的时间
char* path = argv[0];
char* args[] = { path, nullptr };
execv(path, args); // 再次调用init的main函数,启动用户态的init进程
// execv() only returns if an error happened, in which case we
// panic and never fall through this conditional.
PLOG(ERROR) << "execv(\"" << path << "\") failed";
security_failure(); // 内核态的进程不应该退出,若退出则会重启
}
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
... ...
}
上面所有的源码我们都是围绕第一阶段分析(is_first_stage),自此第一阶段结束,会复位一些信息,并设置一些环境变量,最后启动用户态的init进程,进入init第二阶段。
第二阶段(用户态)
init进程的第二阶段仍然从main函数开始入手(继续分析main函数剩余源码)
初始化属性域
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
// 同样进行一些判断及环境变量设置的工作
... ...
// 现在 is_first_stage 为 false 了
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
// 这部分工作不再执行了
if (is_first_stage) {
...........
}
// At this point we're in the second stage of init.
InitKernelLogging(argv); // 同样屏蔽标准输入输出及定义Kernel logger
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_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1); // 最后调用syscall,设置安全相关的值
// Indicate that booting is in progress to background fw loaders, etc.
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000)); // 这里的功能类似于“锁”
... ...
property_init(); // 初始化属性域 --> 定义于system/core/init/property_service.cpp
// 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);
... ...
}
这部分代码主要的工作应该就是调用 property_init 初始化属性域,然后设置各种属性。
在Android平台中,为了让运行中的所有进程共享系统运行时所需要的各种设置值,系统开辟了属性存储区域,并提供了访问该区域的API。
跟踪property_init():system/core/init/property_service.cpp
void property_init() {
if (__system_property_area_init()) { // 最终调用_system_property_area_init函数初始化属性域
LOG(ERROR) << "Failed to initialize property area";
exit(1);
}
}
清空环境变量
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
// Clean up our environment.
unsetenv("INIT_SECOND_STAGE");
unsetenv("INIT_STARTED_AT"); // 清除掉之前使用过的环境变量
unsetenv("INIT_SELINUX_TOOK");
unsetenv("INIT_AVB_VERSION");
... ...
}
完成SELinux相关工作
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
/* 02. 清空环境变量 */
// Now set up SELinux for second stage.
selinux_initialize(false);
selinux_restore_context(); // 再次完成selinux相关的工作
我们发现在init进程的第一阶段,也调用了selinux_initialize函数,那么两者有什么区别?
init进程第一阶段主要加载selinux相关的策略,而第二阶段调用selinux_initialize仅仅注册一些处理器。
我们跟下selinux_initialize():
static void selinux_initialize(bool in_kernel_domain) {
Timer t;
selinux_callback cb;
cb.func_log = selinux_klog_callback;
selinux_set_callback(SELINUX_CB_LOG, cb);
cb.func_audit = audit_callback;
selinux_set_callback(SELINUX_CB_AUDIT, cb);
if (in_kernel_domain) {
... ... // 这边就是第一阶段的工作
} else {
selinux_init_all_handles(); // 这边就是第二阶段的工作:注册处理器
}
}
再来看一下selinux_restore_context():主要是按 selinux policy 要求,重新设置一些文件的属性。
// The files and directories that were created before initial sepolicy load or
// files on ramdisk need to have their security context restored to the proper
// value. This must happen before /dev is populated by ueventd.
// 如注释所述,以下文件在selinux被加载前就创建了
// 于是,在selinux启动后,需要重新设置一些属性
static void selinux_restore_context() {
LOG(INFO) << "Running restorecon...";
selinux_android_restorecon("/dev", 0);
selinux_android_restorecon("/dev/kmsg", 0);
selinux_android_restorecon("/dev/socket", 0);
selinux_android_restorecon("/dev/random", 0);
selinux_android_restorecon("/dev/urandom", 0);
selinux_android_restorecon("/dev/__properties__", 0);
selinux_android_restorecon("/plat_file_contexts", 0);
selinux_android_restorecon("/nonplat_file_contexts", 0);
selinux_android_restorecon("/plat_property_contexts", 0);
selinux_android_restorecon("/nonplat_property_contexts", 0);
selinux_android_restorecon("/plat_seapp_contexts", 0);
selinux_android_restorecon("/nonplat_seapp_contexts", 0);
selinux_android_restorecon("/plat_service_contexts", 0);
selinux_android_restorecon("/nonplat_service_contexts", 0);
selinux_android_restorecon("/plat_hwservice_contexts", 0);
selinux_android_restorecon("/nonplat_hwservice_contexts", 0);
selinux_android_restorecon("/sepolicy", 0);
selinux_android_restorecon("/vndservice_contexts", 0);
selinux_android_restorecon("/dev/block", SELINUX_ANDROID_RESTORECON_RECURSE);
selinux_android_restorecon("/dev/device-mapper", 0);
selinux_android_restorecon("/sbin/mke2fs_static", 0);
selinux_android_restorecon("/sbin/e2fsdroid_static", 0);
}
创建epoll句柄
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
/* 02. 清空环境变量 */
/* 03. 完成SELinux相关工作 */
epoll_fd = epoll_create1(EPOLL_CLOEXEC); // 调用epoll_create1创建epoll句柄
if (epoll_fd == -1) {
PLOG(ERROR) << "epoll_create1 failed";
exit(1);
}
... ...
}
在linux的网络编程中,很长的时间都在使用 select 来做事件触发。在linux新的内核中,有了一种替换它的机制,就是 epoll。
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的 select 实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
epoll机制一般使用epoll_create(int size)函数创建epoll句柄,size用来告诉内核这个句柄可监听的fd的数目。
注意这个参数不同于select()中的第一个参数,在select中需给出最大监听数加1的值。
此外,当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到创建出的fd,因此在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
上述代码使用的epoll_create1(EPOLL_CLOEXEC)来创建epoll句柄,该标志位表示生成的epoll fd具有“执行后关闭”特性。
装载子进程信号处理器
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
/* 02. 清空环境变量 */
/* 03. 完成SELinux相关工作 */
/* 04. 创建epoll句柄 */
signal_handler_init(); // 装载子进程信号处理器
}
init是一个守护进程,为了防止init的子进程成为僵尸进程(zombie process),需要init在子进程在结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间(程序表的空间达到上限时,系统就不能再启动新的进程了,会引起严重的系统问题)。
在linux当中,父进程是通过捕捉 SIGCHLD 信号来得知子进程运行结束的情况,此处init进程调用 signal_handler_init 的目的就是捕获子进程结束的信号。
我们跟踪下signal_handler_init():
void signal_handler_init() {
// Create a signalling mechanism for SIGCHLD.
int s[2];
// 利用socketpair创建出已经连接的两个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));
// 信号处理器对应的执行函数为SIGCHLD_handler
// 被存在sigaction结构体中,负责处理SIGCHLD消息
act.sa_handler = SIGCHLD_handler;
act.sa_flags = SA_NOCLDSTOP;
// 调用信号安装函数sigaction,将监听的信号及对应的信号处理器注册到内核中
sigaction(SIGCHLD, &act, 0);
// 用于终止出现问题的子进程
ServiceManager::GetInstance().ReapAnyOutstandingChildren();
// 注册信号处理函数handle_signal
register_epoll_handler(signal_read_fd, handle_signal);
}
在深入分析代码前,我们需要了解一些基本概念:Linux进程通过互相发送消息来实现进程间的通信,这些消息被称为“信号”。每个进程在处理其它进程发送的信号时都要注册处理者,处理者被称为信号处理器。
注意到sigaction结构体的sa_flags为SA_NOCLDSTOP。由于系统默认在子进程暂停时也会发送信号SIGCHLD,init需要忽略子进程在暂停时发出的SIGCHLD信号,因此将act.sa_flags 置为SA_NOCLDSTOP,该标志位表示仅当进程终止时才接受SIGCHLD信号。
接下来,我们分步骤详细了解一下signal_handler_init具体的工作流程。
SIGCHLD_handler
// system/core/init/signal_handler.cpp
static void SIGCHLD_handler(int) {
if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {
PLOG(ERROR) << "write(signal_write_fd) failed";
}
}
从上面代码我们知道,init进程是所有进程的父进程,当其子进程终止产生SIGCHLD信号时,SIGCHLD_handler将对signal_write_fd执行写操作。由于socketpair的绑定关系,这将触发信号对应的signal_read_fd收到数据。
register_epoll_handler
根据前文的代码我们知道,在装载信号监听器的最后,signal_handler_init调用了register_epoll_handler,其代码如下所示,注意传入的参数分别为signal_read_fd和handle_signal:
// system/core/init/init.cpp
void register_epoll_handler(int fd, void (*fn)()) {
epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = reinterpret_cast<void*>(fn);
// epoll_fd增加一个监听对象fd,fd上有数据到来时,调用fn处理
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
PLOG(ERROR) << "epoll_ctl failed";
}
}
根据代码不难看出:当epoll句柄监听到signal_read_fd中有数据可读时,将调用handle_signal进行处理。
至此,结合上文我们知道:当init进程调用signal_handler_init后,一旦收到子进程终止带来的SIGCHLD消息后,将利用信号处理者SIGCHLD_handler向signal_write_fd写入信息;由于绑定的关系,epoll句柄将监听到signal_read_fd收到消息,于是将调用handle_signal进行处理。
handle_signal
// system/core/init/signal_handler.cpp
static void handle_signal() {
// Clear outstanding requests.
char buf[32];
read(signal_read_fd, buf, sizeof(buf));
ServiceManager::GetInstance().ReapAnyOutstandingChildren();
}
从代码中可以看出,handle_signal只是清空signal_read_fd中的数据,然后调用ServiceManager::GetInstance().ReapAnyOutstandingChildren()。
ServiceManager定义于 system/core/init/service.cpp 中,是一个单例对象:
... ...
ServiceManager::ServiceManager() {
}
ServiceManager& ServiceManager::GetInstance() {
static ServiceManager instance;
return instance;
}
... ...
void ServiceManager::ReapAnyOutstandingChildren() {
while (ReapOneProcess()) {
}
}
... ...
如上所示,ReapAnyOutstandingChildren函数实际上调用了ReapOneProcess。我们结合代码,看看ReapOneProcess的具体工作。
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.
//用waitid函数获取状态发生变化的子进程pid
//waitid的标记为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;
}
// 利用FindServiceByPid函数,找到pid对应的服务
// FindServiceByPid主要通过轮询解析init.rc生成的service_list,找到pid与参数一致的srvc
Service* svc = FindServiceByPid(pid);
... ... // 输出服务结束的原因
if (!svc) { // 没有找到,说明已经结束了
return true;
}
svc->Reap();
// 根据svc的类型,决定后续的处理方式
if (svc->flags() & SVC_EXEC) {
exec_waiter_.reset(); // 可执行服务则重置对应的waiter
}
if (svc->flags() & SVC_TEMPORARY) {
RemoveService(*svc); // 移除临时服务
}
return true;
}
Reap
void Service::Reap() {
// 清理未携带SVC_ONESHOT 或 携带了SVC_RESTART标志的srvc的进程组
if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {
KillProcessGroup(SIGKILL);
}
// Remove any descriptor resources we may have created.
// 清除srvc中创建出的任意描述符
std::for_each(descriptors_.begin(), descriptors_.end(),
std::bind(&DescriptorInfo::Clean, std::placeholders::_1));
// 清理工作完毕后,后面决定是否重启机器或重启服务
// TEMP服务不用参与这种判断
if (flags_ & SVC_TEMPORARY) {
return;
}
pid_ = 0;
flags_ &= (~SVC_RUNNING);
// Oneshot processes go into the disabled state on exit,
// except when manually restarted.
// 对于携带了SVC_ONESHOT并且未携带SVC_RESTART的srvc,将这类服务的标志置为SVC_DISABLED,不再自启动
if ((flags_ & SVC_ONESHOT) && !(flags_ & SVC_RESTART)) {
flags_ |= SVC_DISABLED;
}
// Disabled and reset processes do not get restarted automatically.
if (flags_ & (SVC_DISABLED | SVC_RESET)) {
NotifyStateChange("stopped");
return;
}
// If we crash > 4 times in 4 minutes, reboot into recovery.
boot_clock::time_point now = boot_clock::now();
// 未携带SVC_RESTART的关键服务,在规定的间隔内,crash字数过多时,会导致整机重启;
if ((flags_ & SVC_CRITICAL) && !(flags_ & SVC_RESTART)) {
if (now < time_crashed_ + 4min) {
if (++crash_count_ > 4) {
LOG(ERROR) << "critical process '" << name_ << "' exited 4 times in 4 minutes";
panic();
}
} else {
time_crashed_ = now;
crash_count_ = 1;
}
}
// 将待重启srvc的标志位置为SVC_RESTARTING(init进程将根据该标志位,重启服务)
flags_ &= (~SVC_RESTART);
flags_ |= SVC_RESTARTING;
// Execute all onrestart commands for this service.
// 重启在init.rc文件中带有onrestart选项的服务
onrestart_.ExecuteAllCommands();
NotifyStateChange("restarting");
return;
}
不难看出,Reap函数的主要作用就是清除问题进程相关的资源,然后根据进程对应的类型,决定是否重启机器或重启进程。
ExecuteAllCommands
我们在这一部分的最后,看看定义于system/core/init/Action.cpp中的ExecuteAllCommands函数:
void Action::ExecuteAllCommands() const {
for (const auto& c : commands_) {
ExecuteCommand(c);
}
}
void Action::ExecuteCommand(const Command& command) const {
android::base::Timer t;
// 进程重启时,将执行对应的函数
int result = command.InvokeFunc();
// 打印log
auto duration = t.duration();
// Any action longer than 50ms will be warned to user as slow operation
if (duration > 50ms || android::base::GetMinimumLogSeverity() <= android::base::DEBUG) {
std::string trigger_name = BuildTriggersString();
std::string cmd_str = command.BuildCommandString();
LOG(INFO) << "Command '" << cmd_str << "' action=" << trigger_name << " (" << filename_
<< ":" << command.line() << ") returned " << result << " took "
<< duration.count() << "ms.";
}
}
整个signal_handler_init的内容比较多,在此总结一下:signal_handler_init的本质就是监听子进程死亡的信息,然后进行对应的清理工作,并根据死亡进程的类型,决定是否需要重启进程或机器。
启动属性服务
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
/* 02. 清空环境变量 */
/* 03. 完成SELinux相关工作 */
/* 04. 创建epoll句柄 */
/* 05. 装载子进程信号处理器 */
property_load_boot_defaults(); // 进程调用property_load_boot_defaults进行默认属性配置相关的工作
export_oem_lock_status(); // 最终就是决定"ro.boot.flash.locked"的值
start_property_service(); // 启动属性服务
set_usb_controller();
... ...
}
老样子,这边我们跟踪几个重要的函数。
property_load_boot_defaults
void property_load_boot_defaults() {
// 就是从各种路径读取默认配置
// load_properties_from_file的基本操作就是read_file,然后解析并设置
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(); // 就是设置"persist.sys.usb.config"相关的配置
}
如代码所示,property_load_boot_defaults 实际上就是调用 load_properties_from_file 解析配置文件,然后根据解析的结果,设置系统属性。
start_property_service
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);
}
// 调用listen函数监听property_set_fd, 于是该socket变成一个server
listen(property_set_fd, 8);
// 监听server socket上是否有数据到来
register_epoll_handler(property_set_fd, handle_property_set_fd);
}
init进程在共享内存区域中,创建并初始化属性域。其它进程可以访问属性域中的值,但更改属性值仅能在init进程中进行。这就是init进程调用start_property_service的原因。
其它进程修改属性值时,要预先向init进程提交值变更申请,然后init进程处理该申请,并修改属性值。在访问和修改属性时,init进程都可以进行权限控制。
匹配命令和函数之间对应关系
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
/* 02. 清空环境变量 */
/* 03. 完成SELinux相关工作 */
/* 04. 创建epoll句柄 */
/* 05. 装载子进程信号处理器 */
/* 06. 启动属性服务*/
const BuiltinFunctionMap function_map; // system/core/init/builtins.cpp,定义Action中的function_map_为BuiltinFuntionMap
Action::set_function_map(&function_map); // 在Action中保存function_map对象,记录了命令与函数之间的对应关系
/* 07. 匹配命令和函数之间对应关系 */
/* ------------ 第二阶段 ------------ END ------------ */
... ...
}
至此,init进程的准备工作执行完毕, 接下来就要开始解析init.rc文件了。
第三阶段(init.rc)
解析init.rc
int main(int argc, char** argv) {
/* ------------ 第一阶段 ------------ BEGIN------------ */
/* 01. 判断及增加环境变量 */
/* 02. 创建文件系统目录并挂载相关的文件系统 */
/* 03. 重定向输入输出/内核Log系统 */
/* 04. 挂在一些分区设备 */
/* 05. 完成SELinux相关工作 */
/* 06. is_first_stage 收尾 */
/* ------------ 第一阶段 ------------- END ------------ */
/* ------------ 第二阶段 ------------ BEGIN------------ */
/* 01. 初始化属性域 */
/* 02. 清空环境变量 */
/* 03. 完成SELinux相关工作 */
/* 04. 创建epoll句柄 */
/* 05. 装载子进程信号处理器 */
/* 06. 启动属性服务*/
/* 07. 匹配命令和函数之间对应关系 */
/* ------------ 第二阶段 ------------ END ------------ */
/* ------------ 第三阶段 ----------- BEGIN------------ */
ActionManager& am = ActionManager::GetInstance();
ServiceManager& sm = ServiceManager::GetInstance();
Parser& parser = Parser::GetInstance(); // 构造解析文件用的parser对象
// 为一些类型的关键字,创建特定的parser
parser.AddSectionParser("service", std::make_unique<ServiceParser>(&sm)); // 增加ServiceParser为一个section,对应name为service
parser.AddSectionParser("on", std::make_unique<ActionParser>(&am)); // 增加ActionParser为一个section,对应name为action
parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser)); // 增加ActionParser为一个section,对应name为import
std::string bootscript = GetProperty("ro.boot.init_rc", ""); // 判断是否存在bootscript
// 如果没有bootscript,则解析init.rc文件
if (bootscript.empty()) { // 8.0引入
parser.ParseConfig("/init.rc"); // 开始实际的解析过程
parser.set_is_system_etc_init_loaded(
parser.ParseConfig("/system/etc/init"));
parser.set_is_vendor_etc_init_loaded(
parser.ParseConfig("/vendor/etc/init"));
parser.set_is_odm_etc_init_loaded(parser.ParseConfig("/odm/etc/init"));
} else {
// 若存在bootscript, 则解析bootscript
parser.ParseConfig(bootscript);
parser.set_is_system_etc_init_loaded(true);
parser.set_is_vendor_etc_init_loaded(true);
parser.set_is_odm_etc_init_loaded(true);
}
... ...
}
如果没有定义bootScript,那么init进程还是会解析init.rc文件。init.rc文件是在init进程启动后执行的启动脚本,文件中记录着init进程需执行的操作。
此处解析函数传入的参数为“/init.rc”,解析的是运行时与init进程同在根目录下的init.rc文件。该文件在编译前,定义于system/core/rootdir/init.rc中。
✨ 继续往下分析main函数之前;
✨ 我们先了解一下init.rc是什么,然后分析下parser解析init.rc过程;
✨ 最后我们再继续跟源码!
init.rc配置文件
init.rc是一个配置文件,内部由Android初始化语言编写(Android Init Language)编写的脚本,主要包含五种类型语句:Action、Command、Service、Option 和 Import,在分析代码的过程中我们会详细介绍。
init.rc的配置代码在:system/core/rootdir/init.rc 中。
init.rc文件是在init进程启动后执行的启动脚本,文件中记录着init进程需执行的操作。
init.rc文件大致分为两大部分:
一部分是以“on”关键字开头的 动作列表(action list):
on early-init // Action类型语句
# Set init and its forked children's oom_adj. // #:注释符号
write /proc/1/oom_score_adj -1000
... ...
# Shouldn't be necessary, but sdcard won't start without it. http://b/22568628.
mkdir /mnt 0775 root system
... ...
start ueventd
Action类型语句格式:
on <trigger> [&& <trigger>]* // 设置触发器
<command>
<command> // 动作触发之后要执行的命令
另一部分是以“service”关键字开头的 服务列表(service list): 如 Zygote
service ueventd /sbin/ueventd // Service类型语句
class core
critical
seclabel u:r:ueventd:s0
Service类型语句格式:
service <name> <pathname> [ <argument> ]* // <service的名字><执行程序路径><传递参数>
<option> // option是service的修饰词,影响什么时候、如何启动services
<option>
...
借助系统环境变量或Linux命令,
Android 8.1 源码_启动篇(一) -- 深入研究 init(转 Android 9.0 分析)的更多相关文章
- Android 8.1 源码_启动篇(二) -- 深入研究 zygote(转 Android 9.0 分析)
前言 在Android中,zygote是整个系统创建新进程的核心进程.zygote进程在内部会先启动Dalvik虚拟机,继而加载一些必要的系统资源和系统类,最后进入一种监听状态.在之后的运作中,当其他 ...
- 第一部分:开发前的准备-第八章 Android SDK与源码下载
第8章 Android SDK与源码下载 如果你是新下载的SDK,请阅读一下步骤了解如何设置SDK.如果你已经下载使用过SDK,那么你应该使用AVD Manager,来更新即可. 下面是构建Andro ...
- jQuery2.x源码解析(构建篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...
- jQuery2.x源码解析(设计篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...
- jQuery2.x源码解析(回调篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...
- Android 开源项目源码解析(第二期)
Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...
- 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(下)
关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/12/es-code03/ 前提 上篇文章写完了 ES 流程启动的一部分,main 方法都入 ...
- 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(上)
关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/11/es-code02/ 前提 上篇文章写了 ElasticSearch 源码解析 -- ...
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
随机推荐
- async/await 的基本实现和 .NET Core 2.1 中相关性能提升
前言 这篇文章的开头,笔者想多说两句,不过也是为了以后再也不多嘴这样的话. 在日常工作中,笔者接触得最多的开发工作仍然是在 .NET Core 平台上,当然因为团队领导的开放性和团队风格的多样性(这和 ...
- sublime中安装sublimecodeintel插件
本文是基于在Windows上对sublime进行相关配置. 1.安装sublime,在官网http://www.sublimetext.com/3. 如果是在Linux系统上安装只需要输入命令直接安装 ...
- Kotlin : Retrofit + RxAndroid + Realm
https://jqs7.com/kotlin-retrofit-rxandroid-realm/ 原作者:Ahmed Rizwan 原文链接:Kotlin : Retrofit + RxAndroi ...
- 【转】asp.net获取当前页面的url地址
设当前页完整地址是:http://www.jb51.net/aaa/bbb.aspx?id=5&name=kelli "http://"是协议名 "www.jb5 ...
- Jupyter-notebook 导出时不显示Input[]代码
参考: https://stackoverflow.com/questions/34818723/export-notebook-to-pdf-without-code 1. 第一个方式是直接在 ...
- Linux用户登录日志查询
# 1 utmp.wtmp.btmp文件 Linux用户登录信息放在三个文件中: 1 /var/run/utmp:记录当前正在登录系统的用户信息,默认由who和w记录当前登录用户的信息,uptime记 ...
- 基于Go的websocket消息服务
3个月没写PHP了,这是我的第一个中小型go的websocket微服务.那么问题来了,github上那么多轮子,我为什么要自己造轮子呢? Why 造轮子? 因为这样不仅能锻炼自己的技术能力,而且能帮助 ...
- sql server 高可用故障转移(上)
群集准备工作 个人电脑 内存12G,处理器 AMD A6-3650CPU主频2.6GHz 虚拟机 VMware Workstation 12 数据库 sql server 2008 r2 三台虚拟服 ...
- genymotion的安装及运行
一.下载工具 安装genymontion一共需要下载三个东西,分别是genymotion.虚拟机virtualbox和ova 笔者提供百度云下载:mac版虚拟机 mac上genymotion.wind ...
- 一个相对健壮的node 静态http服务器
先上代码,然后说说坑,算是一个总结,以后在mac上就用这个开启服务调试了,挺好.然后接着想写一个动态的返回页面的,刚好练手mv*的框架. var http = require('http'); var ...