块设备buffer cache机制
在EXT3文件IO踏上新的征程之前,需要介绍一位EXT3文件IO的同伴,他们即将踏上相同的旅程。只不过这位同伴没有经历过EXT3文件系统的精彩,却领略了另外一番略有差别的风情。这位同伴是在块设备写操作时创建诞生的,我们可以称它为块设备IO。
在很多应用中都会直接进行块设备操作,我们常用的命令dd就可以进行块设备的读写。例如:dd if=/dev/sda of=./abcbs=512 count=1 命令可以实现将/dev/sda设备的第一个扇区读入到当前目录的abc文件中。读到这里我们想一下:访问一个块设备文件和一个EXT3文件到底有何本质上的区别呢?说到区别还是可以列举一二的:
1)EXT3是一个通用的文件系统,数据在磁盘上的分布是采用元数据的形式进行描述的。所以,对于一个EXT3文件的读写操作会涉及到元数据和文件数据两种类型。由于这两种数据类型存在相关性,所以为了保证两者之间操作的原子性,EXT3通常会采用日志的方式保证操作的原子性。
2)块设备没有EXT3文件系统那么复杂,写入的数据直接在磁盘存储,不存在任何的元数据信息。
所以,比较块设备和EXT3文件系统,数据的读写方式存在差别。两者之间一个很大的共同点是都存在磁盘访问的性能问题,都可以采用内存对磁盘进行性能优化。EXT3采用page cache对IO性能进行优化,块设备同样可以采用page cache对其进行性能优化。前面我们已经了解到,每个EXT3文件都有一棵radix tree用于维护这个文件内容的page cache,而裸设备可以等价成一个EXT3文件,同样可以采用一棵radix tree对块设备上的数据进行cache维护。所以,在这两者之间是有很大共同点的。
正因为如此,Linux在实现块设备IO操作的时候和EXT3是类似的,对于块设备访问的这个子系统可以称之为bdev文件系统。VFS实现了所有类型文件访问的功能,应用程序所调用的API是完全相同的。对于块设备的访问,穿过VFS层会调用bdev文件系统提供的相关函数。
在初始化块设备的时候,会调用init_special_inode函数初始化这个块设备的inode:
在用户程序调用open函数打开指定块设备的时候,会初始化一个file对象,并且采用上述inode对file对象进行初始化。因此,通过file对象的文件操作方法就可以调用通用的块设备操作函数。Linux中定义的通用块设备操作函数说明如下:
因此,对于一个块设备的写操作,通过write函数陷入内核执行do_sync_write。在do_sync_write函数中会调用blkdev_aio_write函数实现块设备的异步写入过程。blkdev_aio_write函数的核心是__generic_file_aio_write。到此为止,我们发现块设备的所有调用函数和EXT3文件系统基本一样。前面已经分析过,__generic_file_aio_write分成两种情况:Direct_IO和Buffer_IO。对于buffer_IO分成关键的几个步骤:
1)write_begin 2)copy buffer 3)write_end
write_begin和write_end函数是每类文件系统具体相关的方法,对于bdev文件系统,其定义为:
对于EXT3文件而言,write_begin会进行日志操作,块设备文件系统没有这种操作,只会进行一些page页初始化方面的工作。对于write_end函数,EXT3文件系统会清除日志,并且需要通知writeback守护线程去回写数据。对于块设备文件系统而言,write_end函数的主要工作就是将page页标识成脏,然后通知回写线程去处理这个块设备中的脏页。实现设置脏页的函数说明如下:
从整个分析来看,裸块设备的写操作中Cache的机制、原理和EXT3并没有什么本质上的区别。大家都采用了radix tree管理的page cache。如果不采用Direct_IO的方式,那么都会首先将数据写入page cache,然后再通过writeback机制将数据回写到磁盘。整个机制是完全相同的。所不同的是,EXT3和块设备的缓存块大小是不相同的。对于EXT3而言,缓存块大小就是page size,对于块设备而言,缓存块大小会采用一定的策略得到。具体关于buffer cache的缓存块大小参考《Linux中Buffer cache性能问题一探究竟》。
虽然块设备和EXT3文件看上去差别很大,但是,由于系统所要解决的问题基本类似,因此,在IO处理的机制上是类似的。好!言归正传,到目前为止,EXT3文件IO以及块设
备IO都已经准备完毕,writeback回写机制已经已经将这些IO统统回写到底层设备中。这些IO都将离开短暂的page cache,一同踏上块设备层,即将面临块设备层即公平又难以公平的调度处理过程。
块设备层分析
无论是经过EXT3文件系统还是块设备文件,最终都要通过writeback机制将数据刷新到磁盘,除非用户在对文件进行读写的时候采用了DirectIO的方式。为了提高性能,文件系统或者是裸设备都会采用Linux的cache机制对数据读写性能进行优化,因此都会采用writeback回写机制将数据写入磁盘。
通过上述分析我们已经知道,writeback机制会调用不同底层设备的address_space_operations函数将数据刷新到设备。例如,EXT3文件系统会调用blkdev_writepage函数将radix tree中的page页写入设备。在确认需要将一个page页写入设备时,最终都需要调用submit_bh函数,该函数描述如下:
Submit_bh函数的主要任务是为page页分配一个bio对象,并且对其进行初始化,然后
将bio提交给对应的块设备对象。提交给块设备的行为其实就是让对应的块设备驱动程序对其进行处理。在Linux中,每个块设备在内核中都会采用bdev(block_device)对象进行描述。通过bdev对象可以获取块设备的所有所需资源,包括如何处理发送到该设备的IO方法。因此,在初始化bio的时候,需要设备目标bdev,在Linux的请求转发层需要用到bdev对象对bio进行转发处理。
在通用块设备层,提供了一个非常重要的bio处理函数generic_make_request,通过这个函数实现bio的转发处理,该函数的实现如下:
在generic_make_request函数中,最主要的操作是获取请求队列,然后调用make_request_fn方法处理bio。在Linux中一个块设备驱动通常可以分成两大类:有queue和无queue。有queue的块设备就是驱动程序提供了一个请求队列,make_request_fn方法