上一篇博客中《Linux内核块设备总结(一)》[link][https://blog.csdn.net/weixin_37867857/article/details/88316757]介绍了从qemu中启动增加块设备的方法。最后一步gdb调试中提及了blkdev_open函数的系统调用,但是只是提及了系统调用,并没有深度的去解析这个函数。只是我们提及在执行cat /dev/sda操作之后gdb调试从阻塞状态从新进入了命令行调试状态。意味着我们执行了blkdev_open函数。
本章重点是介绍块设备文件系统的框架,然后通过对于blkdev_open函数介绍入手,介绍bdev文件系统,首先大家有这么一个概念:bdev文件系统是一个内核文件系统,也就意味着在用户空间是不可见的文件系统,只有在内核层可见。
在正式介绍块设备之前我们先想想块设备和字符设备有什么不一样的地方?为什么我们非要使用块设备呢?而不是使用字符设备来替代块设备进行管理我们的硬盘,移动硬盘,U盘甚至是光驱呢?
OK,先从字符设备入手,字符设备一次性可以读入1个字节,到N个字节(N<=1页字节大小),如果放入我们的块设备的话,比如机械硬盘,如果使用字符设备通过对于同一个文件lseek读出10个字节大小的数据(注意:这十个字节在磁盘里面的位置,顺序不固定),我们的机械硬盘最多转10圈最少转1圈读出我们的数据了。但是如果我们把这些文件数据放入缓存中呢?效果大大不一样,只需要硬盘磁头转1圈就足够了。这样引出了我们的块设备文件:我们的块设备文件是带有缓存特性的,最小化减少对硬盘的数据交互次数是我们块设备文件系统的设计初衷。这就引出了我们块设备和字符设备的驱动方面的不同了:字符设备和设备交互的过程是以字节为单位的,块设备和设备交互的过程是以数据块为单位的。
块设备和设备交互的数据块的大小一般是512 * 2^n,这是因为我们的物理设备(比如硬盘,U盘等)存储数据都是以512个字节为一个数据块的,这样限定了我们对于数据块的读取方式必须是512的幂次方。

1.块设备文件系统的框架

如下图为linux内核块设备文件系统的总体设计框架。
在这里插入图片描述

1.1 VFS文件系统

VFS文件系统是对于通用文件系统的封装,主要完成用户空间和内核空间文件的交互,比如打开,关闭,删除,新建和增删改查等的操作。
VFS文件系统是一层胶水层,不止粘贴用户控件和块设备层,还用于粘贴其他特殊的文件系统层,比如字符设备,管道文件,socket文件 etc.

1.2 磁盘缓存

用户在读取一个文件的时候并不是先读取磁盘上的文件,而是先判断磁盘上是否缓存了这个文件的一个副本,如果读取的文件内容已经缓存了,那么就对于这个文件进行读取。如果没有对于文件进行缓存,则先缓存再读取。写也是一个道理。
我们日常用到的磁盘缓存最多的是在写文件的时候使用fflush()操作,这个是强制刷入磁盘的操作,这样可以及时让写入更新到磁盘中,防止文件丢失。

1.3 映射层

映射层主要做的有两方面内容:
1.确定文件有多少个block大小;
2.确定文件的inode是否有效,可用,根据文件系统的inode来确数据在磁盘上的位置

1.4 通用块设备层

	通用块设备层主要作用是对于所有的块设备做一个通用的架构:比如一个磁盘有多少个分区,各个分区名字以及表示方式。分区的合并删除就是在这个层表示的。

以下几章会先从块设备驱动设备文件打开,到块设备驱动文件创建,再到最后块设备驱动和设备交互着手去一一剥离块设备驱动的整个执行过程。

1. blkdev_open函数介绍

从上述的执行步骤可以看到,执行cat /dev/sda操作可以执行blkdev_open函数的。那么blkdev_open函数具体做了哪些操作呢?
还有另外一方面:blkdev_open执行打开的是一个文件还是一个设备?
上述最后一个问题困扰了我将近两个月,通过对于分析blkdev_open函数执行的过程,现在我可以肯定的从它的作用方面来侧面的回答大家,blkdev_open函数的作用是连接块设备文件的VFS文件和块设备系统的一个中间层。我们都知道VFS文件系统的最常用的数据结构为struct inode,块设备的最常用的数据结构为struct blockdevice。而blkdev_open函数的作用就是把这两个数据结构组合成一个数据结构:struct blkdev_inode,至于这个数据结构的具体作用留作下文再讨论。

1.1 blkdev_open触发步骤

先不着急回答blkdev_open函数具体执行了哪些操作步骤,先给大家详细解释一下blkdev_open是由哪些操作步骤触发的。之所以把上述一句标红色,是由于这一句理解很重要。
我们在内核代码中搜索blkdev_open的调用者发现只有def_blkdev_ops调用了,而def_blkdev_ops的调用者则是init_special_inode。而init_special_inode则是在设备文件生成的时候调用的,也就是文章刚开始讨论的/dev/sda创建时候调用的。
我们从新梳理一下整个调用流程如下:

在这里插入图片描述
以下是从块设备文件创建到块设备文件打开的块设备驱动代码的整个代码执行过程:

文件创建阶段:

	init_special_inode

这个函数的主要作用是对于在内核层创建一个VFS层inode并把内核层的inode初始化,如果是块设备文件把其赋予def_block_ops,其表示如下:

const struct file_operations def_blk_fops = {
        .open           = blkdev_open,
        .release        = blkdev_close,
        .llseek         = block_llseek,
        .read           = do_sync_read,
        .write          = do_sync_write,
        .aio_read       = generic_file_aio_read,
        .aio_write      = blkdev_aio_write,
        .mmap           = generic_file_mmap,
        .fsync          = block_fsync,
        .unlocked_ioctl = block_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = compat_blkdev_ioctl,
#endif
        .splice_read    = generic_file_splice_read,
        .splice_write   = generic_file_splice_write,
};

其作用就是给设备文件的inode赋予默认的打开关闭lseek等的操作。
文件打开阶段:
文件打开阶段,最重要的当属blkdev_open函数了,其主要作用如下:

	1. 执行获取块设备结构操作,即struct block_device,而struct block_device结构一般都在struct blkdev_inode结构里面,这个结构是内核blkdev文件系统维护的关键结构,这个结构还包含着另外一个inode结构,这是内核维护的另外一个关于块设备的结构;
	注意:这个inode结构有和块设备文件初始创建时创建的inode有区别,区别如下:在获取block_device结构时候获取的是内核blkdev文件系统使用的,在块设备文件初始化时创建的inode是对于块设备文件维护使用的。
	2.执行权限检查。
	3.对于已经获得的block_device结构体进行所有者+1操作。

OK,blkdev_open函数分析到此结束,下一章讲解bdev文件系统。

Logo

更多推荐