22.Linux-块设备驱动之框架详细分析(详解)
本节目的:
通过分析2.6内核下的块设备驱动框架,知道如何来写驱动
1.之前我们学的都是字符设备驱动,先来回忆一下
字符设备驱动:
当我们的应用层读写(read()/write())字符设备驱动时,是按字节/字符来读写数据的,期间没有任何缓存区,因为数据量小,不能随机读取数据,例如:按键、LED、鼠标、键盘等
2.接下来本节开始学习块设备驱动
块设备:
块设备是i/o设备中的一类, 当我们的应用层对该设备读写时,是按扇区大小来读写数据的,若读写的数据小于扇区的大小,就会需要缓存区, 可以随机读写设备的任意位置处的数据,例如 普通文件(*.txt,*.c等),硬盘,U盘,SD卡,
3.块设备结构:
- 段(Segments):由若干个块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。
- 块 (Blocks): 由Linux制定对内核或文件系统等数据处理的基本单位。通常由1个或多个扇区组成。(对Linux操作系统而言)
- 扇区(Sectors):块设备的基本单位。通常在512字节到32768字节之间,默认512字节
4.我们以txt文件为例,来简要分析下块设备流程:
比如:当我们要写一个很小的数据到txt文件某个位置时, 由于块设备写的数据是按扇区为单位,但又不能破坏txt文件里其它位置,那么就引入了一个“缓存区”,将所有数据读到缓存区里,然后修改缓存数据,再将整个数据放入txt文件对应的某个扇区中,当我们对txt文件多次写入很小的数据的话,那么就会重复不断地对扇区读出,写入,这样会浪费很多时间在读/写硬盘上,所以内核提供了一个队列的机制,再没有关闭txt文件之前,会将读写请求进行优化,排序,合并等操作,从而提高访问硬盘的效率
(PS:内核中是通过elv_merge()函数实现将队列优化,排序,合并,后面会分析到)
5.接下来开始分析块设备框架
当我们对一个*.txt写入数据时,文件系统会转换为对块设备上扇区的访问,也就是调用ll_rw_block()函数,从这个函数开始就进入了设备层.
5.1先来分析ll_rw_block()函数(/fs/buffer.c):
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
//rw:读写标志位, nr:bhs[]长度, bhs[]:要读写的数据数组
{
int i;
for (i = ; i < nr; i++) {
struct buffer_head *bh = bhs[i]; //获取nr个buffer_head
... ...
if (rw == WRITE || rw == SWRITE) {
if (test_clear_buffer_dirty(bh)) {
... ...
submit_bh(WRITE, bh); //提交WRITE写标志的buffer_head
continue;
}}
else {
if (!buffer_uptodate(bh)) {
... ...
submit_bh(rw, bh); //提交其它标志的buffer_head
continue;
}}
unlock_buffer(bh); }
}
其中buffer_head结构体,就是我们的缓冲区描述符,存放缓存区的各种信息,结构体如下所示:
struct buffer_head {
unsigned long b_state; //缓冲区状态标志
struct buffer_head *b_this_page; //页面中的缓冲区
struct page *b_page; //存储缓冲区位于哪个页面
sector_t b_blocknr; //逻辑块号
size_t b_size; //块的大小
char *b_data; //页面中的缓冲区 struct block_device *b_bdev; //块设备,来表示一个独立的磁盘设备 bh_end_io_t *b_end_io; //I/O完成方法 void *b_private; //完成方法数据 struct list_head b_assoc_buffers; //相关映射链表 /* mapping this buffer is associated with */
struct address_space *b_assoc_map;
atomic_t b_count; //缓冲区使用计数
};
5.2然后进入submit_bh()中, submit_bh()函数如下:
int submit_bh(int rw, struct buffer_head * bh)
{
struct bio *bio; //定义一个bio(block input output),也就是块设备i/o
... ...
bio = bio_alloc(GFP_NOIO, ); //分配bio
/*根据buffer_head(bh)构造bio */
bio->bi_sector = bh->b_blocknr * (bh->b_size >> ); //存放逻辑块号
bio->bi_bdev = bh->b_bdev; //存放对应的块设备
bio->bi_io_vec[].bv_page = bh->b_page; //存放缓冲区所在的物理页面
bio->bi_io_vec[].bv_len = bh->b_size; //存放扇区的大小
bio->bi_io_vec[].bv_offset = bh_offset(bh); //存放扇区中以字节为单位的偏移量 bio->bi_vcnt = ; //计数值
bio->bi_idx = ; //索引值
bio->bi_size = bh->b_size; //存放扇区的大小 bio->bi_end_io = end_bio_bh_io_sync; //设置i/o回调函数
bio->bi_private = bh; //指向哪个缓冲区
... ...
submit_bio(rw, bio); //提交bio
... ...
}
submit_bh()函数就是通过bh来构造bio,然后调用submit_bio()提交bio
5.3 submit_bio()函数如下:
void submit_bio(int rw, struct bio *bio)
{
... ...
generic_make_request(bio);
}
最终调用generic_make_request(),把bio数据提交到相应块设备的请求队列中,generic_make_request()函数主要是实现对bio的提交处理
5.4 generic_make_request()函数如下所示:
void generic_make_request(struct bio *bio)
{
if (current->bio_tail) { // current->bio_tail不为空,表示有bio正在提交
*(current->bio_tail) = bio; //将当前的bio放到之前的bio->bi_next里面
bio->bi_next = NULL; //更新bio->bi_next=0;
current->bio_tail = &bio->bi_next; //然后将当前的bio->bi_next放到current->bio_tail里,使下次的bio就会放到当前bio->bi_next里面了
return; }
BUG_ON(bio->bi_next);
do {
current->bio_list = bio->bi_next;
if (bio->bi_next == NULL)
current->bio_tail = ¤t->bio_list;
else
bio->bi_next = NULL; __generic_make_request(bio); //调用__generic_make_request()提交bio
bio = current->bio_list;
} while (bio);
current->bio_tail = NULL; /* deactivate */
}
从上面的注释和代码分析到,只有当第一次进入generic_make_request()时, current->bio_tail为NULL,才能调用__generic_make_request().
__generic_make_request()首先由bio对应的block_device获取申请队列q,然后要检查对应的设备是不是分区,如果是分区的话要将扇区地址进行重新计算,最后调用q的成员函数make_request_fn完成bio的递交.
5.5 __generic_make_request()函数如下所示:
static inline void __generic_make_request(struct bio *bio)
{
request_queue_t *q;
int ret;
... ...
do {
q = bdev_get_queue(bio->bi_bdev); //通过bio->bi_bdev获取申请队列q
... ...
ret = q->make_request_fn(q, bio); //提交申请队列q和bio
} while (ret);
}
这个q->make_request_fn()又是什么函数?到底做了什么,我们搜索下它在哪里被初始化的
如下图,搜索make_request_fn,它在blk_queue_make_request()函数中被初始化mfn这个参数
继续搜索blk_queue_make_request,找到它被谁调用,赋入的mfn参数是什么
如下图,找到它在blk_init_queue_node()函数中被调用
最终q->make_request_fn()执行的是__make_request()函数
5.6我们来看看__make_request()函数,对提交的申请队列q和bio做了什么
static int __make_request(request_queue_t *q, struct bio *bio)
{ struct request *req; //块设备本身的队列
... ...
//(1)将之前的申请队列q和传入的bio,通过排序,合并在本身的req队列中
el_ret = elv_merge(q, &req, bio);
... ... init_request_from_bio(req, bio); //合并失败,单独将bio放入req队列
add_request(q, req); //单独将之前的申请队列q放入req队列
... ...
__generic_unplug_device(q); //(2) 执行申请队列的处理函数
}
1)上面的elv_merge()函数,就是内核中的电梯算法(elevator merge),它就类似我们坐的电梯,通过一个标志,向上或向下.
比如申请队列中有以下6个申请:
4(in),2(out),5(in),3(out),6(in),1(out) //其中in:写出队列到扇区,ou:读入队列
最后执行下来,就会排序合并,先写出4,5,6,队列,再读入1,2,3队列
2) 上面的__generic_unplug_device()函数如下:
void __generic_unplug_device(request_queue_t *q)
{ if (unlikely(blk_queue_stopped(q)))
return;
if (!blk_remove_plug(q))
return;
q->request_fn(q);
}
最终执行q的成员request_fn()函数, 执行申请队列的处理函数
6.本节框架分析总结,如下图所示:
7.其中q->request_fn是一个request_fn_proc结构体,如下图所示:
7.1那这个申请队列q->request_fn又是怎么来的?
我们参考自带的块设备驱动程序drivers\block\xd.c
在入口函数中发现有这么一句:
static struct request_queue *xd_queue; //定义一个申请队列xd_queue xd_queue = blk_init_queue(do_xd_request, &xd_lock); //分配一个申请队列
其中blk_init_queue()函数原型如下所示:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
// *rfn: request_fn_proc结构体,用来执行申请队列中的处理函数
// *lock:队列访问权限的自旋锁(spinlock),该锁需要通过DEFINE_SPINLOCK()函数来定义
显然就是将do_xd_request()挂到xd_queue->request_fn里.然后返回这个request_queue队列
7.2我们再看看申请队列的处理函数 do_xd_request()是如何处理的,函数如下:
static void do_xd_request (request_queue_t * q)
{
struct request *req; if (xdc_busy)
return; while ((req = elv_next_request(q)) != NULL) //(1)while获取申请队列中的需要处理的申请
{
int res = ;
... ...
for (retry = ; (retry < XD_RETRIES) && !res; retry++)
res = xd_readwrite(rw, disk, req->buffer, block, count);
//将获取申请req的buffer成员 读写到disk扇区中,当读写失败返回0,成功返回1
end_request(req, res); //申请队列中的的申请已处理结束,当res=0,表示读写失败
}
}
(1)为什么要while一直获取?
因为这个q是个申请队列,里面会有多个申请,之前是使用电梯算法elv_merge()函数合并的,所以获取也要通过电梯算法elv_next_request()函数获取.
通过上面代码和注释,内核中的申请队列q最终都是交给驱动处理,由驱动来对扇区读写
8.接下来我们就看看drivers\block\xd.c的入口函数大概流程,是如何创建块设备驱动的
static DEFINE_SPINLOCK(xd_lock); //定义一个自旋锁,用到申请队列中
static struct request_queue *xd_queue; //定义一个申请队列xd_queue static int __init xd_init(void) //入口函数
{
if (register_blkdev(XT_DISK_MAJOR, "xd")) //1.创建一个块设备,保存在/proc/devices中
goto out1; xd_queue = blk_init_queue(do_xd_request, &xd_lock); //2.分配一个申请队列,后面会赋给gendisk结构体的queue成员
... ... for (i = ; i < xd_drives; i++) {
... ...
struct gendisk *disk = alloc_disk(); //3.分配一个gendisk结构体, 64:次设备号个数,也称为分区个数
/* 4.接下来设置gendisk结构体 */
disk->major = XT_DISK_MAJOR; //设置主设备号
disk->first_minor = i<<; //设置次设备号
disk->fops = &xd_fops; //设置块设备驱动的操作函数
disk->queue = xd_queue; //设置queue申请队列,用于管理该设备IO申请队列
... ... xd_gendisk[i] = disk;
} ... ...
for (i = ; i < xd_drives; i++)
add_disk(xd_gendisk[i]); //5.注册gendisk结构体
}
其中gendisk(通用磁盘)结构体是用来存储该设备的硬盘信息,包括请求队列、分区链表和块设备操作函数集等,结构体如下所示:
struct gendisk {
int major; /*设备主设备号*/
int first_minor; /*起始次设备号*/
int minors; /*次设备号的数量,也称为分区数量,如果改值为1,表示无法分区*/
char disk_name[]; /*设备名称*/
struct hd_struct **part; /*分区表的信息*/
int part_uevent_suppress;
struct block_device_operations *fops; /*块设备操作集合 */
struct request_queue *queue; /*申请队列,用于管理该设备IO申请队列的指针*/
void *private_data; /*私有数据*/
sector_t capacity; /*扇区数,512字节为1个扇区,描述设备容量*/
....
};
9.所以注册一个块设备驱动,需要以下步骤:
- 创建一个块设备
- 分配一个申请队列
- 分配一个gendisk结构体
- 设置gendisk结构体的成员
- 注册gendisk结构体
未完待续~ ~下节便开始写块设备驱动程序
22.Linux-块设备驱动之框架详细分析(详解)的更多相关文章
- linux块设备驱动---概念与框架(转)
基本概念 块设备(blockdevice) --- 是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性 ...
- Linux块设备驱动详解
<机械硬盘> a:磁盘结构 -----传统的机械硬盘一般为3.5英寸硬盘,并由多个圆形蝶片组成,每个蝶片拥有独立的机械臂和磁头,每个堞片的圆形平面被划分了不同的同心圆,每一个同心圆称为一个 ...
- linux块设备驱动之实例
1.注册:向内核注册个块设备驱动,其实就是用主设备号告诉内核这个代表块设备驱动 sbull_major = register_blkdev(sbull_major, "sbull&quo ...
- linux块设备模型架构框架
Linux块设备的原理远比字符设备要复杂得多,尽管在linux这一块的方法论有很多相似之处,但考虑到它是用中块结构,它常常要搭配内存页管理,页缓冲块缓冲来改善硬盘访问的速度,按照块硬件最大的性能要求进 ...
- linux块设备驱动---相关结构体(转)
上回最后面介绍了相关数据结构,下面再详细介绍 块设备对象结构 block_device 内核用结构block_device实例代表一个块设备对象,如:整个硬盘或特定分区.如果该结构代表一个分区,则其成 ...
- Linux 块设备驱动 (一)
1.块设备的I/O操作特点 字符设备与块设备的区别: 块设备只能以块为单位接受输入和返回输出,而字符设备则以字符为单位. 块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设 ...
- Linux块设备驱动(一) _驱动模型
块设备是Linux三大设备之一,其驱动模型主要针对磁盘,Flash等存储类设备,本文以3.14为蓝本,探讨内核中的块设备驱动模型 框架 下图是Linux中的块设备模型示意图,应用层程序有两种方式访问一 ...
- Linux块设备驱动(二) _MTD驱动及其用户空间编程
MTD(Memory Technology Device)即常说的Flash等使用存储芯片的存储设备,MTD子系统对应的是块设备驱动框架中的设备驱动层,可以说,MTD就是针对Flash设备设计的标准化 ...
- Linux块设备驱动_WDS
推荐书:<Linux内核源代码情景分析> 1.字符设备驱动和使用中等待某一事件的方法①查询方式②休眠唤醒,但是这种没有超时时间③poll机制,在休眠唤醒基础上加一个超时时间④异步通知,异步 ...
随机推荐
- 向GitHub 提交你的源代码
之前的这篇文章「Git入门篇」相信大家都已经对 Git 的基本操作熟悉了,但是这篇文章只介绍了对本地 Git 仓库的基本操作,今天我就来介绍下如何跟远程仓库一起协作,教你们向 GitHub 上提交你们 ...
- 深入剖析ConcurrentHashMap 一
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt201 ConcurrentHashMap是Java5中新增加的一个线程安全的 ...
- Linux-ps命令(7)
名称:ps(process status) 使用权限:所有使用者说明:显示瞬间进程 (process) 的动态 示例: 比如输入ps查看进程,如下图: 其中输出状态栏参数: PID 该 process ...
- [Unity 设计模式]桥接模式(BridgePattern)
1.前言 继上一讲IOC模式的基础上继续本讲桥接模式,笔者感觉桥接模式是23种设计模式中桥接模式是最好用但也是最难理解的设计模式之一,23中设计模式就好武侠剧中一本武功秘籍,我们在工作过程中想要熟练运 ...
- poj 2723 二分+2-sat判定
题意:给出n对钥匙,每对钥匙只能选其中一个,在给出每层门需要的两个钥匙,只要一个钥匙就能开门,问最多能到哪层. 思路:了解了2-SAT判定的问题之后主要就是建图的问题了,这里建图就是对于2*n个钥匙, ...
- 201521123061 《Java程序设计》第八周学习总结
201521123061 <Java程序设计>第八周学习总结 1. 本周学习总结 2. 书面作业 1.List中指定元素的删除(题目4-1) 1.1 实验总结 主要是应用到了list中的a ...
- 201521123077 《Java程序设计》第6周学习总结
1. 本周学习总结 1.1 向对象思想总结 1.2 使用常规方法总结其他上课内容 Swing一些常用组件的基本用法 Object类的clone及hashcode方法 2. 书面作业 1. clone方 ...
- 201521123066 《Java程序设计》第四周学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. 1.多态性: (1)概念:相同的方法名,不同的实现方法 (2)instanceof运算符:判 ...
- 201521123117 《Java程序设计》第4周学习总结
1. 本周学习总结 Q1.1 尝试使用思维导图总结有关继承的知识点. Q1.2 使用常规方法总结其他上课内容. 答:控制可见性的4个访问修饰符private -- 仅对本类可见public -- 对所 ...
- 201521123024《Java程序设计》第3周学习总结
1. 本周学习总结 2. 书面作业 1.代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; p ...