Android系统启动流程 四--init进程

本文深入剖析Android系统中init进程的工作原理及流程,包括信号处理、文件权限设置、系统初始化和服务启动等内容。

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

一切都始于init,bootloader 加载了内核,内核启动了init 进程。Linux系统中的init进程(pid=1)是除了idle进程(pid=0,也就是init_task)之外另一个比较特殊的进程,它是Linux内核开始建立起进程概念时第一个通过kernel_thread产生的进程,其开始在内核态执行,然后通过一个系统调用,开始执行用户空间的/sbin/init程序,期间Linux内核也经历了从内核态到用户态的特权级转变,然后所有的用户进程都有该进程派生出来。

其中包括:
•用来管理USB 连接的USB 守护进程(usbd)
•用来管理Android adb 连接的守护进程(adbd)
•用来管理调试过程的调试器守护程序(debuggerd)
•用于打电话及相关功能的后台进程 (rild)

init程序源码在Android官方源码的system/core/init中,main在init.cpp里。我们的分析就从main开始
(1)init是一个守护进程,为了防止init的子进程成为僵尸进程(zombie process),需要init在子进程在结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间,当程序表的空间达到上限时,则系统就不能再启动新的进程了,那么就会引起很严重的系统问题。
    在linux当中,父程序是通过捕捉SIGCHLD信号来得知子进程结束的情况的;由于系统默认在子进程暂停时也会发送信号SIGCHLD,init需要忽略子进程在暂停时发出的SIGCHLD信号,因此将act.sa_flags 置为SA_NOCLDSTOP,该标志位的含义是就是要求系统在子进程暂停时不发送SIGCHLD信号。
static void sigchld_handler(ints)
{
    write(signal_fd, &s, 1);
}
int main(int argc, char **argv)
{
act.sa_handler= sigchld_handler;
act.sa_flags= SA_NOCLDSTOP;
sigaction(SIGCHLD,&act, 0);
structsigaction act;
act.sa_handler= sigchld_handler;
act.sa_flags= SA_NOCLDSTOP;
       ………………………………………..
}
Linux进程通过互相发送接收消息来实现进程间的通信,这些消息被称为“信号”。每个进程在处理其他进程发送的信号时都需要注册程序,此程序被称为信号处理。当进程的运行状态改变或者终止时,就会产生某种信号,init进程是所有进程的父进程,当其子进程终止产生SIGCHLD信号时,init进程需要调用信号安装函数sigaction(),并通过参数传递至sigcation结构体中,已完成信号处理器的安装。
  Init进程通过上述代码注册与子进程相关的SIGCHLD信号处理器,并把sigcation结构体的sa_flags设置为SA_NOCLDSTOP,该值表示仅当进程终止时才接收SIGCHLD信号。
sigchld_handler函数用于通知全局变量signal_fd,SIGCHLD信号已发生。对于产生的信号的实际处理,在init进程的事件处理循环中进行。
(2)对umask进行清零。
当我们登录系统之后创建一个文件总是有一个默认权限的,那么这个权限是怎么来的呢?这就是umask干的事情。umask设置了用户创建文件的默认权限,它与chmod的效果刚好相反,umask设置的是权限“补码”,而chmod设置的是文件权限码。一般在/etc/profile、$ [HOME]/.bash_profile或$[HOME]/.profile中设置umask值。
如何计算umask值?
umask命令允许你设定文件创建时的缺省模式,对应每一类用户(文件属主、同组用户、其他用户)存在一个相应的umask值中的数字。对于文件来说,这一数字的最大值分别是6。系统不允许你在创建一个文本文件时就赋予它执行权限,必须在创建后用chmod命令增加这一权限。目录则允许设置执行权限,这样针对目录来说,umask中各个数字最大可以到7。
该命令的一般形式为:umasknnn,其中nnn为umask置000 - 777。
我们只要记住u m a s k是从权限中“拿走”相应的位即可。下表是umask值与权限的对照表:

 

umask 文件 目录
0 6 7
1 6 6
2 4 5
3 4 4
4 2 3
5 2 2
6 0 1
7 0 0

 

如:umask值为022,则默认目录权限为755,默认文件权限为644。
(3)为rootfs建立必要的文件夹,并挂载适当的分区
目前Linux有很多通讯机制可以在用户空间和内核空间之间交互,例如设备驱动文件(位于/dev目录中)、内存文件(/proc、/sys目录等)。了解Linux的同学都应该知道Linux的重要特征之一就是一切都是以文件的形式存在的,例如,一个设备通常与一个或多个设备文件对应。这些与内核空间交互的文件都在用户空间,所以在Linux内核装载完,需要首先建立这些文件所在的目录。而完成这些工作的程序就是本文要介绍的init。Init是一个命令行程序。其主要工作之一就是建立这些与内核空间交互的文件所在的目录。
    /dev (tmpfs)
    /dev/pts (devpts)
    /dev/socket
    /proc (proc)
    /sys  (sysfs)
编译Android系统源码时,在生成的根文件系统中,不存在/dev,/proc/,/sys这类目录,他们是系统运行时的目录,由init进程在运行中生成,当系统终止时,他们就会消失。
Init进程执行后,生成/dev目录,包含系统使用的设备,而后调用open_devnull_stdio();函数,创建运行日志输出设备。open_devnull_stdio()函数会在/dev目录下生成__null__设备节点文件,并将标准输入,标准输出,标准错误,标准错误输出全部重定向__null__设备中。
(4)创建/dev/kmsg节点。
init进程通过log_init函数,生成"/dev/__kmsg__"设备节点文件。__kmsg__设备调用内核信息输出函数printk(),init进程即是通过该函数输出log信息。
Init进程通过__kmsg__设备定义用于输出信息的宏。关于宏输出信息,可以使用dmesg实用程序进行确认,dmesg用于显示内核信息。
#define ERROR(x...)   log_write(3, "<3>init: " x)
#define NOTICE(x...)  log_write(5, "<5>init: " x)
#define INFO(x...)    log_write(6, "<6>init: " x)
(5)解析/init.rc,将所有服务和操作信息加入链表
parse_config_file("/init.rc");
parse_config_file()函数用来分析*.rc配置文件,用来指定init.rc文件的路径。执行parse_config_file函数,读取并分析init.rc文件后,生成服务列表与动作列表。动作列表与服务列表全部会以链表的形式注册到service_list和action_list中,service_list和action_list是init进程中声明的全局结构体。
解析详情:   Init.rc文件解析
(6) 初始化qemu设备,设置模拟器环境;从/proc/cmdline中提取信息内核启动参数,并保存到全局变量。
qemu_init();
QEMU模拟器允许Android应用开发者在缺少Android实际设备的情况下运行处于开发中的应用程序。QEMU是面向PC的开源模拟器,能够模拟具有特定处理器设备,此外还提供虚拟网络,视频设备等。Android模拟器是虚拟的硬件平台,运行在模拟器的软件可以执行ARM命令集,LCD,相机,SD卡控制器等硬件设备都可以运行在Goldfish这种虚拟平台上。
import_kernel_cmdline(0);
(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo中提取,并保存到全局变量。
(8)根据硬件信息选择一个/init.(硬件).rc并解析,将服务和操作信息加入链表。
在G1的ramdisk根目录下有两个/init.(硬件).rc:init.goldfish.rc和init.trout.rc,init程序会根据上一步获得的硬件信息选择一个解析。
(9)执行链表中带有“early-init”触发的的命令。                                    action_for_each_trigger("early-init",action_add_queue_tail);触发在init脚本文件中名字为early-init的action,并且执行其commands,其实是:on early-init,在我们的init.rc中是没有的。action_for_each_trigger函数会将第一个参数中的命令保存到action_add_queue_tail,而后通过drain_action_queue()函数将运行队列中的命令逐一取出执行。
(10)遍历/sys文件夹, 将这些目录下的uevent文件找出,并使kernel重新生成那些在init的设备管理器开始前的设备添加事件。 初始化动态设备管理,使内核产生设备添加事件(为了自动产生设备节点), 设备文件有变化时反应给内核。
(11)初始化属性系统,并导入初始化属性文件。  
初始化属性服务,本质上属性服务是通过共享内存实现的,逻辑上类似与windows系统的注册表服务。
首先创建一个名字为system_properties的匿名共享内存区域,对init进程做mmap读写映射,然后将这个prop_area结构体通过全局变量__system_property_area__传递给property services。
其余进程通过执行中的进程锁提供的API,访问属性中的设置值。当修改属性值时,要预先向init进程提交值变更申请,然后init进程处理该申请,并修改属性值。
(12)从属性服务中得到ro.debuggable,如果ro.debuggable为1,则初始化组合键(keychord )监听  这段代码是从属性里获取调试标志,如果是可以调试,就打开组合按键输入驱动程序,初始化keychord监听。
(13)打开console,如果cmdline中沒有指定console則打开默认的/dev/console。  
(14)读取/initlogo.rle,是一张565 rle 压缩的位图,如果成功则在/dev/fb0显示Logo,如果失败则将/dev/tty0设为TEXT模式并打开/dev/tty0,输出文本的ANDROID字样。
load_565rle_image(INIT_IMAGE_FILE)函数将加载由参数传递过来的图像文件,而后将该文件显示在LCD屏幕上。如果想更改logo,只需修改INIT_IMAGE_FILE即可。由于函数只支持rle565格式图像的显示,再更改图像时,注意所选图像文件的格式。
(15) 这段代码是用来判断是否使用模拟器运行,如果是,就加载内核命令行参数。
if (qemu[0])
import_kernel_cmdline(1);
(16)这段代码是根据内核命令行参数来设置工厂模式测试,比如在工厂生产手机过程里需要自动化演示功能,就可以根据这个标志来进行特别处理。
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");
//这段代码是设置手机序列号到属性里保存,以便上层应用程序可以识别这台手机。
property_set("ro.serialno",serialno[0] ? serialno : "");
//这段代码是保存启动模式到属性里。
property_set("ro.bootmode",bootmode[0] ? bootmode : "unknown");
//这段代码是保存手机基带频率到属性里。
property_set("ro.baseband",baseband[0] ? baseband : "unknown");
//这段代码是保存手机硬件载波的方式到属性里。
property_set("ro.carrier",carrier[0] ? carrier : "unknown");
//保存引导程序的版本号到属性里,以便系统知道引导程序有什么特性。
property_set("ro.bootloader",bootloader[0] ? bootloader : "unknown");
//这里是保存硬件信息到属性里,其实就是获取CPU 的信息。
property_set("ro.hardware",hardware);
//这里是保存硬件修订的版本号到属性里,这样可以方便应用程序区分不同的硬件版本。
snprintf(tmp,PROP_VALUE_MAX, "%d", revision);
property_set("ro.revision",tmp);
(17)执行所有触发标识为init的action。
 /* execute all the boot actions to get usstarted */
//执行所有触发标志为init的action
action_for_each_trigger("init",action_add_queue_tail);
(18)启动property服务
/* read any property files on system or dataand
* fire up the property service. This musthappen
* after the ro.foo properties are set aboveso
* that /data/local.prop cannot interferewith them.
*/
/*
开始property 服务,读取一些property 文件,这一动作必须在前面那些ro.foo设置后做,
以便/data/local.prop 不能干预到他们
- /system/build.prop
- /system/default.prop
- /data/local.prop
- 在读取认识的property 后读取presistentpropertie,在/data/property 中
这段代码是加载system 和data 目录下的属性,并启动属性监听服务。
(19)为sigchld handler创建信号机制
property_set_fd = start_property_service();
(20)创建一个全双工的通讯机制的两个SOCKET
创建一个全双工的通讯机制的两个SOCKET,信号可以在signal_fd
和signal_recv_fd 双向通讯,从而建立起沟通的管道。其实这个信号管理,就
是用来让init 进程与它的子进程进行沟通的,子进程从signal_fd 写入信息,init
进程从signal_recv_fd 收到信息,然后再做处理。
(21)判断关键组件是否成功初始化
主要就是设备文件系统是否成功初始化,属性服务是否成功初始化,信号通讯机制是否成功初始化。
(22)执行所有触发标识为early-boot的
action  action_for_each_trigger("early-boot",action_add_queue_tail);
action_for_each_trigger("boot",action_add_queue_tail);
(23)基于當前property狀態,執行所有触发标识为property的action 
//这段代码是根据当前属性,运行属性命令。
queue_all_property_triggers();
drain_action_queue();
/* enable propertytriggers */
// 标明属性触发器已经初始化完成。
property_triggers_enabled= 1;
/*
这段代码是保存三个重要的服务socket,以便后面轮询使用。
*/
ufds[0].fd =device_fd;
ufds[0].events =POLLIN;
ufds[1].fd =property_set_fd;
ufds[1].events =POLLIN;
ufds[2].fd =signal_recv_fd;
ufds[2].events =POLLIN;
fd_count = 3;
//这段代码是判断是否处理组合键轮询。
ufds[3].events = POLLIN;
fd_count++;
} else {
ufds[3].events = 0;
ufds[3].revents = 0;
}
//如果支持BOOTCHART,则初始化BOOTCHART
#if BOOTCHART
/*
这段代码是初始化linux 程序启动速度的性能分析工具,这个工具有一个好处,就是图形化显示每个进程启动顺序和占用时间,如果想优化系统的启动速度,记得启用这个工具。
*/
bootchart_count = bootchart_init();
if (bootchart_count < 0) {
ERROR("bootcharting initfailure\n");
} else if (bootchart_count > 0) {
NOTICE("bootcharting started(period=%d ms)\n",
bootchart_count*BOOTCHART_POLLING_MS);
} else {
NOTICE("bootcharting ignored\n");
}
#endif
/*
进入主进程循环:
- 重置轮询事件的接受状态,revents 为0
- 查询action 队列,并执行。
- 重启需要重启的服务
- 轮询注册的事件
- 如果signal_recv_fd 的revents 为POLLIN,则得到一个信号,获取并处理
- 如果device_fd 的revents 为POLLIN,调用handle_device_fd
- 如果property_fd 的revents 为POLLIN,调用handle_property_set_fd
- 如果keychord_fd 的revents 为POLLIN,调用handle_keychord
这段代码是进入死循环处理,以便这个init 进程变成一个服务。
*/
for(;;) {
int nr, i, timeout = -1;
// 清空每个socket 的事件计数。
for (i = 0; i < fd_count; i++)
ufds[i].revents = 0;
// 这段代码是执行队列里的命令。
drain_action_queue();
// 这句代码是用来判断那些服务需要重新启动。
restart_processes();
// 这段代码是用来判断哪些进程启动超时。
if (process_needs_restart) {
timeout = (process_needs_restart -gettime()) * 1000;
if (timeout < 0)
timeout = 0;
}
#if BOOTCHART
//这段代码是用来计算运行性能。
if (bootchart_count > 0) {
if (timeout < 0 || timeout >BOOTCHART_POLLING_MS)
timeout = BOOTCHART_POLLING_MS;
if (bootchart_step() < 0 ||--bootchart_count == 0) {
bootchart_finish();
bootchart_count = 0;
}
}
#endif
// 这段代码用来轮询几个socket 是否有事件处理。
nr = poll(ufds, fd_count, timeout);
if (nr <= 0)
continue;
/*
这段代码是用来处理子进程的通讯,并且能删除任何已经退出或者杀死进程,这样做可以保持系统更加健壮性,增强容错能力。
*/
if (ufds[2].revents == POLLIN) {
/* we got a SIGCHLD - reap and restart asneeded */
read(signal_recv_fd, tmp, sizeof(tmp));
while (!wait_for_one_process(0))
;
continue;
}
// 这段代码是处理设备事件。
if (ufds[0].revents == POLLIN)
handle_device_fd(device_fd);
// 这段代码是处理属性服务事件。
if (ufds[1].revents == POLLIN)
handle_property_set_fd(property_set_fd);
// 这段代码是处理调试模式下的组合按键。
if (ufds[3].revents == POLLIN)
handle_keychord(keychord_fd);
}
return 0;
}
概括起来大体上也可做以下分析:
init:
(1)安装SIGCHLD信号。(如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。
(2)对umask进行清零。
 
(3)为rootfs建立必要的文件夹,并挂载适当的分区。
    /dev (tmpfs)
       /dev/pts(devpts)
       /dev/socket
    /proc (proc)
   /sys  (sysfs)
  (4)创建/dev/null和/dev/kmsg节点。
(5)解析/init.rc,将所有服务和操作信息加入链表。
  (6)从/proc/cmdline中提取信息内核启动参数,并保存到全局变量。
(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo中提取,并保存到全局变量。
(8)根据硬件信息选择一个/init.(硬件).rc,并解析,将服务和操作信息加入链表。
       在G1的ramdisk根目录下有两个/init.(硬件).rc:init.goldfish.rc和init.trout.rc,init程序会根据上一步获得的硬件信息选择一个解析。
(9)执行链表中带有“early-init”触发的的命令。
(10)遍历/sys文件夹,是内核产生设备添加事件(为了自动产生设备节点)。
(11)初始化属性系统,并导入初始化属性文件。
(12)从属性系统中得到ro.debuggable,若为1,則初始化keychord監聽。
(13)打开console,如果cmdline中沒有指定console则打开默认的/dev/console。
(14)读取/initlogo.rle(一张565 rle压缩的位图),如果成功则在/dev/graphics/fb0显示Logo,如果失败则将/dev/tty0设置为TEXT模式并打开/dev/tty0,输出文本“ANDROID”字样。
(15)判断cmdline 中的参数,并设置属性系统中的参数:
        1、 如果bootmode为
        -factory,设置ro.factorytest值为1
        -factory2,设置ro.factorytest值为2
        -其他的設ro.factorytest值为0
      2、如果有serialno参数,则设置ro.serialno,否则为""
      3、如果有bootmod参数,则设置ro.bootmod,否则为"unknown"
     4、如果有baseband参数,则设置ro.baseband,否则为"unknown"
      5、如果有carrier参数,则设置ro.carrier,否则为"unknown"
     6、如果有bootloader参数,则设置ro.bootloader,否则为"unknown"
     7、通过全局变量(前面从/proc/cpuinfo中提取的)设置ro.hardware和ro.version。
(16)执行所有触发标识为init的action。
(17)开始property服务,读取一些property文件,這一动作必須在前面那些ro.foo设置后做,以便/data/local.prop不能干预到他们。
     - /system/build.prop
     - /system/default.prop
     - /data/local.prop
     - 在读取默认的property后续取presistentpropertie,在/data/property中
(18)为sigchld handler创建信号机制。
(19)确认所有初始化工作完成:
        device_fd(device init 完成)
        property_set_fd(property server start 完成)
        signal_recv_fd (信号机制建立)
(20) 执行所有触发标识为early-boot的action
(21) 执行所有触发标识为boot的action
(22)基于当前property状态,执行所有触发标识为property的action
(23)注册轮询事件:
         -device_fd
         -property_set_fd
         -signal_recv_fd
         -如果有keychord,則注册keychord_fd
(24)如果支持BOOTCHART,则初始化BOOTCHART
(25)进入主进程死循环:
     - 重置轮询事件的接受状态,revents为0
     - 查询action队列,并执行。
     - 重启需要重启的服务
     - 轮训注册的事件
         -如果signal_recv_fd的revents为POLLIN,则得到一个信号,获取并处理
         -如果device_fd的revents为POLLIN,调用handle_device_fd
         -如果property_fd的revents为POLLIN,调用handle_property_set_fd
         -如果keychord_fd的revents为POLLIN,调用handle_keychord
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值