当前课程知识点:Linux 内核分析与应用 > 第9章 设备驱动 > 9.7工程实践-编写块设备驱动的基础(上) > Video
大家好我是《奔跑吧Linux内核》一书的作者笨叔
今天我给大家简单介绍一下如何编写一个简单的块设备驱动程序
在前面的内容中我们介绍了Linux
设备驱动程序中最常见的一种驱动
字符设备驱动
它是以字节流为单位的设备
今天要介绍的块设备驱动
它的最大特点是以块为单位进行传输的一种设备
这个特性和我们常见的磁盘非常吻合
在介绍块设备驱动之前
我们来简单回顾一下传统的机械硬盘的一些基本结构
它是由磁头(head)柱面(cylinder)扇区(sector)等组成
那什么是磁头呢
磁头是固定在可移动的机械臂上用来读写数据的
那什么是磁道呢
磁道是每个盘面都有多个同心圆组成
每个同心圆我们称之为一个磁道
那什么是柱面呢
多个盘面相同的磁道 (位置相同) 就共同组成了一个柱面
那什么是扇区呢
从磁盘中心往外画一条直线
那么可以将磁道划分为若干个弧段
每个磁道上的一个弧段
我们称之为一个扇区
那扇区是磁盘最小的组成单元
通常是 512 个字节
所以像磁盘这样的硬件设备
它的最小读写单元就是一个扇区(sector)
从磁盘的特性我们可以看到
块设备另外一个非常显著的特点
就是块设备是可以随机访问的
比如同一个文件
两个扇区大小的内容
在文件中是顺序存放的
但是在磁盘里
它可能不是顺序存放的
它有可能存放在两个不同的磁道上
的两个不同的扇区里面
而我们以键盘来做字符设备的话
比如我们输入“hello world”这几个字符
我们的键盘驱动只能以
键盘发出的“hello world”字节流来接收
我们不能只接收第一个字符然后跳过第二个字符
直接去接收第三个字符
这样是不行的
这就是字符设备和块设备第二个显著特点
除了传统的机械硬盘和光盘
其实还是有很多其他的块设备和存储技术
比如ramdisk SSD RAID
存储网络虚拟化存储等等
下面和大家介绍块设备驱动的整体结构这幅图是
块设备驱动在Linux内核中的架构框图
最上面的是文件系统层
包括我们最常用的文件系统比如EXT4ramfs等等
这里面还包括Linux的虚拟文件系统层VFS
对于块设备来说
我们通常不会像字符设备驱动一样
在应用程序中直接打开和操作块设备
而是通过文件系统来访问块设备的
第二层是通用块层
Generic Block Layer
这一层为各种类型的块设备
它建立一个统一的模型
这一层的主要工作是
接收文件系统层发出的磁盘请求
并最终向磁盘设备发出IO请求
所以它隐藏了很多底层硬件块设备的细节
为块设备提供一个抽象的一个模型
那 第三层是IO调度层
这个IO调度层
接收通用块层发出的IO请求
缓存这些请求然后根据IO调度算法
来合并相邻的请求
然后调用驱动层提供的处理函数
发送这些IO请求到硬件设备
这里IO调度层有多种调度算法
比如电梯调度算法
CFQ调度算法
因为传统机械硬盘物理结构特性
内核不会简单的按照请求
的顺序来提交给
块设备的那么IO调度器会根据场景进行优化
来进行合并和排序这样一些预操作
最下面的一层就是
块设备驱动
这一层就是实际的
块设备的驱动程序了比如磁盘驱动
光驱驱动 SSD驱动
一般来说
块设备驱动包含块设备的注册
打开关闭以及具体的IO处理
从上面的介绍我们知道了Linux内核的块设备之系统的架构
也了解了块设备驱动为了解决上层应用程序访问块设备
和底层块设备硬件之间的衔接问题
还建立了一个通用块层
但是要深入了解块设备驱动
我们还需要了解Linux内核为块设备驱动
设计的哪些主要的数据结构
要去理解为什么要抽象和设计这些数据结构
那我们首先介绍第一个数据结构就是block_device
数据结构
block_device数据结构用来抽象和描述一个块设备
Linux内核通常把
block_device数据结构和虚拟文件系统VFS关联起来
它的作用有点类似
虚拟文件系统和块设备子系统的一个粘合剂
用户程序一般是不会直接操作块设备的
通常是通过文件系统的接口来访问块设备
我们来介绍第二个数据结构
gendisk
Linux内核把磁盘类的设备
关于磁盘通用的那部分
提取出来使用一个gendisk
这样的一个数据结构来描述它
所以它实际上也是一种抽象
那么这个gendisk数据结构
它是可以表示一个已经分区的磁盘
也可以表示一个未分区的磁盘
那我们来看一下
gendisk它的一些重要成员
第一个是major
那major它表示的是这个
块设备的主设备号
那么它通常用来指定
当前设备对应的哪个设备驱动
那么下面的一个成员叫first
minor和minors
这两个是用来表示从设备号的一个范围
那么minors表示当前
这个gendisk对象所能包含的最大从设备个数
disk_name表示当前
块设备对象名称
part
tbl表示gendisk对象
的分区表信息fops
表示块设备的一组文件操作集合
queue它表示
当前这个gendisk对象
所代表的块设备的IO请求队列
下面我来介绍第三个非常重要的数据结构
就是block_device_operations
对于字符设备驱动
Linux内核里面有一个整套的操作方法
包含在file_operations数据结构里
那么对于块设备驱动而言Linux内核
也为它准备了一套类似的操作方法集
相对于字符设备的操作方法
块设备的函数
操作方法集明显要弱化了很多
这里主要的操作方法有
open打开块设备release
关闭块设备ioctl用来实现块设备的标准命令
media_changed 检查驱动器的介质是否改变
getgeo
获取磁盘器的一些信息
大家发现块设备的操作方法集
里已经没有了read write的这个方法了
并不是说块设备不需要实现读写函数
正是因为块设备的一些特性
已经把块设备的读写方法
使用了特别的读写请求队列来实现了
这个差异也是块设备和字符设备的一个重要的特点
块设备一般来讲
和系统交互的数据量是很大的
而字符设备一般来说交互的数据量不大
那么因此对于块设备子系统来说
针对这些不同的需求和特性
Linux内核进行了单独的优化
下面我们来介绍第四个数据结构就是请求队列
request_queue数据结构
那么对于来自通用块层的请求
在提交到块设备驱动的时候
我们需要构造一个请求就是request
也要把这些request
插入到块设备的请求队列里面
那么因此文件系统
有新的请求时就会把新的请求
加入到这个
请求队列里面
这里面通常会通过submit_io这个API
把来自文件系统的这个请求
添加到请求队列里面
只要请求队列不为空那么
队列里面对应的这些块设备驱动
程序就会从请求队列里面获取请求
然后把这些对应的请求发送到具体的块设备驱动里去处理
下面我们来介绍第五个数据结构bio数据结构
块设备子系统最重要的一个功能就是把文件系统层
发来的数据请求进行处理
而这个处理过程中最核心的一个数据
对象就是bio
那么bio有点类似一个网络的协议包
那么它在文件系统和块设备子系统里会来回地流动
把要读的数据
和要写的数据来回地搬移
通常来讲bio
它代表来自文件系统的一个请求
每个bio都代表不同的上
访问上下文
可能来自不同的文件系统
或者来自不同的应用程序
所以bio对象包含了
块设备处理一个请求
他所需要的所有的信息
上面给大家介绍了
块设备系统里面最常用的数据结构
那么这些数据结构怎么和文件进行一个关联呢
下面我们举一个例子
比如说文件系统里面有一个
test文件
我们在用户空间里使用
open函数来打开它
那么在程序中它就会
返回一个文件句柄fd
那么在内核空间里
这个被打开的文件
怎么和存储它的块设备关联起来的
我们来看这个图
当在用户空间打开一个文件的时候
在内核空间就会有一个
file数据结构和它对应
这个file数据结构有一个f_mapping字段
它是address_space数据结构
那么 这个数据结构是用来管理文件映射
到内存页面的
那么这个数据结构里面有一个
host这一个成员
它是inode数据结构
那么这个inode数据结构就是索引节点
用来存放文件和目录的基本信息
每个文件或者目录都有一个inode
那么这个inode数据结构里面有一个i_bdef
成员
那么它就是我们今天讲过的block_device数据结构
那么在这个block_device数据结构里面
有一个叫做bd_disk成员那么它指向了
gendisk数据结构
在gendisk数据结构里面有一个queue成员
它指向request_queue数据结构
那么来自文件系统的一个个请求
会被添加到request_queue队列里面
这个就是我们说的请求队列了
那么实际上的块设备驱动
就会从这个请求队列里面取request
然后进行处理最后把处理发送到硬件设备里面
这样就完成了一个完整的处理流程了
我们了解了块设备中的主要数据结构的设计理念
下面和大家介绍一下块设备驱动中
最常用的API接口函数
第一个是块设备的注册函数register_blkdev函数
这个函数有两个参数
major参数是块设备使用的主设备号
第二参数name是设备名
如果第一个参数major等于0的话
那么内核会自动分配一个新的主设备号
这个函数成功的话会返回一个主设备号
如果返回负数的话表示失败
第二个函数是注销函数
unregister_blkdev
它也有两个参数
传递的参数必须和register
blkdev
函数保持一致
下面介绍第三个API接口函数
初始化请求队列blk_init_queue
这个函数为当前设备分配一个request请求队列
第一个参数rfn
表示请求处理函数
它是一个函数指针
第二个参数lock
是请求队列要使用的一个锁
那么通常我们
设备驱动里面需要初始化这个锁
那么第四个函数是
注销请求队列
blk_cleanup_queue
下面我们看下一个
API函数分配gendisk对象
alloc_disk这个函数动态分配一个gendisk数据结构
第一个参数minors表示这个磁盘使用的次设备号的数量
也就是磁盘分区的数量
那么下一个函数是
注册gendisk对象
这个函数是add_dissk
那么这个函数会
向Linux内核注册一个磁盘设备
这个和字符设备的
cdev_add函数非常类似
都是向系统注册一个设备
这个函数的参数是刚才使用alloc
disk函数分配的gendisk的对象
那接下来一个
接口函数是释放gendisk函数
del_gendisk这个函数
当磁盘设备不再使用的时候
我们可以使用这个函数来释放一个gendisk对象
那么下面一个
接口函数是初始化一个BIO
大家可以使用 bio_init这个函数
那么接下来是一个提交BIO
submit
bio这个函数
当需要和块设备进行数据传输
或者对块设备发送IO请求的时候
内核需要发送请求对象bio到请求队列里面
那么时候可以调用submit
io接口函数来完成
那么这个函数有两个参数
第一个参数rw
表示发送请求是读呢还是写
那么第二个参数bio对象
如果
驱动程序需要单独调用submit
bio这个API的时候
那么驱动程序需要去负责填充bio
实验是最好的学习方法
我们在这里留了一个块设备的实验
我们使用
系统的内存作为存储介质来构造一个块设备
这个就是ramdisk
那么这个实验要求大家编写一个
块设备驱动来生成一个ramdisk
要求在这个ramdisk里面
可以使用ext4文件格式工具来进行格式化
然后对其进行读写操作
比如说我要存一个文件
或生成一个目录等等
另外还需要实现一个ioctl的操作来实现
实现HDIO_
GETGEO这个ioctl的命令
那么这个命令可以
读出这个块设备的一些参数
比如说有多少个磁头
多少个柱面
多少个扇区信息
那么最后要求写一个简单的用户
空间的测试程序
来读出这个块设备的一些参数
那么在节目最后啊
我们留一个有一点难度的思考题
有兴趣的同学可以顺着这个思考题去阅读Linux内核的代码
我们假设系统使用eMMC作为存储介质
文件系统采用ext4
那么使用C语言的open函数来打开一个文件
然后使用write函数
来写入内容
比如字符串“hello world”
请阅读Linux内核代码
跟踪从用户空间write函数
到eMMC存储设备
了解这个写入了动作
它经历了内核中哪些内核模块
这期间里面
它使用了哪些内核主要的数据结构
然后画出代码的流程图
那这个就是
这个进阶的思考题
这个思考题的阅读量是很大的
它涉及到虚拟文件系统层VFS
ext4文件系统
page cache管理
还有我们今天讲的bio的管理
还有我们今天讲的通用块层
以及整个块
设备的子系统
还有eMMC的块
设备驱动的程序等等
那么对理解
Linux内核整个存储架构是非常有帮助的
那我们今天的节目就到这里
谢谢大家
-1.1 Linux操作系统概述
-1.2 Linux内核结构以及内核模块编程
--Video
-1.3 Linux内核源码中的双链表结构
--Video
-1.4 源码分析-内核中的哈希表
--Video
-1.5 动手实践-Linux内核模块的插入和删除
--Video
-第1章 概述--章节测验
-2.1 内存管理之内存寻址
--Video
-2.2 段机制
--Video
-2.3分页机制
--Video
-2.4 动手实践-把虚拟地址转换成物理地址
--Video
-第2章 内存寻址--章节测验
-3.1 进程概述
--Video
-3.2 Linux进程创建
--Video
-3.3 Linux进程调度
--Video
-3.4 动手实践-打印进程描述符task_struct中的字段
--Video
-3.5工程实践-基于内核模块的负载监控
--Video
-第3章 进程管理--章节测验
-4.1 Linux内存管理机制
--Video
-4.2 进程用户空间管理机制
--Video
-4.3 物理内存分配与回收机制(上)
--Video
-4.4 物理内存分配与回收机制(下)
--Video
-4.5 动手实践-Linux内存映射基础(上)
--Video
-4.6 动手实践-Linux内存映射实现(中)
--Video
-4.7 动手实践-Linux内存映射测试(下)
--Video
-4.8 初学者对内存管理的常见疑惑
-第4章 内存管理--章节测验
-5.1 中断机制概述
--Video
-5.2 中断处理机制
--Video
-5.3 中断下半部处理机制
--Video
-5.4 时钟中断机制
--Video
-5.5 动手实践-中断上半部的代码分析及应用
--Video
-5.6 动手实践-中断下半部的代码分析及应用
--Video
-第5章 中断--章节测验
-6.1 Linux中的各种API
--Video
-6.2 系统调用机制
--Video
-6.3 动手实践-添加系统调用(系统调用日志收集系统)
--Video
-第6章 系统调用--章节测验
-7.1 内核同步概述
--Video
-7.2 内核同步机制
--Video
-7.3 动手实践-内核多任务并发实例(上)
--Video
-7.4 动手实践-内核多任务并发实例(下)
--Video
-第7章 内核同步--章节测验
-8.1 虚拟文件系统的引入
--Video
-8.2 虚拟文件系统的主要数据结构
--Video
-8.3 文件系统中的各种缓存
--Video
-8.4 页高速缓存机制以及读写
--Video
-8.5 动手实践-编写一个文件系统(上)
--Video
-8.6 动手实践-编写一个文件系统(中)
--Video
-8.7 动手实践-编写一个文件系统(下)
--Video
-第8章 文件系统--章节测验
-9.1 设备驱动概述
--Video
-9.2 I/O空间管理
--Video
-9.3 设备驱动模型
--Video
-9.4 字符设备驱动程序简介
--Video
-9.5 块设备驱动程序简介
--Video
-9.6 动手实践-编写字符设备驱动程序
--Video
-9.7工程实践-编写块设备驱动的基础(上)
--Video
-9.8 工程实践-块设备驱动程序分析(中)
--Video
-9.9 工程实践-块设备驱动程序实现(下)
--Video
-第9章 设备驱动--章节测验
-致谢与说明
--Video