块设备驱动初步 2010-05-02 19:55 单击,返回主页,查看更多内容
本文的素材以及源代码(稍有改动)均来源于《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. |
|