通过分析块设备驱动框架,学习如何写块设备驱动
字符设备驱动:
当我们的应用层读写(read()/write())字符设备驱动时,是按字节/字符来读写数据的,期间没有任何缓存区,因为数据量小,不能随机读取数据,例如:按键、LED、鼠标、键盘等
块设备:
块设备是i/o设备中的一类, 当我们的应用层对该设备读写时,是按扇区大小来读写数据的,若读写的数据小于扇区的大小,就会需要缓存区, 可以随机读写设备的任意位置处的数据,例如 普通文件(*.txt,*.c等),硬盘,U盘,SD卡,
块设备结构:
- 段(Segments):由若干个块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。
- 块 (Blocks): 由Linux制定对内核或文件系统等数据处理的基本单位。通常由1个或多个扇区组成。(对Linux操作系统而言)
- 扇区(Sectors):块设备的基本单位。通常在512字节到32768字节之间,默认512字节
以txt文件为例,来简要分析下块设备流程:
比如:当我们要写一个很小的数据到txt文件某个位置时, 由于块设备写的数据是按扇区为单位,但又不能破坏txt文件里其它位置,那么就引入了一个“缓存区”,将所有数据读到缓存区里,然后修改缓存数据,再将整个数据放入txt文件对应的某个扇区中,当我们对txt文件多次写入很小的数据的话,那么就会重复不断地对扇区读出,写入,这样会浪费很多时间在读/写硬盘上,所以内核提供了一个队列的机制,再没有关闭txt文件之前,会将读写请求进行优化,排序,合并等操作,从而提高访问硬盘的效率。
一、块设备驱动框架分析
应用程序对“1.txt”的读写,最终会转化成对硬件的操作,而硬件又由驱动程序操作
读写一个普通的文件,如何转换成对扇区的读写——由文件系统完成
ll_rw_block是进入扇区读写的一个通用入口,他的功能:
1.把读写放入队列
2.调用队列的处理函数(优化/调序/合并)
为什么是ll_rw_block,涉及到文件系统,可详见《Linux 内核源代码情景分析》,此处更关心驱动程序
1.1分析ll_rw_block (low level read/write block) ---(linux-2.6.22.6fsBuffer.c)-->通用的文件
1 void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]) 2 //rw:读写标志位, nr:bhs[]长度, bhs[]:要读写的数据数组 3 { 4 int i; 5 for (i = 0; i < nr; i++) { 6 struct buffer_head *bh = bhs[i]; //获取nr个buffer_head 7 ... ... 8 if (rw == WRITE || rw == SWRITE) { 9 if (test_clear_buffer_dirty(bh)) { 10 ... ... 11 submit_bh(WRITE, bh); //提交WRITE写标志的buffer_head 12 continue; 13 }} 14 else { 15 if (!buffer_uptodate(bh)) { 16 ... ... 17 submit_bh(rw, bh); //提交其它标志的buffer_head 18 continue; 19 }} 20 unlock_buffer(bh); } 21 }
其中buffer_head结构体,就是我们的缓冲区描述符,存放缓存区的各种信息,结构体如下所示:
1 struct buffer_head { 2 unsigned long b_state; //缓冲区状态标志 3 struct buffer_head *b_this_page; //页面中的缓冲区 4 struct page *b_page; //存储缓冲区位于哪个页面 5 sector_t b_blocknr; //逻辑块号 6 size_t b_size; //块的大小 7 char *b_data; //页面中的缓冲区 8 9 struct block_device *b_bdev; //块设备,来表示一个独立的磁盘设备 10 11 bh_end_io_t *b_end_io; //I/O完成方法 12 13 void *b_private; //完成方法数据 14 15 struct list_head b_assoc_buffers; //相关映射链表 16 17 /* mapping this buffer is associated with */ 18 struct address_space *b_assoc_map; 19 atomic_t b_count; //缓冲区使用计数 20 };
1.2然后进入submit_bh()中
1 int submit_bh(int rw, struct buffer_head * bh) 2 { 3 struct bio *bio; //定义一个bio(block input output),也就是块设备i/o 4 ... ... 5 bio = bio_alloc(GFP_NOIO, 1); //分配bio 6 /*根据buffer_head(bh)构造bio */ 7 bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9); //存放逻辑块号 8 bio->bi_bdev = bh->b_bdev; //存放对应的块设备 9 bio->bi_io_vec[0].bv_page = bh->b_page; //存放缓冲区所在的物理页面 10 bio->bi_io_vec[0].bv_len = bh->b_size; //存放扇区的大小 11 bio->bi_io_vec[0].bv_offset = bh_offset(bh); //存放扇区中以字节为单位的偏移量 12 13 bio->bi_vcnt = 1; //计数值 14 bio->bi_idx = 0; //索引值 15 bio->bi_size = bh->b_size; //存放扇区的大小 16 17 bio->bi_end_io = end_bio_bh_io_sync; //设置i/o回调函数 18 bio->bi_private = bh; //指向哪个缓冲区 19 ... ... 20 submit_bio(rw, bio); //提交bio 21 ... ... 22 }
submit_bh()函数就是通过bh来构造bio,然后调用submit_bio()提交bio
1.3 submit_bio()函数如下:
void submit_bio(int rw, struct bio *bio) { ... ... generic_make_request(bio); }
最终调用generic_make_request(),把bio数据提交到相应块设备的请求队列中,generic_make_request()函数主要是实现对bio的提交处理
1.4 generic_make_request()函数如下所示:
1 void generic_make_request(struct bio *bio) 2 { 3 if (current->bio_tail) { // current->bio_tail不为空,表示有bio正在提交 4 *(current->bio_tail) = bio; //将当前的bio放到之前的bio->bi_next里面 5 bio->bi_next = NULL; //更新bio->bi_next=0; 6 current->bio_tail = &bio->bi_next; //然后将当前的bio->bi_next放到current->bio_tail里,使下次的bio就会放到当前bio->bi_next里面了 7 8 return; } 9 10 BUG_ON(bio->bi_next); 11 do { 12 current->bio_list = bio->bi_next; 13 if (bio->bi_next == NULL) 14 current->bio_tail = ¤t->bio_list; 15 else 16 bio->bi_next = NULL; 17 18 __generic_make_request(bio); //调用__generic_make_request()提交bio 19 bio = current->bio_list; 20 } while (bio); 21 current->bio_tail = NULL; /* deactivate */ 22 }
从上面的注释和代码分析到,只有当第一次进入generic_make_request()时, current->bio_tail为NULL,才能调用__generic_make_request().
__generic_make_request()首先由bio对应的block_device获取申请队列q,然后要检查对应的设备是不是分区,如果是分区的话要将扇区地址进行重新计算,最后调用q的成员函数make_request_fn完成bio的递交.
1.5 __generic_make_request()函数如下所示:
1 static inline void __generic_make_request(struct bio *bio) 2 { 3 request_queue_t *q; 4 int ret; 5 ... ... 6 do { 7 q = bdev_get_queue(bio->bi_bdev); //通过bio->bi_bdev获取申请队列q 8 ... ... 9 ret = q->make_request_fn(q, bio); //提交申请队列q和bio 10 } while (ret); 11 }
这个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()函数
1.6 看看__make_request()函数,对提交的申请队列q和bio做了什么
1 static int __make_request(request_queue_t *q, struct bio *bio) 2 { 3 4 struct request *req; //块设备本身的队列 5 ... ... 6 //(1)将之前的申请队列q和传入的bio,通过排序,合并在本身的req队列中 7 el_ret = elv_merge(q, &req, bio); 8 ... ... 9 10 init_request_from_bio(req, bio); //合并失败,单独将bio放入req队列 11 add_request(q, req); //单独将之前的申请队列q放入req队列 12 ... ... 13 __generic_unplug_device(q); //(2) 执行申请队列的处理函数 14 }
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()函数如下:
1 void __generic_unplug_device(request_queue_t *q) 2 { if (unlikely(blk_queue_stopped(q))) 3 return; 4 if (!blk_remove_plug(q)) 5 return; 6 q->request_fn(q); 7 }
最终执行q的成员request_fn()函数, 执行申请队列的处理函数
框架分析总结
应用层:read/write——普通文件、U盘、内存
----------------------------------------------------------------------------------------
文件系统(FS)
---------------------------------------------------------------------------------------
内核层:
ll_rw_block(); //进入内核设备层,提交buffer_head缓存区结构体
submit_bh(); //通过提交上来的buffer_head来构造bio,然后提交bio
submit_bio(); //把提交上来的bio提交到相应块设备的请求队列中
generic_make_request(bio); //对bio进行提交处理
__generic_make_request(bio); //获取等待队列q, 然后提交q和bio
__make_request; //合并q和bio,然后执行队列
elv_merge(q, &req, bio); //先尝试合并
init_request_from_bio(req, bio); // 若合并不成用bio构造请求
add_request(q, req); // 把请求放入队列
__generic_unplug_device(q); // 执行队列
q->request_fn(q); // 调用队列的"处理函数"
小结
- 应用程序通过文件系统最终调用了 ll_rw_block(); 来实现对块设备文件的读写,读写过程中会根据硬件的限制优化读写的顺序,先将读写放入队列再执行。‘
- 内核中用include/linux/genhd.h 中定义的gendisk[通用磁盘(generic disk)]结构体表示一个磁盘,其中包含请求队列queue,由结构体request_queue描述。
- ll_rw_block(); 最终会·调用到队列中的执行函数 q->request_fn(q); 也就是说读写函数是在此处实现的,其他都由框架做好了。
二、 q->request_fn(q);
2.1 q->request_fn是一个request_fn_proc结构体,如下图所示:
那这个申请队列q->request_fn又是怎么来的?
参考自带的块设备驱动程序 driverslockxd.c
入口函数中发现有:
static struct request_queue *xd_queue; //定义一个申请队列xd_queue xd_queue = blk_init_queue(do_xd_request, &xd_lock); //分配一个申请队列
其中blk_init_queue()函数原型如下所示:
1 request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); 2 // *rfn: request_fn_proc结构体,用来执行申请队列中的处理函数 3 // *lock:队列访问权限的自旋锁(spinlock),该锁需要通过DEFINE_SPINLOCK()函数来定义
显然就是将请求处理函数 do_xd_request() 挂到 xd_queue->request_fn 里,然后返回这个request_queue队列
2.2 申请队列的处理函数 do_xd_request()是如何处理的,函数如下:
1 static void do_xd_request (request_queue_t * q) 2 { 3 struct request *req; 4 5 if (xdc_busy) 6 return; 7 /*以电梯调度算法从队列 q 中取出下一个请求*/ 8 while ((req = elv_next_request(q)) != NULL) //(1)while获取申请队列中的需要处理的申请 9 { 10 int res = 0; 11 ... ... 12 for (retry = 0; (retry < XD_RETRIES) && !res; retry++) 13 res = xd_readwrite(rw, disk, req->buffer, block, count); 14 //将获取申请req的buffer成员 读写到disk扇区中,当读写失败返回0,成功返回1 15 16 end_request(req, res); //申请队列中的的申请已处理结束,当res=0,表示读写失败 17 } 18 }
三、看看driverslockxd.c的入口函数大概流程,是如何创建块设备驱动的
1 static DEFINE_SPINLOCK(xd_lock); //定义一个自旋锁,用到申请队列中 2 static struct request_queue *xd_queue; //定义一个申请队列xd_queue 3 4 static int __init xd_init(void) //入口函数 5 { 6 if (register_blkdev(XT_DISK_MAJOR, "xd")) //1.创建一个块设备,保存在/proc/devices中 7 goto out1; 8 9 xd_queue = blk_init_queue(do_xd_request, &xd_lock); //2.分配一个申请队列,后面会赋给gendisk结构体的queue成员 10 ... ... 11 12 for (i = 0; i < xd_drives; i++) { 13 ... ... 14 struct gendisk *disk = alloc_disk(64); //3.分配一个gendisk结构体, 64:次设备号个数,也称为分区个数 15 16 /* 4.接下来设置gendisk结构体 */ 17 disk->major = XT_DISK_MAJOR; //设置主设备号 18 disk->first_minor = i<<6; //设置次设备号 19 disk->fops = &xd_fops; //设置块设备驱动的操作函数 20 disk->queue = xd_queue; //设置queue申请队列,用于管理该设备IO申请队列 21 ... ... 22 23 xd_gendisk[i] = disk; 24 } 25 26 ... ... 27 for (i = 0; i < xd_drives; i++) 28 add_disk(xd_gendisk[i]); //5.注册gendisk结构体 29 }
gendisk(通用磁盘)结构体是用来存储该设备的硬盘信息,包括请求队列、分区链表和块设备操作函数集等,结构体如下所示:
1 struct gendisk { 2 int major; /*设备主设备号*/ 3 int first_minor; /*起始次设备号*/ 4 int minors; /*次设备号的数量,也称为分区数量,如果改值为1,表示无法分区*/ 5 char disk_name[32]; /*设备名称*/ 6 struct hd_struct **part; /*分区表的信息*/ 7 int part_uevent_suppress; 8 struct block_device_operations *fops; /*块设备操作集合 */ 9 struct request_queue *queue; /*申请队列,用于管理该设备IO申请队列的指针*/ 10 void *private_data; /*私有数据*/ 11 sector_t capacity; /*扇区数,512字节为1个扇区,描述设备容量*/ 12 .... 13 };
四、注册一个块设备驱动,需要以下步骤:
- 创建一个块设备
- 分配一个申请队列
- 分配一个gendisk结构体
- 设置gendisk结构体的成员
- 注册gendisk结构体
五、自己写程序实现内存盘(内存模拟硬盘)
参考内核自带的块设备驱动程序:
drivers/block /xd.c
drivers/block /z2ram.c
5.1、所需的结构体
gendisk磁盘结构体,同上
request申请结构体:
1 struct request { 2 //用于挂在请求队列链表的节点,使用函数elv_next_request()访问它,而不能直接访问 3 4 struct list_head queuelist; 5 struct list_head donelist; /*用于挂在已完成请求链表的节点*/ 6 struct request_queue *q; /*指向请求队列*/ 7 8 unsigned int cmd_flags; /*命令标识*/ 9 10 enum rq_cmd_type_bits cmd_type; //读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写 11 12 sector_t sector; //要提交的下一个扇区偏移位置(offset) 13 ... ... 14 unsigned int current_nr_sectors; //当前需要传送的扇区数(长度) 15 ... ... 16 17 char *buffer; //当前请求队列链表的申请里面的数据,用来读写扇区数据(源地址) 18 ... ... 19 };
5.2、 所需函数
int register_blkdev(unsigned int major, const char *name);
创建一个块设备,当major==0时,表示动态创建,创建成功会返回一个主设备号
unregister_blkdev(unsigned int major, const char *name);
卸载一个块设备, 在出口函数中使用,major:主设备号, name:名称
struct gendisk *alloc_disk(int minors);
分配一个gendisk结构,minors为分区数,填1表示不分区
void del_gendisk(struct gendisk *disk);
释放gendisk结构,在出口函数中使用,也就是不需要这个磁盘了
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
分配一个request_queue请求队列,分配成功返回一个request_queue结构体
rfn: request_fn_proc结构体,用来执行放置在队列中的请求的处理函数
lock:队列访问权限的自旋锁(spinlock),该锁通过DEFINE_SPINLOCK()来定义
void blk_cleanup_queue(request_queue_t * q);
清除内核中的request_queue请求队列,在出口函数中使用
static DEFINE_SPINLOCK(spinlock_t lock);
定义一个自旋锁(spinlock)
static inline void set_capacity(struct gendisk *disk, sector_t size);
设置gendisk结构体的扇区数(成员copacity), size等于扇区数
该函数内容如下:
disk->capacity = size;
void add_disk(struct gendisk *gd);
向内核中注册gendisk结构体
void put_disk(struct gendisk *disk);
注销内核中的gendisk结构体,在出口函数中使用
struct request *elv_next_request(request_queue_t *q);
通过电梯算法获取申请队列中未完成的申请,获取成功返回一个request结构体,不成功返回NULL
(PS: 不使用获取到的这个申请时,应使用end_request()来结束获取申请)
void end_request(struct request *req, int uptodate);
结束获取申请, 当uptodate==0,表示使用该申请读写扇区失败, uptodate==1,表示成功
static inline void *kzalloc(size_t size, gfp_t flags);
分配一段静态缓存,这里用来当做我们的磁盘扇区用,分配成功返回缓存地址,分配失败会返回0
void kfree(const void *block);
注销一段静态缓存,与kzalloc()成对,在出口函数中使用
rq_data_dir(rq);
获取request申请结构体的命令标志(cmd_flags成员),当返回READ(0)表示读扇区命令,否则为写扇区命令
6.步骤
6.1在入口函数中:
- 1)使用register_blkdev()创建一个块设备
- 2) blk_init_queue()使用分配一个申请队列,并赋申请队列处理函数
- 3)使用alloc_disk()分配一个gendisk结构体
- 4)设置gendisk结构体的成员
- ->4.1)设置成员参数(major、first_minor、disk_name、fops)
- ->4.2)设置queue成员,等于之前分配的申请队列
- ->4.3)通过set_capacity()设置capacity成员,等于扇区数
- 5)使用kzalloc()来获取缓存地址,用做扇区
- 6)使用add_disk()注册gendisk结构体
6.2在申请队列的处理函数中
- 1) while循环使用elv_next_request()获取申请队列中每个未处理的申请
- 2)使用rq_data_dir()来获取每个申请的读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写
- 3)使用memcp()来读或者写扇区(缓存)
- 4)使用end_request()来结束获取的每个申请
6.3在出口函数中
- 1)使用put_disk()和del_gendisk()来注销,释放gendisk结构体
- 2)使用kfree()释放磁盘扇区缓存
- 3)使用blk_cleanup_queue()清除内存中的申请队列
- 4)使用unregister_blkdev()卸载块设备
7.代码如下
1 /* 2 * 参考内核自带的块设备驱动程序: 3 * drivers/block /xd.c 4 *�rivers/block /z2ram.c 5 */ 6 #include <linux/module.h> 7 #include <linux/errno.h> 8 #include <linux/interrupt.h> 9 #include <linux/mm.h> 10 #include <linux/fs.h> 11 #include <linux/kernel.h> 12 #include <linux/timer.h> 13 #include <linux/genhd.h> 14 #include <linux/hdreg.h> 15 #include <linux/ioport.h> 16 #include <linux/init.h> 17 #include <linux/wait.h> 18 #include <linux/blkdev.h> 19 #include <linux/blkpg.h> 20 #include <linux/delay.h> 21 #include <linux/io.h> 22 23 #include <asm/system.h> 24 #include <asm/uaccess.h> 25 #include <asm/dma.h> 26 27 static DEFINE_SPINLOCK(ramblock_lock); //定义一个自旋锁 28 29 static struct gendisk *ramblock_disk; //磁盘结构体 30 static request_queue_t *ramblock_queue; //申请队列 31 static int major; 32 #define RAMBOCK_SIZE (1024*1024) //设置磁盘容量为1M 33 static unsigned char *ramblock_buf; //分配一块内存(地址) 34 35 36 static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo) 37 { 38 //容量 = heads*cylinders*sectors*512 39 geo->heads = 2; //两个磁头分区 40 geo->cylinders = 32; //一个磁头有32个柱面 41 geo->sectors = RAMBOCK_SIZE/2/32/512; //一个柱面有多少个扇区 42 43 return 0; 44 } 45 46 47 static struct block_device_operations ramblock_fops = { 48 .owner = THIS_MODULE, 49 .getgeo = ramblock_getgeo, //获得几何属性,保存磁盘的信息(柱头,柱面,扇区) 50 }; 51 52 /* 申请队列处理函数 */ 53 static void do_ramblock_request(request_queue_t * q) 54 { 55 struct request *req; 56 static int r_cnt = 0; 57 static int w_cnt = 0; 58 59 while ((req = elv_next_request(q)) != NULL) //获取每个申请 60 { 61 /* 数据传输三要素:源,目的,长度 */ 62 /* 源/目的 */ 63 unsigned long offset = req->sector << 9; //左移9位-->乘以512 64 65 /* 目的/源 */ 66 //req->buffer 67 68 /* 长度 */ 69 unsigned long len = req->current_nr_sectors << 9; 70 71 if (rq_data_dir(req) == READ) 72 { 73 //printk("do_ramblock_request read %d ", ++r_cnt); 74 //从磁盘里的源中读长度为len的数据到buffer中 75 memcpy(req->buffer, ramblock_buf + offset, len); 76 } 77 else 78 { 79 //printk("do_ramblock_request write %d ", ++w_cnt); 80 //把源里的len长度的数据写到目的buffer中 81 memcpy(ramblock_buf + offset, req->buffer, len); 82 } 83 end_request(req, 1); /* wrap up, 0 = fail, 1 = success */ 84 } 85 } 86 87 /*入口函数*/ 88 static int ramblock_init(void) 89 { 90 /* 1.分配一个gendisk结构体*/ 91 ramblock_disk = alloc_disk(16); //次设备号个数:分区个数+1--->15个分区 92 /* 2.设置*/ 93 /* 2.1分配/设置队列:提供读写*/ 94 ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock); 95 ramblock_disk->queue = ramblock_queue; //构造好的队列放到gendisk结构体中 96 97 /* 2.2 设置其他属性:比如容量*/ 98 //之前对于字符设备,注册字符设备时,还有一个fop结构体 99 major = register_blkdev(0, "ramblock"); /* cat/proc/devices */ 100 ramblock_disk->major = major; 101 ramblock_disk->first_minor = 0; //从0开始的16个次设备都对应这个块设备 102 sprintf(ramblock_disk->disk_name, "ramblock", i+'a'); 103 ramblock_disk->fops = &ramblock_fops; 104 set_capacity(ramblock_disk, RAMBOCK_SIZE/512); //第二个参数单位是扇区(512字节):容量/512 105 106 /* 3.硬件相关操作*/ 107 ramblock_buf = kzalloc(RAMBOCK_SIZE, GFP_KERNEL); 108 /* 4.注册*/ 109 add_disk(ramblock_disk); 110 111 return 0; 112 } 113 /*出口函数*/ 114 static void ramblock_exit(void) 115 { 116 unregister_blkdev(major, "ramblock"); 117 del_gendisk(ramblock_disk); 118 put_disk(ramblock_disk); 119 blk_cleanup_queue(ramblock_queue); 120 121 kfree(ramblock_buf); 122 } 123 124 /*由于以上只是C函数,通过宏修饰使之成为入口出口函数*/ 125 module_init(ramblock_init); 126 module_exit(ramblock_exit); 127 MODULE_LICENSE("GPL");