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代码的流程,我们放到下一篇博客介绍。

猜你喜欢

转载自blog.csdn.net/gaugamela/article/details/79287649