目录
VFS结构概览
VFS各个管理结构之间的关系如下:
VFS结构体简述 | |
Super_block | 超级块,用于存储特定文件系统的信息,文件系统挂载的时候会从特定扇区读入内存。 |
super_operations | 超级块操作表,包含超级块本身和索引节点的底层操作 |
inode | 索引节点,包含操作一个文件或者目录的全部信息,文件打开时候从磁盘索引节点读入。 |
inode_operations | 操作索引节点的方法,主要用于对inode原数据的操作,由具体的文件系统实现。 |
File | 代表一个已经打开的文件,与inode不同,一个文件只有一个inode,但是文件每次打开都有一个file |
file_operations | 包含对文件data部分的操作函数,由具体文件系统实现 |
dentry | 目录项缓存,关联文件名和inode的结构,在读取一个文件/目录之后创建一个dentry置于lru缓存中,便于下次打开同样的文件时候可以根据文件名快速查找到对应的inode。 |
dentry_operations | 对dentry的操作,目录项散列值计算,以及目录项与inode关联的解除等等 |
address_space | 页缓存,在文件第一次被打开时会为这个文件创建一个页缓存,文件数据被访问的时候会以页为单位读入内存并插入页缓存红黑树中,以便于下次访问相同位置数据的时候可以直接从内存中拿到 |
address_space_operations | 关联后备存储器与页缓存的操作函数(从后备存储中读入数据插入页缓存或者将页缓存中的页写入后备存储器中),由具体文件系统实现。 |
Task_struct->files | Files数组,存放进程打开的所有文件的file结构 |
文件系统挂载
2.1文件系统类型
每一个文件系统类型都由结构体struct file_system_type来描述,其主要成员描述如下:
file_system_type主要成员 | |
const char *name | 文件系统名,如"ext2" |
struct dentry *(*mount) (…) | 用于文件系统挂载的时候创建超级块 |
void (*kill_sb) (struct super_block *); | 文件系统卸载的时候做清理工作 |
struct file_system_type * next | 文件系统类型注册后通过next链接起来,全局连表头是file_systems |
struct hlist_head fs_supers | 用于链接同一个文件系统的多个超级块,比如挂载发生在不同的命名空间等 |
下面是类型为ext2的文件系统的定义和注册实例:
static struct file_system_type ext2_fs_type = {
.owner = THIS_MODULE,
.name = "ext2",
.mount = ext2_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
MODULE_ALIAS_FS("ext2");
static int __init init_ext2_fs(void)
{
int err;
err = init_inodecache();
if (err)
return err;
err = register_filesystem(&ext2_fs_type);
if (err)
goto out;
return 0;
out:
destroy_inodecache();
return err;
}
2.2文件系统的挂载点
文件系统挂载到某个目录下,这个目录称为挂载点,例如mount -t ext2 /dev/sda7 /mnt/disk2,'/mnt/disk2'就是一个挂载点。每一个已经挂载的文件系统都有一个对应的结构体struct mount来描述,下面是mount结构体部分重要成员描述。我把它分为两个部分:1)目录承接(指针)描述部分,2)文件系统关系(链表)描述部分。
struct mount主要成员 | ||
struct mount *mnt_parent | 指向父文件系统mount结构 | |
struct dentry *mnt_mountpoint | 挂载点在父文件系统中的dentry | |
struct vfsmount mnt |
文件系统根目录dentry,和超级块等信息 | struct vfsmount { struct dentry *mnt_root; //文件系统根目录的dentry struct super_block *mnt_sb; //指向superblock int mnt_flags;//不可执行,禁止setuid,等等标识 }; |
…… | …… | |
struct list_head mnt_mounts | 用于链接到父文件系统的mnt_child | |
struct list_head mnt_child | 用于链接子文件系统 | |
struct list_head mnt_instance | 用于链接到sb->s_mounts,一个文件系统可以多次挂载 |
mount系统调用
mount()是mount系统调用在内核中的实现,函数首先将应用空间中的mount参数拷贝到内核中,然后将工作交给do_mount。do_mount首先根据传递下来的dir_name找到挂载点在父文件系统中的dentry,这个后面会细将,然后根据挂载参数调用不同的挂载函数做进一步挂载动作。
do_remount | 修改一个已经挂载的文件系统的装载选项 |
do_loopback | 回环装载,比如将一个文件作为一个硬盘分区挂载到一个目录下 |
do_change_type | 改变装载标志,可以处理共享装载,从属装载或者绑定装载 |
do_move_mount | 移动一个已经装载的文件系统 |
do_new_mount | 发起一个新的装载,默认装载操作 |
默认装载将走do_new_mount分支,其具体执行流程如下:
get_fs_type:根据挂载的文件提供类型名在链表file_systems查找对应的文件系统类型。
vfs_kern_mount:调用alloc_vfsmnt分配mount结构来管理一个装载的文件系统;调用
mount_fs来创建super_block并设置文件系统根目录的dentry。
do_add_mount:设置文件系统挂载点信息,将mount中的各个链表链接到对应的量表上。
文件打开
3.1 open系统调用
对文件进行操作之前需要先open,open系统调用返回文件描述符fd,文件描述符在当前进程中唯一指定了一个打开的文件。open系统调用的实际工作都委托给了do_sys_open,函数首先调用t_unused_fd_flags获取一个没有使用的编号作为文件描述符。然后调用函数do_filp_open根据文件名找到文件inode,然后创建struct file结构,file结构代表一个打开的文件,它关联了文件的inode。
3.2 file创建和inode查找
无论是文件还是目录都有一个inode,这个inode包含了文件或者目录操作的全部信息。我们在文件系统挂载还是文件打开的的时候都会传递一个路径参数,在对这个路径的文件或者目录操作之前首先是要找到他的inode,函数path_openat就是用于根据路径找到对应inode的。
get_empty_filp:创建空的struct file结构。
path_init:初始化结构体struct nameidata,这个结构用于存放查找的中间值,这里初始化查
找起始位置,根据传入的flag设置初始查找位置为根目录或者当前位置或者打开的目录
文件描述符制定查找位置。
link_path_walk:查找路径中间节点,比如路径/usr/lib/apt/apt-helper中的'/usr/lib/apt/'
do_last: 查找目标文件,比如/usr/lib/apt/apt-helper中的apt-helper
结构体struct nameidata用于保存路径查找过程中的中间结果,其结构体内容和路径节点查找过程如下:
struct nameidata主要成员 | ||
struct path path | 保存查找的中间节点 | struct path { struct vfsmount *mnt;//路径节点所在的文件系统 struct dentry *dentry;//路径节点的dentry }; |
struct qstr last | struct qstr { union { struct { HASH_LEN_DECLARE; }; u64 hash_len; //高32位包含文件名长度低32位包含文件名哈希值 }; const unsigned char *name;//下一个需要查找的文件名 }; | |
struct path root | 查找路径的根节点 | |
struct inode | path.dentry.d_inode | |
…… | …… |
下面是文件/usr/lib/apt/apt-helper的查找过程:
最后对目标文件apt-helper的查找是由函数do_last来完成的,函数首先在lru缓存中查找是否存在对应的dentry,如果没有就调用函数dir_inode->i_op->lookup到具体的文件系统中去查找。
文件数据读取
4.1 read系统调用
file_pos_read: 获取文件读的起始位置。
vfs_read:完成实际的读取工作,它将调用到特定文件系统的函数file->f_op->read_iter,最
终调用到通用读取列程generic_file_read_iter,这个函数后面详解。
file_pos_write:更新读取位置。
4.2 通用文件读取列程
如果设置了IOCB_DIRECT标志标识不经过页缓存直接通过块设备读取文件。但是读取数据之前要将页缓存中的数据刷出到设备中保证读取到的数据是最新的。如果没有设置IOCB_DIRECT标志,就通过函数do_generic_file_read来去读文件,这个函数首先会到页缓存中去找,如果找到了就直接返回,如果没有找到就到文件系统中去读,其代码逻辑如下:
static ssize_t do_generic_file_read(struct file *filp, loff_t *ppos,
struct iov_iter *iter, ssize_t written)
{
........
for (;;) {
........
find_page:
........
page = find_get_page(mapping, index); //在页缓存中找是否有缓存对应的页
if (!page) {
page_cache_sync_readahead(mapping,
ra, filp,
index, last_index - index);//如果页缓存中没有缓存index这个页就发起一个同步预读操作,实际上并没有等待读取完成
page = find_get_page(mapping, index);//预读之后再次查找页缓存中是否存在对应页,如果不存在就跳转到no_cached_page
if (unlikely(page == NULL))
goto no_cached_page;
}
if (PageReadahead(page)) {//如果遇到预读标记页就发起一个异步预读操作
page_cache_async_readahead(mapping,
ra, filp, page,
index, last_index - index);
}
if (!PageUptodate(page)) {//如果找到了相关页但是没有与磁盘做过同步
error = wait_on_page_locked_killable(page);
........
}
page_ok:
........
ret = copy_page_to_iter(page, offset, nr, iter);//如果数据读取到了内存页就将数据拷贝到应用空间
if (ret < nr) {
error = -EFAULT;
goto out;
}
continue;
page_not_up_to_date:
error = lock_page_killable(page);//如果页没有与磁盘同步就在页上等待
if (unlikely(error))
goto readpage_error;
page_not_up_to_date_locked:
if (PageUptodate(page)) {//如果同步完成就跳转到page_ok去将页中数据拷贝到应用空间缓存
unlock_page(page);
goto page_ok;
}
readpage:
error = mapping->a_ops->readpage(filp, page);//调用特定文件系统的地址空间函数readpage读取文件数据到内存页
........
goto page_ok;
readpage_error:
put_page(page);
goto out;
no_cached_page:
page = page_cache_alloc_cold(mapping);//如果缓存中没有找到对应的页就,分配一个页
if (!page) {
error = -ENOMEM;
goto out;
}
........
error = add_to_page_cache_lru(page, mapping, index,
mapping_gfp_constraint(mapping, GFP_KERNEL)); //将分配到的页添加到页缓存(以便下次快速查找)和lru缓存(以便页回收)
goto readpage;
}
out:
........
4.3页缓存预读
预读就是根据结构体file_ra_state中保存预读的信息展开的:
struct file_ra_state主要成员 | |
pgoff_t start; | 预读起始偏移位置 |
unsigned int size | 预读的页数 |
unsigned int async_size | 预读方向上剩余页数,预读标记页开始到预读窗口结束的大小 |
unsigned int ra_pages | 预读窗口的最大长度,在文件打开的时候设置 |
unsigned int mmap_miss | 访问mmap的文件miss的次数计数 |
loff_t prev_pos | 缓存上一次读的位置 |
当读取文件的时候发现所读的数据不在页缓存中就调用函数page_cache_sync_readahead发起一次预读,预读的页数为file_ra_state->size,并设置剩余页数为file_ra_state->async_size的时候触发下一次预读,将(file_ra_state->size-file_ra_state->=async_size)这一页设置标志PG_Readahead用于作为预读触发页。
预读流程
int __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
pgoff_t offset, unsigned long nr_to_read,
unsigned long lookahead_size)
{
......
for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
pgoff_t page_offset = offset + page_idx;
if (page_offset > end_index)
break;
rcu_read_lock();
page = radix_tree_lookup(&mapping->page_tree, page_offset);
rcu_read_unlock();
if (page && !radix_tree_exceptional_entry(page))
continue;
page = __page_cache_alloc(gfp_mask); //分配内存页用于存储预读文件数据
if (!page)
break;
page->index = page_offset;
list_add(&page->lru, &page_pool);
if (page_idx == nr_to_read - lookahead_size) // 在file_ra_state->size-file_ra_state->=async_size位置设置预读标记页
SetPageReadahead(page);
ret++;
}
if (ret)
read_pages(mapping, filp, &page_pool, ret, gfp_mask);//通过特定文件系统函数将文件数据读入内存
…………