Physical Address:
ChongQing,China.
WebSite:
文件系统是每一个系统相关的技术人员不得不接触的一类模块,其包含的知识点非常丰富,涉及的知识面也非常广,今天这篇文章将针对Android12.1系统的文件系统挂载流程做一个简要分析。
在了解文件系统前,需要先了解文件系统中的一些基础概念。
目录:目录用于组织文件和子目录的结构。通过目录我们可以在文件系统中创建相应的层次结构,用于我们快速索引到具体的文件或者对文件进行相应的组织。
分区:分区是物理存储设备(如硬盘)上的一个逻辑部分,每个分区可以用特定的文件系统格式进行格式化。不同的分区可以使用不同的文件系统格式。但一个分区只能有一个文件系统格式;而同一块儿硬盘可以有多个分区。
挂载点:挂载点是文件系统中的一个位置,一个目录。用于将一个文件系统与文件系统层次结构中的某个具体位置关联起来,也就是说挂载点其实是把分区关联到具体目录的。
文件系统格式:文件系统格式定义了数据在存储设备上的组织方式和存储结构。不同的文件系统格式具有不同的特性、优缺点和适用场景。常见的文件系统格式包括:FAT32,NTFS,EXT4,APFS等。
fstab:英文全称为File System Table,也就是文件系统表;类似于Kernel启动中所涉及到的dtb(设备树),fstab用于定义系统启动时挂载文件系统的方式和规则,描述了设备上文件系统的布局,指定了如何挂载不同分区或者存储设备,并设置了每个文件系统的挂载选项。
fstab的配置遵循一定的语法规则,具体可以参考如下示例:
#<dev> <mnt_point> <type> <mnt_flags options> <fs_mgr_flags>
system /system ext4 ro,barrier=1 wait,slotselect
vendor /vendor ext4 ro,barrier=1 wait,slotselect,avb=vbmeta
product /product ext4 ro,barrier=1 wait,slotselect,avb,logical
在Android2.1中,文件系统的挂载起始于init.rc中的设定,这里我们以一个init.rc为例:
on fs
mount_all /vendor/etc/fstab.${ro.boot.hardware.platform} --early
mkdir /mnt/vendor/persist/audio 0770 media audio
mkdir /mnt/vendor/persist/data 0700 system system
mkdir /mnt/vendor/persist/display 0770 system graphics
mkdir /mnt/vendor/persist/haptics 0770 system system
mkdir /mnt/vendor/persist/rfs 0770 root system
mkdir /mnt/vendor/persist/hlos_rfs 0770 root system
mkdir /mnt/vendor/persist/touch 0770 system system
这里我们看到,rc启动配置中显式地使用了mount_all这个command,紧跟着的是一个位于/vendor/etc路径下以fstab开头的文件,这里${ro.boot.hardware.platform}是用于替代变量的。这里的fstab开头的文件其实也就是安卓系统中所用的fstab配置。
我们已经知道,各个rc文件的解析是由init进程进行的,具体过程是怎样的呢,这里我们可以看一下相关源码:
#system/core/init/builtins.cpp
const BuiltinFunctionMap& GetBuiltinFunctionMap() {
.....
{"mount_all", {0, kMax, {false, do_mount_all}}},
{"mount", {3, kMax, {false, do_mount}}},
{"perform_apex_config", {0, 0, {false, do_perform_apex_config}}},
{"umount", {1, 1, {false, do_umount}}},
{"umount_all", {0, 1, {false, do_umount_all}}},
.....
}
这里其实就是通过GetBuiltinFunctionMap将rc配置文件中的mount_all命令对应到do_mount_all函数,从而完成rc配置文件的解析,进入到fstab的配置解析。
这里我们进入到do_mount_all函数,看看里面到底做了些什么:
/* <= Q: mount_all <fstab> [ <path> ]* [--<options>]*
* >= R: mount_all [ <fstab> ] [--<options>]*
*
* This function might request a reboot, in which case it will
* not return.
*/
static Result<void> do_mount_all(const BuiltinArguments& args) {
auto mount_all = ParseMountAll(args.args);
if (!mount_all.ok()) return mount_all.error();
const char* prop_post_fix = "default";
bool queue_event = true;
if (mount_all->mode == MOUNT_MODE_EARLY) {
prop_post_fix = "early";
queue_event = false;
} else if (mount_all->mode == MOUNT_MODE_LATE) {
prop_post_fix = "late";
}
std::string prop_name = "ro.boottime.init.mount_all."s + prop_post_fix;
android::base::Timer t;
Fstab fstab;
if (mount_all->fstab_path.empty()) {
if (!ReadDefaultFstab(&fstab)) {
return Error() << "Could not read default fstab";
}
} else {
if (!ReadFstabFromFile(mount_all->fstab_path, &fstab)) {
return Error() << "Could not read fstab";
}
}
auto mount_fstab_result = fs_mgr_mount_all(&fstab, mount_all->mode);
SetProperty(prop_name, std::to_string(t.duration().count()));
if (mount_all->import_rc) {
import_late(mount_all->rc_paths);
}
if (mount_fstab_result.userdata_mounted) {
// This call to fs_mgr_mount_all mounted userdata. Keep the result in
// order for userspace reboot to correctly remount userdata.
LOG(INFO) << "Userdata mounted using "
<< (mount_all->fstab_path.empty() ? "(default fstab)" : mount_all->fstab_path)
<< " result : " << mount_fstab_result.code;
initial_mount_fstab_return_code = mount_fstab_result.code;
}
if (queue_event) {
/* queue_fs_event will queue event based on mount_fstab return code
* and return processed return code*/
auto queue_fs_result = queue_fs_event(mount_fstab_result.code, false);
if (!queue_fs_result.ok()) {
return Error() << "queue_fs_event() failed: " << queue_fs_result.error();
}
}
return {};
}
在该函数内,会先通过ParseMountAll得到一个T类型为MountAllOptions的模板类对象,MountAllOptions为一个结构体,其具体的定义如下:
struct MountAllOptions {
std::vector<std::string> rc_paths;
std::string fstab_path; //fstab文件路径
mount_mode mode; //mount模式
bool import_rc;
};
我们沿着ParseMountAll的具体实现追溯调用链,可得:
utils.cpp/ParseMountAll->fs_mgr_fstab.cpp/ReadDefaultFstab or ReadFstabFromFile->fs_mgr.cpp/ fs_mgr_mount_all
如果我们在init.rc中使用mount_all命令加载fstab时没有指定具体的fstab文件路径,则会以通过fs_mgr_fstab.cpp中的ReadDefaultFstab函数读取默认应加载的fstab,如添加了fstab文件路径,则读取该fstab文件并解析,这里的工作将在ReadFstabFromFile中完成:
bool ReadFstabFromFile(const std::string& path, Fstab* fstab_out) {
auto fstab_file = std::unique_ptr<FILE, decltype(&fclose)>{fopen(path.c_str(), "re"), fclose};
if (!fstab_file) {
PERROR << __FUNCTION__ << "(): cannot open file: '" << path << "'";
return false;
}
bool is_proc_mounts = path == "/proc/mounts";
Fstab fstab;
if (!ReadFstabFile(fstab_file.get(), is_proc_mounts, &fstab)) {
LERROR << __FUNCTION__ << "(): failed to load fstab from : '" << path << "'";
return false;
}
....
}
继续追踪调用链,我们可以得到解析fstab的两个关键函数,分别是ParseMountFlags与ParseMgrFlags;所有的解析结果最终都会存储到一个名为FstabEntry的结构体内,该结构体定义如下:
struct FstabEntry {
std::string blk_device;
std::string logical_partition_name;
std::string mount_point;
std::string fs_type;
unsigned long flags = 0;
…..
}
这里我们可以结合具体的代码来看看fstab文件的解析过程:
FstabEntry entry;
//line参数通过getline 获取,delim指代具体的分隔符,这里统一使用” \t”,也就是空格
if (!(p = strtok_r(line, delim, &save_ptr))) {
LERROR << "Error parsing mount source";
goto err;
}
entry.blk_device = p; //获取对应的磁盘设备
if (!(p = strtok_r(NULL, delim, &save_ptr))) {
LERROR << "Error parsing mount_point";
goto err;
}
entry.mount_point = p; //获取挂载点信息
if (!(p = strtok_r(NULL, delim, &save_ptr))) {
LERROR << "Error parsing fs_type";
goto err;
}
entry.fs_type = p;// 获取文件类型
....
ParseMountFlags(p, &entry); //解析mount选项
ParseFsMgrFlags(p, &entry); //解析fs_mgr配置选项
我们再结合具体的fstab文件来看看:
system /system ext4 ro,barrier=1,discard,errors=continue wait,first_stage_mount,logical
system_ext /system_ext ext4 ro,barrier=1,discard wait,first_stage_mount,logical
vendor /vendor ext4 ro,barrier=1,discard wait,first_stage_mount,logical
product /product ext4 ro,barrier=1,discard wait,first_stage_mount,logical
vendor_dlkm /vendor_dlkm ext4 ro,noatime,errors=panic wait,first_stage_mount,logical
以该fstab文件中的条目1为例:这里entry.blk_device即为system,即指代system分区;entry.mount=/system,即挂载点为/system目录,且该分区的格式为ext4;ro,barrier=1,discard,errors=continue为mount选项,剩下的部分为fs_mgr的配置选项;
我们回到utils.cpp中的do_mount_all函数内,在解析完fstab配置之后,我们进入到fs_mgr_mount_all函数,该函数为执行mount动作的具体实现方。
相关代码如下:
// When multiple fstab records share the same mount_point, it will try to mount each
// one in turn, and ignore any duplicates after a first successful mount.
// Returns -1 on error, and FS_MGR_MNTALL_* otherwise.
MountAllResult fs_mgr_mount_all(Fstab* fstab, int mount_mode) {
int encryptable = FS_MGR_MNTALL_DEV_NOT_ENCRYPTABLE;
int error_count = 0;
CheckpointManager checkpoint_manager;
AvbUniquePtr avb_handle(nullptr);
bool wiped = false;
bool userdata_mounted = false;
if (fstab->empty()) {
return {FS_MGR_MNTALL_FAIL, userdata_mounted};
}
// Keep i int to prevent unsigned integer overflow from (i = top_idx - 1),
// where top_idx is 0. It will give SIGABRT
for (int i = 0; i < static_cast<int>(fstab->size()); i++) {
auto& current_entry = (*fstab)[i];
// If a filesystem should have been mounted in the first stage, we
// ignore it here. With one exception, if the filesystem is
// formattable, then it can only be formatted in the second stage,
// so we allow it to mount here.
if (current_entry.fs_mgr_flags.first_stage_mount &&
(!current_entry.fs_mgr_flags.formattable ||
IsMountPointMounted(current_entry.mount_point))) {
continue;
}
// Don't mount entries that are managed by vold or not for the mount mode.
if (current_entry.fs_mgr_flags.vold_managed || current_entry.fs_mgr_flags.recovery_only ||
((mount_mode == MOUNT_MODE_LATE) && !current_entry.fs_mgr_flags.late_mount) ||
((mount_mode == MOUNT_MODE_EARLY) && current_entry.fs_mgr_flags.late_mount)) {
continue;
}
....
}
涉及到具体mount时的行为时,通过for循环针对fstab配置条目进行挂载,涉及到重复的条目时,若第一次挂载成功则不再进行挂载,所有的文件系统挂载结果将存储至MountAllResult内进行返回。
具体的挂载行为涉及到挂载的时机,文件系统的类型,挂载点的不同,是否支持AVB,分区属性以及与Vold系统的交互等等,限于篇幅问题,不在此篇文章内进行深究。
安卓的文件系统是与分区是相绑定的,我们在编译安卓版本是会涉及到分区大小,分区的格式等等设定,这部分设定我们都可以在编译时通过Makefile进行配置:
如设定分区大小:
BOARD_USERDATAIMAGE_PARTITION_SIZE:设置usrdata大小
BOARD_CACHEIMAGE_PARTITION_SIZE:设置cache.img大小
BOARD_VENDORIMAGE_PARTITION_SIZE:设置vendor.img大小
BOARD_BOOTIMAGE_PARTITION_SIZE:设置boot.img大小
BOARD_DTBOIMG_PARTITION_SIZE:设置dtbo.img大小
BOARD_SUPER_PARTITION_SIZE:设置super.img大小
如设定分区对应的文件系统格式:
BOARD_BOOTIMAGE_FILE_SYSTEM_TYPE := ext4
BOARD_SUPERIMAGE_FILE_SYSTEM_TYPE := ext4