单击,返回主页,查看更多内容
本文的素材以及源代码(稍有改动)均来源于《Linux Device Driver》,因此本文可视为该书块设备
驱动相关章节的阅读理解。
一、体验块设备
驱动(单击下载
驱动源码)
本
驱动模拟了一个硬盘。想想你去中关村(不过我更喜欢去成都@世界)买了一个硬盘,迫不及待地安装在你的Linux机器上,你怎么样才能使用这个新硬盘呢?当然要先把它
驱动起来。
1、make生成sbull.ko后加载
驱动:sudo insmod sbull.ko
2、对硬盘分区 执行suod fdisk /dev/sbulla
输入x,进入高级菜单
输入h,change number of heads 为4
输入c,change number of cylinders 为4
输入r,退回主菜单
顺次输入n、p、1、1、4,创建一个主分区占有cylinders 1-4
输入w,保存并退出
3、输入 sudo mkfs.ext2 /dev/sbulla1 在硬盘分区上格式化ext2文件系统
4、挂载新硬盘上的分区 输入 mkdir testdir创建空目录
输入mount –t ext2 /dev/sbulla1 ./testdir
之后,你就可以通过testdir目录来访问新硬盘分区了。
二、块设备
驱动框架介绍
1、
驱动接口简介 上层接口
用户接口
块设备 b:/dev/sda、/dev/sda2、 /dev/ram0
用户空间
程序 mkfs、mount、fdisk;
直接调用open、release、ioctl接口
读写时,不直接调用读写接口,而是将读写request提交给OS的block layer层
操作系统接口
block layer I/O scheduler决定需要执行磁盘I/O时,调用
驱动的读写接口
抽象物理设备为 cylinder、header、sector组成,视其为编号从0开始的flat型sector集合。(注1)
总假定物理设备一个sector大小为512byte(注2)
下层接口
抽象物理设备为编号从0开始的flat型sector集合,但转换sector大小为物理设备的实际值,通过物理寄存器的读写,将数据写入指定的sector位置
由物理设备的中断(读写完成)来唤醒用户进程(或内核线程)
注1: 借助minor number辨别partition编号(也可能是整个磁盘),再借助分区表决定分区的起始sector号
注2: #define KERNEL_SECTOR_SHIFT 9 #define KERNEL_SECTOR_SIZE (1 << KERNEL_SECTOR_SHIFT)
2、块设备结构体
块设备结构体 struct gendisk 是块设备
驱动中最重要的数据结构,它在操作系统和
驱动中代表一个物理磁盘。其主要字段有: major:磁盘对应的主设备号,出现在/proc/devices中。register_blkdev(sbull_major, "sbull");
first_minor:磁盘对应的第1个次设备号。dev->gd->first_minor = which*SBULL_MINORS;
minors :磁盘拥有的次设备号的总数。dev->gd = alloc_disk(SBULL_MINORS);
disk_name:磁盘的名称。出现在/proc/partitions中
fops:块设备
驱动中的功能函数。包括:open\release\media_change\revalidate_disk\ioctl等
queue:OS回调读写函数时使用的request queue队列(它绑定了读写函数)
private_data:私有数据,常用于存放包裹设备结构体
capacity:512字节大小的sector的总数量
set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
3、块设备结构体注册 申请major number
sbull_major = register_blkdev(sbull_major, "sbull"); //sbull出现在/proc/devices中
分配(初始生成)块设备结构体
dev->gd = alloc_disk(SBULL_MINORS); //同时也关联设备号数量
将块设备结构体与设备号、设备操作函数、读写队列关联
dev->gd->major = sbull_major; //关联主设备号
dev->gd->first_minor = which*SBULL_MINORS; //关联次设备号
dev->gd->fops = &sbull_ops; //关联功能函数
dev->queue = blk_init_queue(sbull_request, &dev->lock);
dev->gd->queue = dev->queue; //关联读写队列
将块设备结构体注册进OS
add_disk(dev->gd);
4、块设备结构体注销 将块设备结构体从OS中注销
del_gendisk(dev->gd);
销毁块设备结构体(移除对kobject的最后ref.,释放结构体内存)
put_disk(dev->gd);
释放major number
unregister_blkdev(sbull_major, "sbull");
5、块设备的简单读写-原理 用户
程序或内核组件提出读写request时,会将该request提交给block layer层
block layer层构造request结构,并将其链入块设备结构体绑定的request queue中
在适当的时候,block layer层回调request queue中绑定的
驱动中的读写函数,并将request queue的指针作为参数传给
驱动中的读写函数
驱动中的读写函数根据传给它的request queue的指针,从request queue中取出一个request,根据request中指定的方向(读或写)、数据在内存中的位置、在设备上的位置(起始sector编号)、传输数据量的大小(sector数量),完成物理读写
驱动通知block layer层实际完成的读写量,block layer层据此更新其内部各个数据结构;并告知
驱动是否整个request已经处理完成,若是,
驱动则负责将request从request queue中摘下,并释放request结构的内存,唤醒等待该request完成的所有进程
6、读写队列结构体-注册与注销 分配(初始生成)读写队列结构体,并与读写函数绑定
dev->queue = blk_init_queue(sbull_request, &dev->lock);
block layer在回调读写函数sbull_request时,会先获得自旋锁dev->lock,这样block layer就可以与
驱动的其它函数共享相同的临界区
blk_queue_hardsect_size(dev->queue, hardsect_size);
设置实际设备的扇区大小,这样block layer层就会根据实际设备能够处理的扇区大小来构造request结构体,从而不会出现实际设备处理不了的request
dev->queue->queuedata = http://blog.soso.com/qz.q/dev
将块设备结构体与读写队列关联
dev->gd->queue = dev->queue;
将读写队列结构体间接注册进OS
add_disk(dev->gd);
将读写队列结构体间接从OS中注销
del_gendisk(dev->gd);
销毁读写队列结构体(移除对kobject的最后ref.,释放结构体内存)
blk_cleanup_queue(dev->queue);
7、块设备的简单读写-实现
108 static void sbull_request(request_queue_t *q)
109 {
110 struct request *req;
112 while ((req = elv_next_request(q)) != NULL) { //从request queue中取出一个request,循环直到request queue中的所有request被传送完
//因为读写队列与块设备结构体已关联,所以block layer层在将读写请求链入request queue时,能将rq_disk字段指向块设备结构体
113 struct sbull_dev *dev = req->rq_disk->private_data;
114 if (! blk_fs_request(req)) {
116 end_request(req, 0);
117 continue;
118 }
123 sbull_transfer(dev, req->sector, req->current_nr_sectors, //根据request中指定的方向(rq_data_dir)、数据在内存中的位置( req->buffer )、
124 req->buffer, rq_data_dir(req)); //在设备上的位置( req->sector )、传输数据量的大小( req->current_nr_sectors ),完成物理读写
125 end_request(req, 1); //通知block layer层;将request从request queue中摘下;释放request结构的内存,唤醒等待该request完成的所有进程
126 }
127 }
void end_request(struct request *req, int uptodate)
{
//
驱动通知block layer层实际完成的读写量,block layer层据此更新其内部各个数据结构;并告知
驱动是否整个request已经处理完成
if (!end_that_request_first(req, uptodate, req->hard_cur_sectors)) {
add_disk_randomness(req->rq_disk);
blkdev_dequeue_request(req); //若是,
驱动则负责将request从request queue中摘下
end_that_request_last(req); //释放request结构的内存,唤醒等待该request完成的所有进程
}
}
8、简单读写的不足 一次只命令硬件传输1个数据块segment(内存中不超过1page的连续单元),其只是request中的一小部分而已
虽然简单读写采用while循环请求elv_next_request,但block层会认为硬件可能由于某种原因不能一次性完成一个完整request的传输,因此相邻2次elv_next_request极有可能返回的是不同的request
block layer尽了很大努力,实施电梯调度算法(drivers/block/ll_rw_block.c and elevator.c ),使得一个request结构体中包含多个在内存中离散,但在物理设备上却连续的数据块。
先后2次elv_next_request的简单读写,使得磁头必须寻道,产生较大延迟
简单读写忽视block layer层的工作,对同一个request结构体中包含的多个segment在物理设备上连续,不予理睬,实在是暴殄天物
9、请求队列 一个块设备的I/O请求的序列
跟踪未完成的块I/O请求
允许使用多I/O调度器,以最大化性能的方式提交I/O请求给你的
驱动,I/O调度器还负责合并邻近的请求
请求队列的实现
drivers/block/Ll_rw_block.c和elevator.c
10、块设备的高效读写-原理与实现
一个request结构体中包含多个在内存中离散,但在物理设备上却连续的数据块,由bio和bio_vec结构体来表示
337 static void setup_device(struct sbull_dev *dev, int which)
362 switch (request_mode) {
370 case RM_FULL:
371 dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
374 break;
385 }
387 dev->queue->queuedata = http://blog.soso.com/qz.q/dev;
409 }
175 static void sbull_full_request(request_queue_t *q)
176 {
177 struct request *req;
178 int sectors_xferred;
179 struct sbull_dev *dev = q->queuedata;
181 while ((req = elv_next_request(q)) != NULL) { //每次取出读写队列中的一个request
187 sectors_xferred = sbull_xfer_request(dev, req);
188 if (! end_that_request_first(req, 1, sectors_xferred)) {
189 blkdev_dequeue_request(req);
191 end_that_request_last(req, 1);
192 }
193 }
194 }
#define rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
157 static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
158 {
159 struct bio *bio;
160 int nsect = 0;
162 rq_for_each_bio(bio, req) { //while循环,每次取出req中的一个bio
163 sbull_xfer_bio(dev, bio);
164 nsect += bio->bi_size/KERNEL_SECTOR_SIZE; //bi_size记录一个bio中数据的总字节数
165 }
167 return nsect;
168 }
#define bio_for_each_segment(bvl, bio, i) \
__bio_for_each_segment(bvl, bio, i, (bio)->bi_idx)
#define __bio_for_each_segment(bvl, bio, i, start_idx) \
for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx); \
i < (bio)->bi_vcnt; \
bvl++, i++)
133 static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
134 {
135 int i;
136 struct bio_vec *bvec;
137 sector_t sector = bio->bi_sector; //bi_sector记录bio中首字节应位于硬件的哪个扇区。bio中的所有segment在硬件上的sector位置全部连续
139 /* Do each segment independently. */
140 bio_for_each_segment(bvec, bio, i) { //while循环,每次取出bio中的一个bio_vec来传输
141 char *buffer = __bio_kmap_atomic(bio, i, KM_USER0); //获取bv_page的内核virtual address
143 sbull_transfer(dev, sector, bio_cur_sectors(bio), //bio_cur_sectors(bio)求出bio当前segment的大小(sector数目)
144 buffer, bio_data_dir(bio) == WRITE);
145 sector += bio_cur_sectors(bio); //累进数据在硬件上的位置(sector)
147 __bio_kunmap_atomic(bio, KM_USER0);
149 }
151 return 0; /* Always "succeed" */
152 }
11、块设备的其它操作接口fops(open、release、media_change、revalidate_disk、ioctl)
1) open与release(本
驱动可以模拟光盘从光驱中更换)
216 static int sbull_open(struct inode *inode, struct file *filp)
217 {
218 struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data; //inode->i_bdev->bd_disk指向关联的gendisk structure
223 if (! dev->users)
224 check_disk_change(inode->i_bdev); //check_disk_change将导致对media_change的调用,若介质已改变,将导致调用revalidate_disk
225 dev->users++;
228 } media_changed不为NULL时,则会被内核API check_disk_change所调用;
在media_changed返回true的情况下, revalidate_disk会被内核API check_disk_change所调用
int check_disk_change(struct block_device *bdev)
{
struct gendisk *disk = bdev->bd_disk;
struct block_device_operations * bdops = disk->fops;
if (!bdops->media_changed) //media_changed为NULL
return 0;
if (!bdops->media_changed(bdev->bd_disk)) //media_changed不为NULL时,则会被内核API check_disk_change所调用
return 0;
if (__invalidate_device(bdev))
printk("VFS: busy inodes on changed media.\n");
if (bdops->revalidate_disk) // 在media_changed返回true的情况下,revalidate_disk不为NULL时,则会被内核API check_disk_change所调用
bdops->revalidate_disk(bdev->bd_disk);
if (bdev->bd_disk->minors > 1)
bdev->bd_invalidated = 1;
return 1;
}
230 static int sbull_release(struct inode *inode, struct file *filp)
231 {
232 struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
235 dev->users--;
244 } 12、media_change与revalidate_disk media_changed不为NULL时,则会被内核API check_disk_change所调用
若磁盘介质已更改,应返回true;否则返回false
在media_changed返回true的情况下, revalidate_disk会被内核API check_disk_change所调用
应完成对新磁盘介质进行操作的准备工作
249 int sbull_media_changed(struct gendisk *gd)
250 {
251 struct sbull_dev *dev = gd->private_data;
253 return dev->media_change;
254 } 260 int sbull_revalidate(struct gendisk *gd)
261 {
262 struct sbull_dev *dev = gd->private_data;
264 if (dev->media_change) {
265 dev->media_change = 0;
266 // memset (dev->data, 0, dev->size);
267 }
269 } 13、ioctl 大部分的ioctl命令都已经被block layer层所截获并处理,到达不了
驱动程序 驱动ioctl函数中需要处理的命令是HDIO_GETGEO (用户,例如fdisk,要求获得磁盘的几何参数)(测试结果似乎OS并未调用ioctl,原因待查)
291 int sbull_ioctl (struct inode *inode, struct file *filp,
292 unsigned int cmd, unsigned long arg)
293 {
294 long size;
295 struct hd_geometry geo;
296 struct sbull_dev *dev = filp->private_data;
298 switch(cmd) {
299 case HDIO_GETGEO:
301 /*
302 * Get geometry: since we are a virtual device, we have to make
303 * up something plausible. So we claim 16 sectors, four heads,
304 * and calculate the corresponding number of cylinders. We set the
305 * start of data at sector four.
306 */
307 // size = dev->size*(hardsect_size/KERNEL_SECTOR_SIZE);
308 size = dev->size / KERNEL_SECTOR_SIZE;
309 geo.cylinders = (size & ~0x3f) >> 6;
310 geo.heads = 4;
311 geo.sectors = 16;
312 geo.start = 4;
313 if (copy_to_user((void __user *) arg, &geo, sizeof(geo)))
314 return -EFAULT;
315 return 0;
316 }
318 return -ENOTTY; /* unknown command */
319 } 单击,与作者交流
more http://www.ieing.cn