Do not panic!
分类: LINUX
2015-09-24 08:35:21
原文地址:UBIFS文件系统分析(二):通过VFS的读写流程 作者:mournjust
通过VFS的写流程
断断续续的看ubifs这么久了,感觉越看越乱,所以想先从VFS的读写接口开始慢慢的扩展一下。
const struct file_operations ubifs_file_operations = {
.llseek = generic_file_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = generic_file_aio_read,
.aio_write = ubifs_aio_write,
.mmap = ubifs_file_mmap,
.fsync = ubifs_fsync,
.unlocked_ioctl = ubifs_ioctl,
.splice_read = generic_file_splice_read,
.splice_write = generic_file_splice_write,
#ifdef CONFIG_COMPAT
#endif
};
其中ubifs_aio_write的代码很短。
static ssize_t ubifs_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t pos)
{
int err;
ssize_t ret;
struct inode *inode = iocb->ki_filp->f_mapping->host;
struct ubifs_info *c = inode->i_sb->s_fs_info;
err = update_mctime(c, inode);
if (err)
return err;
ret = generic_file_aio_write(iocb, iov, nr_segs, pos);
if (ret < 0)
return ret;
if (ret > 0 && (IS_SYNC(inode) || iocb->ki_filp->f_flags & O_SYNC)) {
err = ubifs_sync_wbufs_by_inode(c, inode);
if (err)
return err;
}
return ret;
}
对于异步的情况,直接调用,generic_file_aio_write将数据写入到缓冲区中,由后台进程来具体的将数据写入的flash media中去。对于采用sync模式挂载的情况,就不是由后台进程来讲数据刷新到flash media中去了,而是直接调用ubifs_sync_wbufs_by_inode来讲数据直接写入到flash media中去。
下面具体的看一下这个函数的代码:
int ubifs_sync_wbufs_by_inode(struct ubifs_info *c, struct inode *inode)
{
int i, err = 0;
for (i = 0; i < c->jhead_cnt; i++) {
struct ubifs_wbuf *wbuf = &c->jheads[i].wbuf;
if (i == GCHD)
/*
* GC head is special, do not look at it. Even if the
* head contains something related to this inode, it is
* a _copy_ of corresponding on-flash node which sits
* somewhere else.
*/
continue;
if (!wbuf_has_ino(wbuf, inode->i_ino))
continue;
mutex_lock_nested(&wbuf->io_mutex, wbuf->jhead);
if (wbuf_has_ino(wbuf, inode->i_ino))
err = ubifs_wbuf_sync_nolock(wbuf);
mutex_unlock(&wbuf->io_mutex);
if (err) {
ubifs_ro_mode(c, err);
return err;
}
}
return 0;
}
该函数通过调用wbuf_has_ino来判断这些inode中是否存在数据。在获得wbuf的信号了的情况,接着调用ubifs_wbuf_sync_nolock来sync数据。
在struct ubifs_info中存在这样几个成员:
int jhead_cnt;
struct ubifs_jhead *jheads;
是用于日志文件系统的管理的。在
/fs/ubifs/ubifs-media.h中有这样的宏定义:
/* Garbage collector journal head number */
#define UBIFS_GC_HEAD 0
/* Base journal head number */
#define UBIFS_BASE_HEAD 1
/* Data journal head number */
#define UBIFS_DATA_HEAD 2
这是ubifs中用于管理的三种不同目的的缓冲区,分别用于垃圾回收, Journal head used for non-data nodes.和数据读写这三种用途。
struct ubifs_jhead {
struct ubifs_wbuf wbuf;
struct list_head buds_list;
};
struct ubifs_wbuf {
struct ubifs_info *c;
void *buf;//具体的分配用来缓冲数据的空间
int lnum;//缓冲的是哪一个flash块
int offs;//缓冲的数据的位移,也就是这一块中,offs之前位置的数据被缓冲在wbuf中。
int avail;//缓冲区的可用字节数
int used;//缓冲区中已用字节数
int dtype;// type of data stored in this LEB (%UBI_LONGTERM, %UBI_SHORTTERM,
* %UBI_UNKNOWN)
int jhead;
int (*sync_callback)(struct ubifs_info *c, int lnum, int free, int pad);
struct mutex io_mutex;
spinlock_t lock;
ktime_t softlimit;
unsigned long long delta;
struct hrtimer timer;
unsigned int no_timer:1;
unsigned int need_sync:1;
int next_ino;
ino_t *inodes;//缓冲区中数据的host
};
从注释中可以看出struct ubifs_wbuf - UBIFS write-buffer.,这个wbuf是ubifs层的一个缓冲区,我们慢慢来看这个缓冲区是怎么实现的。
int ubifs_wbuf_sync_nolock(struct ubifs_wbuf *wbuf)
{
struct ubifs_info *c = wbuf->c;
int err, dirt;
cancel_wbuf_timer_nolock(wbuf);
//取消wbuf的定时器,因为在后台进程中通过定时器的定期刷新数据
if (!wbuf->used || wbuf->lnum == -1)
/* Write-buffer is empty or not seeked */
return 0;
dbg_io("LEB %d:%d, %d bytes, jhead %s",
wbuf->lnum, wbuf->offs, wbuf->used, dbg_jhead(wbuf->jhead));
ubifs_assert(!(c->vfs_sb->s_flags & MS_RDONLY));
ubifs_assert(!(wbuf->avail & 7));
ubifs_assert(wbuf->offs + c->min_io_size <= c->leb_size);
if (c->ro_media)
return -EROFS;
ubifs_pad(c, wbuf->buf + wbuf->used, wbuf->avail);
调用ubi_leb_write来对LEB(逻辑块)进行读写
err = ubi_leb_write(c->ubi, wbuf->lnum, wbuf->buf, wbuf->offs,
c->min_io_size, wbuf->dtype);
if (err) {
ubifs_err("cannot write %d bytes to LEB %d:%d",
c->min_io_size, wbuf->lnum, wbuf->offs);
dbg_dump_stack();
return err;
}
dirt = wbuf->avail;
spin_lock(&wbuf->lock);
wbuf->offs += c->min_io_size;
wbuf->avail = c->min_io_size;
wbuf->used = 0;
wbuf->next_ino = 0;
spin_unlock(&wbuf->lock);
if (wbuf->sync_callback)
err = wbuf->sync_callback(c, wbuf->lnum,
c->leb_size - wbuf->offs, dirt);
return err;
}
这个函数的主体是调用UBI层的ubi_leb_write函数来将数据写入flash中。
ubi_leb_write的函数调用关系:
->ubi_leb_write(对逻辑块进行读写)
->ubi_eba_write_leb(内核中每一个volume都维护一个vol->eba_tbl的数组,其中是关于逻辑块与物理块之间的映射关系,这些映射关系同时保持在实际物理介质中的VID header中,在ubiattach的时候,建立这样的eba_tbl)
->ubi_io_write_data
->ubi_io_write
->ubi->mtd->write(ubi->mtd, addr, len, &written, buf);
这儿可以看出UBI是构建在MTD层之上的。UBI的读写之后调用了MTD层的读写。
上面的generic_file_aio_write刚才没有分析,这个函数比较复杂,在此之前我们先看一个ubifs中address_space_operations类型的一个关于内核缓冲区的操作结构体。
const struct address_space_operations ubifs_file_address_operations = {
.readpage = ubifs_readpage,
.writepage = ubifs_writepage,
.write_begin = ubifs_write_begin,
.write_end = ubifs_write_end,
.invalidatepage = ubifs_invalidatepage,
.set_page_dirty = ubifs_set_page_dirty,
.releasepage = ubifs_releasepage,
};
->generic_file_aio_write
->__generic_file_aio_write
->generic_file_buffered_write
->generic_perform_write
->ubifs_write_begin
->ubifs_write_end
这儿有一个关于write_begin和write_end的资料:
generic_file_aio_write函数结束的时候,整个写过程也就结束了,到这儿的时候,数据已经被写入了buffer_head中去了,等待内核线程pdflush发现radix树上的脏页,并最终调用ubifs_writepages。
关于ubifs_writepages,作者有一段注释,大意是说在VFS中,是先写入属于inode的数据,最后才写入inode节点的。但是对ubifs这样的日志文件系统就可能存在问题。设想存在下面的情况:一个原来长度为0的inode节点,现在想往该节点写入数据,ubifs提交日志,最终完成了写操作。在没有写入inode之前发生了一次unclear reboot,这时候重新启动的时候就会发现该inode节点还是0字节,但是数据已经写入了,占用了flash media。所以这部分空间就没办法释放了。为了避免这种情况,需要在ubifs中先写入inode节点,然后再用log的形式写入数据,这时候即使发生unclear reboot,由于提交了日志,所以数据还是可以恢复的。
static int ubifs_writepage(struct page *page, struct writeback_control *wbc)
{
struct inode *inode = page->mapping->host;
struct ubifs_inode *ui = ubifs_inode(inode);
loff_t i_size = i_size_read(inode), synced_i_size;
pgoff_t end_index = i_size >> PAGE_CACHE_SHIFT;
int err, len = i_size & (PAGE_CACHE_SIZE - 1);
void *kaddr;
dbg_gen("ino %lu, pg %lu, pg flags %#lx",
inode->i_ino, page->index, page->flags);
ubifs_assert(PagePrivate(page));
if (page->index > end_index || (page->index == end_index && !len)) {
err = 0;
goto out_unlock;
}
spin_lock(&ui->ui_lock);
synced_i_size = ui->synced_i_size;
spin_unlock(&ui->ui_lock);
/* Is the page fully inside @i_size? */
if (page->index < end_index) {
if (page->index >= synced_i_size >> PAGE_CACHE_SHIFT) {
err = inode->i_sb->s_op->write_inode(inode, 1);
if (err)
goto out_unlock;
/*
* The inode has been written, but the write-buffer has
* not been synchronized, so in case of an unclean
* reboot we may end up with some pages beyond inode
* size, but they would be in the journal (because
* commit flushes write buffers) and recovery would deal
* with this.
*/
}
return do_writepage(page, PAGE_CACHE_SIZE);
}
/*
* The page straddles @i_size. It must be zeroed out on each and every
* writepage invocation because it may be mmapped. "A file is mapped
* in multiples of the page size. For a file that is not a multiple of
* the page size, the remaining memory is zeroed when mapped, and
* writes to that region are not written out to the file."
*/
kaddr = kmap_atomic(page, KM_USER0);
memset(kaddr + len, 0, PAGE_CACHE_SIZE - len);
flush_dcache_page(page);//将Dcache中的数据刷回内存中
kunmap_atomic(kaddr, KM_USER0);
if (i_size > synced_i_size) {
err = inode->i_sb->s_op->write_inode(inode, 1);
if (err)
goto out_unlock;
}
return do_writepage(page, len);
out_unlock:
unlock_page(page);
return err;
}
首先调用inode->i_sb->s_op->write_inode(inode, 1);将inode节点写入flash media中去,接着调用do_writepage将在page中的数据写入flash media中,在do_writepage中调用ubifs_jnl_write_data来进行日志文件系统的写操作。该函数首先将数据拷贝到wbuf中,由后台进程来进行些操作。但是日志文件系统是怎么保证unclear reboot的recovery工作的呢?
我们ubifs_add_bud_to_log(bud -----An eraseblock used by the journal)就可以看到,每次找到一块可用块将其添加到wbuf的时候,都会在日志中(ubifs在flash中保持一定的块数用于日志的目的)写入一个REF的节点。一个ref结点代表的是journal中的一个LEB,可以称之为bud。那么log中记录的就是在前一次commit之后,下一次commit之前,我们的写操作涉及到的LEB。所以我们可以理解为什么struct ubifs_jhead结构的成员中会有struct ubifs_wbuf wbuf;
struct ubifs_jhead {
struct ubifs_wbuf wbuf;
struct list_head buds_list;
};因为wbuf差不多是journal的一部分,wbuf中缓冲的是将要写入到journal中的数据。
Ubifs中一共分为六个区域:superblock,master node,the log area,the lpt area,the orphan area 和the main area.其中master node中记载着idx树的根节点,an index node records the onflash position of its child nodes,the UBIFS wandering tree can be viewed as having two parts.A top part consisting of index nodes that create the structure of the tree, and a bottom part consisting of leaf nodes that hold the actual file data。因为wandering tree是保存在flash上的,所以在进行数据更新的时候就必然需要更新wandering tree,频繁的数据更新显然会降低文件系统的性能,所以采用了journal。Log是journal的一部分,是为了防止在flash media中频繁的更新idx树而降低文件系统效率。Ubifs将需要更新的信息写入log中,然后在提交的时候一起更新,从而降低了wandering tree的更新频率。
Ubifs文件系统在对一个文件进行修改的时候,它会将修改的数据写入到一个新块中,然后将LNUM指向该新页,将原来LNUM指向的PNUM擦除掉。所以在修改的过程中发生unclear reboot的时候,在重新启动的时候就会发现有两个PNUM指向同一个LNUM,这就说明发生了错误。同时旧的PNUM中的数据没有擦除掉,很容易恢复。
通过VFS的读流程
从上面的文件操作结构体可以看出,UBIFS对于VFS的接口函数为generic_file_aio_read。
下面来看一下函数之间的调用关系:
generic_file_aio_read:
->do_generic_file_read
->readpage
这儿的readpage的函数指针指向ubifs_readpage。
ubifs_readpage的代码清单:
static int ubifs_readpage(struct file *file, struct page *page)
{
if (ubifs_bulk_read(page))
return 0;
do_readpage(page);
unlock_page(page);
return 0;
}
上面的代码很简短。
static int ubifs_bulk_read(struct page *page)
{
struct inode *inode = page->mapping->host;
struct ubifs_info *c = inode->i_sb->s_fs_info;
struct ubifs_inode *ui = ubifs_inode(inode);
pgoff_t index = page->index, last_page_read = ui->last_page_read;
struct bu_info *bu;
int err = 0, allocated = 0;
ui->last_page_read = index;
if (!c->bulk_read)
return 0;
/*
* Bulk-read is protected by @ui->ui_mutex, but it is an optimization,
* so don't bother if we cannot lock the mutex.
*/
if (!mutex_trylock(&ui->ui_mutex))
return 0;
if (index != last_page_read + 1) {
ui->read_in_a_row = 1;
if (ui->bulk_read)
ui->bulk_read = 0;
goto out_unlock;
}
if (!ui->bulk_read) {
ui->read_in_a_row += 1;
if (ui->read_in_a_row < 3)
goto out_unlock;
ui->bulk_read = 1;
}
/*
* If possible, try to use pre-allocated bulk-read information, which
* is protected by @c->bu_mutex.
*/
if (mutex_trylock(&c->bu_mutex))
bu = &c->bu;
else {
bu = kmalloc(sizeof(struct bu_info), GFP_NOFS | __GFP_NOWARN);
if (!bu)
goto out_unlock;
bu->buf = NULL;
allocated = 1;
}
bu->buf_len = c->max_bu_buf_len;
data_key_init(c, &bu->key, inode->i_ino,
page->index << UBIFS_BLOCKS_PER_PAGE_SHIFT);
err = ubifs_do_bulk_read(c, bu, page);
if (!allocated)
mutex_unlock(&c->bu_mutex);
else
kfree(bu);
out_unlock:
mutex_unlock(&ui->ui_mutex);
return err;
}
在ubifs中数据都是以LEB的形式的组织的,ubifs层的基本读写也是块读写。所以在ubifs中必然出现很多文件的尾部只占用了一整块的一小部分。当对这文件添加内容的时候,就会以另外一个DATA_NODE的形式附加在后面,这样就会在某一个块中出现在物理上连续,属于同一个节点的不同的DATA_NODE形式的ubifs节点。
ubifs_bulk_read来擦看ubifs是否支持bulk_read ,如果支持,那么就执行bulk-read操作,在ubifs中,存在一个TNC树(tree node cache),里面保持的是在内存中的inode树。首先ubifs_bulk_read通过key来从TNC树中查找到znode,bulk-read的相关信息保持在znode 的zbranch数组中。
为什么要采用bulk操作,而不是直接在添加数据到文件的tail后面?因为ubifs中对数据进行了压缩,所以数据不能直接添加的,需要压缩之后以另外一个DATA_NODE的形式接在后面。
上面的两种方式最终都调用到了ubi_leb_read。注意这儿的的名字中leb,leb是logical erase block ,是UBI层虚拟的逻辑块,逻辑块与物理上的块是一一对应的,ubi中是如何实现虚拟块与物理块之间的映射关系的呢?
子系统
在ubifs中,存在两个头部分别用来进行ubifs的管理,分别为EC(erase header)和VID(volumne identifier header),看名字可以大体擦除这些头部的用途,其中EC是用来统计erase 次数的,用于均衡损耗。VIDheader用于LEB和PEB之间的映射。
struct ubi_vid_hdr {
ubi32_t magic;
uint8_t version;
uint8_t vol_type;
uint8_t copy_flag;
uint8_t compat;
ubi32_t vol_id;//属于哪一个volumn
ubi32_t lnum;//该peb属于哪一个leb
ubi32_t leb_ver;
ubi32_t data_size;
ubi32_t used_ebs;
ubi32_t data_pad;
ubi32_t data_crc;
uint8_t padding1[12];
uint8_t ivol_data[UBI_VID_HDR_IVOL_DATA_SIZE];
ubi32_t hdr_crc;
} __attribute__((packed));
在VID头部中有一个成员为lnum,用于表示与该peb相对于的leb的number。Ubi在atatch一个MTD分区的时候会扫描每一个块,然后收集相应的信息,建立起来volumn分区信息和每一个分区的eba_tbl表。
Ubifs设计文档中提到ubifs是一个out-of-place updates的文件系统,即文件系统修改一个文件系统的时候,是先将数据读出,在缓冲区中进行写覆盖,然后将这些数据写入到一个新块中。为什么这么做呢?
我们知道Nand Flash在写之前进行擦除(具体原因不在说明),如果我只是修改了很小的不部分内容,就会发现这个读-擦除-写的代价比写入一个新块的代价大的多。
所以在ubifs在对一个文件修改时,直接修改数据写入一个新块,然后再新块中使得VID头部的lnum为原来的leb就可以了。然后将原来的leb unmap掉,这个unmap的过程将该块丢给ubi的后台进程去擦除。当然这个擦除的过程需要读出EC头部,然后更新擦除次数,重新写入被擦除的物理块中。这时候这个块就是一个free的块了。所以UBI中空块是存在EC头部但是不存在VID头部的。
上面提到了unmap一个erase block,下面看看eba子系统是如何unmap一个erase block的。
int ubi_eba_unmap_leb(struct ubi_device *ubi, struct ubi_volume *vol,int lnum)
{
int err, pnum, vol_id = vol->vol_id;
if (ubi->ro_mode)
return -EROFS;
err = leb_write_lock(ubi, vol_id, lnum);
if (err)
return err;
pnum = vol->eba_tbl[lnum];
if (pnum < 0)
/* This logical eraseblock is already unmapped */
goto out_unlock;
dbg_eba("erase LEB %d:%d, PEB %d", vol_id, lnum, pnum);
vol->eba_tbl[lnum] = UBI_LEB_UNMAPPED;
err = ubi_wl_put_peb(ubi, pnum, 0);
out_unlock:
leb_write_unlock(ubi, vol_id, lnum);
return err;
}
该函数首先在分区的eba_tbl中查找看看该erase block时候被map了,如果没有被map,那么函数直接返回。
ubi_wl_put_peb 函数最终调用schedule_erase(ubi, e, torture);来进行erase 操作。
static int schedule_erase(struct ubi_device *ubi, struct ubi_wl_entry *e,
int torture)
{
struct ubi_work *wl_wrk;
dbg_wl("schedule erasure of PEB %d, EC %d, torture %d",
e->pnum, e->ec, torture);
wl_wrk = kmalloc(sizeof(struct ubi_work), GFP_NOFS);
if (!wl_wrk)
return -ENOMEM;
wl_wrk->func = &erase_worker;
wl_wrk->e = e;
wl_wrk->torture = torture;
schedule_ubi_work(ubi, wl_wrk);
return 0;
}
这个函数的实现也比较简单,首先创建了一个ubi_work结构体,初始化之。
static void schedule_ubi_work(struct ubi_device *ubi, struct ubi_work *wrk)
{
spin_lock(&ubi->wl_lock);
list_add_tail(&wrk->list, &ubi->works);
ubi_assert(ubi->works_count >= 0);
ubi->works_count += 1;
if (ubi->thread_enabled)
wake_up_process(ubi->bgt_thread);
spin_unlock(&ubi->wl_lock);
}
将这个ubi_work结构体结构体加到&ubi->works队列中,然后唤醒ubi的后台进程。
这个后台进程是ubi_thread,主要调用do_work来执行具体的操作。在此之前我们需要详细的了解一下ubi_work结构体。
struct ubi_work {
struct list_head list;//用于将其连到队列中去
int (*func)(struct ubi_device *ubi, struct ubi_work *wrk, int cancel);
函数指针,用于执行具体的work
/* The below fields are only relevant to erasure works */
struct ubi_wl_entry *e;//一个红黑树的入口,用于查找leb
int torture;
};
在后台擦除的时候,这个func的具体执行函数为
static int erase_worker(struct ubi_device *ubi, struct ubi_work *wl_wrk,int cancel)
->sync_erase(读出该peb的ec头部,后的erase counter)
->do_sync_erase(具体执行mtd层的erase操作,并检查擦除之后该块是否全是0xff)
->ubi_io_write_ec_hdr(重新写入ec头)
至此,一个块的unmap工作就完成了。
.Bit filpint ubi_eba_read_leb(struct ubi_device *ubi, struct ubi_volume *vol, int lnum,void *buf, int offset, int len, int check)
是eba层的读函数,被ubifs层调用。当发生位反转(bit filp)的时候,ubi认为该块不适合继续用来存储数据了,就会进行scrub操作。
具体的scrub操作由ubi_wl_scrub_peb函数执行。与上面的erase一样,也是创建一个ubi_worker,只不是现在的具体的回调函数是wear_leveling_worker
该函数将原来lnum对应的pnum中的数据拷贝到另外一个物理块中,然后将原来的物理块擦除。
.Init过程
Nand_scan用来在attach某一个MTD分区的时候扫描分区的每一个块,通过读出EC头跟VID头来确定volumn的个数,并将这些块分类(free,corp等等。在这个扫描的过程建立了一个很大的红黑树,树中的每一个节点代表一个leb。
后面在ubi_eba_init_scan中建立每一个volumn的vtl的时候会用到这个建立起来的树。
err = register_filesystem(&ubifs_fs_type);
if (err) {
ubifs_err("cannot register file system, error %d", err);
return err;
}
这儿是在ubifs_init(super.c)中关于注册ubifs文件系统类型的。
static struct file_system_type ubifs_fs_type = {
.name = "ubifs",
.owner = THIS_MODULE,
.get_sb = ubifs_get_sb,
.kill_sb = kill_anon_super,
};
文件系统类型中最主要的是关于超级块的读取。每一个类型的文件系统都有自己类型的自定义的超级块。但是为了跟VFS挂钩,需要将这些信息跟VFS定义的超级块衔接一起。所以每一个文件系统类型中都定义了这样的超级块读取函数。