块设备剖析之两种IO的处理方式

3600阅读 0评论2015-08-02 夕阳下的孤影
分类:LINUX

    本文所有内容基于内核版本Linux-v3.2.40。

    块设备驱动处理IO的方式主要有两种:
    方式1:透过内核的IO调度层,不进行request的合并,驱动直接实现make_request函数。
    方式2:经过内核的IO调层对request进行必要的合并,以减少底层硬件的寻址耗时。

    两种方式在层次结构上的区别如下图所示:
    

    对于方式1而言,典型的应用就是NVME、sd等真正的随机读写设备(对任意地址的访问耗时是相同的);而方式2的典型应用就是机械硬盘,对不同地址的寻址耗时是不同的。因此,通过合并request可有效的减少磁头移动,提高块设备的处理速度和硬盘的寿命。

    针对以上两种方式,底层驱动在实现上也是有差别的:
    方式1:直接实现make_request函数,透过IO调度层,减少IO路径
    驱动直接实现自己的make_request函数,申请请求队列并通过blk_queue_make_request()将make_request注册到队列中。
    如ramdisk中的实现:
    brd->brd_queue = blk_alloc_queue(GFP_KERNEL); /* 申请一个块设备的queue */
    blk_queue_make_request(brd->brd_queue, brd_make_request);  /* 注册make_request函数 */

    注意:
    make_request的函数原型为:void make_request_fn(struct request_queue *q, struct bio *bio)
    其中bio是通用块层传入的待处理的bio,驱动程序可直接使用并处理该bio。

    方式2:借助内核默认的请求制造函数blk_queue_bio进行bio的合并,驱动实现request的处理函数
    request处理函数的原型为void request_fn(struct request_queue *q),所有待处理的request都挂在q下。
    驱动程序可通过blk_init_queue()注册request_fn,此时不需要单独申请request_queue,如scsi层中的实现:
    scsi_alloc_queue()->__scsi_alloc_queue()->blk_init_queue()

    总结:
    1. 方式1比较简单,只需要处理传入的单个bio;而方式2处理的是request链表,而且每个request又是一个bio链表,所以处理时相对复杂。
    2. 方式1透过了IO调度层,IO路径更短,IO时延更低;但方式2在某些特定的场合却可以发挥巨大的作用,如机械硬盘。
    3. IO调度层是一个比较复杂的子系统,涉及到几种IO调度算法(NOOP、CFQ、Deadline、AS等)以及request的合并策略,后续会专门写一篇相关的博客。

     最后参考网上的一个示例,说明request_fn的实现方法:
     1) 对于不太关心bio的场景,可以直接使用request->buffer进行处理,如下所示:

点击(此处)折叠或打开

  1. /*
  2.  * Handle I/O request.
  3.  */
  4. static void example_disk_transfer(struct example_dev *dev, unsigned long sector,
  5.                                   unsigned long nsect, char *buffer, int rw)
  6. {
  7.     unsigned long offset = sector * KERNEL_SECTOR_SIZE;
  8.     unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE;
  9.   
  10.     if ((offset + nbytes) > dev->size) {
  11.         printk (KERN_NOTICE "Beyond-end write (%ld %ld)\n", offset, nbytes);
  12.         return;
  13.     }
  14.     if (rw) { /* write */
  15.         memcpy(dev->data + offset, buffer, nbytes);
  16.     } else { /* read */
  17.         memcpy(buffer, dev->data + offset, nbytes);
  18.     }
  19. }

  20. /*
  21.  * The example of a request function.
  22.  */
  23. static void example_request(struct request_queue *q)
  24. {
  25.     struct request *req;
  26.   
  27.     while ((req = blk_fetch_request(q)) != NULL) { /* 从queue队列中取出一个request */
  28.         struct example_dev *dev = req->rq_disk->private_data;
  29.         
  30.         if(req->cmd_type != REQ_TYPE_FS) {
  31.             printk (KERN_NOTICE "Skip non-fs request\n");
  32.             __blk_end_request_all(req, -EIO);
  33.             continue;
  34.         }
  35.         
  36.         /* 底层数据传输,注意req->buffer最多只能表示一个page,因此如果当前request包括多个bio,或者bio中包括多个段
  37.          * 就不能使用该方法,只能使用下面的方法2)
  38.          */
  39.         example_disk_transfer(dev, blk_rq_pos(req), blk_rq_cur_sectors(req), req->buffer, rq_data_dir(req));
  40.         
  41.         if(blk_end_request_cur(req, 0)) { /* 结束request */
  42.             printk("End current request failed!\n");
  43.             return ;
  44.         }
  45.     }
  46. }
    2) 如果对于底层驱动来说不得不对request中的每一个bio单独处理,那么除去遍历queue中request链表,还要遍历request中的bio链表。如下:

点击(此处)折叠或打开

  1. /*
  2.  * Transfer a full request.
  3.  */
  4. static int example_disk_xfer_request(struct example_dev *dev, struct request *req)
  5. {
  6.     struct req_iterator iter;
  7.     int nsect = 0;
  8.     struct bio_vec *bvec;
  9.     struct bio *bio;

  10. #if defined(use_bio)
  11.     /* 遍历request中的所有bio */
  12.     __rq_for_each_bio(bio, req) {
  13.         /* 处理bio,一般会借助bio_for_each_segment宏 */
  14.     }
  15. #elif defined(use_bvec)
  16.     /* 两次遍历,先遍历request中的bio,再遍历bio中的bvec */
  17.     rq_for_each_segment(bvec, req, iter) {
  18.         char *buffer = __bio_kmap_atomic(iter.bio, iter.i, KM_USER0);
  19.         sector_t sector = iter.bio->bi_sector;
  20.         
  21.         /* 底层数据传输 */
  22.         data_transfer(dev, sector, bio_sectors(iter.bio),
  23.             buffer, bio_data_dir(iter.bio) == WRITE);
  24.         sector += bio_sectors(iter.bio);
  25.         __bio_kunmap_atomic(iter.bio, KM_USER0);
  26.         nsect += iter.bio->bi_size/KERNEL_SECTOR_SIZE;
  27.     }
  28. #endif
  29.   
  30.     return nsect;
  31. }

  32. /*
  33.  * The example of a request function.
  34.  */
  35. static void example_request(struct request_queue *q)
  36. {
  37.     struct request *req;
  38.   
  39.     while ((req = blk_fetch_request(q)) != NULL) {
  40.         struct example_dev *dev = req->rq_disk->private_data;
  41.         
  42.         if(req->cmd_type != REQ_TYPE_FS) {
  43.             printk (KERN_NOTICE "Skip non-fs request\n");
  44.             __blk_end_request_all(req, -EIO);
  45.             continue;
  46.         }
  47.         
  48.         /* 处理request */
  49.         example_disk_xfer_request(dev, req);
  50.         
  51.         if(blk_end_request_cur(req, 0)) { /* 结束request */
  52.             printk("End current request failed!\n");
  53.             return ;
  54.         }
  55.     }
  56. }
    需要注意,处理完request后要记得end该request,借助blk_end_request_cur实现;另外还有一个函数__blk_end_request_cur,二者的区别主要是调用__blk_end_request_cur时,需要获得queue->queue_lock锁,而
 blk_end_request_cur在内部实现时会自动上锁,也就是说,如果驱动代码中已经使用了类似spin_lock(q->queue_lock)的锁操作,就不能再调用blk_end_request_cur,否则会产生死锁。

    本文主要是做一个备忘,方便以后查询。
上一篇:块设备剖析之BIO
下一篇:Linux中虚拟地址转物理地址的方法