Kujira Sama`s Blog | 什么都略懂一点,生活更多彩一些

  • 首页

  • 标签

  • 分类

  • 归档

init的角色和任务

发表于 2019-08-05 更新于 2019-08-06 分类于 安卓架构大剖析 评论数:

在桌面linux中,第一个用户态进程为/sbininit,它会读取/etc/inittab文件,以获取所支持的”运行级“(run-levels)、运行时配置信息(单用户、多用户、网络文件系统等)、需随机启动的进程以及当下用户按下CTRL+Alt+Del组合键时该做出何种反应等信息。

在android中也是用这样的init程序,但android中的init和linux中的init仅仅是名字相同罢了,它们之前的区别如下表所示:

配置文件 /etcinittab /init.rc以及它导入的任何文件[通常是init.hardware.rc和init.usb.rc]
多种配置 支持:”运行级“的概念(0:系统停机状态; 1:单用户工作状态;2-3: 多用户状态;……)。每个”运行级“都会从/etc/rcrunlevel.d2那里加载脚本 没有”运行级“的概念,但是通过触发器(trigger)和系统属性提供了配置选项
watchdog 支持:用respawn关键字定义过的守护进程会在退出时重启—除非该进程反复崩溃,会在这种情况下,反复崩溃的进程会被挂起几分钟 支持:服务默认是应该保持活跃的。除非启动它时使用了oneshot参数。服务启动时还可以使用critical参数,这会使系统在该服务无法启动时强制重启
收容孤儿进程 支持/sbin/init会调用wait4()系统调用去获取孤儿进程的返回码,避免出现僵尸进程 支持:/init注册了一个SIGCHLD信号的处理模块,SIGCHLD信号是内核在子进程退出时自动发送的,大多数进程会默默的调用wait(NULL)清理掉已退出的子进程,而不去管他的返回码是什么
系统属性 不支持 支持:/init通过共享一块内存区域的方式,让系统中所有进程都能读取系统属性(通过getopt),并通过一个名为”property_service“的socket让所有权限的进程能够(通过’setopt’)写相关属性
分配socket 不支持:linux的init不会向子进程提供socket,这个功能交个inetd的 支持:/init 会绑定一个UNIX domain socket(从L版开始,是seqpacket socket)提供给子进程,子进程可以通过android_get_control_socker函数获取它
触发操作 不支持: linux只支持非常特殊的触发操作,如CTRL+Alt+Del和UPS电源事件,但是不允许任意的触发操作 支持:/init可以在任何一个系统属性被修改时,执行trigger语句块中的,由任何一个用户预先写好的指令
处理uevent事件 不支持:linux依靠的时hotplug守护进程(通常为udevd) /init也会化身为ueventd,用专门的配置文件来指导其行为

/init是静态链接的可执行文件,在编译时它所依赖的库都已经被合并到这个二进制文件里去了、这样做的目的是为了防止仅仅因为缺少某个库而造成的系统无法正常启动的情况发生。在/init刚被执行时,只有和内核一起打包放在boot分区的RAM disk被mount上来,换句话说,系统中只有/和/sbin。

系统属性

android的系统属性提供了一个可以全局访问的配置设置仓库[1],它在形式上和MIB数组作为参数调用的sysctl(2)有些类似,只不过是在用户态中,由init实现了。在init的相关源码property_service.c中的代码,会按照下表给出的顺序,从多个文件中加载属性。

文件 内容
/default.prop(PROP_PATH_RAMDISK_DEFAULT) 初始设置。注意。这个文件是initramfs中的一部分,直接到设备的闪存分区上是找不到的
/system/build.prop(PROP_PATH_SYSTEM_BUILD) 编译android过程中产生的设置
/system/default.prop(PROP_PATH_SYSTEM_DEFAULT) 通常是厂商添加的设置
/data/local.prop(PROP_PATH_LOCAL_OVERRIDE) 如果编译init时使用了ALLOW_LOCAL_PROP_OVERRIDE选项,并且ro.debuggable属性被设置为1,那么就会加载这个文件。这使得开发者可以通过/data分区里push一个文件的方式,修改之前的配置
/data/proper/persist.*(PERSISTENT_PROPERTY_DIR) 重启后不会丢失的属性(Persistent property),这些属性文件会被加上前缀persist。它们会被分别存放在/data/proper目录中的各个文件里,躲过重启。只要/init.rc脚本中有这条指令,init就会重新加载这些属性

除了上面中列出的属性文件外,还有另外一个属性文件/factory/factory.prop(PROP_PATH_FACTORY),这个属性文件现在已经不再被支持。

因为init时系统所有进程的祖先,所以只有它才天生适合实现系统属性的初始化。在它刚开始初始化的时候,init中的代码会调用property_init去安装系统属性。这个函数最终会调用map_prop_area()函数,并打开PROP_FILENAME(这个宏定义指的是/dev/__properties__),然后在关闭这个文件的描述符之前,用mmap(2)系统调用以”读/写“权限把这个文件的内容map到内存里。之后,init又会再次打开这个文件,不过这词用的是O_READONLY,然后再unlink掉它。

属性文件的只读文件描述符被设为可以被子进程继承。这使得系统中任何一个进程都可以方便的通过mmap(2)映射这个文件描述符的方式访问到系统的属性—尽管只能读。这是一个很巧妙的方法,它让所有可以访问属性的用户共享了这块被称为__system_property_area的物理内存[2](它的大小是用宏定义PA_SIZE来表示,这个宏定义的默认值为128KB)。除了init进程,别的进程都无法对__system_property_area进行写操作。下面为华为荣耀平板(JDN-W09)的输出结果。

1
2
3
4
5
6
7
#init 的内存区域
root@hwjdn:/proc # grep properties /proc/1/maps
7fa045b000-7fa0483000 rw-s 00000000 00:0c 10356 /dev/__properties__
#其它进程 的内存区域
root@hwjdn:/proc # grep properties /proc/$$/maps
7fa7991000-7fa79b9000 r--s 00000000 00:0c 10356 /dev/__properties__
7fa7baf000-7fa7bd7000 r--s 00000000 00:0c 10356 /dev/__properties__

__system_property_area这块内存区域的开头部分是一个很短的头部,其中记录了一个序列号(内部版本号)、一个签名文件(0x504f5250,也是PROP)以及一个版本号。紧接着这个头部的时112字节的被存储在一种混杂使用了单词查找树[Trie树或字典树]和二叉树的数据结构中。这个数据结构在Bionic的system_properties.cpp文件中有相当明晰的文档,如下面代码所示[3]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/*
* Properties are stored in a hybrid trie/binary tree structure.
* Each property's name is delimited at '.' characters, and the tokens are put
* into a trie structure. Siblings at each level of the trie are stored in a
* binary tree. For instance, "ro.secure"="1" could be stored as follows:
*
* +-----+ children +----+ children +--------+
* | |-------------->| ro |-------------->| secure |
* +-----+ +----+ +--------+
* / \ / |
* left / \ right left / | prop +===========+
* v v v +-------->| ro.secure |
* +-----+ +-----+ +-----+ +-----------+
* | net | | sys | | com | | 1 |
* +-----+ +-----+ +-----+ +===========+
*/

property_service

为了能提供写(系统属性)请求的服务,init专门打开了一个专用的UNIX domain socket—/dev/socket/property_service。只要能连上这个socket,任何人都可以对它进行写操作(0666)。这些(通过/dev/socket/property_service这个socket)写入的命令会被直接送给init,init首先回去检查调用者是不是有设置相关属性的权限(这些权限信息在property_perms数组中)。权限的检查方式为判断uid和gid,判断调用者的uid和gid(/dev/socket/property_service这个socket的调用者证书中包含相关信息)是否和数组中的定义相符。如果系统包含有SELinux增强,属性的namespace还会改用安全上下文(security context,定义带/property_contetxts中)提供更好的保护。

特殊的namespace前缀

init能够识别出一些特定的前缀,这些前缀表示应该如何处理相关属性。

  • persist伪前缀:这个前缀是为了让这些属性在系统重启后依然生效,这些重启后不会丢失的属性保存在/data/property/目录里的各个文件中,这些文件的拥有者必须要是root:root,且不是链接。
  • ro伪前缀:这个是用来标记只读属性的。不管是操作者还是拥有者,它都能设置且只能设置一次。通常,这些设置在场上提供build文件时就已经设置好了。
  • ctl前缀:这个前缀为了方便控制init中提供的各种服务而设立的,通过设置相关服务的ctl.start或者ctl.stop属性的值,就能很便捷的控制相关的服务。(toolbox中的start和stop工具实际上就是利用这个带ctl前缀的属性来控制zygote、surfacefliger和netd的)。在control_perms数组中记录一个不连续的ACL(Access Control List,访问控制列表),通过uid和gid来指定谁有启动和停止的权利。从Lollipop版本之后,SELinux接管了ACL的访问控制职能。

属性的访问方法

在toolbox里提供了两个属性访问的命令行工具getprop和setprop,另外还有属性监听工具watchprops。属性访问的原生api都在system/core/include/cutils/property.h文件中:

1
2
3
4
5
6
// 获取指定属性key和value,如果key的值不存在的话,也可以用参数default_value给一个默认值
int property_get(const char* key, char* value, const char* default_value);
// 设定指定属性key的值,这个接口把key和value串在一起,用property_service这个socket发送给init
int property_set(const char *key, const char *value);
// 用一个回调函数propfn来一一处理各个属性。只要有一个属性,这个回调函数就会被调用一次。
int property_list(void (*propfn)(const char *key, const char *value, void *cookie), void *cookie);

sys/_system_properties.h文件中还定义了一些没有文档支持的接口,其中__system_property_wait_any(unsigned int serial),它会让调用它的进程进入休眠状态,直到某个属性被修改时被被唤醒,在watchprops工具中就用了这个函数。在框架中,使用接口android.os.SystemProperties类来访问系统属性。

.rc文件

init的主要操作是加载它的配置文件,并依照配置文件执行相应的命令。传统上使用的配置文件有两个:主配置文件/init.rc和与设备相关的配置文件/init.hardware.rc,其中hardware相关的配置应该被替换为从内核参数androidboot.hardware,或从/proc/cpuinfo那里获得的字符串。比如,模拟器中默认使用的这个hardware字段就是goldfish。在实体设备中,系统中直接复制了/init.goldfish.rc文件的情况也不在少数,这可能是因为大部分的设备安装的android系统直接复制了模拟器的文件系统,而忽视了这个细节的缘故。这个设计的主要目的是让所有的android设备都能够共享一个init.rc文件,而与设备紧密相关的特定配置则让厂商写在/init.hardware.rc文件里。但在实际应用中,有时为了省事,也会把一些硬件相关的配置直接加在了/init.rc中。

trigger、action和service

.rc文件是由trigger语句块和service语句块构成的。trigger语句块中的命令,会在触发条件被满足时执行;而service语句块中定义的时各个守护进程,init会根据命令启动相关守护进程,也可以根据相关参数修改服务状态。service语句块由关键字service开头,后面跟着服务名及命令行。trigger语句块由关键字on定义,后面跟着一个参数(这个参数既可以是预先规定的各个启动阶段boot stage的名称,也可以是一个property关键字,后面跟冒号加”属性名称=属性值“这样的表达式),如果条件被满足,相应的属性值会改成指定的值。在执行指定action或command时,init会分别把属性init.action或init.command的值设置为当前正在执行的动作的名称。预先规定的启动阶段见下表(并不一定每个启动阶段都必须使用):

启动阶段 内容
early-init 初始化的第一个阶段,用于设置SELinux和ODM
init 创建文件系统,mount点以及写内核变量
early-fs 文件系统准备被mount前需要完成的工作
fs 专门用于加载各个分区
post-fs 在各个文件系统(/data分区除外)mount完毕后需要执行的命令
post-fs-data 解密/data分区(可选),并mount之
early-boot 在属性服务(property service)初始化之后,启动剩余内容之前需完成的作业
boot 正常启动命令
charger 当手机处于充电模式时需要执行的命令

init.rc的语法和命令集

init.rc及其导入的文件都有非常好的注释。init_parser.c中的代码在解析rc文件时,能否识别出COMMAND(值在on关键字开头的语句块中有效)和OPTION(只在service关键字开头的语句块中有效)。下表列出了init所支持的各个COMMAND关键字(keywords.h)。

命令 语法
bootchart_init 启用启动时的信任链验证
chdir dir 等价于cd命令
chmod actal_perms file 修改制定文件的权限
chown user group file 等价于chown user:group file命令
Chroot dir 等价于chroot
class_reset service_calss 停止与service_class相关的所有服务
class_[start|stop] class 启动或者定制class参数制定的service_class相关的所有服务
copy src dst 类似于cp命令
domainname domainname 把制定的域名写入/prco/sys/kernel/domainname伪文件中
exec command 不再支持
enable service 启动一个被disable的服务
export variable value 全局环境中,设置环境变量variable的值,该命令的执行结果会影响所用到的所有进程
hostname hostname 把主机名写入/proc/sys/kernel/hostname伪文件中
ifup interface 激活指定的网卡
insmod ko 加载一个内核模块
import filename.rc 导入另一个rc文件
load_all_props 加载build,default,factory文件中规定的属性
load_persist_props 加载/data/propert目录中的各个文件中的属性
loglevel level 设置内核的日志级别
mkdir dir 创建一个目录
mount fstype fs point 把指定分区类型(fstype)的分区(fs)mount到指定的mount点(point)上去
mount_all mount所有在vold守护进程使用的/fstab.hardware中规定的文件系统,这会使init fork()出一个子进程,并在这个子进程中用fs_mgr执行mount操作。init同时也能检测出所有加密的文件系统
powerctl shoutdown/reboot 对shutdown和reboot的封装
[re]start service_name 启动/重启服务名与参数service_name相同的service语句块中规定的服务
restorecon[_recursive] path 用path参数指定的文件重新加载SELinux上下文
rm[dir] filename 删除文件或目录
setcon SEcontext 设置或改变SELinux的上下文,init使用的上下文是u:r:init:s0
setenforce [0|1] 强制启动或者关闭SELinux
setkey table index value 设置键盘映射表的内容
setprop key value 设置指定的系统属性
setsebool name value 设置某个与SELinux相关的bool型属性
setlimit category min max 使用setlimit(2)系统调用,设置进程可用资源的上下限
stop service_name 停止服务名与参数service_name相同的service语句块中规定的服务
swapon all 激活所有fstab中的swap分区
symlink target src 创建一个符号链接
sysclktz tzoffset 设置系统时区
verity_load_state 加载DM-verity(分区加密)状态
verity_update_state mount 更新DM-verity状态,并设置系统属性partition.mount.verified
wait file timeout 等待文件file创建完,等待最大时间不超过timeout
write file value 把value写入到文件file中去

COMMAND关键字指令会被用在各个启动阶段中,去执行在系统启动时所需要的操作,如设置目录结构、调整权限、通过/proc或/sys设置相关的内核参数。启动阶段所要执行的指令处理完成后,就需要处理服务相关的指令,按照规定,service语句块中使用的命令必须是用OPTION关键字指令,这些指令确定了该如何运行服务,以及相关服务退出/崩溃时需要进行的操作。下表列出了各个可用的OPTION关键字。

关键字 说明
capability 支持Linux的capability(7)
class 把服务加入某个服务组,可以用class_[start|stop|reset]命令同时操作组内的服务
console 把该服务定义为一个console服务,stdin/stdout/stderr会被link到/dev/console上
critical 把该服务定义为一个关键服务,关键服务崩溃后会自动重启。如果在CRITICAL_CRASH_WINDOW(240)秒之内,它的崩溃次数超过了CRITICAL_CRASH_THRESHOLD(4)次,系统将会进入recovery模式
disable 表示该服务不需要启动,但可以手动启动
group 规定该服务以指定的gid启动。init会调用setgid(2)来完成这一操作
ioprio 指定该服务的I/O优先级,init会调用ioprio_set来完成
keycodes 指定触发该服务的组合键
oneshot 让init启动该服务后就不在管它了(忽略SIGCHLD信号)
onrestart 列出该服务重启时要执行的命令,通常用来重启其它依赖服务
seclabel 指定应用在该服务上的SELinux标签
setenv 该服务被fork()出来并被exec()之前,先给它设置一个环境变量。这个变量只对该服务有效
socket 告诉init打开一个UNIX Domain socket,并让该服务集成这个socket,这样做的目的是解决服务的stdin/stdout的设置问题
user 指定该服务以指定的uid运行,init将调用setuid(2)完成这个任务
writepid 把子进程的PID写到指定的文件中去,用于设置cgroup资源控制

启动服务

android中的init还是用pid=1进程的传统方式启动服务的,即它fork()出服务子进程,然后调用setuid(2)/setpid(2)设置服务子进程的权限,并设置该服务子进程用来捕获输入的socket及配置环境变量、I/O优先级(在服务块语句中使用ioprio关键字)和SELinux上下文。对于使用console关键字定义的服务,init会把/dev/console连到它的stdin/stdout/stderr上,而对其他服务,它会把stdio给干掉。只有当这些操作都执行完毕后,init才会去执行服务本身的二进制可执行文件。

在服务启动之后,init会维持一个指向该服务的父链接(parental link),一旦服务停止运行或者崩溃,init就会收到一个SIGCHLD信号,并注意这一事件,然后重启该服务。onrestart关键字会使init在各个指定的服务之间建立起关联,当特定的服务需要重启时,init会去运行这个使用了onrestart关键字的service语句块中的命令,或者重启与该服务有依赖关系的服务。对于每一个服务,init还会维持一个反映了该服务当前运行状态的属性init.svc.service。

组合键

init能够在用户按下某个组合键(keychords)是启动某些服务。每个键都有指定的ID(来自Linux的evdev输入机制)。写在keycodes关键字后面的,是android键盘布局文件(/system/usr/keylayout)中规定的键位代码,而不是android框架规定和使用的键位代码(frameworks/native/include/android/keycodes.h)。

要支持组合键,/dev/keychord文件是必须存在的。这个文件是由一个keychord内核驱动导出的设备节点。

mount文件系统

尽管有vold,但init仍然需要执行一些mount操作。在init刚刚启动时,只有root文件系统被mount上来,此时/system和/data都没有被mount,且vold等守护进程都在/system中,所以需要init进程至少把/systemmount上来。

init能够认出/init.rc中的mount_all指令,并执行mount所有默认文件系统(/fstab.hardware文件中)的操作。执行mount操作的代码位于fs_mgr中,无论/init还是vold都会使用它。当/init执行mount操作时,它首先会fork()一个子进程执行相关操作,这是为了避免在执行mount时出现问题而影响自己的启动活动。

被fork()出来的子进程会去执行mount操作,如果有需要,还会对文件系统进行fsck操作。fs_mgr中规定了对各种不同的文件系统执行fsck操作的程序的所在路径,而且这个fsck操作也是由再次fork()出来的子进程来完成。fs_mgr中会提升日志级别,所以,使用dmesg能够看到fs_mgr输出的各种消息。如果时机足够早的话,是可以看到fs_mgr特有标记的消息。

fork()出来的mount操作子进程会向父进程返回一个返回码。/init会根据这个返回码设置属性vold.decrypt的值,这个属性会被vold读取,并对相应的文件系统进行解密操作。如果没有任何文件系统被加密,init就会执行名为”nonencrypted”的trigger语句块中的指令。

init总结

init进程完全遵循建立服务的经典模式:初始化,然后陷入一个循环中,而且永远不想从中退出来。

初始化流程

init进程初始化工作由以下步骤组成:

  • 检查自身是否被当成ueventd或watchdogd调用的,如果是,则余下的执行流程会转到相应的守护进程的主循环那里去。

  • 创建/dev、/proc、/sys等目录,并mount它们。

  • 添加文件/dev/.booting,在启动完毕后,这个文件会被(check_startup)清空。

  • 调用open_devnull_stdio()函数完成“守护进程化”操作(把stdin/stdout/stderr链接到/dev/null中)

  • 调用klog_init()函数创建/dev/__kmsg__(Major 1, Minor 11),然后立即删除它。

  • 调用property_init()函数,在内存创建__system_property_area区域。

  • 调用get_hard_name()函数,读取/proc/cpuinfo伪文件中的内容,并提取出“Hardware”一行的内容作为硬件名。以这种方式获取硬件名。

  • 调用process_kernel_cmdline()函数,读取/proc/cmdline伪文件中的内容,并把所有androidboot.XXX的属性都复制一份出来,变成rro.boot.XXX。

  • 初始化SELinux。SELinux是放在/dev和/sys里面的。

  • 检查设备是否处在“充电模式”。如果设备处于“充电模式”,会使init跳过大部分初始化阶段,并且只加载各个服务中的“charger”类(当前只有“charger”守护进程有这个类)。如果设备没有处于“充电模式”,那么init将会去加载/default.prop,正常执行启动过程。

  • 调用init_parse_config_file()函数去解析/init.rc脚本文件。

  • init会把init.rc文件中各个on语句块里规定的action(action_for_each_trigger()函数)以及内置的action(queue_builtin_action()函数)添加一个名为action_queue的队列里去。

  • 最后,主循环中将逐个执行init.rc中的所有命令

    init将在它的生命周期中的大多数时间里处于休眠状态,偶尔轮询各个文件描述符,并根据宏BOOTCHART有没有被定义,来判断是否需要将日志发送到bootchart,只有在必要的时候init进程才会被唤醒。我们可以通过查看/proc伪文件系统,来了解init进程打开了哪些文件描述符。

主循环

init的主循环相当简洁,一共由三个步骤组成:

  • execute_one_command():从队列action_queue的头部取出一个action并执行。

  • restart_processes():逐个检查所有已经注册过的服务,并在必要时重启。

  • 安装并轮询如下三个socket描述符。

    • property_set_id(/dev/socket/property_service),这个socket是用来让想要设置某个属性的客户端进程,通过它把需要设置的属性的key和value发送给init。init会根据对端进程的证书,来判断这个进程是否有权限进行属性设置。

    • keychord_fd(/dev/keychord),它是用来处理上文讨论过的启动服务的组合键。

    • signal_recv_fs它是socketpair中的一端,创建它是为了处理因子进程死亡而发来的SIGCHLD信号。当init收到这个信号后,sigchld_hander就会向socketpair(signal_fd)写入数据,这样在这个sockerpair的接受端就能收到这些数据,并导致init去调用wait_for_one_process(0)。这样就能获得已死亡的子进程的返回码,以便去释放该进程残留的资源,防止它成为僵尸进程,然后init会清理这个socketpair中的数据,等待下一个进程挂掉。如果挂掉的进程是critical服务的守护进程的话,init也会重启这个进程。

除了上述init监听的三个文件以外,/init是不会再其它任何地方接受输入的。唯一能够修改/init操作行为的方法就是编辑/init.rc,但这个文件位于一个完全隔离的分区,和内核放在一起,除非设备的Boot Loader被解锁,否则无法进行修改。


  1. 1.类似于windows注册表的东西,每个属性实际上就是键值对。 ↩
  2. 2./dev/__properties__文件被mmap到内存中后,map了该文件内容的内存区域的起始位置就会被记录到全局变量__system_property_area上。 ↩
  3. 3.原文中说的是c文件,但实际上下面这段代码是在cpp文件中:code。 ↩

ARTS-3

发表于 2019-08-04 分类于 ARTS 评论数:

Week 3

阅读全文 »

android系统镜像

发表于 2019-07-21 分类于 安卓架构大剖析 评论数:

​一般为一套镜像,由厂商提供,刷机时会将各个镜像刷入到各自的分区中。

  • Boot Loader:启动时由CPU(或芯片中的应用处理器)执行的代码。这些代码一般用来寻找和加载boot镜像;或者固件升级;或者让系统启动到recovery模式下。多数Boot Loader还会实现简单的usb栈,用来提供给用户控制启动以及升级过程(fastboot)。通常被刷入/aboot分区。
  • boot镜像:由内核和ramdisk组成,用来加载系统。ramdisk用作root文件系统,里面包含系统内目录的基本框架,并在/init.rc和相关文件中规定了其它目录的加载方法。通常被刷入/boot分区。
  • recovery镜像:由内核和ramdisk(另一个)组成,用来在启动失败或OTA升级时把系统加载到recovery模式下。通常被刷入/recovery分区。
  • /system镜像:存放的完整的android系统,包含谷歌提供的可执行文件和框架,还有厂商提供的类似定制化的东西。
  • /data镜像:存放的“默认出厂设置”的数据,它是/system分区中程序正常运行所必需的文件。恢复出厂设置,只需要将这个分区的镜像刷写回去就可以了。
    阅读全文 »

受保护的文件系统和伪文件系统

发表于 2019-07-21 更新于 2019-08-06 分类于 安卓架构大剖析 评论数:

本章节描述了android系统中常用的两种文件保护系统以及android中用到的linux伪文件系统。

OBB:Opaque Binary Blobs

谷歌商店里,会把apk限制在50MB以内,如果超过这个大小,则需要用户以OBB文件的形式提供最大不超过2GB的额外数据文件。

Opaque代表不透明,这表示OBB文件的数据格式由开发者决定(尽管在许多时候,它就是个vfat文件系统镜像),有volume守护进程来mount它。vold随后会调用linux内核中的device mapper。用loop(将文件系统中的镜像挂载为一个块设备,这里的loop是指循环挂载)参数进行mount操作。device mapper支持tow-fish算法,在发起OBB mount请求时,把秘钥传递给它。随后应用程序调用android.os.StorageManager中的mountOBB方法,来用指定的秘钥来mount OBB文件。这一过程如图所示(p75,图2-1)。

OBB虽然不透明,但是OBB文件中仍然有一些类型的元数据,这些数据能够被解析。解析OBB的代码位于/system/lib/libabdroidfw.so中。OBB的文件元数据位于文件尾部,解析该元数据需要倒着解析。文件元数据中各个字段的含义如图(p76,图2-2)。

android源码树中有一个obbtool工具,这个工具是一个脚本,用来创建OBB文件。该脚本的处理流程如下:

  1. 这个脚本首先会创建一个空的vfat镜像,然后使用device mapper以loop参数mount它。
  2. mount之后,往里面添加文件。
  3. umount之后,会把数据再次写入vfat镜像。

除此之外,sdk中也提供了一个能创建和维护obb文件的jobb工具;再android开发框架中也有一个ObbScanner类,它能够获取obb文件中的基本元数据。

Asec(android secure storage)

它提供了一种能安全的安装到设备上的机制,它让用户没法将一台设备的应用拷贝到另一台设备上(也称为预先锁定)。asec的创建和管理都是由vold完成,vold根据MountService发来的指令,执行相关的操作。asec创建和mount都需要密钥,在asec容器被mount的时候,vold会使用内核中device mapper,用loop参数执行mount操作,并通过DM_TABLE_LOAD ioctl操作,把密钥传递给内核中的dm-crypt模块。

密钥本身会以明文的形式保存在/data/misc/systemkeys/AppsOnSD.sks文件中。如果设备被root,则这个加密也会失去其木来的目的。

asec和obb这两种加密都是用了device mapper和它的文件加密功能(dm-crypt)创建和访问数据。asce实际上可以被视为obb的升级版,从加密存储应用的扩展文件,升级到加密存储的整个应用。

linux伪文件系统

伪文件系统所包含的文件都不会被存储在物理存储设备中,它们是直接由内核中的回调函数维护的。访问一个伪文件或目录时,某个对应的内核级处理函数就会被调用。因此这些伪文件的大小是没有意义的。

大多数伪文件都是以只读权限创建的,这些文件向用户态程序提供一种使之能够查看一些内核态中原本是不可访问的变量和结构体。少部分伪文件则是可写的,这些文件能够向用户态程序提供一种实时对内核中的数据施加影响的方法,就像注册表配置一样,且不用重启,但这些配置会因为系统的重启而丢失,在android系统中的init.rc脚本的重要功能就是保存这些配置。

FS 对应目录
cgroupfs androdi中只用来CPU计时和线程调度 -
debugfs 用于输出内核级别的调试信息,mount -t debugfs none /sys/kernel/debug /d
functionfs 由内核提供一个通用的文件系统,让驱动能够通过用户态的形式进行配置 /dev/usb-ffs/adb
procfs 提供一个机遇目录的观察系统中运行进程的方式 /proc
pstore 抓取内核崩溃的数据 /sys/fs/pstore
selinuxfs 存储了与安全策略相关的文件 /sys/fs/selinux
sysfs linux 2.6以后作为补充而引入,比/proc更井井有条 /sys

android文件系统中存储的内容

发表于 2019-07-21 更新于 2019-08-06 分类于 安卓架构大剖析 评论数:

本章描述了android的文件系统,以及root文件系统、/system分区、/data分区、/cache分区以及/vendor目录和SDk的细节。

root文件系统

android的root文件系统(/)是mount自ramdisk.img(initramfs)的文件。
每次启动时fastboot从boot分区中把这个镜像加载到内存中,并将其提交到内核进行管理。除非进行刷机(flashed)否则root文件系统将不会被更改。root系统中包含以后/init和与其相关的配置文件以及可执行文件,具体细节在下表:

目录或文件 注释
default.prop 编译时/build/core/main.mk中ADDITIONAL_DEFAULT_PROPERTIES变量的值,init根据它加载其他系统范围内的属性文件
file_contexts 记录SELinux中文件的context。用于限制非授权用户访问系统文件和目录
init 将会被内核以pid 1执行的二进制文件
init[…].rc /init的配置文件,主要是init.rc
property_contexts 记录SELinux中的系统属性的contexts
seapp_contexts 记录SELinux中的应用的context,限制应用的操作域
sepolicy SELinux策略设置编译后的结果
sbin/ 该目录中有abd、healthd以及recovery等可执行文件,即使不mount /system也能够使用
verity_key 含有认证/system所需要的DM_Verity RSA密钥

/system分区

该分区是存放谷歌或者其他厂商提供andorid组件的位置,该目录及其下所有文件都属于root:root,其权限为0755,但是该文件系统是以只读方式mount的;这样做的目的是提高文件系统的稳定性和安全性。理论上对于绝大多数设备来说,本分区下的所有目录以及文件都是一样的。实际上,一些厂商和运营商都会向/system添加自己的内容(android专门为厂商设计了/vendor目录来放置相关内容),具体细节在下表:

目录 注释
app 存放系统应用,有谷歌预绑定的app,也有厂商提供的app
bin 存放二进制可执行文件,有守护进程的,也有一些命令行shell使用的
build.prop 在编译时生成的属性文件,/init根据它在启动过程中去加载其他的属性文件
etc 存储了各种配置文件,它是/etc的符号链接
fonts 字体文件
framework 存放android的框架,各个framework都会存放在各自的jar文件中,每个framework的dex文件经过优化后,会被存放在与jar文件同名的odex文件中
lib 存放运行时的库文件
lost+found 在/system进行fsck操作时生成的目录,系统如果崩溃,该目录可能会含有不知道自己上一级目录是谁的inode
media 以ogg形式存放的各种音频、以及在系统启动时播放的动画
priv-app 该目录存放了特权应用
usr 支持文件,比如unicode的映射文件、设备和键盘的键盘布局文件
vendor 用于存放厂商提供的文件,实际上厂商都不会放这里
xbin 存放用途特殊的,正常操作过程中不需要的二进制可执行文件。在模拟器中,这个目录都是存放来自asop的/system/extras的各种工具。各种提取su权限的root程序也放在这里

/system/bin

本目录放置andorid使用的各种可执行文件(包含调试工具)。具体可以分为5类:

  • 用来提供服务的可执行文件,这类二进制文件都是在系统运行过程中有/init来进行调用的,他们调用的路径保存在/init使用的rc文件中。

    可执行文件 功能
    aoo_process 用户app的宿主进程,Zygote都是这个可执行文件的实例,它由Dalvik VM/ART初始化
    applypatch[_static] 在OTA升级过程中使用–根据脚本来应用补丁
    bootanimation 在图形界面子系统(surfacefliger)加载时,播放的启动动画
    clatd ipv4转ipv6的转换器
    dalvikvm 用于启动dalvik虚拟机的实例
    debuggerd 在系统崩溃时产生tombstone,也可以和一个远程gdb相连接
    drmserver DRM模块的宿主进程
    dnsmasq 伪dns服务,在设备提供热点服务时,提供dns代理服务
    hostpad wifi热点守护进程
    keystore android的密钥存储和管理服务
    linker android运行时的链接器
    mdnsd 组播dns的守护进程,用来使相邻的设备能够通过和wifi直连的方式相互发现和通信
    mediaserver 音视频录制以及播放
    mtpd 用来支持ppp/l2tp
    netd 用来管理网卡以及防火墙
    pppd 点对点协议守护进程,vpn需要使用
    racoon 提供对vpn的支持
    rlid 无线界面层守护进程
    sdcard sd卡守护进程,实现了sd卡文件系统,通过fuse模拟多用户权限设置
    sensorservice sensor hub,并发读取各种传感器
    servicemanager 提供了服务的注册以及查找功能
    surfaceflinger 画出图形界面的样子,并把他们加载到framebuffer中
    vold Volume守护进程,用来mount和unmount文件系统,也有文件系统的解密功能
    uncrypt 解密文件系统,在recovery前使用
    wpa_supplicant 无线访问适配层,提供wifi和wifi p2p的客户端支持
  • 调试工具,被归入这一类的事一些用于调试的原生二进制可执行文件(一般在模拟器中包含,厂商一般会在实机中删除)。

  • UNIX命令,unix相关的常用命令,和android特有的命令(getprop/setprop/watchprop),一般集成在/system/bin/toolbox中。

  • 调用Dalvik的脚本(upcall script),这些脚本通过shell与Dalvik进行交互(主要是调试),这些脚本都是从/sysytem/bin/app_process派生未来,用它们在/system/framework目录中的同名框架,加载Dalvik类(zygote本身就是app_process的一个实例),使用时,脚本会把用户传给它的参数直接传递给Dalvik类。我们只需要看Dalvik脚本中的am就可以了解这些脚本的结构了。下表描述了/system/bin中封装了app_process的脚本。

    脚本 用途
    am 与ActivityManager进行交互,启动activity发起的intent
    bmgr 备份管理接口
    bu 启动备份
    content 与android content provider交互的接口
    ime 控制输入法编辑器
    input 与InputManager进行交互,注入输入事件
    media 控制当前的媒体播放器
    monkey 用随机生成的输入时间运行一个APK
    pm 与包管理器进行交互,可以用来列出、安装、删除包以及列出权限
    requestsync 同步账号
    setting 获取、设置系统权限
    svc 控制电源、数据、wifi和usb类服务
    uiautomator 进行UI自动化测试,测试dump view之间的层次关系
    wm 与窗口管理器进行交互、修改显示尺寸和分辨率等
  • 厂商定制的二进制脚本文件,这类文件完全可以由厂商提供,通常为提供服务或者调试的工具。当然这些文件也可以按照aosp的约定放到/vendor中,但这也不是强制的。

/system/xbin

这个目录类似于unix中的/sbin,这里面含有管理员可能会觉得非常有用的二进制可执行文件,一般不提供给普通用户使用。这里命名为“x”而不是“s”是为了避免与android自身的/sbin(root文件系统中的一部分,含有系统操作时必须的二进制可执行文件)冲突。
这个目录中的可执行文件时从asop的/system/extras目录中编译得来的。这个目录会不会出现取决于厂商(有的厂商会吧这个目录删除)。下表中包含模拟器在该目录中包含的文件:

可执行文件 功能
add-property-tag 向系统的.prop文件中添加属性
check-lost+found 在fsck操作完成之后,检查lost+found目录
cpueater 让cpu跑到100%的死循环
cpustats 显示cpu和处理器调节器的统计信息
daemonize 把一个可执行文件作为守护进程启动,并关闭stdin/stdout/stderr
dexdump dex文件dump工具,也能dump文件头和字节码
directiotest 测试块设备io性能
kexecload 使用kexec系统调用,用一个新的内核重写内核镜像
ksminfo 显示ksm的使用信息
latencytop 以可读性更强的形式显示/proc/sys/kernel/latencytop中的数据
librank 给出各个共享内存区域所在各个进程中的使用情况
memtrack 跟踪进程的内存使用情况
micro_bench 内存基准测试工具
nc netcat,分析TCP和UDP的工具
netperf 客户端网络性能分析工具
netserver 服务端网络性能分析工具
procmem 显示进程内存使用情况统计信息
procrank 和librank互补,它可以逐个进程给出共享内存使用信息
rawbu 在底层备份/恢复 /data中的数据
sane_schedstat 以刻度形式展示scheduler信息
showmap 显示进程内存的分配情况
showslab 显示内核slab分配器的信息
SQLite3 命令行工具
strace 系统调用trace工具
su 切换用户
taskstats 提供liunx的taskstats接口的详细使用统计信息
tcpdump 抓包工具
timeinfo 输出时间的相关信息

在真实设备中,这些可执行文件作为调试工具特别有用,安装方法如下:

1
2
3
4
adb pull /system/xbin #把模拟器中的文件复制到主机中
#如果设备的/system文件可写
adb push .. /system/xbin #把主机中的文件复制到设备中
#除此之外,还有一些可执行文件的依赖库在/system/lib中可以找到

/system/lib[64]

该目录中包含/system/bin和/system/xbin中可执行文件所使用的共享库。在大多数的设备中,/system/lib中包含有多个字目录,如下:

目录 介绍
drm/ 提供drm引擎。比如一些包含专利的so,libfwdlockengine.so
egl/ android版本的opengles
hw/ HAL模块
ssl/engines 含有libkeystore.so,该库使得OpenSSL能够使用android的Keystore机能

在Intel设备中,本目录还包含有一个名为arm/的子目录,这个目录包含有arm架构编译的共享库的拷贝。这些库会被用在Intel二进制执行环境转换层(binary translation layer)Houdini上,用来为执行arm的可执行文件提供一个完整的运行时环境。

在android中几乎所有的二进制文件都是动态链接的。唯一例外的时/sbin目录中的二进制可执行文件。因为这些可执行文件会在/system被mount之前就会被用到。

/system/etc

这个目录和unix类似,里面都存放这配置文件。/etc也是这个目录的符号链接,这样做的目的时为了让ASOP项目以外的项目也能找到配置文件(unix的配置文件目录就是/etc)。

名称 描述
NOTICE.html.gz 法律文件
audio_effects.conf 被android的HAL层使用的文件
audio_polic.conf 被android的HAL层使用的文件
apns-conf.xml 被com.android.providers.telephony.TelephonyProvider使用
asound.conf 设备ALSA的配置文件
bluetooth 蓝牙配置文件
clatd.conf CLATd,实现ipv4 over ipv6的配置文件
event-log-tags 各个android组件的日志log,被android.util.EventLog使用
fallback_fonts.xml 列出了在加载system_fonts.xml中没有指定的font-family时所能选用的fallback font。它会被android中的layoutlib的FontLoader所使用
gps.conf gps配置文件
hosts 主机-IP表对应关系
media_codecs.xml 列出了StageFright所支持的所有codec
media_profiles.xml 列出了LibMedia所支持的所有profile
ppp/ 存放启动/停止vpn和ppp连接活动的可执行文件
permissions 存放了多个xml文件,每个文件规定了一个内置文件应用的权限,它会被PackageManager使用
security 这个目录存有被各种安全证书
system_fonts.xml 列出字体样式,并把字体样式和/system/fonts中的各个TTF文件一一对应起来
wifi WPA supplicant适配层的配置目录,用于控制wifi和wifi p2p的连接活动

在不同的设备制造商手里,本目录中可能还有一些其他文件。

/data分区

这个分区使用来存放用户个人数据的,将用户的个人数据单独存放起来有如下的好处:

  • 降低/data和android系统的耦合。系统在升级或者恢复的时候仅会对/system目录进行操作(擦除或重写),而不会对/data中的用户数据有其他的影响。系统在恢复出厂设置时则仅会格式化/data分区,而不会对系统有其他的影响。
  • 在用户需要时,/data能够被加密,而加密以及解密都会对系统运行造成延迟。而由于这种设计,/system中是没有敏感设备,加密用户的数据也就不会因加密而影响到系统,给系统带来相关的延迟。
  • /data可以设置为不可执行,这样极大的加大了恶意软件的攻击难度。因为,恶意软件在设置为不可以执行后,就没有了可写又可执行的分区。因为DEX和OAT是运行在虚拟机里面的,所有这对Dalvik和ART app不会有任何影响。root后的设备是需要remount该分区的。

/data分区是以nosuid的权限mount的,这使得root设备更加复杂了一些,因为su不能放在/data中运行而只能放在 /system这样的只读分区中。如果系统在对/system进行hash校验的话,则root就更加不容易执行了。
下表列出了存储在/data分区中的内容:

目录 注释
anr dumpstate用来记录失去响应的应用的函数调用栈当前状态的地方
app 用户自己安装的应用
app-asec 存放asec的容器,每个asec加密的应用都对应一个asec容器
app-lib 应用的JNI库都可以在这里被找到
app-private 提供应用的私有存储空间,目前被asec替代
backup 供备份服务使用
bugreports bugreport专用,用来存放bugreport生成的报告
davlvik-cache 用于存放优化过的系统应用和用户安装的应用的class.dex
data 各个已经安装的应用的目录,目录名为逆dns格式
dontpanic 未使用
drm 供android的数字版权管理器使用
local 供uid shell使用的一个临时目录,在adb中也可以使用
lost+found 对/data分区执行fsck操作时自动生成的目录
media 供sdcard服务吧sd卡mount到这个mount点上
mediadrm 供Media DRM服务使用
misc 各种其他文件的目录
nfc 存储NFC参数
property 存放持久性属性,文件名就是属性名
resource-cache 供AssetManager使用的资源缓存
security 一般为空
ssh 供那些ssh服务使用
system 存放了大量的系统配置文件
tombstone 存放由debuggerd生成的应用崩溃报告
user 与4.1版本开始引入。不同的用户会把各自的数据和应用安装在/data/user/用户号下的各个目录中,系统运行时,把/data/data下的对应目录做符号链接,使之指向/data/user/用户号下对应的目录。这样做可以使android系统支持多用户,/data/data会被直接指向/data/user/0

/data/data

这个目录是所有应用(系统应用、用户安装的应用)都存储其对应数据的地方。每一个应用都对应有一个目录,每个子目录权限为0751,而/data/data本身的权限为0771,system system,这意味着这个目录本身是可以执行的(cd命令),但却不能读出其包含的子目录。在应用中,每个应用对应的子目录也是这样的,能够执行却不能被非拥有者读取。

/data/data下各个目录的字目录都是其对应应用在整个文件系统中唯一能够执行写入的位置。

/data/misc

和它的名字相反,这个目录有一些重要的文件。如下表:

目录 内容
adb 存储可信的允许进行链接的电脑公钥
bluetooth BlueZ[<4.2]的配置文件
dhcp 存储实现dhcp的ctdent守护进程的PID文件
keychain 存放android内置证书pin码和黑名单
sensors 用于存储传感器调试数据
sms 存储短信codes数据库
systemkeys 用来存储asec容器的密钥
vpn 用来存储vpn状态配置文件
wifi 用于存储wifi子系统的配置文件和套接字

/data/system

这个目录中含有对维护设备状态非常重要的文件。这个目录的访问权限也是被限制为system system,如果设备没有root,则无法看到该目录下的各个文件。下表中列出了相关文件:

目录 注释
appops.xml 供控制应用权限的AppOps服务使用
batterystats.bin 供BatteryStats服务使用
called_pre_boots.dat 供ActivityManager记录每个Boot broadcast receiver
device_policies.xml DevicePolicyManagerService使用的配置文件
dropbox 供DropBox服务使用
entropy.dat 供EntropyMixer生成随机数使用
gesture.key 锁屏图案的hash
framework_atlas.config 供负责将预加载的bitmap组装成纹理贴图的AssetAtlasService使用
ifw/ Intent防火墙规则库
locksettings.db 锁屏设置,记录了设备的锁屏策略
netpolicy.xml 供NetworkPolicyManagerService使用的配置文件
netstats/ 用来记录NetworkStatsService按device、uid或xt得到的网络传输数据统计的目录
packages.list PackageManager列出的所有安装在系统中的包
packages.xml PackageManager列出的所有已安装包的元数据
password.key 锁屏PIN码/口令的hash
procstats/ 供ProcessStats服务存储文件的目录
registered_services/ 供android.content.pm.RegisteredServicesCache使用的目录
usagestats/ 供UsageStats服务存储文件,特别是usage-history.xml文件的目录
users/ android的多用户支持

/cache分区

android在系统升级的过程中使用本分区。系统的升级包会被下载到这里。设备在recovery/升级模式下启动时,会用到这个分区。
recovery这个可执行文件和系统在启动到recovery模式时,也会用到这个分区来交换信息。

/vendor分区

本分区是用来存储厂商对android系统的修改。这样做是避免在系统更新时厂商的修改也被抹除。一些专门的组件会在添加/system目录之前,回先去检查/vendor目录,具体会去检查哪些路径。

由于这个目录交由厂商处理,所以这些目录存储的内容在不同的设备上存在很大的区别。如/vendor/app目录是提供给厂商进行放置预装的应用,但实际上基本上没有人去用。

sd卡

大多数的sd卡都是exfat或者fat32文件系统,这两种系统都不支持权限管理。为了能实现权限管理、多用户配置,android通过FUSE(file user,用户态下的文件系统)模拟sd卡的方式来解决这个问题.FUSE使我们在用户态进程中实现文件系统。FUSE会在内核中安装一个用于支持通用文件系统的小模块,同过这个模块和VFS对接实现基本的注册文件系统功能,而真正的文件系统是在一个用户态进程(/system/bin/sdcard)中实现。在新版的android中,sd卡的mount点在/storage/ext_sd中,对于没有sd的设备,这个mount点通常会指向/data分区中的目录(/data/media/0)。

上面提到的一些目录都已经在android.os.Environment类中被定义为了常量。

android还提供了模拟sd卡文件系统,无论有没有sd卡都可以使用。通过mount命令,可以观察到sd卡文件系统。

ARTS Week 2

发表于 2019-07-17 更新于 2019-07-21 分类于 ARTS 评论数:

Week 2

阅读全文 »

ARTS是什么

发表于 2019-07-16 更新于 2019-07-21 分类于 ARTS 评论数:

Algorithm

主要是为了编程训练和学习。每周至少做一个 leetcode 的算法题(先从Easy开始,然后再Medium,最后才Hard)。进行编程训练,如果不训练你看再多的算法书,你依然不会做算法题,看完书后,你需要训练。关于做Leetcode的的优势,你可以看一下我在coolshell上的文章:LEETCODE 编程训练。

阅读全文 »

Mu Yi

什么都略懂一点,生活更多彩一些
7 日志
2 分类
7 标签
RSS
© 2019 Mu Yi
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Muse v7.3.0