您的位置:首页 > 移动开发 > Android开发

Android O: init进程启动流程分析(阶段二)

2018-02-08 16:23 579 查看
在前一篇博客Android O: init进程启动流程分析(阶段一)中,

我们分析了init进程第一阶段(内核态)的流程。

在本篇博客中,我们来看看init进程第二阶段(用户态)的工作。

一、初始化属性域

init进程的第二阶段仍然从main函数开始入手。

int main(int argc, char** argv) {
//同样进行一些判断及环境变量设置的工作
..........
//现在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.
// 同样屏蔽标准输入输出及定义Kernel logger
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.
// 最后调用syscall,设置安全相关的值
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));

//初始化属性域
property_init();

//初始化完属性域后,以下均完成一些属性的设定

// 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中,

如下面代码所示,最终调用_system_property_area_init函数初始化属性域。

void property_init() {
if (__system_property_area_init()) {
LOG(ERROR) << "Failed to initialize property area";
exit(1);
}
}


二、清空环境变量,完成selinux相关的工作

我们回到main函数,看看接下来的工作:

.......
// Clean up our environment.
// 清除掉之前使用过的环境变量
unsetenv("INIT_SECOND_STAGE");
unsetenv("INIT_STARTED_AT");
unsetenv("INIT_SELINUX_TOOK");
unsetenv("INIT_AVB_VERSION");

// Now set up SELinux for second stage.
// 再次完成selinux相关的工作
selinux_initialize(false);
selinux_restore_context();
..............


在init进程的第一阶段,也调用selinux_initialize函数,

主要加载selinux相关的策略。

第二阶段调用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
// 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...";
restorecon("/dev");
restorecon("/dev/kmsg");
restorecon("/dev/socket");
restorecon("/dev/random");
restorecon("/dev/urandom");
restorecon("/dev/__properties__");

restorecon("/file_contexts.bin");
restorecon("/plat_file_contexts");
restorecon("/nonplat_file_contexts");
restorecon("/plat_property_contexts");
restorecon("/nonplat_property_contexts");
restorecon("/plat_seapp_contexts");
restorecon("/nonplat_seapp_contexts");
restorecon("/plat_service_contexts");
restorecon("/nonplat_service_contexts");
restorecon("/plat_hwservice_contexts");
restorecon("/nonplat_hwservice_contexts");
restorecon("/sepolicy");
restorecon("/vndservice_contexts");

restorecon("/sys", SELINUX_ANDROID_RESTORECON_RECURSE);
restorecon("/dev/block", SELINUX_ANDROID_RESTORECON_RECURSE);
restorecon("/dev/device-mapper");
}


三、创建epoll句柄

接下来如下面代码所示,init进程调用epoll_create1创建epoll句柄。

.............
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
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具有“执行后关闭”特性。

四、装载子进程信号处理器

紧接着,init进程调用signal_handler_init装载子进程信号处理器,

该函数定义于system/core/init/signal_handler.cpp中。

.............
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需要关注的内容还是比较多的,我们分步骤来看看。

4.1、SIGCHLD_handler

我们先来看看SIGCHLD_handler的具体工作。

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收到数据。

4.2、 register_epoll_handler

根据前文的代码我们知道,在装载信号监听器的最后,

signal_handler_init调用了register_epoll_handler,

其代码如下所示,注意传入的参数分别为signal_read_fd和handle_signal:

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进行处理。

整个过程如下图所示:



4.3、 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中,是一个单例对象:

............
//C++中默认是private属性
ServiceManager::ServiceManager() {
}

ServiceManager& ServiceManager::GetInstance() {
static ServiceManager instance;
return instance;
}
............

void ServiceManager::ReapAnyOutstandingChildren() {
while (ReapOneProcess()) {
}
}
............


如上所示,ReapAnyOutstandingChildren函数实际上调用了ReapOneProcess。

我们结合代码,看看ReapOneProcess的具体工作。

bool ServiceManager::ReapOneProcess() {
int status;
//用waitpid函数获取状态发生变化的子进程pid
//waitpid的标记为WNOHANG,即非阻塞,返回为正值就说明有进程挂掉了
pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, WNOHANG));
if (pid == 0) {
return false;
} else if (pid == -1) {
PLOG(ERROR) << "waitpid failed";
return false;
}

//利用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) {
//可执行服务则重置对应的waiter
exec_waiter_.reset();
}
if (svc->flags() & SVC_TEMPORARY) {
//移除临时服务
RemoveService(*svc);
}
return true;
}


上文中,waitpid的函数原型为:

pid_t waitpid(pid_t pid, int *status, int options)


其中:

第一个参数pid为预等待的子进程的识别码,pid=-1表示等待任何子进程是否发出SIGCHLD。

第二个参数status,用于返回子进程的结束状态。

第三个参数决定waitpid函数是否处于阻塞处理方式;

WNOHANG表示若pid指定的子进程没有结束,则waitpid()函数返回0,不予等待;

若子进程结束,则返回子进程的pid。

waitpid如果出错,则返回-1。

容易看出handle_signal的主要作用就是找出出现问题的进程,

然后调用对应的Reap函数处理。

4.4、Reap

我们来看看Service的Reap函数:

bool 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 true;
}

// 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 true;
}


不难看出,Reap函数的主要作用就是清除问题进程相关的资源,

然后根据进程对应的类型,决定是否重启机器或重启进程。

4.5、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 {
Timer t;
//进程重启时,将执行对应的函数
int result = command.InvokeFunc();
//打印log
double duration_ms = t.duration_s() * 1000;
// Any action longer than 50ms will be warned to user as slow operation
if (duration_ms > 50.0 ||
android::base::GetMinimumLogSeverity() <= android::base::DEBUG) {
.................
}
}


整个signal_handler_init的内容比较多,在此总结一下:

signal_handler_init的本质就是监听子进程死亡的信息,

然后进行对应的清理工作,并根据死亡进程的类型,

决定是否需要重启进程或机器。

上述过程其实最终可以简化为下图:



五、设置默认系统属性及启动配置属性的服务端

我们重新将视角拉回到init的main函数,看看接下来的工作:

..............
property_load_boot_defaults();

//最终就是决定"ro.boot.flash.locked"的值
export_oem_lock_status();

start_property_service();

//最终就是决定"sys.usb.controller"的值
set_usb_controller();
..............


这部分工作最终都会更改一些系统属性。

5.1、property_load_boot_defaults

我们先来看看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);

//就是设置"persist.sys.usb.config"相关的配置
update_sys_usb_config();
}


如代码所示,property_load_boot_defaults实际上就是调用load_properties_from_file解析配置文件;

然后根据解析的结果,设置系统属性。

该部分功能较为单一,不再深入分析。

5.2、start_property_service

我们再来看看start_property_service函数的内容:

void start_property_service() {
property_set("ro.property_service.version", "2");

//创建了一个非阻塞socket
property_set_fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
0666, 0, 0, NULL);
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进程都可以进行权限控制。

5.2.1、题外话

我们知道,在create_socket函数返回套接字property_set_fd时,property_set_fd是一个主动连接的套接字。

此时,系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接。

由于在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接,

于是需要调用listen函数使用主动连接套接字变为被连接套接字,

使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。

因此,上述代码调用listen后,init进程成为一个服务进程,

其它进程可以通过property_set_fd连接init进程,提交设置系统属性的申请。

listen函数的第二个参数,涉及到一些网络的细节。

在进程处理一个连接请求的时候,可能还存在其它的连接请求。

因为TCP连接是一个过程,所以可能存在一种半连接的状态。

有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。

因此,内核会在自己的进程空间里维护一个队列,以跟踪那些已完成连接但服务器进程还没有接手处理的用户,

或正在进行的连接的用户。

这样的一个队列不可能任意大,所以必须有一个上限。

listen的第二个参数就是告诉内核使用这个数值作为上限。

因此,init进程作为系统属性设置的服务器,最多可以同时为8个试图设置属性的用户提供服务。

5.2.2、handle_property_set_fd

从前文可以看到,在启动配置属性服务的最后,调用函数register_epoll_handler。

前文已经分析过register_epoll_handler函数,该函数将利用之前创建出的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 */

//接受请求
int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
if (s == -1) {
return;
}

//构造对应的socket
struct ucred cr;
socklen_t cr_size = sizeof(cr);
if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
close(s);
PLOG(ERROR) << "sys_prop: unable to get SO_PEERCRED";
return;
}

//创建socket connection
SocketConnection socket(s, cr);
uint32_t timeout_ms = kDefaultSocketTimeout;

uint32_t cmd = 0;
//收取消息存入cmd
if (!socket.RecvUint32(&cmd, &timeout_ms)) {
PLOG(ERROR) << "sys_prop: error while reading command from the socket";
socket.SendUint32(PROP_ERROR_READ_CMD);
return;
}

//根据cmd执行对应的操作
switch(cmd) {
case PROP_MSG_SETPROP: {
.........
handle_property_set(socket, prop_value, prop_value, true);
break;
}
.........
}
}


从上面的代码可以看出:

handle_propery_set_fd函数实际上是调用accept函数监听连接请求。

收到请求后,就会建立socket通信,并利用recv函数接受到来的数据。

最后根据到来数据的类型,进行设置系统属性等相关操作。

在这一部分的最后,我们简单举例介绍一下,系统属性改变的一些用途。

在init.rc中定义了一些与属性相关的触发器。

当某个条件相关的属性被改变时,与该条件相关的触发器就会被触发。

举例来说,如下面代码所示,debuggable属性变为1时,将执行启动console进程等操作。

on property:ro.debuggable=1
# Give writes to anyone for the trace folder on debug builds.
# The folder is used to store method traces.
chmod 0773 /data/misc/trace
start console


总结一下,其它进程修改系统属性时,大致的流程如下图所示:

其它的进程像init进程发送请求后,由init进程检查权限后,修改共享内存区。



六、总结

至此,init进程的准备工作执行完毕,

接下来就要开始解析init.rc文件了。

解析init.rc代码的流程,我们放到下一篇博客介绍。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: