注,本文中用到的图片来《自存储技术原理分析》一书)
linux 操作系统秉承“一切都是文件”的设计思想,将所有的块设备也看成文件,内核发现一个块设备时候,会通知用户空间,用户空间的udevd后台进程接受到这些消息后,会按照用户指定的规则为他们创建(mknod)块设备文件。
理解块设备文件,关键有两方面,一,从外部表现看,他是属于某个外部文件系统上的一个文件,通常将他们存放在/dev目录下,用户像常规文件一样通过文件名对他们进行访问;二,从内部实现上看,他又可以看做是一个特殊文件系统(bdev)的一个文件,块设备文件的文件逻辑编号和块设备编号一一对应。
一般来说,前一个文件系统被称为宿主文件系统,一般是根文件系统,可以是各种文件系统类型。通过特殊方式来区别常规文件和块设备文件。例如Minix文件系统采用文件磁盘inode中的i_mode表示文件是否对应一个块设备文件,块设备文件的内容是块设备编号(主设备号和此设备号),被保存在块设备文件的磁盘上i结点的i_zone[0]。
而后一个文件系统就是bdev文件系统,他是一个“伪”文件系统,他存在的目的就是建立块设备文件的外部表现和内部实现之间的关系。bdev文件系统只被内核使用,并不需要装载到全局文件系统树上。
块设备的主inode与次inode
与常规文件不同,块设备文件除了上面在根文件系统的inode外,在bdev文件系统中也有一个相应的inode,两个inode之间是通过设备号联系,为了区别,我们将宿主文件系统上的inode叫做块设备文件的次inode(slave inode),将在bdev文件系统上的inode叫做块设备文件的主inode(master inode)。
1 宿主文件系统的块设备inode
磁盘inode,内存inode,inode区别
在linux中,存在一个抽象化的设备目录/dev 该目录下存有指向系统中硬件的特殊文件,这些指向硬件设备的文件,极大的简化了程序员对硬件的操作,程序员可以像访问普通文件一样来访问硬件,而无需使用特殊的接口函数(恩,所以我们可以直接使用open等打开文件方式来访问块设备?)
宿主文件系统上的块设备文件对应的inode特点:
1 i_mode为块设备文件
2 文件内容为块设备编号,保存在磁盘inode中
3 文件长度为0
如果根文件系统是minix file system,则块设备文件的inode示意图如下:
bdev文件系统的块设备inode
在系统中,只有一个bdev file system装载实例,bd_mnt指向他的vfsmount blockdev_superblock指向super_block
每个块设备,在bdev file system中都有一个inode,叫块设备文件的主inode。磁盘与分区各有各自的inode,表示块设备inode的是bdev_inode,有block_device和inode两个域。从inode得到block_device使用I_BDEV宏,从block_device得到inode也是类似的方式。
bdev文件系统还有另一个独特的地方:除了跟dentry外,没有其他的dentry,所有的动作都是在inode上进行的。
bdev文件系统的初始化是在内核启动过程中调用bdev_cache_init函数。
创建分配块设备文件的主inode和slab分配器缓存
注册bdev 文件系统类型bd_type
构造局部设备文件系统树,返回指向vfsmount的指针保存在bd_mnt中,在这个过程中会调用注册时候提供的get_sb而他进而调用bd_get_sb调用get_sb_pseudo为bdev file system构建VFS超级块
主inode与次inode联系
次inode链入(连接件为i_devices)到块设备描述符的次inode链表(以block_device的bd_inodes域为表头)
block_device的次inode链表可能会链入多个次inode,例如,我们分别调用
mknod /dev/sda5 b 5 1
mknod /dev/myhd b 5 1
就会为编号为<5,1>的块设备创建两个块设备文件,各有一个次inode,两个次inode被链入到块设备的次inode链表。而udev为每个块设备只生成真正意义上的设备节点,如果你想为设备节点提供别名,则需要使用符号链接功能。也就是说,这时候有两个inode,一个是到另一个的符号链接。而只有一个真正的次inode被链入到块设备的次inode链表。
对块设备文件的操作转化为对块设备的操作
在磁盘上读取块设备文件的inode(次inode)后,获得块设备号,然后在bdev文件系统中查找对应的inode(主inode),块设备文件的地址空间如下:
这里关键是打开块设备文件的处理,因为打开文件的本质是做好各种数据结构之间的关系,为后续操作做准备。在打开一个文件时候,会调用对应inode的文件操作表中的open方法,对于块设备文件,这个文件操作表是def_blk_fops,事实上,minix file system就是调用init_special_inode函数(fs/inode.c)为块设备文件设置inode操作表的,而这个文件操作表中,open方法被实例化为blkdev_open方法。
blkdev_open有两个参数,一个是次inode,一个是file结构。
调用bd_acquire(inode)根据次inode获取在bdev文件系统中的主inode及block_device。
bd_acquire以次inode为参数,他的i_bdev指向block_device,其他进程可能已经执行过块设备的动作,建立好次inode与block_device的关系,这时候i_bdev应该不为空,我们就增加主inode的引用计数i_count,返回block_device。而如果次inode的i_bdev为空,说明我们还没有block_device,block_device是和主inode一起分配的,其inode编号取决于保存在次inode中的i_rdev的块设备编号,调用bdget在bdev文件系统装载实例中进行分配,最后分配是调用bdev文件系统超级块操作表的bdev_alloc_inode完成,这样分配好block_device后,就建立他与次inode的关系,主要是A)将block_device的地址记录在次inode的i_bdev中;B)修改次inode的i_mapping,将他指向主inode内嵌的地址空间;C)将次inode链入到块设备的次inode链表。
2)将file的f_mapping也指向块设备的主inode内嵌地址空间,这个很关键,他将对根文件系统上块文件的操作“转移”到对bdev文件系统上的主inode的地址空间的操作。
到现在为止,我们仅仅有了block_device,而他只是关联文件系统和底层(块IO子系统)之间桥梁,要让操作得以进行,我们还需要将block_device关联到底层的数据结构,例如gendisk,于是 blk_open调用blkdev_get
blkdev_get 根据block_device获得gendisk及分区编号,返回设备号(这里有疑问,就是在_blkdev_get中,disk->fops->open(bdev,mode)我追踪不到),这样建立了底层数据结构的联系,为通过块设备文件对块设备进行操作做好了准备。
调用bd_claim这个函数会把打开者(这里是file描述符)的信息记录在块设备的bd_holder域中。
对块设备文件的读写作用于块设备之上
现在,块设备文件的文件描述符中f_mapping已经指向块设备文件的主inode,即在bdev文件系统中的inode的地址空间,而他的地址空间操作表在分配这个inode时候被设置为def_blk_aops,其中,readpage,writepage,write_begin,write_end等方法分别被实例化为blkdev_readpage,blkdev_writepage,blkdev_write_begin,blkdev_write_end,前边三个方法都要实现从文件块逻辑编号到磁盘块逻辑编号的映射,涉及的函数是blkdev_get_block(fs/block_dev.c),将两个编号赋为相等的值。