Liunx I/O分层
- 虚拟文件系统层 VFS Layer
- Cache(bufer Cache,page Cache)
- 文件系统层(Ext2,ext3,f2fs,nfs,ntfs)
- 通用块层 (block 概念)
- I/O调度层 (调度算法 none)
- 块设备驱动层 驱动
- 物理块设备层
Virtual Filesystem Layer
虚拟文件系统层是内核中为用户空间程序提供文件系统接口的软件层,在内核中进行抽象,允许不同的文件系统实现共存,VFS提供了open,start,read,write,chmod等函数以供调用。
int fd = open(FILEPATH,O_RDONLY);
我们通常是根据上面类似的函数封装,得到一个文件句柄来进行I/O操作。
它存在的主要意义就是屏蔽了不同的文件系统之间的区别,我们无缝切换底层的文件系统,比如ext4切换到F2FS,应用程序是不用做任何修改的。
VFS中包含着向物理文件系统转换的一系列数据结构,如VFS超级块、VFS的Inode、各种操作函数的转换入口等。Linux中VFS依靠四个主要的数据结构来描述其结构信息,分别为超级块、索引结点、目录项和文件对象。
kernel/x/include/linux/fs.h
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
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 *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
...
} __randomize_layout;
内核与不同的文件系统之间可以快速的无缝切换,就是依赖于定义了一套通用的接口,只要对应的文件系统实现了对应的接口,上层也无需关注对应的底层文件系统是什么。
- 常见的文件系统
ext4,f2fs。可以通过查看/proc/filesystems来查看当前内核支持的文件系统列表。nodev指不需要挂载块设备,也称虚拟文件文件系统或者伪文件系统,后文有讲到。
# cat /proc/filesystems
nodev sysfs
nodev tmpfs
nodev bdev
nodev proc
nodev cgroup
nodev cgroup2
nodev cpuset
...
nodev devpts
ext3
ext2
ext4
...
nodev fuse
nodev fusectl
nodev virtiofs
nodev overlay
nodev incremental-fs
f2fs
erofs
...
Buffer Cache (Page Cache)
Linux系统存在两种缓存机制,分别是Page Cache和Buffer Cache, 其中Page Cache缓存文件的页以优化文件IO。Buffer Cache缓存块设备的块以优化块设备IO。为了加快IO访问,操作系统在文件系统和快设备之间使用内存缓存硬盘上的数据,这个就是buffer head。但是基于块的设计,文件系统层和块设备耦合比较紧密,而且文件系统并不都是基于块设备的,比如内存文件系统,网络文件系统等,再加上块和使用页的方式管理内存,因此如果使用物理页面PAGE缓存数据,支持上层文件系统,就可以和内核中的内存管理系统很好的结合。所以综合种种考虑,后来LINUX使用页代替块支持文件系统,缓存硬盘数据,相对于buffer cache,这些页面的集合相应的称之为page cache。
以上是Linux中比较多的介绍,这次我想从另外一个角度去分析这个问题,drop_caches。
drop_caches
drop_caches是内核对用户态暴露的一个修改sysctl参数的接口,对应的proc文件是/proc/sys/vm/drop_caches,默认值是0,通过修改该值,可以达到释放部分内存的目的。针对sysctl内核参数都会有对应的handler处理函数,drop_caches对应的handler是drop_caches_sysctl_handler。
kernel/xx_xx/fs/drop_caches.c
int drop_caches_sysctl_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
...
//如果是1
if (sysctl_drop_caches & 1) {
// 传递drop_pagecache_sb 函数指针
iterate_supers(drop_pagecache_sb, NULL);
count_vm_event(DROP_PAGECACHE);
}
...
return 0;
}
如果传入参数1,就会调用iterate_supers,并传入drop_pagecache_sb的函数指针。
kernel/xx_xx/fs/super.c
void iterate_supers(void (*f)(struct super_block *, void *), void *arg)
{
struct super_block *sb, *p = NULL;
spin_lock(&sb_lock);
//Liunx中将所有的文件系统挂载时创建的超级块形成一个链表
list_for_each_entry(sb, &super_blocks, s_list) {
if (hlist_unhashed(&sb->s_instances))
continue;
sb->s_count++;
spin_unlock(&sb_lock);
down_read(&sb->s_umount);
//根节点已经初始化完成,处于可用状态
if (sb->s_root && (sb->s_flags & SB_BORN))
f(sb, arg); //重点 ,找到了对应的超级块,将超级块传入
up_read(&sb->s_umount);
spin_lock(&sb_lock);
if (p)
__put_super(p);
p = sb;
}
if (p)
__put_super(p);
spin_unlock(&sb_lock);
}
上面的代码中循环遍历设备中的超级块,找到处于可用状态的超级块,调用drop_pagecache_sb函数
kernel/xx_xx/fs/drop_caches.c
static void drop_pagecache_sb(struct super_block *sb, void *unused)
{
struct inode *inode, *toput_inode = NULL;
spin_lock(&sb->s_inode_list_lock);
list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
spin_lock(&inode->i_lock);
/*
* We must skip inodes in unusual state. We may also skip
* inodes without pages but we deliberately won't in case
* we need to reschedule to avoid softlockups.
*/
if ((inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) ||
(inode->i_mapping->nrpages == 0 && !need_resched())) {
spin_unlock(&inode->i_lock);
continue;
}
__iget(inode);
spin_unlock(&inode->i_lock);
spin_unlock(&sb->s_inode_list_lock);
invalidate_mapping_pages(inode->i_mapping, 0, -1);
iput(toput_inode);
toput_inode = inode;
cond_resched();
spin_lock(&sb->s_inode_list_lock);
}
spin_unlock(&sb->s_inode_list_lock);
iput(toput_inode);
}
上面的代码中遍历super_block中的inode节点,异常节点则直接跳过,可用节点则调用invalidate_mapping_pages函数。
kernel/x/mm/truncate.c
unsigned long invalidate_mapping_pages(struct address_space *mapping,
pgoff_t start, pgoff_t end)
{
return __invalidate_mapping_pages(mapping, start, end, NULL);
}
static unsigned long __invalidate_mapping_pages(struct address_space *mapping,
pgoff_t start, pgoff_t end, unsigned long *nr_pagevec)
{
pgoff_t indices[PAGEVEC_SIZE];
struct pagevec pvec;
pgoff_t index = start;
unsigned long ret;
unsigned long count = 0;
int i;
pagevec_init(&pvec);
while (find_lock_entries(mapping, index, end, &pvec, indices)) {
for (i = 0; i < pagevec_count(&pvec); i++) {
struct page *page = pvec.pages[i];
/* We rely upon deletion not changing page->index */
index = indices[i];
if (xa_is_value(page)) {
count += invalidate_exceptional_entry(mapping,
index,
page);
continue;
}
index += thp_nr_pages(page) - 1;
ret = invalidate_inode_page(page);
unlock_page(page);
/*
* Invalidation is a hint that the page is no longer
* of interest and try to speed up its reclaim.
*/
if (!ret) {
deactivate_file_page(page);
/* It is likely on the pagevec of a remote CPU */
if (nr_pagevec)
(*nr_pagevec)++;
}
count += ret;
}
pagevec_remove_exceptionals(&pvec);
pagevec_release(&pvec);
cond_resched();
index++;
}
return count;
}
起始地址是0,结束地址是-1,这里的0和-1不是传统意义上的数学概念,-1会被转化为LONG_MAX类似的概念,也就是表示的末尾。先找到所有的pagevec,然后for循环每一个page,如果当前page页是Transparent Huge Pages透明大页,则跳过。然后调用invalidate_inode_page去尝试释放页面关联数据。
int invalidate_inode_page(struct page *page)
{
struct address_space *mapping = page_mapping(page);
if (!mapping)
return 0;
if (PageDirty(page) || PageWriteback(page))
return 0;
if (page_mapped(page))
return 0;
return invalidate_complete_page(mapping, page);
}
上面的函数中会先对page进行检查,如果没有关联的地址空间,当前页为脏页或者正在写回或者检查当前页是否被共享文件映射的页,都会返回0。然后才会调用invalidate_complete_page进行最终的释放操作。后续的流程中就没有出现更关键的数据结构了,所以这里就没有继续追踪下去。感兴趣的可以自行查看源码。
/ # cat /proc/meminfo
...
Dirty: 348 kB
Writeback: 0 kB
Mapped: 190948 kB
...
无法被释放的内存查询如上。
小结
以上的代码中,我们总结一下,我们跟踪了drop_caches中提供的清理页缓存的代码
- 1:遍历系统中的
super_blocks链表,找到已经初始化完成,且处于可用状态的super_block - 2:遍历
super_block中的s_inodes,它也是一个链表,跳过异常的inode节点。 - 3:遍历
inode节点所指向的地址段*i_mapping,遍历地址段中的所有page。 - 4:最后就是在判断当前页有实际的关联地址,且不为脏页不被共享。然后在去释放对应的页。
根据以上的总结整个流程,剩下的就是我去理解其中的重要的数据结构。然后在思考一个问题,整个文件系统中那么多环节,硬件,驱动,调度算法,中间那么多环节,都是可能出现变动的,它是不是靠这些数据结构去协调整个流程得,我们后面一层一层的去分析。
Direct I/O
直接I/O直接访问磁盘数据,减少了一次数据拷贝和一些系统调用的耗时,能够降低CPU的使用和内存占用。它的负面就是相当耗时,会造成磁盘的同步读,我们需要使用异步I/O去配合使用。
Android没有Java直接I/O的API,需要我们在Opne文件的时候,指定O_DIRECT参数。使用直接I/O相当于绕开了page Cache这一层,一定要充分的测试,和确定缓存I/O开销很大。
Android内核文件系统
在Android中通常支持debugfs,overlayfs,procfs,sysfs,tmpfs,tracefs。
debugfs,procfs,sysfs都是内核开发者从内核空间和用户空间进行交换数据使用。
procfs只提供进程信息和设备的常用信息。
sysfs管理设备驱动的文件系统,Linux2.5开发,严格的要求每个文件一个值。
debugfs是一种用来调试内核的内存文件系统。在内核调试时,我们可以通过printf()来输出信息,更开放,没有具体的约束。通常挂载在/sys/kernel/debug, CONFIG_DEBUG_FS=y,就会自动挂载。
overlayfs是一种堆叠文件系统,用于remount操作。overlayfs详细介绍:
不要把这个和虚拟文件系统层混淆即可。
数据结构
kernel/x/include/linux/fs.h
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; /* 文件系统类型 */
...
struct dentry *s_root; /* 指向根目录的指针*/
struct rw_semaphore s_umount;
int s_count; /* 引用计数*/
atomic_t s_active;/*活动引用计数,表示被mount的次数*/
...
/*
* The list_lru structure is essentially just a pointer to a table
* of per-node lru lists, each of which has its own spinlock.
* There is no need to put them into separate cachelines.
*/
struct list_lru s_dentry_lru;
struct list_lru s_inode_lru;
struct rcu_head rcu;
struct work_struct destroy_work;
...
/* s_inode_list_lock protects s_inodes */
spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp;
struct list_head s_inodes; /* all inodes */ //inode 链表
spinlock_t s_inode_wblist_lock;
struct list_head s_inodes_wb; /* writeback inodes */
} __randomize_layout;
主要的数据结构都存在fs.h的头文件中,有兴趣的可以去详细的看看。