用户空间第一个程序Init

平台:Android4.0
场景:针对老邓第一本Android书的学习总结。
时间:2012.9

以下内容主要来自对老邓第一本Android书的总结。

http://www.cnblogs.com/hangxin1940/archive/2011/10/14/2206754.html

init为android用户空间的第一个程序,其实现代码主要是在system/core/init/init.c中。因为是第一个用户空间的程序,因此init完成了比较多的工作,重要的如下:
(1).解析init.rc和init.xxx.rc文件,并执行其中的command—其中包含了以service方式启动的zygote这个关键进程的启动

(2).创建了属性系统使用的匿名共享内存区域,并创建了一个属性服务器来供客户端与属性系统的交互—客户端通过socket来设置属性

(3).进入循环中,pollin等待监听的几个fd的事件并进行响应:

        for (i = 0; i < fd_count; i++) {
            if (ufds[i].revents == POLLIN) {
                if (ufds[i].fd == get_property_set_fd())---属性服务消息
                    handle_property_set_fd();
                else if (ufds[i].fd == get_keychord_fd())---keychord是组合按键。
        从源码的行为来看,应该是考虑到组合键盘这种外设,大部分情况不会用到手机上,而多用在智能设备上,也就是没有触屏以及按键很少的android设备,比如运行android的手表神马的,通过不同的按键组合,来代表一个标准键盘的输入
                    handle_keychord();
                else if (ufds[i].fd == get_signal_fd())---子进程死亡消息
                    handle_signal();
            }
        }

1.init_parse_config_file()
此函数用于解析init.rc等文件。在其调用的parse_config()中:

                if (kw_is(kw, SECTION)) {
                    state.parse_line(&state, 0, 0);
                    parse_new_section(&state, kw, nargs, args);
                }

说明函数以SECTION为单位进行解析,解析的执行函数为parse_new_section()。
在system/core/init/keywords.h中,定义了init.rc中使用的各个标签,其主要分为三类:
(1).SECTION—on, service, import。parse_config()中的解析,就是以这三个标签为节点的。将每个节点中包含的所有操作进行解析并保存到链表中,以便后面的统一执行。请注意区别:
在2.2的代码中:

    action_for_each_trigger("early-init", action_add_queue_tail);
    drain_action_queue();

将每个节点的所有操作加入到处理队列中后,立即调用drain_action_queue()来执行。
在4.0的代码中:

    action_for_each_trigger("early-init", action_add_queue_tail);

    queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    queue_builtin_action(property_init_action, "property_init");
    queue_builtin_action(keychord_init_action, "keychord_init");
    queue_builtin_action(console_init_action, "console_init");
    queue_builtin_action(set_init_properties_action, "set_init_properties");

    /* execute all the boot actions to get us started */
    action_for_each_trigger("init", action_add_queue_tail);

只看到了往处理队列中添加操作函数,却并没有看到相关的执行调用?
原来4.0的init的main()进行到for循环中后,将调用execute_one_command(),同时:

        if (!action_queue_empty() || cur_action)
            timeout = 0;

直接利用了for来遍历并执行了之前push的所有等待操作!
为什么如此修改?google担心一个节点就执行一次操作,若那个节点中存在一个耗时操作,就会阻塞当前的主线程。4.0中的这种将所有的节点都push到处理队列中,然后利用for循环来完成等待操作的执行—for循环中的fd监听会被监听到了!每次执行一个处理队列中的command,都将去poll一次。
关于对SECTION的解析,在init_parser.c的parse_new_section()函数中。

(2).OPTION—主要是用来描述service的。其对应的解析执行在init_parser.c中。主要的作用描述如下:
disable—不随class的启动而启动
oneshot—退出后不需要重启
critical—非常重要的服务才使用。在规定时间内不断重启,则系统会重启并进入恢复模式
console—service需要使用控制台
group—service进程所属的用户组
user—service进程所属的用户
onrestart—此OPTION一般会接command,表示此service是重启的时候需要执行的command。其在service对应的结构体中使用action结构体来描述onrestart,其所接的command保存在action结构体的双向链表中
ioprio—-设置IO优先级相关—-rt和be的含义?rt应该是real time实时进程。
解析执行函数是在init_parser.c的parse_line_service()函数中。

(3).COMMAND—每个command都会对应一个执行函数。全志在其方案上定制的format_userdata就是通过添加了一个对应的command执行函数来实现的。

此处需要注意的用法是,在init_parser.c中对keywords.h文件的引用:

#include "keywords.h"
#define KEYWORD(symbol, flags, nargs, func) \
    [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },----------此处需要注意!

struct {
    const char *name;
    int (*func)(int nargs, char **args);
    unsigned char nargs;
    unsigned char flags;
} keyword_info[KEYWORD_COUNT] = {
    [ K_UNKNOWN ] = { "unknown", 0, 0, 0 },
#include "keywords.h"
}; 
#undef KEYWORD

第一次包含keywords.h时,它申明了一些command对应的函数,另外定义了一个枚举,枚举值为K_class等。
第二次包含keywords.h后,得到了一个keyword_info的结构体数组,这个keyword_info结构体以前面定义的枚举值为索引,存储对应的关键字信息。

2.解析过程中,需要注意重要的数据结构有:
两个列表,一个队列
static list_declare(service_list);
static list_declare(action_list);
static list_declare(action_queue);
*.rc 脚本中所有 service关键字定义的服务将会添加到 service_list 列表中。
*.rc 脚本中所有 on 关键开头的项将会被会添加到 action_list 列表中。
每个action列表项都有一个列表,此列表用来保存该段落下的Commands脚本解析结果。

3.service如何被init启动?如何被重启?
如vold:

service vold /system/bin/vold
    class core
    socket vold stream 0660 root mount
    ioprio be 2

其class core,通过class这个OPTION来设置了vold这个service的name。在on boot这个SECTION中存在:
class_start core
class_start main
此处class_start对应的是do_class_start()函数,其将根据后面的name来遍历service_list,找到相同的name,然后进行启动—最后的实现代码在init.c的service_start()函数中:

        if (properties_inited()) {
            get_property_workspace(&fd, &sz);
            sprintf(tmp, "%d,%d", dup(fd), sz);
            add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);
        }

这部分代码将属性存储空间的信息添加到环境变量中,以便后面的使用?—其与属性在本地进程中获取fd有关?具体联系?
在bionic\libc\bionic\system_properties.c中:

    env = getenv("ANDROID_PROPERTY_WORKSPACE");
    if (!env) {
        return -1;
    }
    fd = atoi(env);
    env = strchr(env, ',');
    if (!env) {
        return -1;
    }
    sz = atoi(env + 1);

    pa = mmap(0, sz, PROT_READ, MAP_SHARED, fd, 0);

获取了之前在启动服务时候保存的关于属性系统共享内存的环境变量。
PS:java的世界是由zygote通过fork出来的,而zygote又是以service的方式启动的—在启动service的时候保存了共享内存的地址,然后在加载bionic libc库的时候获取这个地址并通过mmap映射到当前的进程空间,即zygote进程的控件,因此其fork的子进程(java应用程序)都能访问到这块共享内存的区域了,只是此时的mmap是只读的方式,当需要设置属性值时,就需要使用属性服务器了。

关于service的启动,在service_start()中,通过fork出子进程,然后调用execve执行对应的程序,使得子进程功能独立,并通过notify_service_state(svc->name, “running”)来设置属性系统中对应的服务的状态值。

当一个服务进程死亡后,将通过:

                else if (ufds[i].fd == get_signal_fd())
                    handle_signal();

监听到,并通过handle_signal()来调用signal_handler.c中的wait_for_one_process()函数进行处理。在其中,将杀掉死亡进程的所有子进程,并清理socket信息,然后:
notify_service_state(svc->name, “restarting”)
将服务的状态值设为restarting。这样,在init.c的for循环当中的restart_processes()被执行时,就将重新启动所有状态值为restarting的服务。

4.关于init.xxx.rc解析的相关。
在init.c中,通过:

    /* pull the kernel commandline and ramdisk properties file in */
    import_kernel_cmdline(0, import_kernel_nv);
    /* don't expose the raw commandline to nonpriv processes */
    chmod("/proc/cmdline", 0440);
    get_hardware_name(hardware, &revision);

来获取hardware的名字,然后组合成init.hardware.rc来进行文件的寻找和解析!
此处强调一下hardware的获取,首先通过import_kernel_cmdline(0, import_kernel_nv)来获取kernel设置的command。注意此函数的第一个参数将传入到第二个参数所指的函数中—0表示非虚拟机的情况,因此将获取真实的kernel设置的command:

else if (!strcmp(name,"androidboot.hardware")) {
            strlcpy(hardware, value, sizeof(hardware));

此处的kernel的command何解?androidboot.hardware又是?
在A13的机器上,cat proc/cmdline,显示如下:

root@android:/ # cat proc/cmdline
cat proc/cmdline
console=ttyS0,115200 rw init=/init loglevel=5

kernel的command即kernel在启动init时,传入的启动参数。而查看import_kernel_cmdline()函数的实现可以发现—其读取proc/cmdline所写入的命令,以空格为分隔符,然后调用import_kernel_nv()进行解析,而androidboot.hardware就类似于上面的”console”,其为等号左边的常量名,然后读取其右边的参数并进行保存。

若kernel没有设置hardware相关的command,那么在get_hardware_name()函数中,将通过读取proc/cpuinfo下面的信息来获取hardware的名字!
最后通过queue_builtin_action(set_init_properties_action, “set_init_properties”)调用,将在处理队列执行set_init_properties_action()函数的时候,将hardware的值设置到属性系统中。
可能存在一些修改hardware名字的需求,那么需要考虑清楚从哪个方面入手来修改,是kernel的command?是proc/cpuinfo下面的值?还是仅仅修改ro.hardware的设置?同时在修改的时候,需要注意的是这样的修改是否会引起系统其它的问题?

5.关于uevent事件监听的变化。
在2.2中,直接在init.c中对uevent事件进行了监听处理:

        if (ufds[0].revents == POLLIN)
            handle_device_fd(device_fd);

并且,直接在devices.c文件中定义了各种需要创建的设备节点。
而在2.3以及之后的版本中,我们会发现在root/sbin/下面多了一个ueventd程序,以及在root/下面多了ueventd.rc文件。这是因为,在2.3以及之后的init.c中并不直接监听处理uevent事件了。取而代之的是,在init.rc中多了如下代码:

service ueventd /sbin/ueventd
    class core
    critical

关于uevent的监听处理通过单独的service ueventd来实现了,其实现代码为system/core/init/ueventd.c:

    while(1) {
        ufd.revents = 0;
        nr = poll(&ufd, 1, -1);
        if (nr <= 0)
            continue;
        if (ufd.revents == POLLIN)
               handle_device_fd();
    }

一个单独的进程专门进行uevent的处理。而之前的devices.c中定义的各种设备节点则放入到了root/ueventd.rc文件中,通过ueventd.c的main()函数中调用ueventd_parse_config_file()函数进行解析并执行。
PS:ueventd.c文件是在system/core/init目录下,而此目录的文件对应编译输出文件为init程序。那何来的root/sbin/ueventd呢?在其Android.mk文件中:

# Make a symlink from /sbin/ueventd to /init
SYMLINKS := $(TARGET_ROOT_OUT)/sbin/ueventd

其实质,就是做了一次SYMLINKS!而root/sbin/ueventd和root/init就是同一文件。那如何这个service又能监听uevent事件了呢?答案在init.c的main()函数中:

    if (!strcmp(basename(argv[0]), "ueventd"))
        return ueventd_main(argc, argv);

再结合看看uevent这个service的定义?

你可能感兴趣的:(用户空间第一个程序Init)