UBI文件系统分析

UBI文件系统

引言

什么是UBIFS文件系统
UBIFS是UBI file system的简称,用于裸的flash设备,作为jffs2的后继文件系统之一。UBIFS通过UBI子系统处理与MTD设备之间动作。UBIFS文件系统更适合MLCNAND FLASH。需要注意的是UBIFS并不是为SSD,MMC,SD,Compact Flash等之类的基于flash的存储设备,其是针对于裸flash设备。

裸flash有以下特点:
l 其包含的块被称为可擦除块,而对于SSD这类的设备,并无可擦除块的概念,取而代之的是扇区的概念。

l 包括读、写、擦除可擦除块三种操作。

l 硬件并不管理坏的可擦除块,而SSD之类的设备则具有专门的控制器处理坏块。

l 可擦除块的读写寿命从几千到几十万之间不等。
在这里插入图片描述
MTD(Memory Technology Devices)对闪存存储器提供了一个抽象,隐藏了特定flash的独特之处,提供统一的API存取各种类型的flash。

MTD在内核层的API是struct mtd_device而用户空间的API接口是/dev/mtd0,这些接口提供了设备信息,读写可擦除块,擦除一个可擦除块,标记一个可擦除块是坏块,检查可擦除块是否是坏块。MTD的API并不隐藏坏的可擦除块也不做任何损耗平衡。

UBI(Unsorted Block Images)的内核API是include/mtd/ubi-user.h,用户空间的则是/dev/ubi0,提供损耗平衡,隐藏坏块,允许运行时容量创建、删除和修改,有点类似LVM功能。UBI线性扩展,在初始化时会读取所有的可擦除块头,所以当flash容量越大,初始化所花费的时间越多,但是就可扩展性而言比JFFS2要好很多

在了解ubi文件系统之前需要先了解虚拟文件系统的一些关键知识

VFS虚拟文件系统:

虚拟文件系统(Virtual File System,简称VFS)是Linux内核的子系统之一,它为用户程序提供文件和文件系统操作的统一接口,屏蔽不同文件系统的差异和操作细节。借助VFS可以直接使用open()、read()、write()这样的系统调用操作文件,而无须考虑具体的文件系统和实际的存储介质。

举个例子,Linux用户程序可以通过read() 来读取ext3、NFS、XFS等文件系统的文件,也可以读取存储在SSD、HDD等不同存储介质的文件,无须考虑不同文件系统或者不同存储介质的差异。

通过VFS系统,Linux提供了通用的系统调用,可以跨越不同文件系统和介质之间执行,极大简化了用户访问不同文件系统的过程。另一方面,新的文件系统、新类型的存储介质,可以无须编译的情况下,动态加载到Linux中。

"一切皆文件"是Linux的基本哲学之一,不仅是普通的文件,包括目录、字符设备、块设备、套接字等,都可以以文件的方式被对待。实现这一行为的基础,正是Linux的虚拟文件系统机制。
在这里插入图片描述

linux文件系统四大对象:
1)超级块(super block)
2)索引节点(inode)
3)目录项(dentry)
4)文件对象(file)

 superblock:记录此filesystem 的整体信息,包括inode/block的总量、使用量、剩余量, 以及档案系统的格式与相关信息等;
 inode:记录档案的属性,一个档案占用一个inode,同时记录此档案的资料所在的block 号码;
 block:实际记录档案的内容,若档案太大时,会占用多个block 。

超级块(super_block)

super_block的含义:
超级块代表了整个文件系统,超级块是文件系统的控制块,有整个文件系统信息,一个文件系统所有的inode都要连接到超级块上,可以说,一个超级块就代表了一个文件系统。

超级块用于存储文件系统的元信息,由super_block结构体表示,定义在<linux/fs.h>中,元信息里面包含文件系统的基本属性信息,比如有:索引节点信息、挂载的标志、操作方法 s_op、安装权限、文件系统类型、大小、区块数,等等

其中操作方法 s_op 对每个文件系统来说,是非常重要的,它指向该超级块的操作函数表,包含一系列操作方法的实现,这些方法有:分配inode、销毁inode/读、写inode/文件同步等等

创建文件系统时,其实就是往存储介质的特定位置,写入超级块信息

struct super_block {
struct list_head    s_list;     
	这是第一个成员,是一个双向循环链表,把所有的super_block连接起来,一个super_block代表一个在linux上的文件系统,这个list上边的就是所有的在linux上记录的文件系统。
dev_t           s_dev;      /* search index; _not_ kdev_t */
	包含该具体文件系统的块设备标识符。例如,对于 /dev/hda1,其设备标识符为 0x301
unsigned char       s_blocksize_bits;
	下面的size大小占用位数,例如512字节就是9 bits
unsigned long       s_blocksize;
	文件系统中数据块大小,以字节单位
loff_t          s_maxbytes; /* Max file size */
	允许的最大的文件大小(字节数)
struct file_system_type *s_type;
	文件系统类型(也就是当前这个文件系统属于哪个类型?ext2,3,4还是ubi)要区分“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block
    const struct super_operations   *s_op;
    const struct dquot_operations   *dq_op;
    const struct quotactl_ops   *s_qcop;
    const struct export_operations *s_export_op;
    unsigned long       s_flags;
    unsigned long       s_iflags;   /* internal SB_I_* flags */
unsigned long       s_magic;
	区别于其他文件系统的标识
struct dentry       *s_root;
	指向该具体文件系统安装目录的目录项
    struct rw_semaphore s_umount;
int         s_count;
	对超级块的使用计数
… … 
struct block_device *s_bdev;
	指向文件系统被安装的块设备
… …
    char s_id[32];              /* Informational name */
    u8 s_uuid[16];              /* UUID */

void            *s_fs_info; /* Filesystem private info */
	指向一个文件系统的私有数据

… … 以下省略
};

索引节点(inode)

索引节点对象包含Linux内核在操作文件、目录时,所需要的全部信息,这些信息由inode结构体来描述,定义在<linux/fs.h>中,主要包含:超级块相关信息、目录相关信息、文件大小、访问时间、权限相关信息、引用计数,等等

一个索引节点inode代表文件系统中的一个文件,只有当文件被访问时,才在内存中创建索引节点。与超级块类似的是,索引节点对象也提供了许多操作接口,供VFS系统使用,这些接口包括:create(): 创建新的索引节点(创建新的文件)、link(): 创建硬链接、symlink(): 创建符号链接。mkdir(): 创建新的目录。等等,我们常规的文件操作,都能在索引节点中找到相应的操作接口

同时注意:inode有两种,一种是VFS的inode,一种是具体文件系统的inode。前者在内存中,后者在磁盘中。所以每次其实是将磁盘中的inode调进填充内存中的inode,这样才是算使用了磁盘文件inode。

注意inode怎样生成的:每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定(现代OS可以动态变化),一般每2KB就设置一个inode。一般文件系统中很少有文件小于2KB的,所以预定按照2KB分,一般inode是用不完的。所以inode在文件系统安装的时候会有一个默认数量,后期会根据实际的需要发生变化。

注意inode号:inode号是唯一的,表示不同的文件。其实在Linux内部的时候,访问文件都是通过inode号来进行的,所谓文件名仅仅是给用户容易使用的。当我们打开一个文件的时候,首先,系统找到这个文件名对应的inode号;然后,通过inode号,得到inode信息,最后,由inode找到文件数据所在的block,现在可以处理文件数据了。

inode和文件的关系:当创建一个文件的时候,就给文件分配了一个inode。一个inode只对应一个实际文件,一个文件也会只有一个inode。inodes最大数量就是文件的最大数量。

struct inode {
    umode_t         i_mode;
    unsigned short      i_opflags;
    kuid_t          i_uid;
    kgid_t          i_gid;
    unsigned int        i_flags;

#ifdef CONFIG_FS_POSIX_ACL
    struct posix_acl    *i_acl;
    struct posix_acl    *i_default_acl;
#endif

	const struct inode_operations   *i_op;
	索引节点操作
	struct super_block  *i_sb;
	inode所属文件系统的超级块指针
   struct address_space    *i_mapping;

#ifdef CONFIG_SECURITY
   void            *i_security;
#endif

    /* Stat data, not accessed from path walking */
	unsigned long       i_ino;
	索引节点号,每个inode都是唯一的
… …
	loff_t          i_size;
	inode所代表的的文件的大小,以字节为单位
	struct timespec     i_atime;
	文件最后一次访问时间
	struct timespec     i_mtime;
	文件最后一次修改时间
	struct timespec     i_ctime;
	inode最后一次修改时间
	spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
	unsigned short          i_bytes;
	文件中最后一个块的字节数
	unsigned int        i_blkbits;
	块大小,bit单位
	blkcnt_t        i_blocks;
	文件所占块数

……

    struct hlist_node   i_hash;
    struct list_head    i_lru;      /* inode LRU list */
    struct list_head    i_sb_list;
    struct list_head    i_wb_list;  /* backing dev writeback list */
    union {
        struct hlist_head   i_dentry;
	指向目录项链表指针,注意一个inode可以对应多个dentry,因为一个实际的文件可能被链接到其他的文件,那么就会有另一个dentry,这个链表就是将所有的与本inode有关的dentry都连在一起
        struct rcu_head     i_rcu;
    };
    u64         i_version;
    atomic_t        i_count;
    atomic_t        i_dio_count;
    atomic_t        i_writecount;
#ifdef CONFIG_IMA
    atomic_t        i_readcount; /* struct files open RO */
#endif
const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
	文件操作
    struct file_lock_context    *i_flctx;
    struct address_space    i_data;
	struct list_head    i_devices;
	如果inode代表设备,那么就是设备号
    union {
        struct pipe_inode_info  *i_pipe;
        struct block_device *i_bdev;
        struct cdev     *i_cdev;
        char            *i_link;
        unsigned        i_dir_seq;
    };

    … …

    void            *i_private; /* fs or device private pointer */
};

目录(dentry)

VFS把目录当做文件对待,比如/usr/bin/vim,usr、bin和vim都是文件,不过vim是一个普通文件,usr和bin都是目录文件,都是由索引节点对象标识。

由于VFS会经常的执行目录相关的操作,比如切换到某个目录、路径名的查找等等,为了提高这个过程的效率,VFS引入了目录项的概念。一个路径的组成部分,不管是目录还是普通文件,都是一个目录项对象。/、usr、bin、vim都对应一个目录项对象。不过目录项对象没有对应的磁盘数据结构,是VFS在遍历路径的过程中,将它们逐个解析成目录项对象。

目录项由dentry结构体标识,定义在<linux/dcache.h>中,主要包含:父目录项对象地址、子目录项链表、目录关联的索引节点对象、目录项操作指针,等等

目录项是描述文件的逻辑属性,只存在于内存中,并没有实际对应的磁盘上的描述,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计。注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。例如:open一个文件/home/xxx/yyy.txt,那么/、home、xxx、yyy.txt都是一个目录项,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。

注意:目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。

struct dentry {
    /* RCU lookup touched fields */
    unsigned int d_flags;       /* protected by d_lock */
    seqcount_t d_seq;       /* per dentry seqlock */
	struct hlist_bl_node d_hash;    /* lookup hash list */
	内核使用dentry_hashtable对dentry进行管理,dentry_hashtable是由list_head组成的链表,一个dentry创建之后,就通过d_hash链接进入对应的hash值的链表中
	struct dentry *d_parent;    /* parent directory */
	父目录的目录项
	struct qstr d_name;
	目录项名称
	struct inode *d_inode;      
	与该目录项关联的inode
	unsigned char d_iname[DNAME_INLINE_LEN];    /* small names */
	存放短的文件名

    /* Ref lookup also touches following */
    struct lockref d_lockref;   /* per-dentry lock and refcount */
	const struct dentry_operations *d_op;
	目录项操作
	struct super_block *d_sb;   /* The root of the dentry tree */
	这个目录项所属的文件系统的超级块
    unsigned long d_time;       /* used by d_revalidate */
    void *d_fsdata;         /* fs-specific data */

    union {
        struct list_head d_lru;     /* LRU list */
        wait_queue_head_t *d_wait;  /* in-lookup ones only */
    };
    struct list_head d_child;   /* child of parent list */
    struct list_head d_subdirs; /* our children */
    /*
     * d_alias and d_rcu can share memory
     */
    union {
        struct hlist_node d_alias;  /* inode alias list */
	一个有效的dentry必然与一个inode关联,但是一个inode可以对应多个dentry,因为一个文件可以被链接到其他文件,所以,这个dentry就是通过这个字段链接到属于自己的inode结构中的i_dentry链表中的
        struct hlist_bl_node d_in_lookup_hash;  /* only for in-lookup ones */
        struct rcu_head d_rcu;
    } d_u;
};

文件(file)

文件对象:注意文件对象描述的是进程已经打开的文件。因为一个文件可以被多个进程打开,所以一个文件可以存在多个文件对象。但是由于文件是唯一的,那么inode就是唯一的,目录项也是定的!

进程其实是通过文件描述符来操作文件的,注意每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。

struct file {
union {
        struct llist_node   fu_llist;
	所有的打开的文件形成的链表!注意一个文件系统所有的打开的文件都通过这个链接到super_block中的s_files链表中
        struct rcu_head     fu_rcuhead;
} f_u;
struct path     f_path;
struct path {
struct vfsmount *mnt;
	该文件在这个文件系统中的安装点
struct dentry *dentry;
	与该文件相关的dentry
};

struct inode        *f_inode;   /* cached value */
const struct file_operations    *f_op;
	文件操作,当进程打开文件的时候,这个文件的关联inode中的i_fop文件操作会初始化这个f_op字段

    /*
     * Protects f_ep_links, f_flags.
     * Must not be taken from IRQ context.
     */
spinlock_t      f_lock;
atomic_long_t       f_count;
unsigned int        f_flags;
	打开文件时候指定的标识
fmode_t         f_mode;
	文件的访问模式
struct mutex        f_pos_lock;
loff_t          f_pos;
	目前文件的相对开头的偏移
struct fown_struct  f_owner;
const struct cred   *f_cred;
struct file_ra_state    f_ra;

u64         f_version;
#ifdef CONFIG_SECURITY
    void            *f_security;
#endif
/* needed for tty driver, and maybe others */
void            *private_data;
	私有数据(文件系统和驱动程序使用)
    struct address_space    *f_mapping;
} __attribute__((aligned(4)));

重点解释一些重要字段:
 首先,f_flags、f_mode和f_pos代表的是这个进程当前操作这个文件的控制信息。这个非常重要,因为对于一个文件,可以被多个进程同时打开,那么对于每个进程来说,操作这个文件是异步的,所以这个三个字段就很重要了。
 第二:对于引用计数f_count,当我们关闭一个进程的某一个文件描述符时候,其实并不是真正的关闭文件,仅仅是将f_count减一,当f_count=0时候,才会真的去关闭它。对于dup,fork这些操作来说,都会使得f_count增加,具体的细节,以后再说。
 第三:f_op也是很重要的!是涉及到所有的文件的操作结构体。例如:用户使用read,最终都会调用file_operations中的读操作,而file_operations结构体是对于不同的文件系统不一定相同。里面一个重要的操作函数式release函数,当用户执行close时候,其实在内核中是执行release函数,这个函数仅仅将f_count减一,这也就解释了上面说的,用户close一个文件其实是将f_count减一。只有引用计数减到0才关闭文件。
在这里插入图片描述

UBI文件系统基础

UBI分区介绍:

UBIFS中一共分为六个区,分别为,还有一个对用户隐藏的卷层
 superblock area:超级块区,superblock是每一个文件系统必备的
 master node area:主节点区
 log area:日志区(为了区分journal ,这里统一用log区指代)
 LPT(LEB properties tree) area:LPT区
 Orphan area:孤立区
 The mian area:主分区
在这里插入图片描述

其中,superblock区域固定占用LEB0,master区域固定占用LEB1和LEB2,其他区域占据的LEB数量则视该文件系统分区实际占有的总的LEB数量而定,orphan区域一般占用1到2个LEB。
master区域中的两个LEB相互备份,以确保在异常掉电的情况能够恢复master区域的内容。master区域的数据在每次发生commit的时候进行更新。
log区域记录日志数据的存储位置lnum:offs。ubifs是一种日志文件系统,文件数据采用异地更新的方式(out_of_place_update),即文件数据的更新不会每次都同步到flash,而是记录到日志区,当日志区的数据累计到一定程度时,才将数据同步到flash中。
lpt区域记录了磁盘空间中各个LEB的状态(free、dirty、flag),用于实现对LEB的分配、回收和状态查询。
main区域则保存文件的数据和索引。

主节点区

MASTER AREA:UBIFS为了进行垃圾回收,采用了node结构来进行文件的管理。node就是文件信息和数据的一个结合。主节点结构体(下面会列出)中除了__u8 data[]之外都可以称之为文件信息,而__u8 data[]称之为文件数据。为了便于垃圾回收,文件系统必须为所有的文件建立这样的树状结构来进行管理。为了降低启动时的扫描时间和运行的内存消耗,UBIFS将这样的树状结构保持在FLASH上,而不是在内存中。但是问题就来,怎么知道这棵树的根在哪儿?所以master区就是为了这样的目的,当然不仅仅是为了这样的目的,这棵树的根就保存在masterarea中

LOG区

用于存储日志区更新提交的节点信息。详情见日志管理部分
实际上journal区由 LOG和BUD两部分组成,而LOG是一个固定长度和分区的一个区域,而BUD区是可以在主区域中的任何一个位置上。BUD区包含了文件系统的数据(数据节点,inode节点等),而LOG仅包含用来索引BUD区的引用节点和提交开始节点。
当日志提交时(即ubi文件系统将数据拷贝入存储介质的行为,被称为提交),并不只单纯的复制数据,而是将bud区的数据作为某种索引,而之所以被叫做BUD(芽),是因为这个区域内的数据在将来会成为文件系统索引树的叶子节点(在LOG区,被replay的时候)
日志区记录时有一个jhead作为日志头标识,这个数据是根据实际情况变化的,正如原码中journal.c的注释所说,ubi的作者希望以尽可能最佳的方式将数据写入日记账,在一个LEB中最好所有节点都属于同一个索引节点,因此可以将不同索引节点所拥有的数据标记成不同的日志头,但是目前为止,只有一个数据头被使用了,即出于恢复原因,基本头(base jhead=1)包含所有inode节点、所有目录条目节点和所有truncate节点。数据头(data jhead=2)仅包含数据节点。还有一个垃圾回收标志头(GC:Garbage collector journal head number jhead=0)
引申一下,ubi写入数据实际上就是写日志的过程,如果日志区的BUD满了,就会进行一次提交,提交后的BUD区数据就变成了叶子节点,然后再去找新的LEB作为BUD区继续作为日志

LPT区

LPT有两种不同的形式big model和small model,使用哪种模式由LEB Properties table的大小决定的,当整个LEB Properties table可以写入单个eraseblock中时使用small model,否则big model。对于big model,lsave被用来保存部分重要的LEB properties, 以加快超找特定LEB的速度。

LEBproperties中主要包含三个重要的参数:freespace、dirty space 和whether the eraseblock is an indexeraseblock or not。空闲空间是指可擦除块中未使用的空间。Dirty space 是指一个可擦除块中废弃的(被trunk掉的)和填充的空间的字节数(UBIFS中存在minI/O,也就是最小的写入数据字节数,如果数据不够,就需要padding来填充)。我们上面提到了master区中放的是node树的根,那么它的枝放在哪儿呢?是以index node的形式存放在可擦除块中,所以需要标记一下知道main area中这个可擦除块中存放的是否是index node。LPT area的大小是根据分区的大小来确定的。LPT也有自己的LPT,这是什么意思,就是LPT内部建立了一个ltab(LEB properties table,因为LPTarea所占的可擦出块毕竟是少数,所以采用表的形式),是LPT表所占LEB的LPT。LPT是在commit的时候更新的。

孤立区和主节点区:

ORPHAN AREA:在理解在这个区的作用之前,我们必须准确的了解inode node结点在UBIFS中的作用。用这篇文章中的话来解释的话,A node that holds the metadata for an inode,Every inode has exactly one(non-obsolete)inode node。Orphan area is an area for storingthe inode numbers of deleted by still open inodes,needed for recovery from unclean unmounts(百度翻译:保存inode元数据的节点,每个inode只有一个(非过时的)inode节点。孤立区域是用于存储由仍然打开的inode删除的inode编号的区域,用于从不干净的卸载中恢复)。

MAIN AREA:这个区就不用多说了,是用来存放文件数据和index结点的。

Volume,PEB和LEB

PEB:physical eraseblocks 也就是对应flash上的一个擦写块
LEB:logical eraseblocks 软件上的概念
Volume:卷
在这里插入图片描述

ubi层对flash的管理是以擦写块为单位的,LEB对应软件上的概念,PEB对应flash上一个实实在在的擦写块,每一个LEB对应一个PEB。

往上看多个LEB可以组成一个volume,也就是说,可以根据不同的功能,将LEB划分到不同的卷中;其中valume-layout是一个ubi内部使用的卷,用来存放该MTD设备上所划分的各个卷的信息,其包含两个LEB,它们存储的内容是一样,互为备份。

往下看每个PEB的内容包含3部分ech(erase counter header),vidh(volume identifier header),data。

卷层

在这里插入图片描述

volume-layout是UBI内部使用的一个卷,其包含两个LEB(互为备份),对应PEB中的数据内容如上图,data(灰色)部分是一个struct ubi_vtbl_record 结构数组,记录了当前UBI设备所有卷的信息, ubi_read_volume_table() 函数先遍历临时结构struct ubi_attach_info 找出volumelayout所在PEB,然后 读出struct ubi_vtbl_record 结构数组并保存到内存中,也就是struct ubi_device 的struct ubi_volume *volumes[] 字段中,初始化后的数组结构如下图,其中struct ubi_volume *volumes[] 是一个指针数组,数组中的每一个元素都是struct ubi_volume 结构(详细过程见ubi_read_volume_table() 函数)。
在这里插入图片描述

struct ubi_vtbl_record {
__be32  reserved_pebs;  
	给该卷预留的PEB
    __be32  alignment;
__be32  data_pad; 
	在每个物理块的末尾有多少字节未使用以满足请求的对齐
    __u8    vol_type;
    __u8    upd_marker;
    __be16  name_len;
    __u8    name[UBI_VOL_NAME_MAX+1];
    __u8    flags;
    __u8    padding[23];
    __be32  crc;
} __packed;

在这里插入图片描述

EC头:ubi_ec_hdr

struct ubi_ec_hdr {
    __be32  magic;
    __u8    version;
    __u8    padding1[3];
__be64  ec; /* Warning: the current limit is 31-bit anyway! */
	表示该逻辑块被擦除过的次数
__be32  vid_hdr_offset; 
	表示vid 头的偏移,一般跟在ec header 后面
__be32  data_offset;
	表示用户数据的偏移位置
    __be32  image_seq;
    __u8    padding2[32];
    __be32  hdr_crc;
} __packed;

vid头:ubi_vid_hdr

其中ubi_vid_hdr(UBI volume identifier header卷标识头,但实际是描述logical 块的信息)

struct ubi_vid_hdr {
    __be32  magic;
    __u8    version;
    __u8    vol_type;
    __u8    copy_flag;
    __u8    compat;
    __be32  vol_id; 卷id号
    __be32  lnum; 逻辑块号
    __u8    padding1[4];
    __be32  data_size; 逻辑块包含字节数
    __be32  used_ebs; 此卷中使用的逻辑擦除块的总数
    __be32  data_pad;
    __be32  data_crc; 存储在该逻辑块上数据的CRC checksum
    __u8    padding2[4];
    __be64  sqnum; *@sqnum是创建此VID标头时全局序列计数器的值。每次UBI将新的VID头写入闪存时,即当它将逻辑擦除块映射到新的物理擦除块时,全局序列计数器将递增。全局序列计数器是一个无符号64位整数,我们假设它从不溢出。@sqnum(序列号)用于区分逻辑块的旧版本和新版本。
    __u8    padding3[12];
    __be32  hdr_crc;
} __packed;

在这里插入图片描述

UBI索引和管理:

ubifs用node标准化每一个存储对象,用lprops(struct ubifs_lprops - logical eraseblock properties 逻辑块属性)描述每一个逻辑块空间,用TNC组织管理所有的node对象,用LPT组织管理所有的lprops对象。

ubifs中除了存储用户数据,还要存储索引节点、目录项、超级块等数据。这些数据结构各异,差异很大,为了统一数据视图,便于管理,ubifs标准化了所有数据的表现形式,所有数据以node表示呈现。并根据不同用途对node进行分类、存储和组织。ubifs的node汇总介绍如下。
黑色部分为通用的文件系统数据,红色部分为ubifs专有的文件系统数据。

在这里插入图片描述

文件索引(TNC):

ubifs文件系统中的数据统一以node的形式顺序存储于LEB中,其中node包含metadata和data两部分:metadata标识了node包含的数据类型,包括ubifs_ino_node(inode node)、ubifs_data_node、ubifs_dent_node(directory entry node)、ubifs_xent_node(extended attribute node 拓展属性节点)和ubifs_trun_node(truncation node 截断结点)五种;data部分则为实际的有效数据。node的长度根据数据类型的不同而有所差别,node在磁盘中的存储序列如下所示:

在这里插入图片描述

ubifs_trun_node:This node exists only in the journal and never goes to the main area. (此节点仅存在于日志中,从不转到主区域)

ubifs采用一棵B+树对文件的数据进行索引:其中保存文件路径信息的node组成B+树的索引节点,组成文件数据的node组成B+树的叶子节点。B+树中的索引节点在内存中为ubifs_znode,该结构体只有部分数据需要保存到flash中,而其他部分只存在于内存当中,保存到flash中的部分组成结构体为ubifs_idx_node。ubifs将用于索引文件数据的B+树称之为TNC(Tree Node Cache)树,其索引结构如下所:
在这里插入图片描述

假设B+树的数高为N,每个索引节点包含的最大分支数为M,则该B+数能够索引到的最大叶子节点数为M的(N+1)次方个。在ubifs的TNC中,B+树的树高N默认为bottom_up_buf=64(该值可以根据实际情况进行扩展);每个索引节点包含的最大分支数M为8(该值在进行文件系统分区格式化时指定),而每个索引节点实际占有的分支数记录与索引节点的child_cnt域中。

TNC中Level 1至Level N的内部索引节点(ubifs_znode)的分支(ubifs_zbranch)分别指向下一级索引节点的存储位置(lnum:offs:len),而Level 0处的索引节点的分支则指向数据节点的存储位置(lnum:offs:len)。保存索引节点和数据节点的存储位置,而不保存节点的实际数据内容,可以有利用在同一个LEB中保存多的节点,减少LEB的访问次数,提高访问效率。B+树通过键值进行索引,数据节点的键值计算规则如下:
在这里插入图片描述

其中,对于同属于一个文件node的多个数据节点ubifs_data_node,其键值key按照block number进行区分;对于同属于一个文件node的多个目录项节点ubifs_dent_node,其键值key以目录项文件名的hash值进行区分;同理,ubifs_xent_node以attr entry的hash值进行区分。注意:在进行键值比较时,首先比较键值的低32位,再比较两者的高32位,因此同属于同一个inode的所有数据node都保存在B+树的相邻位置,这样便可以快速的找到同一个inode的各个数据node。在进行crash分析时,可能需要用到利用键值查找znode/zbranch的过程。此外,TNC的叶子节在内存中集合被称之为LNC(Leaf Node Cache),便于查找需要频繁访问的目录项ubifs_dent_node以及扩展属性项(和目录项共用一个数据结构)。

索引节点(index-node)和叶子节点(非索引节点 non-index-node)永远保存在不同的LEB中,即同一个LEB不可能同时包含index node和non-index node。

空间管理(LPT):

ubifs采用另一个B+树对磁盘的空间进行管理,此时B+树的叶子节点保存的是LEB属性(空闲空间free、脏空间dirty和是否是索引LEB),该B+树在ubifs中被称之为LPT(LEB properties tree),其索引结构如下图所示:

在这里插入图片描述

LPT和TNC两者的区别在于:TNC中叶子节点和索引节点采用统一的数据结构ubifs_znode表示,其保存的内容一致均为key、lunm、offs和len,而在LPT中其索引节点对应的数据结构为ubifs_nnode,保存索引节点所在的lnum和offs,其叶子节点对应的数据结构为ubifs_pnode,保存LEB的空间属性free、dirty和flag;TNC各索引节点的分支数不确定,但在LPT中每个索引节点包含固定的分支数fanout=4;对于固定大小的文件系统分区,TNC的树高不确定,其树高由文件系统实际包含的文件数(数据节点的数量)决定,而LPT的树高固定为log4M,M为main区域的LEB数量。TNC中需要根据不同的数据节点计算键值,并作为节点的一个元素伴随节点一起插入到B+树中,而LPT中不需要额外的键值用于索引节点,而是直接以LEB号作为键值对节点进行索引,LPT叶子节点从左到右其LEB号依次为LEB(main first) ~ LEB(main first + M)。

LPT存在big model和small model两种存储模式,当所有的nnode、pnode、ltab、lsave能够存储到一个LEB中时,使用small model,否则使用big model。两种模式的区别在于回写磁盘时的规则不同,对于small model其在flash中的数据存储格式如下所示:

在这里插入图片描述

LPT为main区域LEB的空间管理索引树,而ltab则为LPT区域的空间管理索引树,标记LPT区域的LEB的使用情况。

日志管理

ubifs是一种应用于nand flash之上的文件系统,对nand flash进行写操作之前必须以earse block为单位对其进行擦除,并且擦除之后只能对其写一次。在这种情况下,对文件数据先读、再擦、再写的inplace_update方式就变得非常耗时,因此ubifs采用out_of_place_update(即异地更新)的方式对文件数据进行操作。异地更新是指将修改的文件数据写到已经擦除过的磁盘块,并且在内存中修改文件索引将其指向新的数据块,此时原始的文件索引关系仍保存在磁盘,而新的文件索引存储在内存当中,存储在内存中的索引关系会根据情况定时的同步到磁盘中(在ubifs中称之为commit),此时才完成真正的文件数据修改。

ubifs将所有写入新数据的磁盘块称之为日志区(journal),它并不是一段连续的LEB块区域,而是由任意位置和任意多个main区域的LEB组成。根据不同的数据类型,ubifs包含三个动态变化的日志区,分别是GC、BASE和DATA。ubifs以LEB为单位对flash进行写操作,因此每一个日志区在内存中都维护一个称之为journal head的缓冲区,其大小与LEB相同,当ubifs写数据时总是先将数据写到对应的缓冲区中,当缓冲区填满之后才将数据实际写到磁盘当中,这也是为了减少对磁盘的操作次数。

ubifs将每个作为日志区的LEB的信息(lnum:offs)以ubifs_ref_node的数据结构记录于log区域,为区分log区域中的数据是否同步到了flash,ubifs在每次同步操作时向log区域写入一个ubifs_cs_node以作标识。ubifs中的所有node结构体都包含一个统一的头部元素为ubifs_ch,在元素中包含有一个标记该node创建时间的squem,因此log区域中大于ubifs_cs_node的squem的ubifs_ref_node即为尚未被同步到flash的日志LEB,反之则为已经被同步到flash的日志LEB,其对应的ubifs_ref_node则可以被删除以腾空间。当系统发生异常掉电时,ubifs扫描log区域的LEB,便可对掉电前的TNC和LPT等数据结构进行重建,从而达到掉电恢复的目的。

在数据进行同步(即commit)时,ubifs_cs_node总是寻找一个新的LEB,并占据其起始位置,每当一个LEB加入到日志区时,ubifs便会创建一个ubifs_ref_node结构体,并将该结构体同步到磁盘中。log区的数据排列结构如下所示:
在这里插入图片描述

ubifs_cs_node和ubifs_ref_node的数据结构分别如下所示:
在这里插入图片描述

ubifs在内存中还维护了一个与ubifs_ref_node对应的bud结构便于快速查找日志区的LEB。

EBA子系统:

EBA(Eraseblock Association)UBI擦除块关联(EBA)子系统,包含所有LEB到PEB的映射信息。

EBA子系统主要提供如下功能:
 LEB/PEB的映射表管理:上层只看到LEB,不再关心块的读写错误处理、替换等细节;
 LEB的sequence counter管理:seq counter主要是为了标记顺序,解决LEB/PEB的映射冲突;
 LEB访问接口封装:如read, write, copy, check, unmap, atomic change等;
 LEB访问保护:每一个LEB的并发访问都由读写信号量锁rwsem进行保护;

EBA提供了2种写方式:ubi_eba_write_leb和ubi_eba_atomic_leb_change。
ubi_eba_write_leb用于对块的write,ubi_eba_atomic_leb_change用于对块的修改(modify)或者追加(append)。ubi_eba_write_leb写后会做读校验,如果有-EIO错误,将老PEB上的数据移动到新的PEB上,并将新数据也写到新的PEB中,对老PEB进行磨损(torture)。ubi_eba_atomic_leb_change为了避免破坏已有数据,采用异地更新的方式来实现原子写,并加一个ubi->alc_mutex来进行串行化保护,其具体流程如下:

  1. 读取leb数据(ubifs内完成)
  2. 检查写数据长度是否为0,为0时,unmap leb
  3. 分配初始化vid_hdr
  4. 分配新的peb(ubi_wl_get_peb)
  5. 新peb中写入vid_hdr
  6. 新peb中写入老leb数据+新增数据
  7. 回收老的peb(ubi_wl_put_peb)
  8. 更新leb map(vol->eba_tbl)

在这里插入图片描述

在struct ubi_volume 结构体中,有一个比较重要的字段struct ubi_eba_table *eba_tbl ,该字段记录了当前volume中所有LEB与PEB的映射关系,其中struct ubi_eba_entry *entries 是一个数组结构,每一个元素对应一个struct ubi_eba_table 结构体, struct ubi_eba_entry *entries 数组的下标对应于LEB的编号,数组元素的内容对应PEB的编号,这样就将LEB与PEB关联起来了(详细过程见ubi_eba_init() 函数)
对EBA表最重要的操作是map和unmap,map过程是首先找到相应的PEB,然后将VID头写入PEB,然后修改内存中相应的EBA表。unmap首先解除LEB与相应PEB的映射,然后调度擦除该PEB,unmap并不会等到擦除完成,该擦除由UBI后台进程负责

WL子系统

wl(wear-leveling)磨损均衡子系统,在UBI中将PEB分为4种情况,正在使用、空闲状态、需要擦除、已经损坏,各个状态的PEB被放到不同的红黑树中管理。在ubi_eba_init() 函数中,会先分配一个struct ubi_wl_entry 指针数组并存储在sruct ubi_wl_entry **lookuptbl 字段中,数组下标为PEB的编号,数组内容记录了PEB的擦写次
擦写均衡:flash的擦写块都是有寿命限制的,如果频繁的擦写flash的某一个PEB,很快这个PEB就会损坏,而擦写均衡的目的就是将擦除操作平均分配到整个flash,这样就能提高flash的使用寿命。那怎样将擦除操作平均分配到整个flash呢,要达到这个条件还是有些难度的,因此我们退一步,将条件修改为PEB的最大擦写次数与最小次数的的差值小于某个值。比如flash中包含20个PEB,其中数字表示该PEB被擦写的次数,我们约定擦写次数的差值最大为15,现在flash中PEB的最小与最大擦写次数分别为10、39,由于超过门限值,因此需要我们想一些方法,增加擦写次数为10的PEB被擦写的机会,减少擦写次数为39的PEB被擦写的机会,从而使整个flash的擦写次数趋于平均

UBI文件系统初始化

函数主体是ubifs_init:
在这里插入图片描述

kmem_cache_create:申请ubifs_inode相同大小的缓冲区,而ubifs_inode是ubifs_ino_node在内存中的表现形式
register_shrinker(&ubifs_shrinker_info):注册一个内存回收的功能
在这里插入图片描述

ubifs_shrink_scan->shrink_tnc_trees用于TNC树的回收

ubifs_compressors_init:初始化压缩空间,ubi文件系统再写入文件时可能会将文件进行压缩

register_filesystem:注册文件系统

在这里插入图片描述

kill_ubifs_super:实际调用kill_anon_super(),用于卸载虚拟文件系统

格式化

对一个设备格式化第一次

如下图为格式化过程,格式化源码没找到,所以只能通过操作进行分析
在这里插入图片描述

对格式化后的块内容进行读取,发现PEB0,PEB1被写入EC,VID和空卷信息,其后的块只写入了EC头,然后数据内容全为0xff。分析可知,第一块EC头中,擦除次数为5,第二块也是5,第三块是3,第四块也是3。然后分析vid头可知,建立的是动态卷

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

对一个设备格式化第二次

第一块EC头变为了6,第二块同样是6,第三块是4,第四块也是4

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

对一个设备填充随机数后进行初始化

对设备填充入随机数,然后挨个查看其对应EC头的位置的值
在这里插入图片描述
在这里插入图片描述

格式化后如下,发现EC头的位置被完全清0了

在这里插入图片描述
在这里插入图片描述

对设备的某个块进行擦除

对刚刚进行完格式化的设备的第1块进行擦除
在这里插入图片描述

再次格式化,第一块EC头擦除计数为0,第二块及其之后的EC头数据都为1
在这里插入图片描述
在这里插入图片描述

ubi文件系统格式化做的事情
 判断是否存在EC头,如果存在则对EC头进行数据保护,然后对全块擦除成0xff,EC头中的擦除计数+1后在写入到每块的起始位置长度64,然后PEB0和PEB1写入空卷信息
 如果不存在EC头,则直接全块擦除,然后写入EC头,此时EC的擦除机数为0,然后在PEB0和PEB1写入空卷信息
 可以通过 –e 指定EC头中count的数量
 可以通过 –n指定不写入空卷信息

在这里插入图片描述

注 -n属性格式化后直接attach就会出现如下错误,无法发现卷层而附着失败
在这里插入图片描述
在这里插入图片描述

附着

属性说明
在这里插入图片描述

 -d:链接到mtd设备后创建指定num的ubi设备
 -p:通过路径链接指定mtd设备
 -m:通过mtd号链接指定mtd设备(-p,-m同时只能选一个)
 -b:给坏块预留,没1024块预留 4*num 块,默认不预留

ubi_attach过程:

实际上就是将设备中的ubi数据进行读取解析到ubi_device结构体中的过程
ctrl_cdev_ioctl

ubi_attach的过程实际是调用了ubi_ctrl设备,最终调用ctrl_cdev_ioctl函数,该函数获取通过mtd号获取mtd设备指针,然后调用ubi_attach_mtd_dev
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

case UBI_IOCATT:
{
中间部分校验过程被删掉
        struct ubi_attach_req req;
        struct mtd_info *mtd;
        dbg_gen("attach MTD device");
        
        mtd = get_mtd_device(NULL, req.mtd_num);
 
        mutex_lock(&ubi_devices_mutex);
        err = ubi_attach_mtd_dev(mtd, req.ubi_num, req.vid_hdr_offset,
                     req.max_beb_per1024);
        mutex_unlock(&ubi_devices_mutex);
        break;
}

显然,ubi_attach_mtd_dev传参的数据mtd通过mtd号获取,ubi号是直接传参,vid_offset也是可以直接通过传参的或者ubi通过ec头也能读取出来,max_beb默认为0

ubi_attach_mtd_dev
该函数在进行主要操作前会对数据进行一些判断,比如一下操作:对已经附着的设备再次附着,就会在这个进行判断并返回。

在这里插入图片描述

如果判断都ok会进行如下操作,申请一个ubi设备结构体,最后如果附着成功,还会放入全局变量ubi_devices[32]中,这个数组在mount时有大用处
在这里插入图片描述

io_init
该函数负责读取mtd信息给ubi_device结构体中,主要包括最小IO数量(一次写入擦除的大小,比如Flash最小擦除单位为页,一页大小可能为2048byte的大小),因为这个大小可能导致EC头和VID头的位置页发生变化,获取peb块大小,peb的数量,总大小,获取LEB数据起始位置,leb可用大小等等

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ubi_attach

该函数实现附着到mtd设备,主要分为4个步骤,scan_all扫描全盘,ubi_read_volume_table读取卷表信息,然后初始化wl和eba子系统
scan_all
函数主体如下:实现对全peb进行扫描,该函数主体scan_peb中具有坏块扫描,读取EC头信息,读取VID头信息,读取卷所在PEB并加入传参ai中,ai结构体在scan_all中进行了申请(struct ubi_attach_info - MTD device attaching information),实际上最后读出的ai信息最终还是赋值给了ubi_device结构体。
在这里插入图片描述
在这里插入图片描述

ubi_read_volume_table
判断是不是空卷,对卷进行解析(主要是将卷层中数据,即卷表读到LEB0和LEB1中),对卷进行初始化,进行默认的配置(都是在内存中的数据)
ubi_wl_init
初始化磨损平衡子系统,将全部ai中读取到的peb信息(使用,未使用,卷层)加入磨损平衡树
ubi_eba_init
初始化eba子系统,并创建eba表(看前面EBA子系统有说明),将leb于peb一一对应后,将该表给到卷层信息中

在这里插入图片描述
在这里插入图片描述

uif_init
当附着完成后,该函数会生成一个传参时 –d x的对应ubi_x设备。是一个典型的字符设备注册过程,其主要参数如下,其中ubi_cdev_ioctl是制作卷所需要调用的方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

卷制作

属性说明
在这里插入图片描述

 -a: alignment对齐属性,如果为1,则物理块的所有空间都可以被利用,这个字段只有在卷被创建时指定,以后无法更改,默认1
在这里插入图片描述

 -n: vol_id卷号,如果不指定,会自动进行分配,从0开始
 -s: 给改卷分配指定大小的空间,单位是Kb或者Mb
 -S:给卷分配指定逻辑块大小的空间(逻辑块大小等同于PEB块大小)
 -m:给卷分配可用的最大空间(注:-s,-S,-m同时只能选择一个)
 -t:制作成动态卷还是静态卷(静态卷只读),默认动态卷

在这里插入图片描述
在这里插入图片描述

ubi_mkvol过程:

ubimkvol -N mram -m /dev/ubi0

起始解析参数过程的源码找不到,但是解析完调用的就是ubi_cdev_ioctl,其主体是ubi_create_volume,其中verify_mkvol_req是对传参进行校验的函数
在这里插入图片描述

ubi_cdev_ioctl
该函数实现动态分配卷id(从0开始最大127),通过wl子系统找到两个适合的PEB做卷层,然后创建eba表并给到卷信息(ubi_volume),之后创建字符设备 ubix_y,再之后将卷信息部分数据给到卷表(ubi_vtbl_record)中,最后,将卷信息赋值给ubi_device中的卷信息数组里,完成卷的创建
在这里插入图片描述
在这里插入图片描述

其中ubi_change_vtbl_record是一个原子改变PEB的过程,总之就是将卷表信息赋给新的PEB,然后改变LEB和PEB的映射最后擦除原先映射过的PEB

挂载

mount的溯源:

首先溯源一下mount的流程:
系统调用sys_mount其源码如下
在这里插入图片描述

然后调用do_mount-> do_new_mount-> vfs_kern_mount-> mount_fs
从程序上看,发现传参设备名,目录名,类型,标志和操作,对比以下mount的shell命令:
mount –t ubifs ubi0_0 /home
就是传入了-t ubifs(文件系统类型),ubi0_0(设备名),/home(目录名),没有传参-o(操作)
在这里插入图片描述

通过之前的注册,已经把ubi文件系统的.mount注册进去了,所以可以mount ubi
file_system_type:
每一个文件系统驱动程序都有一个文件系统对象
调用register_filesystem注册时文件系统对象,所有文件系统对象通过struct file_system_type * next指针链接成链表,全部结构体变量static struct file_system_type *file_systems指向链表的头部。

register_filesystem在作用是把文件系统对象加入到链表中且防止重复。

在这里插入图片描述

do_new_mount:
在该函数中主要调用了两个函数:vfs_kern_mount和do_add_mount
其中do_add_mount完成将vfs_kern_mount返回得vfsmount进行实际挂载
static int do_add_mount(struct mount *newmnt, struct path *path, int mnt_flags)
因此执行到do_add_mount相当于已经挂载完成了(进行挂载点加入一个链表得过程)
所以实际找文件系统进行挂载动作是vfs_kern_mount而函数中实际执行为mount_fs
struct dentry *mount_fs(struct file_system_type *type, int flags, const char *name, void *data)
其返回的dentry中包含了超级块信息
而mount_fs中实际上进行调用的是root = type->mount(type, flags, name, data);
即最后调用了ubifs_mount

ubifs_mount:

函数原型如下:
static struct dentry *ubifs_mount(struct file_system_type *fs_type, intflags,const char *name, void *data)
该函数有四个关键点:
 open_ubi:打开ubi卷
 alloc_ubifs_info+sget:申请ubi_info,申请或查找超级块
 ubifs_fill_super:超级块数据填充与其他ubi复杂操作
 dget:返回挂载点目录
从函数中看,return dget(sb->s_root);发现最终返回挂载到得根目录。

open_ubi:

该函数实际上就是通过卷名,或者卷id等方式,获取ubi_device和卷号,即该函数就是实现了mount ubi设备的几种方式。
函数原型如下:

  • open_ubi - parse UBI device name string and open the UBI device.
  • @name: UBI volume name
  • @mode: UBI volume open mode
    static struct ubi_volume_desc *open_ubi(const char *name, int mode)

mount -t ubifs ubi0:mram_ubi /home/mram
mount -t ubifs ubi0_0 /home/mram

实际最终调用为:open_ubi-> ubi_open_volume

  • ubi_open_volume - open UBI volume.
  • @ubi_num: UBI device number
  • @vol_id: volume ID
  • @mode: open mode
    struct ubi_volume_desc *ubi_open_volume(int ubi_num, int vol_id, int mode)
    即open_ubi实现将字符串转成ubi_num和vol_id也就是ubi设备号和卷号。对于一个ubi设备,可以分成很多卷,卷表信息专门存放在卷表区中。
    以下打印就是open卷的过程
    在这里插入图片描述

其实现过程如下:
调用ubi_get_device,通过ubi_num获取对应的ubi设备
struct ubi_device *ubi_get_device(int ubi_num)
在这里插入图片描述

而ubi_device中有卷信息,在ubimkvol创建卷时候加入。然后通过如下获取卷信息
vol = ubi->volumes[vol_id];
其之所以能获取其中信息是因为 ubiattach /dev/ubi_ctrl -m 0 -d 0 时会扫描全盘信息,将数据读取到内存中,在上面附着的过程中提到过

程序中可以看到ubi设备最大可以有32个,一个ubi设备最大可以有128个卷
在这里插入图片描述

alloc_ubifs_info 与 sget

为描述UBIFS文件系统的结构体struct ubifs_info申请内存,并且初始化自旋锁、互斥锁、链表、树节点等操作

Sget首先在该文件系统中查找超级块,如果没有找到则调用alloc_super创建一个超级块,并初始化超级块的相关成员,该函数并未定义于ubifs文件目录内
实际调用关系如下: sget ->sget_userns,然后在sget_userns判断是否有超级块,没有就申请一个然后在进行判断,如果成了,就返回该超级块
在这里插入图片描述

ubifs_fill_super:

mount的核心步骤,填充ubi文件系统的超级块。在上一步sget获取了超级块之后,判断这个文件系统有没有被挂载过,如果挂载过,就注销之前申请的内存和链表,如果没挂载就执行ubifs_fill_super函数。
在这里插入图片描述

bdi设备简介
在ubifs_fill_super函数涉及到了bdi设备,即是backing device info的缩写。
bdi,备用存储设备,简单点说就是能够用来存储数据的设备,而这些设备存储的数据能够保证在计算机电源关闭时也不丢失。硬盘存储设备就是所谓的备用存储设备

相对于内存来说,bdi设备(比如最常见的硬盘存储设备)的读写速度是非常慢的,因此为了提高系统整体性能,Linux系统对bdi设备的读写内容进行了缓冲,那些读写的数据会临时保存在内存里,以避免每次都直接操作bdi设备,但这就需要在一定的时机(比如每隔5秒、脏数据达到的一定的比率等)把它们同步到bdi设备,否则长久的呆在内存里容易丢失(比如机器突然宕机、重启)

在Linux-3.2新内核中,page cache和buffer cache的刷新机制发生了改变。放弃了原有的pdflush机制,改成了bdi_writeback机制。这种变化主要解决原有pdflush机制存在的一个问题:在多磁盘的系统中,pdflush管理了所有磁盘的page/buffer cache,从而导致一定程度的IO性能瓶颈。bdi_writeback机制为每个磁盘都创建一个线程,专门负责这个磁盘的page cache或者buffer cache的数据刷新工作,从而实现了每个磁盘的数据刷新程序在线程级的分离,这种处理可以提高IO性能。

在Linux内核中有一个常驻内存的线程bdi_forker_thread,该线程负责为bdi_object创建writeback线程,同时检测如果writeback线程长时间处于空闲状态,bdi_forker_thread线程便会将其进行销毁。bdi_forker_thread在系统中只有一个,其会被定时唤醒,检查全局链表bdi_list队列中是否存在dirty的数据需要刷新到磁盘。如果存在dirty数据并且对应bdi的writeback线程还没有被创建,bdi_forker_thread会为该bdi创建一个writeback的线程进行写回操作。
在这里插入图片描述

超级块相关初始化:

在这里插入图片描述
在这里插入图片描述

主要将ubi私有结构体指针赋值到超级块中,并定义了超级块的ubi的操作函数。

mount_ubifs:
init_constants_early:
此函数用于初始化ubifs_info中的一些常量。它还检查UBI卷是否满足基本UBIFS要求。成功时返回零,失败时返回负错误代码。检查包括如下信息:
 LEB大小需要大于UBIFS_MIN_LEB_SZ (15*1024)
 LEB块数量需要大于UBIFS_MIN_LEB_CNT (UBIFS_SB_LEBS + UBIFS_MST_LEBS + UBIFS_MIN_LOG_LEBS + UBIFS_MIN_LPT_LEBS + UBIFS_MIN_ORPH_LEBS + UBIFS_MIN_MAIN_LEBS)即17
 UBIFS_SB_LEBS(LEB0):为超级块区域预留的大小,数值为1
 UBIFS_MST_LEBS(LEB1-2):为主区域预留的大小,数值为2
 UBIFS_MIN_LOG_LEBS(LEB3-4):为日志预留的大小,数值为2
 UBIFS_MIN_LPT_LEBS(LEB5-6):为LPT区预留的大小,数值为2
 UBIFS_MIN_ORPH_LEBS(LEB7):为孤立区预留的大小,数值为1
 UBIFS_MIN_MAIN_LEBS(LEB8+):主逻辑区大小,主区域逻辑块的最小数量(3个用于索引,1个用于GC,1个用于删除,至少1个用于提交的数据)数值为3+6
 一次性最大写入数量必须是IO写入的整数倍
 IO写入数在ubi中会被自动改成8,即一次mtd写入最小8字节

也就是说,存储介质必须大于255K才能使用UBI

check_volume_empty:
在这里插入图片描述

该函数通过判断eba映射情况,判断卷是否为空。其流程为check_volume_empty –> ubifs_is_mapped -> ubi_is_mapped -> ubi_eba_is_mapped
在刚格式化结束,附着后进行的第一次挂载,eba中是没有关联信息的,所以会从头到尾(被预留使用的leb全大小)的判断map情况(eba映射情况),而空卷则会在下一步进行超级块的创建
[166967.844297] UBI DBG gen (pid 538): test LEB 0:0
[166967.850015] UBI DBG gen (pid 538): test LEB 0:1
中间略
[166968.425462] UBI DBG gen (pid 538): test LEB 0:122
[166968.430276] UBI DBG gen (pid 538): test LEB 0:123
而在第二次挂载,由于超级块处于leb0的位置,所以检测map时,找第一块就会直接退出,即判断出非空卷
在这里插入图片描述

ubifs_read_superblock:
读取超级块操作,如果在上一步发现是个空卷,即第一次进行mount的设备,会进入如下函数进行超级块和其他初始化,否则直接读取超级块节点信息
create_default_filesystem
该函数会计算日志区大小(默认占用总LEB数量的5%,即LEB=128时,日志区大小为6,当然,如果一块LEB大小足够大时可以小于这个默认百分比,日志区最大占用32M的大小)
然后计算LOG区大小孤立区大小,lpt区大小,主分区大小等等,再然后创建LPT区,再然后在LEB0写入超级块(ubifs_sb_node)信息,在主节点区写入主节点(ubifs_mst_node)信息。最后在主分区的第一块写入根节点的索引节点(ubifs_idx_node)信息,在主分区的第二块写入inode(ubifs_ino_node)作为根节点

ubifs_sb_node
struct ubifs_sb_node {
    struct ubifs_ch ch;
    __u8 padding[2];
    __u8 key_hash;
    __u8 key_fmt;
    __le32 flags;
    __le32 min_io_size;
    __le32 leb_size;
    __le32 leb_cnt;
    __le32 max_leb_cnt;
    __le64 max_bud_bytes;
    __le32 log_lebs;
    __le32 lpt_lebs;
    __le32 orph_lebs;
    __le32 jhead_cnt;
    __le32 fanout;
    __le32 lsave_cnt;
    __le32 fmt_version;
    __le16 default_compr;
    __u8 padding1[2];
    __le32 rp_uid;
    __le32 rp_gid;
    __le64 rp_size;
    __le32 time_gran;
    __u8 uuid[16];
    __le32 ro_compat_version;
    __u8 padding2[3968];
} __packed;

在这里插入图片描述

使用mtd_debug read命令读出数据到一个文件并od查看数据如下,可以分析出超级块节点长度0x1000,node_type=6,一次IO读写大小8,leb可用大小0xff80=65408与程序中完全相符

在这里插入图片描述
在这里插入图片描述

ubifs_mst_node:
在建立超级块程序中会向主节点区写入主节点信息,长度512

struct ubifs_mst_node {
    struct ubifs_ch ch;
    __le64 highest_inum;
    __le64 cmt_no;
    __le32 flags;
    __le32 log_lnum;
    __le32 root_lnum; LEB number of the root indexing node
    __le32 root_offs; offset within @root_lnum
    __le32 root_len; root indexing node length
    __le32 gc_lnum;
    __le32 ihead_lnum; LEB number of index head
    __le32 ihead_offs; offset of index head
    __le64 index_size; size of index on flash
    __le64 total_free;
    __le64 total_dirty;
    __le64 total_used;
    __le64 total_dead;
    __le64 total_dark;
    __le32 lpt_lnum;
    __le32 lpt_offs;
    __le32 nhead_lnum; LEB number of LPT head
    __le32 nhead_offs; offset of LPT head
    __le32 ltab_lnum;
    __le32 ltab_offs;
    __le32 lsave_lnum;
    __le32 lsave_offs;
    __le32 lscan_lnum;
    __le32 empty_lebs;
    __le32 idx_lebs;
    __le32 leb_cnt;
    __u8 padding[344];
} __packed;

在这里插入图片描述

ubifs_ino_node:

struct ubifs_ino_node {
 * @ch: common header
 * @key: node key
 * @creat_sqnum: sequence number at time of creation
 * @size: inode size in bytes (amount of uncompressed data)
 * @atime_sec: access time seconds
 * @ctime_sec: creation time seconds
 * @mtime_sec: modification time seconds
 * @atime_nsec: access time nanoseconds
 * @ctime_nsec: creation time nanoseconds
 * @mtime_nsec: modification time nanoseconds
 * @nlink: number of hard links
 * @uid: owner ID
 * @gid: group ID
 * @mode: access flags
 * @flags: per-inode flags (%UBIFS_COMPR_FL, %UBIFS_SYNC_FL, etc)
 * @data_len: inode data length
 * @xattr_cnt: count of extended attributes this inode has
 * @xattr_size: summarized size of all extended attributes in bytes
 * @padding1: reserved for future, zeroes
 * @xattr_names: sum of lengths of all extended attribute names belonging to
 *               this inode
 * @compr_type: compression type used for this inode
 * @padding2: reserved for future, zeroes
 * @data: data attached to the inode
} __packed;

ubifs_read_master:
此函数用于在文件系统装载期间查找和读取主节点。如果闪存为空,它也会创建默认主节点。成功时返回零,失败时返回负错误代码。如下为查找主节点
在这里插入图片描述

从程序上看,分别从两个主节点分区进行读取,然后比较是否相同

在这里插入图片描述

ubifs_lpt_init:
LPT树初始化,给lpt树申请节点所用空间

在这里插入图片描述

ubifs_replay_journal:
此函数用于扫描日志、重播和清理日志。它确保构建与未提交日志相关的所有内存数据结构
[ 41.861054] UBIFS DBG mnt (pid 325): start replaying the journal
[ 41.867104] UBIFS DBG mnt (pid 325): replay log LEB 5:0
[ 41.872356] UBIFS DBG scan (pid 325): scan LEB 5:0
[ 41.877189] UBI DBG gen (pid 325): read 65408 bytes from LEB 0:5:0
[ 41.883401] UBI DBG eba (pid 325): read 65408 bytes from offset 0 of LEB 0:5, PEB 59
[ 41.891200] UBI DBG io (pid 325): read 65408 bytes from PEB 59:128
[ 41.962918] UBIFS DBG scan (pid 325): look at LEB 5:0 (65408 bytes left)
[ 41.969675] UBIFS DBG scan (pid 325): scanning commit start node at LEB 5:0
[ 41.976691] UBIFS DBG scan (pid 325): look at LEB 5:32 (65376 bytes left)
[ 41.983519] UBIFS DBG scan (pid 325): scanning reference node at LEB 5:32
[ 41.990372] UBIFS DBG scan (pid 325): look at LEB 5:96 (65312 bytes left)
[ 41.997216] UBIFS DBG scan (pid 325): scanning reference node at LEB 5:96
[ 42.004039] UBIFS DBG scan (pid 325): look at LEB 5:160 (65248 bytes left)
[ 42.010967] UBIFS DBG scan (pid 325): hit empty space at LEB 5:160
[ 42.017195] UBIFS DBG scan (pid 325): stop scanning LEB 5 at offset 160
[ 42.023991] UBIFS DBG mnt (pid 325): commit start sqnum 43
[ 42.029531] UBIFS DBG mnt (pid 325): add replay bud LEB 11:1088, head 1
[ 42.036204] UBIFS DBG log (pid 325): LEB 11:1088, jhead 1 (base), bud_bytes 64320
[ 42.043723] UBIFS DBG mnt (pid 325): add replay bud LEB 13:96, head 2
[ 42.050217] UBIFS DBG log (pid 325): LEB 13:96, jhead 2 (data), bud_bytes 129632
[ 42.057666] UBIFS DBG mnt (pid 325): replay log LEB 3:0
[ 42.062916] UBIFS DBG scan (pid 325): scan LEB 3:0
[ 42.067751] UBI DBG gen (pid 325): read 65408 bytes from LEB 0:3:0
[ 42.073967] UBI DBG eba (pid 325): read 65408 bytes from offset 0 of LEB 0:3 (unmapped)
[ 42.082056] UBIFS DBG scan (pid 325): look at LEB 3:0 (65408 bytes left)
[ 42.088848] UBIFS DBG scan (pid 325): hit empty space at LEB 3:0
[ 42.094886] UBIFS DBG scan (pid 325): stop scanning LEB 3 at offset 0
[ 42.101539] UBIFS DBG mnt (pid 325): replay bud LEB 11, head 1, offs 1088, is_last 1
[ 42.109345] UBIFS DBG rcvry (pid 325): 11:1088, jhead 1, grouped 1
[ 42.115577] UBIFS DBG scan (pid 325): scan LEB 11:1088
[ 42.120743] UBI DBG gen (pid 325): read 64320 bytes from LEB 0:11:1088
[ 42.127327] UBI DBG eba (pid 325): read 64320 bytes from offset 1088 of LEB 0:11, PEB 57
[ 42.135476] UBI DBG io (pid 325): read 64320 bytes from PEB 57:1216
[ 42.206175] UBIFS DBG scan (pid 325): look at LEB 11:1088 (64320 bytes left)
[ 42.213265] UBIFS DBG scan (pid 325): hit empty space at LEB 11:1088
[ 42.219683] UBIFS DBG rcvry (pid 325): found corruption (-1) at 11:1088
[ 42.226629] UBIFS DBG rcvry (pid 325): cleaning corruption at 11:1088
[ 42.233129] UBIFS DBG scan (pid 325): stop scanning LEB 11 at offset 1088
[ 42.239976] UBIFS DBG rcvry (pid 325): fixing LEB 11 start 1088 endpt 1088
[ 42.246904] UBI DBG gen (pid 325): read 1088 bytes from LEB 0:11:0
[ 42.253119] UBI DBG eba (pid 325): read 1088 bytes from offset 0 of LEB 0:11, PEB 57
[ 42.260917] UBI DBG io (pid 325): read 1088 bytes from PEB 57:128
[ 42.268176] UBI DBG gen (pid 325): atomically write 1088 bytes to LEB 0:11
[ 42.275122] UBI DBG eba (pid 325): change LEB 0:11
[ 42.279945] UBI DBG wl (pid 325): PEB 60 EC 0
[ 42.284324] UBI DBG wl (pid 325): added PEB 60 EC 0 to the protection queue
[ 42.291340] UBI DBG eba (pid 325): write VID hdr and 1088 bytes at offset 0 of LEB 0:11, PEB 60
[ 42.300101] UBI DBG io (pid 325): write VID header to PEB 60
[ 42.305806] UBI DBG io (pid 325): write 64 bytes to PEB 60:64
[ 42.311713] UBI DBG io (pid 325): write 1088 bytes to PEB 60:128
[ 42.319097] UBI DBG wl (pid 325): PEB 57
[ 42.323044] UBI DBG wl (pid 325): schedule erasure of PEB 57, EC 0, torture 0
[ 42.330252] UBI DBG wl (pid 324): erase PEB 57 EC 0 LEB 0:11
[ 42.335964] UBI DBG wl (pid 324): erase PEB 57, old EC 0
[ 42.335999] UBIFS DBG mnt (pid 325): bud LEB 11 replied: dirty 0, free 64320
[ 42.336005] UBIFS DBG mnt (pid 325): replay bud LEB 13, head 2, offs 96, is_last 1
[ 42.336010] UBIFS DBG rcvry (pid 325): 13:96, jhead 2, grouped 1
[ 42.336014] UBIFS DBG scan (pid 325): scan LEB 13:96
[ 42.336019] UBI DBG gen (pid 325): read 65312 bytes from LEB 0:13:96
[ 42.336026] UBI DBG eba (pid 325): read 65312 bytes from offset 96 of LEB 0:13, PEB 58
[ 42.336030] UBI DBG io (pid 325): read 65312 bytes from PEB 58:224
[ 42.387604] UBI DBG io (pid 324): erase PEB 57
[ 42.402557] UBIFS DBG scan (pid 325): look at LEB 13:96 (65312 bytes left)
[ 42.409517] UBIFS DBG scan (pid 325): hit empty space at LEB 13:96
[ 42.416036] UBIFS DBG rcvry (pid 325): found corruption (-1) at 13:96
[ 42.422788] UBIFS DBG rcvry (pid 325): cleaning corruption at 13:96
[ 42.429416] UBIFS DBG scan (pid 325): stop scanning LEB 13 at offset 96
[ 42.436250] UBIFS DBG rcvry (pid 325): fixing LEB 13 start 96 endpt 96
[ 42.442812] UBI DBG gen (pid 325): read 96 bytes from LEB 0:13:0
[ 42.449107] UBI DBG eba (pid 325): read 96 bytes from offset 0 of LEB 0:13, PEB 58
[ 42.456887] UBI DBG io (pid 325): read 96 bytes from PEB 58:128
[ 42.479851] UBI DBG wl (pid 324): erased PEB 57, new EC 1
[ 42.479991] UBI DBG gen (pid 325): atomically write 96 bytes to LEB 0:13
[ 42.480004] UBI DBG eba (pid 325): change LEB 0:13
[ 42.480011] UBI DBG wl (pid 325): PEB 61 EC 0
[ 42.480015] UBI DBG wl (pid 325): added PEB 61 EC 0 to the protection queue
[ 42.480021] UBI DBG eba (pid 325): write VID hdr and 96 bytes at offset 0 of LEB 0:13, PEB 61
[ 42.480024] UBI DBG io (pid 325): write VID header to PEB 61
[ 42.480030] UBI DBG io (pid 325): write 64 bytes to PEB 61:64
[ 42.480156] UBI DBG io (pid 325): write 96 bytes to PEB 61:128
[ 42.480316] UBI DBG wl (pid 325): PEB 58
[ 42.480322] UBI DBG wl (pid 325): schedule erasure of PEB 58, EC 0, torture 0
[ 42.480332] UBIFS DBG mnt (pid 325): bud LEB 13 replied: dirty 0, free 65312
[ 42.480342] UBIFS DBG lp (pid 325): LEB 11, free 64320, dirty 928, flags 3
[ 42.480347] UBIFS DBG lp (pid 325): LEB 11, free 64320, dirty 928, flags 19
[ 42.480352] UBIFS DBG lp (pid 325): LEB 11, free 64320, dirty 928, flags 3
[ 42.480363] UBIFS DBG io (pid 325): LEB 11:1088, jhead 1 (base)
[ 42.480369] UBIFS DBG lp (pid 325): LEB 13, free 65312, dirty 96, flags 5
[ 42.480373] UBIFS DBG lp (pid 325): LEB 13, free 65312, dirty 96, flags 21
[ 42.480378] UBIFS DBG lp (pid 325): LEB 13, free 65312, dirty 96, flags 5
[ 42.480382] UBIFS DBG io (pid 325): LEB 13:96, jhead 2 (data)
[ 42.480388] UBIFS DBG mnt (pid 325): finished, log head LEB 5:160, max_sqnum 46, highest_inum 65

挂载后的一些操作

目录操作
首先需要说明一下,在之前创建根节点的时候,定义了根节点的mode,是一个目录,然后再挂载填充超级块的时候会从根节点读取其属性,这样就将目录相关操作读取出来了
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

然后分析ls所需要操作的流程:ubifs_readdir -> ubifs_tnc_next_ent -> ubifs_lookup_level0 -> ubifs_search_zbranch + ubifs_load_znode(如果没在内存中缓存过就需要读取)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当出现pos=2即说明已经查找完毕了
同时通过ls能够看出tnc树下znode节点分支模式,一个znode最多可以挂8个分支,而这里已经存在9个,因此这9个文件分别挂在了两个znode下

在这里插入图片描述

文件数据读写
在这里插入图片描述

下图为写入数据然后同步(调用fsync的过程)
在这里插入图片描述

UBI自带调试方法

Ubi文件系统在创建时会使用debugfs创建一些能够打印数据的目录和文件,因此,利用debugfs可以将数据进行打印,使用方法如下:
mount -t debugfs none /your/debugfs/dir
先将debugfs进行挂载,因为目前使用的内核不会自动进行挂载,挂载后就可以发现ubifs和ubi两个可以调试的目录,ubifs可以打印出和文件系统相关内容,ubi则偏向于设备和io

在这里插入图片描述

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜暝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值