一、页缓存
Linux 内核使用 页缓存(Page Cache) 机制来对文件中的数据进行缓存。
1.什么是页缓存?
- 为了提升对文件的读写效率,Linux 内核会以页大小(4KB)为单位,将文件划分为多数据块。
当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为 页缓存)与文件中的数据块进行绑定。 - 如下图所示:
- 如上图所示,当用户对文件进行读写时,实际上是对文件的 页缓存 进行读写。
所以对文件进行读写操作时,会分以下两种情况进行处理: (1)当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户。 (2)当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。 否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。 对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。
2.页缓存的实现
address_space
- 在 Linux 内核中,使用 file 对象来描述一个被打开的文件,其中有个名为 f_mapping 的字段,定义如下:
struct file {
...
struct address_space *f_mapping;
};
从上面代码可以看出,f_mapping 字段的类型为 address_space 结构,其定义如下:
struct address_space {
struct inode *host;
struct radix_tree_root page_tree;
rwlock_t tree_lock;
...
};
address_space 结构其中的一个作用就是用于存储文件的页缓存,下面介绍一下各个字段的作用:
host:指向当前 address_space 对象所属的文件 inode 对象(每个文件都使用一个 inode 对象表示)。
page_tree:用于存储当前文件的页缓存。
tree_lock:用于防止并发访问page_tree导致的资源竞争问题。
从 address_space 对象的定义可以看出,文件的 页缓存 使用了 radix树 来存储。
radix树:又名基数树,它使用键值(key-value)对的形式来保存数据,并且可以通过键(文件偏移量)快速查找到其对应的页缓存。
(内核以文件读写操作中的数据 偏移量 作为键,以数据偏移量所在的 页缓存 作为值,存储在 address_space 结构的 page_tree 字段中。)
- 下图显示了各个结构的关系
二、LinuxVFS之文件打开、读、写逻辑
Linux中所有文件系统都是依靠VFS系统进行协同工作的
- 使用VFS可以利用标准的Unix系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作。
- Unix使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点。 VFS中共有四个主要对象类型分别是: (1)超级块对象,代表一个具体的已安装文件系统,操作对象为super_operations (2)索引节点对象,代表一个具体文件,操作对象为inode_operations (3)目录项对象,代表一个目录项,是路径的一个组成部分,操作对象为dentry_operations (4)文件对象,代表由进程打开的文件,操作对象为file_operations
1.文件打开操作
eg:
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
int main ()
{
int i, f;
FILE *fp;
char string[24];
fp = fopen ("test.dat", "w+");
sprintf (string, "helloworld\n");
fwrite (string, 11, 1, fp);
fclose (fp);
}
内核入口
- 使用strace ./io后,可以发现会调用系统调用open来实现文件的打开
open("test.dat", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
这个系统调用才是内核中的函数,该函数定义在如下:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
do_sys_open()功能如下:
通过函数build_open_flags()来设置需要打开文件的flags(其结构体为open_flags);
通过函数get_unused_fd_flags()获取一个可用的fd;
调用alloc_fd()函数从fd_table中获取一个可用fd,并做些简单初始化得到一个文件描述符;
调用do_filp_open()函数获取file对象;
最后通过fd_install(),建立文件描述符和file之间的关联,即安装在进程的fd数组中。
do_filp_open()功能如下:
要根据文件名字进行搜索,如果不存在需要进行文件创建。
这里相关数据结构是ext4_dir_inode_operations,不同的文件系统会有不同的数据结构,从而指定不同的函数。
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
.lookup = ext4_lookup,
.link = ext4_link,
.unlink = ext4_unlink,
.symlink = ext4_symlink,
.mkdir = ext4_mkdir,
.rmdir = ext4_rmdir,
.mknod = ext4_mknod,
.tmpfile = ext4_tmpfile,
.rename = ext4_rename2,
.setattr = ext4_setattr,
.getattr = ext4_getattr,
.listxattr = ext4_listxattr,
.get_acl = ext4_get_acl,
.set_acl = ext4_set_acl,
.fiemap = ext4_fiemap,
};
- 逻辑流程如下:
到submit_bio(红色框内)后会调用generic_make_request从而进入块层:
2.文件读操作
read系统调用读取文件中的数据,调用链如下:
read()
└→ sys_read()
└→ vfs_read()
└→ do_sync_read()
└→ generic_file_aio_read()
└→ do_generic_file_read()
└→ do_generic_mapping_read()
内核中的读文件基于页的,内核总是一次传送几个完整的数据页。
如果数据不在RAM 中,内核会分配一个新页框,并将文件适当部分填充并放入到页高速缓存,最后把所需读字节复制到进程地址空间中。
我们从系统调用read开始,其系统调用实现如下,相比之前版本使用了ksys_read函数进行重新封装。
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
ksys_read函数如下:
ssize_t ksys_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);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
然后调用了vfs_read函数,该函数是read的具体实现,也是虚拟文件系统读的总开始,很多关于文件系统的监控点都会设置在此函数上.
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_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count);
if (!ret) {
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
ret = __vfs_read(file, buf, count, pos);
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
}
return ret;
}
主要的核心是__vfs_read()函数,其定义如下:
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,loff_t *pos)
{
if (file->f_op->read)
return file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
return new_sync_read(file, buf, count, pos);
else
return -EINVAL;
}
先是使用file的f_op函数集,ext4则是结构体ext4_ ,定义如下,
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
非文件系统的操作函数集如下def_blk_fops,在没有文件系统的时候会使用此处的函数:
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read_iter = blkdev_read_iter,
.write_iter = blkdev_write_iter,
.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 = iter_file_splice_write,
.fallocate = blkdev_fallocate,
};
此外xfs文件系统的操作函数集如下:
const struct file_operations xfs_file_operations = {
.llseek = xfs_file_llseek,
.read_iter = xfs_file_read_iter,
.write_iter = xfs_file_write_iter,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.unlocked_ioctl = xfs_file_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = xfs_file_compat_ioctl,
#endif
.mmap = xfs_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = xfs_file_open,
.release = xfs_file_release,
.fsync = xfs_file_fsync,
.get_unmapped_area = thp_get_unmapped_area,
.fallocate = xfs_file_fallocate,
.clone_file_range = xfs_file_clone_range,
.dedupe_file_range = xfs_file_dedupe_range,
};
其read函数并没有定义,所以调用new_sync_read()函数
static ssize_t new_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;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
iov_iter_init(&iter, READ, &iov, 1, len);
ret = call_read_iter(filp, &kiocb, &iter);
BUG_ON(ret == -EIOCBQUEUED);
*ppos = kiocb.ki_pos;
return ret;
}
通过init_sync_kiocb()来初始化kiocb
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{
*kiocb = (struct kiocb) {
.ki_filp = filp,
.ki_flags = iocb_flags(filp),
.ki_hint = file_write_hint(filp),
};
}
iov_iter_init()用来初始化iov_iter
接着就是调用call_read_iter函数,如下,其实就是调用ext4_file_operations中的 ext4_file_read_iter。
static inline ssize_t call_read_iter(struct file *file, struct kiocb *kio, struct iov_iter *iter)
{
return file->f_op->read_iter(kio, iter);
}
ext4_file_read_iter函数
static ssize_t ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
if (unlikely(ext4_forced_shutdown(EXT4_SB(file_inode(iocb->ki_filp)->i_sb))))
return -EIO;
if (!iov_iter_count(to))
return 0;
#ifdef CONFIG_FS_DAX
if (IS_DAX(file_inode(iocb->ki_filp)))
return ext4_dax_read_iter(iocb, to);
#endif
return generic_file_read_iter(iocb, to);
}
如果是xfs文件系统则调用函数xfs_file_buffered_aio_read(),继而调用generic_file_read_iter函数。
generic_file_read_iter()函数是文件系统的读路径
该函数比较长不列出来了,可以自行观察mm/filemap.c.
该函数是会先根据iocb中打开文件的flag来判断是否是Direct IO,如果是则进入到Direct IO分支,
判断上次写操作是否需要filemap_write_and_wait_range函数同步,确保读到的数据是最新的;
然后调用mapping->a_ops->direct_IO来访问数据,其中dirct_IO是address_space_operations函数集指定的函数,在ext4中是ext4_direct_IO。
static const struct address_space_operations ext4_aops = {
.readpage = ext4_readpage,
.readpages = ext4_readpages,
.writepage = ext4_writepage,
.writepages = ext4_writepages,
.write_begin = ext4_write_begin,
.write_end = ext4_write_end,
.set_page_dirty = ext4_set_page_dirty,
.bmap = ext4_bmap,
.invalidatepage = ext4_invalidatepage,
.releasepage = ext4_releasepage,
.direct_IO = ext4_direct_IO,
.migratepage = buffer_migrate_page,
.is_partially_uptodate = block_is_partially_uptodate,
.error_remove_page = generic_error_remove_page,
};
默认的操作函数集是def_blk_aops
static const struct address_space_operations def_blk_aops = {
.readpage = blkdev_readpage,
.readpages = blkdev_readpages,
.writepage = blkdev_writepage,
.write_begin = blkdev_write_begin,
.write_end = blkdev_write_end,
.writepages = blkdev_writepages,
.releasepage = blkdev_releasepage,
.direct_IO = blkdev_direct_IO,
.is_dirty_writeback = buffer_check_dirty_writeback,
};
如果不是直接IO则调用generic_file_buffered_read。
该函数是通用文件读路径。
循环在内存中寻找所读取内容是否在内存中缓存,如果cache命中失败,使用
page_cache_async_readahead/page_cache_sync_readahead会从磁盘中读取页,并进行预读;
此外,还要判断页是否是最新,以免读到脏数据;
如果非最新,则需要调用address_space_operations中readpage函数进行读操作获取最新页,读页的函数最后都会调用submit_bio;
此外,如果内存已经没有page cache,则需要调用函数page_cache_alloc来进行申请一个page并加入到page_cache_lru,最后通过copy_page_to_iter将内存中数据复制到用户空间。
最后通过函数file_accessed来更新文件访问时间
- 从上面的调用链可以看出,read 系统调用最终会调用 do_generic_mapping_read 函数来读取文件中的数据,其实现如下:
(1)通过调用 find_get_page 函数查找要读取的文件偏移量所对应的页缓存是否存在,如果存在就把页缓存中的数据拷贝到应用程序的内存中。 (2)否则调用 page_cache_alloc_cold 函数申请一个空闲的内存页作为新的页缓存,并且通过调用add_to_page_cache_lru函数把新申请的页缓存添加到文件页缓存和 LRU 队列中。 (3)通过调用 readpage 接口从文件中读取数据到页缓存中,并且把页缓存的数据拷贝到应用程序的内存中。
void
do_generic_mapping_read(struct address_space *mapping,
struct file_ra_state *_ra,
struct file *filp,
loff_t *ppos,
read_descriptor_t *desc,
read_actor_t actor)
{
struct inode *inode = mapping->host;
unsigned long index;
struct page *cached_page;
...
cached_page = NULL;
index = *ppos >> PAGE_CACHE_SHIFT;
...
for (;;) {
struct page *page;
...
find_page:
page = find_get_page(mapping, index);
if (!page) {
...
goto no_cached_page;
}
...
page_ok:
...
ret = actor(desc, page, offset, nr);
...
if (ret == nr && desc->count)
continue;
goto out;
...
readpage:
error = mapping->a_ops->readpage(filp, page);
...
goto page_ok;
...
no_cached_page:
if (!cached_page) {
cached_page = page_cache_alloc_cold(mapping);
...
}
error = add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL);
...
page = cached_page;
cached_page = NULL;
goto readpage;
}
out:
...
}
- 从上面代码可以看出,当页缓存不存在时会申请一块空闲的内存页作为页缓存,并且通过调用
(1)add_to_page_cache_lru 函数把其添加到文件的页缓存和 LRU 队列中。 (2)add_to_page_cache_lru 函数主要完成两个工作: 通过调用 add_to_page_cache 函数把页缓存添加到文件页缓存中,也就是添加到 address_space 结构的 page_tree 字段中。 通过调用 lru_cache_add 函数把页缓存添加到 LRU 队列中。LRU 队列用于当系统内存不足时,对页缓存进行清理时使用。
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
pgoff_t offset, gfp_t gfp_mask)
{
int ret = add_to_page_cache(page, mapping, offset, gfp_mask);
if (ret == 0)
lru_cache_add(page);
return ret;
}
- 逻辑流程如下:
3.文件写操作
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
{
return ksys_write(fd, buf, count);
}
其逻辑同读操作基本是一致的,此处也是调用ksys_write()函数,该函数如下,逻辑同读操作并无二:
ssize_t ksys_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);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
函数接着是调用vfs_write,如下,会做一些写之前的检测,最后会更新进程中的静态统计:
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_mode & FMODE_CAN_WRITE))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (!ret) {
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
file_start_write(file);
ret = __vfs_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
__vfs_write函数的调用如下,函数中会使用file_operations中实现的函数,先判断是否有.write函数,
如果没有则判断是有.write_iter函数,如果有则调用new_sync_write函数。
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
在new_sync_write()函数中,会初始化kiocb,并调用函数call_write_iter()。
call_write_iter()函数会调用file->f_op->write_iter,不同文件系统有不同对应的函数,数据结构体如上篇读中多列。
Ext4为函数ext4_write_iter(),xfs文件系统为xfs_file_write_iter(),无文件系统默认的操作为blkdev_write_iter()。
根据不同的文件系统出现分支。
在ext4_write_iter()函数中,会调用函数__generic_file_write_iter(),该函数会将数据写到文件中。
该函数中判断IOCB_DIRECT,如果是直接写,最后需要调用filemap_write_and_wait_range()函数将page cache中的页刷入到磁盘,并无效化映射的页。
如果不是IOCB_DIRECT,则直接调用generic_perform_write()函数。
generic_perform_write()该函数是ext4文件系统和裸设备写操作的核心,
在generic_perform_write()函数中,会循环的调用iov_iter_copy_from_user_atomic函数,将数据从用户层复制到内核。
其中内核接收用户层数据的时候,使用了结构体iov_iter,代码如下:
struct iov_iter {
int type;
size_t iov_offset;
size_t count;
union {
const struct iovec *iov;
const struct kvec *kvec;
const struct bio_vec *bvec;
struct pipe_inode_info *pipe;
};
union {
unsigned long nr_segs;
struct {
int idx;
int start_idx;
};
};
};
iov_iter结构体其实是iovec的迭代器,iovec描述了在物理内存或虚拟内存中分散的缓存buffer。
通过iov_iter迭代器可以一次进行数据传输的处理非常高效。
struct iovec
{
void __user *iov_base;
__kernel_size_t iov_len;
};
不过在执行iov_iter_copy_from_user_atomic()函数执行会调用a_ops->write_begin来将数据读入到缓存中,
执行完毕后需要将页标记为脏,因为并没有直接刷入到磁盘,这是和直接IO存在差异的地方。
最后结束后,需要调用函数generic_write_sync,如果是IOCB_DSYNC需要调用函数vfs_fsync_range来同步写。
XFS文件系统写
- xfs文件系统与ext4和裸设备存在较大差异,其核心函数是iomap_file_buffered_write。该函数引入的一个参数是操作函数结构体iomap_ops如下,指定了两个函数:
const struct iomap_ops xfs_iomap_ops = {
.iomap_begin = xfs_file_iomap_begin,
.iomap_end = xfs_file_iomap_end,
};
这两个函数类似在ext4文件系统中的a_ops->write_begin和a_ops->write_end。
xfs_file_iomap_begin()会根据IS_DAX(inode),如果不是直接IO,则直接调用函数xfs_file_iomap_begin_delay(),
然后通过函数iomap_write_actor()(调用iov_iter_copy_from_user_atomic())将数据从用户态复制到内核态。
最后刷IO是在file结构体对象释放时候,调用file_operations()中指定的.release函数,
Ext4文件系统对应的release函数是ext4_release(),
xfs文件系统对应的release函数是xfs_file_release(),
通用块对应的release函数是blkdev_close()。
Release函数会触发调用aops->write_pages,最后都会调用submit_bio函数。
这样不会每次io都提交一个请求给块设备,在可扩展性方面得到了较大的提升。
|