Android O: init进程启动流程分析(阶段二)
2018-02-08 16:23
579 查看
在前一篇博客Android O: init进程启动流程分析(阶段一)中,
我们分析了init进程第一阶段(内核态)的流程。
在本篇博客中,我们来看看init进程第二阶段(用户态)的工作。
一、初始化属性域
init进程的第二阶段仍然从main函数开始入手。
这部分代码主要的工作应该就是调用property_init初始化属性域,
然后设置各种属性了。
在Android平台中,为了让运行中的所有进程共享系统运行时所需要的各种设置值,
系统开辟了属性存储区域,并提供了访问该区域的API。
property_init函数定义于system/core/init/property_service.cpp中,
如下面代码所示,最终调用_system_property_area_init函数初始化属性域。
二、清空环境变量,完成selinux相关的工作
我们回到main函数,看看接下来的工作:
在init进程的第一阶段,也调用selinux_initialize函数,
主要加载selinux相关的策略。
第二阶段调用selinux_initialize仅仅注册一些处理器:
selinux_restore_context()的作用主要是按selinux policy要求,
重新设置一些文件的属性:
三、创建epoll句柄
接下来如下面代码所示,init进程调用epoll_create1创建epoll句柄。
在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中。
init是一个守护进程,为了防止init的子进程成为僵尸进程(zombie process),
需要init在子进程在结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,
防止成为僵尸进程的子进程占用程序表的空间(程序表的空间达到上限时,系统就不能再启动新的进程了,会引起严重的系统问题)。
在linux当中,父进程是通过捕捉SIGCHLD信号来得知子进程运行结束的情况,
此处init进程调用signal_handler_init的目的就是捕获子进程结束的信号。
我们来看看signal_handler_init相关的代码:
在深入分析代码前,我们需要了解一些基本概念:
Linux进程通过互相发送消息来实现进程间的通信,这些消息被称为“信号”。
每个进程在处理其它进程发送的信号时都要注册处理者,处理者被称为信号处理器。
注意到sigaction结构体的sa_flags为SA_NOCLDSTOP。
由于系统默认在子进程暂停时也会发送信号SIGCHLD,init需要忽略子进程在暂停时发出的SIGCHLD信号,
因此将act.sa_flags 置为SA_NOCLDSTOP,该标志位表示仅当进程终止时才接受SIGCHLD信号。
signal_handler_init需要关注的内容还是比较多的,我们分步骤来看看。
4.1、SIGCHLD_handler
我们先来看看SIGCHLD_handler的具体工作。
从上面代码我们知道,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:
根据代码不难看出:
当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中:
从代码中可以看出,handle_signal只是清空signal_read_fd中的数据,
然后调用ServiceManager::GetInstance().ReapAnyOutstandingChildren()。
ServiceManager定义于system/core/init/service.cpp中,是一个单例对象:
如上所示,ReapAnyOutstandingChildren函数实际上调用了ReapOneProcess。
我们结合代码,看看ReapOneProcess的具体工作。
上文中,waitpid的函数原型为:
其中:
第一个参数pid为预等待的子进程的识别码,pid=-1表示等待任何子进程是否发出SIGCHLD。
第二个参数status,用于返回子进程的结束状态。
第三个参数决定waitpid函数是否处于阻塞处理方式;
WNOHANG表示若pid指定的子进程没有结束,则waitpid()函数返回0,不予等待;
若子进程结束,则返回子进程的pid。
waitpid如果出错,则返回-1。
容易看出handle_signal的主要作用就是找出出现问题的进程,
然后调用对应的Reap函数处理。
4.4、Reap
我们来看看Service的Reap函数:
不难看出,Reap函数的主要作用就是清除问题进程相关的资源,
然后根据进程对应的类型,决定是否重启机器或重启进程。
4.5、ExecuteAllCommands
我们在这一部分的最后,看看定义于system/core/init/Action.cpp中的ExecuteAllCommands函数:
整个signal_handler_init的内容比较多,在此总结一下:
signal_handler_init的本质就是监听子进程死亡的信息,
然后进行对应的清理工作,并根据死亡进程的类型,
决定是否需要重启进程或机器。
上述过程其实最终可以简化为下图:
五、设置默认系统属性及启动配置属性的服务端
我们重新将视角拉回到init的main函数,看看接下来的工作:
这部分工作最终都会更改一些系统属性。
5.1、property_load_boot_defaults
我们先来看看property_load_boot_defaults函数的内容:
如代码所示,property_load_boot_defaults实际上就是调用load_properties_from_file解析配置文件;
然后根据解析的结果,设置系统属性。
该部分功能较为单一,不再深入分析。
5.2、start_property_service
我们再来看看start_property_service函数的内容:
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的具体内容:
从上面的代码可以看出:
handle_propery_set_fd函数实际上是调用accept函数监听连接请求。
收到请求后,就会建立socket通信,并利用recv函数接受到来的数据。
最后根据到来数据的类型,进行设置系统属性等相关操作。
在这一部分的最后,我们简单举例介绍一下,系统属性改变的一些用途。
在init.rc中定义了一些与属性相关的触发器。
当某个条件相关的属性被改变时,与该条件相关的触发器就会被触发。
举例来说,如下面代码所示,debuggable属性变为1时,将执行启动console进程等操作。
总结一下,其它进程修改系统属性时,大致的流程如下图所示:
其它的进程像init进程发送请求后,由init进程检查权限后,修改共享内存区。
六、总结
至此,init进程的准备工作执行完毕,
接下来就要开始解析init.rc文件了。
解析init.rc代码的流程,我们放到下一篇博客介绍。
我们分析了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进程启动流程分析(阶段三)
- Android O: init进程启动流程分析(阶段一)
- Android启动流程分析(二) init进程的启动
- Android启动流程分析(四) init进程分析
- Android启动流程分析(三) init进程初窥
- 分析Android 根文件系统启动过程(init守护进程分析)
- ARM-Linux移植之(三)——init进程启动流程分析
- ARM-Linux移植之(三)——init进程启动流程分析
- Android启动流程分析(七) init.rc的解析
- Android init进程启动分析
- 分析Android 根文件系统启动过程(init守护进程分析)
- MTD系列 - android平台上linux启动时init进程解析init.rc文件分析
- Android系统启动流程(一)解析init进程启动过程
- android系统启动流程之init.rc详细分析笔记
- 分析Android 根文件系统启动过程(init守护进程分析)
- 分析Android 根文件系统启动过程(init守护进程分析)
- 内核启动阶段kernel_init(init)进程分析
- 分析Android 根文件系统启动过程(init守护进程分析)
- 分析Android 根文件系统启动过程(init守护进程分析)
- Android情景分析之详解init进程(以启动zygote为例)