I want to be a complete engineer - technical genius and sensitive humanist all in one!

Thursday, September 3, 2009

Linux 虚拟文件系统概述

[翻译说明:Richard Gooch 的这份 Overview of the Linux Virtual File System 位于内核
源代码的 Documentation/filesystems/下, 文件名是 vfs.txt。 该文档在 Understanding
the Linux Kernel 中被推荐, 从 2.6.26 官方内核中拷贝出来翻译,详细情况看原文。

介绍
====

虚拟文件系统(也被称做虚拟文件系统切换)是内核中为应用程序提供文件系统接口的一个软件层。
同时, 它也为不同的文件系统共存, 提供了一种抽象。

open(2),stat(2),read(2),write(2),chmod(2)等VFS系统调用, 是在进程上下文中被
调用的。 文件锁的描述在 Documnetation/filesystems/Locking中。


目录项缓存(dcache)
-----------------

VFS实现了诸如 open(2),stat(2),chmod(2)等系统调用。 路径名作为参数传递给这些系统
调用, 然后VFS使用路径名, 在目录项缓存(也叫 dentry cache 或 dcache)中查找相应的目
录项(dentry)。 通过目录项缓存, 路径名(文件名)能迅速得转换成相应的目录项。 目录项只存
在于内存中, 不会保存到磁盘上:它们存在的意义在于性能。

目录项缓存(dcache)旨在维护整个文件空间的一个视图。 但大多数计算机无法将文件系统的整
个目录结构缓存在内存中, 因此有些文件没有被缓存。 为了把路径名解析成相应目录项(dentry),
VFS得一步步的将路径名分解,创建目录项(dentry),读取相应的Inode。 这个过程是通过
Inode中的查找方法做到的。

Inode 对象
---------

一个目录项 (dentry) 通常包含指向 inode 的指针。 Inode是文件系统中的对象, 诸如常规文件,
目录, 管道(FIFO) 和其他的文件系统对象。 它们要么保存在于磁盘上(块设备上的文件系统), 要么
存在于内存中(伪文件系统)。 当需要时, 在磁盘上的 inode会被读取到内存中; 对inode的修改也会
写回到磁盘上。 一个 Inode可以被多个目录项(dentry)指向(例如硬链接就是这样)。

查找一个文件的Inode, VFS需要调用它的父目录对应Inode的lookup()方法。 该方法由Inode所在
的文件系统实现来定义。 一旦 VFS找到了dentry(因此也就知道了inode), open(2)系统调用就能
打开文件、 stat(2)就能查看inode内的数据。 Stat(2)操作非常简单: 一旦 VFS找到了dentry,
它就查看相关的inode并把部分数据返回给用户空间。

File 对象
--------

打开文件时还需要一个操作: 分配一个file结构(就是内核对文件描述符的实现)。 初始化这个新分配的
file结构时, 其中的一个指针会指向目录项(dentry),另一个指针会指向文件操作成员函数表──这是
从 inode中取来的。 然后函数表中的open()方法会被调用, 这样, 就调用了文件系统自己的open方法。
File结构被放置在进程的文件描述符表中。

读、写和关闭文件(以及其他与 VFS有关的操作)时, 首先使用用户空间提供的的文件描述符找到相应的
file结构, 然后调用file结构指向的函数表中的相应函数, 去执行相应的操作。只要文件还在打开着, 目录
项(dentry)就处于使用状态, 也就意味着inode也处在使用状态。


注册和挂载一个文件系统
=====================

注册或注销一个文件系统, 使用如下的 API 函数:

#include

extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);

传递进来的 file_system_type 结构描述了一个文件系统。 当有将把某个设备挂载到文件空间的某个目录
上的请求时, VFS会调用对应文件系统(由 file_system_type 结构代表)的 get_sb()方法。 挂载点的
目录项(dentry)将被更新, 指向inode的指针将指向新挂载的文件系统的根目录inode。
( 这里更新挂载点d_inode的描述是错的 )

在/proc/filesystems文件中, 你可以看到内核中所有已注册的文件系统。

file_system_type 结构
---------------------

该结构描述了文件系统。 例如 2.6.22 内核中的代码, 该结构具有下列成员:

struct file_system_type {
const char *name;
int fs_flags;
int (*get_sb) (struct file_system_type *, int,
const char *, void *, struct vfsmount *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct list_head fs_supers;
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
};


name: 文件系统的名字, 例如"ext2"、"iso9660"、"msdos"等

fs_flags: 各种标志(亦即: FS_REQUIRES_DEV, FS_NO_DCACHE 等)

get_sb: 每当该类型的文件系统被挂载时, 调用该方法

kill_sb: 每当该类型的文件系统被卸载时, 调用该方法

owner: VFS 内部使用:多数情况下该被赋值为 THIS_MODULE

next: VFS 内部使用: 多数情况下该被赋值为 NULL

s_lock_key, s_umount_key: 处理锁依赖相关

get_sb()方法有如下参数:

struct file_system_type *fs_type: 描述文件系统, 由特定文件系统初始化

int flags: 挂载标志

const char *dev_name:要挂载的设备名

void *data: 任意的挂载选项, 通常是 ASCII 字符串的形式

struct vfsmount *mnt: 挂载点的VFS内部呈现

get_sb()方法必须探测 dev_name指定的块设备所包含的文件系统类型, 判断该方法是否支持fs_type
指定的文件系统。 如果dev_name指定的块设备被成功打开, 它将为块设备中的文件系统初始化一个
super_block结构, 如果失败, 将返回错误代码。

由get_sb()方法填充的 super_block结构的一些域中, s_op 最值得关注。 它指向是一个super_operations结构,
这个结构描述了文件系统的下一层的实现。

通常, 一个文件系统会使用通用的 get_sb()实现并自己提供一个 fill_super()方法。 通用的 get_sb()实现有:

get_sb_bdev: 挂载一个基于块设备的文件系统

get_sb_nodev: 挂载不存在于磁盘上的文件系统

get_sb_single: 挂载所有挂载点共享一个super_block结构的文件系统

fill_super()方法具有下列参数:

struct super_block *sb: superblock 结构, fill_super()方法必须初始化它

void *data: 任意的挂载选项, 通常由 ASCII 字符串组成

int silent: 出错时是否打印错误信息


Superblock 对象
==============

一个 superblock 对象代表一个挂载的文件系统。

super_operations 结构
---------------------

该结构描述了 VFS 如何操作文件系统上的 superblock, 以 2.6.22 为例, 它具有下列成员:

struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);

void (*read_inode) (struct inode *);

void (*dirty_inode) (struct inode *);
int (*write_inode) (struct inode *, int);
void (*put_inode) (struct inode *);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
void (*write_super_lockfs) (struct super_block *);
void (*unlockfs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);

int (*show_options)(struct seq_file *, struct vfsmount *);

ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
};

除非特别说明, 所有这些方法都在不持有锁的情况下调用, 这意味着这些方法可以安全的阻塞,
这些方法都必须在进程上下文调用(亦即:不是在中断处理函数或者 boottom half 中)。

alloc_inode: 由 inode_alloc()调用, 来为 inode分配空间并初始化它。 如果不定义该
方法,则分配的空间只够inode结构使用。 而通常情况下,alloc_inode会分配一个更大
的数据结构, 其中包含一个inode结构"。

destroy_inode: 由 destroy_inode()调用, 撤消为 inode分配的资源。 该方法只有在
alloc_inode被定义的时候才有效,它撤消alloc_inode做的所有事。

read_inode: 从某个挂载的文件系统中读取相关的 inode。 inode 结构的 i_ino 成员由 VFS
初始化, 来指定要读取的 inode; 其他成员由本方法填充。
你可以将该字段设置为NULL,这样就不能使用iget了, 而要使用iget5_locked来读取inode。
对于不能只通过inode号找到相应inode的文件系统, 则必需使用这种方法。

dirty_inode: 由 VFS 调用, 来把一个 inode 标记为脏。

write_inode: 当 VFS 需要把某个 inode 写入到磁盘上时, 调用本方法。 第二个参数指定
了写操作是否需要同步, 并非所有的文件系统都会检查这个标志。

put_inode: 当inode被从inode缓存(inode cache)中移除时调用

drop_inode: 当所有对inode的引用都被移除时调用, 调用时必须持有 inode_lock自旋锁。
该方法或者为 NULL(正常的 Unix 文件系统语义), 或者为 "generic_delete_inode"
(那些不想 缓存inode的文件系统。 这会导致不管 inode的引用计数的值是多少, delete_inode
总会被调用)"generic_delete_inode"的行为, 与 put_inode()中使用"force_delete"是等价的, 但
不会象后者那样会引发竞争情形。

delete_inode: 当 VFS 想删除一个 inode 时调用

put_super: 当 VFS 想释放 superblock(比如卸载时)时调用。 调用前应先调用lock_super锁住superblock

wirte_super: 当 VFS 需要将superblock写入到磁盘时调用, 该方法是可选的。

sysc_fs: 当 VFS需要把有隶属于 superblock的脏数据写入到磁盘上时调用。 第二个参 数指示了该方法是否需要
一直等待写操作的完成。 可选。

write_super_lockfs: 当 VFS需要锁定一个文件系统, 强制使它进入一致状态时调用该方法。
该方法目前用于逻辑卷管理 (Logical Volume Manager, LVM)

unlockfs: 当 VFS解锁一个文件系统, 并标记它为可写的, 此时调用本方法。

statfs: 当 VFS 想获得文件系统的一些统计数据时调用。 调用时需要持有内核锁(翻译疑 问:看 2.6.16 的 vfs_statfs 函数调用 sb->s_op->statfs 时并没有持有锁, 不知道作者指的是哪把锁?)

remount_fs: 当文件系统被 remount 时调用, 调用需持有内核锁

clear_inode: 当 VFS 清除 inode 时调用。 可选。

umount_begin: 当 VFS 卸载一个文件系统时调用

show_options: VFS 需要在/proc//mounts 显示挂载选项时调用

quota_read: VFS 想读取文件系统的磁盘配额文件时调用

quota_write: VFS 想写入文件系统的磁盘配额文件时调用

read_inode()方法负责填充"i_op"域, 它是一个指向"struct inode_operations"的指针, 该
结构描述了那些操作于每个 inode 的方法。

Inode 对象
==========

一个 inode 对象代表了文件系统内的一个对象。

inode_operations 结构
---------------------

描述了 VFS 如何操作你的文件系统中的一个 inode。 例如在 2.6.22 内核中, 有如下的成员:

struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*readlink) (struct dentry *, char __user *,int);
void * (*follow_link) (struct dentry *, struct nameidata *);
void (*put_link) (struct dentry *, struct nameidata *, void *);
void (*truncate) (struct inode *);
int (*permission) (struct inode *, int, struct nameidata *);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*truncate_range)(struct inode *, loff_t, loff_t);
};

正如 super_operations 的方法一样, inode_operations 的成员也是在无锁情形下调用──
除非有特别说明。

create: 由 open(2)和 creat(2)系统调用来调用。 只有你想在文件系统中支持正规文件时,
才需要提供该方法的实现。 该方法参数中的 dentry应该还没有跟 inode 相关连(亦即:是一个负的
dentry)。 该方法中一般会调用d_instantiate()将 dentry于inode相关联。

lookup: VFS 需要在父目录中寻找一个 inode 时调用。 要查找的文件名在 dentry 中。 该方
法必须调用 d_add()来将dentry与找到的inode相关联 , 该 inode 的"i_count"域随之增
加。 如果该名字的 inode 未找到, 则 dentry 与 NULL 关联(亦即:是一个负的 dentry)。
本方法只有在真正遇到不可恢复的错误时才返回错误, 否则的话, 那些会创建 inode 的系
统调用, 如 create(2)、mknod(2)、mkdir(2)等将无法工作。 如果你想重载 dentry 的方法,
那么就初始化 dentry 的"d_dop"域, 它是一个指向"struct dentry_operations"的指针。

link: 由系统调用 link(2)来调用。 只有你想在自己的文件系统内支持硬链接时, 才需要提供该方法的实现。
多数情形下, 需要象 create 方法那样调用 d_instantiate()函数。

unlink: 由 unlink(2)系统调用来调用。 只有你想支持删除 inode 时才提供。

symlink: 由 symlink(2)系统调用来调用。 只有你想在自己的文件系统里支持符号链接时,
才需要提供该方法的实现。 多数情形下, 需要象 create 方法那样调用 d_instantiate()函数。

mkdir: 由系统调用 mkdir(2)来调用。 只有你想在文件系统中支持创建子目录时, 才需要提供其实现。
需要象 create 方法那样调用 d_instantiate()函数

rmdir: 由系统调用 rmdir(2)来调用。 只有想支持删除子目录时, 才需要。

mknod: 由系统调用 mknod(2)来调用, 以创建设备文件或者命名管道(FIFO)或者
本地socket。 只有你想支持创建这些类型的 inode 时, 你的文件系统才需要提供该方法的实现。
需要象 create 方法那样调用 d_instantiate()方法。

rename: 由系统调用 rename(2)来调用, 以重命名一个对象, 使之具有由第二个inode和 denrty
给定的父目录和名字。


readlink: 由系统调用readlink(2)来调用. 只有在文件系统支持符号链接时才需提供

follow_link: 由 VFS 调用, 以跟踪符号链接到它所指向的 inode。 只有你想支持符号链接
时才需要提供。 该方法返回了一个 void 型指针 cookie 传递给 put_link(), 参考下面的 put_link()方法。

put_link: 由 VFS 调用, 来释放由 follow_link 方法申请的临时性资源。 由follow_link()返回的
cookie 作为最后一个参数传递给本方法。 由一些诸如 NFS 这样的文件系统使用, 因为在这样的文
件系统中, page cache 是不稳定的(亦即, 随着跟踪符号链接的过程中建立起的 page cache 可能在
跟踪的最后已经不存在了)。

truncate: 由 VFS 调用以改变文件的大小。 在调用本方法之前, VFS 先把 inode 的 i_size
域设为期望的值。 本方法主要由 truncate(2)系统调用等使用。

permission: 由 VFS 调用, 来检查 POSIX 类文件系统的访问权限。

setattr: 由 VFS 调用, 来设定文件的属性, 该方法主要由 chmod(2)等使用

truncate_range: 该方法由文件系统提供, 用于截去文件中一个连续区域中块. (例如在文件中制造一个空洞)

getattr: 由 VFS 调用, 来设定文件的属性, 主要由 stat(2)等系统调用使用

setxattr: 由 VFS 调用, 来设置文件的扩展属性。 所谓扩展属性,就是和 inode 相关的一 对「名称-值」,
它是在inode 分配的时候与之关联的。 由 setxattr(2)系统调用使用。

getxattr: 由 VFS 调用, 获取根据扩展属性的名称, 获取其值。 由 getxattr(2)系统调用使用。

listxattr: 由 VFS 调用, 来列出给定文件的所有扩展属性。 给 listxattr(2)系统调用使用。

removexattr: 由 VFS 调用, 移除给定文件的扩展属性。 给 removexattr(2)系统调用使用。

truncate_range: 由文件系统实现提供的方法, 用于截去指定范围内的块 (例如: 在文件中制造一个空洞)

地址空间对象(The Address Space Object)
======================================

地址空间对象用来对Page Cache中的页进行分组、管理。 它可以用来跟踪文件中的(或其他地方的)
页面,也可以用来跟踪文件映射到进程地址空间的映射区。

地址空间对象有多种用处,其关系有时并不紧密。 这些用处包括:动态调整内存使用情况
根据地址来查找页面,跟踪那些标记为Dirty和Writeback的页面。

在这用处中,第一项独立于其他项。VM可以把脏页写入磁盘,从而使得它变为clean;也可以释放clean的页,
以便重新使用它。重新使用页前, 脏页需要调用->writepage方法;clean的页, 如果有PagePrivate标记,
则需要调用->releasepage方法; 没有PagePrivate标记的Clean页,如果又没有外部的引用,可以直
接释放,而不用通知它所属的地址空间对象。

为了做到这点,需要将页面放入一个"最近最少使用"链表,每当页面被使用,就调用lru_cache_add和
mark_page_active。

通常情况下地址空间中的页面要位于基树(radix tree)中,由page结构的->index成员来索引。该基树为每个
页面维护了PG_Dirty和PG_Writeback的信息,这样,设置了这些标志的页面可以通过基树很快 找到。

Dirty标签主要是由mpage_writeages──默认的->writepages方法──使用的,用来寻找那么需要调用
->writepages方法的页面。 如果未使用mpage_writepages函数(亦即,地址空间提供了自己定义的
->writepages方法),PAGECACHE_TAG_DIRTY标签就基本没用了。 write_inode_now和sync_inode函
数通过该标签,来检查->writepages方法是否成功把地址空间对象里的所有脏页都写出去了。
/* "PAGECACHE_TAG_DIRTY标签就基本没用了", 这句肯定是不对的 */

Writeback标签由 filemap*_wait* 和 sync_page* 等函数使用(通过wait_on_page_writeback_range),
以等待写回操作完成。 在等待的时候,会针对每个在写回的页面调用->sync_page方法(如果定义了)。

地址空间对象的操作可能会给页面附加一些信息,通常会使用'struct page'的'private'。 如果附加了这样
的信息,那么就应该设置页面的PG_Private标志,这样,各个VM子系统就会在相应的场合调用地址空
间对象的处理函数来处理这些附加信息。

地址空间对象是联系应用程序和磁盘存储的媒介。 数据是以页为单位,从存储读入地址空间的;再提供
给应用程序时,要么是拷贝该页中的数据到程序空间,要么是通过内存映射。同样的, 程序将 数据写入到地址空间中,
然后写回到磁盘存储--通常也是以页为单位,但实际上地址空间会的精确的控制写回操作大小。

读的过程很简单,只需要'readpage'方法;相对来说,写的过程要复杂的多,使用prepare_write/
commit_write或set_page_dirty来把数据写入到地址空间中,再使用writepage,sync_page和
writepages来把数据从地址空间中写入到磁盘存储上。

从地址空间中添加和删除页面,都必须得持有inode的i_mutex互斥锁。

当有数据写入到页面,就应设置该页面的PG_Dirty标志。 该标志会一直保持着,直到writepage请求把该页写回存
储--这会清除PG_Dirty标志,而设置PG_Writeback标志。 写回操作实际上是发生在PG_Dirty标志清除后的任意时刻。
当页成功写回到存储后,PG_Writeback标志会被清除。

Writeback使用了一个writeback_control结构。


address_space_operations结构
----------------------------

该结构描述了 VFS 如何把文件映射到 page cache 中。 例如在 2.6.22 内核中, 它有以下成
员:

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 (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
sector_t (*bmap)(struct address_space *, sector_t);
int (*invalidatepage) (struct page *, unsigned long);
int (*releasepage) (struct page *, int);
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 *);
};

writepage: 由 VM 用来把脏页写到磁盘上。 这可能是为了数据的完整性(亦即,'sync'),
或者是为了释放内存(flush)。二者的区别见wbc->sync_mode。
writepage方法调用前应保证PG_Dirty标志已清除,PageLocaked已设置。该方法执行回写操作,
并在写操作执行前设置PG_Writeback标志,清楚页面的PageLocaked标记。

如果wbc->syn_mode的值为WB_SYNC_NONE, ->writepage方法就不必费力的完成工作,
它可以选择容易写的其他页面(例如,由于内部依赖的问题)。 如果它决定不回写页面,则应
当返回AOP_WRITEPAGE_ACTIVE,以便VM不再对该页调用->writepage方法了。

更多细节可以参考"Locking"文件。

readpage: 由 VM 用来从磁盘上读取页面。
调用readpage方法前应保证在已经对页面加锁, 当读操作完成时会解锁该页,并将页面标记为最新。
如果出于某种原因,->readpage方法需要解锁该页,它倒也可以这么做,然后返回AOP_TRUNCATED_PAGE。
这种情况下,该页将被重定位、重新加锁,都成功了后VFS会重新调用->readpage方法。

sync_page: VM 调用它来通知存储设备, 执行所有与某一页有关的正等待的 I/O 操作。 同一
address_space 中的其他页的 I/O 也可能被执行。

该方法是可选的,只有在等待writeback完成时才会调用,而且只针对那些设置了PG_Writeback的页调用。

writepages; VM 调用, 把该 address_space中的"脏"页回写到磁盘。 如果wbc->sync_mode
为WBC_SYNC_ALL,那么writeback_control将指定需要回写页面的范围。如果是WBC_SYNC_NONE,
就要给一个nr_to_write值, writepages应该尽可能的回写这么多个的页面到存储设备。
如果未提供->writepages方法,那么就使用mpage_writepages函数。它会选择地址空间
对象中那些标记为脏的页面,并传给->write_page方法。
/* 没提供writepages, VFS会使用generic_writepages */

set_page_dirty: VM调用它把某页标记为脏。
如果一个地址空间对象给页面附加了数据,且当页面变脏时那些数据需要更新,那么就需要
该本方法了。 例如,当一个内存映射的页被修改了,本方法就被调用。它需要设置页的PageDirty标志,
在基树中为该页加上PAGECACHE_TAG_DIRTY标签。

readpages: VM 调用, 从磁盘上读取跟该 address_space 有关的页。
基本可以说本方法就是readpage方法的向量化版本(a vector version of readpage)。
readpage方法只读一个页面,而本方法读多个页面。
readpages方法只用与预读(read-ahead),所以其错误可以被忽略,不管什么地方出错
了,只管放弃,没问题的。

prepare_write: 在通常的写操作路径中, VM调用它来设置跟页有关的写请求。 它表示页面中的一定区域将会
写入数据。 地址空间应该检查该写操作请求是否能成功完成, 如果需要的话,分配磁盘空间, 做其他需要的处理。
如果改写操作只修改一个文件系统中数据块中的部分数据, 这些被部分修改的数据块中的数据需要预先读入 (如果还
没有被读入)。 只有这样, 这些被部分修改的数据块才能正确的更新。 和readpage一样, 如果prepare_write
需要解锁页面, 它也可以这么做,然后返回AOP_TRUNCATED_PAGE.

提示: 不要将页面标记为最新, 除非页面真的为最新。 如果标记为最新, 其他的并发的读操作可能会读到错误的数据。


commit_write: 如果prepare_write成功, 写入的数据将拷贝到页面中, 然后commit_write会被调用。 一般情况下,
它会更新文件的大小、 将inode标记为脏、 作一些相关的处理。 如果可能, 该函数应该避免返回错误, 错误应该在 prepare_write发现


bmap: VFS 调用它, 把对象内的逻辑块号映射到物理块号上。 传统的 FIBMAP ioctl 系统调和激活交换文件时
使用该函数。 为了将页面换出到交换文件中, 交换文件必须直接映射到块设备上。 Swap子系统读写操作不会通过
文件系统, 而是使用bmap将文件的逻辑块映射到设备的物理块上,读写操作时直接使用物理块号。

invalidatepage: 如果一个页面有PagePrivate标记, 当要把页面中部分或全部数据从地址空间移出时, 需要调用该方法。在截断(truncate)地址空间时和使整个地址空间无效时会将页面数据从地址空间移出(在后一种情况, 传递给该函数offset
参数的值是0)。 页面的私有数据也应该依照截断(truncate)的情况更新, 如果offset参数的值为0, 则应该释放私有数据。
这是因为, 当offset为0时, 整个页面的数据都会被丢弃, 这有点象releasepage的功能, 但在这种情况下, invalidatepage必须成功的释放页面。

releasepage: 该方法在想要释放一个有PagePrivate标记的页时被调用。->releasepage应该移除页面的私有数据, 清除PagePrivate标记。 它也可能会将页面从地址空间中移出。 如果出于某种原因, 操作失败, 则返回0。 该函数在以下两种情况下被调用。 第一种情况:虚拟内存子系统(VM)找到一个干净(clean)的页面 , 并且该页面没有活动用户使用时, 虚拟内存子系统会尝试回收该页面。如果releasepage中的操作都成功, 页面会从地址空间中移出, 变为空闲。第二中情况: 收到将地址空间中的部分或全部页面变为无效的请求时。 invalidate_inode_pages2处理这种请求, 它可能由 fadvice(2)
系统调用发出, 也可能由一些文件系统显示的发出,比如nfs, 9fs (当文件系统发现地址空间中的数据已经过期)。 如果是文件系统发出这种请求, 并且所有指定的页面都要变为无效, 则需要通过->releasepage来保证页面都要变为无效。 当某些页面的私有数据不能清除时, ->releasepage可能会清除页面的Update标记。

direct_IO: VM 为直接 I/O 的读/写操作调用它。 直接 I/O是指跳过页面缓存(page cache), 直接在用程序地址空间和存储之间传输数据。

get_xip_page: VM 调用它, 把块号转换成页。 在相关的文件系统卸载之前, 该页保持有效。 那些想实现「适当执行」
(execute-in-place,XIP)的文件系统需要提供该方法的实现。在 fs/ext2/xip.c 文件中可以找到例子。
/* 感觉将XIP翻译成适当执行不是很贴切 */

migrate_page: 用于压缩内存的使用。 如果虚拟内存子系统(VM)希望重定位一个页面(比如一个内存即将失效), 它将传递新页面和被替换的页面的描述符 给该函数。 该函数应该将被替换的页面的私有数据转移到新页面上, 并且更新所有对被替换的页面的引用。
launder_page: 释放一个页面之间调用, 它将脏页面回写到存储设备。 它可以防止页面再次被标记为脏, 整个操作在持有页面锁的情况下进行。



文件对象(The File Object)
=========================

一个文件对象, 代表了进程的一个打开文件。

file_operations 结构
--------------------

该结构描述了 VFS 如何操作一个打开的文件。 例如在内核 2.6.13 中, 它有如下成员:

struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t,
loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned
long);
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 *, struct dentry *, 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 (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
};

除非特别说明, 否则这些方法都可以在不加锁的情况下调用。

llseek: VFS 想移动文件的读写位置指针时调用

read: 由 read(2)及其它相关系统调用调用

aio_read: 由 io_submit(2)及其他异步 I/O 操作调用

write: 由 write(2)及相关系统调用调用

aio_write: 由 io_submit(2)及其他异步 I/O 操作调用

readdir: VFS 想读取目录内容时调用

poll: VFS 调用。 调用的时机为: 当进程想检查某一文件上是否出现特定特征, 并且
(可选地)阻塞, 直到所等待特征出现。 给 select(2)和 poll(2)系统调用使用。

ioctl: 由 ioctl(2)调用

unlocked_ioctl: 由 ioctl(2)调用。 那些并不获取 BKL(译注:Big Kernel Lock, 大内核
锁,一种同一时刻只允许一个 CPU 在内核态、允许递归获取的锁,详见 lib/kernel_lock.c 代码注释)

compat_ioctl: 由 ioctl(2)调用。 调用时机为: 在 64 位内核上执行 32 位的 ioctl 系统调用。

mmap: 由 mmap(2)调用

open: 当 VFS 想打开一个 inode 时调用。 VFS 打开文件时, 先创建一个新的 struct file, 然后
调用该 file 结构的 open 方法。 嗯, 你可能会想:open 方法为什么不放在
struct inode_operations 里呢? 可能这种想法也有道理, 但我觉得象内核这样设计, 可以简
化文件系统的实现。 并且, 该 open()方法适合初始化 file 结构的"private_data"成员──如
果你想让该成员指向某个设备的数据结构。

flush: 由 close(2)调用, 来冲刷文件。

release: 当最后一个对 file 结构的指向也被关闭时调用

fsync: 由 fsync(2)调用

fasync: 由 fcntl(2)调用, 前提是该 file 的异步(非阻塞)模式已被激活

lock: 由带 F_GETLK,F_SETLK 和 F_SETLKW 命令的 fcntl(2)调用

readv: 由 readv(2)调用

writev: 由 writev(2)调用

sendfile: 由 sendfile(2)调用

get_unmapped_aera: 由 mmap(2)调用

check_flags: 由带 F_SETFL 命令的 fcntl(2)调用

dir_notify: 由带 F_NOTIFY 命令的 fcntl(2)调用

flock: 由 flock(2)调用

注意, 文件操作的这些方法, 是由其 inode 所在的分区的文件系统来实现的。 当打开一
个设备文件(字符设备或块设备特殊文件)时, 多数文件系统会调用 VFS 的一些例程来定
位该设备所属的驱动程序信息。 这些例程将用设备驱动程序中实现的的 file operations
替换文件系统中实现的的那个, 并继续调用新的 open 方法, 这是「为什么打开文件系统
中的设备文件,会最终导致调用设备驱动中的 open()方法」的原因。


目录项 Cache(Directory Entry Cache, dcache)
===========================================

dentry_operations 结构
----------------------

该结构描述了一个文件系统如何重载标准的 dentry 操作集。 Dentry 和 dcache 是 VFS 和具
体文件系统实现的概念, 设备驱动程序就和他们不搭边了。 这些方法可以被置为 NULL,
因为它们是可选的, 如果你不实现, VFS 就使用默认的。 例如 2.6.13 内核中, 该结构有
如下成员:

struct dentry_operations {
int (*d_revalidate)(struct dentry *, struct nameidata *);
int (*d_hash) (struct dentry *, struct qstr *);
int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
int (*d_delete)(struct dentry *);
void (*d_release)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
};

d_revalidate: 当 VFS 想重新使一个 dentry 有效时调用, 这一般发生在某次查找中在dcache
中找到了 dentry。 多数文件系统会把这个方法置为 NULL, 因为它们留在 dache 中的 dentry 还是有效的。

d_hash: 当 VFS 把一个 dentry 加入到哈希表中时调用

d_compare: 比较两个 dentry 时调用

d_delete: 当 dentry 的最后一个引用被删除时调用。 这意味着没有人在使用这个 dentry
了, 但它依然是有效的, 并且在 dcache 中。

d_release: 当 dentry 真正被销毁时调用。

d_input: 当一个 denrty 失去了它所属的 inode 时(正好在 dentry 被销毁之前)调用。
如果这个方法置为 NULL, VFS 就会调用 iput(); 如果你自己定义了该方法, 必须在自己
的实现中调用 iput()。

每个 dentry 含有一个指向父 dentry 的指针, 还有一个所有子 dentries 的哈希链表。 基
本上, 子 dentries 就象目录中的文件一样。



Dcache API
----------

内核中定义了许多函数, 供文件系统来操作 dentries:

dget: 打开一个已存在的 dentry 的句柄(在这里,只是增加引用计数而已)

dput: 关闭 dentry 的一个句柄(减少引用计数)。 如果引用计数减到了 0, 就调用

d_delete 方法, 把该 dentry 置入「未使用」队列。 「把 dentry 置入未使用队列」意味着,
如果内存不够用了, 将遍历「未使用队列」并调用 deallocates 方法来销毁 dentries,
以腾出内存。 如果 dentry 已经是「unhashed」(译注:指不在父 dentry 的 hash 链中)且
引用计数为 0, 这时候调用 d_delete 方法然后销毁它。

d_drop: 该方法把一个 dentry 从它的父 dentry 的 hash 链中脱链。 如果它的引用计数变为
0, 随后的调用 dput()将销毁该 dentry。

d_delete: 删除一个 dentry。 如果该 dentry 没有其他的引用了, 则变为「负的 dentry」
并调用 d_iput()方法; 如果还有其他引用, 就不走这些而调用 d_drop()。

d_add: 把一个 dentry 放入它的父 dentry 的哈希链表, 并调用 d_instantiate()。

d_instantiate: 把一个 dentry 链入 inode 的「别名哈希链表」并更新 d_inode 域。 inode
结构的i_count 域应该被设置/增加。 如果 dentry 不和任何 inode 关联, 则它就是一个
「负的 dentry」。 该函数一般在为负的 dentry 新创建一个 inode 时调用。

d_lookup: 给出父 dentry 和名字等信息, 在 dcache 哈希表中查找一个 dentry。 如果找到,
增加其引用计数并返回其地址。 调用者在使用完毕时, 必须调用 d_put()方法来释放
dentry。

关于访问 dentry 时加锁的更多信息, 请参考文档 Documentation/filesystems/dentry-locking.txt。

No comments:

Labels

Followers