ok6410学习笔记(19.块设备驱动程序设计)

来源:互联网 发布:origin软件绘图 编辑:程序博客网 时间:2024/05/29 17:33

本节还存在问题:

问题一:为什么开辟的内存是0扇区,下文中有具体说明。
问题二:块设备能不能像字符设备一样进行,read,write,下文中有详细说明。
问题三:对于整体的块设备的体系结构开不是很透彻,应该再看看<linux内核注释> 和 <linux内核设计与实现>两本书的块设备部分

本节知识点:

预备知识:

1.块设备与字符设备的区别:a.块设备和字符设备的读取单元不同,块设备是以一块为基本单元进行读写的,一般是512字节,字符设备是以字节为单位进行读写的。
                                                b.块设备可以随机访问(随机对各个块进行访问)    字符设备必须按照顺序访问(按照地址进行访问)
2.块设备在linux中的体系架构:
   
        当访问块设备文件的时候,linux先调用VFS虚拟文件系统,先在硬盘缓存中去寻找有没有要访问的数据,如果有就直接返回,没有就继续调用下面的文件系统设备文件。然后在Mapping layer层来寻找文件的inode,通过inode让操作系统找到数据在磁盘上的逻辑地址,即sector。Generic Block Layer层是通用块设备层,这个层把块设备分成若干个扇区,并处理上层的读写请求,变成若干个bio结构。IO scheduler layer层针对,各种机械存储设备(硬盘),进行IO请求的调度,如电梯调度算法。Block Device Driver 块设备驱动,负责对硬件进行控制。
        总结一下:VFS:负责统一文件接口
                           Mapping Layer:负责通过inode找到sector
                           Generic Block Layer:负责分扇区和处理上层读写请求
                           IO scheduler layer:负责IO请求进行调度

重点函数:

1、描述块设备的结构
        struct  gendisk
{
int major; //  主设备号
                int first_minor; //第一个设备的次设备号
int minor; //有多少设备  一般是在申请块设备函数alloc_disk函数中填写的
      char disk_name[DISK_NAME_LEN];//驱动名  /dev路径下的名
                struct block_device_operations *fops;// 块设备处理函数
   struct request_queue *queue;//IO请求队列
}
2、set_capacity(struct gendisk *gd, MY_BLKDEV_BYTES>>9)  这个函数的第一个参数是 块设备结构  第二个参数是开辟的内存除上512 应该是有多少个块  这个函数的功能应该是告诉内核这个块设备有多少个块
3、alloc_disk(minor)  对于块设备的申请要使用这个函数,不能自己直接给上面的那个结构体赋值,这个函数的参数是 此类块设备有多少个设备
4、add_disk(struct gendisk *gd) 把块设备添加到linux内核中去  如果在内核配置的时候安装了udev 这个函数可以在/dev目录下直接产生块设备文件
5、del_gendisk(struct gendisk *gd) 把块设备从内核中卸载掉
6、IO请求的结构
      struct  request
      {
struct list_head queuelist;//链表结构
sector_t sector;//要操作的首扇区
     unsigned long nr_sectors;//要操作的扇区数目
struct bio* bio;//请求的bio结构体的链表
struct bio* biotail;//请求的bio结构体的链表尾
       }
     request IO请求和bio结构的关系就是  request是操作系统根据IO调度机制把一个一个的bio(一次块设备请求)组成成一个链表 就变成了一次IO请求了  好多个request IO请求就变成了request_queue  IO请求队列
7、struct request_queue *blk_init_queue(request_fn_proc *rfn,spinlock_t *lock)  该函数初始化IO请求队列(是用在想要使用linuxIO调度器的情况下的,如访问硬盘等) 第一个参数是处理IO请求队列的函数,第二个参数是自旋锁   返回值是IO请求队列
8、blk_cleanup_queue(request_queue *p) 清楚IO请求队列
9、struct request *elv_next_request(request_queue *q) 通过当前请求队列 去寻找下一个IO请求 (我觉得应该是在请求队列中 用一个链表保存了所有的IO请求和当前IO请求)
10、void blkdev_dequeue_request(request_queue *q) 从请求队列中 删除当前IO请求
11、end_request(req,1)  次函数非常重要 我出了问题 就是在这个函数上面  此函数功能是结束当前IO请求 如果成功第二个参数写1,如果不成功写0。1和0影响很大,千万别写错了,第一个参数是当前IO请求。
                                                           上面的函数是使用在 使用linuxIO调度器的情况的,如访问硬盘设备
                                                           下面的函数是使用在  无linuxIO调度器的情况   如U盘  内存
12、一次块设备IO请求结构
       struct bio
{
sector_t bi_sector;//要访问的第一个扇区
   unsigned int bi_size;//以字节为单位所需传输的数据大小
struct bio_vec *bi_io_vec;//实际的vec列表
}
struct bio_vec
{
struct page *bv_page;//页指针  用来寻找IO请求缓冲区的页指针
unsigned int bv_len;//传输的数据长度,与bi_size有区别  后面详细说
unsigned int bv_offset;//偏移量 和页指针配合一起寻找IO请求缓冲区
}
13、struct request_queue *blk_alloc_queue(GFP_KERNEL)申请一个IO请求队列
14、blk_queue_make_request(request_queue *q, make_request_fn *mfn) 绑定请求函数和制造请求函数(详细结构在后面驱动结构里面详细说) 第一个参数是请求队列  第二个参数是处理请求函数
15、bio_endio(bio,0,-EIO) 结束当前bio(失败的情况)       bio_endio(bio,bio->bi_size,0)结束当前bio(成功的情况)
bio_endio()原型如下:void bio_endio(struct bio* bio, unsigned int byetes, int error);
bytes是已经传送的字节数(注意:bytes≤bio->bi_size),这个函数同时更新了bio的当前缓冲区指针.当设备进一步处理bio后,驱动应再次调用bio_endio(),如不能完成请求,将错误码赋给error参数,并在函数中得以处理.此函数无论处理IO成功与否都返回0,如果返回非零值,则bio将再次被提交

驱动结构:

本节最重要的知识点就是了解块设备的驱动结构。本节的块设备分两类,一类是有IO调度器的,一类是没有IO调度器的。
一、有linux IO调度器的情况
1.使用blk_init_queue函数,初始化一个IO请求队列,并把请求队列处理函数绑定给这个请求队列。
2.使用alloc_disk函数,申请一个块设备结构
3.填充gendisk块设备结构(主设备号  次设备号  设备名  IO请求队列   块设备函数操作集,块设备的函数操作集没有read和write的 只有open,release,ioctl等)
4.使用set_capacity函数,把块设备有多少块,赋值给gendisk
6.使用add_disk函数,将这个块设备注册到linux内核,此时产生了设备文件。
7.填写IO请求处理函数:
a.因为是处理一个请求队列,所以应该使用elv_next_request遍历所有的IO请求,知道没有请求了,才退出函数
b.当通过elv_next_request得到一个请求request的时候,然后通过linux内核回馈的sector头扇区和current_nr_sectors操作扇区个数判断操作的扇区是否,超过了开辟的内存大小。这里默认内存开辟的首地址是0扇区,原因我也不知道。可能是跟后面的格式化有关系,这是我还没弄懂的知识点一
c.通过rq_data_dir()函数,判断IO请求的数据处理方向,即是读还是写。
        d.根据读写,决定是从IO请求的缓冲区(req->buffer)中度数据,还是往缓冲区中写数据。如果是对硬盘进行操作,则是通过寄存器的控制,把硬盘中的数据读出来写入buffer和把buffer中的数据写入硬盘。本节的代码是用内存模拟硬件设备,所以是在内存与缓冲区之间进行copy。
总结:首先声明,对于块设备能否进行应用程序的read和write  我不知道,这是我没弄懂的知识二。本节的测试是ramdisk,把块设备中开辟的内存,格式化成ext3文件系统,并挂载到mnt中的某个目录,制作成硬盘,进行文件的拷贝。
1.写入文件的过程,用户态-------->内核态------------>硬件设备,(1.)内核根据你要拷贝的文件大小先去寻找哪些硬件空间没有被使用,文件被内核分成好多个块即分成好多的bio结构,这些bio都有自己的sector,这些sector跟刚刚的硬件空间有着某种转换关系(Mapping Layer层),(2.)然后内核再把你要拷贝的文件一块一块的写入每一个IO请求的缓冲区中(Generic Block Layer层),(3.)对于这个使用了IO调度器的驱动,blk_init_queue调用了blk_queue_make_request(q,_make_request),这里面首先调用了_make_request函数,这个函数是把所以产生的bio结构,通过IO调度器(调度算法)制作成一个一个的IO请求,即request结构,再把request结构制作成request_queueIO请求队列,然后调用了q->make_request_fn=mfn,mfn这个函数就是blk_init_queue中我们自己写的那个函数,用来处理IO请求队列的。(IO schedule层),在这个层次中,产生了我们常常见到的request中的sector和current_nr_sectors,他们的来源是bio的sector,(4.)在驱动中,你再把这个缓存区中的数据写入硬件设备中去(Block Driver层),但是我觉得硬件设备不应该仅仅是通过纯寄存器去控制,还应该是同过内核分配的sector和current_nr_sectors去找到硬件空间,因为只有这样才能确定那里是被使用的,那里是空闲的。这里面sector和current_nr_sectors与内核直接的关系应该是在,linux内核移植的时候完成的。这个过成就是上面那个体系结构图的具体解释。
补充:bio、request和request_queue三者关系:
         首先是IO请求队列,这个里面有好多个IO请求request,当确定了文件大小,找到空闲硬件空间的时候,内核就产生了好多IO请求,每个IO请求都有一个sector,一个current_nr_sectors。一个IO请求一次可以操作好多个块,每一个块的每次IO请求就是一个bio,IO调度器就是优化一个request中的多个bio的。而一个request中的多个bio又有自己的sector,这就保证了不管IO调度器怎么优化,对于一次request的处理,都不会把文件拷贝到错的地址。(这里面就体现出了块设备的访问随机性),在这里我们考虑的最小单位是IO请求,bio的排序是调度器处理的,bio的顺序不会影响IO请求的处理的原因上面已经阐述过了

2.读出文件的过程,硬件设备------------>内核态---------->用户态,当读一个文件的时候,确定了文件的inode属性,找到了文件的物理地址属性,操作系统可以根据这个物理地址找到文件对应的一组sector和current_nr_sectors并形成一个IO请求队列,因为产生和查找都是根据物理地址来的所以这组sector和current_nr_sectors应该是当时write的时候的那组是完全对应的。所以就可以毫无错误的找到你要的问题,在写入各个IO请求的缓冲区中,用户态就得到了你想要的文件了。这里也体现了块设备的访问随机性。

这里面有一个问题很纠结:比如我用read和write在应用程序中,往块设备中写入和读出数据,因为是顺序读取的,在读取数据的时候没有inode,所以sector和current_nr_sectors是随机的,不一定是写入时候的那个sector和current_nr_sectors,所以我在IO请求的缓存区中没有读出对应的数据,但是貌似我的应用程序还真的接到了对应的数据。原因我也不是很清楚。

注意:在整个过程中,程序中对错误的处理,都是有一个前提的,对于本节代码来说前提就是内存开辟的头地址,就是0扇区(可能是因为格式化成ext3文件系统的缘故,把这个内存空间当成了一个硬盘了,自然内存首地址就成了0扇区)。对于真实的硬盘驱动程序,应该是在内核移植的时候都移植好的,不管是硬盘还是nandflash,他们的起始地址应该就是0扇区,也就是说在安全性检测的时候,只需用   (头扇区+扇区数)*512再与总共硬盘大小进行比较就好了。

二、无linux IO调度器的情况
1.使用blk_alloc_queue(GFP_KERNEL)  申请一个IO请求队列
2.blk_queue_make_request(request_queue *q, make_request_fn *mfn)  这个函数是把申请的IO请求队列和你自己定义的处理函数建立联系
3.使用alloc_disk函数,申请一个块设备结构
4.填充gendisk块设备结构(主设备号  次设备号  设备名  IO请求队列   块设备函数操作集,块设备的函数操作集没有read和write的 只有open,release,ioctl等)
5.使用set_capacity函数,把块设备有多少块,赋值给gendisk
6.使用add_disk函数,将这个块设备注册到linux内核,此时产生了设备文件。
7.填写IO请求队列处理函数:
a.使用bio的sector和size来进行安全检测,判断是否超出我们开辟的内存大小
b.使用bio_for_each_segment函数遍历bio,因为每一个bio应该是由很多个bio_vec组成的,每一个bio_vec中都有一个缓冲区。这里可以看出bi_size和bio_vec->len的区别了,bi_size应该是整个bio进行操作的总字节数,bio_vec->len应该是每一次bio_vec缓冲区数据传输的字节数。
c.使用bio_rw函数来判断bio的数据传输方向
d.根据页指针和偏移量找到bio_vec的缓冲区,再根据读写方向进行数据传输。此处也有像上一方法一样的对硬件进行的操作,此时的硬件应该也是与bio的sector有关的
总结:对于这个没有使用IO调度器的情况,步骤和上面的写入过程是一样的,只有第三步是不一样,当到达第三步的时候直接使用了blk_queue_make_request(q,make_request_fn *mfn)函数,中的mfn函数即我们自己建立的处理好多bio结构的函数,没有调用系统中__make_queue函数了,因为这个函数中有一步IO调度器,优化bio顺序,我们直接使用这个函数来处理各个bio结构,很自然这个方法也没用IO请求request。与上面的方法不同的是有IO调度器的最小单元是request,没有IO调度器的最小单元是bio。

下面的图展示出了,两者中的第三步的函数调用关系:

本节代码:

1.如果对本节代码进行检测:
a.insmod block.ko 块设备
b.ls /dev/my_blkdev  查看有没有产生设备文件
c.mkfs.ext3 /dev/my_blkdev  把块设备格式化成ext3文件系统
d.mkdir -p /mnt/blk   在mnt目录下创建一个文件夹 用来映射这个块设备
e.mount /dev/my_blkdev  /mnt/blk   把格式化好的块设备映射到新建的目录下
f.cp考入一些文件   再考出这些文件  看见你的块设备能不能读写
g.umount  /dev/my_blkdev   卸载 看看 blk目录下还有没有文件了
2.不使用IO调度器的 block.c:
#include <linux/module.h>#include <linux/moduleparam.h>#include <linux/init.h>#include <linux/sched.h>#include <linux/kernel.h>/* printk() */#include <linux/slab.h>/* kmalloc() */#include <linux/fs.h>/* everything... */#include <linux/errno.h>/* error codes */#include <linux/timer.h>#include <linux/types.h>/* size_t */#include <linux/fcntl.h>/* O_ACCMODE */#include <linux/hdreg.h>/* HDIO_GETGEO */#include <linux/kdev_t.h>#include <linux/vmalloc.h>#include <linux/genhd.h>#include <linux/blkdev.h>#include <linux/buffer_head.h>/* invalidate_bdev */#include <linux/bio.h>#include <linux/major.h>static struct gendisk *my_blkdev_disk;static struct request_queue *my_blkdev_queue;#define MY_BLKDEV_BYTES (16*1024*1024) //块设备大小为16Munsigned char DATA[MY_BLKDEV_BYTES]; //这是当作块设备的内存static int do_queue(struct request_queue *q,struct bio *bio) //IO请求队列处理函数{struct bio_vec *bvec;int i;      void *dsk_mem;      /*bio->bi_sector头扇区     bio->bi_size 传输字节大小*/      if ((bio->bi_sector << 9) + bio->bi_size > MY_BLKDEV_BYTES)       {          printk(KERN_EMERG": bad request: block=%llu, count=%u\n",(unsigned long long)bio->bi_sector, bio->bi_size);          bio_endio(bio,0,-EIO);          return 0;      }      dsk_mem = DATA + (bio->bi_sector << 9); //把bio内存中的头地址保存下来      bio_for_each_segment(bvec, bio, i) //遍历bio  应该是一个bio 会分成好多个bvec步骤进行 数据传输     {      void *iovec_mem;      switch(bio_rw(bio))      {      case READ:      case READA:      iovec_mem=kmap(bvec->bv_page) + bvec->bv_offset;//用bio的页地址和偏移量 计算出缓冲区的虚拟地址      memcpy(iovec_mem, dsk_mem, bvec->bv_len);//把头扇区开始的内容copy到  bio的缓冲区中      printk(KERN_EMERG "READing is %s\n",(char *)iovec_mem);      kunmap(bvec->bv_page);//释放映射      break;      case  WRITE:      iovec_mem=kmap(bvec->bv_page) + bvec->bv_offset;      memcpy(dsk_mem, iovec_mem, bvec->bv_len);  //把来自用户空间的内容  从bio缓冲区copy到头扇区      printk(KERN_EMERG "WRITing is %s\n",(char *)iovec_mem);      kunmap(bvec->bv_page);      break;      default:      printk(KERN_EMERG": unknown value of bio_rw: %lu\n",bio_rw(bio));                        bio_endio(bio,0,-EIO);//结束当前bio请求                        return 0;      break;      }      dsk_mem += bvec->bv_len; //在每次完成bio中 的一个bvec的时候  都要把模仿设备的内存指针进行移动      }      bio_endio(bio,bio->bi_size,0);      return 0;}struct block_device_operations my_blkdev_fop = {        .owner                = THIS_MODULE,};static int __init blkdev_init(void){printk(KERN_EMERG "module init is finished!!!\n");int ret;/*初始化IO请求队列  并绑定IO队列处理函数*/my_blkdev_queue = blk_alloc_queue(GFP_KERNEL); //申请一个IO请求队列if (!my_blkdev_queue) {          ret = -ENOMEM;          goto err_alloc_queue;      }      blk_queue_make_request(my_blkdev_queue, do_queue); //把申请的IO请求队列和我们自己的请求队列处理函数绑定起来   跳过以前操作系统带IO调度的那个函数/*跟系统申请 描述块设备的结构体  赋值给定义的全局变量指针*/my_blkdev_disk=alloc_disk(1); //动态分配gendisk结构体  参数为该设备使用的次设备号数量    if (!my_blkdev_disk)     {          ret = -ENOMEM;          goto err_alloc_disk;      }/*填充块设备结构体*/my_blkdev_disk->major=255;//主设备号为255my_blkdev_disk->first_minor=0;//只有一个设备在alloc_disk中体现的,这个是第一个设备的次设备号strcpy(my_blkdev_disk->disk_name,"my_blkdev");//填写设备名my_blkdev_disk->fops=&my_blkdev_fop; //填写设备操作函数集合my_blkdev_disk->queue=my_blkdev_queue; //填写该块设备的IO请求队列set_capacity(my_blkdev_disk, MY_BLKDEV_BYTES>>9); //一般一个扇区的大小是512个字节  这个函数应该是得到这个块设备上有几个扇区/*注册块设备 进入内核*/add_disk(my_blkdev_disk);//注册块设备驱动return 0;err_alloc_disk:        blk_cleanup_queue(my_blkdev_queue);//如果IO请求队列申请失败 则应该清除队列err_alloc_queue:        return ret;}static void  __exit blkdev_exit(void){blk_cleanup_queue(my_blkdev_queue);del_gendisk(my_blkdev_disk);}module_init(blkdev_init);module_exit(blkdev_exit);

3.使用IO调度器的block.c:
#include <linux/module.h>#include <linux/moduleparam.h>#include <linux/init.h>#include <linux/sched.h>#include <linux/kernel.h>/* printk() */#include <linux/slab.h>/* kmalloc() */#include <linux/fs.h>/* everything... */#include <linux/errno.h>/* error codes */#include <linux/timer.h>#include <linux/types.h>/* size_t */#include <linux/fcntl.h>/* O_ACCMODE */#include <linux/hdreg.h>/* HDIO_GETGEO */#include <linux/kdev_t.h>#include <linux/vmalloc.h>#include <linux/genhd.h>#include <linux/blkdev.h>#include <linux/buffer_head.h>/* invalidate_bdev */#include <linux/bio.h>#include <linux/major.h>static struct gendisk *my_blkdev_disk;static struct request_queue *my_blkdev_queue;#define MY_BLKDEV_BYTES (16*1024*1024) //块设备大小为16Munsigned char DATA[MY_BLKDEV_BYTES]; //这是当作块设备的内存static void do_queue(struct request_queue *q) //IO请求队列处理函数{struct request *req;//定义一个请求/*要处理完这个请求队列上的所有请求*/while((req=elv_next_request(q))!=NULL)  //当第一次调用elv_next_request函数的时候  q中的保存的应该是链表头  然后每次调用elv_next_request函数都指向下一个  也就是说第一次调用elv_next_request函数得到的request应该是第一次io请求{/*我想看看每次请求中的sector都是不是0*/printk(KERN_EMERG "sector is %llu , nr_sectors is %u \n",(unsigned long long)req->sector,req->current_nr_sectors);if(((req->sector+req->current_nr_sectors)<<9)>MY_BLKDEV_BYTES)  //我觉得这里应该不加上req->sector  问问老师{printk(KERN_EMERG "error:sector is %llu , nr_sectors is %u \n",(unsigned long long)req->sector,req->current_nr_sectors);end_request(req,0);//结束本次请求   第二个参数是返回请求是否执行成功 成功为1 失败为0continue; //如果所操作的扇区大小 大于 我们最大分配内存的大小(此处做ramdisk 用内存当扇区) 则结束这次请求  进行下面的请求}/*下面是针对请求的方向进行处理*/switch(rq_data_dir(req))//该函数是判断请求的方向的 即数据流向{case READ: //这里还不能使用copy_from_user或者copy_to_user因为req->buffer是内核空间的内存  是需要操作系统传递到用户空间的不是这里传递的  但是最好还是检查一下这个内存的有效性/*这里的读 应该是用户空间读  内核把req->buffer中的数据传递给用户空间*/memcpy(req->buffer,DATA+(req->sector<<9),req->current_nr_sectors<<9);//memcpy(req->buffer,DATA+(0<<9),3<<9);//memcpy(req->buffer,"hello0",5);printk(KERN_EMERG"READing!!!   %s\n",req->buffer);end_request(req,1);//结束本次请求   第二个参数是返回请求是否执行成功 成功为1 失败为0break;case WRITE:/*这里的写  应该是用户空间写  用户空间通过内核把数据写入req->buffer中 应把这个buffer中的值 写入硬件中*/memcpy(DATA+(req->sector<<9),req->buffer,req->current_nr_sectors<<9);//memcpy(DATA+(0<<9),req->buffer,3<<9);printk(KERN_EMERG"WRITEing!!!  %s\n",req->buffer);end_request(req,1);//结束本次请求   第二个参数是返回请求是否执行成功 成功为1 失败为0break;default:break;}}}struct block_device_operations my_blkdev_fop = {        .owner                = THIS_MODULE,};static int __init blkdev_init(void){printk(KERN_EMERG "module init is finished!!!\n");int ret;/*初始化IO请求队列*/my_blkdev_queue=blk_init_queue(do_queue,NULL);//初始化块设备的IO请求队列  第一个参数是IO请求队列的处理函数名  第二个参数是一个自旋锁if (!my_blkdev_queue) {          ret = -ENOMEM;          goto err_alloc_queue;      }/*跟系统申请 描述块设备的结构体  赋值给定义的全局变量指针*/my_blkdev_disk=alloc_disk(1); //动态分配gendisk结构体  参数为该设备使用的次设备号数量    if (!my_blkdev_disk)     {          ret = -ENOMEM;          goto err_alloc_disk;      }/*填充块设备结构体*/my_blkdev_disk->major=255;//主设备号为255my_blkdev_disk->first_minor=0;//只有一个设备在alloc_disk中体现的,这个是第一个设备的次设备号strcpy(my_blkdev_disk->disk_name,"my_blkdev");//填写设备名my_blkdev_disk->fops=&my_blkdev_fop; //填写设备操作函数集合my_blkdev_disk->queue=my_blkdev_queue; //填写该块设备的IO请求队列set_capacity(my_blkdev_disk, MY_BLKDEV_BYTES>>9); //一般一个扇区的大小是512个字节  这个函数应该是得到这个块设备上有几个扇区/*注册块设备 进入内核*/add_disk(my_blkdev_disk);//注册块设备驱动return 0;err_alloc_disk:        blk_cleanup_queue(my_blkdev_queue);//如果IO请求队列申请失败 则应该清除队列err_alloc_queue:        return ret;}static void  __exit blkdev_exit(void){blk_cleanup_queue(my_blkdev_queue);del_gendisk(my_blkdev_disk);}module_init(blkdev_init);module_exit(blkdev_exit);


原创粉丝点击