一、块设备
这里从文件和页面管理的角度来看块设备。在Linux系统下,根据“一切皆文件”的思想,可以把一个磁盘当做一个文件来读取。为了看一个磁盘,例如第一块硬盘,可以通过hexdump这种通用的工具来显示一个硬盘的原始数据(没有验证是否需要root权限),例如对于一个文件系统来说
[root@Harry malloc]# mount
/dev/mapper/vg_harry-lv_root on / type ext4 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
devpts on /dev/pts type devpts (rw,gid=5,mode=620)
tmpfs on /dev/shm type tmpfs (rw,rootcontext="system_u:object_r:tmpfs_t:s0")
/dev/sda1 on /boot type ext4 (rw)
[root@Harry malloc]# hexdump -v /dev/sda1 | more
……
0000400 c800 0000 2000 0003 2800 0000 b092 0002这里是struct ext4_super_block 结构在设备中的存储内容
0000410 c7dc 0000 0001 0000 0000 0000 0000 0000
0000420 2000 0000 2000 0000 0800 0000 ec5a 4ef5
0000430 ec5a 4ef5 0018 ffff ef53 0001 0001 0000 ef53是ext系列文件系统的超级块签名#define EXT4_SUPER_MAGIC 0xEF53
0000440 3463 4d7b 0000 0000 0000 0000 0001 0000
块设备的一个重要(可能是最重要)作用就是在上面创建文件系统,对于一个文件系统来说,可以认为有两种不同属性的数据,一种是用来存放文件系统自身的管理数据的,例如inode数据(例如ext2文件系统使用的ext2_inode节点),通过看这个数据结构可以知道,其中包含了一个文件的修改时间,文件长度、创建者、访问权限等各种信息,这些和文件的内容不同,它们是用来控制文件,单不是文件本身。根据大家的叫法,这个通常称为“元数据”(metadata),这部分数据一般不是通过read/write接口操作,但是好像也没有找到统一的修改接口,例如touch是通过utime系统调用来实现,而权限相关则通过sys_chmod来修改,当然还有sys_truncate来修改文件长度等操作。这一点对于理解内存的cache和buffer系统还是比较重要的,也就是说,内存的缓冲中不仅有文件内容,还有元数据。对于通过mmap映射的页面来说,mmap将会修改vm_area_struct结构的vm_ops操作,而对于元数据,则需要各个文件系统自己管理。
块设备的一个重要问题就是缓冲问题。因为设备文件的读取一般很慢,例如对于PC中最为常见的硬盘设备,甚至更为早期的磁带设备等。当从设备中读出数据之后,操作系统希望将这些数据在内存中保持尽可能多的时间,这样在之后再次访问这些页面的时候可以避免耗时的设备操作。缓冲固然是好事,这里的问题如果这些缓冲内容被修改了,这些修改内容需要在什么时候由谁将这些设备写回到设备中,如何写回?
根据“谁污染、谁治理”、“谁收益、谁维护”的原理,这里就要考虑是谁希望进行缓冲以及谁在进行缓冲?这两个的答案都是操作系统,操作系统为了减少自己的访问设备时间,把设备内容缓冲在内存中。但是“操作系统”这个概念太笼统,再详细一点说,是内核的缓冲系统(cache)进行了缓冲(注意:不是设备驱动),所以,这个缓冲的写回还是需要始作俑者来执行。
还有一个问题是内存和外设基本管理单位的不一致问题。内存管理是页面为单位,一般是4096字节一个单位,这个单位是CPU硬件中设置的,比如说一个PTE项代表的内容,比如说访问属性设置的最小单位等,总之是硬件而不是软件决定了页面大小。对于硬盘来说,它的对小单位为512字节,也就是一个扇区,这个同样是硬件决定的,例如,你不能启动一次硬盘读操作湖综合写操作来只操作256字节内容。总之它也是一个硬件决定的内容。更糟糕的是,为了提供灵活性,一个文件逻辑相邻的内容可能被放在任意的扇区内,而物理相邻的扇区可能存放的又是不同文件的内容。一般外设都是一个block为单位,这个block是不同文件系统格式化的时候可以设置的一个选项,这样可以将连续扇区分给同一个文件,这样一次就可以连续读取一个文件的更多数据。但是缺点就是可能浪费的空间也更多。假设block为4个扇区,也就是2048字节,那么有一个2049字节的文件,就会占用两个block并浪费2047字节的硬盘。
二、buffer_head
这个结构的主要作用就是为了适配页面缓冲和块缓冲之间的不同。它本身并在缓冲页中,事实上它也没法在缓冲页中,因为缓冲页中需要保存纯粹的缓冲数据,正如struct page不能放入页面中一样。该结构中重要成员包括
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* circular list of page's buffers */
struct page *b_page; /* the page this bh is mapped to */
sector_t b_blocknr; /* start block number */该buffer_head对应设备中逻辑块编号,这是一个块设备认可的可识别值
char *b_data; /* pointer to data within the page */该buffer_head对应的真正数据所在内存位置。
struct block_device *b_bdev;该buffer_head缓冲的数据所在的块设备,这个结构的重要性在于内存数据的写入需要调用该设备的驱动程序来完成。
至于struct page 和 bufffer_head之间的关系,可以看《understanding the linux kernel》第三版中一个幅图片
三、mmap文件页面的读取
do_sync_read--->>>generic_file_aio_read--->>>do_generic_file_read--->>>do_generic_mapping_read--->>>ext2_readpages--->>>mpage_readpages---->>>do_mpage_readpage--->>>ext2_get_block--->>>ext2_get_branch--->>sb_bread--->>>__bread--->>>__bread_slow--->>>submit_bh--->>>submit_bio--->>>generic_make_request
在do_mpage_readpage函数中使用的block的编号都是文件内的编号,也就是用户看到的逻辑内容向块编号之间的一个对应关系。但是正如之前说的,一个文件逻辑相邻的内容可能在一个设备的任意位置,此时就需要通过do_mpage_readpage函数传入的get_block函数来完成这个转换,即将一个文件内的逻辑block编号转换为设备上真正扇区的编号,并将其内容读出来。对于ext2文件系统来说,这个转换和读取函数就是ext2_get_block。当这个转换完成之后,就通过mpage_readpages函数最后的
mpage_bio_submit(READ, bio);
调用来使块设备驱动来读取指定块的内容,这里的调用链分析到这里就可以暂时打住,走到generic_make_request之后已经开始执行每个设备特有的驱动队列。可以认为每个设备都是一个服务器,而各个块的读写则是客户端的请求,服务器可以统筹调度自己的请求队列,通常通过电梯算法来减少寻道时间。
在IO操作请求提交之后,请求者指定页面读取结束时执行bio->bi_end_io = mpage_end_io_read,这个mpage_end_io_read--->>>unlock_page--->>wake_up_page将等待这个页面读取的任务唤醒,对于这里的常见,这个等待位于filemap_nopage函数中
error = mapping->a_ops->readpage(file, page);
if (!error) {
wait_on_page_locked(page);读取请求者在此处进行等待IO操作结束后被唤醒。
四、以块为单位的读取
对于一个设备,同样可以把它看做一些block的线性集合,就像把内存看作是一组页面集合一样。例如可以通过hexdump -CV /dev/hda来显示一个硬盘的原始数据。但是设备文件本身只是代表了一个设备,用户可以通过mknod /dev/shadowhda b 3 0来创建一个新的硬盘设备,此时和默认的/dev/hda相比,它们要公用相同的缓冲页面。为此,内核中专门实现了一个块设备文件系统,也就是linux-2.6.21fslock_dev.c中定义的
static struct file_system_type bd_type = {
.name = "bdev",
.get_sb = bd_get_sb,
.kill_sb = kill_anon_super,
};
对于块设备的默认打开函数,其执行流程为
def_blk_fops.blkdev_open--->>>>bd_acquire
inode->i_bdev = bdev;
inode->i_mapping = bdev->bd_inode->i_mapping;这里修改了打开着inode内的i_mapping指针,默认的inode的这个成员是指向inode结构内嵌的i_data,但是这里对块设备进行了修改,这也就是说,所有的具有相同主设备和次设备号的设备文件,它们的address_space都是相同的,而address_space的相同进而意味着它们的缓冲页面相同,所有所有的块设备将会共享相同的页面缓冲。
list_add(&inode->i_devices, &bdev->bd_inodes);
在这里之后,块设备就和普通的文件操作相同,它有自己的页面读取方法,有自己的地址共享,可以进行缓冲
在某些情况下需要单独读取某个块,例如在将用户提供的字符串形式的路径转换为inode的过程中,需要读取元数据内容,而此时一般都是只需要读取若干个零散块而不是整个页面。这个最为常见的接口为sb_bread,
__bread--->>>>__getblk--->>>__find_get_block--->>__find_get_block_slow
struct inode *bd_inode = bdev->bd_inode;
struct address_space *bd_mapping = bd_inode->i_mapping;这里的i_mapping和前面在bd_aquire中初始化的i_mapping相同,所以此时所有的块设备都会在缓冲区中搜索。
……………………
index = block >> (PAGE_CACHE_SHIFT - bd_inode->i_blkbits);
page = find_get_page(bd_mapping, index);
而如果这个页面不在块设备的缓冲区中,就需要将这个block从设备中读取__bread--->>>__bread_slow--->>>submit_bh中完成,而这个页面同样是在grow_dev_page函数中通过page = find_or_create_page(inode->i_mapping, index, GFP_NOFS);从块设备的address_space中分配一个新的页面,并将它放在块设备的页面缓冲中。
总之,此处实现将设备的块操作同样放在page cache中,这样对于文件系统的一些接口更加统一,例如当需要释放页面的时候进行系统页面扫描。
还有一点,就是块设备的缓冲页面和文件的缓冲页面不会共用。例如,当通过read系统调用需要从设备中读取一些block的内容,即使这些block已经在块设备的页面缓冲中,此时仍然会启动驱动进行再次读取。