用户空间第一个程序Init

本文详细解析了Android系统中init的工作原理,包括init.rc配置文件的解析流程、服务启动机制和服务重启逻辑,以及uevent事件监听的变化等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

平台: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的定义?

<think>嗯,用户问的是init系统如何初始化启动用户空间的代码。我需要先回顾一下之前的对话,确保回答的一致性。之前用户问了用户程序是否被添加到kernel_init中,我解释了kernel_init的作用和init系统的角色。现在用户想更深入了解init系统如何初始化用户空间。 首先,我需要确认用户init系统的了解程度。可能用户已经知道init第一个用户进程,但需要详细步骤。我应该从内核启动后的阶段开始,解释init系统如何接管,并启动其他用户进程。 接下来,要分步骤说明init系统的初始化流程。可能需要包括不同的init系统,比如传统的SysV init和现代的systemd,以及它们的不同方法。还要提到关键步骤,比如读取配置文件、执行启动脚本、管理服务依赖等。 要注意技术细节的准确性。比如,内核如何通过kernel_init启动init进程,init进程的PID是否为1,以及init系统如何根据运行级别或target来启动服务。此外,可能需要解释用户空间初始化的各个阶段,比如挂载文件系统、配置网络、启动守护进程等。 用户可能对具体的配置文件或命令感兴趣,比如/etc/inittab、systemd的target,或者服务单元文件。需要给出例子,但避免过于复杂。同时,要强调用户空间和内核空间的分离,以及init系统在此过程中的桥梁作用。 还要注意可能存在的误区,比如用户可能认为init系统直接控制所有硬件,但实际上它通过系统调用与内核交互。需要澄清这些点,确保用户理解权限和职责的划分。 最后,总结整个过程,强调init系统的重要性,以及不同Linux发行版可能使用不同的init系统,但核心目标一致。如果有必要,可以推荐进一步的学习资源或命令,如查看systemd文档或使用systemctl命令。</think>在Linux系统中,**init系统**是内核启动后第一个用户空间进程(PID=1),负责初始化用户空间环境并管理系统服务。以下是其初始化用户空间的核心流程: --- ### 1. **内核移交控制权** - **内核完成初始化**: 内核启动后,通过`kernel_init`函数加载**init程序**(路径由内核参数`init=`指定,默认如`/sbin/init`)。 - **权限切换**: init进程以**用户空间权限**运行,标志着系统从内核态切换到用户态。 --- ### 2. **init系统初始化流程** 不同init系统(如SysV init、systemd)实现方式不同,但核心步骤类似: #### **阶段1:环境初始化** - **挂载关键文件系统** ```bash mount -t proc proc /proc mount -t sysfs sysfs /sys ``` 挂载`/proc`(进程信息)、`/sys`(硬件设备)等虚拟文件系统。 - **设置系统环境** 初始化系统时钟、主机名、日志系统(如`rsyslogd`)等。 #### **阶段2:运行级别(Runlevel)或目标(Target)** - **SysV init(传统方式)** 根据`/etc/inittab`定义**运行级别**(如0-6),例如: ```bash id:5:initdefault: # 默认运行级别为图形界面(5) ``` 随后执行对应级别的脚本(如`/etc/rc.d/rc5.d/`中的符号链接)。 - **systemd(现代主流方式)** 通过**目标(target)** 组织启动流程(如`graphical.target`),依赖关系由单元文件(unit)定义: ```bash systemctl get-default # 查看默认目标 systemctl isolate multi-user.target # 切换到多用户模式 ``` #### **阶段3:启动系统服务** - **SysV init** 按顺序执行`/etc/rc.d/rcX.d/`目录下的脚本(以`S`开头的为启动,`K`开头的为停止): ```bash S01network -> ../init.d/network # 示例:网络服务启动脚本 ``` - **systemd** 并行启动服务,通过依赖关系(`Requires`、`Wants`)和类型(`service`、`socket`)优化速度: ```ini # 示例:nginx.service单元文件 [Unit] Description=NGINX Web Server After=network.target [Service] ExecStart=/usr/sbin/nginx ``` #### **阶段4:用户登录环境** - 启动**登录管理器**(如gdm、lightdm)或直接运行`getty`生成终端登录提示: ```bash /sbin/getty -L tty1 0 # 在tty1上启动登录界面 ``` - 用户登录后,加载Shell(如bash)及个人配置文件(`~/.bashrc`)。 --- ### 3. **关键技术细节** - **依赖管理** systemd通过**控制组(cgroups)** 跟踪进程,确保服务依赖关系正确(例如:数据库服务必须在Web服务之前启动)。 - **事件驱动** systemd监听硬件事件(如插入U盘触发`udev`规则),动态加载驱动或挂载设备。 - **容错机制** 若服务启动失败,systemd可自动重启(通过`Restart=on-failure`配置)。 --- ### 4. **用户空间初始化的核心目标** 1. **隔离性**:用户进程无权直接访问硬件,需通过系统调用与内核交互。 2. **灵活性**:通过配置文件(而非修改内核)管理服务。 3. **效率**:并行启动服务(systemd)显著缩短启动时间。 --- ### 总结 init系统通过**层级化的初始化流程**,将内核移交的“空白”用户空间逐步配置为可用的操作系统环境。现代Linux主要依赖**systemd**,其设计解决了传统SysV init的串行瓶颈和依赖管理问题。理解这一过程,有助于调试启动故障或定制系统服务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值