Android O: init进程启动流程分析(阶段二)
Posted ZhangJianIsAStark
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android O: init进程启动流程分析(阶段二)相关的知识,希望对你有一定的参考价值。
在前一篇博客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代码的流程,我们放到下一篇博客介绍。
以上是关于Android O: init进程启动流程分析(阶段二)的主要内容,如果未能解决你的问题,请参考以下文章