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来进行串行化保护,其具体流程如下:
- 读取leb数据(ubifs内完成)
- 检查写数据长度是否为0,为0时,unmap leb
- 分配初始化vid_hdr
- 分配新的peb(ubi_wl_get_peb)
- 新peb中写入vid_hdr
- 新peb中写入老leb数据+新增数据
- 回收老的peb(ubi_wl_put_peb)
- 更新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 标签:
3968连接器