2025年4月1日 星期二 乙巳(蛇)年 正月初二 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

安卓启动 Init 进程源码分析

时间:12-14来源:作者:点击数:3
CDSY,CDSY.XYZ

基于 Linux 内核的 android 系统,在内核启动完成后将创建一个 Init 用户进程,实现了内核空间到用户空间的转变。在 Android 启动过程介绍一文中介绍了 Android 系统的各个启动阶段,init 进程启动后会读取 init.rc 配置文件,通过 fork 系统调用启动 init.rc 文件中配置的各个 Service 进程。init 进程首先启动启动android的服务大管家ServiceManager 服务,然后启动 Zygote 进程。

Zygote 进程的启动开创了 Java 世界,无论是 SystemServer 进程还是 android 的应用进程都是 Zygote 的子进 程,Zygote 进程启动过程的源代码分析一文中详细介绍了 Zygote 进程的启动过程,System Server 进程启动过程源码分析则详细介绍了在 Zygote 进程启动完成后创建的第一个进程 SystemServer 进程的启动过程,SystemServer 进程的启动包括两个阶段,在第一阶段主要是启动 C++ 相关的本地服务,如 SurfaceFlinger 等,在第二阶段通过在 ServerThread 线程中启动 android 的各大关键Java服务。

Zygote 孵化应用进程过程的源码分析一 文中详细介绍了 Zygote 进程创建 android 应用进程的过程,当用户点击 Luncher 上的应用图标时,Luncher进程通过socket向 Zygote进程发送进程创建请求,Zygote 进程接受客户端的请求后,通过 fork 系统调用为应用程序创建相应的进程。

本文则介绍 android 用户,进程的始祖 Init 进程,Init 进程是 Linux 系统中用户空间的第一个进程,负责创建系统中的关键进程,同时提供属性服务来管理系统属性。

Android 进程模型

Linux 通过调用 start_kernel 函数来启动内核,当内核启动模块启动完成后,将启动用户空间的第一个进程——Init 进程,下图为 Android 系统的进程模型图:

从上图可以看出,Linux 内核在启动过程中,创建一个名为 Kthreadd 的内核进程,PID=2,用于创建内核空间的其他进程;同时创建第一个 用户空间Init进程,该进程PID = 1,用于启动一些本地进程,比如 Zygote 进程,而 Zygote 进程也是一个专门用于孵化Java进程的本地进程,上图清晰地描述了整个Android 系统的进程模型,为了证明以上进程模型的正确性,可以通过 ps 命令来查看进程的 PID 级 PPID,下图显示了Init进程的PID为1,其他的本地进程的 PPID都是1,说明它们的父进程都是Init进程,都是由 Init 进程启动的。

下图显示kthreadd进程的PID=2,有一部分内核进程如binder、dhd_watchdog等进程的PPID=2,说明这些进程都是由kthreadd进程创建:

上图中显示zygote进程PID=107,下图显示了zygote进程创建的子进程,从图中可以看到,zygote进程创建的都是Java进程,证明了zygote进程开创了Android系统的Java世界。

上面介绍了Android系统的进程模型设计,接下来将详细分析Init进程。

Init 进程源码分析

上节介绍了Init进程在Linux内核启动时被创建的,那它是如何启动的呢?

Init 进程启动分析

在Linux内核启动过程中,将调用Start_kernel来初始化配置:

  • asmlinkage void __init start_kernel(void)
  • {
  • .............. //执行初始化工作
  • rest_init();
  • }

start_kernel函数调用一些初始化函数完成初始化工作后,调用rest_init()函数来创建新的进程:

在rest_init函数里完成两个新进程的创建:Init进程和kthreadd进程,因为Init进程创建在先,所以其PID=1而kthreadd的PID=2,本文只对Init进程进行详细分析,如果读者对kthreadd进行感兴趣,可自行分析。

kernel_thread函数仅仅调用了fork系统调用来创建新的进程,创建的子进程和父进程都执行在fork函数调用之后的代码,子进程是父进程的一个拷贝。

在kernel_init函数中调用__initcall_start到__initcall_end之间保存的函数进行驱动模块初始化,然后直接调用init_post()函数进入用户空间,执行Init 进程代码。

  • static noinline int init_post(void)
  • {
  • async_synchronize_full();
  • free_initmem();
  • mark_rodata_ro();
  • system_state = SYSTEM_RUNNING;
  • numa_default_policy();
  • current->signal->flags |= SIGNAL_UNKILLABLE;
  • //如果ramdisk_execute_command不为空,ramdisk_execute_command下的Init程序
  • if (ramdisk_execute_command) {
  • run_init_process(ramdisk_execute_command);
  • printk(KERN_WARNING "Failed to execute %s\n",ramdisk_execute_command);
  • }
  • //如果execute_command不为空,execute_command下的Init程序
  • if (execute_command) {
  • run_init_process(execute_command);
  • printk(KERN_WARNING "Failed to execute %s. Attempting ""defaults...\n", execute_command);
  • }
  • //如果以上路径下都没有init程序,就从/sbin、/etc、/bin三个路径下寻找init程序,同时启动一个sh进程
  • run_init_process("/sbin/init");
  • run_init_process("/etc/init");
  • run_init_process("/bin/init");
  • run_init_process("/bin/sh");
  • //如果以上路径都没有找到init程序,调用内核panic
  • panic("No init found. Try passing init= option to kernel. "
  • "See Linux Documentation/init.txt for guidance.");
  • }

当 根文件系统顶层目录中不存在init进程,或未指定启动选项"init="时,内核会到/sbin、/etc、/bin目录下查找init文件。如果在这 些目录中仍未找到init文件,内核就会中止执行init进程,并引发Kernel Panic。run_init_process函数通过系统调用do_execve从内核空间跳转到用户空间,并且执行用户空间的Init程序的入口函 数。

  • static void run_init_process(const char *init_filename)
  • {
  • argv_init[0] = init_filename;
  • kernel_execve(init_filename, argv_init, envp_init);
  • }

这里就介绍完了内核启动流程,run_init_process函数的将执行Init程序的入口函数,Init的入口函数位于/system/core/init/init.c

Init 进程源码分析

Android的init进程主要功能:

  1. 分析init.rc启动脚本文件,根据文件内容执行相应的功能;
  2. 当一些关键进程死亡时,重启该进程;
  3. 提供Android系统的属性服务;
屏蔽标准的输入输出
  • void open_devnull_stdio(void)
  • {
  • int fd;
  • //创建一个字符专用文件/dev/__null__
  • static const char *name = "/dev/__null__";
  • if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) {
  • //获取/dev/__null__的文件描述符,并输出该文件
  • fd = open(name, O_RDWR);
  • unlink(name);
  • if (fd >= 0) {
  • //将与进程相关的标准输入(0),标准输出(1),标准错误输出(2),均定向到NULL设备
  • dup2(fd, 0);
  • dup2(fd, 1);
  • dup2(fd, 2);
  • if (fd > 2) {
  • close(fd);
  • }
  • return;
  • }
  • }
  • exit(1);
  • }

将标准输入输出,错误输出重定向到/dev/_null_设备中

初始化内核 log 系统
  • void klog_init(void)
  • {
  • static const char *name = "/dev/__kmsg__";
  • //创建/dev/__kmsg__设备节点
  • if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {
  • klog_fd = open(name, O_WRONLY);
  • //当进程在进行exec系统调用时,要确保log_fd是关闭的
  • fcntl(klog_fd, F_SETFD, FD_CLOEXEC);
  • unlink(name);
  • }
  • }
属性存储空间初始化
  • void property_init(void)
  • {
  • init_property_area();
  • }

关于Android的属性系统,请查看Android 系统属性SystemProperty分析一文,在这篇文章中详细分析了Android的属性系统。

读取机器硬件名称

从 /proc/cpuinfo 中获取 Hardware 字段信息写入;Reversion 字段信息写入

  • void get_hardware_name(char *hardware, unsigned int *revision)
  • {
  • char data[1024];
  • int fd, n;
  • char *x, *hw, *rev;
  • if (hardware[0])
  • return;
  • //打开/proc/cpuinfo文件
  • fd = open("/proc/cpuinfo", O_RDONLY);
  • if (fd < 0) return;
  • //读取/proc/cpuinfo文件内容
  • n = read(fd, data, 1023);
  • close(fd);
  • if (n < 0) return;
  • data[n] = 0;
  • hw = strstr(data, "\nHardware");
  • rev = strstr(data, "\nRevision");
  • if (hw) {
  • x = strstr(hw, ": ");
  • if (x) {
  • x += 2;
  • n = 0;
  • while (*x && *x != '\n') {
  • if (!isspace(*x))
  • hardware[n++] = tolower(*x);
  • x++;
  • if (n == 31) break;
  • }
  • hardware[n] = 0;
  • }
  • }
  • if (rev) {
  • x = strstr(rev, ": ");
  • if (x) {
  • *revision = strtoul(x + 2, 0, 16);
  • }
  • }
  • }

get_hardware_name函数从/proc/cpuinfo文件中读取硬件名称等信息,/proc/cpuinfo文件内容如下:

  • Processor : ARMv7 Processor rev 1 (v7l)
  • BogoMIPS : 1024.00
  • Features : swp half thumb fastmult vfp edsp thumbee neon vfpv3
  • CPU implementer : 0x41
  • CPU architecture: 7
  • CPU variant : 0x0
  • CPU part : 0xc05
  • CPU revision : 1
  • Hardware : sc7710g
  • Revision : 0000
  • Serial : 0000000000000000
设置命令行参数属性
  • static void process_kernel_cmdline(void)
  • {
  • chmod("/proc/cmdline", 0440);
  • import_kernel_cmdline(0, import_kernel_nv);
  • if (qemu[0])
  • import_kernel_cmdline(1, import_kernel_nv);
  • export_kernel_boot_props();
  • }

process_kernel_cmdline 函数首先修改/proc/cmdline文件权限,然后调用import_kernel_cmdline函数来读取/proc/cmdline文件的内 容,并查找格式为: = 的字串,调用import_kernel_nv函数来设置属性。函数export_kernel_boot_props()用于设置内核启动时需要的属性。

  • void import_kernel_cmdline(int in_qemu,void (*import_kernel_nv)(char *name, int in_qemu))
  • {
  • char cmdline[1024];
  • char *ptr;
  • int fd;
  • //打开并读取/proc/cmdline文件
  • fd = open("/proc/cmdline", O_RDONLY);
  • if (fd >= 0) {
  • int n = read(fd, cmdline, 1023);
  • if (n < 0) n = 0;
  • if (n > 0 && cmdline[n-1] == '\n') n--;
  • cmdline[n] = 0;
  • close(fd);
  • } else {
  • cmdline[0] = 0;
  • }
  • ptr = cmdline;
  • while (ptr && *ptr) {
  • char *x = strchr(ptr, ' ');
  • if (x != 0) *x++ = 0;
  • //回调import_kernel_nv函数,in_qemu =0
  • import_kernel_nv(ptr, in_qemu);
  • ptr = x;
  • }
  • }

/proc/cmdline文件内容如下:

  • static void import_kernel_nv(char *name, int for_emulator)
  • {
  • char *value = strchr(name, '=');
  • int name_len = strlen(name);
  • if (value == 0) return;
  • *value++ = 0;
  • if (name_len == 0) return;
  • #ifdef HAVE_SELINUX
  • if (!strcmp(name,"enforcing")) {
  • selinux_enforcing = atoi(value);
  • } else if (!strcmp(name,"selinux")) {
  • selinux_enabled = atoi(value);
  • }
  • #endif
  • //判断是否为模拟器
  • if (for_emulator) {
  • char buff[PROP_NAME_MAX];
  • int len = snprintf( buff, sizeof(buff), "ro.kernel.%s", name );
  • if (len < (int)sizeof(buff))
  • property_set( buff, value );
  • return;
  • }
  • //如果/proc/cmdline文件中有qemu关键字
  • if (!strcmp(name,"qemu")) {
  • strlcpy(qemu, value, sizeof(qemu));
  • //如果/proc/cmdline文件中有以androidboot.开头的关键字
  • } else if (!strncmp(name, "androidboot.", 12) && name_len > 12) {
  • const char *boot_prop_name = name + 12;
  • char prop[PROP_NAME_MAX];
  • int cnt;
  • //格式化为ro.boot.xx 属性
  • cnt = snprintf(prop, sizeof(prop), "ro.boot.%s", boot_prop_name);
  • if (cnt < PROP_NAME_MAX)
  • property_set(prop, value);
  • }
  • }

最后调用函数export_kernel_boot_props设置内核启动属性

  • static void export_kernel_boot_props(void)
  • {
  • char tmp[PROP_VALUE_MAX];
  • const char *pval;
  • unsigned i;
  • //属性表
  • struct {
  • const char *src_prop;
  • const char *dest_prop;
  • const char *def_val;
  • } prop_map[] = {
  • { "ro.boot.serialno", "ro.serialno", "", },
  • { "ro.boot.mode", "ro.bootmode", "unknown", },
  • { "ro.boot.baseband", "ro.baseband", "unknown", },
  • { "ro.boot.bootloader", "ro.bootloader", "unknown", },
  • };
  • //循环读取ro.boot.xxx属性值,并设置ro.xxx属性
  • for (i = 0; i < ARRAY_SIZE(prop_map); i++) {
  • pval = property_get(prop_map[i].src_prop);
  • property_set(prop_map[i].dest_prop, pval ?: prop_map[i].def_val);
  • }
  • //读取ro.boot.console属性值
  • pval = property_get("ro.boot.console");
  • if (pval)
  • strlcpy(console, pval, sizeof(console));
  • //读取ro.bootmode属性值
  • strlcpy(bootmode, property_get("ro.bootmode"), sizeof(bootmode));
  • //读取ro.boot.hardware属性值
  • pval = property_get("ro.boot.hardware");
  • if (pval)
  • strlcpy(hardware, pval, sizeof(hardware));
  • //设置ro.hardware属性
  • property_set("ro.hardware", hardware);
  • //设置ro.revision属性
  • snprintf(tmp, PROP_VALUE_MAX, "%d", revision);
  • property_set("ro.revision", tmp);
  • //设置ro.factorytest属性
  • if (!strcmp(bootmode,"factory"))
  • property_set("ro.factorytest", "1");
  • else if (!strcmp(bootmode,"factory2"))
  • property_set("ro.factorytest", "2");
  • else
  • property_set("ro.factorytest", "0");
  • }
init.rc 文件解析
  • init_parse_config_file(const char *fn)
  • {
  • char *data;
  • //读取/init.rc文件内容
  • data = read_file(fn, 0);
  • if (!data) return -1;
  • //解析读取到的文件内容
  • parse_config(fn, data);
  • DUMP();
  • return 0;
  • }

函数首先调用read_file函数将init.rc文件的内容读取保存到data中,在调用parse_config对其进行解析

  • void *read_file(const char *fn, unsigned *_sz)
  • {
  • char *data;
  • int sz;
  • int fd;
  • struct stat sb;
  • data = 0;
  • //打开/init.rc文件
  • fd = open(fn, O_RDONLY);
  • if(fd < 0) return 0;
  • // for security reasons, disallow world-writable
  • // or group-writable files
  • if (fstat(fd, &sb) < 0) {
  • ERROR("fstat failed for '%s'\n", fn);
  • goto oops;
  • }
  • if ((sb.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
  • ERROR("skipping insecure file '%s'\n", fn);
  • goto oops;
  • }
  • //将文件指针移到文件尾部,得到文件内容长度
  • sz = lseek(fd, 0, SEEK_END);
  • if(sz < 0) goto oops;
  • if(lseek(fd, 0, SEEK_SET) != 0) goto oops;
  • //分配buffer
  • data = (char*) malloc(sz + 2);
  • if(data == 0) goto oops;
  • //读取文件
  • if(read(fd, data, sz) != sz) goto oops;
  • close(fd);
  • data[sz] = '\n';
  • data[sz+1] = 0;
  • if(_sz) *_sz = sz;
  • return data;
  • oops:
  • close(fd);
  • if(data != 0) free(data);
  • return 0;
  • }
init.rc 文件语法介绍

在Android根文件系统下存在多个.rc文件,该文件为Android启动配置脚本文件,文件内容如下:

  • service keystore /system/bin/keystore /data/misc/keystore
  • class main
  • user keystore
  • group keystore drmrpc
  • socket keystore stream 666

init.rc 是一个可配置的初始化文件,通常定制厂商可以配置额外的初始化配置,如果关键字中有空格,处理方法类似于C语言,使用/表示转义,使用“”防止关键字被断 开,另外注意/在末尾表示换行,由 # (前面允许有空格)开始的行都是注释行。init.rc包含4种状态类别:Actions/Commands/Services/Options。当声明 一个service或者action的时候,它将隐式声明一个section,它之后跟随的command或者option都将属于这个 section,action和service不能重名,否则忽略为error。

Action

actions就是在某种条件下触发一系列的命令,通常有一个trigger。

trigger 主要包括:

boot 当 /init.conf 加载完毕时

= 当被设置为时

device-added- 设备被添加时

device-removed- 设备被移除时

service-exited- 服务退出时

Service

service就是要启动的本地服务进程

service [ ]*

Option

option是service的修饰词,由它来指定何时并且如何启动Services程序,主要包括:

critical 表示如果服务在4分钟内存在多于4次,则系统重启到recovery mode

disabled 表示服务不会自动启动,需要手动调用名字启动

setEnv 设置启动环境变量

socket [ []] 开启一个unix域的socket,名字为/dev/socket/ , 只能是dgram或者stream,和默认为0

user 表示将用户切换为,用户名已经定义好了,只能是system/root

group 表示将组切换为

oneshot 表示这个service只启动一次

class 指定一个要启动的类,这个类中如果有多个service,将会被同时启动。默认的class将会是“default”

onrestart 在重启时执行一条命令

Command

comand主要包括:

exec [ ]*执行一个指定的程序

export 设置一个全局变量

ifup 使网络接口连接

import 引入其他的配置文件

hostname 设置主机名

chdir 切换工作目录

chmod 设置访问权限

chown 设置用户和组

chroot 设置根目录

class_start 启动类中的service

class_stop 停止类中的service

domainname 设置域名

insmod 安装模块

mkdir [mode] [owner] [group] 创建一个目录,并可以指定权限,用户和组

mount

[ ]* 加载指定设备到目录下 包括"ro", "rw", "remount", "noatime"

setprop 设置系统属性

setrlimit 设置资源访问权限

start 开启服务

stop 停止服务

symlink 创建一个动态链接

sysclktz 设置系统时钟

trigger 触发事件

write [ ]* 向路径的文件写入多个

Properties(属性)

Init更新一些系统属性以提供对正在发生的事件的监控能力:

init.action 此属性值为正在被执行的action的名字,如果没有则为""。

init.command 此属性值为正在被执行的command的名字,如果没有则为""。

init.svc. 名为的service的状态("stopped"(停止), "running"(运行), "restarting"(重启))

在默认情况下,程序在被init执行时会将标准输出和标准错误都重定向到/dev/null(丢弃)。若你想要获得调试信息,你可以通过 Andoird系统中的logwrapper程序执行你的程序。它会将标准输出/标准错误都重定向到Android日志系统(通过logcat访问)。

例如:

  • service akmd /system/bin/logwrapper /sbin/akmd
init.rc 解析过程

1. 扫描init.rc中的token

找到其中的 文件结束EOF/文本TEXT/新行NEWLINE,其中的空格‘ ’、‘\t’、‘\r’会被忽略,#开头的行也被忽略掉;而对于TEXT,空格‘ ’、‘\t’、‘\r’、‘\n’都是TEXT的结束标志。

2. 对每一个TEXT token,都加入到args[]数组中

3. 当遇到新一行(‘\n’)的时候,用args[0]通过lookup_keyword()检索匹配关键字;

1) 对Section(on和service),调用parse_new_section() 解析:

- 对on section,调用parse_action(),并设置解析函数parse_line为parse_line_action()

- 对service section,调用parse_service(),并设置解析函数parse_line为parse_line_service()

2) 对其他关键字的行(非on或service开头的地方,也就是没有切换section)调用parse_line()

- 对于on section内的命令行,调用parse_line_action()解析;

- 对于service section内的命令行,调用parse_line_service()解析。

Token的定义

  • #define T_EOF 0
  • #define T_TEXT 1
  • #define T_NEWLINE 2

解析过程中的双向循环链表的使用,android用到了一个非常巧妙的链表实现方法,一般情况下如果链表的节点是一个单独的数据结构的话,那么针对不同的数 据结构,都需要定义不同链表操作。而在初始化过程中使用到的链表则解决了这个问题,它将链表的节点定义为了一个非常精简的结构,只包含前向和后向指针,那 么在定义不同的数据结构时,只需要将链表节点嵌入到数据结构中即可。链表节点定义如下:

  • struct listnode
  • {
  • struct listnode *next;
  • struct listnode *prev;
  • };

对于Action数据结构为例:

  • struct action {
  • struct listnode alist;
  • struct listnode qlist;
  • struct listnode tlist;
  • unsigned hash;
  • const char *name;
  • struct listnode commands;
  • struct command *current;
  • };

这样的话,所有的链表的基本操作,例如插入,删除等只会针对listnode进行操作,而不是针对特定的数据结构,链表的实现得到了统一,即精简了代码,又 提高了效率。 但是这样的链表实现,存在一个问题,链表节点listnode中只有前向和后向指针,并且前向和后向指针均指向listnode,那么我们通过什么方式来 访问数据结构action的内容呢?我们使用offsetof宏来计算链表节点在数据结构中的偏移量,从而计算数据结构实例的地址。

  • #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
  • 1.#define node_to_item(node, container, member) \
  • (container *) (((char*) (node)) - offsetof(container, member))

这种链表的优点:(1)所有链表基本操作都是基于listnode指针的,因此添加类型时,不需要重复写链表基本操作函数(2)一个container数据结构可以含有多个listnode成员,这样就可以同时挂到多个不同的链表中。

Service 数据结构定义:

  • struct service {
  • struct listnode slist;
  • const char *name;
  • const char *classname;
  • unsigned flags;
  • pid_t pid;
  • time_t time_started;
  • time_t time_crashed;
  • int nr_crashed;
  • uid_t uid;
  • gid_t gid;
  • gid_t supp_gids[NR_SVC_SUPP_GIDS];
  • size_t nr_supp_gids;
  • #ifdef HAVE_SELINUX
  • char *seclabel;
  • #endif
  • struct socketinfo *sockets;
  • struct svcenvinfo *envvars;
  • struct action onrestart;
  • int *keycodes;
  • int nkeycodes;
  • int keychord_id;
  • int ioprio_class;
  • int ioprio_pri;
  • int nargs;
  • char *args[1];
  • };

对于某些Service可能采用Socket来实现进程间通信,因此该Service需要创建多个socket,比如:

  • service wril-daemon /system/bin/rild_sp -l /system/lib/libreference-ril_sp.so -m w -n 0
  • class core
  • socket rild stream 660 root radio
  • socket rild-debug stream 660 radio system
  • disabled
  • user root
  • group radio cache inet misc audio sdcard_rw log

该service需要创建rild 和rild-debug socket,这些socket的信息在解析init.rc文件时保存在Service的成员变量sockets链表中。socketinfo 数据结构定义如下:

  • struct socketinfo {
  • struct socketinfo *next;
  • const char *name;
  • const char *type;
  • uid_t uid;
  • gid_t gid;
  • int perm;
  • };

某些 Service 在运行时需要设置环境变量,这些环境变量被保存在 Service 的成员变量 envvars 链表中,svcenvinfo 数据结构定义如下:

  • struct svcenvinfo {
  • struct svcenvinfo *next;
  • const char *name;
  • const char *value;
  • };

在每个Action或Service下可能需要执行多个Command,关于command数据结构定义如下:

  • struct command
  • {
  • struct listnode clist;
  • int (*func)(int nargs, char **args);
  • int nargs;
  • char *args[1];
  • };

在Init进程中分别使用了3个链表来存储init.rc文件中的Action和Service:

  • static list_declare(service_list);
  • static list_declare(action_list);
  • static list_declare(action_queue);

service_list链表用于保存init.rc文件中的Service配置信息,service_list链表的存储如下图所示:

service_list 链表保存init.rc文件中的所有service,每个service下的所有socket信息保存在该service的成员变量sockets链表 中,当该service重启时,需要重启某些服务,对于重启某些服务的命令以Action的形式保存在Service的成员变量onrestart链表 中,而真正执行的命令却存放在该Action下的commands链表里。

action_list用于保存init.rc文件中的所有以on开头的section,action_list链表的存储如下图所示:

从上图可以看出action_queue和action_list都是用来保存所有的Action,它们之间的区别是action_list用于保 存从init.rc中解析出来的所有Action,而action_queue却是用于保存待执行的Action,action_queue是一个待执行队列。

文章太长了,详情请下载查看。

CDSY,CDSY.XYZ
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐