android 开机启动流程分析(三)init启动中关键进程 uevent & watchdog

系列文章解读&说明:

启动流程的分析主要分为这样几个部分:

(一)init之前启动说明

(二)init的启动流程分析

(三)init启动中关键进程 uevent & watchdog

(四)init启动中关键服务-属性服务

(五)SE Android 的解读

(六)init.rc解析流程

(七)action队列分析

(八)无限循环的处理

(九)bootchart 解读

本模块分享的内容:init启动中关键进程 uevent & watchdog

本章关键点总结 & 说明:

android 开机启动流程分析(三)init启动中关键进程 uevent & watchdog

说明:思维导图是基于之前文章不断迭代的,本章内容我们关注➕uevent & watchdog部分即可

1 Uevent入口

Uevent是接收uevent的守护进程,这里它的主要作用根据kernel接收到的uevent事件来创建或删除/dev/xxx(xxx设备名),主函数实现如下:

int ueventd_main(int argc, char **argv)
{
    struct pollfd ufd;
    int nr;
    char tmp[32];

    umask(000);
    signal(SIGCHLD, SIG_IGN);
    open_devnull_stdio();//输入输出重定向
    klog_init();
#if LOG_UEVENTS
    /* Ensure we're at a logging level that will show the events */
    if (klog_get_level() < KLOG_INFO_LEVEL) {
        klog_set_level(KLOG_INFO_LEVEL);
    }
#endif
    //selinux相同,同init
    union selinux_callback cb;
    cb.func_log = log_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);
    INFO("starting ueventd\n");

    //----1 解析和处理ueventd的rc文件,start
    import_kernel_cmdline(0, import_kernel_nv);
    get_hardware_name(hardware, &revision);
    ueventd_parse_config_file("/ueventd.rc");//<关键点1,解析配置文件>
    snprintf(tmp, sizeof(tmp), "/ueventd.%s.rc", hardware);
    ueventd_parse_config_file(tmp);
    //----1 解析和处理ueventd的rc文件,end
    //----2 polling uevent消息,对设备进行管理,start
    device_init();//设备初始化
    ufd.events = POLLIN;
    ufd.fd = get_device_fd();//获取由device_init中uevent_open_socket打开的device_fd
    while(1) {
        ufd.revents = 0;
        nr = poll(&ufd, 1, -1);//poll监听ufd
        if (nr <= 0)
            continue;
        if (ufd.revents & POLLIN) // polling到消息,处理event消息
            handle_device_fd();
    }
    //----2 polling uevent消息,对设备进行管理,end
}

1.1  解析和处理uevent的rc文件

解析流程参考后面的init.rc的解析流程,流程如下:

/**
ueventd_parse_config_file
{根据rc文件,生成一个参考数据结构图}
  ->parse_config
  -->loop循环
  -->int token = next_token(&state);
  -->根据token执行parse_line
  --->lookup_keyword,查找关键字
  --->分支kw_is(kw, SECTION)
  ---->parse_new_section(state, kw, nargs, args);
  ----->根据kw获取不同的parse_line,这里设置为parse_line_subsystem或no_op{空操作}
  ------>parse_line_subsystem中调用lookup_keyword();->根据kw 获取s->devname_src和s->dirname
  --->分支kw_is(kw, OPTION)
  ---->state->parse_line(state, nargs, args);
  --->分支,其他,即表示根据rc文件创建数据结构,为后期创建节点做数据参考图
  ---->parse_line_device(state, nargs, args);->set_device_permission()
  ----->获取(name, attr, perm, uid, gid, prefix, wildcard)参数
  ----->add_dev_perms(name, attr, perm, uid, gid, prefix, wildcard);
*/

注意:ueventd_parse_config_file并不创建设备节点,它的作用是提供数据库,当有设备节点生成的时候,eventd会参考这个数据库设置设备节点的权限。处理和解析ueventd.rc这部分相比init.rc简单,主要是通过解析rc文件,实现控制目录节点的权限,如:

/dev/ttyUSB2              0666   radio   radio
/dev/ts0710mux*           0640   radio   radio
/dev/ppp                  0666   radio   v*n
# sysfs properties
/sys/devices/virtual/input/input*   enable      0666  system   system
/sys/devices/virtual/input/input*   poll_delay  0666  system   system

1.2 polling uevent消息,对设备进行管理

这里主要针对device_init(),get_device_fd()与handle_device_fd()进行分析。

1.2.1 device_init()实现如下:

/**
主要是创建了uevent的socket handle,同时触发/sys/clas,/sys/block,/sys/devices
这三个目录及其子目录下的uevent,然后接受并创建设备节点
*/
void device_init(void)
{
    suseconds_t t0, t1;
    struct stat info;
    int fd;
    ...//SELinux相关
    /* is 256K enough? udev uses 16MB! */
    //打开uevent的socket。这里的uevent是用到netlink中的内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)功能
    //是内核和用户态进行双向数据传输的非常好的方式,除了eventd外,netd和vold也是使用uevent的
    device_fd = uevent_open_socket(256*1024, true);
    if(device_fd < 0)
        return;
    fcntl(device_fd, F_SETFD, FD_CLOEXEC);
    fcntl(device_fd, F_SETFL, O_NONBLOCK);
    if (stat(coldboot_done, &info) < 0) {
        t0 = get_usecs();
        coldboot("/sys/class");
        coldboot("/sys/block");
        coldboot("/sys/devices");
        t1 = get_usecs();
        fd = open(coldboot_done, O_WRONLY|O_CREAT, 0000);
        close(fd);
        log_event_print("coldboot %ld uS\n", ((long) (t1 - t0)));
    } else {
        log_event_print("skipping coldboot, already done\n");
    }
}

这里关键分析coldboot进而分析,代码实现如下:

static void coldboot(const char *path)
{
    DIR *d = opendir(path);
    if(d) {
        do_coldboot(d);
        closedir(d);
    }
}
static void do_coldboot(DIR *d)
{
    struct dirent *de;
    int dfd, fd;
    dfd = dirfd(d);
    fd = openat(dfd, "uevent", O_WRONLY);
    if(fd >= 0) {
        write(fd, "add\n", 4);//**内核且重发add事件的uevent
        close(fd);
        handle_device_fd();//针对event消息做响应的处理
    }
    while((de = readdir(d))) {
        DIR *d2;
        if(de->d_type != DT_DIR || de->d_name[0] == '.')
            continue;
        fd = openat(dfd, de->d_name, O_RDONLY | O_DIRECTORY);
        if(fd < 0)
            continue;
        d2 = fdopendir(fd);
        if(d2 == 0)
            close(fd);
        else {
            do_coldboot(d2);//递归调用函数
            closedir(d2);
        }
    }
}

继续,关键分析handle_device_fd()的处理,实现如下:

void handle_device_fd()
{
    char msg[UEVENT_MSG_LEN+2];
    int n;
    //接收kernel通过netlink的socket方式发送的msg事件
    while ((n = uevent_kernel_multicast_recv(device_fd, msg, UEVENT_MSG_LEN)) > 0) {
        if(n >= UEVENT_MSG_LEN)   /* overflow -- discard */
            continue;
        msg[n] = '\0';
        msg[n+1] = '\0';
        struct uevent uevent;
        parse_event(msg, &uevent);//将msg解析成uevent
        ...//SELinux相关
        handle_device_event(&uevent);
        handle_firmware_event(&uevent);
    }
}

1.2.2 关注➕handle_device_event 和handle_firmware_event的分析

handle_device_event的处理流程如下所示(部分给出伪代码,主要分析流程):

static void handle_device_event(struct uevent *uevent)
{
    if (!strcmp(uevent->action,"add") || !strcmp(uevent->action, "change") 
        || !strcmp(uevent->action, "online"))
        fixup_sys_perms(uevent->path);    //通过之前的数据库查表,重新修正节点权限
    if (!strncmp(uevent->subsystem, "block", 5)) {
        /**
           handle_block_device_event(struct uevent *uevent)的处理流程如下:
		   {根据数据库创建对应的节点}
            ->parse_device_name(),根据uevent解析出name
            ->make_dir(),根据解析出的name创建文件夹
            ->对links类型文件进行处理
            ->handle_device() 处理
            -->make_device()
            --->get_device_perm()获取设备的perm链表节点
            --->mknod创建设备节点+setegid变更节点权限+chown设置节点权限
         */
        handle_block_device_event(uevent);
    } else if (!strncmp(uevent->subsystem, "platform", 8)) {
        /**
           handle_platform_device_event(struct uevent *uevent)
            ->分支add_platform_device(path);
            -->platform节点添加操作
            ->分支remove_platform_device(path);
            -->platform节点删除操作
         */
        handle_platform_device_event(uevent);
    } else {
        /**
		   handle_generic_device_event(struct uevent *uevent)
			{根据subsystem创建各种类型文件夹}
			->分支 subsystem 不为空
			-->parse_device_name(),根据uevent解析出name
			-->ueventd_subsystem_find_by_name
			-->mkdir_recursive_for_devpath{call mkdir_recursive->call make_dir}
			->分支 uevent->subsystem为usb
			-->mkdir_recursive_for_devpath{call mkdir_recursive->call make_dir}
			-->make_dir
			->分支 uevent->subsystem为drm,input,adsp等等
			-->make_dir
			->get_character_device_symlinks()做链接处理
			->handle_device() 处理
			-->make_device()
			--->get_device_perm()获取设备的perm链表节点
			--->mknod创建设备节点+setegid变更节点权限+chown设置节点权限
         */
        handle_generic_device_event(uevent);
    }
}

如果有协处理器, 还要下载协处理器的firmware,这里是处理协处理器要下载firmware的指令,fork一个子进程处理,代码实现如下:

static void handle_firmware_event(struct uevent *uevent)
{
    pid_t pid;
    int ret;
    if(strcmp(uevent->subsystem, "firmware"))
        return;
    if(strcmp(uevent->action, "add"))
        return;
    /* we fork, to avoid making large memory allocations in init proper */
    pid = fork();
    if (!pid) {
	    /**
		 process_firmware_event(struct uevent *uevent)
          ->确保isbooting
          ->通过uevent->firmware和uevent->path获取文件路径path
          ->open path,firmware,loading,data
          ->load_firmware(从firmware中读取数据到data中,且向loading_fd中写入状态)
          ->close path,firmware,loading,data
		 */
        process_firmware_event(uevent);
        exit(EXIT_SUCCESS);
    }
}

1.3 总结{uevent功能}

@1 uevent本质上是解析对应的uevent.rc以及硬件相关rc,生成一张数据库表
@2 监听来自kernel的socket信息,将其转换成uevent,调用事件处理方法来处理
@3 处理的事件

处理的事件如下图所示:

android 开机启动流程分析(三)init启动中关键进程 uevent & watchdog

2  watchdog分支分析

2.1 watchdog逻辑说明:

@1 init进程中会解析/init.rc,init.rc文件中有启用watchdog的服务,service watchdogd /sbin/watchdogd 10 20进行喂狗,即当系统出现崩溃或死机达到30s时会进行重启;watchdogd <argv1,argv2> 参数1为间隔的喂狗时间,argv1+argv2为超时时间,当argv2设置为0则到达argv1时间后系统会进行重启

2.2 watchdog进程的主函数实现如下:

#define DEV_NAME "/dev/watchdog" //设备节点,实际上上层仅仅是通过设备节点对硬件进行控制
int watchdogd_main(int argc, char **argv)
{
    int fd;
    int ret;
    int interval = 10;
    int margin = 10;
    int timeout;
    open_devnull_stdio();
    klog_init();
    INFO("Starting watchdogd\n");
    if (argc >= 2)
        interval = atoi(argv[1]);//喂狗时间,即每间隔internal喂狗一次
    if (argc >= 3)
        margin = atoi(argv[2]);//internal+margin,过了超时时间还没有喂狗,则重新启动系统
    timeout = interval + margin;
    fd = open(DEV_NAME, O_RDWR);//打开看门狗设备
    if (fd < 0) {
        ERROR("watchdogd: Failed to open %s: %s\n", DEV_NAME, strerror(errno));
        return 1;
    }
    ret = ioctl(fd, WDIOC_SETTIMEOUT, &timeout);//通过ioctl设置超时时间
    if (ret) {//如果超时设置失败,则获取超时
        ERROR("watchdogd: Failed to set timeout to %d: %s\n", timeout, strerror(errno));
        ret = ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
        if (ret) {//设置和获取都失败就不管了,直接报错
            ERROR("watchdogd: Failed to get timeout: %s\n", strerror(errno));
        } else {//如果第一次设置失败但获取成功,根据timeout和margin重新设置interval,保证看门狗有效
            if (timeout > margin)
                interval = timeout - margin;
            else
                interval = 1;
            ERROR("watchdogd: Adjusted interval to timeout returned by driver: timeout %d, interval %d, margin %d\n",
                  timeout, interval, margin);
        }
    }
    while(1) {//无限循环,不断喂狗,除非整个系统挂掉,否则会每隔interval,喂狗一次
        write(fd, "", 1);
        sleep(interval);
    }
}

至此,除了init,另外两个和init相关的进程 uevent和watchdog也分析完毕;这里说明下,实际上通过system/core/init/ 目录下的android.mk文件可以知道,三者就是同一个程序,仅仅是名字不同而已;而通过名字的不同而在开始时执行不同的函数调用。