2018-Linux IO 过程自顶向下分析
前言
文件与存储
普通文件与设备文件的

linux io 体系结构

本文将按照上图的架构自顶向下依次分析每一层的要点。
从Hello world 说起
#include "apue.h"
#define BUFFSIZE 4096
int
main(void)
{
int n;
char buf[BUFFSIZE];
int fd1;
int fd2;
fd1 = open("helloworld.in", O_RONLY);
fd2 = open("helloworld.out", O_WRONLY);
while ((n = read(fd1, buf, BUFFSIZE)) > 0)
if (write(fd2, buf, n) != n)
err_sys("write error");
if (n < 0)
err_sys("read error");
exit(0);
}
看一个简单的
现在我们看看
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
我们可以很清楚地看到,
虚拟文件系统(VFS)
在深入
先思考一个问题,如果我们自己要实现一个文件系统,需要做什么?先不管实现细节,对外我们总得定义自己的文件系统接口吧,只有有了相应的函数接口,用户程序才能进行读写操作。所以,最基本最基本我们也得有自己的读和写接口。
但是,如果每一家文件系统都定义自己的一套接口,对于上层应用来说就很难去管理。在这样的时代背景下,

superblock 对象
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;
unsigned long s_blocksize;
loff_t s_maxbytes; /* Max file size */
struct file_system_type *s_type;
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_magic;
struct dentry *s_root;
struct rw_semaphore s_umount;
int s_count;
atomic_t s_active;
#ifdef CONFIG_SECURITY
void *s_security;
#endif
const struct xattr_handler **s_xattr;
struct list_head s_inodes; /* all inodes */
struct hlist_bl_head s_anon; /* anonymous dentries for (nfs) exporting */
#ifdef __GENKSYMS__
#ifdef CONFIG_SMP
struct list_head __percpu *s_files;
#else
struct list_head s_files;
#endif
#else
#ifdef CONFIG_SMP
struct list_head __percpu *s_files_deprecated;
#else
struct list_head s_files_deprecated;
#endif
#endif
struct list_head s_mounts; /* list of mounts; _not_ for fs use */
/* s_dentry_lru, s_nr_dentry_unused protected by dcache.c lru locks */
struct list_head s_dentry_lru; /* unused dentry lru */
int s_nr_dentry_unused; /* # of dentry on lru */
/* s_inode_lru_lock protects s_inode_lru and s_nr_inodes_unused */
spinlock_t s_inode_lru_lock ____cacheline_aligned_in_smp;
struct list_head s_inode_lru; /* unused inode lru */
int s_nr_inodes_unused; /* # of inodes on lru */
struct block_device *s_bdev;
struct backing_dev_info *s_bdi;
struct mtd_info *s_mtd;
struct hlist_node s_instances;
struct quota_info s_dquot; /* Diskquota specific options */
struct sb_writers s_writers;
char s_id[32]; /* Informational name */
u8 s_uuid[16]; /* UUID */
void *s_fs_info; /* Filesystem private info */
unsigned int s_max_links;
fmode_t s_mode;
/* Granularity of c/m/atime in ns.
Cannot be worse than a second */
u32 s_time_gran;
/*
* The next field is for VFS *only*. No filesystems have any business
* even looking at it. You had been warned.
*/
struct mutex s_vfs_rename_mutex; /* Kludge */
/*
* Filesystem subtype. If non-empty the filesystem type field
* in /proc/mounts will be "type.subtype"
*/
char *s_subtype;
/*
* Saved mount options for lazy filesystems using
* generic_show_options()
*/
char __rcu *s_options;
const struct dentry_operations *s_d_op; /* default d_op for dentries */
/*
* Saved pool identifier for cleancache (-1 means none)
*/
int cleancache_poolid;
struct shrinker s_shrink; /* per-sb shrinker handle */
/* Number of inodes with nlink == 0 but still referenced */
atomic_long_t s_remove_count;
/* Being remounted read-only */
int s_readonly_remount;
/* AIO completions deferred from interrupt context */
RH_KABI_EXTEND(struct workqueue_struct *s_dio_done_wq)
};
这里字段非常多,我们没必要一一解释,有个大概的感觉就行。有几个字段比较重要的这里提一下:
s_list
该字段是双向循环链表相邻元素的指针,所有的superblock 对象都以链表的形式链在一起。s_fs_info
字段指向属于具体文件系统的超级块信息。很多具体的文件系统,例如ext2 ,在磁盘上有对应的superblock 的数据块,为了访问效率,s_fs_info
就是该数据块在内存中的缓存。这个结构里最重要的是用bitmap 形式存放了所有磁盘块的分配情况,任何分配和释放磁盘块的操作都要修改这个字段。s_dirt
字段表示超级块是否是脏的,上面提到修改了s_fs_info
字段后超级块就变成脏了,脏的超级块需要定期写回磁盘,所以当计算机掉电时候是有可能造成文件系统损坏的。s_dirty
字段引用了脏inode 链表的首元素和尾元素。s_inodes
字段引用了该超级块的所有inode 构成的链表的首元素和尾元素。s_op
字段封装了一些函数指针,这些函数指针就指向真实文件系统实现的函数,superblock 对象定义的函数主要是读、写、分配inode 的操作。多态主要是通过函数指针指向不同的函数实现的。
inode 对象
i_op
字段里定义了统一的接口函数。
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;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct mutex i_mutex;
unsigned long dirtied_when; /* jiffies of first dirtying */
struct hlist_node i_hash;
struct list_head i_wb_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock *i_flock;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
void *i_private; /* fs or device private pointer */
};
除了i_op
外,介绍几个重要的字段:
i_state
表示inode 的状态,主要是表示inode 是否是脏的。一般的文件系统在磁盘上都有相应的inode 数据块,内核的inode 结构便是这个磁盘数据块的内存缓存,所以与superblock 一样,也是需要定期写回磁盘的,否则会导致数据丢失。i_list
把操作系统里的某些inode 用双向循环链表连接起来,该字段指向相应链表的前一个元素和后一个元素。内核中有好几个关于inode 的链表,所有inode 必定出现在其中的某个链表内。第一个链表是有效未使用链表,链表里的inode 都是非脏的,并且没有被引用,仅仅是作为高速缓存存在。第二个链表是正在使用链表,inode 不为脏,但i_count
字段为整数,表示被某些进程引用了。第三个链表是脏链表,由superblock 的s_dirty
字段引用。i_sb_list
存放了超级块对象的s_inodes
字段引用的链表的前一个元素和后一个元素。- 所有的
inode 都存放在一个inode_hashtable 的哈希表中,key 是inode 编号和超级块对象的地址计算出来的,作为高速缓存。因为哈希表可能会存在冲突,i_hash
字段也是维护了链表指针,就指向同一个哈希地址的前一个inode 和后一个inode 。
dentry 对象
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 */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
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 */
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct hlist_node d_alias; /* inode alias list */
};
介绍几个重要的字段:
d_inode
指向该dentry 对应的inode ,找到了dentry 就可以通过它找到inode 。- 同一个
inode 的所有dentry 都在一个链表内,d_alias
指向该链表的前一个和后一个元素。 d_op
定义了一些关于目录项的函数指针,指向具体文件系统的函数。
目录项高速缓存
按照我们平时使用
同时,由于内存有限,不可能把所有查找过的i_dentry
字段所引用的链表中,d_alias
字段则指向链表相邻的元素。
file 对象
作为应用程序的开发者或使用者,我们平时能接触到的
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
#ifdef __GENKSYMS__
#ifdef CONFIG_SMP
int f_sb_list_cpu;
#endif
#else
#ifdef CONFIG_SMP
int f_sb_list_cpu_deprecated;
#endif
#endif
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
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;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
#ifndef __GENKSYMS__
struct mutex f_pos_lock;
#endif
};
f_inode
指向对应的inode 对象。f_dentry
指向对应的dentry 对象。f_pos
表示当前文件的偏移,可见文件偏移是每个file 对象都有自己的独立的文件偏移量。f_op
表示当前文件相关的所有函数指针,实际上在文件open 的时候f_op
会全部赋值为i_op
相应的函数指针。
由于
首先,每个进程有一个
struct fs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
};
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
我们看到,每个进程都维护一个根目录和当前工作目录的信息,每一个目录由vfsmount
和dentry
组合唯一确定,dentry
代表目录项前面已经说到,vfsmount
则代表相应目录项所在文件系统的挂载信息,会在后面展开介绍一下。
然后,每个进程都有当前打开的文件表,存放在进程的
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
我们只要关注一下
vfs 管理文件系统的注册与挂载
前面介绍了s_op,i_op,d_op
这f_op
简单地复制i_op
文件系统注册
文件系统要么是固化在内核代码中的,要么是通过内核模块动态加载的,在内核代码中的随着操作系统启动会自动注册,而通过内核模块动态加载的也可以用操作系统的启动参数配置成自动注册,或者我们可以人为地执行类似这样的命令insmod fuse.ko
去动态注册,这里的
struct file_system_type {
const char *name;
int fs_flags;
#define FS_REQUIRES_DEV 1
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE 4
#define FS_USERNS_MOUNT 8 /* Can be mounted by userns root */
#define FS_USERNS_DEV_MOUNT 16 /* A userns mount does not imply MNT_NODEV */
#define FS_HAS_RM_XQUOTA 256 /* KABI: fs has the rm_xquota quota op */
#define FS_HAS_INVALIDATE_RANGE 512 /* FS has new ->invalidatepage with length arg */
#define FS_HAS_DIO_IODONE2 1024 /* KABI: fs supports new iodone */
#define FS_HAS_NEXTDQBLK 2048 /* KABI: fs has the ->get_nextdqblk op */
#define FS_HAS_DOPS_WRAPPER 4096 /* kabi: fs is using dentry_operations_wrapper. sb->s_d_op points to
dentry_operations_wrapper */
#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */
struct dentry *(*mount) (struct file_system_type *, int,
const char *, void *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct hlist_head fs_supers;
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
struct lock_class_key s_vfs_rename_key;
struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
struct lock_class_key i_lock_key;
struct lock_class_key i_mutex_key;
struct lock_class_key i_mutex_dir_key;
};
在注册文件系统的时候,我们需要提交一个file_system_type
get_sb
(linux kernel 2.6)或者是mount
(linux kernel 3.1)对象,这个是一个函数指针,主要是分配fs_supers
引用了所有属于该文件系统类型的file_system_type
的next
字段指向链表的下一个元素。
文件系统挂载
正常情况下,操作系统启动后,常用的文件系统类型都是自动注册的,不需要用户干预。但一个块设备要以某文件系统的形式被操作系统识别的话,需要挂载到某个目录下,例如执行如下的挂载命令:
mount -t xfs /dev/sdb /var/cold-storage/
当执行这条命令以后,内核会首先分配一个vfsmount
的对象,该对象唯一标识一个挂载的文件系统。
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
};
vfsmount
主要存放了该文件系统的
有可能这个文件系统会被挂载了多次,之前已经被挂载到其他目录上了,就意味着其file_system_type
的fs_supers
链表,如果找到,就直接用该vfsmount
对象的mnt_sb
字段。
如果这个文件系统是第一次被挂载,则调用注册的file_system_type
的get_sb
或者mount
函数,分配新的
当
所有的
以open 系统调用为例小结vfs 的基本知识
在继续探究vfs_read
和vfs_write
之前,先通过
回忆一下前面的
fd1 = open("helloworld.in", O_RONLY);
fd2 = open("helloworld.out", O_WRONLY);
这里核心的任务就是要通过传入的路径参数,最终创建出
具体步骤如下:
路径查找
首先进行路径查找,调用path_lookup()
函数。该函数主要接受两个参数,一个是路径名,一个是vfsmount
和dentry
唯一确定了某个路径。
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
- 首先判断路径是绝对路径还是相对路径,决定用进程的
root 还是pwd 字段去填充这个path 结构体,作为起始参数。 - 用
/ 去划分路径,依次解析每一层路径,对于每一层路径,首先找出其目录项的dentry 对象,大概率会在目录项高速缓存中命中,如果缓存中没有,则读取磁盘,然后放到缓存中,并更新path 字段。 - 检查该层目录的
dentry 是否是某文件系统的挂载点,如果是, 则用当前path 的vfsmount
和dentry
计算哈希值,找出mount_hashtable 中的子文件系统的vfsmount
和dentry
,并更新path 的vfsmount
和dentry
。 - 直到把所有分路径都解析完成,获得了最后的
path 。
创建file 对象
找到了目的路径的vfsmount
和dentry
,
把
vfs_read, vfs_write
现在,我们已经有了足够的vfs_read
和vfs_write
函数了。
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count);
if (ret >= 0) {
count = ret;
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else
ret = do_sync_read(file, buf, count, pos);
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
}
return ret;
}
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
count = ret;
file_start_write(file);
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else
ret = do_sync_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
可以看到,read,vfs_read
和vfs_write
中。这两个函数主要是对
对于通用的磁盘文件系统,
const struct file_operations xfs_file_operations = {
.llseek = xfs_file_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = xfs_file_aio_read,
.aio_write = xfs_file_aio_write,
.splice_read = xfs_file_splice_read,
.splice_write = xfs_file_splice_write,
.unlocked_ioctl = xfs_file_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = xfs_file_compat_ioctl,
#endif
.mmap = xfs_file_mmap,
.open = xfs_file_open,
.release = xfs_file_release,
.fsync = xfs_file_fsync,
.fallocate = xfs_file_fallocate,
};
这是
ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = buf, .iov_len = len };
struct kiocb kiocb;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
kiocb.ki_nbytes = len;
ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&kiocb);
*ppos = kiocb.ki_pos;
return ret;
}
ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
kiocb.ki_nbytes = len;
ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&kiocb);
*ppos = kiocb.ki_pos;
return ret;
}
这两个函数实际上调用了具体文件系统的xfs_file_aio_read
和xfs_file_aio_write
。
这两个函数的代码就有点复杂了,不过我们不需要细究xfs_file_aio_read
和xfs_file_aio_write
虽然有很多xfs_file_aio_read
最终会调用generic_file_aio_read
函数,而xfs_file_aio_write
则最终会调用generic_perform_write
函数。这些通用函数是基本上所有文件系统的核心逻辑。
进入到这里,就开始涉及到高速缓存这一层了。我们先立足于generic_file_aio_read
函数做了什么事情,非常简单:
- 根据文件偏移量计算出在要读的内容在高速缓存中的位置。
- 搜索高速缓存,看看要读的内容是否在高速缓存中存在,若存在则直接返回,若不存在则触发读磁盘任务。若触发读磁盘任务,则判断当前是否顺序读,尽量预读取磁盘数据到高速缓存中,减少磁盘
io 的次数。 - 数据读取后,拷贝到用户态的
buffer 中。
generic_perform_write
的逻辑则是:
- 根据文件偏移量计算出在要写的内容在高速缓存中的位置。
- 搜索看看要写的内容是否已在高速缓存中分配了相应的数据结构,若没有,则分配相应内存空间。
- 从用户态的
buffer 拷贝数据到高速缓存中。 - 检查高速缓存中的空间是否用得太多,如果占用过多内存则唤醒后台的写回磁盘的线程,把高速缓存的部分内容写回到磁盘上,可能会造成不定时间的写阻塞。
- 向上层返回写的结果。
可以看到,

高速缓存
在
由于文件可能非常大,所以无论是读或者写
radix tree
前面我们提到

可以看到
我们就以前面说的
计算page 编号
无论读写,都要根据文件偏移量计算
page索引=文件偏移 / 4KB
查找page
首先查看当前
根据基数的深度去解析索引,如果树只有
插入新page
假设我们的
如果树的深度不够,那就在树的顶端分配适当数量的节点,增加深度,然后再沿着路径分配中间节点。
修改page
删除page
如果系统内存不足,可能会触发
删除页就是要先在
标记脏页和正在写回的页
如果写
预读
从
预读的基本逻辑是:
- 维护两个窗口,一个是当前窗,一个是预读窗。当前窗的
page 是正在请求的页或者是预读的页,预读窗仅包含预读的页。 - 预读窗紧挨着当前窗。
- 理想情况下正在请求的页会落在当前窗,同时预读窗不断传送新的页;当进程请求的页落到预读窗时,预读窗会变为当前窗,然后分配新的预读窗,去预读后面的页。
- 预读的大小是动态变化的,如果进程持续顺序读取文件,那么预读会持续增加,直到达到文件系统的上限(默认是
128KB ) ;如果出现随机访问,预读会逐渐减少直到完全禁止。 - 当进程重复访问文件的很小一部分,预读就会停止。

buffer cache
其实在
但随着时代的发展,访问单独磁盘块的场景越来越少,
在现代
- 文件
page 内的磁盘块不相邻,或者page 内有洞。 - 单独访问一个块,例如
superblock 或者inode 。
因此,仅仅在
我们来看一下
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* circular list of page's buffers */
struct page *b_page; /* the page this bh is mapped to */
sector_t b_blocknr; /* start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */
struct block_device *b_bdev;
bh_end_io_t *b_end_io; /* I/O completion */
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 */
};
b_page
指向buffer cache 所在的page 。b_this_page
指向同属于一个page 的下一个buffer cache 。b_blocknr
buffer cache 在磁盘中的逻辑块号。b_size
buffer cache 的大小。b_data
指向buffer cache 数据的地址,该地址必定在b_page
指向的页内。b_bdev
代表该buffer cache 所映射的块设备,该字段和b_blocknr
,b_size
一起唯一决定了该数据所在的磁盘位置。

通用块层
前面说到,高速缓存这一层分为

从buffer_head
的结构中可以看到,每一个
当从高速缓存中读某个
通用块层的核心–bio
掌握通用块层只需要掌握一个数据结构–
struct bio {
sector_t bi_sector; /* device address in 512 byte
sectors */
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short bi_idx; /* current index into bvl_vec */
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
unsigned int bi_size; /* residual I/O count */
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
unsigned int bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/* FOR RH USE ONLY
*
* The following padding has been replaced to allow extending
* the structure, using struct bio_aux, while preserving ABI.
*/
RH_KABI_REPLACE(void *rh_reserved1, struct bio_aux *bio_aux)
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};
bi_sector
代表这次IO 请求的的磁盘扇区号。对于buffer cache ,可以通过b_blocknr * b_size / 512 计算得到。如果是page cache ,则稍微复杂一点,不过page 的第一个磁盘块的逻辑块号也能通过文件的元信息间接计算得到。bio_io_vec
记录了高速缓存层要提交给磁盘的数据。一个bio_io_vec
可看作一个连续的内存段。bi_vcnt
代表内存段的数目。bi_idx
代表当前已经传输到哪个内存段了,这个字段会在传输过程中被修改。bi_bdev
表示该请求指向哪个块设备。bi_rw
表示是读还是写请求。
struct bio_vec {
/* pointer to the physical page on which this buffer resides */
struct page *bv_page;
/* the length in bytes of this buffer */
unsigned int bv_len;
/* the byte offset within the page where the buffer resides */
unsigned int bv_offset;
};

不管是bio_vec
的形式,然后放到bio
结构内。
读page 请求
回忆之前讲到的
以下
const struct address_space_operations xfs_address_space_operations = {
.readpage = xfs_vm_readpage,
.readpages = xfs_vm_readpages,
.writepage = xfs_vm_writepage,
.writepages = xfs_vm_writepages,
.set_page_dirty = xfs_vm_set_page_dirty,
.releasepage = xfs_vm_releasepage,
.invalidatepage_range = xfs_vm_invalidatepage,
.write_begin = xfs_vm_write_begin,
.write_end = xfs_vm_write_end,
.bmap = xfs_vm_bmap,
.direct_IO = xfs_vm_direct_IO,
.migratepage = buffer_migrate_page,
.is_partially_uptodate = block_is_partially_uptodate,
.error_remove_page = generic_error_remove_page,
};
xfs_vm_readpage
。我们不打算去探究该函数的代码细节,而是直接概括一下对于大部分文件系统,xfs_vm_readpage
也只是在这个核心业务的基础上再添加自己的逻辑而已。
- 检查
page 的PG_private 字段,如果是1 ,则该页被用于buffer cache ,就会对该页的每一个buffer cache 都生成一个bio 结构,提交给下一层。 - 如果该
page 是一般的page ,则根据文件元信息计算该page 的第一个文件块的块号以及块数目。 - 分配新的
bio 结构,用page cache 或者buffer cache 的元信息初始化bi_sector
,bi_size
,bi_bdev
,bi_io_vec
,bi_rw
等字段。 - 可能要对
bi_bdev
进行remap ,然后再将bio 提交给下一层。
所谓的块设备
bi_bdev
指向的是bi_contains
以及bd_part
。bi_contains
指向该分区所在的物理磁盘的bi_bdev
,如果该分区是逻辑分区,那么可以通过该字段找到物理磁盘的bi_bdev
结构。bd_part
则保存了物理磁盘的分区描述符hd_struct
,可以通过该结构完成逻辑分区扇区号到整个物理磁盘扇区号的映射。
可以看到,bio_io_vec
,把连续的磁盘块一次读出来,减少

IO 调度程序层
通用块层构建了
请求队列与请求
struct request {
#ifdef __GENKSYMS__
union {
struct list_head queuelist;
struct llist_node ll_list;
};
#else
struct list_head queuelist;
#endif
union {
struct call_single_data csd;
RH_KABI_REPLACE(struct work_struct mq_flush_work,
unsigned long fifo_time)
};
struct request_queue *q;
struct blk_mq_ctx *mq_ctx;
u64 cmd_flags;
enum rq_cmd_type_bits cmd_type;
unsigned long atomic_flags;
int cpu;
/* the following two fields are internal, NEVER access directly */
unsigned int __data_len; /* total data len */
sector_t __sector; /* sector cursor */
struct bio *bio;
struct bio *biotail;
#ifdef __GENKSYMS__
struct hlist_node hash; /* merge hash */
#else
/*
* The hash is used inside the scheduler, and killed once the
* request reaches the dispatch list. The ipi_list is only used
* to queue the request for softirq completion, which is long
* after the request has been unhashed (and even removed from
* the dispatch list).
*/
union {
struct hlist_node hash; /* merge hash */
struct list_head ipi_list;
};
#endif
/*
* The rb_node is only used inside the io scheduler, requests
* are pruned when moved to the dispatch queue. So let the
* completion_data share space with the rb_node.
*/
union {
struct rb_node rb_node; /* sort/lookup */
void *completion_data;
};
/*
* Three pointers are available for the IO schedulers, if they need
* more they have to dynamically allocate it. Flush requests are
* never put on the IO scheduler. So let the flush fields share
* space with the elevator data.
*/
union {
struct {
struct io_cq *icq;
void *priv[2];
} elv;
struct {
unsigned int seq;
struct list_head list;
rq_end_io_fn *saved_end_io;
} flush;
};
struct gendisk *rq_disk;
struct hd_struct *part;
unsigned long start_time;
#ifdef CONFIG_BLK_CGROUP
struct request_list *rl; /* rl this rq is alloced from */
unsigned long long start_time_ns;
unsigned long long io_start_time_ns; /* when passed to hardware */
#endif
/* Number of scatter-gather DMA addr+len pairs after
* physical address coalescing is performed.
*/
unsigned short nr_phys_segments;
#if defined(CONFIG_BLK_DEV_INTEGRITY)
unsigned short nr_integrity_segments;
#endif
unsigned short ioprio;
void *special; /* opaque pointer available for LLD use */
char *buffer; /* kaddr of the current segment if available */
int tag;
int errors;
/*
* when request is used as a packet command carrier
*/
unsigned char __cmd[BLK_MAX_CDB];
unsigned char *cmd;
unsigned short cmd_len;
unsigned int extra_len; /* length of alignment and padding */
unsigned int sense_len;
unsigned int resid_len; /* residual count */
void *sense;
unsigned long deadline;
struct list_head timeout_list;
unsigned int timeout;
int retries;
/*
* completion callback.
*/
rq_end_io_fn *end_io;
void *end_io_data;
/* for bidi */
struct request *next_rq;
};
sector
代表要传送的扇区号。nr_sectors
代表整个请求要传送的扇区数。current_nr_sectors
代表当前bio 还需传输的扇区数。bio
表示请求的第一个bio 结构biotail
表示请求的最后一个bio 结构。
上层提交的
在请求被处理时,下层的设备驱动程序有可能会修改nr_sectors
和current_nr_sectors
字段。
The Linus Elevator
这个调度器是
- 新请求到达时,先看看能不能与队列内已有的请求合并,凡是在磁盘内连续的都可以合并。
- 如果不能合并,则按照磁盘块的顺序插入到正确的位置,始终保持队列是有序的。
- 为了防止请求饿死,当发现有请求在队列的时间过长,将不执行任何合并与排序的优化,而是直接插入到队列末尾。
优点:使得设备驱动器总是进行顺序读写,最大化了吞吐。 缺点:某些请求的延迟会较大,且有可能会饿死。虽然有一定的措施防止请求饿死,但策略还不完善,没有一个时间上的保证。
Deadline
这个是目前我们项目里采用的调度器。这个调度器在继承了

这个调度器维护了
在一般情况下,磁盘的执行队列会从
anticipatory
这个调度器建立在
这个调度器是最复杂的,而且它的功能可以通过配置其他调度器达到相似的效果,因此在
CFQ
叫做完全公平调度器。这个调度器的主要目标在于让磁盘带宽在所有进程中平均分配。该调度器使用多个排序队列(缺省
Noop
最简单的调度器,基本不做什么,不排序,但还是会合并。新请求一般都是插入队尾,跟普通的
设备驱动层
每一类块设备都有它的驱动程序,该驱动程序负责管理块设备的硬件读写,例如
scatter-gather 传送方式
设备驱动程序需要向磁盘控制器发送:
- 要传输的起始磁盘扇区号以及总扇区数。
- 内存区域链表,链表中的每项包含一个内存地址还有长度。
这种
策略例程
每一个请求队列都有自己的
设备驱动程序顺序地处理请求队列中的每一个请求,并设置在数据传送完成时产生中断。当中断产生时,中断程序重新调用策略例程,如果当前请求还没有全部完成,则重新发起请求,否则在请求队列中删除该请求,并处理下一个请求。
如果块设备控制器支持
中断产生时,如果请求没有完全完成,设备驱动程序会修改以下字段:
- 修改
bio 字段使其指向第一个未完成的bio 。 - 修改未完成的
bio 结构,使其bi_idx 字段指向第一个未完成的bio_io_vec 。 - 修改
bio_io_vec 的bv_offset 以及bv_len ,使其表示该内存段中仍需要传送的数据。
块设备文件
以上的讲述基本上是针对普通文件的读写,但还有一种特殊的文件需要关注,就是设备文件(/dev/sda,/dev/sdb
这些设备文件仍然由
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = blkdev_aio_read,
.aio_write = blkdev_aio_write,
.mmap = generic_file_mmap,
.fsync = blkdev_fsync,
.unlocked_ioctl = block_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_blkdev_ioctl,
#endif
.splice_read = generic_file_splice_read,
.splice_write = generic_file_splice_write,
};
可以看到,
每当文件系统被映射到磁盘或分区上,或者显式执行
写回机制
最后介绍一下最为复杂的写回机制。前面我们讲述从通用块层到设备驱动层的时候,主要是以
- 脏页缓存占用太多,内存空间不足。
- 脏页存在的时间过长。
- 用户强制刷新脏页。
write 写page 时检查是否需要刷新。
在
写回架构

每一个磁盘都对应一个work_list
存储了该设备的所有写回任务,每一个写回任务由wb_writeback_work
定义,包括要写回多少页,写回哪些页,是否同步等等。bdi_writeback
结构则定义了写回线程执行的函数,写回线程会在必要性被唤醒,然后执行写回逻辑。bdi_writeback
主要有b_dirty
队列中,b_io
则是所有需要写回的wb_writeback_work
所定义的写回任务就是针对b_io
定义的b_more_io
则是保存所有需要再次写回的b_io
中的b_more_io
中。
定时写回
写回线程会被定时唤醒,检查每一个b_dirty
队列移到b_io
队列。定时写回的任务一般不会从work_list
里面取,而是尽可能多的写回每一个
内存空间不足
当内存空间不足时,内核会尝试释放
用户强制刷新脏页
如果是
write 调用写page 时检查是否需要刷新
每当用户写一个
为了防止写入速度过快,使得高速缓存占用过高,每写一定数量的
除了在写
如果写回线程被唤醒时
writepages
具体的写回业务由具体操作系统的
写回inode 本身
不仅是脏页需要写回,
delay allocation
传统的文件系统会选择则把
open 系统调用的关键参数解析
前面讲的
实际上,在
O_NONBLOCK
该参数不能用于普通文件,加上该参数将以同步非阻塞方式读写文件。
O_SYNC
在写入高速缓存后不马上返回,而是要马上把高速缓存的数据以及文件元信息都写回到磁盘上,当磁盘写成功后返回。相当于每次写完之后调用一下
这里的
O_DSYNC
在写入高速缓存后不马上返回,而是要马上把高速缓存的数据写回到磁盘上,当磁盘写成功后返回。相当于每次写完之后调用一下
O_DIRECT
该参数会绕开高速缓存,而是直接由
O_ASYNC
信号驱动