1、定义
只有在“打开”了文件以后,或者说建立了进程与文件的“连接”之后,才能对文件进行读写。为了提高效率,Linux的读写操作都是带缓冲的,即写的时候先写到缓冲副本中,读的时候也从缓冲副本中读入。在多进程的系统中,由于同一文件可为多个进程共享,缓冲的作用就更加显著。
Linux文件缓冲设置在文件层的inode结构中。它里面有一个指针i_mapping,它指向一个address_space数据结构(通常这个结构就是inode中的i_data),缓冲区队列就在这个数据结构中。不过,挂载缓冲区队列中的并不是记录块(逻辑磁盘块)而是内存页面。也就是说,文件的内容并不是以逻辑磁盘块为单位而是以页面为单位进行缓冲的。如果一个记录块的大小为1K字节,那么一个页面就相当于4个逻辑磁盘块(记录块)。至于为什么这么做,是为了把文件内容的缓冲与文件的内存映射结合在一起(这也是为什么取名叫i_mapping、address_space,详细参考情景分析P580)。
在文件层是以页面为单位缓冲,但在设备层则是以逻辑磁盘块为单位缓冲。在一个记录块的缓冲区头部即buffer_head结构中有一个指针b_data指向记录块缓冲区,而buffer_head结构本身则不在缓冲区中。
以一个缓冲页面为例,在文件层它通过一个page数据结构挂入所属inode结构的缓冲页面队列,并且同时又可以通过各个进程的页面映射表映射到这些进程的内存空间;而在设备层则有通过若干buffer_head结构挂入其所在设备的缓冲区队列。
数据缓冲区的大小等于逻辑磁盘块(记录块)的大小,为物理磁盘块(扇区)大小的整数倍;同时内存页(文件层缓冲页page)的大小又为逻辑磁盘块的整数倍。在从磁盘读取数据时,文件系统一次读取若干个磁盘扇区大小的数据存放在记录块中,若干个记录块再组成一个内存页。如下图所示:
上面这些都是为了讲明白内存页page与逻辑磁盘块在文件系统所处的位置以及它们之间的关系(因此说到文件缓冲,要分清楚是文件层的缓冲页page还是设备层的逻辑磁盘块缓冲区buffer_head)。缓冲页面page结构除链入附属于inode结构的缓冲页面队列外,同时也链入到一个杂凑表page_hash_table中的杂凑队列中,所以寻找目标页面的的操作也是很高效的。
除了通过缓冲来提高文件读写效率外,还有个措施是“预读”。如果一个进程发动了对某一个缓冲页面的读写操作,并且该页面上不再内存中而需要从设备读入,那么就可以预测,通常情况下它接下去可能会继续往下读写,因此不妨预先将后面几个页面也一起读进来。其实以页面单位的缓冲本身就隐含着预读,因为一个页面包含着多个记录块(通常是4块),只不过预读的量很小而已。现在file结构中其实要维持两个上下文了。一个就是由“当前位置”f_pos代表的真正的读写上下文,而另一个就是预读的上下文。为此目的,在file结构中增设了f_reada、f_ramax、f_raend、f_rawin等几个字段(ra表示read ahead)。
2、文件写
2.1代码分析
1→sys_write()
注意,在调用参数中并不指明在文件中写的位置,因为文件的file结构代表着上下文,记录着在文件中的“当前位置”。
2→→fget_light ()
根据打开文件号fd找到该已打开文件的file结构。而它实质上就是通过调用下面函数实现的。
3→→→fcheck_files ()
struct fdtable *fdt = files_fdtable(files);
if (fd < fdt->max_fds)
file = rcu_dereference(fdt->fd[fd]);
即通过fdtable,根据数组下标fd得到file结构。
fget_file()返回打开的文件file结构后,便开始为写做准备。
2→→file_pos_read ()
很简单,就是返回file中的文件“当前位置”file->f_pos。再就是通过vfs_write()开始了真正的写流程。
2→→vfs_write ()
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
一个进程要对一个已打开的文件进行写操作,应该满足几个必要条件。其一是相应file结构里f_mode字段中的标志位FMODE_WRITE为1.这个字段的内容是在打开文件时根据对系统调用open()的参数flags经过变换而来的。若标志位FMODE_WRITE为0,则表示这个文件是按“只读”方式打开的,所以该标志位为1是写操作的一个必要条件。另外file结构必须包含有具体文件系统的写操作函数。
3→→rw_verify_area ()
这是检查文件是否加锁以及是否允许使用强制锁。
检查了锁之后,就是写操作本身了。具体的文件系统通过其中file_operation数据结构提供用于写操作的函数指针。在2.6内核中,Ext2文件系统的写操作函数指针指向的就是do_sync_write()。
3→→→do_sync_write()
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
首先这段代码是对iovec结构和kiocb结构的初始化。 iovec 主要是用于存放两个内容:用来接收所读取数据的用户地址空间缓冲区的地址(iov_base)和缓冲区的大小(iov_len)。kiocb描述符用来跟踪正在运行的同步和异步I/O操作的完成状态。在Linux内核中,每个IO请求都对应一个kiocb结构体,其ki_filp成员指向对应的file指针,通过is_sync_kiocb可以判断某Kiocb是否为同步IO请求,如果非真,表示是异步IO请求。块设备和网络设备本身就是异步的。调用宏init_sync_kiocb来初始化描述符kiocb,并设置一个同步操作对象的有关字段。主要设置ki_filp字段和ki_obj字段以及在kiocb中设置io读写的位置和长度。
接下来又是调用ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos),进行真正的写,参数就是kiocb和iovec结构变量。可见linux的块设备也是异步IO。
4→→→→generic_file_aio_write ()
5→→→→→__generic_file_aio_write_nolock ()
struct file *file = iocb->ki_filp;
struct address_space * mapping = file->f_mapping;
先从kiocb中获得file结构和address_space结构。
6→→→→→→generic_segment_checks ()
针对iovec段做一些检查。传下来的nr_segs值为1。
/* Performs necessary checks before doing a write
* @iov: io vector request
* @nr_segs: number of segments in the iovec
* @count: number of bytes to write
* @access_flags: type of access: %VERIFY_READ or %VERIFY_WRITE
*
* Adjust number of segments and amount of bytes to write (nr_segs should be
* properly initialized first). Returns appropriate error code that caller
* should return or zero in case that write should be allowed.
*/
6→→→→→→generic_write_checks ()
针对写文件位置和长度做一些检查。
/*
* Performs necessary checks before doing a write
*
* Can adjust writing