ocfs2文件系统的写流程及page_cache介绍

目录

1.整体流程

2.直接写

3.缓存写

3.1 page_cache

3.1.1 基本概念

3.1.2 address_space数据结构

3.1.3 page基树

3.1.4 基树结构

3.1.5 page基树的查找

3.1.6 基树的标记

3.2 建立page_cache与磁盘物理位置的关系(buffer_head)

3.2.1 buffer_head数据结构

3.2.2 buffer_head和page关联

3.2.3 buffer_head和设备的块关联

3.2.4 buffer_head的分配

3.3 page_cache和磁盘如何交互数据(readpages\writepages)

3.3.1 writepage

3.3.2 同步写

3.3.3 非同步写


基于linux 4.18分析。

1.整体流程

2.直接写

直接写与缓存写最显著的区别是不使用page_cahce,每一次读写都尽量不经过page_cache。

不过,对于ocfs2而言,以下情况下直接写会转换成缓存写:写inline数据,append写,写空洞。

直接写流程很简单,主要流程如下:

  1. 将缓存页刷到磁盘;
  2. 使缓存页无效;
  3. 调用vfs通用函数__block_direct_IO,该函数内数据层层转换,最终封装为bio,通过submit_bio函数提交给块层。

3.缓存写

“数据存储张”在知乎上的博客对缓存写的流程写得非常清楚详细、通俗易懂,这里就不再鹦鹉学舌。

OCFS2文件写数据流程分析 - 知乎

抛开代码,缓存写有以下流程:

  1. 函数generic_perform_write()将数据拆分,每次写入的数据长度不超过1个page大小。
  2. ocfs2_write_begin函数分配磁盘空间和缓存页:计算是否需要分配cluster;根据数据在文件内偏移pos获取已有的页或分配新页插入到address_space的radix-tree中;分配cluster,并通过buffer_head对象建立page和磁盘物理位置的映射关系。
  3. 将用户的数据拷贝到上一步获取的page中的对应位置。
  4. ocfs2_write_end函数更新buffer_head的状态为脏(其所属page也将被标记为脏,inode将被放入sb的脏链表),时机合适时page中的数据将被刷新到磁盘。
  5. 对于缓存写, 如果文件标志有同步属性(file->f_flags & O_DSYNC),则函数filemap_fdatawrite_range将数据从缓存刷写到磁盘(里面最终调用mapping->a_ops->writepages)。

3.1 page_cache

3.1.1 基本概念

页高速缓存的核心数据结构是 address_space 对象,它是一个嵌入在页所有者的索引节点对象中的数据结构。

struct inode {
	...
	struct address_space	*i_mapping; // 指向其关联的address_space
    ...
}

《深入理解linux内核》说address_space更应该叫page_cache_entry或者physical_page_of_a_file。(传神)

我的对address_space的理解:文件的数据缓存在许多页中,而address_space对象关联了这些页;这些页以一定的形式将这些页组织起来,根据文件内偏移pos可以找到对应的页;每个文件只有一个address_space数据结构。

address_space:与一个inode关联,其host域就会指向该inode。(address_space也可能和其他对象关联,host域就其他)。

address_space_operation:提供管理页高速缓存的各种行为。每个后备存储都通过自己的address_space_operation描述自己如何与高速缓存交互。

3.1.2 address_space和page数据结构

struct address_space {
	struct inode		*host;		/* owner: inode, block_device */ // 拥有该page_chache对象的索引节点
	struct radix_tree_root	i_pages;	/* cached pages */  // 包含全部页面的radix树
	atomic_t		i_mmap_writable;/* count VM_SHARED mappings */
	struct rb_root_cached	i_mmap;		/* tree of private and shared mappings */
	struct rw_semaphore	i_mmap_rwsem;	/* protect tree, count, list */
	/* Protected by the i_pages lock */
	unsigned long		nrpages;	/* number of total pages */
	/* number of shadow or DAX exceptional entries */
	unsigned long		nrexceptional;
	pgoff_t			writeback_index;/* writeback starts here */
	const struct address_space_operations *a_ops;	/* methods */
	unsigned long		flags;		/* error bits */
	spinlock_t		private_lock;	/* for use by the address_space */
	gfp_t			gfp_mask;	/* implicit gfp mask for allocations */
	struct list_head	private_list;	/* for use by the address_space */
	void			*private_data;	/* ditto */
	errseq_t		wb_err;
}

当一个page被作为页高速缓存使用,其中的部分union结构会适配该方式而使用。我们需要关注以下两个重要的字段:

  • mapping字段指向拥有该页的inode的address_space对象;
  • index字段指当前page在address_space中的顺序索引,也就是在文件的逻辑空间中以页为单位的偏移。在address_space的page cache基树中查找逻辑偏移pos所在的页面,就需要先计算出pos对应的页面index。
<linclude/linux/mm_types.h>

struct page {
    unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */ // 原子标志,有些可能是异步更新的。表示页的状态,页是不是脏的,是不是被锁定在内存中
    union {
		struct {	/* Page cache and anonymous pages */
			/**
			 * @lru: Pageout list, eg. active_list protected by
			 * zone_lru_lock.  Sometimes used as a generic list
			 * by the page owner.
			 */
			struct list_head lru;
			/* See page-flags.h for PAGE_MAPPING_FLAGS */
			struct address_space *mapping; // 该页被作为页缓存使用,指向和这个页关联的address_space对象(一个inode只有一个)
			pgoff_t index;		/* Our offset within mapping. 与文件逻辑偏移对应 */
			/**
			 * @private: Mapping-private opaque data.
			 * Usually used for buffer_heads if PagePrivate.
			 * Used for swp_entry_t if PageSwapCache.
			 * Indicates order in the buddy system if PageBuddy.
			 */
			unsigned long private; // 该页作为私有数据
		};
    ......
    }
......
    atomic_t _refcount; // 页的引用计数。调用方法获取,不要直接使用
}

3.1.3 page基树

为实现页高速缓存的高效查找,每个 address_space 对象将page以基树的形式组织起来。i_pages为基树的根。

<include/linux/radix-tree.h>

struct radix_tree_root {
	spinlock_t		xa_lock;
	gfp_t			gfp_mask;
	struct radix_tree_node	__rcu *rnode;
};

struct radix_tree_node {
	unsigned char	shift;		/* Bits remaining in each slot */ // unsigned int offset = (index >> parent->shift) & RADIX_TREE_MAP_MASK;
	unsigned char	offset;		/* Slot offset in parent */
	unsigned char	count;		/* Total entry count */ // 节点中非空指针数量的计数器
	unsigned char	exceptional;	/* Exceptional entry count */
	struct radix_tree_node *parent;		/* Used when ascending tree */
	struct radix_tree_root *root;		/* The tree we belong to */
	union {
		struct list_head private_list;	/* For tree user */
		struct rcu_head	rcu_head;	/* Used when freeing node */
	};
	void __rcu	*slots[RADIX_TREE_MAP_SIZE]; // 节点数组,指向page或者下一层redix_tree_node
/* tags顾名思义是标签,代表此节点的所有孩子节点的标签。tags是二维数组,RADIX_TREE_MAX_TAGS定义为3,即最多支持3种标签。RADIX_TREE_TAG_LONGS的长度使得可以放下所有子节点的tag(一个tag占1位) */
	unsigned long	tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};

3.1.4 基树结构

page_cache的基树结构如下图所示(引用自《深入理解linux内核》)。

基树中,页索引相当于线性地址,但页索引中要考虑的字段的数量依赖于基树的深度。

如果基树的深度为 1,就只能表示从 0 ~ 63 范围的索引,因此页索引的低 6 位被解释为 slots 数组的下标,每个下标对应第一层的一个节点。

如果基树深度为2,就可以表示从 0 ~ 4085 范围的索引,页索引的低 12 位分成两个 6 位的字段,高位的字段表示第一层节点数组的下标,而低位的字段用于表示第二层节点数组的下标。依次类推。

计算每一层的slot号先用index右移shift位,再取右移结果的低6位作为slot号,取下一层的节点。

<lib/radix-tree.c>

static unsigned int radix_tree_descend(const struct radix_tree_node *parent,
			struct radix_tree_node **nodep, unsigned long index)
{
	unsigned int offset = (index >> parent->shift) & RADIX_TREE_MAP_MASK;
	void __rcu **entry = rcu_dereference_raw(parent->slots[offset]);
	*nodep = (void *)entry;
	return offset;
}

如果基树的最大索引小于应该增加的页的索引,则内核相应地增加树的深度;基数的中间节点依赖于页索引的值。

3.1.5 page基树的查找

函数find_or_create_page查找offset(文件内偏移pos换算成以page为单位的偏移)对应的page,若未查找到,则创建page并插入到radix树中。

<include/linux/pagemap.h>

static inline struct page *find_or_create_page(struct address_space *mapping,
					pgoff_t offset, gfp_t gfp_mask)
{
	return pagecache_get_page(mapping, offset,
					FGP_LOCK|FGP_ACCESSED|FGP_CREAT,
					gfp_mask);
}


<mm/filemap.c>

struct page *pagecache_get_page(struct address_space *mapping, pgoff_t offset,
	int fgp_flags, gfp_t gfp_mask)

其中offset:page的index

3.1.6 基树的标记

3.1.6.1 tag的作用

(本节大部分内容引用自《深入理解Linux内核》)

页高速缓存不仅允许内核能快速获得含有块设备中指定数据的页,还允许内核从高速缓存中快速获得给定状态的页。例如,内核必须从高速缓存获得某个inode的所有脏页,从而将这些脏页刷写到磁盘。存放在页描述符中的 PG_dirty 标志表示页是否是脏的。但如果大多数页不是脏页,遍历整个基树的操作就太慢了。为了能快速搜索脏页,基树中的每个中间节点都包含一个针对每个孩子节点的脏标记,当且至少一个孩子节点的脏标记被置位时该标记被设置。最底层节点的脏标记通常是页描述符的 PG_dirty 标志的副本。
通过这种方式,当内核遍历基树搜索脏页时,就不用遍历所有子节点,可以跳过脏标记为 0 的中间节点的所有子树。PG_writeback 标志同理,该标志表示页正在被写回磁盘。

在页高速缓存基树节点中,字段tags用于表示子节点的状态,其中一个bit代表一个子节点。

<include/linux/radix-tree.h>

#define RADIX_TREE_MAX_TAGS 3

// 一个bit代表一个节点,计算需要多少字节的空间。BITS_PER_LONG为8
#define RADIX_TREE_TAG_LONGS	\
	((RADIX_TREE_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG)

struct radix_tree_node {
	void __rcu	*slots[RADIX_TREE_MAP_SIZE]; // 节点数组,指向page或者下一层redix_tree_node
    
	unsigned long	tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; // 二维标志
};

节点状态有以下3种,每种状态对应一个一维数组。如果某个节点下有对应状态的page,则将该节点对应的bit位置为1。
tag[0][]表示PAGECACHE_TAG_DIRTY;
tag[1][]表示PAGECACHE_TAG_WRITEBACK;
tag[2][]表示PAGECACHE_TAG_TOWRITE;

<include/linux/fs.h>

#define PAGECACHE_TAG_DIRTY	0
#define PAGECACHE_TAG_WRITEBACK	1
#define PAGECACHE_TAG_TOWRITE	2
3.1.6.2 关于tag的操作函数

radix_tree_tag_set() 设置页高速缓存中页的 PG_dirty 或 PG_writeback 标志,它作用于三个参数:基树的根、页的索引及要设置的标记的类型(PAGECACHE_TAG_DIRTY 或 PAGECACHE_TAG_WRITEBACK)。

  1. 函数从树根开始并向下搜索到与指定索引对应的叶子节点;
  2. 对于从根通往叶子路径上的每个节点,利用指向路径中下一个节点的指针设置标记。
  3. 最后,返回页描述符的地址。
  4. 结果是,从根节点到叶子节点的路径中的所有节点都被加上了标记。


radix_tree_tag_clear() 清除页高速缓存中页的 PG_dirty 或 PG_writeback 标志,参数与 radix_tree_tag_set() 相同。

  1. 从树根开始向下到叶子节点,建立描述路径的 radix_tree_path 结构的数组。
  2. 然后,从叶子节点到根节点进行操作:
  3. 清除底层节点的标记,然后检查是否节点数组中所有标记都被清 0,如果是,把上层父节点的相应标记清 0。
  4. 最后,返回页描述符的地址。


radix_tree_delete() 从基树删除页描述符,并更新从根节点到叶子节点的路径中的相应标记。。
radix_tree_insert() 不更新标记,因为插入基树的所有页描述符的 PG_dirty 和 PG_writeback 标志都被认为是清 0 的。
radix_tree_tagged() 利用树的所有节点的标志数组测试基树是否至少包括一个指定状态的页。

3.2 建立page_cache与磁盘物理位置的关系(buffer_head)

page与磁盘物理位置的关系建立需要依赖与buffer_head结构。buffer_head描述的是磁盘block和page之间的映射关系,buffer_head跟page关联,buffer_head也跟文件系统块设备的具体块关联,从而使得page与磁盘物理位置关联。

3.2.1 buffer_head数据结构

struct buffer_head {
	unsigned long b_state;		//buffer的状态
	struct buffer_head *b_this_page;//该page中的下一个buffer
	struct page *b_page;		//buffer所在page

	sector_t b_blocknr;		// 在块设备上的块号
	size_t b_size;			/* size of mapping */
	char *b_data;			// 页面内的数据指针(指向内核虚拟地址)

	struct block_device *b_bdev;  //对应的块设备
	bh_end_io_t *b_end_io;		/* I/O completion */ // IO完成后的回调函数
 	void *b_private;		/* reserved for b_end_io */
	struct list_head b_assoc_buffers; /* associated with another mapping */
	struct address_space *b_assoc_map;	/* mapping this buffer is associated with */
	atomic_t b_count;		/* users using this buffer_head */ // buffer使用计数
};

3.2.2 buffer_head和page关联

关键函数流程

create_empty_buffers
  -- alloc_page_buffers // buffer_head关联page
    -- alloc_buffer_head // 创建buffer_head对象
    -- set_bh_page // bh对象指向页面内数据
  -- attach_page_buffers // page关联buffer_head链的头部
    -- SetPagePrivate // 设置PG_Private标志,表示有page有对应fs的数据,即buffer
    -- set_page_private // 将page->private指向buffer_head

函数create_empty_buffers给指定的page创建buffer_head,并将其与page关联。我们先看一下如何为指定的page创建和初始化一组buffer_head。
一个文件系统的block对应一个buffer_head,一个page对应一个或多个buffer_head。函数alloc_page_buffers将page按block size分成多份,每份对应一个buffer_head。重要字段:

b_size:这个buffer_head映射的块大小。

b_data:指向页面内数据的指针(内核虚拟地址)

<fs/buffer.c>

struct buffer_head *alloc_page_buffers(struct page *page, unsigned long size,
		bool retry)
{
	struct buffer_head *bh, *head;
	gfp_t gfp = GFP_NOFS;
	long offset;

	if (retry)
		gfp |= __GFP_NOFAIL;

	head = NULL;
	offset = PAGE_SIZE; // bh指向的数据在page内的偏移
	while ((offset -= size) >= 0) { // 对于ocfs2, 入参size为block size
		bh = alloc_buffer_head(gfp); // 创建buffer_head对象
		if (!bh)
			goto no_grow;

		bh->b_this_page = head; // 指向page中的下一个buffer_head
		bh->b_blocknr = -1;
		head = bh;

		bh->b_size = size;

		/* Link the buffer to its page */
		set_bh_page(bh, page, offset);
	}
	return head;

...
异常处理代码,忽略
}

void set_bh_page(struct buffer_head *bh,
		struct page *page, unsigned long offset)
{
	bh->b_page = page;
	BUG_ON(offset >= PAGE_SIZE);
	if (PageHighMem(page))
		/*
		 * This catches illegal uses and preserves the offset:
		 */
		bh->b_data = (char *)(0 + offset);
	else
		bh->b_data = page_address(page) + offset; // #define page_address(page) lowmem_page_address(page)
}

alloc_page_buffers返回上层函数后,fs中的create_empty_buffers()函数进一步处理:

  • 将buffer_head进一步头尾相连,变成环形链表;
  • 将buffer关联到对应的page上,page->private指向buffer_head头部。
<fs/buffer.c>

void create_empty_buffers(struct page *page,
			unsigned long blocksize, unsigned long b_state)
{
	struct buffer_head *bh, *head, *tail;
    
	head = alloc_page_buffers(page, blocksize, true); // 创建buffer_head并指向page内的数据
	bh = head;
	do {
		bh->b_state |= b_state;
		tail = bh;
		bh = bh->b_this_page;
	} while (bh);
	tail->b_this_page = head; // 尾部bh指向首部的bh

	...

	attach_page_buffers(page, head); // page->private指向buffer_head头部
	spin_unlock(&page->mapping->private_lock);
}

static inline void attach_page_buffers(struct page *page,
		struct buffer_head *head)
{
	get_page(page);
	SetPagePrivate(page); // 设置PG_Private标志,表示有page有对应fs的数据,即buffer
	set_page_private(page, (unsigned long)head); // 将page->private指向buffer_head
}

buffer_head和page的关联关系如下图所示。

3.2.3 buffer_head和设备的块关联

create_empty_buffers被调用后,此时已对给定的page创建了关联的buffer_head。返回上层函数后,ocfs2遍历该page的buffer_head,调用函数map_bh()将buffer_head和文件系统对应的块设备、block号、block size关联。

<include/linux/buffer_head.h>

static inline void
map_bh(struct buffer_head *bh, struct super_block *sb, sector_t block)
{
	set_buffer_mapped(bh);
	bh->b_bdev = sb->s_bdev;
	bh->b_blocknr = block;
	bh->b_size = sb->s_blocksize;
}

3.2.4 buffer_head的分配

前面着重于关联的建立。此时可以浅看一下它如何分配。buffer_head有专门的内存分配器“bh_cachep”进行分配。这个分配器属于slab/slub分配器,与kmalloc类似。不同的是,kmalloc是从通用的slab/slub分配器分配空间,可能存在空间浪费,而这个“bh_cachep”分配器专门用于分配buffer_head,其一次分配的内存大小是转为bh定制。

<fs/buffer.c>

struct buffer_head *alloc_buffer_head(gfp_t gfp_flags)
{
	struct buffer_head *ret = kmem_cache_zalloc(bh_cachep, gfp_flags);
	if (ret) {
		INIT_LIST_HEAD(&ret->b_assoc_buffers);
		preempt_disable();
		__this_cpu_inc(bh_accounting.nr);
		recalc_bh_state();
		preempt_enable();
	}
	return ret;
}

3.3 page_cache和磁盘如何交互数据(readpages\writepages)

3.3.1 writepage

page_cahce和磁盘的数据交互依赖于函数指针readpage(ocfs2_readpage)和writepage(ocfs2_writepage)。

ocfs2_writepage函数进行一些校验后,直接调用vfs的函数block_write_full_page。该函数及其下层调用,根据buffer_head构建bio,submit_bio到块层。

ocfs2_writepage(struct page *page, struct writeback_control *wbc)
  -- block_write_full_page(struct page *page, get_block_t *get_block, struct writeback_control *wbc)

3.3.2 同步写

对于缓存写, 如果文件标志有同步属性(file->f_flags & O_DSYNC),则ocfs2_write_iter函数返回前,调用filemap_fdatawrite_range将数据从缓存刷写到磁盘。经过层层调用,该函数最终调用mapping->a_ops->writepage。

3.3.3 非同步写

(这一块暂时没看代码,资料来源于书籍,可能不适用linux4.18)

只要进程修改了数据,相应的页就被标记为脏页,其 PG_dirty 标志置位。

在下列条件下把脏页写入磁盘:

  • 空闲内存低于阈值。低于阈值时,唤醒一个或多个flusher线程,将脏页写回磁盘
  • 脏页内存驻留时间超过阈值。flusher线程会被周期性唤醒。
  • 用户进程调用sync(), fsync(),fdatasync()

与每个缓冲区页相关的缓冲区首部使内核能了解每个独立块缓冲区的状态。

如果至少有一个缓冲区首部的 BH_Dirty 标志被置位,就设置相应缓冲区页的 PG_dirty 标志。

当内核选择要刷新的缓冲区页时,它扫描相应的缓冲区首部,并只把脏块的内容写到磁盘。

一旦内核把缓冲区的所有脏页刷新到磁盘,就把页的 PG_dirty 标记清 0。

  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值