Linux VFS机制简析(二)

Linux VFS机制简析(二)

接上一篇Linux VFS机制简析(一),本篇继续介绍有关Address space和address operations、file和file operations、dentry和dentry operations和dentry cache API。

Address Space

Address Space用于管理page caches里的page页,它关联某个文件的所有pages,并管理文件的内容到进程地址空间的映射。它还提供了内存管理接口(page回收等)、根据地址查找page、跟踪page的tags(如dirty和writeback)等等功能。
VM模块会调用->write_page方法去尝试将脏页刷盘,以及调用->releasepage方法将clean page释放。带有PagePrivate标记的clean page(引用为0)会被VM直接释放而不通知Address Space。
为了实现这个功能,Address Space通过lru_cache_add()将page放入LRU,并通过mark_page_active()标记page正在使用。

Pages通过->index保存在一个radix树里,该radix树维护page的PG_Dirty和PG_Writeback信息,因此查找这两个标识的pages变得非常快。
Dirty标记(PAGECACHE_TAG_DIRTY)主要由->writepages(默认方法mpage_writepages())方法使用。它使用该标记查找脏页并调用->writepage方法。如果Address operations实现了自己的->writepages(不使用mpage_writepages),则Dirty标记将几乎没有作用。write_inode_now()和sync_inode()通过Dirty标记来检查->writepages是否成功完成。
Writeback标记主要是由filemap_wait 方法和sync_page* 方法使用,通过调用filemap_fdatawait_range()等待所有的writeback完成。如果定义了->sync_page,则会调用它来等待所有需要writeback的page结束。

Address Space Handler可以通过page的private字段保存额外的数据,此时需要设置PG_Private标识。这样VM的相关操作会调用address的handler处理这些数据。

上面说的这么多Page相关的管理,其实Address Space最核心的作用是充当存储和应用程序的中间缓存。数据从存储侧以page为单位读入address space,通过拷贝或者mapping的方式提供给应用层。应用写入数据到address space,然后通过writeback机制写入到存储。
读操作的核心是readpage()。写操作稍微复杂些,可以通过write_begin/write_end或者set_page_dirty写入数据到address space,再通过writepagesync_pagewritepages写入数据到存储。从address space里增加删除page由inode的i_mutex锁保护。

当数据写入到page,需要设置PG_Dirty标识,当writepage准备写入存储时清除PG_Dirty,并设置PG_Writeback标识,知道数据完全写入存储后清除PG_Writeback。

struct address_space_operations

struct address_space_operations的定义如下:

struct address_space_operations {
	int (*writepage)(struct page *page, struct writeback_control *wbc);
	int (*readpage)(struct file *, struct page *);
	int (*sync_page)(struct page *);
	int (*writepages)(struct address_space *, struct writeback_control *);
	int (*set_page_dirty)(struct page *page);
	int (*readpages)(struct file *filp, struct address_space *mapping,
	                struct list_head *pages, unsigned nr_pages);
	int (*write_begin)(struct file *, struct address_space *mapping,
	                        loff_t pos, unsigned len, unsigned flags,
	                        struct page **pagep, void **fsdata);
	int (*write_end)(struct file *, struct address_space *mapping,
	                        loff_t pos, unsigned len, unsigned copied,
	                        struct page *page, void *fsdata);
	sector_t (*bmap)(struct address_space *, sector_t);
	int (*invalidatepage) (struct page *, unsigned long);
	int (*releasepage) (struct page *, int);
	void (*freepage)(struct page *);
	ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
	                loff_t offset, unsigned long nr_segs);
	struct page* (*get_xip_page)(struct address_space *, sector_t,
	                int);
	/* migrate the contents of a page to the specified target */
	int (*migratepage) (struct page *, struct page *);
	int (*launder_page) (struct page *);
	int (*error_remove_page) (struct mapping *mapping, struct page *page);
	int (*swap_activate)(struct file *);
	int (*swap_deactivate)(struct file *);
};

writepage:VM调用,用于将脏页写入后端存储。参数wbc->sync_mode显示是什么原因触发,'sync'或者'flush'(释放内存)。调用时PG_Dirty已经被清除,并且PageLocked已经设置。writepage开始写入数据时需要设置PG_Writeback,并且写入结束时清除该标记。无论是同步还是异步写入,都要保证函数返回时page处于unlocked状态。
如果wbc->sync_mode是WB_SYNC_NONE(不等待),则writepage遇到困难时可以不那么努力的写入,而是返回AOP_WRITEPAGE_ACTIVATE,这样VM不会老是来写该page。

readpage:VM调用,用于从后端存储读取数据。调用时,page处于lock状态,并且在读取结束时需要设置为unlock状态,并设置uptodate。如果readpage处理过程中需要unlock page,则unlcok之后需要返回AOP_TRUNCATED_PAGE,调用者将重新定位page并重新lock,成功之后会再次调用readpage

sync_page:VM调用,用于通知后端存储处理该page的I/O。该page所属address space的其他Pages的I/O也可能被处理。该函数是可选的,仅用于等待PG_Writeback的page处理完成。

writepages:VM调用,将address space里所有Dirty的pages写入后端存储。如果wbc->sync_mode是WBC_SYNC_ALL,则writeback_control会选取一个范围的pages必须写入。如果是WBC_SYNC_NONE,则根据参数nr_to_write尽可能写入这么多pages。如果没有设置,则默认调用mpage_writepages()。

set_page_dirty:VM调用,用于设置page为dirty。通常用于address space里有新的数据写入,如memory mapping的page被修改。该函数将设置PageDirty标记,并在Radix树里设置PAGECACHE_TAG_DIRTY标识。

readpages:VM调用,用于读取address space里的指定pages。主要是通过调用readpage将一组pages读取。通常用于预读,因此读取失败的错误码可能会被忽略。

write_begin:由通用的buffered写流程调用,写入len长度数据到文件的offset处。address space可能需要申请额外的存储空间来保证写操作可以完成,或者需要从后端存储读取不在缓存里的pages。该函数返回的pagep要处于locked状态,调用者将直接写入数据。返回参数fsdata用于私有数据指针,它将传递给write_end函数。如果函数返回<0,则write_end将不会调用。
write_end:数据拷贝到write_begin返回的page后,调用write_end将page unlock,递减引用计数并更新i_size字段。

bmap:VFS调用用于映射逻辑块的偏移和物理块编号。该方法由FIBMAP ioctl使用,并且是swap文件。swap系统不直接进入文件系统,而是通过BMAP方式建立内存地址和文件的块映射,然后直接使用内存地址。

invalidatepage:如果设置了PagePrivate,则当Page部分或者全部从address space里删除时调用该方法。通常是因为address space里执行了一个截断或者是失效所有数据。和page关联的私有信息需要更新,或者直接被释放(如果失效的offset为0的话,整个page将被释放)。

releasepage:用于将PagePrivate pages释放,它将把私有数据释放,然后清除PagePrivate标识。releasepage有两种使用场景,一是VM发现没有引用计数的clean page,想将其变成free page。通过调用releasepage将其从address space里摘掉变为clean page。二是有invalid请求需要将address space里的部分或全部pages失效。通常是fadvice系统调用或者文件系统自己认为缓存里的数据已经不是最新的了,此时通过调用invalidate_inode_pages2()将pages释放。调动该函数前,需要保证pages已经是invalidate的。如果释放私有数据失败,则需要在返回错误之前将PageUptodate清除。

freepage:用于将不在pagecache里的page释放,page必须不属于任何address space。通常有内存回收处理程序调用。
direct_IO:由通用读写流程调用,绕过pagecache,DIO方式读取数据。
get_xip_page:VM调用,将block number转换为page。支持XIP(execute in place)的文件系统需要实现该函数。
migrate_page:在old page和new page之间迁移数据,通常用于内存整理(减少碎片)。迁移时需要将私有数据和引用一起迁移。
launder_page:在free之前调用,用于writeback dirty page。为了防止再次被设置dirty,操作过程中持有page lock。
error_remove_page:用于内存分配失败的处理,如果address space支持truncation,通常设置为generic_error_remove_page()。
swap_activate and swap_deactivate:用于swapon在一个文件上时,分配空间并将block信息保存在内存中。以及swapoff时释放空间。

File

一个File数据结构代表一个进程里打开的一个文件。所以File结构是跟进程相关的,不同的进程打开同一个文件会在每个进程里都有一个File对象,对应到进程的文件句柄。
同一个文件File结构指向的inode是同一个,所以通过pagecache缓存进行数据读写的时候,使用的是inode里同一个address space,保证文件数据在不同进程里的一致性。

struct file_operations

struct file_operations的定义如下:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
	ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
	ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long arg, struct file_lock **);
	long (*fallocate)(struct file *, int mode, loff_t offset, loff_t len);
};

同样,如果没有特别说明,则所有操作都在没有锁持有的情况下调用。
file_operations里大部分函数跟POSIX文件系统接口语义一样,就不单独列出了。

Dentry

dcache(dentry cache)用于缓存dentry,每个dentry用于索引filename和inode number。dentry也有一套操作合集dentry operations用于管理dentry。底层文件系统可以选择实现自己的dentry operations来替换默认的operations。

struct dentry_operations

struct dentry_operations的定义如下:

struct dentry_operations {
	int (*d_revalidate)(struct dentry *, unsigned int);
	int (*d_weak_revalidate)(struct dentry *, unsigned int);
	int (*d_hash)(const struct dentry *, const struct inode *,
	                struct qstr *);
	int (*d_compare)(const struct dentry *, const struct inode *,
	                const struct dentry *, const struct inode *,
	                unsigned int, const char *, const struct qstr *);
	int (*d_delete)(const struct dentry *);
	void (*d_release)(struct dentry *);
	void (*d_iput)(struct dentry *, struct inode *);
	char *(*d_dname)(struct dentry *, char *, int);
	struct vfsmount *(*d_automount)(struct path *);
	int (*d_manage)(struct dentry *, bool);
};

d_revalidate:VFS用于检查在dcache里找到的dentry是否有效。通常设置为NULL,则只要在dcache找到即认为是有效的。但对网络文件系统如NFS来说,dentry可能在一段时间之后就会失效,因此需要实现该函数用于检查是否有效。如果有效,函数需要返回一个正数。
d_revalidate可能在rcu-walk模式(flags & LOOKUP_RCU)下被调用。此时该函数里不能阻塞也不能写入数据到dentry,并且d_parent和d_inode不能使用,因为他们可能瞬间就可能被修改。如果在rcu-walk模式遇到困难,则返回-ECHILD,将在ref-walk模式下重新调用。

d_weak_revalidate:用于检查'jumped'的dentry,即那些不是通过lookup获取的dentry,如'', '.'或者'..'。这种场景只需要检查dentry对应inode是否OK即可。该函数不会在rcu-walk模式下调用,所以可以放心的使用inode。

d_hash:用于VFS将dentry放入HASH列表。并不清楚HASH表用来做啥,通常不需要设置它,使用VFS默认的即可。

d_compare:用于比较dentry name和指定的name。该函数必须是可重入的,即每次的返回结果一样。
d_delete:用于引用计数递减为0时调用,返回1则dcache立即删除dentry,返回0则继续缓存该dentry。默认为NULL,则总是将dentry进行缓存。该函数必须是可重入的,即每次的返回结果一样。
d_release:用于释放dentry资源。
d_iput:用于释放dentry对应inode引用计数。该函数在释放dentry之前调用。如果为NULL,则VFS默认调用iput()。
d_dname:用于生成dentry的pathname,主要是一些伪文件系统(sockfs, pipefs等)用于延迟生成pathname。一般文件系统不实现该函数,因为其dentry存在于dcache的hash表里(通过pathname做hash),所以并不希望pathname变化。
d_automount:可选函数,用于穿越到一个自动挂载的dentry。它会创建一个新的vfsmount记录,并将其返回,成功后调用者将根据vfsmount去尝试mount它到挂载点。
d_manage:可选函数,用于管理从dentry进行transition。

Directory Entry Cache API

以下函数是VFS提供给文件系统参与维护和管理的dentry cache的API接口。

dget:用于增加dentry引用计数。
dput:递减引用计数,如果减为0,则调用d_delete判断是否留在缓存里。如果判断为否,或者该dentry已经不在其父目录hash列表里,则将其删除。如果判断为是,则dentry放入LRU链表,并在触发内存回收时删除。

d_drop:将dentry从其父目录的hash列表里删除。随后如果引用计数减为0,该dentry将被删除。
d_delete:将dentry删除。如果引用计数不为0,则调用d_drop。如果为0,则调用d_iput将dentry搞成nagtive dentry。注意该函数不是dentry operations->d_delete函数指针,而是VFS提供的API接口。

d_add:将dentry加入到父目录的hash列表里,并调用d_instantiate
d_instantiate:将dentry加入到对应的inode的hash列表里,并更新其d_inode字段。inode的引用计数i_count字段需要递增。该函数通常用于新创建inode给一个nagtive dentry。

d_lookup:根据pathname,查找父目录dentry下的某个dentry。如果找到,则增加引用计数并返回dentry。调用用完该dentry之后需要通过dput将引用计数递减。

总结

VFS的角色包括:

  • 管理可用的文件系统类型,将设备和文件系统实例进行关联。
  • 处理文件系统的相关操作,为应用程序提供标准文件系统接口。

VFS和具体的文件系统系统之间主要通过几个数据结构:super_block, inode, dentry, file和address space以及对应的operations: sb_ops, i_ops, d_ops, f_ops和a_ops来实现文件系统的功能。

参考

Linux Documentation: VFS

posted @ 2018-12-14 15:15  舰队  阅读(6021)  评论(0编辑  收藏  举报