文件操作函数在VFS层的实现
参考"Understanding Linux kernel"中的"12.6 Implementations of VFS System Calls"中的介绍。
虚拟文件系统(Virtual Filesystem Switch,VFS)为各种文件系统提供了一个通用的接口,它使得上层进程在进行与文件系统相关的操作时可以使用同一组系统调用,但是系统调用在内核中可以根据不同的文件系统执行不同的操作。
与文件相关的基本操作函数有:open、read、write和close,本文将结合内核源代码分析这些函数在虚拟文件系统中的实现。
1.open()的实现
open系统调用的作用是打开或创建一个文件,并且返回该文件的文件描述符。在内核中,open系统调用主要完成的工作是为此次打开的文件创建file对象,该对象在files_struct
-->fd_array数组中的索引值为返回用户空间的文件描述符。
open系统调用对应的系统调用服务例程为sys_open,不过目前内核已经统一使用SYSCALL_DEFINEn这种方式对系统调用服务例程进行定义。在open系统调用服务例程中又直接调用了do_sys_open函数,它是打开动作的主体函数。
1 | long do_sys_open( int dfd, const char __user *filename, int flags, int mode) |
3 | char *tmp = getname(filename); |
7 | fd = get_unused_fd_flags(flags); |
9 | struct file *f = do_filp_open(dfd, tmp, flags, mode, 0); |
14 | fsnotify_open(f->f_path.dentry); |
用户进程使用open打开文件时将传递文件路径filename,因此该函数第一步先通过getname函数从用户空间读取文件路径到内核空间(用户空间指针转化为内核空间指针),暂存到tmp。通过get_unused_fd_flags函数在当前进程的fd_array数据中找到一个合适的位置,并返回其索引值。
接下来通过do_filp_open函数执行打开文件的核心操作:根据系统调用中的标志参数flags和访问模式mode设置相应的局部变量以便后续使用;根据要打开文件的路径tmp调用path_lookup()函数寻找其inode节点,关于Pathname Lookup,在“Understanding Linux Kernel,3rd”的Section 12.5中有详细介绍,这里不再赘述。如果该inode节点不存在并且设置了O_CREATE标志则通过调用parent inode的create方法在磁盘上创建一个新的磁盘索引节点;调用dentry_open()函数,该函数的参数为在lookup操作中得到的dentry object和文件所在的挂载的文件系统对象,以及access mode flags。dentry_open函数用来创建file对象并对该对象进行初始化,包括:分配一个新的文件对象,并根据系统调用传递的标志和访问模式设置文件对象的f_flags和f_mode字段;根据传入的参数denrty object和文件所在的挂载的文件系统对象初始化file对象中的f_dentry和f_vfsmnt; 使用索引节点的i_fop字段初始化文件对象的f_op字段将,这为将来的文件操作确定了所有的方法;该文件对象插入到超级块指向的打开文件链表中;如果文件对象操作函数集中的open函数有定义则调用它;调用file_ra_state_init()函数来初始化read-ahead数据结构;如果设置了O_DIRECT flg, 检查direct I/O操作是否可在这个文件上执行;最后返回这个文件对象的地址;
如果这个文件对象创建成功,则通过fd_install函数将该文件对象赋值到fd_array数组的第fd个元素中。
综上,在看open()之前最好先看看Pathname Lookup,在“Understanding Linux Kernel,3rd”的Section 12.5中有详细介绍。如果找不到文件则在磁盘中创建文件,最后open()函数创建file对象并对该对象进行初始化, 尤其是file对象的f_op的初始化,为后续的函数调用奠定了基础。
同时,注意open()函数的access mode flags, 尤其是O_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_NONBLOCK(同O_NDELAY),O_SYNC,O_DIRECT,因为这些flags会保存在file对象的f_flags和f_mode中,会面的read、write等函数会用到。
2.read()的实现
读文件系统调用read()和写文件系统调用write()非常相似,read()的作用是根据文件描述符fd读取指定长度size的数据到缓冲区buffer中。该系统调用的实现涉及了内核中对I/O进行处理的各个层次,但是对于VFS层来说实现方法比较清晰。
这里多说一句,dd命令和fio命令中的bs其实就是用户态buffer的大小。
1 | SYSCALL_DEFINE3(read, unsigned int , fd, char __user *, buf, size_t , count) |
7 | file = fget_light(fd, &fput_needed); |
9 | loff_t pos = file_pos_read(file); |
10 | ret = vfs_read(file, buf, count, &pos); |
11 | file_pos_write(file, pos); |
12 | fput_light(file, fput_needed); |
read()和write()系统调用进行非常类似的操作,下面以read()为例来介绍:
在read系统调用对应的服务例程中,首先使用fget_light函数通过fd获取对应的file对象;如果file->f_mode不允许请求的访问(读或写操作)则返回error code-EINVAL;如果file对象没有read()【对写就是write()]或者aio_read方法[对写就是aio_write()方法】,则返回error code -EINVAL;等等.......,一堆错误检查;如果file->f_op->read() 【对写则是file-> f_op-> write()]】定义了,则调用,如果没调用就调用file->f_op->aio_read【对写则是file-> f_op-> aio_write()】;这些会在“Understanding Linux Kernel,3rd”的Chapter 16中有介绍;file pointer也被妥善的更新了;通过fput_light函数释放文件对象;最终返回vfs_read函数的返回值ret,该值则为实际读取数据的长度。
1 | ssize_t vfs_read( struct file *file, char __user *buf, size_t count, loff_t *pos) |
5 | if (!(file->f_mode & FMODE_READ)) |
7 | if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read)) |
9 | if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) |
12 | ret = rw_verify_area(READ, file, pos, count); |
16 | ret = file->f_op->read(file, buf, count, pos); |
18 | ret = do_sync_read(file, buf, count, pos); |
20 | fsnotify_access(file->f_path.dentry); |
21 | add_rchar(current, ret); |
既然说到了这里,下面继续深入挖掘read()函数的底层实现,主要看chapter 16 Accessing Files。
第16章的内容既适用于存储在基于磁盘的文件系统的regular files以及block device files, 这两种files都被称作files。本章内容的起点是第12章VFS中调用read和write之后,到read把需要的数据发送到user mode process以及write把数据标记为ready for transfer to disk这段中间过程,其余的transfer是在14章block device drivers和15章Page cache中介绍的。
访问文件的不同方式:
1. Canonical mode(标准模式):文件打开时, 没有O_SYNC和O_DIRECT(这两个flags没显示标明),那么这种方式就是ASYNC和Page cached,这种模式也是不指定flag时候的默认模式。O_SYNC的意思是Synchronous write(block until physical write terminates), O_DIRECT的意思是Direct I/O transfer(no kernel buffering)。该模式下,read()系统调用阻塞当前进程直到数据拷贝到用户地址空间(kernal允许返回少于请求数量的数据)。而write()系统调用是不同的,只要数据拷贝到page cache中,write()系统调用就结束了。这种情况在本章的“Reading and Writing a File”中介绍。
2. Synchronous mode(同步模式):文件打开时有O_SYNC flag,或者O_SYNC在后来被fcntl()系统调用设置。O_SYNC只影响write,read会被永远block。阻塞进程直到数据被有效的写到Disk之后。这种情况在本章的“Reading and Writing a File”中介绍。
3. Direct I/O mode: 文件打开时有O_DIRECT flag, 任何的read或write传输都是直接从User Mode address space到disk,或者说绕过page cage。在本章的“Direct I/O Transfers”中介绍。
4. Asynchronous mode(异步模式): 文件通过一组POSIX APIs或者特定的Linux系统调用进行asynchronous I/O,这意味着数据传输不会block当前进程,而是,当应用程序继续执行时,它们在后台继续执行。这部分在“Asynchronous I/O”介绍。
5. Memory mapping mode: 这部分在“Memory Mapping”部分介绍。
因此根据file打开方式的不同,后面read和write就有不同的实现方式,即有不同的程序走向。
Reading and Writing a File [注意这里介绍的内容只适用于1标准模式和2同步模式]
在12章VFS中介绍read()和write()系统调用如何实现时,介绍到了最终会调用file对象的read和write方法时,这两个方法可能会是文件系统相关的,例如网络文件系统和磁盘文件系统,read/write是不一样的,但是对于所有基于磁盘的文件系统,其read方法是用一个通用的函数generic_file_read()实现的["Understanding Linux kernel, 3rd"中16.1第二段有介绍,可能是因为基于磁盘的文件系统实现原理大同小异吧,可以用一个通用的函数来读写]。对基于磁盘的文件系统,这些方法找到包含这些要访问的数据的磁盘的物理块,并调用block device driver去进行数据的传输。
下图是"Understanding Linux kernel, 3rd"中16.1第二段和第三段,介绍了read和write的大致原理。
![](https://img-blog.csdn.net/20170224163431067?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ3gxOTg2MjAwNQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
文件系统说白了就是定义了文件在disk上怎么布局存储,即格式。这样就有了:For most filesystems,reading a page of data from a file is just a matter of finding what blocks on disk contain the requested data.
接着往下介绍generic_file_read()函数
该函数用于实现对block device files和大部分基于disk的文件系统的regular files的读操作的。该函数中有两个局部变量,第一个是iovec类型的局部变量local_iov,该变量保存着用户态buffer的起始地址和长度,第二个变量是kiocb类型的局部变量kiocb,用于跟踪正在进行中的sync或者async的I/O操作的完成情况。接着generic_file_read()函数调用__generic_file_aio_read()函数并把上述两个局部变量传给这个函数,这个函数真正从文件中读数据,并返回读取的数据的字节数给generic_file_read(),generic_file_read()也最终返回这个字节数而结束。
下面介绍__generic_file_aio_read()。
该函数是一个被所有文件系统用来实现sync和async读操作的通用的routine。接下来看看该函数如何处理只有O_SYNC这个flag时的流程:
<1> Invokes access_ok()
<2>建立一个数据结构read_descriptor(read_descriptor_t类型),该数据结构用于存储当前的正在进行的读文件的状态,因为是异步的,所以就需要跟踪当前读的状态。
<3> invoke函数do_generic_files_read(), 该函数传入file对象指针, 保存file offset的ppos, read_descriptor, 以及函数file_read_actor,该函数后面会介绍到。
<4> 返回拷贝到用户空间buffer的字节数。
下面介绍上面<3>中的do_generic_files_read()
该函数从disk中读取请求的pages, 并拷贝他们到用户态的buffer中,该函数执行如下动作比较多,如下:
<1> 获取要读取的文件的address_space对象, file-->f_mapping,这下知道address_space在从file读、写数据到page cache的作用了。
<2> 获取address_space对象的owner, 即拥有将被file中的数据填充的pages的inode对象,该inode对象在address_space的host中保存。但如果读取的文件是个block device file,那owner是bdev特殊文件系统中的一个inode而不是file-->f_dentry-->d_inode所指向的inode[这个inode应该是block device所挂载的文件系统的表示该block device的inode], 细节参考15章中内容。
<3>根据file offset计算包含第一个请求的字节所在的page的logical number,并把该logical number保存在local变量index中,同时在local变量offset中保存第一个请求的字节在这个page中的偏移。
<4> 开始一个循环,去读取包含请求数据的所有的pages, 读取的字节数保存在read_descriptor_t的count 中。在循环的一次执行中,显然传输的是一个page, 具体实现请参考“Understanding Linux kernal, 3rd”中章节,比较复杂,不在赘述。概述该过程为:现在page cache中找是否有请求的数据,有而且是有效的就直接拷贝给用户态buffer,如果page cache中的数据是无效的或者page cache中没有就分配一个新的page, 并将该page插入到page cache中。接下来就进行真正的I/O操作了,通过调用address_space对象中的readpage函数,该函数会将数据从disk拷贝到page中, 同时该函数对block devices file 和 regular file有不同的处理。接下来会将page中的数据拷贝到user space的buffer中,在这个过程中调用了file_read_actor()函数,该函数是作为参数传入到do_generic_file_read()中。
上面我们提到address_space的readpage方法对block device file 和对regular file不同,下面分别介绍:
The readpage method for regular files
address_space-->readpage是一个函数指针,对regular file,readpage指向一个wrapper,这个wrapper调用mpage_readpage()函数, 比如EXT3文件系统的readpage方法实现如下:
int ext3_readpage(struct file *file, struct page *page)
{
return mpage_readpage(page, ext3_get_block);
}
The wrapper是文件系统相关的,并且可以提供合适的函数(例如ext3_get_block)去获取正确block。对于文件中的每一个block,这个函数能将相对于文件开头的block numbers转化成相对于磁盘分区开头的逻辑block number,显然只有特定的文件系统自己才能根据自己的文件系统格式规范计算出对应于磁盘分区开头的逻辑block number(对于EXT文件系统,可以查看18章),这里注意区别根据文件的offset 计算包含第一个请求的字节所在的page的logical number,所以文件在page cache和在disk中的位置都是可以计算出来的。ext3_get_block总是使用一个buffer head去存储一些珍贵的信息:关于block device(b_dev field),请求数据在device中的位置(b_blocknr field),以及block的状态(b_state field)。当从disk中读一个page时, mpage_readpage()函数在两种不同的策略之间选择。如果blocks中包含的请求数据在磁盘中是连续的,那么该函数通过使用一个单个bio descriptor把read I/O操作提交给generic block layer。另一种情况,page中的每一个block通过不同的bio descripter来从generic block layer中读取。文件系统相关的get_block函数(EXT3对应ext3_get_block函数)在决定文件中的下一个block是不是磁盘中的下一个block起着关键作用。
The readpage method for block device files
在13章的“VFS Handing of Device Files”这一节以及14章的“Opening a Block Device File”,讨论了kernel如何处理打开block device file的请求。能看到init_special_inode()函数如何建立device inode以及blkdev_open函数如何完成opening phase。
Block devices使用一个保存在相应的block device inode中i_data的address_space对象。不像regular files,其address_space对象中的readpage方法取决于文件所属的文件系统类型,block device files的readpage方法总是相同的。其由blkdev_readpage()函数实现,blkdev_readpage()又调用了block_read_full_page函数:
int blkdev_readpage(struct file *file, struct * page page)
{
return block_read_full_page(page, blkdev_get_block);
}
上述函数又是一个wrapper,blkdev_get_block函数将相对于文件开头的file block number转化成相对于block device开头的logical block number,对block device files,这两个number是一致的。
显然整个读的过程比较复杂,我们只要记住几个关键点,然后将“Understanding Linux kernal, 3rd”作为SPEC或者说工具书,用到的时候查阅即可。
Memory Mapping
类似于把文件传输到内核page cache,Memory Mapping把文件内容映射到用户态的buffer中。
Direct I/O Transfers
有一些很复杂的应用程序(是self-caching的应用程序)想完全控制整个I/O数据传输。因为应用程序有自己的应用层的cache,如果在使用kernel内的page cache,那么数据就会在内存中重复存储而造成内存的浪费,同时因为处理page cache和read-ahead多余的指令而领系统变慢。
正如上面“”Reading and Writing a File”介绍的,Direct I/O read依然是调用generic_file_read()函数,该函数初始化iovec和kiocb descriptors,并调用__generic_file_read()函数,该函数调用generic_file_direct_IO(rw,.....), rw如果为WRITE则为direct write,如果为READ则为direct read。generic_file_direct_IO()执行如下动作:
<1> 获取file对象,以及address_space的地址file-->f_mapping,后面会看到会调用file-->mapping-->direct_IO函数。
<2>如果是WRITE,并且一个以上的process已经创建了该文件的memory mapping,则调用unmap_mapping_range()函数unmap该文件的所有page。
<3> 如果address_space中的radix tree不是空的,即address_page-->nrpages大于0,则调用filemap_fdatawrite()和filemap_fdatawait()函数去flush所有的dirty pages到disk中,因为尽管self-caching应用直接访问文件,但是可能有其他应用程序通过page cache访问文件,因此在进行direct I/O之前先把所有的该文件在page cache内容flush回文件,所以为了避免数据丢失,在进行direct I/O传输之前先将page cache中的数据同步回disk中。
<4>调用address_space-->direct_IO函数。
在大部分情况中,direct_IO函数是一个wrapper, wrap了__blockdev_direct_IO()函数。该函数及其复杂,并调用了大量的辅助数据结构和函数。但是实质上该函数执行了本章中同样的操作:它把要读或者写的数据分开在不同的blocks中,找到数据在disk中的位置,填充一个或者多个bio descriptors,当然数据直接从用户态的buffer中读或者写,而不是先放到内核的page cache中,该用户态数据在iov array的iovec中指定。最终通过调用submit_bio()函数将bio descriptors提交给generic block layer。
3.write函数的实现
write系统调用在VFS层的实现流程与read类似,只不过在出现read的地方将其相应的置换为write。
1 | SYSCALL_DEFINE3(write, unsigned int , fd, const char __user *, buf, |
8 | file = fget_light(fd, &fput_needed); |
10 | loff_t pos = file_pos_read(file); |
11 | ret = vfs_write(file, buf, count, &pos); |
12 | file_pos_write(file, pos); |
13 | fput_light(file, fput_needed); |
当然最终实现写文件操作的函数也是file->f_op->write或者内核中通用的写操作generic_file_aio_write。
4.close()的实现
close系统调用对应的服务例程中,它首先通过fd在文件对象数组中获取文件对象,接着则将fd处的文件对象清空。接下来的大部分工作都通过filp_close函数完成,它主要的工作是调用flush钩子函数将页高速缓存中的数据全部写回磁盘,释放该文件上的所有锁,通过fput函数释放该文件对象。最后返回0或者一个错误码。