Linux设备驱动程序学习(九)——块设备驱动程序

Linux块设备特点

  字符设备与块设备的不同主要有:

  • 块设备只能以块为单位接收输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。
  • 块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写。对于存储设备而言,调整读写的顺序作用巨大,因为在读写连续的扇区的存储速度比分离的扇区更快。
  • 字符设备只能被顺序读写,而块设备可以随机访问。

    注意:虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能

Linux块设备驱动结构

块设备对象结构 block_device

  内核用结构block_device实例代表一个块设备对象,如:整个硬盘或特定分区。如果该结构代表一个分区,则其成员bd_part指向设备的分区结构。如果该结构代表设备,则其成员bd_disk指向设备的通用硬盘结构gendisk。
  当用户打开块设备文件时,内核创建结构block_device实例,设备驱动程序还将创建结构gendisk实例,分配请求队列并注册结构block_device实例。
  块设备对象结构block_device列出如下(在include/Linux/fs.h中):

struct block_device {  
   dev_t bd_dev;                     /* not a kdev_t - it's a search key */  
   struct inode * bd_inode;             /* 分区节点 */  
   struct super_block * bd_super;  
   int bd_openers;  
   struct mutex bd_mutex;             /* open/close mutex 打开与关闭的互斥量*/  
   struct semaphore bd_mount_sem;     /*挂载操作信号量*/   
   struct list_head bd_inodes;  
   void * bd_holder;  
   int bd_holders;  
#ifdef CONFIG_SYSFS  
   struct list_head bd_holder_list;  
#endif  
   struct block_device * bd_contains;  
   unsigned bd_block_size;                 /*分区块大小*/  
   struct hd_struct * bd_part;  
   unsigned bd_part_count;                   /*打开次数*/  
   int bd_invalidated;  
   struct gendisk * bd_disk;          /*设备为硬盘时,指向通用硬盘结构*/  
   struct list_head bd_list;        
   struct backing_dev_info *bd_inode_backing_dev_info;  
   unsigned long bd_private;  
   /* The counter of freeze processes */  
   int bd_fsfreeze_count;  
   /* Mutex for freeze */  
   struct mutex bd_fsfreeze_mutex;  
};  

通用硬盘结构gendisk

  结构体gendisk代表了一个通用硬盘(generic hard disk)对象,它存储了一个硬盘的信息,包括请求队列、分区链表和块设备操作函数集等。块设备驱动程序分配结构gendisk实例,装载分区表,分配请求队列并填充结构的其他域。
  支持分区的块驱动程序必须包含 头文件,并声明一个结构gendisk,内核还维护该结构实例的一个全局链表gendisk_head,通过函数add_gendisk、del_gendisk和get_gendisk维护该链表。
  结构gendisk列出如下(在include/linux/genhd.h中):

struct gendisk {  
    int major;                  /* 驱动程序的主设备号 */  
    int first_minor;              /*第一个次设备号*/  
    int minors;                 /*次设备号的最大数量,没有分区的设备,此值为1 */  
    char disk_name[32];         /* 主设备号驱动程序的名字*/  
    struct hd_struct **part;      /* 分区列表,由次设备号排序 */  
    struct block_device_operations *fops;  /*块设备操作函数集*/  
    struct request_queue *queue;         /*请求队列*/  
    struct blk_scsi_cmd_filter cmd_filter;  
    void *private_data;          /*私有数据,类似字符设备中的private_data*/  
    sector_t capacity;           /* 函数set_capacity设置的容量,以扇区为单位*/  
    int flags;                 /*设置驱动器状态的标志,如:可移动介质为 GENHD_FL_REMOVABLE*/  
    struct device dev;                 /*从设备驱动模型基类结构device继承*/  
    struct kobject *holder_dir;  
    struct kobject *slave_dir;  
    struct timer_rand_state *random;  
    int policy;   
    atomic_t sync_io;        /* RAID */  
    unsigned long stamp;  
    int in_flight;  
#ifdef  CONFIG_SMP  
    struct disk_stats *dkstats;    
#else  
/*硬盘统计信息,如:读或写的扇区数、融合的扇区数、在请求队列的时间等*/  
    struct disk_stats dkstats;  
#endif  
    struct work_struct async_notify;  
#ifdef  CONFIG_BLK_DEV_INTEGRITY  
    struct blk_integrity *integrity;   /*用于数据完整性扩展*/  
#endif  
};  

  Linux内核提供了一组函数来操作gendisk,主要包括:

分配gendisk
struct gendisk *alloc_disk(int minors);
/*minors 参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。*/
增加gendisk
void add_disk(struct gendisk *gd);
/*gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备:*/

特别要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。

释放gendisk

  当不再需要一个磁盘时,应当使用如下函数释放gendisk:
  void del_gendisk(struct gendisk *gd);

设置gendisk容量

  void set_capacity(struct gendisk *disk, sector_t size);
  块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备 无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。虽然大多数块设备的扇区大小都是512字节,不过其它大小的扇区也很常见, 比如,很多CD-ROM盘的扇区都是2K大小。不管物理设备的真实扇区大小是多少,内核与块设备驱动交互的扇区都以512字节为单位。因此,set_capacity()函数也以512字节为单位。
  分区结构hd_struct代表了一个分区对象,它存储了一个硬盘的一个分区的信息,驱动程序初始化时,从硬盘的分区表中提取分区信息,存放在分区结构实例中。

block_device_operations结构

  在块设备驱动中,有一个类似于字符设备驱动中file_operations结构体的block_device_operations结构,定义在

int (*open)(struct inode *inode, struct file *filp); 
int (*release)(struct inode *inode, struct file *filp); 
/*就像它们的字符驱动程序中函数功能相同; 无当设备被打开和关闭时调用它们. 一个块设备驱动程序可能用旋转盘片、锁住仓门(对可移动介质)等来响应open调用,如果你将介质锁入设备, 你当然应当在 release 方法中解锁*/
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, 
unsigned long arg); 
/*实现 ioctl 系统调用的方法. 块设备会先截取大量的标准请求; 因此大部分的块驱动 ioctl 方法相当短小. */
int (*media_changed) (struct gendisk *gd); 
/*内核调用该函数来检查用户是否更换了驱动器内的介质,如果用户更换了,那么返回非零值。该函数只适用于支持可移动介质的驱动器,在别的情况下,该函数没意义*/
int (*revalidate_disk) (struct gendisk *gd); 
/*当介质被更换时,调用revalidata_disk函数做出响应,他告诉驱动程序完成必须的工作,以便使用新的介质,该函数返回一个int值,但是该值被内核忽略。*/
struct module *owner; 
/*一个指向拥有这个结构的模块的指针; 它应当常常被初始化为 THIS_MODULE.*/

  注意:看完block_device_operations结构我们发现它不像字符设备驱动的是它没有读写函数,因为在块设备驱动程序中,读写的操作是由request函数来实现的,request函数可以完成很多操作。

request结构体

  在 Linux 块设备驱动中,使用 request 结构体来表征等待进行的 I/O 请求,结构体具体定义如下:

 struct request 
 { 
    struct list_head queuelist;               /*链表结构*/ 
    unsigned long flags;                    /* REQ_ */  
    sector_t sector;                       /* 要传送输的下一个扇区 */ 
    unsigned long nr_sectors;               /*要传送的扇区数目*/ 
    unsigned int current_nr_sectors;          /*当前要传送的扇区数目*/ 
    sector_t hard_sector;                   /*要完成的下一个扇区*/ 
    unsigned long hard_nr_sectors;           /*要被完成的扇区数目*/ 
    unsigned int hard_cur_sectors;           /*当前要被完成的扇区数目*/ 
    struct bio *bio;                        /*请求的 bio 结构体的链表*/ 
    struct bio *biotail;                     /*请求的 bio 结构体的链表尾*/ 
    void *elevator_private; 
    unsigned short ioprio; 
    int rq_status; 
    struct gendisk *rq_disk; 
    int errors; 
    unsigned long start_time;  
    /*请求在物理内存中占据的不连续的段的数目,scatter/gather 列表的尺寸*/ 
    unsigned short nr_phys_segments;  
 /*与 nr_phys_segments 相同,但考虑了系统 I/O MMU 的 remap */ 
    unsigned short nr_hw_segments;  
    int tag; 
    char *buffer;                          /*传送的缓冲,内核虚拟地址*/  
    int ref_count;                          /* 引用计数 */ 
 ... 
 }; 

  request 结构体的主要成员包括:

  • 扇区参数
   sector_t hard_sector; 
   unsigned long hard_nr_sectors; 
   unsigned int hard_cur_sectors;

  上述 3 个成员标识还未完成的扇区,hard_sector 是第一个尚未传输的扇区,hard_nr_sectors 是尚待完成的扇区数,hard_cur_sectors 是当前 I/O 操作中待完成的扇区数。这些成员只用于内核块设备层,驱动不应当使用它们。
  在驱动程序中一般使用的是:

   sector_t sector; 
   unsigned long nr_sectors; 
   unsigned int current_nr_sectors;

  这 3 个成员在内核和驱动交互中发挥着重大作用。它们以 512 字节大小为一个扇区,如果硬件的扇区大小不是 512 字节,则需要进行相应的调整。例如,如果硬件的扇区大小是 2048 字节,则在进行硬件操作之前,需要用 4 来除起始扇区号。
  注意:hard_sector 、 hard_nr_sectors 、 hard_cur_sectors 与 sector 、 nr_sectors 、
current_nr_sectors 之间可认为是“副本”关系。

  • struct bio *bio;
    bio 是这个请求中包含的 bio 结构体的链表,驱动中不宜直接存取这个成员,而应该使用后文将介绍的 rq_for_each_bio()。
  • char *buffer;
    指向缓冲区的指针,数据应当被传送到或者来自这个缓冲区,这个指针是一个内核虚拟地址,可被驱动直接引用。
  • unsigned short nr_phys_segments;
    该值表示相邻的页被合并后,这个请求在物理内存中占据的段的数目。如果设备支持分散/聚集(SG,scatter/gather )操作,可依据此字段申请sizeof(scatterlist)* nr_phys_segments 的内存,并使用下列函数进行 DMA 映射:
  • int blk_rq_map_sg(request_queue_t) *q, struct request *req, struct scatterlist *sglist;
    该函数与 dma_map_sg()类似,它返回 scatterlist 列表入口的数量。
  • struct list_head queuelist;
    用于链接这个请求到请求队列的链表结构,blkdev_dequeue_request()可用于从队列中移除请求。
  • 使用如下宏可以从 request 获得数据传送的方向
    rq_data_dir(struct request *req);
    0 返回值表示从设备中读,非 0 返回值表示向设备写。

请求队列

  一个块请求队列是一个块 I/O 请求的队列,定义如下:

struct request_queue 
 { 
 ... 
 /* 保护队列结构体的自旋锁 */ 
 spinlock_t _ _queue_lock; 
 spinlock_t *queue_lock;  
 /* 队列 kobject */ 
 struct kobject kobj; 
/* 队列设置 */ 
 unsigned long nr_requests;            /* 最大的请求数量 */  
 unsigned short max_sectors;           /* 最大的扇区数 */ 
 unsigned short max_phys_segments;    /* 最大的段数 */ 
 unsigned short hardsect_size;          /* 硬件扇区尺寸 */ 
 unsigned int max_segment_size;        /* 最大的段尺寸 */  
 unsigned long seg_boundary_mask;     /* 段边界掩码 */ 
 unsigned int dma_alignment;           /* DMA 传送的内存对齐限制 */ 
 struct blk_queue_tag *queue_tags;  
 atomic_t refcnt;                      /* 引用计数 */  
 int node;  
 struct list_head drain_list;  
 struct request *flush_rq; 
 unsigned char ordered; 
 }; 

  请求队列跟踪等候的块 I/O 请求,它存储用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求。
  请求队列的的一些方法:

  1. 初始化请求队列和清除请求队列
    初始化请求队列
    request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
    函数的第一个参数是请求处理函数的指针,第二个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,它可能会失败,因此一定要检查它的返回值。这个函数一般在块设备驱动的模块加载函数中调用。
    清除请求队列
    void blk_cleanup_queue(request_queue_t * q);
    这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
    blk_put_queue()宏定义为:
    #define blk_put_queue(q) blk_cleanup_queue((q))

  2. “分配”请求队列
    request_queue_t *blk_alloc_queue(int gfp_mask);
    对于 Flash、RAM 盘等完全随机访问的非机械设备,并不需要进行复杂的 I/O 调度,这个时候,应该使用上述函数分配一个“请求队列”,并使用如下函数来绑定请求队列和“制造请求”函数:
    void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);

  3. 提取请求
    struct request *elv_next_request(request_queue_t *queue);
    上述函数用于返回下一个要处理的请求(由 I/O 调度器决定),如果没有请求则返回 NULL。elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,但是标识它为活动的,这个标识将阻止I/O调度器合并其他的请求到已开始执行的请求。因为 elv_next_request()不从队列里清除请求,因此连续调用它两次,两次会返回同一个请求结构体。

  4. 去除请求
    void blkdev_dequeue_request(struct request *req);
    上述函数从队列中去除一个请求。如果驱动中同时从同一个队列中操作了多个请求,它必须以这样的方式将它们从队列中去除。如果需要将一个已经出列的请求归还到队列中,可以进行以下调用:
    void elv_requeue_request(request_queue_t *queue, struct request *req);

  5. 启停请求队列
    void blk_stop_queue(request_queue_t *queue);
    void blk_start_queue(request_queue_t *queue);
    如果块设备到达不能处理等候的命令的状态,应调用 blk_stop_queue()来告知块设备层。之后,请求函数将不被调用,除非再次调用 blk_start_queue()将设备恢复到可处理请求的状态。

  6. 参数设置
    void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);
    void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max);
    void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);
    void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);
    这些函数用于设置描述块设备可处理的请求的参数。blk_queue_max_sectors()描述任一请求可包含的最大扇区数,默认值为 255;blk_queue_max_phys_segments() 和blk_queue_max_hw_segments()都控制一个请求中可包含的最大物理段(系统内存中不相邻的区),blk_queue_max_hw_segments()考虑了系统 I/O 内存管理单元的重映射,这两个参数缺省都是 128。blk_queue_max_segment_size 告知内核请求段的最大字节数,缺省值为 65,536。

bio结构体

  通常一个 bio 对应一个 I/O 请求,I/O 调度算法可将连续的 bio 合并成一个请求。所以,一个请求可以包含多个 bio。bio结构体:

struct bio 
 { 
    sector_t bi_sector;           /* 标识这个 bio 要传送的第一个(512 字节)扇区。 */ 
    struct bio *bi_next;                           /* 下一个 bio */ 
    struct block_device *bi_bdev; 
    unsigned long bi_flags;       /* 一组描述 bio 的标志,如果这是一个写请求,最低有效位被置位,
                                     可以使用bio_data_dir(bio)宏来获得读写方向。 */ 
    unsigned long bi_rw;             /* 低位表示 READ/WRITE,高位表示优先级*/  
    unsigned short bi_vcnt;           /* bio_vec 数量 */ 
    unsigned short bi_idx;            /* 当前 bvl_vec 索引 */  
    /*不相邻的物理段的数目*/ 
    unsigned short bi_phys_segments;  
    /*物理合并和 DMA remap 合并后不相邻的物理段的数目*/ 
    unsigned short bi_hw_segments;  
    unsigned int bi_size;       /* 以字节为单位所需传输的数据大小,驱动中可以使用bio_sectors(bio)宏获得以扇区为单位的大小。 */ 
    
    /* 为了明了最大的 hw 尺寸,我们考虑这个 bio 中第一个和最后一个 虚拟的可合并的段的尺寸 */ 
    unsigned int bi_hw_front_size; 
    unsigned int bi_hw_back_size;  
    unsigned int bi_max_vecs;               /* 我们能持有的最大 bvl_vecs 数 */  
    struct bio_vec *bi_io_vec;               /* bio_vec 结构体,bio 的核心*/ 
    bio_end_io_t *bi_end_io; 
    atomic_t bi_cnt; 
    void *bi_private;  
    bio_destructor_t *bi_destructor; 
 }; 

  bio_vec结构体

struct bio_vec 
 { 
 struct page *bv_page;                   /* 页指针 */ 
 unsigned int bv_len;                     /* 传输的字节数 */ 
 unsigned int bv_offset;                   /* 偏移位置 */ 
 }; 

  我们不应该直接访问 bio 的 bio_vec 成员,而应该使用 bio_for_each_segment()宏来进行这项工作,可以用这个宏循环遍历整个 bio 中的每个段。
  内核还提供了一组函数(宏)用于操作 bio:

int bio_data_dir(struct bio *bio); 
/*这个函数可用于获得数据传输的方向是 READ 还是 WRITE。*/
struct page *bio_page(struct bio *bio) ; 
/*这个函数可用于获得目前的页指针。*/
int bio_offset(struct bio *bio) ; 
/*这个函数返回操作对应的当前页内的偏移,通常块 I/O 操作本身就是页对齐的。*/
int bio_cur_sectors(struct bio *bio) ; 
/*这个函数返回当前 bio_vec 要传输的扇区数。*/
char *bio_data(struct bio *bio) ; 
/*这个函数返回数据缓冲区的内核虚拟地址*/

块设备驱动程序的加载和卸载

Linux块设备驱动程序的注册

  块设备驱动中的第一个工作通常是注册它们自己到内核,完成这个任务的函数是register_ blkdev(),其原型为:
   int register_blkdev(unsigned int major, const char *name);
  major 参数是块设备要使用的主设备号,name 为设备名,它会在/proc/devices 中被显示。如果 major 为 0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。如果 register_blkdev()返回一个负值,表明发生了一个错误。
  与 register_blkdev()对应的注销函数是 unregister_blkdev(),其原型为:
  int unregister_blkdev(unsigned int major, const char *name);
  这里,传递给 register_blkdev()的参数必须与传递给 register_blkdev()的参数匹配,否则这个函数返回-EINVAL。
块设备驱动注册模板:

 xxx_major = register_blkdev(xxx_major, "xxx"); 
 if (xxx_major <= 0) //注册失败
 { 
 printk(KERN_WARNING "xxx: unable to get major number\n"); 
 return -EBUSY; 
 } 

Linux块设备驱动程序的模块加载

  在块设备驱动的模块加载函数中通常需要完成如下工作:

  1. 分配、初始化请求队列,绑定请求队列和请求函数。
  2. 分配、初始化 gendisk,给 gendisk 的 major、fops、queue 等成员赋值,最后添
    加 gendisk。
  3. 注册块设备驱动。

  块设备驱动的模块加载函数模板(使用 blk_init_queue)(主要用于请求队列的request函数):

static int _ _init xxx_init(void) 
{ 
   //块设备驱动注册
   if (register_blkdev(XXX_MAJOR, "xxx")) 
      { 
          err = - EIO; 
          goto out; 
      } 
    //请求队列初始化
   xxx_queue = blk_init_queue(xxx_request, xxx_lock);
   if (!xxx_queue)  { 
      goto out_queue; 
     } 
   blk_queue_hardsect_size(xxx_queue, xxx_blocksize);  //硬件扇区尺寸设置 
    //gendisk 初始化 
   xxx_disks->major = XXX_MAJOR;  
   xxx_disks->first_minor = 0;  
   xxx_disks->fops = &xxx_op; 
   xxx_disks->queue = xxx_queue;  
   sprintf(xxx_disks->disk_name, "xxx%d", i);  
   set_capacity(xxx_disks, xxx_size *2);  
   add_disk(xxx_disks);     //添加 gendisk  
   return 0; 
   out_queue: unregister_blkdev(XXX_MAJOR, "xxx") 
   out: put_disk(xxx_disks);  
   blk_cleanup_queue(xxx_queue);  
   return - ENOMEM; 
} 

  块设备驱动的模块加载函数模板(使用 blk_alloc_queue):(主要用于不带请求队列的制造请求函数)

static int _ _init xxx_init(void) 
 { 
    //分配 gendisk 
    xxx_disks = alloc_disk(1); 
    if (!xxx_disks) 
      { 
         goto out; 
      }  
     //块设备驱动注册
    if (register_blkdev(XXX_MAJOR, "xxx")) 
    { 
       err = - EIO; 
       goto out; 
    }  
     //“请求队列”分配
    xxx_queue = blk_alloc_queue(GFP_KERNEL); 
    if (!xxx_queue) 
    { 
       goto out_queue; 
    }  
    blk_queue_make_request(xxx_queue, &xxx_make_request);     //绑定“制造请求”函数
    blk_queue_hardsect_size(xxx_queue, xxx_blocksize);           //硬件扇区尺寸设置 
    //gendisk 初始化
    xxx_disks->major = XXX_MAJOR; 
    xxx_disks->first_minor = 0; 
    xxx_disks->fops = &xxx_op; 
    xxx_disks->queue = xxx_queue; 
    sprintf(xxx_disks->disk_name, "xxx%d", i); 
    set_capacity(xxx_disks, xxx_size);                    //xxx_size 以 512bytes 为单位
    add_disk(xxx_disks); 
                              //添加 gendisk  
    return 0; 
    out_queue: unregister_blkdev(XXX_MAJOR, "xxx"); 
    out: put_disk(xxx_disks); 
    blk_cleanup_queue(xxx_queue);  
    return - ENOMEM; 
 } 

  块设备驱动程序的模块卸载
  在块设备驱动的模块卸载函数中完成与模块加载函数相反的工作:
① 清除请求队列。
② 删除 gendisk 和对 gendisk 的引用。
③ 删除对块设备的引用,注销块设备驱动。
  块设备驱动的模块卸载函数的模板

 static void _ _exit xxx_exit(void) 
 { 
    if (bdev) 
    { 
      invalidate_bdev(xxx_bdev, 1); 
      blkdev_put(xxx_bdev); 
    } 
    del_gendisk(xxx_disks);                  //删除 gendisk 
    put_disk(xxx_disks); 
    blk_cleanup_queue(xxx_queue[i]);         //清除请求队列
    unregister_blkdev(XXX_MAJOR, "xxx");      //解除设备
 } 

块设备驱动程序的函数操作

   类似字符设备操作函数,块设备操作函数也有open()、release()、ioctl()等方法。

open()和 release()函数

  块设备驱动的 open()和 release()函数并非是必须的,一个简单的块设备驱动可以不提供 open()和 release()函数。
  块设备驱动的 open()函数和其字符设备驱动的对等体非常类似,都以相关的 inode和 file 结构体指针作为参数。当一个节点引用一个块设备时,inode->i_bdev->bd_disk包含一个指向关联 gendisk 结构体的指针。因此,类似于字符设备驱动,我们也可以将 gendisk 的 private_data 赋给 file 的 private_data,private_data 同样最好是指向描述该设备的设备结构体 xxx_dev 的指针,在块设备的 open()函数中赋值 private_data 。

 static int xxx_open(struct inode *inode, struct file *filp) 
 { 
    struct xxx_dev *dev = inode->i_bdev->bd_disk->private_data; 
    filp->private_data = dev;        //赋值 file 的 private_data 
    ... 
    return 0; 
 } 

  注意:在一个处理真实的硬件设备的驱动中,open()和 release()方法还应当设置驱动硬件的状态,这些工作可能包括启停磁盘、加锁一个可移出设备和分配 DMA 缓冲等。

ioctl()函数

  与字符设备驱动一样,块设备可以包含一个 ioctl()函数以提供对设备的 I/O 控制能力。实际上,高层的块设备层代码处理了绝大多数 ioctl(),因此,具体的块设备驱动中通常不再需要实现很多 ioctl 命令。
  块设备驱动的 I/O 控制函数模板:

 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) 
 { 
    long size; 
    struct hd_geometry geo; 
    struct xxx_dev *dev = filp->private_data;     //通过 file->private 获得设备结构体 
    switch (cmd) 
    { 
       case HDIO_GETGEO:    /*只实现一个命令 HDIO_GETGEO,用于获得磁
盘的几何信息(geometry,指 CHS,即 Cylinder、Head、Sector/Track)。*/
       size = dev->size *(hardsect_size / KERNEL_SECTOR_SIZE); 
       geo.cylinders = (size &~0x3f) >> 6; 
       geo.heads = 4; 
       geo.sectors = 16; 
       geo.start = 4; 
    if (copy_to_user((void _ _user*)arg, &geo, sizeof(geo))) 
    { 
       return - EFAULT; 
    } 
    return 0; 
 }  
    return - ENOTTY; //不知道的命令
 } 

块设备驱动程序的request函数

使用请求队列实现request函数

  块设备驱动请求函数的原型为:
  void request(request_queue_t *queue);
  这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时,它才调用这个函数。
  请求函数可以在没有完成请求队列中的所有请求的情况下返回,甚至它一个请求不完成都可以返回。但是,对大部分设备而言,在请求函数中处理完所有请求后再返回通常是值得推荐的方法。
  实例:

static void xxx_request(request_queue_t *q) 
 { 
    struct request *req; 
    while ((req = elv_next_request(q)) != NULL) /*使用 elv_next_request()获得队列中第一个未完成的请求*/
    { 
       struct xxx_dev *dev = req->rq_disk->private_data;    //获得设备
       if (!blk_fs_request(req))             //不是文件系统请求
       { 
           printk(KERN_NOTICE "Skip non-fs request\n"); 
           end_request(req, 0);          //通知请求处理失败,将请求从请求队列中剥离
           continue; 
       } 
      xxx_transfer(dev, req->sector, req->current_nr_sectors, 
                   req->buffer, 
                   rq_data_dir(req));   //处理这个请求
                   end_request(req, 1);            //通知成功完成这个请求,将请求从请求队列中剥离
       } 
 } 
 //完成具体的块设备 I/O 操作
static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, 
   unsigned long nsect, 
   char *buffer, int write) 
   { 
      unsigned long offset = sector * KERNEL_SECTOR_SIZE; 
      unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE; 
      if ((offset + nbytes) > dev->size) 
      { 
         printk(KERN_NOTICE "Beyond-end write (%ld %ld)\n", offset, nbytes); 
         return ; 
      } 
     if (write) 
     { 
         write_dev(offset, buffer, nbytes);    //向设备写 nbytes 个字节的数据
     } 
    else 
     { 
         read_dev(offset, buffer, nbytes);    //从设备读 nbytes 个字节的数据
     } 
 } 

  更复杂的请求函数,它进行了 3 层遍历:遍历请求队列中的每个请求,遍历请求中的每个 bio,遍历 bio 中的每个段:

static void xxx_full_request(request_queue_t * q)
	{
	struct request * req;
	int sectors_xferred;
	struct xxx_dev * dev = q->queuedata;
	/* 遍历每个请求 */
 
	while ((req = elv_next_request(q)) != NULL)
		{
		if (!blk_fs_request(req))
			{
			printk(KERN_NOTICE "Skip non-fs request\n");
			end_request(req, 0);
			continue;
			}
		sectors_xferred = xxx_xfer_request(dev, req);
		if (!end_that_request_first(req, 1, sectors_xferred))
			{
			blkdev_dequeue_request(req);
			end_that_request_last(req);
			}
		}
	} 
 /* 请求处理 */
 static int xxx_xfer_request(struct xxx_dev * dev, struct request * req)
	{
	struct bio * bio;
	int nsect = 0;
	/* 遍历请求中的每个 bio */
	rq_for_each_bio(bio, req)
		{
		xxx_xfer_bio(dev, bio);
		nsect += bio->bi_size / KERNEL_SECTOR_SIZE;
		}
	return nsect;
	}
 /* bio 处理 */
 static int xxx_xfer_bio(struct xxx_dev * dev, struct bio * bio)
	{
	int i;
	struct bio_vec * bvec;
	sector_t sector = bio->bi_sector;
	/* 遍历每一段 */
	bio_for_each_segment(bvec, bio, i)
		{
		char *buffer = _ _bio_kmap_atomic(bio, i, KM_USER0);
		xxx_transfer(dev, sector, bio_cur_sectors(bio), buffer, bio_data_dir(bio) == WRITE);
		sector += bio_cur_sectors(bio);
		_ _bio_kunmap_atomic(bio, KM_USER0);
		}
	return 0;
	}

不使用请求队列的request函数

  使用请求队列对于一个机械的磁盘设备而言的确有助于提高系统的性能,但是对于许多块设备,如数码相机的存储卡、RAM 盘等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。对于这些设备,块层支持“无队列”的操作模式,为使用这个模式,驱动必须提供一个“制造请求”函数,而不是一个请求函数,“制造请求”函数的原型为:
  typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
  上述函数的第一个参数仍然是“请求队列”,但是这个“请求队列”实际不包含任何请求。因此,“制造请求”函数的主要参数是 bio 结构体,这个 bio 结构体表示一个或多个要传送的缓冲区。“制造请求”函数或者直接进行传输,或者把请求重定向给其他设备。
  在“制造请求”函数中处理 bio完成后应该使用 bio_endio()函数通知处理结束:
  void bio_endio(struct bio *bio, unsigned int bytes, int error);
  参数 bytes 是已经传送的字节数,它可以比这个 bio 所代表的字节数少,这意味着“部分完成”,同时 bio 结构体中的当前缓冲区指针需要更新。当设备进一步处理这个 bio 后,驱动应该再次调用 bio_endio(),如果不能完成这个请求,应指出一个错误,错误码赋值给 error 参数。
  不管对应的 I/O 处理成功与否,“制造请求”函数都应该返回 0。如果“制造请求”函数返回一个非零值,bio 将被再次提交。
  制造请求的例子:

  static int xxx_make_request(request_queue_t * q, struct bio * bio)
	{
	struct xxx_dev * dev = q->queuedata;
	int status;
	status = xxx_xfer_bio(dev, bio);	            //处理 bio,和上面处理方式一样
	bio_endio(bio, bio->bi_size, status);			//通告结束
	return 0;
	}

  注意:使用请求队列的request函数的话,那么块设备驱动的模块加载函数要使用blk_init_queue 的方式,使用无队列的制造请求函数的话,那么块设备驱动的模块加载函数使用blk_alloc_queue的方式。

你可能感兴趣的:(Linux设备驱动程序,Linux设备驱动程序,Linux,块设备,驱动程序)