1 关键字

ext4, updater, mke2fs

2 问题描述

OH3.1的/data分区在重新格式化之后,分区大小发生了变化。

1.烧录后,使用df命令查看分区大小,/data分区大小为3682824

2.输入reboot updater回车,重启后进入updater,选择userdata reset,擦除用户数据

3.mount或再次重启后,/data分区大小显示为3819920

3 问题原因

3.1 正常机制

打包烧录与updater擦除用户数据后,/data分区大小不变化

3.2 异常机制

打包烧录与updater擦除用户数据的参数不同,造成重新分区后的分区大小不同。

4 解决方案

调整updater格式化时的参数

  1. 指定日志大小为16M,增加参数-J size=16

  2. 指定inode节点数量,增加参数-N 992000

updater的格式化命令修改为: /bin/mke2fs -F -J size=16 -N 992000-t ext4 -b 4096 /dev/block/platform/soc/10100000.himci.eMMC/by-name/userdata

格式化命令修改位置: base/startup/init_lite/interfaces/innerkits/fs_manager/fstab_mount.c 98行

5 定位过程

5.1.df显示分区大小的过程

5.1.1.源码位置

third_party\toybox\toys\posix\df.c

third_party\toybox\lib\portability.c

5.1.2.入口df_main()

  1. xgetmountlist(0),获取mtab_list链表

  2. 获取链表尾mtend

  3. 去除重复装载的分区(overmount)

  4. 打印分区信息show_mt()

5.1.3.xgetmountlist(0)

  1. 打开文件/proc/mounts

  2. setmntent():打开文件系统描述文件文件名,并返回可由getmntent()使用的文件指针。

  3. getmntent():getmntent()函数从流中读取文件系统描述文件的下一行,并返回指向包含文件中一行中断开字段的结构的指针。指针指向内存的静态区域,该区域将被随后对getmntent()的调用覆盖。

  4. 从struct mntent复制数据到struct mtab_list。

  5. 获取文件系统状态:

    stat(me->mnt_dir, &(mt->stat));

  6. 获取文件系统统计信息:

    statvfs(me->mnt_dir, &(mt->statvfs));

    ubuntu下,可使用man statvfs获取关于statvfs函数用法的更多说明。

    显示的分区大小来源于statvfs函数填充的结构mt->statvfs

  7. endmntent() :关闭与文件系统描述文件关联的流。

    ubuntu下,可使用man setmntent获取关于setmntent、getmntent、endmntent三个函数用法的更多说明。

  8. statvfs()源码位于:

    third_party\musl\src\stat\statvfs.c

    调用__statfs()函数;

    调用syscall(SYS_statfs64, fd, buf);

    SYS_statfs64=266

5.1.4.show_mt()

参数-a,打印所有分区

参数-h,显示为M,G可阅读的单位,1k=1024

参数-H,1k=1000

参数-i,显示单位为Inodes

5.1.5.syscall()和内核调用

kernel/linux/linux-5.10/arch/arm/tools/syscall.tbl

266 common  statfs64        sys_statfs64_wrapper

kernel/linux/linux-5.10/fs/statfs.c

//1
SYSCALL_DEFINE3(statfs64, const char __user *, pathname, size_t, sz, struct statfs64 __user *, buf);
//2
int user_statfs(const char __user *pathname, struct kstatfs *st);
//3
int vfs_statfs(const struct path *path, struct kstatfs *buf);
//4
static int statfs_by_dentry(struct dentry *dentry, struct kstatfs *buf);
//5
//进入到ext4的函数接口函数statfs中

kernel/linux/linux-5.10/fs/ext4/super.c

static int ext4_statfs(struct dentry *dentry, struct kstatfs *buf)
{
    struct super_block *sb = dentry->d_sb;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    sbi->s_overhead);
    struct ext4_super_block *es = sbi->s_es;
    ext4_fsblk_t overhead = 0, resv_blocks;
    u64 fsid;
    s64 bfree;
    resv_blocks = EXT4_C2B(sbi, atomic64_read(&sbi->s_resv_clusters));
​
    if (!test_opt(sb, MINIX_DF))
        overhead = sbi->s_overhead;
    //这里烧录后的overhead=66174,格式化后的overhead=31900
​
    buf->f_type = EXT4_SUPER_MAGIC;
    buf->f_bsize = sb->s_blocksize;
    buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);
    //f_blocks即为显示的分区大小
    //取决于两个值:ext4_blocks_count(es)和overhead
    //ext4_blocks_count(es)为blocks总数,来源于ext4的超级块,这里为986880
    
    //......略
    return 0;
}

ext4_blocks_count()源码:

#define ext4_read_incompat_64bit_val(es, name) \
    (((es)->s_feature_incompat & cpu_to_le32(EXT4_FEATURE_INCOMPAT_64BIT) \
        ? (ext4_fsblk_t)le32_to_cpu(es->name##_hi) << 32 : 0) | \
        le32_to_cpu(es->name##_lo))
​
static inline ext4_fsblk_t ext4_blocks_count(struct ext4_super_block *es)
{
    return ext4_read_incompat_64bit_val(es, s_blocks_count);
}
//blocks数量由高32位s_blocks_count_hi和低32位s_blocks_count_lo拼接而成:
//ext4_blocks_count(es) = es->s_blocks_count_hi << 32 | es->s_blocks_count_lo

分析和调试内核源码可以得出,updater重新格式化之后,overhead值的变化是造成显示值不同的直接原因。

5.2.获取分区大小的其他工具以及其显示与df是否不同

5.2.1./proc/partitions文件

这里显示/data分区的大小为3947520,这个值与df命令显示的值不一致,该值为这个分区的全部大小。

5.2.2.stat命令

stat /data -f

可以查看分区信息,但这里显示的分区格式为ext3,暂不清楚为何会显示为ext3。

显示的total blocks值与df命令保持一致。

stat命令的源码位于:third_party/toybox/toys/other/stat.c

5.2.3mount命令

或者/proc/mount文件

这个命令可以查看已挂载的分区信息,但不包含分区大小信息

5.2.4dd命令

使用dd命令,将分区的超级块数据保存成文件,使用十六进度查看工具查看超级块信息,推算分区容量

dd if=/dev/block/platform/soc/10100000.himci.eMMC/by-name/userdata of=/1.txt bs=1024 skip=1 count=4

使用od命令打印超级块信息:

# od 1.txt -t x                                                                
0000000    000f2300    000f0f00    0000c0c0    000dd0ea
0000020    000f1747    00000000    00000002    00000002
0000040    00008000    00008000    00007d00    0005374e
0000060    0005374e    ffff0003    0001ef53    00000001
0000100    628c7ab7    00000000    00000000    00000001
0000120    00000000    0000000b    00000100    0000003c
0000140    000102c6    0000046b    bbcb12b6    1944914c
0000160    987976a9    91c8b39a    7461642f    00000061
0000200    00000000    00000000    7461642f    00000061
0000220    00000000    00000000    00000000    00000000

根据ext4分区格式的规范,可以推算出:

s_inodes_count 0f2300=992000 s_blocks_count_lo 0f0f00=986880 s_log_block_size 00000002=2 2^(10+2)=4096

986880*4=3,947,520 这个值与/proc/partitions中的值一致

 

5.3.打包烧录和updater的操作的区别

5.3.1.打包命令

位于编译构建子系统,打包命令为:

mke2fs  -I 256 -O encrypt -L /data -M /data -t ext4 -b 4096 ../../../../out/hi3516dv300/packages/phone/images/userdata.img 128000
e2fsdroid -e -C ../../../../build/ohos/images/mkimage/dac.txt -f ../../../../out/hi3516dv300/packages/phone/data -a /data ../../../../out/hi3516dv300/packages/phone/images/userdata.img

源码位置:

build/ohos/images/mkimage/mkextimage.py 64行

5.3.2.格式化命令

位于启动子系统,格式化/data的命令为:

/bin/mke2fs -F -t ext4 -b 4096 /dev/block/platform/soc/10100000.himci.eMMC/by-name/userdata

源码位置:

base/startup/init_lite/interfaces/innerkits/fs_manager/fstab_mount.c 98行

5.3.3.工具程序

mke2fs和esfsdroid为第三方库程序,源码位置:

third_party\e2fsprogs\misc\mke2fs.c

third_party\e2fsprogs\contrib\android\e2fsdroid.c

编译之后的输出位置:

host:

out\hi3516dv300\clang_x64\distributeddatamgr\e2fsprogs

target:

out\hi3516dv300\distributeddatamgr\e2fsprogs

开发板上的位置:/system/bin

mke2fs的帮助信息如下:

pwx1145119@wuhphis00513:~/master$ mke2fs 
Usage: mke2fs [-c|-l filename] [-b block-size] [-C cluster-size]
	[-i bytes-per-inode] [-I inode-size] [-J journal-options]
	[-G flex-group-size] [-N number-of-inodes] [-d root-directory]
	[-m reserved-blocks-percentage] [-o creator-os]
	[-g blocks-per-group] [-L volume-label] [-M last-mounted-directory]
	[-O feature[,...]] [-r fs-revision] [-E extended-option[,...]]
	[-t fs-type] [-T usage-type ] [-U UUID] [-e errors_behavior][-z undo_file]
	[-jnqvDFSV] device [blocks-count]

5.3.4.不同之处

比较前后两次的操作可以发现:

1.打包命令分了两个命令构建img文件。

2.两次调用mke2fs程序的参数并不一致,这里是导致两次操作后显示大小不一致的关键。

结论:调整mke2fs的参数是解决问题的一种方案。

5.4.内核中的分区大小的计算

5.4.1内核编译命令

out/hi3516dv300目录下执行:

/usr/bin/env ../../kernel/linux/build/build_kernel.sh ../../kernel/linux/build /cloud/pwx1145119/master/out/KERNEL_OBJ /cloud/pwx1145119/master/out/hi3516dv300/packages/phone/images standard arm vendor/hisilicon/Hi3516DV300 hi3516dv300 linux-5.10

5.4.2计算overhead过程

前面(5.1.5)已经介绍了分区大小的值取决于blocks总数和overhead的值。其中,分区的总blocks数量是固定的,格式化参数对overhead的值造成影响。

以下是overhead的计算过程。

内核挂载文件系统时,计算overhead值:

//1.kernel/linux/linux-5.10/fs/ext4/super.c
//ext4格式分区挂载接口函数
static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data);
{
	return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
}

//2.kernel/linux/linux-5.10/fs/ext4/super.c
//填充超级块数据结构sb
static int ext4_fill_super(struct super_block *sb, void *data, int silent)
{
    //...略
    /*
	 * Get the # of file system overhead blocks from the
	 * superblock if present.
	 */
	if (es->s_overhead_clusters)
		sbi->s_overhead = le32_to_cpu(es->s_overhead_clusters);
	else {
        //计算overhead
		err = ext4_calculate_overhead(sb);
		if (err)
			goto failed_mount_wq;
	}
    //...略
}

//3.计算overhead
/*
 * Compute the overhead and stash it in sbi->s_overhead
 */
int ext4_calculate_overhead(struct super_block *sb)
{
	struct ext4_sb_info *sbi = EXT4_SB(sb);
	struct ext4_super_block *es = sbi->s_es;
	struct inode *j_inode;
	unsigned int j_blocks, j_inum = le32_to_cpu(es->s_journal_inum);
	ext4_group_t i, ngroups = ext4_get_groups_count(sb);
	ext4_fsblk_t overhead = 0;
	char *buf = (char *) get_zeroed_page(GFP_NOFS);
	if (!buf)
		return -ENOMEM;

	/*
	 * Compute the overhead (FS structures).  This is constant
	 * for a given filesystem unless the number of block groups
	 * changes so we cache the previous value until it does.
	 */

	/*
	 * All of the blocks before first_data_block are overhead
	 */
    //first_data_block之前的所有块都是开销
	overhead = EXT4_B2C(sbi, le32_to_cpu(es->s_first_data_block));

	/*
	 * Add the overhead found in each block group
	 */
    //将在每个块组中找到的开销相加
	for (i = 0; i < ngroups; i++) {
		int blks;

		blks = count_overhead(sb, i, buf);
		overhead += blks;
        //累加overhead值
		if (blks)
			memset(buf, 0, PAGE_SIZE);
		cond_resched();
	}

	/*
	 * Add the internal journal blocks whether the journal has been
	 * loaded or not
	 */
    //添加内部日记块
	if (sbi->s_journal && !sbi->s_journal_bdev) {
		overhead += EXT4_NUM_B2C(sbi, sbi->s_journal->j_total_len);
        //累加overhead值
	}else if (ext4_has_feature_journal(sb) && !sbi->s_journal && j_inum) {
		/* j_inum for internal journal is non-zero */
		j_inode = ext4_get_journal_inode(sb, j_inum);
		if (j_inode) {
			j_blocks = j_inode->i_size >> sb->s_blocksize_bits;
			overhead += EXT4_NUM_B2C(sbi, j_blocks);
        //累加overhead值,代码并未运行到此处
			iput(j_inode);
		} else {
			ext4_msg(sb, KERN_ERR, "can't get journal size");
		}
	}
	sbi->s_overhead = overhead;
	smp_wmb();
	free_page((unsigned long) buf);
	return 0;

5.4.3计算每一个group的count_overhead

static int count_overhead(struct super_block *sb, ext4_group_t grp,
			  char *buf)
{
	struct ext4_sb_info	*sbi = EXT4_SB(sb);
	struct ext4_group_desc	*gdp;
	ext4_fsblk_t		first_block, last_block, b;
	ext4_group_t		i, ngroups = ext4_get_groups_count(sb);
	int			s, j, count = 0;

	if (!ext4_has_feature_bigalloc(sb)) {
        //调试发现函数从这里返回sbi->s_itb_per_group值为关键值
		return (ext4_bg_has_super(sb, grp) + ext4_bg_num_gdb(sb, grp) +
			sbi->s_itb_per_group + 2);
	}
    //......以下略
}

5.4.4s_itb_per_group计算过程

static int ext4_fill_super(struct super_block *sb, void *data, int silent)
{
    //......略
	sbi->s_blocks_per_group = le32_to_cpu(es->s_blocks_per_group);
	sbi->s_inodes_per_group = le32_to_cpu(es->s_inodes_per_group);
    //s_inodes_per_block = 4096 / 256 = 16
    sbi->s_inodes_per_block = blocksize / EXT4_INODE_SIZE(sb);
    //......
    
    sbi->s_itb_per_group = sbi->s_inodes_per_group /
					sbi->s_inodes_per_block;    
    //......略
}
//分析:
//s_itb_per_group值取决于s_inodes_per_group和s_inodes_per_block
//s_inodes_per_group来源于ext4超级块s_inodes_per_group,这里为32000
//group的个数为31,inode总数为992000,所以s_inodes_per_group = 992000 / 31 =32000
//s_inodes_per_block = 4096 / 256 = 16
//s_itb_per_group = 32000 / 16 = 2000

5.4.5计算日志的overhead

/* Translate # of blks to # of clusters */
#define EXT4_NUM_B2C(sbi, blks)	(((blks) + (sbi)->s_cluster_ratio - 1) >> \
				 (sbi)->s_cluster_bits)
				 
overhead += EXT4_NUM_B2C(sbi, sbi->s_journal->j_total_len);

5.4.6.小结

overhead的值取决于以下几个方面:

s_inodes_per_group 每个组的inode节点数量

blocksize 块大小,一般为4k

inodesize 节点大小,一般为256

j_total_len 日志大小

6 知识分享

该问题涉及到OpenHarmony众多模块(升级子系统、编译构建子系统、启动子系统、三方库musl、linux内核、三方库e2fsprogs)。

涉及到的知识点也较多,以下是本文涉及到的部分参考资料:

内核下载地址

Ext4 Disk Layout

文件系统Ext4详解 - 星空·77 - 博客园 (cnblogs.com)

Linux 下的dd命令使用详解(摘录) - 韦能的博客 (huawei.com)

Linux内核学习:EXT4 INode在磁盘上的读写雪原学长的博客-CSDN博客ext4 inode

Linux 磁盘和文件系统管理 | SJZ's blog (songjinze.github.io)

Linux内核浅析-文件系统 - 知乎 (zhihu.com)

ARM32 架构增加一个系统调用 (biscuitos.github.io)

syscall 中断号 - Y4ng - 博客园 (cnblogs.com)

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐