开篇
本篇以android-11.0.0_r25作为基础解析
PC启动会通过BIOS引导,从0x7c00处找到以0xaa55为结尾的引导程序启动。而Android通常使用在移动设备上,没有PC的BIOS,取而代之的是BootLoader。
BootLoader
在CPU上电复位完成后,会从一个固定的地址加载一段程序,即BootLoader,不同的CPU可能这个地址不同。BootLoader是一段引导程序,其中最为常见的为U-boot,它一般会先检测用户是否按下某些特别按键,这些特别按键是uboot在编译时预先被约定好的,用于进入调试模式。如果用户没有按这些特别的按键,则uboot会从NAND Flash中装载Linux内核,装载的地址是在编译uboot时预先约定好的。
进程
idle
Linux内核启动后,便会创建第一个进程idle。idle进程是Linux中的第一个进程,pid为0,是唯一一个没有通过fork产生的进程,它的优先级非常低,用于CPU没有任务的时候进行空转。
init
init进程由idle进程创建,是Linux系统的第一个用户进程,pid为1,是系统所有用户进程的直接或间接父进程,本篇重点讲的就是它。
kthreadd
kthreadd进程同样由idle进程创建,pid为2,它始终运行在内核空间,负责所有内核线程的调度与管理。
init进程
Android的init进程代码在system/core/init/main.cpp
中,以main方法作为入口,分为几个阶段:
1 | int main(int argc, char** argv) { |
这里的ueventd和subcontext,都是在各自守护进程中执行的,不在init进程中执行,这里就不多介绍了
FirstStageMain
默认不加任何参数启动init的话,便会开始init第一阶段,进入到FirstStageMain函数中,代码在system/core/init/first_stage_init.cpp
中
umask
文档:https://man7.org/linux/man-pages/man2/umask.2.html
原型:mode_t umask(mode_t mask);
这个方法是用来设置创建目录或文件时所应该赋予权限的掩码
Linux中,文件默认最大权限是666,目录最大权限是777,当创建目录时,假设掩码为022,那赋予它的权限为(777 & ~022)= 755
在执行init第一阶段时,先执行umask(0),使创建的目录或文件的默认权限为最高
创建目录、设备节点,挂载
1 | int FirstStageMain(int argc, char** argv) { |
初始化日志
SetStdioToDevNull
由于Linux内核打开了/dev/console作为标准输入输出流(stdin/stdout/stderr)的文件描述符,而init进程在用户空间,无权访问/dev/console,后续如果执行printf的话可能会导致错误,所以先调用SetStdioToDevNull
函数来将标准输入输出流(stdin/stdout/stderr)用/dev/null文件描述符替换
/dev/null被称为空设备,是一个特殊的设备文件,它会丢弃一切写入其中的数据,读取它会立即得到一个EOF
InitKernelLogging
接着调用InitKernelLogging
函数,初始化了一个简单的kernel日志系统
创建设备,挂载分区
1 | int FirstStageMain(int argc, char** argv) { |
结束
至此,第一阶段的init结束,通过execv函数带参执行init文件,进入SetupSelinux
1 | const char* path = "/system/bin/init"; |
exec系列函数
用exec系列函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。
这里在末尾直接打log是因为,exec系列函数如果执行正常是不会返回的,所以只要执行到下面就代表exec执行出错了
SetupSelinux
启动Selinux安全机制,初始化selinux,加载SELinux规则,配置SELinux相关log输出,并启动第二阶段:SecondStageMain
1 | int SetupSelinux(char** argv) { |
SecondStageMain
使用second_stage
参数启动init的话,便会开始init第二阶段,进入到SecondStageMain
函数中,代码在system/core/init/init.cpp
中
1 | int SecondStageMain(int argc, char** argv) { |
Linux OOM Killer机制
Linux下有一种 OOM KILLER 的机制,它会在系统内存耗尽的情况下,启用自己算法有选择性的杀掉一些进程,这个算法和三个值有关:
- /proc/PID/oom_score ,OOM 最终得分,值越大越有可能被杀掉
- /proc/PID/oom_score_adj ,取值范围为-1000到1000,计算oom_score时会加上该参数
- /proc/PID/oom_adj ,取值是-17到+15,该参数主要是为兼容旧版内核
在init过程中,代码设置了init进程和以后fork出来的进程的OOM等级,这里的值为-1000,设置为这个值就可以保证进程永远不会因为OOM被杀死
解析init.rc脚本
Android Init Language
rc文件,是用Android Init Language
编写的特殊文件。用这种语法编写的文件,统一用”.rc”后缀
它的语法说明可以在aosp源码system/core/init/README.md
中找到,这里就简单说明一下语法规则
Actions
Actions
是一系列命令的开始,一个Action
会有一个触发器,用于确定Action
何时执行。当一个与Action
的触发器匹配的事件发生时,该动作被添加到待执行队列的尾部
格式如下:
1 | on <trigger> [&& <trigger>]* |
Triggers(触发器)
触发器作用于Actions
,可用于匹配某些类型的事件,并用于导致操作发生
Commands
Commands
就是一个个命令的集合了
Action
, Triggers
, Commands
共同组成了一个单元,举个例子:
1 | on zygote-start && property:ro.crypto.state=unencrypted |
Services
Services
是对一些程序的定义,格式如下:
1 | service <name> <pathname> [ <argument> ]* |
其中:
- name:定义的服务名
- pathname:这个程序的路径
- argument:程序运行的参数
- option:服务选项,后文将介绍
Options
Options
是对Services
的修饰,它们影响着服务运行的方式和时间
Services
, Options
组成了一个单元,举个例子:
1 | service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote |
Imports
导入其他的rc文件或目录解析,如果path是一个目录,目录中的每个文件都被解析为一个rc文件。它不是递归的,嵌套的目录将不会被解析。
格式如下:
1 | import <path> |
Imports
的内容会放到最后解析
上文所述的Commands
,Options
等具体命令,可以网上搜索一下,或者自己看system/core/init/README.md
Commands
的定义可以在system/core/init/builtins.cpp
中找到
Options
的定义可以在system/core/init/service_parser.cpp
中找到
解析
1 | static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) { |
这个函数会从这些地方寻找rc文件解析,/system/etc/init/hw/init.rc
是主rc文件,剩下的目录,如果system分区尚未挂载的话,就把它们加入到late_import_paths
中,等到后面mount_all
时再加载
主rc文件在编译前的位置为system/core/rootdir/init.rc
简单分析一下:
首先,以ActionManager
和ServiceList
作为参数创建了一个Parser解析器,解析后的结果会存放在ActionManager
和ServiceList
中,这里的两个传进来的参数都是单例模式
1 | Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) { |
先创建了一个Parser
对象,然后往里面添加了ServiceParser
、ActionParser
以及ImportParser
,这三个类都是继承自ServiceParser
,这里的std::make_unique
是new了一个对象,并用其原始指针构造出了一个智能指针
接着走到Parser::ParseConfig
方法中:
1 | bool Parser::ParseConfig(const std::string& path) { |
判断是否是目录,如果是目录,就把目录中的所有文件加入容器中排序后依次解析
1 | bool Parser::ParseConfigDir(const std::string& path) { |
可以看到,最终都调用了Parser::ParseConfigFile
方法
1 | bool Parser::ParseConfigFile(const std::string& path) { |
从文件中读取出字符串,并继续调用Parser::ParseData
方法
1 | void Parser::ParseData(const std::string& filename, std::string* data) { |
这里新建了一个parse_state
结构体,用来以行为单位,分割整个文件字符串,根据分割出来的结果返回相应的TYPE,Parser::ParseData
方法再通过TYPE来做逐行解析
这个结构体以及TPYE和分割分割方法的定义在system/core/init/tokenizer.h
中,在system/core/init/tokenizer.cpp
中实现
1 | int next_token(struct parse_state *state) |
简单来说就是先看看有没有遇到结束符(换行 \n 或者EOF \0)或者注释(#)如果遇到了就返回T_NEWLINE
或者T_EOF
代表这上一行结束了或者整个文件读取完了,没有遇到的话说明读取的是可解析的正文,跳到text段,将文本内容写到state.text
中,直到碰到换行符或空格等分割标志(以空格或换行等作为分隔符,一小段一小段的进行分割),将读取到的最后一个正文的位置+1处的字符置为\0,state.text
里的内容便称为了完整一段的内容,接着返回T_TEXT
表示已读入一段文本
接着回到Parser::ParseData
方法中,如果读到的TYPE是T_TEXT
,就将这一段内容先添加到容器中,当读到T_NEWLINE
时,解析之前读入的一整行内容,先用args[0](一行的开头)去寻找我们之前添加的SectionParser
,如果能找到,说明这一行是service、on或者import,将section_parser
赋值为相应SectionParser
子类的指针,调用其ParseSection
方法解析,如果读入的一行里,不是以service、on或者import开头,并且之前定义的section_parser
不为空指针,说明是service或者on参数的子参数,调用ParseLineSection
方法解析子参数,并加入到父参数中。
最后,每次读取完都会执行args.clear()
清楚这一行的数据,当读取到新的service、on或者import时,需要先执行EndSection
方法,将之前解析好的结构添加到列表中
执行任务
回到SecondStageMain
中,可以看到,最后有一个死循环,用来等待事件处理
1 | int SecondStageMain(int argc, char** argv) { |
其中am.ExecuteOneCommand()
方法便是执行从rc文件中解析出来的指令
1 | void ActionManager::ExecuteOneCommand() { |
里面会执行Action::ExecuteOneCommand
方法
1 | void Action::ExecuteOneCommand(std::size_t command) const { |
接着调用到了Action::ExecuteCommand
方法
1 | void Action::ExecuteCommand(const Command& command) const { |
接着会调用Command::InvokeFunc
方法
1 | Result<void> Command::InvokeFunc(Subcontext* subcontext) const { |
系统原生的rc文件命令都会走到RunBuiltinFunction
方法中
1 | Result<void> RunBuiltinFunction(const BuiltinFunction& function, |
这里的function
是一个以BuiltinArguments
为参数的std::function
函数包装器模板,可以包装函数、函数指针、类成员函数指针或任意类型的函数对象,在Command对象new出来的时候构造函数就指定了这个func_,我们可以看一下Action::AddCommand
方法:
1 | Result<void> Action::AddCommand(std::vector<std::string>&& args, int line) { |
可以看到,是通过rc文件中的字符串去一个function_map_
常量中查找得到的,而这个function_map_
是在哪赋值的呢,答案是在SecondStageMain
函数中
1 | int SecondStageMain(int argc, char** argv) { |
这个在前文代码中有提及,map的定义在system/core/init/builtins.cpp
中
1 | const BuiltinFunctionMap& GetBuiltinFunctionMap() { |
启动服务
以下面一段rc脚本为例,我们看一下一个服务是怎么启动的
1 | on zygote-start |
首先这是一个action,当init进程在死循环中执行到ActionManager::ExecuteOneCommand
方法时,检查到这个action刚好符合event_queue_
队首的EventTrigger
,便会执行这个action
下面的commands
。commands
怎么执行在上面已经分析过了,我们去system/core/init/builtins.cpp
里的map中找key-value对应关系,发现start对应着do_start
函数:
1 | static Result<void> do_start(const BuiltinArguments& args) { |
ServiceList
通过args[1]
即定义的服务名去寻找之前解析好的service,并执行Service::Start
方法:
1 | Result<void> Service::Start() { |
1 | static bool ExpandArgsAndExecv(const std::vector<std::string>& args, bool sigstop) { |
这里先fork
(或clone
)出了一个子进程,再在这个子进程中调用execv
函数执行文件
到此为止,一个服务便被启动起来了
守护服务
当服务启动起来后,init
进程也要负责服务的守护,为什么呢?
假设zygote
进程挂了,那zygote
进程下的所有子进程都可能会被杀,整个Android
系统会出现大问题,那怎么办呢?得把zygote
进程重启起来呀。init
进程守护服务做的就是这些事,当接收到子进程退出信号,就会触发对应的函数进行处理,去根据这个进程所对应的服务,处理后事(重启等)
代码在这个位置:
1 | int SecondStageMain(int argc, char** argv) { |
先创建出来一个epoll句柄,再用它去InstallSignalFdHandler
装载信号handler:
1 | static void InstallSignalFdHandler(Epoll* epoll) { |
sigaction函数
先介绍一下sigaction
函数,它是用来检查和设置一个信号的处理方式的
文档:https://man7.org/linux/man-pages/man2/sigaction.2.html
第一个参数signum
,定义在signal.h
中,用来指定信号的编号(需要设置哪个信号)
第二个参数act
是一个结构体:
1 | struct sigaction { |
其中,sa_handler
表示信号的处理方式,sa_flags
用来设置信号处理的其他相关操作
第三个参数oldact
,如果不为null
,会将此信号原来的处理方式保存进去
对应一下InstallSignalFdHandler
里的调用,.sa_handler = SIG_DFL
表示使用默认的信号处理,.sa_flags = SA_NOCLDSTOP
当参数signum
为SIGCHLD
的时候生效,表示当子进程暂停时不会通知父进程
信号集函数
接下来InstallSignalFdHandler
函数调用了一些信号集函数
sigemptyset
原型:int sigemptyset(sigset_t *set);
文档:https://man7.org/linux/man-pages/man3/sigemptyset.3p.html
该函数的作用是将信号集初始化为空
sigaddset
原型:int sigaddset(sigset_t *set, int signo);
文档:https://man7.org/linux/man-pages/man3/sigaddset.3p.html
该函数的作用是把信号signo添加到信号集set中
sigpromask
原型:int sigpromask(int how, const sigset_t *set, sigset_t *oldset);
文档:https://man7.org/linux/man-pages/man2/sigprocmask.2.html
该函数可以根据参数指定的方法修改进程的信号屏蔽字
第一个参数how
有3种取值:
SIG_BLOCK
:将set中的信号添加到信号屏蔽字中(不改变原有已存在信号屏蔽字,相当于用set中的信号与原有信号取并集设置)SIG_UNBLOCK
:将set中的信号移除信号屏蔽字(相当于用set中的信号的补集与原有信号取交集设置)SIG_SETMASK
:使用set中的信号直接代替原有信号屏蔽字中的信号
第二个参数set
是一个信号集,怎么使用和参数how相关
第三个参数oldset
,如果不为null,会将原有信号屏蔽字的信号集保存进去
为什么init进程要屏蔽这些信号呢?因为它后面会特殊处理这些信号
pthread_atfork
这也是一个Linux函数,用来注册fork的handlers
原型:int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
调用这个函数后,当进程再调用fork时,内部创建子进程钱会先在父进程中调用prepare
函数,创建子进程成功后,会在父进程中调用parent
函数,子进程中调用child
函数
对应到InstallSignalFdHandler
里来,即当init进程fork出子进程后调用UnblockSignals
函数
1 | static void UnblockSignals() { |
也就是,先在init进程中屏蔽了SIGCHLD
、SIGTERM
信号,再在子进程中解除了这两个信号的屏蔽
signalfd函数
同样也是Linux函数,用来创建用于接受信号的文件描述符
原型:int signalfd(int fd, const sigset_t *mask, int flags);
参数fd如果为-1,则该函数会创建一个新的文件描述符与mask信号集相关联,如果不为-1,则该函数会用mask替换之前与这个fd相关联的信号集
flags:
- SFD_NONBLOCK:给新打开的文件描述符设置
O_NONBLOCK
标志,非阻塞I/O模式 - SFD_CLOEXEC:给新打开的文件描述符设置
O_CLOEXEC
标志,当exec函数执行成功后,会自动关闭这个文件描述符
对应到InstallSignalFdHandler
中,它创建了一个用于接受SIGCHLD
、SIGTERM
信号的文件描述符。回忆一下之前对启动服务的分析,是先调用fork创建进程,在exec执行文件,将flags设置为SFD_CLOEXEC
,这样就可以保证在子进程中关闭由fork得到的接收信号的文件描述符
注册信号处理器
最后调用Epoll::RegisterHandler
方法注册处理器,内部调用了epoll_ctl
函数,感兴趣可以自己看一下,文档:https://man7.org/linux/man-pages/man2/epoll_ctl.2.html
这样,当init进程接收到SIGCHLD
、SIGTERM
信号时便会调用HandleSignalFd
方法:
1 | static void HandleSignalFd() { |
我们这里主要看SIGCHLD
,当子进程退出,init进程便会捕获到SIGCHLD
,执行ReapAnyOutstandingChildren
方法:
1 | void ReapAnyOutstandingChildren() { |
1 | static pid_t ReapOneProcess() { |
waitid函数
Linux函数,用于等待一个子进程状态的改变
原型:int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
文档:https://man7.org/linux/man-pages/man3/waitid.3p.html
第一个参数idtype
:
- P_PID:等待的子进程的pid必须和参数id匹配
- P_GID:等待的子进程的组id必须和参数id匹配
- P_ADD:等待所有子进程,此时,参数id被忽略
这个函数会将执行的结果保存在第三个参数infop中
options
:
- WCONTINUED:等待那些由SIGCONT重新启动的子进程
- WEXITED:等待那些已经退出的子进程
- WSTOPPED:等待那些被信号暂停的子进程
- WNOHANG:非阻塞等待
- WNOWAIT:保持返回的子进程处于可等待状态(后续可以再对这个子进程进行wait)
回到ReapOneProcess
函数中来,它先调用waitid
函数,获得一个状态发生改变的子进程(options设置了WEXITED
,即已退出的子进程),使用了WNOWAIT
参数,也就是暂时先不销毁子进程,使用非阻塞的方式获取
ScopeGuard
ScopeGuard
的意思是,出作用域后,自动执行某段代码
函数中那段make_scope_guard
的意思是,当这个函数执行完后,使用waitpid
函数销毁子进程
之后会从ServiceList
中通过pid去查找service,查到后调用Service::Reap
处理后事
1 | void Service::Reap(const siginfo_t& siginfo) { |
service相关的参数可以去system/core/init/README.md
中自行查看
这个函数检查了一堆service的标志和状态,判断如何处理这个service,如果需要重启,则调用onrestart_.ExecuteAllCommands()
执行该service下的所有onrestart
命令,具体的执行过程之前在启动服务那边已经分析过了,这里就不再往下看了
总结
至此,整个init进程的启动过程最重要的部分基本都已分析完成,我也是一边从网上搜集资料一边对照着源码磕磕绊绊看过来的,有什么错误或者遗漏的部分欢迎指正,谢谢~