ext4文件系统

ext4

数据结构与空间布局

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

inode和superblock linux内核中有额外的抽象的一个类,用于抽象表示文件系统和文件的属性,和具体的文件系统无关。有的文件系统没有超级块和inode的概念但是也需要在linux的内存中提供这些抽象,具体的如ext4的inode会额外定义为ext4_inode这样的结构

超级块

super_block

linux中每个文件系统都有的一个结构体,只位于内存中,是抽象结构,不区分文件系统类型,但有一个成员会指向实际的文件类型sb

    struct super_block {
        void			*s_fs_info;	/* Filesystem private info */
    } __randomize_layout;

ext4_super_block

ext4_super_block 参考 ext4 Super Block 定义 是硬盘中的ext4超级块的完整定义。

/*
 * Structure of the super block
 */
struct ext4_super_block {
	__le32	s_inodes_count;		/* Inodes count */
	__le32	s_blocks_count_lo;	/* Blocks count */
	
	__le32	s_log_block_size;	/* Block size */
	
	__le32	s_blocks_per_group;	/* # Blocks per group 每个组有多少个块 根据块的总数计算有多少个组 */

ext4文件系统的块的大小 = 1024 * (2 ^ s_log_block_size),是1024的指数倍

#define BLOCK_SIZE_BITS 10
#define BLOCK_SIZE (1<<BLOCK_SIZE_BITS)

blocksize = BLOCK_SIZE << le32_to_cpu(sbi->s_es->s_log_block_size);

ext4_sb_info

上面的ext4_super_block都是硬盘中实际存储的内容,并不方便在内存中直接使用,有的要做大小端转化,有的要高位地位合并,所以下面的信息就是把常用的信息处理好,方便直接使用

/*
 * fourth extended-fs super-block data in memory
 */
struct ext4_sb_info {
	unsigned long s_desc_size;	/* Size of a group descriptor in bytes */
	...
	struct ext4_super_block *s_es;	/* Pointer to the super block in the buffer 指向超级块的物理结构定义 */

从内存中抽象的super_block获取ext4的实际超级块信息

static inline struct ext4_sb_info *EXT4_SB(struct super_block *sb)
{
	return sb->s_fs_info;
}

group

ext4文件系统要记录inode和数据,最开始设计的时候是考虑到把内容分散到磁盘的各个位置,考虑到磁盘的磨损均衡,但在ssd时代已经没什么用了。。。

ext4_group_desc

物理结构

/*
 * Structure of a blocks group descriptor
 */
struct ext4_group_desc
{
	__le32	bg_block_bitmap_lo;	/* Blocks bitmap block */
	__le32	bg_inode_bitmap_lo;	/* Inodes bitmap block */
	__le32	bg_inode_table_lo;	/* Inodes table block */
  • inode_bitmap

    • 组中的inode_bitmap值记录的是bitmap的位置所在的块
    • bitmap中是以二进制的1和0区分是否inode被占用,如一个块有4096字节,就表示4096 * 8个inode是否已存在
  • block_bitmap

    • 和上面的inode_bitmap一样
    • 描述组中可以管理的块,哪些是可用,哪些是不可用的
  • inode_table

    • 组中记录的inode_table值是table在硬盘的块的起点
    • 一个inode的大小记录在超级块中,通常是256字节,那么会预留一个组能管理的inode数量 * 256字节的区域用以存储这些inode,这个表格中哪里有inode哪里的inode是无效的在inode_bitmap中表示

上面的三个结构是在组创建时候就确定的,一个文件系统能记录多少个inode也是确定的了。这个空间是预留的。

每个group也会标记一定的保留块,这些块可能用来备份超级块,虽然这些保留快通常都比较多,不清楚是否还有其他作用

ext4_group_info

内存中的group信息,和上面的ext4_sb_info作用类似

struct ext4_group_info {
	unsigned long   bb_state;
    ......
};

会在ext4挂载时候收集组信息到内存中

ext4_mount
ext4_fill_super
ext4_mb_init
ext4_mb_init_backend
ext4_mb_add_groupinfo

弹性块组参考:

inode

在ext4中,文件夹是特殊的文件,那么,每个inode就表示一个文件(当然也可以是特殊文件–文件夹)

inode是文件的元属性,比如文件的属主,创建时间,类型,创建一个文件时候即使没有数据,也会有一个inode被记录

ext4_inode

硬盘中的inode数据结构

/*
 * Structure of an inode on the disk
 */
struct ext4_inode  {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */

buffer_head

bh 是内核中使用的内存映射结构体,和ext4无关,但阅读代码或后面代码内容这个经常用到

buffer_head 是块io层提供的块和文件映射

参考 Linux中page、buffer_head、bio的联系

ext4_mount

ext4文件系统里做的主要是查找ext4超级块位置 初始化内核超级块结构体 之后对于文件系统的所有操作都是通过超级块结构体里的函数指针完成

#0  ext4_fill_super (sb=0xffff888007a4c800, data=0x0 <fixed_percpu_data>, silent=0) at fs/ext4/super.c:4015
#1  0xffffffff813444d4 in mount_bdev (fs_type=<optimized out>, flags=<optimized out>, dev_name=<optimized out>, data=0x0 <fixed_percpu_data>, fill_super=0xffffffff8144c0c0 <ext4_fill_super>) at fs/super.c:1429
#2  0xffffffff81382747 in legacy_get_tree (fc=0xffff888006fdacc0) at fs/fs_context.c:593
#3  0xffffffff81342b55 in vfs_get_tree (fc=fc@entry=0xffff888006fdacc0) at fs/super.c:1559
#4  0xffffffff8136a4d5 in do_new_mount (path=0xffffc9000024bef8, fstype=<optimized out>, sb_flags=<optimized out>, mnt_flags=32, name=0xffff888006d75960 "/dev/loop0", data=0x0 <fixed_percpu_data>) at fs/namespace.c:2899
#5  0xffffffff8136b81c in do_mount (data_page=0x0 <fixed_percpu_data>, flags=0, type_page=0xffff8880297962c0 "ext4", dir_name=0x55b82cf5abe0 "/root/ext4", dev_name=0xffff888006d75960 "/dev/loop0") at fs/namespace.c:3242
#6  __do_sys_mount (data=<optimized out>, flags=0, type=<optimized out>, dir_name=0x55b82cf5abe0 "/root/ext4", dev_name=<optimized out>) at fs/namespace.c:3450
#7  __se_sys_mount (data=<optimized out>, flags=0, type=<optimized out>, dir_name=94249516641248, dev_name=<optimized out>) at fs/namespace.c:3427
#8  __x64_sys_mount (regs=<optimized out>) at fs/namespace.c:3427
#9  0xffffffff81b250b3 in do_syscall_64 (nr=<optimized out>, regs=0xffffc9000024bf58) at arch/x86/entry/common.c:46
#10 0xffffffff81c000da in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:125
    #define EXT4_MIN_BLOCK_SIZE		1024

    static int ext4_fill_super(struct super_block *sb, void *data, int silent) {
    	struct ext4_sb_info *sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);

        ext4_fsblk_t sb_block = get_sb_block(&data);
        blocksize = sb_min_blocksize(sb, EXT4_MIN_BLOCK_SIZE); 
        
        /*
    	 * The ext4 superblock will not be buffer aligned for other than 1kB
    	 * block sizes.  We need to calculate the offset from buffer start.
    	 */
    	if (blocksize != EXT4_MIN_BLOCK_SIZE) {
    		logical_sb_block = sb_block * EXT4_MIN_BLOCK_SIZE;
    		offset = do_div(logical_sb_block, blocksize);
    	} else {
    		logical_sb_block = sb_block;
    	}

    	/** 向上几行 找到超级块的块位置 */
        
        bh = ext4_sb_bread_unmovable(sb, logical_sb_block);     // bh 读取超级块

        /** 超级块 */
    	es = (struct ext4_super_block *) (bh->b_data + offset);
    }

找到超级块位置

blocksize 是在没有超级块的情况下,先要找到超级块位置,先获取一个默认的文件块大小

解析参数 获取超级块的index
data通常为NULL,很多文件系统挂载时都设定data=NULL,data可能来自于挂载时的可选参数,比如这里可以指定sb超级块的index

static ext4_fsblk_t get_sb_block(void **data)
{
	ext4_fsblk_t	sb_block;
	char		*options = (char *) *data;

	if (!options || strncmp(options, "sb=", 3) != 0)
		return 1;	/* Default location */

sb_min_blocksize获取设备的逻辑块大小,ext4的最小块大小为1024B,如果设备的块小于ext4的最小块则默认为ext4的最块
当前创建的设备逻辑块为512B,所以默认为了1024B,sb->s_blocksize = 1024

    int sb_set_blocksize(struct super_block *sb, int size)
    {
    	if (set_blocksize(sb->s_bdev, size))
    		return 0;
    	/* If we get here, we know size is power of two
    	 * and it's value is between 512 and PAGE_SIZE */
    	sb->s_blocksize = size;
    	sb->s_blocksize_bits = blksize_bits(size);
    	return sb->s_blocksize;
    }

    EXPORT_SYMBOL(sb_set_blocksize);

    int sb_min_blocksize(struct super_block *sb, int size)
    {
    	int minsize = bdev_logical_block_size(sb->s_bdev);
    	if (size < minsize)
    		size = minsize;
    	return sb_set_blocksize(sb, size);
    }

ref: https://www.kernel.org/doc/html/v4.19/filesystems/ext4/ondisk/index.html#layout

For the special case of block group 0, the first 1024 bytes are unused, to allow for the installation of x86 boot sectors and other oddities.
The superblock will start at offset 1024 bytes, whichever block that happens to be (usually 0).
However, if for some reason the block size = 1024, then block 0 is marked in use and the superblock goes in block 1.
For all other block groups, there is no padding.

对于块组 0 的特殊情况,前 1024 个字节未使用,以允许安装 x86 引导扇区和其他奇怪的东西。
超级块将从偏移量 1024 字节开始,无论是哪个块(通常是 0)。
但是,如果由于某种原因块大小 = 1024,则块 0 被标记为正在使用,并且超级块位于块 1 中。
对于所有其他块组,没有填充。

ln -s 创建软连接

#0  ext4_symlink (dir=0xffff888004f9bcf8, dentry=0xffff88800804a240, symname=0xffff888029647020 "a") at fs/ext4/namei.c:3368
#1  0xffffffff813504c1 in vfs_symlink (oldname=0xffff888029647020 "a", dentry=0xffff88800804a240, dir=0xffff888004f9bcf8) at fs/namei.c:4021
#2  vfs_symlink (dir=0xffff888004f9bcf8, dentry=0xffff88800804a240, oldname=0xffff888029647020 "a") at fs/namei.c:4007
#3  0xffffffff813533b4 in do_symlinkat (oldname=<optimized out>, newdfd=-100, newname=0x7ffc6ead2e91 "e") at fs/namei.c:4048
#4  0xffffffff81b1cf23 in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000217f58) at arch/x86/entry/common.c:46
#5  0xffffffff81c000da in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:125

软连接文件类型,文件内容为指向文件的路径字符串,路径可以是绝对路径,也可以是相对路径

static int ext4_symlink(struct inode *dir,
			struct dentry *dentry, const char *symname)
{
    err = fscrypt_prepare_symlink(dir, symname, len, dir->i_sb->s_blocksize,
				      &disk_link);                              // 连接指向的路径存储到disk_link结构体中
				      
    inode = ext4_new_inode_start_handle(dir, S_IFLNK|S_IRWXUGO, // 创建软连接inode inode的类型为软连接
					    &dentry->d_name, 0, NULL,
					    EXT4_HT_DIR, credits);

    if ((disk_link.len > EXT4_N_BLOCKS * 4)) {                  // 如果软连接>60 记录到文件的Extents中
		__page_symlink(inode, disk_link.name, disk_link.len, 1);
    } else {
        /* clear the extent format for fast symlink */
		ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
		if (!IS_ENCRYPTED(inode)) {
			inode->i_link = (char *)&EXT4_I(inode)->i_data;
		}
		memcpy((char *)&EXT4_I(inode)->i_data, disk_link.name,  // 软连接指向的路径 实际存储在硬盘的inode的block区域
		       disk_link.len);
		inode->i_size = disk_link.len - 1;
    }
}

读取文件夹

先看一下ls /etc的系统调用,使用getdents64系统调用获取文件夹内文件

    # strace ls /etc
    openat(AT_FDCWD, "/etc", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
    getdents64(3, 0x5570e9f71760 /* 201 entries */, 32768) = 6472
    #0  htree_dirblock_to_tree (dir_file=dir_file@entry=0xffff888007a3df00, dir=dir@entry=0xffff888004cb6ae8, block=block@entry=0, hinfo=hinfo@entry=0xffffc90000263d50, start_hash=start_hash@entry=0, start_minor_hash=start_minor_hash@entry=0) at fs/ext4/namei.c:1075
    #1  0xffffffff81424ec5 in ext4_htree_fill_tree (dir_file=dir_file@entry=0xffff888007a3df00, start_hash=0, start_minor_hash=0, next_hash=next_hash@entry=0xffff888006e3aa28) at fs/ext4/namei.c:1160
    #2  0xffffffff813eda1c in ext4_dx_readdir (file=file@entry=0xffff888007a3df00, ctx=ctx@entry=0xffffc90000263ee0) at fs/ext4/dir.c:581
    #3  0xffffffff813ede04 in ext4_readdir (file=0xffff888007a3df00, ctx=0xffffc90000263ee0) at fs/ext4/dir.c:128
    #4  0xffffffff81357bf7 in iterate_dir (file=file@entry=0xffff888007a3df00, ctx=ctx@entry=0xffffc90000263ee0) at fs/readdir.c:65
    #5  0xffffffff813588c4 in __do_sys_getdents64 (count=32768, dirent=<optimized out>, fd=<optimized out>) at fs/readdir.c:369

最终在下面这个函数里实现的遍历文件夹,文件夹类型的文件每个块内,都是n个ext4_dir_entry_2这样的结构体,只需要用这个指针遍历整个块即可。

rec_len属性标记一个条目的长度,这个长度可能超过结构体+name实际长度,甚至是远超,可能是文件夹内删除条目时调整,也可能是占位,因为文件夹类型的文件总是和文件系统的块大小整数倍的,如果文件夹内只有一个条目(即使不包含...),也会直接占用一整个块的大小。

    struct ext4_dir_entry_2 {
    	__le32	inode;			/* Inode number */
    	__le16	rec_len;		/* Directory entry length */
    	__u8	name_len;		/* Name length */
    	__u8	file_type;		/* See file type macros EXT4_FT_* below */
    	char	name[EXT4_NAME_LEN];	/* File name */
    };
    # block 块的id,文件夹类型的文件,里面的条目不会跨块

    static int htree_dirblock_to_tree(struct file *dir_file,
    				  struct inode *dir, ext4_lblk_t block,
    				  struct dx_hash_info *hinfo,
    				  __u32 start_hash, __u32 start_minor_hash)
    {
    	struct buffer_head *bh;
    	struct ext4_dir_entry_2 *de, *top;

    	bh = ext4_read_dirblock(dir, block, DIRENT_HTREE);

    	de = (struct ext4_dir_entry_2 *) bh->b_data;                                # 初始时候,de指向块的开头
    	top = (struct ext4_dir_entry_2 *) ((char *) de +
    					   dir->i_sb->s_blocksize -
    					   EXT4_DIR_REC_LEN(0));

    	for (; de < top; de = ext4_next_entry(de, dir->i_sb->s_blocksize)) {        # 使用de指针遍历整个块
    		if (ext4_check_dir_entry(dir, NULL, de, bh,
    				bh->b_data, bh->b_size,
    				(block<<EXT4_BLOCK_SIZE_BITS(dir->i_sb))
    					 + ((char *)de - bh->b_data))) {
    			/* silently ignore the rest of the block */
    			break;
    		}

文件夹类型有两种形式,一种是上面的列表类型,一种是hash类型,如果inode->flag & EXT4_INDEX_FL表示使用hash类型的文件索引,bug,linux5.10是不支持该类型的。。。

在文件夹内新增文件时,如果是根目录,会尽量将文件放置在负载最轻的块组中,如果是普通文件夹会尽量放置在同一个块组中

getxattr

文件拓展属性,比如设置可执行文件的权能,文件的selinux标签

# setcap cap_dac_override=ep a
# getcap a
a cap_dac_override=ep

# strace getcap a
getxattr("a", "security.capability", "\1\0\0\2\2\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 24) = 20
    #0  ext4_xattr_get (inode=0xffff8880080361b8, name_index=6, name=0xffffffff82355a20 "capability", buffer=0xffff888006e96360, buffer_size=20) at fs/ext4/xattr.c:661
    #1  0xffffffff8136ffd3 in vfs_getxattr_alloc (dentry=dentry@entry=0xffff888008048c00, name=<optimized out>, name@entry=0xffffffff82355a17 "security.capability", xattr_value=xattr_value@entry=0xffffc90000217ce0, xattr_size=xattr_size@entry=24, flags=flags@entry=3136) at fs/xattr.c:359
    #2  0xffffffff8148fd4f in cap_inode_getsecurity (inode=0xffff8880080361b8, name=<optimized out>, buffer=0xffffc90000217d60, alloc=<optimized out>) at security/commoncap.c:390
    #3  0xffffffff81493fc9 in security_inode_getsecurity (inode=inode@entry=0xffff8880080361b8, name=name@entry=0xffffc90000217db9 "capability", buffer=buffer@entry=0xffffc90000217d60, alloc=alloc@entry=true) at security/security.c:1387
    #4  0xffffffff8136f00e in xattr_getsecurity (size=24, value=0xffff888006e96460, name=0xffffc90000217db9 "capability", inode=0xffff8880080361b8) at fs/xattr.c:308
    #5  vfs_getxattr (dentry=dentry@entry=0xffff888008048c00, name=name@entry=0xffffc90000217db0 "security.capability", value=value@entry=0xffff888006e96460, size=size@entry=24) at fs/xattr.c:396
    #6  0xffffffff8136f17e in getxattr (d=0xffff888008048c00, name=name@entry=0x7f4c24862374 "security.capability", value=value@entry=0x7ffcb4afb850, size=size@entry=24) at fs/xattr.c:635
    #7  0xffffffff8136f366 in path_getxattr (pathname=0x7ffcb4afce87 "x", name=0x7f4c24862374 "security.capability", value=0x7ffcb4afb850, size=24, lookup_flags=1) at fs/xattr.c:663
    #8  0xffffffff81b1cf23 in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000217f58) at arch/x86/entry/common.c:46

在函数ext4_xattr_get中,首先会从inode中取xattr,如果没有再从inode的块中计算

    int
    ext4_xattr_get(struct inode *inode, int name_index, const char *name,
    	       void *buffer, size_t buffer_size)
    {
    	int error;

    	error = ext4_xattr_ibody_get(inode, name_index, name, buffer,
    				     buffer_size);
    	if (error == -ENODATA)
    		error = ext4_xattr_block_get(inode, name_index, name, buffer,
    					     buffer_size);

从inode中取xattr

    int ext4_xattr_ibody_get(struct inode *inode, int name_index, const char *name,
    		     void *buffer, size_t buffer_size)
    {
    	struct ext4_xattr_ibody_header *header;
    	struct ext4_xattr_entry *entry;
    	struct ext4_inode *raw_inode;
    	struct ext4_iloc iloc;
    	void *end;

    	if (!ext4_test_inode_state(inode, EXT4_STATE_XATTR))    // 检查 inode->flags 是否有EXTENTS属性
    		return -ENODATA;
    	error = ext4_get_inode_loc(inode, &iloc);
    	if (error)
    		return error;
    	raw_inode = ext4_raw_inode(&iloc);			            // raw_inode inode的硬盘中的结构
    	header = IHDR(inode, raw_inode);                        // header = inode + 128 + inode->i_extra_isize
    	end = (void *)raw_inode + EXT4_SB(inode->i_sb)->s_inode_size;
    	entry = IFIRST(header);                                 // entry = header + 4 这里的entry就是第一个xattr属性
    	error = xattr_find_entry(inode, &entry, end, name_index, name, 0);

                u16 offset = le16_to_cpu(entry->e_value_offs);  // 计算value的偏移量,name无需偏移量
    			void *p = (void *)IFIRST(header) + offset;      // 在inode中,value的偏移量基于第一个xattr节点位置

    			memcpy(buffer, p, size);

硬盘中的属性结构

    struct ext4_xattr_entry {
    	__u8	e_name_len;	/* length of name */
    	__u8	e_name_index;	/* attribute name index */
    	__le16	e_value_offs;	/* offset in disk block of value */
    	__le32	e_value_inum;	/* inode in which the value is stored */
    	__le32	e_value_size;	/* size of attribute value */
    	__le32	e_hash;		/* hash value of name and value */
    	char	e_name[];	/* attribute name */
    };

block中的拓展属性

    static int
    ext4_xattr_block_get(struct inode *inode, int name_index, const char *name,
    		     void *buffer, size_t buffer_size)
    {
    	struct buffer_head *bh = NULL;
    	struct ext4_xattr_entry *entry;
    	size_t size;
    	void *end;
    	int error;

    	if (!EXT4_I(inode)->i_file_acl)                         // 拓展属性的块在inode->i_file_acl
    		return -ENODATA;
    	
    	bh = ext4_sb_bread(inode->i_sb, EXT4_I(inode)->i_file_acl, REQ_PRIO);   // 读取i_file_acl块
    	
    	entry = BFIRST(bh);                                     // = 块 + 0x20偏移量
    	end = bh->b_data + bh->b_size;
    	error = xattr_find_entry(inode, &entry, end, name_index, name, 1);

    			u16 offset = le16_to_cpu(entry->e_value_offs);
    			void *p = bh->b_data + offset;                  // 块中查找属性时,value的偏移量是基于整个块的,和inode中不同

    			memcpy(buffer, p, size);

日志

ext3相比于ext2最重要的特性就是新增了日志功能

什么是日志?就是硬盘的操作会被记录下来,通过日志可以重放对硬盘的操作,故而硬盘数据出现问题时候可以恢复。windows的默认分区ntfs也是属于日志类型的文件系统

ext4的文件系统日志功能通俗来讲分为两种,只记录元数据和同时记录元数据和数据区域,ext4在linux上的默认日志只开启了元数据的日志功能

ext4的日志功能实现非常复杂,涉及非常多的状态转化,且极度缺乏文档,无论是中文还是外文并没有比较优秀的关于日志的详细介绍,ext4的日志模块位于fs/jbd2,本人也没有完全搞懂日志的全部功能,以下只介绍了日志区域是怎么样的,一条普通的提交日志是怎样的。

日志超级块的数据结构

journal_header_s

日志数据块中,除了用来存储更改数据的块(这种块的内容会和修改的块的数据内容一致)外,其余所有的块都会以journal_header_t结构体作为开头,日志超级块也不例外

typedef struct journal_header_s
{
	__be32		h_magic;
	__be32		h_blocktype;
	__be32		h_sequence;
} journal_header_t;
#define JBD2_MAGIC_NUMBER 0xc03b3998U /* The first 4 bytes of /dev/random! */

h_magic 魔数,就像很多文件类型如何区分一样,文件类型如可执行程序ELF的开头是固定的ELF英文作为魔数,日志头部的magic总是等于JBD2_MAGIC_NUMBER

journal_superblock_s

日志有自己的超级块,这里是超级块的硬盘存储样式

typedef struct journal_superblock_s
{
	journal_header_t s_header;

	__be32	s_blocksize;		/* journal device blocksize */
	__be32	s_maxlen;		/* total blocks in journal file */
	__be32	s_first;		/* first block of log information 第一条日志在哪 通常=1,因为0是日志超级块,这之前的块不是日志 */

	__be32	s_start;		/* blocknr of start of log 环形日志的第一条日志从哪里开始 */
} journal_superblock_t;

日志超级块中的重要属性

  • s_blocksize
    • 日志的块大小,并不清楚为什么日志的块和ext4文件系统的块为何可以大小不一致,虽然通常是hi一致的
  • s_maxlen
    • 最大日志区域的数量,通常=(日志的大小(记录在inode[8]元数据中)/s_blocksize - s_first)
  • s_first
    • 第一条日志在哪里,通常是>=1的,因为前面会留下日志超级块本身的位置
  • s_start
    • 第一条日志可以在哪里存储,实际位置=(s_start + s_first),如果start=0表示没有日志或日志没问题可覆盖,在fsck源码中会使用start判断是否需要日志重放
e2fsprogs-1.47.0-3.oe2403.x86_64/e2fsck/journal.c

	if (ext2fs_has_feature_journal(sb) &&
	    !ext2fs_has_feature_journal_needs_recovery(sb) &&
	    journal->j_superblock->s_start != 0) {

初始化日志超级块

journal日志区,inode值是固定的8,就像文件系统里的普通文件一样,有inode号,有数据区域,区别是inode 8这个"文件"并不会出现在哪个目录里,也没有相应的文件名。

journal_init_common函数中找到inode 8的第一个块,第一个块的内容就是日志超级块

#0  journal_init_common (bdev=0xffff888004419a00, fs_dev=0xffff888004419a00, start=49153, len=4096, blocksize=1024) at fs/jbd2/journal.c:1378
#1  0xffffffff8146445c in jbd2_journal_init_inode (inode=inode@entry=0xffff888008076ae8) at fs/jbd2/journal.c:1457
#2  0xffffffff8144992b in ext4_get_journal (journal_inum=8, sb=0xffff888006df4000) at fs/ext4/super.c:5290
#3  ext4_load_journal (sb=sb@entry=0xffff888006df4000, es=es@entry=0xffff888007eb9400, journal_devnum=0) at fs/ext4/super.c:5424
#4  0xffffffff8144e676 in ext4_fill_super (sb=0xffff888006df4000, data=<optimized out>, silent=<optimized out>) at fs/ext4/super.c:4793
#5  0xffffffff813444d4 in mount_bdev (fs_type=<optimized out>, flags=<optimized out>, dev_name=<optimized out>, data=0x0 <fixed_percpu_data>, fill_super=0xffffffff8144c0c0 <ext4_fill_super>) at fs/super.c:1429
#6  0xffffffff81382747 in legacy_get_tree (fc=0xffff888006d77900) at fs/fs_context.c:593
#7  0xffffffff81342b55 in vfs_get_tree (fc=fc@entry=0xffff888006d77900) at fs/super.c:1559
#8  0xffffffff8136a4d5 in do_new_mount (path=0xffffc900001f7ef8, fstype=<optimized out>, sb_flags=<optimized out>, mnt_flags=32, name=0xffff888006c79240 "/dev/loop0", data=0x0 <fixed_percpu_data>) at fs/namespace.c:2899
#9  0xffffffff8136b81c in do_mount (data_page=0x0 <fixed_percpu_data>, flags=0, type_page=0xffff8880297965a8 "ext4", dir_name=0x55a88aa9ebe0 "/root/ext4", dev_name=0xffff888006c79240 "/dev/loop0") at fs/namespace.c:3242
#10 __do_sys_mount (data=<optimized out>, flags=0, type=<optimized out>, dir_name=0x55a88aa9ebe0 "/root/ext4", dev_name=<optimized out>) at fs/namespace.c:3450
#11 __se_sys_mount (data=<optimized out>, flags=0, type=<optimized out>, dir_name=94182369258464, dev_name=<optimized out>) at fs/namespace.c:3427
#12 __x64_sys_mount (regs=<optimized out>) at fs/namespace.c:3427
    static journal_t *journal_init_common(struct block_device *bdev,
    			struct block_device *fs_dev,
    			unsigned long long start, int len, int blocksize)
    {                                                                       // jbd2_journal_init_inode调用时,start固定为0,意思是日志文件的第一个块

    	journal = kzalloc(sizeof(*journal), GFP_KERNEL);                    // 申请空间

    	bh = getblk_unmovable(journal->j_dev, start, journal->j_blocksize); // 从设备中读取该块

    	journal->j_sb_buffer = bh;
    	journal->j_superblock = (journal_superblock_t *)bh->b_data;         // 第一个journal块中的数据就是日志超级块

在上面的地方,块并没有真正的被读取,在接下来的journal_get_superblock地方被真正读取。

    #0  journal_get_superblock (journal=journal@entry=0xffff888006e18800) at fs/jbd2/journal.c:1752
    #1  0xffffffff8146621a in load_superblock (journal=0xffff888006e18800) at fs/jbd2/journal.c:1866
    #2  jbd2_journal_wipe (journal=journal@entry=0xffff888006e18800, write=write@entry=1) at fs/jbd2/journal.c:2352
    #3  0xffffffff814499ad in ext4_load_journal (sb=sb@entry=0xffff888006e1b800, es=es@entry=0xffff888007ec5400, journal_devnum=0) at fs/ext4/super.c:5468
    #4  0xffffffff8144e676 in ext4_fill_super (sb=0xffff888006e1b800, data=<optimized out>, silent=<optimized out>) at fs/ext4/super.c:4793
    #5  0xffffffff813444d4 in mount_bdev (fs_type=<optimized out>, flags=<optimized out>, dev_name=<optimized out>, data=0x0 <fixed_percpu_data>, fill_super=0xffffffff8144c0c0 <ext4_fill_super>) at fs/super.c:1429
static int journal_get_superblock(journal_t *journal)
{
	struct buffer_head *bh;
	journal_superblock_t *sb;
	int err = -EIO;

	bh = journal->j_sb_buffer;

	if (!buffer_uptodate(bh)) {
		ll_rw_block(REQ_OP_READ, 0, 1, &bh);
		wait_on_buffer(bh);
		if (!buffer_uptodate(bh)) {
			printk(KERN_ERR
				"JBD2: IO error reading journal superblock\n");
			goto out;
		}
	}

提交日志

linux-5.10.202/fs/jbd2/commit.c文件中的jbd2_journal_commit_transaction函数用来提交事务,事务是指一系列日志的集合,这个函数会由一个jbd2内核线程执行。搜索引擎搜索jbd2经常能遇到的问题性能分析之解决 jbd2 引起 IO 高问题 Why is most the of disk IO attributed to jbd2 and not to the process that is actually using the IO? 实际上就是这个内核线程执行此函数在提交事务

下面的这个循环发生在jbd2_journal_commit_transaction函数中,用以提交日志

while (commit_transaction->t_buffers) {             // 遍历事务的每个日志 t_buffers 是一个列表

		/* Find the next buffer to be journaled... */

		jh = commit_transaction->t_buffers;

		if (!descriptor) {                          // 一个事务记录到硬盘时,第一个日志的第一个消息必须是 描述日志块

			descriptor = jbd2_journal_get_descriptor_buffer(		// 找到start可用块 标记该块的journal_header_t为描述块
							commit_transaction,
							JBD2_DESCRIPTOR_BLOCK);

jbd2_journal_get_descriptor_buffer函数用来从日志区域找到可用的日志块,并使用journal_header_t指向数据区域,设置一下类型、magic等

struct buffer_head *
jbd2_journal_get_descriptor_buffer(transaction_t *transaction, int type)
{
	journal_t *journal = transaction->t_journal;
	struct buffer_head *bh;
	unsigned long long blocknr;
	journal_header_t *header;
	int err;

	err = jbd2_journal_next_log_block(journal, &blocknr);		// blocknr = 获取当前可用的块

	bh = __getblk(journal->j_dev, blocknr, journal->j_blocksize);

	
	memset(bh->b_data, 0, journal->j_blocksize);
	header = (journal_header_t *)bh->b_data;
	header->h_magic = cpu_to_be32(JBD2_MAGIC_NUMBER);
	header->h_blocktype = cpu_to_be32(type);
	header->h_sequence = cpu_to_be32(transaction->t_tid);
	
	return bh;
}

继续看提交日志的循环体


			tagp = &descriptor->b_data[sizeof(journal_header_t)];   // 偏移一个journal_header_t大小,这个块空间很大,处理描述后面还有别的消息也会记录在这个块里
			space_left = descriptor->b_size -
						sizeof(journal_header_t);
			first_tag = 1;
			wbuf[bufs++] = descriptor;                           // wbuf记录所有要提交到日志区域的块,第一个块就是描述块

		}

		/* Where is the buffer to be written? */

		err = jbd2_journal_next_log_block(journal, &blocknr);   // 找到日志块的下一个块

        flags = jbd2_journal_write_metadata_buffer(commit_transaction,
						jh, &wbuf[bufs], blocknr);	        	// 描述块的下一块存储的是数据

jbd2_journal_write_metadata_buffer函数主要做的就是新申请一个bh指向blocknr并把jh_in块的内容复制到新的块中,也就是把日志需要记录变化的块的内容复制到日志区域中

int jbd2_journal_write_metadata_buffer(transaction_t *transaction,
				  struct journal_head  *jh_in,
				  struct buffer_head **bh_out,
				  sector_t blocknr)
{
	int need_copy_out = 0;
	int done_copy_out = 0;
	int do_escape = 0;
	char *mapped_data;
	struct buffer_head *new_bh;
	struct page *new_page;
	unsigned int new_offset;
	struct buffer_head *bh_in = jh2bh(jh_in);
	journal_t *journal = transaction->t_journal;

	new_bh = alloc_buffer_head(GFP_NOFS|__GFP_NOFAIL);

	set_bh_page(new_bh, new_page, new_offset);
	new_bh->b_size = bh_in->b_size;
	new_bh->b_bdev = journal->j_dev;
	new_bh->b_blocknr = blocknr;
	new_bh->b_private = bh_in;


	*bh_out = new_bh;
}

继续看提交日志的循环体


		tag = (journal_block_tag_t *) tagp;
		write_tag_block(journal, tag, jh2bh(jh)->b_blocknr);  // 描述块后面是第一个数据block_nr块
		tag->t_flags = cpu_to_be16(tag_flag);
		jbd2_block_tag_csum_set(journal, tag, wbuf[bufs],
					commit_transaction->t_tid);
		tagp += tag_bytes;
		space_left -= tag_bytes;
		bufs++;

		if (first_tag) {                                        // 描述块后面是第一个数据block_nr块,然后紧跟一个uuid记录
			memcpy (tagp, journal->j_uuid, 16);
			tagp += 16;
			space_left -= 16;
			first_tag = 0;
		}

		if (bufs == journal->j_wbufsize ||
		    commit_transaction->t_buffers == NULL ||
		    space_left < tag_bytes + 16 + csum_size) {           // 日志都遍历完了或者日志块满了

			tag->t_flags |= cpu_to_be16(JBD2_FLAG_LAST_TAG);     // 最后一条记录添加`JBD2_FLAG_LAST_TAG`

			for (i = 0; i < bufs; i++) {
				struct buffer_head *bh = wbuf[i];
				/*
				 * Compute checksum.
				 */
				if (jbd2_has_feature_checksum(journal)) {
					crc32_sum =
					    jbd2_checksum_data(crc32_sum, bh);
				}

    				submit_bh(REQ_OP_WRITE, REQ_SYNC, bh);      // 在这里,提交块,日志区域记录
			}

日志内容

这里读取一个硬盘的日志,看一下日志的内容

magic: c0 3b 39 98 | 3225106840
blocktype: DESCRIPTOR                       # 日志的开头总是以描述记录开始
sequence: 00 00 00 02 | 2

# 后面的tag和描述记录在同一个日志块中,记录在这个块后面跟的是那些块,这个块后面第一个块是285块
tags: 	tag: blocknr: 00 00 01 1d | 285     
			checksum: 00 00 | 0
			flags: None
			blocknr_high: 00 00 00 00 | 0
		# tag 和 tag3 根据日志超级块中的属性来决定日志描述结构是怎样的 和feature_incompat中的CSUM_V3 CSUM_V2  64BIT 有关
		tag3: blocknr: 00 00 01 1d | 285    
			flags: 00 00 00 00 | 0
			blocknr_high: 00 00 00 00 | 0
			checksum: ff 4f 99 29 | 4283406633
		tag: blocknr: 00 00 01 10 | 272
			checksum: 00 00 | 0
			flags: SAME_UUID
			blocknr_high: 00 00 00 00 | 0
		tag3: blocknr: 00 00 01 10 | 272
			flags: 00 00 00 02 | 2
			blocknr_high: 00 00 00 00 | 0
			checksum: 7e 8e f0 7f | 2123296895
		tag: blocknr: 00 00 00 02 | 2
			checksum: 00 00 | 0
			flags: SAME_UUID
			blocknr_high: 00 00 00 00 | 0
		tag3: blocknr: 00 00 00 02 | 2
			flags: 00 00 00 02 | 2
			blocknr_high: 00 00 00 00 | 0
			checksum: c1 68 3f ba | 3244834746
uuid: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

# 这里间隔 len(tags) 个块

# 紧跟一条 COMMIT 记录,代表此刻以上,都已日志提交完成,一个事务完成

-------------
magic: c0 3b 39 98 | 3225106840
blocktype: COMMIT
sequence: 00 00 00 02 | 2
-------------

软连接、普通文件、硬链接 的区别?

# cd /
# touch a
# ln a hard
# ln -s a soft

这里用到一个python代码分析, 在一个文件系统的根目录(inode=2)中分析这几个文件的区别

root = ext4.inode(2)

print("-------------")
print("\n".join(str(f) for f in root.listdir()))

print("-------------")
print(ext4.inode(12))
print("-------------")

soft = ext4.inode(13)
print(soft)
print("-------------")
print(soft.readlink())
# python3 -m ext4.ext4 
      2      DIR .
      2      DIR ..
     11      DIR lost+found
     12      REG a
     12      REG hard
     13      LNK soft
-------------
mode: REG
links_count: 00 02 | 2
flags: EXTENTS
block: extent: magic: f3 0a | 62218
		entries: 00 00 | 0
		max: 00 04 | 4
		depth: 00 00 | 0
		generation: 00 00 00 00 | 0
size: 00 00 00 00 00 00 00 00 | 0
-------------
mode: LNK | REG | CHR
links_count: 00 01 | 1
flags: None
block: link: a
size: 00 00 00 00 00 00 00 01 | 1
-------------
a

可以看到,文件a的inode是12

  • mode 不同 文件和硬链接都是常规文件REG 软连接是LNK
  • 12的links_count是2,代表同时两个文件名引用,分别是a本身和硬链接hard
  • flags block 不同,软连接的长度很短,只有一个字符a,所以原来用来存储文件数据位置的block被用来存储链接本身

touch 一个新的空文件占用磁盘空间吗?占用的话占用多少?

  • 占用一个inode的大小,但这个大小是预留的,实际不占用
  • 文件夹内占用一个条目,存储文件名,如果文件夹空间足够也不会额外申请,文件夹的大小是整块大小的,可能会有足够空间

新建一个空目录占用磁盘空间吗?占用多少?和新建一个文件相比,哪个占用的更大?

同样mkdir dir再用python脚本分析

print("-------------")
root = ext4.inode(2)
print(root)

print("-------------")
print("\n".join(str(f) for f in root.listdir()))

print("-------------")
dir = ext4.inode(14)
print(dir)
print("-------------")
print("\n".join(str(f) for f in dir.listdir()))
-------------
      2      DIR .
      2      DIR ..
     11      DIR lost+found
     12      REG a
     12      REG hard
     13      LNK soft
     14      DIR dir
-------------
mode: DIR
links_count: 00 02 | 2
size: 00 00 00 00 00 00 04 00 | 1024
extents: 	block: 00 00 00 00 | 0
		len: 00 01 | 1
		start_hi: 00 00 | 0
		start_lo: 00 00 19 8b | 6539
		start: 00 00 00 00 00 00 19 8b | 6539
-------------
 14      DIR .
  2      DIR ..

这个文件夹即使是空的,也会占用一个块的大小(这个ext4.img中的ext4块是1024),因为文件夹内也会有...两个条目

  • 占用inode本身,但预留实际不占

  • 占用一个ext4文件块,用来存储...两个条目

  • 新建空白文件不会有实际数据大小 其他大小同

思考题

问题来自 Linux文件系统十问,两个问题上面已回答, 答案也在里面,不过文章中的环境像是ext4但没有从ext4文件系统的角度回答,期待你的回答

1、机械磁盘随机读写时速度非常慢,操作系统是采用什么技巧来提高随机读写的性能的?
2、touch 一个新的空文件占用磁盘空间吗?占用的话占用多少?
3、新建一个空目录占用磁盘空间吗?占用多少?和新建一个文件相比,哪个占用的更大?
4、你知道文件名是记录在磁盘的什么地方吗?
5、文件名最长多长?受什么制约?
6、文件名太长了会影响系统性能吗?为什么会产生影响?
7、一个目录下最多能建立多少个文件?
8、新建一个内容大小 1 k 的文件,实际会占用多大的磁盘空间?
9、向操作系统发起读取文件 2 Byte 的命令,操作系统实际会读取多少呢?
10、我们使用文件时要怎么样来能提高磁盘IO速度?

参考

文件系统(六):一文看懂linux ext4文件系统工作原理

kernel.org/admin-guide/ext

kernel.org Docs » ext4 Filesystem » 2. Data Structures and Algorithms

pENQYaq.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值