精华内容
下载资源
问答
  • linux 块设备驱动

    千次阅读 2018-06-06 15:45:35
    首先需要了解下块设备驱动和字符设备驱动区别:1.字符设备驱动都是以字节流的形式,而块设备驱动是以块读写和操作(比如磁盘是扇区,flash是page为单位)2.字符设备app可以直接读写,简单快捷。块设备驱动需要中间...

    首先需要了解下块设备驱动和字符设备驱动区别:

    1.字符设备驱动都是以字节流的形式,而块设备驱动是以块读写和操作(比如磁盘是扇区,闪存是页为单位)

    2.字符设备的应用程序可以直接读写,简单快捷。块设备驱动需要中间实现一个缓冲队列,然后实现调度算法(合并,调整顺序等)

    磁盘例子:先读后写,调整了顺序。

    2. flash会合并下两个操作。


    具体流程如下图所示,应用程序开始对磁盘读写时候,首先会进行系统调用,然后通过文件系统实现对通用块层实现调用。


    最终文件系统会调用ll_rw_block。驱动和系统调用IO操作(比如写个TXT)关系如下



    展开全文
  • Linux块设备驱动

    万次阅读 2008-01-29 21:50:00
    第十三章 Linux块设备驱动本章导读块设备是与字符设备并列的概念,这两类设备在Linux中驱动的结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多,在I/O操作上表现出极大的不同,缓冲、I/O调度、请求...
    第十三章  Linux块设备驱动
    本章导读
    块设备是与字符设备并列的概念,这两类设备在Linux中驱动的结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多,在I/O操作上表现出极大的不同,缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。本章将向您展示Linux块设备驱动的编程方法。
    13.1节分析块设备I/O操作的特点,对比字符设备与块设备在I/O操作上的差异。
    13.2节从整体上描述Linux块设备驱动的结构,分析主要的数据结构、函数及其关系。
    13.3~13.5节分别阐述块设备驱动模块加载与卸载、打开与释放和ioctl()函数。
    13.6节非常重要,它讲述了块设备I/O操作所依赖的请求队列的概念及用法。
    13.2节与13.3~13.6节是整体与部分的关系,13.2~13.6节与13.7节是迭代递进关系。
    13.7节在13.1~13.6节讲解内容的基础上,总结Linux下块设备的读写流程。而13.7节则给出了块设备驱动的具体实例,即RAMDISK的驱动。
    13.1块设备的I/O操作特点
    字符设备与块设备I/O操作的不同在于:
    ① 块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。
    ② 块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无需缓冲且被直接读写。对于存储设备而言调整读写的顺序作用巨大,因为在读写连续的扇区比分离的扇区更快。
    ③ 字符设备只能被顺序读写,而块设备可以随机访问。虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能。如图13.1,对磁盘1、10、3、2的请求被调整为对1、2、3、10的请求可以提高读写性能。注意,对SD卡、RAMDISK等块设备而言,不存在机械上的原因,进行这样的调整没有必要。
     
    图13.1 调整块设备I/O操作的顺序
    13.2 Linux块设备驱动结构
    13.2.1 block_device_operations结构体
    在块设备驱动中,有1个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,它是对块设备操作的集合,定义如代码清单13.1。
    代码清单13.1  block_device_operations结构体
    1  struct block_device_operations
    2  {
    3    int(*open)(struct inode *, struct file*);  //打开
    4    int(*release)(struct inode *, struct file*);  //释放
    5    int(*ioctl)(struct inode *, struct file *, unsigned, unsigned long);  //ioctl
    6    long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);
    7    long(*compat_ioctl)(struct file *, unsigned, unsigned long);
    8    int(*direct_access)(struct block_device *, sector_t, unsigned long*);
    9    int(*media_changed)(struct gendisk*);  //介质被改变?
    10   int(*revalidate_disk)(struct gendisk*);  //使介质有效
    11   int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驱动器信息
    12   struct module *owner; //模块拥有者
    13 };
    下面对其主要的成员函数进行分析:
    •  打开和释放
    int (*open)(struct inode *inode, struct file *filp);
    int (*release)(struct inode *inode, struct file *filp);
    与字符设备驱动类似,当设备被打开和关闭时将调用它们。
    •  IO控制
    int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd,
    unsigned long arg);
    上述函数是ioctl() 系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。
    •  介质改变
    int (*media_changed) (struct gendisk *gd);
    被内核调用来检查是否驱动器中的介质已经改变,如果是,则返回一个非零值,否则返回0。这个函数仅适用于支持可移动介质的驱动器(非可移动设备的驱动不需要实现这个方法),通常需要在驱动中增加1个表示介质状态是否改变的标志变量。
     •  使介质有效
    int (*revalidate_disk) (struct gendisk *gd);
    revalidate_disk()函数被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好。
    •  获得驱动器信息
    int (*getgeo)(struct block_device *, struct hd_geometry *);
    该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry结构体包含磁头、扇区、柱面等信息。
    •  模块指针
    struct module *owner;
    一个指向拥有这个结构体的模块的指针,它通常被初始化为THIS_MODULE。
    13.2.2 gendisk结构体
    在Linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区),这个结构体的定义如代码清单13.2。
    代码清单13.2 gendisk结构体
    1  struct gendisk
    2  {
    3    int major; /* 主设备号 */
    4    int first_minor;  /*第1个次设备号*/
    5    int minors; /* 最大的次设备数,如果不能分区,则为1*/
    6    char disk_name[32]; /* 设备名称 */
    7    struct hd_struct **part; /* 磁盘上的分区信息 */
    8    struct block_device_operations *fops; /*块设备操作结构体*/
    9    struct request_queue *queue;  /*请求队列*/
    10   void *private_data;  /*私有数据*/
    11   sector_t capacity; /*扇区数,512字节为1个扇区*/
    12
    13   int flags;
    14   char devfs_name[64];
    15   int number;
    16   struct device *driverfs_dev;
    17   struct kobject kobj;
    18
    19   struct timer_rand_state *random;
    20   int policy;
    21
    22   atomic_t sync_io; /* RAID */
    23   unsigned long stamp;
    24   int in_flight;
    25   #ifdef CONFIG_SMP
    26     struct disk_stats *dkstats;
    27   #else
    28     struct disk_stats dkstats;
    29   #endif
    30 };
    major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享1个主设备号,而次设备号则不同。fops为block_device_operations,即上节描述的块设备操作集合。queue是内核用来管理这个设备的 I/O请求队列的指针。capacity表明设备的容量,以512个字节为单位。private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。
    Linux内核提供了一组函数来操作gendisk,主要包括:
     •  分配gendisk
    gendisk结构体是一个动态分配的结构体,它需要特别的内核操作来初始化,驱动不能自己分配这个结构体,而应该使用下列函数来分配gendisk:
    struct gendisk *alloc_disk(int minors);
    minors 参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。
    •  增加gendisk
    gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备:
    void add_disk(struct gendisk *gd);
    特别要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。
    •  释放gendisk
    当不再需要一个磁盘时,应当使用如下函数释放gendisk:
    void del_gendisk(struct gendisk *gd);
    •  gendisk引用计数
    gendisk中包含1个kobject成员,因此,它是一个可被引用计数的结构体。通过get_disk()和put_disk()函数可用来操作引用计数,这个工作一般不需要驱动亲自做。通常对 del_gendisk()的调用会去掉gendisk的最终引用计数,但是这一点并不是一定的。因此,在del_gendisk()被调用后,这个结构体可能继续存在。
    •  设置gendisk容量
    void set_capacity(struct gendisk *disk, sector_t size);
    块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。虽然大多数块设备的扇区大小都是512字节,不过其它大小的扇区也很常见,比如,很多CD-ROM盘的扇区都是2K大小。
    不管物理设备的真实扇区大小是多少,内核与块设备驱动交互的扇区都以512字节为单位。因此,set_capacity()函数也以512字节为单位。
    13.2.3 request与bio结构体
    1、请求
    在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求,这个结构体的定义如代码清单13.3。
    代码清单13.3 request结构体
    1  struct request
    2  {
    3    struct list_head queuelist; /*链表结构*/
    4    unsigned long flags; /* REQ_ */

    6    sector_t sector; /* 要传送的下1个扇区 */
    7    unsigned long nr_sectors; /*要传送的扇区数目*/
    8    /*当前要传送的扇区数目*/
    9    unsigned int current_nr_sectors;
    10
    11   sector_t hard_sector; /*要完成的下1个扇区*/
    12   unsigned long hard_nr_sectors; /*要被完成的扇区数目*/
    13   /*当前要被完成的扇区数目*/
    14   unsigned int hard_cur_sectors;
    15
    16   struct bio *bio; /*请求的 bio 结构体的链表*/
    17   struct bio *biotail; /*请求的 bio 结构体的链表尾*/
    18
    19   void *elevator_private;
    20
    21   unsigned short ioprio;
    22
    23   int rq_status;
    24   struct gendisk *rq_disk;
    25   int errors;
    26   unsigned long start_time;
    27
    28   /*请求在物理内存中占据的不连续的段的数目,scatter/gather列表的尺寸*/
    29   unsigned short nr_phys_segments;
    30
    31   /*与nr_phys_segments相同,但考虑了系统I/O MMU的remap */
    32   unsigned short nr_hw_segments;
    33
    34   int tag;
    35   char *buffer; /*传送的缓冲,内核虚拟地址*/
    36
    37   int ref_count; /* 引用计数 */
    38   ...
    39 };
    request结构体的主要成员包括:
    sector_t hard_sector;
    unsigned long hard_nr_sectors;
    unsigned int hard_cur_sectors;
    上述3个成员标识还未完成的扇区,hard_sector是第1个尚未传输的扇区,hard_nr_sectors是尚待完成的扇区数,hard_cur_sectors是并且当前I/O操作中待完成的扇区数。这些成员只用于内核块设备层,驱动不应当使用它们。
    sector_t sector;
    unsigned long nr_sectors;
    unsigned int current_nr_sectors;
    驱动中会经常与这3个成员打交道,这3个成员在内核和驱动交互中发挥着重大作用。它们以512字节大小为1个扇区,如果硬件的扇区大小不是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返回值表示向设备写。
    2、请求队列
    一个块请求队列是一个块 I/O 请求的队列,其定义如代码清单13.4。
    代码清单13.4 request队列结构体
    1  struct request_queue
    2  {
    3    ...
    4    /* 保护队列结构体的自旋锁 */
    5    spinlock_t __queue_lock;
    6    spinlock_t *queue_lock;

    8    /* 队列kobject */
    9    struct kobject kobj;
    10
    11   /* 队列设置 */
    12   unsigned long nr_requests; /* 最大请求数量 */
    13   unsigned int nr_congestion_on;
    14   unsigned int nr_congestion_off;
    15   unsigned int nr_batching;
    16  
    17   unsigned short max_sectors;  /* 最大的扇区数 */
    18   unsigned short max_hw_sectors;
    19   unsigned short max_phys_segments; /* 最大的段数 */
    20   unsigned short max_hw_segments; 
    21   unsigned short hardsect_size;  /* 硬件扇区尺寸 */
    22   unsigned int max_segment_size;  /* 最大的段尺寸 */
    23
    24   unsigned long seg_boundary_mask; /* 段边界掩码 */
    25   unsigned int dma_alignment;  /* DMA 传送的内存对齐限制 */
    26
    27   struct blk_queue_tag *queue_tags;
    28
    29   atomic_t refcnt; /* 引用计数 */
    30
    31   unsigned int in_flight;
    32
    33   unsigned int sg_timeout;
    34   unsigned int sg_reserved_size;
    35   int node;
    36
    37   struct list_head drain_list;
    38
    39   struct request *flush_rq;
    40   unsigned char ordered;
    41 };
    请求队列跟踪等候的块I/O请求,它存储用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求。
    请求队列还实现一个插入接口,这个接口允许使用多个I/O调度器,I/O调度器(也称电梯)的工作是以最优性能的方式向驱动提交I/O请求。大部分I/O 调度器累积批量的 I/O 请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。进行这些工作的原因在于,对于磁头而言,当给定顺序排列的请求时,可以使得磁盘顺序地从一头到另一头工作,非常像一个满载的电梯,在一个方向移动直到所有它的“请求”已被满足。
    另外,I/O调度器还负责合并邻近的请求,当一个新 I/O 请求被提交给调度器后,它会在队列里搜寻包含邻近扇区的请求;如果找到一个,并且如果结果的请求不是太大,调度器将合并这2个请求。
    对磁盘等块设备进行I/O操作顺序的调度类似于电梯的原理,先服务完上楼的乘客,再服务下楼的乘客效率会更高,而“上蹿下跳”,顺序响应用户的请求则会导致电梯无序地忙乱。
    Linux 2.6包含4个I/O调度器,它们分别是No-op I/O scheduler、Anticipatory I/O scheduler、Deadline I/O scheduler与CFQ I/O scheduler。
    Noop I/O scheduler是一个简化的调度程序,它只作最基本的合并与排序。
    Anticipatory I/O scheduler是当前内核中默认的I/O调度器,它拥有非常好的性能,在2.5中它就相当引人注意。在与2.4内核进行的对比测试中,在2.4中多项以分钟为单位完成的任务,它则是以秒为单位来完成的,正因为如此它成为目前2.6中默认的I/O调度器。Anticipatory I/O scheduler的缺点是比较庞大与复杂,在一些特殊的情况下,特别是在数据吞吐量非常大的数据库系统中它会变得比较缓慢。
    Deadline I/O scheduler是针对Anticipatory I/O scheduler的缺点进行改善而来的,表现出的性能几乎与Anticipatory I/O scheduler一样好,但是比Anticipatory小巧。
    CFQ I/O scheduler为系统内的所有任务分配相同的带宽,提供一个公平的工作环境,它比较适合桌面环境。事实上在测试中它也有不错的表现,mplayer、xmms等多媒体播放器与它配合的相当好,回放平滑,几乎没有因访问磁盘而出现的跳帧现象。
    内核block目录中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c文件分别实现了上述调度算法。
    可以通过给kernel添加启动参数,选择使用的IO调度算法,如:
    kernel elevator=deadline
    •  初始化请求队列
    request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
    该函数的第1个参数是请求处理函数的指针,第2个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,故它可能会失败,因此一定要检查它的返回值。这个函数一般在块设备驱动的模块加载函数中调用。
    •  清除请求队列
    void blk_cleanup_queue(request_queue_t * q);
    这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
    而blk_put_queue()宏则定义为:
    #define blk_put_queue(q) blk_cleanup_queue((q))
    •  分配“请求队列”
    request_queue_t *blk_alloc_queue(int gfp_mask);
    对于FLASH、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配1个“请求队列”,并使用如下函数来绑定“请求队列”和“制造请求”函数。
    void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);
    在13.6.2节我们会看到,这种方式分配的“请求队列”实际上不包含任何请求,所以给其加上引号。
    •  提取请求
    struct request *elv_next_request(request_queue_t *queue);
    上述函数用于返回下一个要处理的请求(由 I/O 调度器决定),如果没有请求则返回NULL。elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,但是标识它为活动的,这个标识将阻止I/O 调度器合并其它的请求到已开始执行的请求。因为elv_next_request()不从队列里清除请求,因此连续调用它2次,2次会返回同一个请求结构体。
    •  去除请求
    void blkdev_dequeue_request(struct request *req);
    上述函数从队列中去除1个请求。如果驱动中同时从同一个队列中操作了多个请求,它必须以这样的方式将它们从队列中去除。
    如果需要将1个已经出列的请求归还到队列中,可以调用:
    void elv_requeue_request(request_queue_t *queue, struct request *req);
    另外,块设备层还提供了一套函数,这些函数可被驱动用来控制一个请求队列的操作,主要包括:
    •  启停请求队列
    void blk_stop_queue(request_queue_t *queue);
    void blk_start_queue(request_queue_t *queue);
    如果块设备到达不能处理等候的命令的状态,应调用blk_stop_queue()来告知块设备层。之后,请求函数将不被调用,除非再次调用blk_start_queue()将设备恢复到可处理请求的状态。
    •  参数设置
    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()都控制1个请求中可包含的最大物理段(系统内存中不相邻的区),blk_queue_max_hw_segments()考虑了系统I/O内存管理单元的重映射,这2个参数缺省都是 128。blk_queue_max_segment_size告知内核请求段的最大字节数,缺省值为65,536。
    •  通告内核
    void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
    该函数用于告知内核块设备执行DMA时可使用的最高物理地址dma_addr,如果一个请求包含超出这个限制的内存引用,一个“反弹”缓冲区将被用来给这个操作。这种方式的代价昂贵,因此应尽量避免使用。
    可以给dma_addr参数提供任何可能的值或使用预先定义的宏,如BLK_BOUNCE_HIGH(对高端内存页使用反弹缓冲区)、BLK_BOUNCE_ISA(驱动只可在16M的ISA区执行DMA)或者BLK_BOUCE_ANY(驱动可在任何地址执行DMA),缺省值是BLK_BOUNCE_HIGH。
    blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
    如果我们正在驱动编写的设备无法处理跨越一个特殊大小内存边界的请求,应该使用这个函数来告知内核这个边界。例如,如果设备处理跨4MB 边界的请求有困难,应该传递一个0x3fffff 掩码。缺省的掩码是0xffffffff(对应4GB边界)。
    void blk_queue_dma_alignment(request_queue_t *queue, int mask);
    该函数用于告知内核块设备施加于DMA 传送的内存对齐限制,所有请求都匹配这个对齐,缺省的屏蔽是 0x1ff,它导致所有的请求被对齐到 512字节边界。
    void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);
    该函数用于告知内核块设备硬件扇区的大小,所有由内核产生的请求都是这个大小的倍数并且被正确对界。但是,内核块设备层和驱动之间的通信还是以512字节扇区为单位进行。
    3、块I/O
    通常1个bio对应1个I/O请求,代码清单13.5给出了bio结构体的定义。IO调度算法可将连续的bio合并成1个请求。所以,1个请求可以包含多个bio。
    代码清单13.5 bio结构体
    1  struct bio
    2  {
    3    sector_t bi_sector; /* 要传输的第1个扇区 */
    4    struct bio *bi_next; /* 下一个bio */
    5    struct block_device *bi_bdev;
    6    unsigned long bi_flags; /* 状态,命令等 */
    7    unsigned long bi_rw; /* 低位表示READ/WRITE,高位表示优先级*/

    9    unsigned short bi_vcnt; /* bio_vec数量 */
    10   unsigned short bi_idx; /* 当前bvl_vec索引 */
    11
    12   /*不相邻的物理段的数目*/
    13   unsigned short bi_phys_segments;
    14
    15   /*物理合并和DMA remap合并后不相邻的物理段的数目*/
    16   unsigned short bi_hw_segments;
    17
    18   unsigned int bi_size; /* 以字节为单位所需传输的数据大小 */
    19
    20   /* 为了明了最大的hw尺寸,我们考虑这个bio中第1个和最后1个
    21    虚拟的可合并的段的尺寸 */
    22   unsigned int bi_hw_front_size;
    23   unsigned int bi_hw_back_size;
    24
    25   unsigned int bi_max_vecs; /* 我们能持有的最大bvl_vecs数 */
    26
    27   struct bio_vec *bi_io_vec; /* 实际的vec列表 */
    28
    29   bio_end_io_t *bi_end_io;
    30   atomic_t bi_cnt;
    31
    32   void *bi_private;
    33
    34   bio_destructor_t *bi_destructor; /* destructor */
    35 };
    下面我们对其中的核心成员进行分析:
    sector_t bi_sector;
    标示这个 bio 要传送的第一个(512字节)扇区。
    unsigned int bi_size;
    被传送的数据大小,以字节为单位,驱动中可以使用bio_sectors(bio)宏获得以扇区为单位的大小。
    unsigned long bi_flags;
    一组描述 bio 的标志,如果这是一个写请求,最低有效位被置位,可以使用bio_data_dir(bio)宏来获得读写方向。
    unsigned short bio_phys_segments;
    unsigned short bio_hw_segments;
    分别表示包含在这个 BIO 中要处理的不连续的物理内存段的数目和考虑DMA重映像后的不连续的内存段的数目。
    bio的核心是一个称为 bi_io_vec的数组,它由bio_vec结构体组成,bio_vec结构体的定义如代码清单13.6。
    代码清单13.6 bio_vec结构体
    1 struct bio_vec
    2 {
    3   struct page *bv_page;  /* 页指针 */
    4   unsigned int bv_len;  /* 传输的字节数 */
    5   unsigned int bv_offset; /* 偏移位置 */
    6 };
    我们不应该直接访问bio的bio_vec成员,而应该使用bio_for_each_segment()宏来进行这项工作,可以用这个宏循环遍历整个bio中的每个段,这个宏的定义如代码清单13.7。
    代码清单13.7 bio_for_each_segment宏
    1 #define __bio_for_each_segment(bvl, bio, i, start_idx)                  /
    2         for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx);  /
    3              i < (bio)->bi_vcnt; /                                      
    4              bvl++, i++)
    5
    6 #define bio_for_each_segment(bvl, bio, i) /                             
    7           __bio_for_each_segment(bvl, bio, i, (bio)->bi_idx
    图13.2(a)描述了request队列、request与bio数据结构之间的关系,13.2(b)表示了request、bio和bio_vec数据结构之间的关系,13.2(c)表示了bio与bio_vec数据结构之间的关系,因此整个图13.2递归地呈现了request队列、request、bio和bio_vec这4个结构体之间的关系。
     
    (a) request与bio
     
    (b) request、bio和bio_vec
     
    (c)bio与bio_vec
    图13.2 request队列、request、bio和bio_vec结构体之间的关系
    内核还提供了一组函数(宏)用于操作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) ;
    这个函数返回数据缓冲区的内核虚拟地址。
    char *bvec_kmap_irq(struct bio_vec *bvec, unsigned long *flags) ;
    这个函数返回一个内核虚拟地址,这个地址可用于存取被给定的bio_vec入口指向的数据缓冲区。它也会屏蔽中断并返回1个原子kmap,因此,在bvec_kunmap_irq()被调用以前,驱动不应该睡眠。
    void bvec_kunmap_irq(char *buffer, unsigned long *flags);
    这个函数是bvec_kmap_irq()函数的“反函数”,它撤销bvec_kmap_irq()创建的映射。
    char *bio_kmap_irq(struct bio *bio, unsigned long *flags);
    这个函数是对bvec_kmap_irq()的包装,它返回给定的bio的当前bio_vec入口的映射。
    char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
    这个函数通过kmap_atomic()获得返回给定bio的第i个缓冲区的虚拟地址。
    void __bio_kunmap_atomic(char *addr, enum km_type type);
    这个函数返还由__bio_kmap_atomic()获得的内核虚拟地址。
    另外,对bio的引用计数通过如下函数完成:
    void bio_get(struct bio *bio);  //引用bio
    void bio_put(struct bio *bio);  //释放对bio的引用
    13.2.4块设备驱动注册与注销
    块设备驱动中的第1个工作通常是注册它们自己到内核,完成这个任务的函数是 register_blkdev(),其原型为:
    int register_blkdev(unsigned int major, const char *name);
    major参数是块设备要使用的主设备号,name为设备名,它会在/proc/devices中被显示。 如果major为0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。如果register_blkdev()返回1个负值,表明发生了一个错误。
    与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:
    int unregister_blkdev(unsigned int major, const char *name);
    这里,传递给register_blkdev()的参数必须与传递给register_blkdev()的参数匹配,否则这个函数返回-EINVAL。
    值得一提的是,在2.6内核中,对 register_blkdev()的调用完全是可选的,register_blkdev()的功能已随时间正在减少,这个调用最多只完全2件事:
    ① 如果需要,分配一个动态主设备号。
    ② 在/proc/devices中创建一个入口。
    在将来的内核中,register_blkdev()可能会被去掉。但是目前的大部分驱动仍然调用它。代码清单13.8给出了1个块设备驱动注册的模板。
    代码清单13.8 块设备驱动注册模板
    1 xxx_major = register_blkdev(xxx_major, "xxx");
    2 if (xxx_major <= 0) //注册失败
    3 {
    4   printk(KERN_WARNING "xxx: unable to get major number/n");
    5   return -EBUSY;
    6 }
    13.3 Linux块设备驱动模块加载与卸载
    在块设备驱动的模块加载函数中通常需要完成如下工作:
    ① 分配、初始化请求队列,绑定请求队列和请求函数。
    ② 分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。
    ③ 注册块设备驱动。
    代码清单13.9和13.10分别给出了使用blk_alloc_queue()分配请求队列并使用blk_queue_make_request()绑定“请求队列”和“制造请求”函数,以及使用blk_init_queue()初始化请求队列并绑定请求队列与请求处理函数2种不同情况下的块设备驱动模块加载函数模板。
    代码清单13.9 块设备驱动模块加载函数模板(使用blk_alloc_queue)
    1  static int __init xxx_init(void)
    2  {
    3    //分配gendisk
    4    xxx_disks = alloc_disk(1);
    5    if (!xxx_disks)
    6    {
    7      goto out;
    8    }

    10   //块设备驱动注册
    11   if (register_blkdev(XXX_MAJOR, "xxx"))
    12   {
    13     err =  - EIO;
    14     goto out;
    15   }
    16
    17   //“请求队列”分配
    18   xxx_queue = blk_alloc_queue(GFP_KERNEL);
    19   if (!xxx_queue)
    20   {
    21     goto out_queue;
    22   }
    23
    24   blk_queue_make_request(xxx_queue, &xxx_make_request); //绑定“制造请求”函数
    25   blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇区尺寸设置
    26
    27   //gendisk初始化
    28   xxx_disks->major = XXX_MAJOR;
    29   xxx_disks->first_minor = 0;
    30   xxx_disks->fops = &xxx_op;
    31   xxx_disks->queue = xxx_queue;
    32   sprintf(xxx_disks->disk_name, "xxx%d", i);
    33   set_capacity(xxx_disks, xxx_size); //xxx_size以512bytes为单位
    34   add_disk(xxx_disks); //添加gendisk
    35
    36   return 0;
    37   out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
    38   out: put_disk(xxx_disks);
    39   blk_cleanup_queue(xxx_queue);
    40
    41   return  - ENOMEM;
    42 }
    代码清单13.10 块设备驱动模块加载函数模板(使用blk_init_queue)
    1  static int __init xxx_init(void)
    2  {
    3    //块设备驱动注册
    4    if (register_blkdev(XXX_MAJOR, "xxx"))
    5    {
    6      err =  - EIO;
    7      goto out;
    8    }

    10   //请求队列初始化
    11   xxx_queue = blk_init_queue(xxx_request, xxx_lock);
    12   if (!xxx_queue)
    13   {
    14     goto out_queue;
    15   }
    16  
    17   blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇区尺寸设置
    18  
    19   //gendisk初始化
    20   xxx_disks->major = XXX_MAJOR;
    21   xxx_disks->first_minor = 0;
    22   xxx_disks->fops = &xxx_op;
    23   xxx_disks->queue = xxx_queue;
    24   sprintf(xxx_disks->disk_name, "xxx%d", i);
    25   set_capacity(xxx_disks, xxx_size *2);
    26   add_disk(xxx_disks); //添加gendisk
    27
    28   return 0;
    29   out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
    30   out: put_disk(xxx_disks);
    31   blk_cleanup_queue(xxx_queue);
    32
    33   return  - ENOMEM;
    34 }
    在块设备驱动的模块卸载函数中通常需要与模块加载函数相反的工作:
    ① 清除请求队列。
    ② 删除gendisk和对gendisk的引用。
    ③ 删除对块设备的引用,注销块设备驱动。
    代码清单13.11给出了块设备驱动模块卸载函数的模板。
    代码清单13.11 块设备驱动模块卸载函数模板
    1  static void __exit xxx_exit(void)
    2  {
    3    if (bdev)
    4    {
    5      invalidate_bdev(xxx_bdev, 1);
    6      blkdev_put(xxx_bdev);
    7    }
    8    del_gendisk(xxx_disks); //删除gendisk
    9    put_disk(xxx_disks);
    10   blk_cleanup_queue(xxx_queue[i]); //清除请求队列
    11   unregister_blkdev(XXX_MAJOR, "xxx");
    12 }
    13.4块设备的打开与释放
    块设备驱动的open()和release()函数并非是必须的,1个简单的块设备驱动可以不提供open()和release()函数。
    块设备驱动的open()函数和其字符设备驱动中的对等体非常类似,都以相关的inode和file结构体指针作为参数。当一个节点引用一个块设备时,inode->i_bdev->bd_disk 包含一个指向关联 gendisk 结构体的指针。因此,类似于字符设备驱动,我们也可以将gendisk的private_data赋给file的private_data,private_data同样最好是指向描述该设备的设备结构体xxx_dev的指针,如代码清单13.12。
    代码清单13.12 块设备的open()函数中赋值private_data
    1 static int xxx_open(struct inode *inode, struct file *filp)
    2 {
    3   struct xxx_dev *dev = inode->i_bdev->bd_disk->private_data;
    4   filp->private_data = dev;  //赋值file的private_data
    5   ...
    6   return 0;
    7 }
    在一个处理真实的硬件设备的驱动中,open()和release()方法还应当设置驱动和硬件的状态,这些工作可能包括启停磁盘、加锁一个可移出设备和分配DMA缓冲等。
    13.5块设备驱动的ioctl函数
    与字符设备驱动一样,块设备可以包含一个 ioctl()函数以提供对设备的I/O控制能力。实际上,高层的块设备层代码处理了绝大多数ioctl(),因此,具体的块设备驱动中通常不再需要实现很多ioctl命令。
    代码清单13.13给出的ioctl()函数只实现1个命令HDIO_GETGEO,用于获得磁盘的几何信息(geometry,指CHS,即Cylinder、Head、Sector/Track)。
    代码清单13.13 块设备驱动的I/O控制函数模板
    1  int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
    2    unsigned long arg)
    3  {
    4    long size;
    5    struct hd_geometry geo;
    6    struct xxx_dev *dev = filp->private_data; //通过file->private获得设备结构体

    8    switch (cmd)
    9    {
    10     case HDIO_GETGEO:
    11       size = dev->size *(hardsect_size / KERNEL_SECTOR_SIZE);
    12       geo.cylinders = (size &~0x3f) >> 6;
    13       geo.heads = 4;
    14       geo.sectors = 16;
    15       geo.start = 4;
    16       if (copy_to_user((void __user*)arg, &geo, sizeof(geo)))
    17       {
    18         return  - EFAULT;
    19       }
    20       return 0;
    21   }
    22
    23   return  - ENOTTY; //不知道的命令
    24 }
    13.6块设备驱动I/O请求处理
    13.6.1使用请求队列
    块设备驱动请求函数的原型为:
    void request(request_queue_t *queue);
    这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时,它才调用这个函数。
    请求函数可以在没有完成请求队列中的所有请求的情况下返回,甚至它1个请求不完成都可以返回。但是,对大部分设备而言,在请求函数中处理完所有请求后再返回通常是值得推荐的方法。代码清单13.14给出了1个简单的request()函数的例子。
    代码清单13.14 块设备驱动请求函数例程
    1  static void xxx_request(request_queue_t *q)
    2  {
    3    struct request *req;
    4    while ((req = elv_next_request(q)) != NULL)
    5    {
    6      struct xxx_dev *dev = req->rq_disk->private_data;
    7      if (!blk_fs_request(req)) //不是文件系统请求
    8      {
    9        printk(KERN_NOTICE "Skip non-fs request/n");
    10       end_request(req, 0);//通知请求处理失败
    11       continue;
    12     }
    13     xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,
    14       rq_data_dir(req)); //处理这个请求
    15     end_request(req, 1); //通知成功完成这个请求
    16   }
    17 }
    18
    19 //完成具体的块设备I/O操作
    20 static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, unsigned
    21   long nsect, char *buffer, int write)
    22 {
    23   unsigned long offset = sector * KERNEL_SECTOR_SIZE;
    24   unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE;
    25   if ((offset + nbytes) > dev->size)
    26   {
    27     printk(KERN_NOTICE "Beyond-end write (%ld %ld)/n", offset, nbytes);
    28     return ;
    29   }
    30   if (write)
    31   {
    32     write_dev(offset, buffer, nbytes);  //向设备些nbytes个字节的数据
    33   }
    34   else
    35   {
    36     read_dev(offset, buffer, nbytes); //从设备读nbytes个字节的数据
    37   }
    38 }
    上述代码第4行使用elv_next_request()获得队列中第一个未完成的请求,end_request()会将请求从请求队列中剥离。第7行判断请求是否为文件系统请求,如果不是,则直接清除,调用end_request(),传递给end_request()的第2个参数为0意味着处理该请求失败。而第15行传递给end_request()的第2个参数为1意味着该请求处理成功。
    end_request()函数非常重要,其源代码如代码清单13.15。
    代码清单13.15 end_request()函数源代码
    1  void end_request(struct request *req, int uptodate)
    2  {
    3   
    4    if (!end_that_request_first(req, uptodate, req->hard_cur_sectors))
    5    {
    6      add_disk_randomness (req->rq_disk);
    7      blkdev_dequeue_request (req);
    8      end_that_request_last(req);
    9    }
    10 }
    当设备已经完成1个I/O请求的部分或者全部扇区传输后,它必须通告块设备层,上述代码中的第4行完成这个工作。end_that_request_first()函数的原型为:
    int end_that_request_first(struct request *req, int success, int count);
    这个函数告知块设备层,块设备驱动已经完成count个扇区的传送。end_that_request_first()的返回值是一个标志,指示是否这个请求中的所有扇区已经被传送。返回值为0表示所有的扇区已经被传送并且这个请求完成,之后,我们必须使用 blkdev_dequeue_request()来从队列中清除这个请求。最后,将这个请求传递给end_that_request_last()函数:
    void end_that_request_last(struct request *req);
    end_that_request_last()通知所有正在等待这个请求完成的对象请求已经完成并回收这个请求结构体。
    第6行的add_disk_randomness()函数的作用是使用块 I/O 请求的定时来给系统的随机数池贡献熵,它不影响块设备驱动。但是,仅当磁盘的操作时间是真正随机的时候(大部分机械设备如此),才应该调用它。
    代码清单13.16给出了1个更复杂的请求函数,它进行了3层遍历:遍历请求队列中的每个请求;遍历请求中的每个bio;遍历bio中的每个段。
    代码清单13.16 请求函数遍历请求、bio和段
    1  static void xxx_full_request(request_queue_t *q)
    2  {
    3    struct request *req;
    4    int sectors_xferred;
    5    struct xxx_dev *dev = q->queuedata;
    6    /* 遍历每个请求 */
    7    while ((req = elv_next_request(q)) != NULL)
    8    {
    9      if (!blk_fs_request(req))
    10     {
    11       printk(KERN_NOTICE "Skip non-fs request/n");
    12
    13       end_request(req, 0);
    14       continue;
    15     }
    16     sectors_xferred = xxx_xfer_request(dev, req);
    17     if (!end_that_request_first(req, 1, sectors_xferred))
    18     {
    19       blkdev_dequeue_request(req);
    20       end_that_request_last(req);
    21     }
    22   }
    23 }
    24 /* 请求处理 */
    25 static int xxx_xfer_request(struct xxx_dev *dev, struct request *req)
    26 {
    27   struct bio *bio;
    28   int nsect = 0;
    29   /* 遍历请求中的每个bio */
    30   rq_for_each_bio(bio, req)
    31   {
    32     xxx_xfer_bio(dev, bio);
    33     nsect += bio->bi_size / KERNEL_SECTOR_SIZE;
    34   }
    35   return nsect;
    36 }
    37 /* bio处理 */
    38 static int xxx_xfer_bio(struct xxx_dev *dev, struct bio *bio)
    39 {
    40   int i;
    41   struct bio_vec *bvec;
    42   sector_t sector = bio->bi_sector;
    43
    44   /* 遍历每1段 */
    45   bio_for_each_segment(bvec, bio, i)
    46   {
    47     char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);
    48     xxx_transfer(dev, sector, bio_cur_sectors(bio), buffer, bio_data_dir(bio)
    49       == WRITE);
    50     sector += bio_cur_sectors(bio);
    51     __bio_kunmap_atomic(bio, KM_USER0);
    52   }
    53   return 0;
    54 }
    图13.3呈现了1个请求队列内request、bio以及bio中segment的层层遍历关系。
     
    图13.3 遍历1个请求队列
    13.6.2不使用请求队列
    使用请求队列对于一个机械的磁盘设备而言的确有助于提高系统的性能,但是对于许多块设备,如数码相机的存储卡、RAM盘等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。对于这些设备,块层支持“无队列”的操作模式,为使用这个模式,驱动必须提供一个“制造请求”函数,而不是一个请求函数,“制造请求”函数的原型为:
    typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
    上述函数的第1个参数仍然是“请求队列”,但是这个“请求队列”实际不包含任何请求。因此,“制造请求”函数的主要参数是bio结构体,这个bio结构体表示1个或多个要传送的缓冲区。“制造请求”函数或者直接进行传输,或者把请求重定向给其它设备。
    在“制造请求”函数中处理bio的方式与13.6.1节中描述的完全一致,但是在处理完成后应该使用bio_endio()函数通知处理结束:
    void bio_endio(struct bio *bio, unsigned int bytes, int error);
    参数bytes 是已经传送的字节数,它可以比这个bio所代表的字节数少,这意味着“部分完成”,同时bio结构体中的当前缓冲区指针需要更新。当设备进一步处理这个bio后,驱动应该再次调用 bio_endio(),如果不能完成这个请求,应指出一个错误,错误码赋值给error参数。
    不管对应的I/O处理成功与否,“制造请求”函数都应该返回0。如果“制造请求”函数返回一个非零值,bio 将被再次提交。
    代码清单13.17给出了1个“制造请求”函数的例子。
    代码清单13.17 “制造请求”函数例程
    1 static int xxx_make_request(request_queue_t *q, struct bio *bio)
    2 {
    3   struct xxx_dev *dev = q->queuedata;
    4   int status;
    5   status = xxx_xfer_bio(dev, bio); //处理bio
    6   bio_endio(bio, bio->bi_size, status); //通告结束
    7   return 0;
    8 }
    为了使用无队列的I/O请求处理,驱动模块加载函数应遵循代码清单13.9的模板而非13.10的模板,而使用请求队列时,驱动模块加载函数应遵循代码清单13.10的模板。
    13.7实例1:RAMDISK驱动
    13.7.1 RAMDISK的硬件原理
    RAMDISK(RAM盘)是一种模拟磁盘,其数据实际上存储在RAM中,它使用一部分内存空间来模拟出一个磁盘,以块设备的方式来访问这片内存,RAMDISK对应的设备文件一般为/dev/ram%d。
     使用如下一组命令就可以创建并挂载RAMDISK:
    mkdir /tmp/ramdisk0    创建装载点
    mke2fs /dev/ram0      创建一个文件系统
    mount /dev/ram0 /tmp/ramdisk0   装载ramdisk
    其中,mke2f /dev/ram0命令的执行会回馈类似于如下的信息:
    mke2fs 1.14, 9-Jan-1999 for EXT2 FS 0.5b, 95/08/09
    Linux ext2 filesystem format
    Filesystem label=
    1024 inodes, 4096 blocks
    204 blocks (4.98%) reserved for the super user
    First data block=1
    Block size=1024 (log=0)
    Fragment size=1024 (log=0)
    1 block group
    8192 blocks per group, 8192 fragments per group
    1024 inodes per group
    表明创建了1个4MB的块设备,共包含4096块,每块1024字节。
    13.7.2 RAMDISK驱动模块加载与卸载
    RAMDISK的驱动模块加载函数完成的工作与13.3节给出的模板完全一致,由于RAM盘属于完全的随机设备,宜使用无队列的I/O处理方式,其驱动中实现了如图13.4所示的与块设备驱动模板对应的函数。
     
    图13.4 块设备驱动模板与RAMDISK设备驱动的映射
    代码清单13.18给出了RAMDISK设备驱动的模块加载与卸载函数,实现的功能与模块是一致的。
    代码清单13.18 RAMDISK设备驱动的模块加载与卸载函数
    1  static int __init rd_init(void)
    2  {
    3    int i;
    4    int err =  - ENOMEM;
    5    //调整块尺寸
    6    if (rd_blocksize > PAGE_SIZE || rd_blocksize < 512 || (rd_blocksize &
    7      (rd_blocksize - 1)))
    8    {
    9      printk("RAMDISK: wrong blocksize %d, reverting to defaults/n", rd_blocksize) ;
    10      
    11     rd_blocksize = BLOCK_SIZE;
    12   }
    13   //分配gendisk
    14   for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
    15   {
    16     rd_disks[i] = alloc_disk(1); //分配gendisk
    17     if (!rd_disks[i])
    18       goto out;
    19   }
    20   //块设备注册
    21   if (register_blkdev(RAMDISK_MAJOR, "ramdisk"))
    22   //注册块设备
    23   {
    24     err =  - EIO;
    25     goto out;
    26   }
    27
    28   devfs_mk_dir("rd"); //创建devfs目录
    29
    30   for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
    31   {
    32     struct gendisk *disk = rd_disks[i];
    33     //分配并绑定请求队列与“制造请求”函数
    34     rd_queue[i] = blk_alloc_queue(GFP_KERNEL);
    35     if (!rd_queue[i])
    36       goto out_queue;
    37
    38     blk_queue_make_request(rd_queue[i], &rd_make_request);   //绑定“制造请求”函数
    39     blk_queue_hardsect_size(rd_queue[i], rd_blocksize); //硬件扇区尺寸设置
    40
    41     //初始化gendisk
    42     disk->major = RAMDISK_MAJOR;
    43     disk->first_minor = i;
    44     disk->fops = &rd_bd_op;
    45     disk->queue = rd_queue[i];
    46     disk->flags |= GENHD_FL_SUPPRESS_PARTITION_INFO;
    47     sprintf(disk->disk_name, "ram%d", i);
    48     sprintf(disk->devfs_name, "rd/%d", i);
    49     set_capacity(disk, rd_size *2);
    50     add_disk(rd_disks[i]); //添加gendisk
    51   }
    52
    53   // rd_size以kB为单位
    54   printk("RAMDISK driver initialized: "
    55     "%d RAMDISKs of %dK size %d blocksize/n",
    56     CONFIG_BLK_DEV_RAM_COUNT,rd_size, rd_blocksize);
    57
    58   return 0;
    59   out_queue: unregister_blkdev(RAMDISK_MAJOR, "ramdisk");
    60   out:
    61   while (i--)
    62   {
    63     put_disk(rd_disks[i]);
    64     blk_cleanup_queue(rd_queue[i]);
    65   }
    66   return err;
    67 }
    68
    69 static void __exit rd_cleanup(void)
    70 {
    71   int i;
    72
    73   for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
    74   {
    75     struct block_device *bdev = rd_bdev[i];
    76     rd_bdev[i] = NULL;
    77     if (bdev)
    78     {
    79       invalidate_bdev(bdev, 1);
    80       blkdev_put(bdev);
    81     }
    82     del_gendisk(rd_disks[i]); //删除gendisk
    83     put_disk(rd_disks[i]); //释放对gendisk的引用
    84     blk_cleanup_queue(rd_queue[i]); //清除请求队列
    85   }
    86   devfs_remove("rd");
    87   unregister_blkdev(RAMDISK_MAJOR, "ramdisk"); //块设备注销
    88 }
    13.7.3 RAMDISK设备驱动block_device_operations及成员函数
    RAMDISK提供block_device_operations结构体中2个成员函数open()和ioctl()的实现,代码清单13.19给出了RAMDISK设备驱动的block_device_operations结构体定义及open()和ioctl()函数。
     代码清单13.19 RAMDISK设备驱动block_device_operations结构体及成员函数
    1  static struct block_device_operations rd_bd_op =
    2  {
    3    .owner = THIS_MODULE,
    4    .open = rd_open,
    5    .ioctl = rd_ioctl,
    6  };

    8  static int rd_open(struct inode *inode, struct file *filp)
    9  {
    10  unsigned unit = iminor(inode);//获得次设备号
    11
    12  if (rd_bdev[unit] == NULL) {
    13   struct block_device *bdev = inode->i_bdev;//获得block_device结构体指针
    14   struct address_space *mapping; //地址空间
    15   unsigned bsize;
    16   gfp_t gfp_mask;
    17     /* 设置inode成员 */
    18   inode = igrab(bdev->bd_inode);
    19   rd_bdev[unit] = bdev;
    20   bdev->bd_openers++;
    21   bsize = bdev_hardsect_size(bdev);
    22   bdev->bd_block_size = bsize;
    23   inode->i_blkbits = blksize_bits(bsize);
    24   inode->i_size = get_capacity(bdev->bd_disk)<<9;
    25
    26   mapping = inode->i_mapping;   
    27   mapping->a_ops = &ramdisk_aops;
    28   mapping->backing_dev_info = &rd_backing_dev_info;
    29   bdev->bd_inode_backing_dev_info = &rd_file_backing_dev_info;
    30
    31   gfp_mask = mapping_gfp_mask(mapping);
    32   gfp_mask &= ~(__GFP_FS|__GFP_IO);
    33   gfp_mask |= __GFP_HIGH;
    34   mapping_set_gfp_mask(mapping, gfp_mask);
    35  }
    36
    37  return 0;
    38 }
    39
    40 static int rd_ioctl(struct inode *inode, struct file *file,
    41    unsigned int cmd, unsigned long arg)
    42 {
    43  int error;
    44  struct block_device *bdev = inode->i_bdev;
    45
    46  if (cmd != BLKFLSBUF) /* 不是flush buffer cache 命令 */
    47   return -ENOTTY;
    48  /* 刷新buffer cache */
    49  error = -EBUSY;
    50  down(&bdev->bd_sem);
    51  if (bdev->bd_openers <= 2) {
    52   truncate_inode_pages(bdev->bd_inode->i_mapping, 0);
    53   error = 0;
    54  }
    55  up(&bdev->bd_sem);
    56  return error;
    57 }
    13.7.4 RAMDISK I/O请求处理
    鉴于RAMDISK是一种完全随机设备,其驱动中宜使用“制造请求”函数而非请求函数,这个函数的实现如代码清单13.20。
    代码清单13.20 RAMDISK设备驱动“制造请求”函数
    1  static int rd_make_request(request_queue_t *q, struct bio *bio)
    2  {
    3    struct block_device *bdev = bio->bi_bdev;
    4    struct address_space *mapping = bdev->bd_inode->i_mapping;
    5    sector_t sector = bio->bi_sector;
    6    unsigned long len = bio->bi_size >> 9;
    7    int rw = bio_data_dir(bio);//数据传输方向:读/写?
    8    struct bio_vec *bvec;
    9    int ret = 0, i;
    10
    11   if (sector + len > get_capacity(bdev->bd_disk))
    12   //超过容量
    13     goto fail;
    14
    15   if (rw == READA)
    16     rw = READ;
    17   //遍历每个段
    18   bio_for_each_segment(bvec, bio, i)
    19   {
    20     ret |= rd_blkdev_pagecache_IO(rw, bvec, sector, mapping);
    21     sector += bvec->bv_len >> 9;
    22   }
    23   if (ret)
    24     goto fail;
    25
    26   bio_endio(bio, bio->bi_size, 0); //处理结束
    27   return 0;
    28   fail: bio_io_error(bio, bio->bi_size);
    29   return 0;
    30 }
    13.8实例2:IDE硬盘设备驱动
    13.8.1 IDE硬盘设备原理
    IDE(Integrated Drive Electronics)接口,也就是集成驱动器电路接口,原名为ATA(AT Attachment,AT嵌入式)接口,其本意为将硬盘控制器与盘体集成在一起的硬盘驱动器,经历了ATA-1到ATA-7以及SATA-1和SATA-2的发展历史。ATA-1至ATA-4采用40芯排线缆,ATA-5至ATA-7则采用40针80芯线缆,虽然线缆数量增加了,但是逻辑原理没有变,只是通过物理上的改变来达到改善PCB信号完整性的目的,它提供更多的地线并使信号线临近地线,从而减少电流回流的面积。SATA-1和SATA-2与ATA-1至ATA-7相比,数据传输方式由并行转变为串行。
    IDE接口的硬件原理实际上非常简单,对CPU的外围总线进行简单的扩展后就可外接IDE控制器,表13.2给出了40针IDE接口的引脚定义。
    表13.2 IDE接口的引脚定义
    引脚 信号 信号描述 信号方向 引脚 信号 信号描述 信号方向
    1 RSET 复位 I 2 GND 地 I/O
    3 DD7 数据位7 I/O 4 DD8 数据位8 I/O
    5 DD6 数据位6 I/O 6 DD9 数据位9 I/O
    7 DD5 数据位5 I/O 8 DD10 数据位10 I/O
    9 DD4 数据位4 I/O 10 DD11 数据位11 I/O
    11 DD3 数据位3 I/O 12 DD12 数据位12 I/O
    13 DD2 数据位2 I/O 14 DD13 数据位13 I/O
    15 DD1 数据位1 I/O 16 DD14 数据位14 I/O
    17 DD0 数据位0 I/O 18 DD15 数据位15 I/O
    19 GND 地   20 N.C 未用  
    21 DMARQ DMA请求 O 22 GND 地  
    23 DIOW/ 写选通 I 24 GND 地  
    25 DIOR/ 读选通 I 26 GND 地  
    27 IORDY 通道就绪 O 28 DPSYNC:CXEL 同步电缆选择  
    29 DMACK/ DMA应答 O 30 GND 地  
    31 INTRQ/ 中断请求 O 32 IOCS13/ 16为IO O
    33 DA1 地址1 I 34 PDIAG/ 诊断完成 O
    35 DA0 地址0 I 36 DA2 地址2 I
    37 CS1FX/ 片选0 I 38 CS3FX/ 片选1 I
    39 DASP/ 驱动器激活 O 40 GND 地  
    IDE控制器提供了一组寄存器,通过这些寄存器,主机能控制IDE驱动器的行为和查询其状态,表13.3给出了IDE接口寄存器的定义。
    表13.3 IDE接口寄存器定义
    片选1 片选0 地址2 地址1 地址0 读 写 位数
    1 0 0 0 0 数据寄存器 数据寄存器 16
    1 0 0 0 1 错误寄存器 特征寄存器 8
    1 0 0 1 0 扇区数寄存器 扇区数寄存器 8
    1 0 0 1 1 扇区号寄存器 扇区号寄存器 8
    1 0 1 0 0 柱面号寄存器(低8位) 柱面号寄存器(低8位) 8
    1 0 1 0 1 柱面号寄存器(高8位) 柱面号寄存器(高8位) 8
    1 0 1 1 0 驱动器选择/磁头寄存器 驱动器选择/磁头寄存器 8
    1 0 1 1 1 状态寄存器 命令寄存器 8
    0 1 1 1 0 状态寄存器 设备控制器寄存器 8
    IDE硬盘的传输模式有以下3种:
    •  PIO(Programmed I/O)模式:PIO模式是一种通过CPU执行I/O端口指令来进行数据读写的数据交换模式,是最早先的硬盘数据传输模式,数据传输速率低下,CPU占有率也很高。
    •  DMA(Driect Memory Access)模式:DMA模式是一种不经过CPU而直接从内存了存取数据的数据交换模式。PIO模式下硬盘和内存之间的数据传输是由CPU来控制的;而在DMA模式下,CPU只须向DMA控制器下达指令,让DMA控制器来处理数据的传送,数据传送完毕再把信息反馈给CPU,这样就很大程度上减轻了CPU的资源占有率。
    •  Ultra DMA(简称UDMA)模式:它在包含了DMA模式的优点的基础上,又增加了CRC校验技术,提高数据传输过程中的准确性,安全性得到保障。另外,在以往的硬盘数据传输模式下,一个时钟周期只传输一次数据,而在UDMA 模式中逐渐应用了Double Data Rate(双倍数据传输)技术,它在时钟的上升沿和下降沿各自进行一次数据传输,使数据传输速度成倍增长。
    除了可以以CHS(Cylinder、Head 和 Sector)的方式定位硬盘的扇区外,还可以用LBA(逻辑块线性地址)的方式来定位,CHS可以换算为LBA。CHS 设计最多只允许 65536 个柱面、16 个磁头,以及 255 扇区/磁轨。这就将容量限制为 267386880 个扇区,即大约 137 GB。
    假设用c表示当前柱面号,h表示当前磁头号,cs表示起始柱面号,hs表示起始磁头号,ss表示起始扇区号,ps表示每磁道有多少个扇区,ph表示每柱面有多少个磁道(一般情况下,cs=0、hs=0、ss=1、ps=63、ph=255),LBA与CHS有如下对应关系:
    lba=(c-cs)*ph*ps+(h-hs)*ps+(s-ss)
    LBA使得系统忽略硬盘的几何结构,交由驱动器来完成。系统不需要去查询 CHS 值,而只需要查询逻辑块地址(Logical Block Address,LBA),驱动器电子装置会找出要读或写的实际扇区。而LBA48(48位逻辑块地址)则可以使系统支持超过137GB的硬盘。
    Linux内核中,与IDE驱动相关的文件被放置在/drivers/ide目录下,这个目录包含ide.c、ide-cd.c、ide-cd.h、ide-disk.c、ide-dma.c、ide-floppy.c、ide-generic.c、ide-io.c、ide-iops.c、ide-lib.c、ide-pnp.c、ide-probe.c、ide-proc.c、ide-tape.c、ide-taskfile.c、ide-timing.h文件以及针对ARM、PPC、MIPS等外围IDE设备驱动的目录。整个IDE设备驱动的体系结构及其复杂,但大多数都不需要关心,驱动工程师要使Linux支持某嵌入式系统中的IDE硬盘,所需编写的代码量是非常少的。13.8.2~13.8.3纯粹出于学习目的,对IDE硬盘驱动的block_device_operations及IO请求处理过程进行分析以进一步加深读者对Linux块设备I/O操作方法的印象,13.8.4则从工程角度出发,讲解如何使Linux支持新系统中的IDE硬盘。
    13.8.2 IDE硬盘设备驱动block_device_operations及成员函数
     IDE硬盘驱动的block_device_operations中包含了打开、释放、IO控制、获得几何信息、媒介改变和使介质有效的成员函数,这些函数的实现较简单,如代码清单13.21。
     代码清单13.21 IDE硬盘驱动block_device_operations结构体及其成员函数
    1  static struct block_device_operations idedisk_ops =
    2  {
    3   .owner  = THIS_MODULE,
    4   .open  = idedisk_open, 
    5   .release = idedisk_release,
    6   .ioctl  = idedisk_ioctl,
    7   .getgeo  = idedisk_getgeo, //得到几何信息
    8   .media_changed = idedisk_media_changed,  //媒介改变
    9   .revalidate_disk= idedisk_revalidate_disk //使介质有效
    10 };
    11
    12 static int idedisk_ioctl(struct inode *inode, struct file *file,
    13    unsigned int cmd, unsigned long arg)
    14 {
    15  struct block_device *bdev = inode->i_bdev;
    16  struct ide_disk_obj *idkp = ide_disk_g(bdev->bd_disk);
    17  return generic_ide_ioctl(idkp->drive, file, bdev, cmd, arg);//通用IDE的IO控制
    18 }
    19
    20
    21 static int idedisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
    22 {
    23  struct ide_disk_obj *idkp = ide_disk_g(bdev->bd_disk);
    24  ide_drive_t *drive = idkp->drive;
    25  /* 得到几何信息,CHS */
    26  geo->heads = drive->bios_head;
    27  geo->sectors = drive->bios_sect;
    28  geo->cylinders = (u16)drive->bios_cyl; /* truncate */
    29  return 0;
    30 }
    31
    32 static int idedisk_open(struct inode *inode, struct file *filp)
    33 {
    34  struct gendisk *disk = inode->i_bdev->bd_disk;
    35  struct ide_disk_obj *idkp;
    36  ide_drive_t *drive;
    37
    38  if (!(idkp = ide_disk_get(disk)))
    39   return -ENXIO;
    40
    41  drive = idkp->drive;
    42
    43  drive->usage++;  //使用计数加1
    44  if (drive->removable && drive->usage == 1) {
    45   ide_task_t args;
    46   memset(&args, 0, sizeof(ide_task_t));
    47   args.tfRegister[IDE_COMMAND_OFFSET] = WIN_DOORLOCK;
    48   args.command_type = IDE_DRIVE_TASK_NO_DATA;
    49   args.handler   = &task_no_data_intr;
    50   check_disk_change(inode->i_bdev);
    51
    52   if (drive->doorlocking && ide_raw_taskfile(drive, &args, NULL))
    53    drive->doorlocking = 0;
    54  }
    55  return 0;
    56 }
    57
    58 static int idedisk_release(struct inode *inode, struct file *filp)
    59 {
    60  struct gendisk *disk = inode->i_bdev->bd_disk;
    61  struct ide_disk_obj *idkp = ide_disk_g(disk);
    62  ide_drive_t *drive = idkp->drive;
    63
    64  if (drive->usage == 1)
    65   ide_cacheflush_p(drive);
    66  if (drive->removable && drive->usage == 1) {
    67   ide_task_t args;
    68   memset(&args, 0, sizeof(ide_task_t));
    69   args.tfRegister[IDE_COMMAND_OFFSET] = WIN_DOORUNLOCK;
    70   args.command_type = IDE_DRIVE_TASK_NO_DATA;
    71   args.handler   = &task_no_data_intr;
    72   if (drive->doorlocking && ide_raw_taskfile(drive, &args, NULL))
    73    drive->doorlocking = 0;
    74  }
    75  drive->usage--;   //使用计数减1
    76
    77  ide_disk_put(idkp);
    78  return 0;
    79 }
    13.8.3 IDE硬盘设备驱动I/O请求处理
    Linux对IDE驱动进行了再封装,定义了ide_driver_t结构体,这个结构体容纳了IDE硬盘的探测、移除、请求处理和结束请求处理等函数指针。结束请求处理函数ide_end_request()是对end_request()函数针对IDE的修改。代码清单13.21给出了ide_driver_t结构体的定义。
     代码清单13.22 ide_driver_t结构体
    1  static ide_driver_t idedisk_driver = {
    2   .gen_driver = {
    3    .owner  = THIS_MODULE,
    4    .name  = "ide-disk",
    5    .bus  = &ide_bus_type,
    6   },
    7   .probe   = ide_disk_probe,  //探测
    8   .remove   = ide_disk_remove,  //移除
    9   .shutdown  = ide_device_shutdown, //关闭
    10  .version  = IDEDISK_VERSION,
    11  .media   = ide_disk,  //媒介类型
    12  .supports_dsc_overlap = 0,
    13  .do_request  = ide_do_rw_disk,  //请求处理函数
    14  .end_request  = ide_end_request,  //请求处理结束
    15  .error   = __ide_error,
    16  .abort   = __ide_abort,
    17  .proc   = idedisk_proc,
    18 };
    代码清单13.22第13行的ide_do_rw_disk()函数完成硬盘I/O操作请求的处理,如代码清单13.23所示。
    代码清单13.23 IDE硬盘驱动I/O请求处理
    1   static ide_startstop_t ide_do_rw_disk(ide_drive_t *drive, struct request *rq,
    2     sector_t block)
    3   {
    4     ide_hwif_t *hwif = HWIF(drive);
    5  
    6     BUG_ON(drive->blocked);
    7  
    8     if (!blk_fs_request(rq)) //不是文件系统请求
    9     {
    10      blk_dump_rq_flags(rq, "ide_do_rw_disk - bad command");
    11      ide_end_request(drive, 0, 0);  //以失败结束该请求
    12      return ide_stopped;
    13    }
    14 
    15    pr_debug("%s: %sing: block=%llu, sectors=%lu, buffer=0x%08lx/n",
    16      drive->name,rq_data_dir(rq) == READ ? "read" : "writ", (unsigned long
    17      long)block, rq->nr_sectors, (unsigned long)rq->buffer);
    18 
    19    if (hwif->rw_disk)
    20      hwif->rw_disk(drive, rq);
    21 
    22    return __ide_do_rw_disk(drive, rq, block);  //具体的请求处理
    23  }
    24 
    25  static ide_startstop_t __ide_do_rw_disk(ide_drive_t *drive, struct request *rq,
    26    sector_t block)
    27  {
    28    ide_hwif_t *hwif = HWIF(drive);
    29    unsigned int dma = drive->using_dma;
    30    u8 lba48 = (drive->addressing == 1) ? 1 : 0;
    31    task_ioreg_t command = WIN_NOP;
    32    ata_nsector_t nsectors;
    33 
    34    nsectors.all = (u16)rq->nr_sectors; //要传送的扇区数
    35 
    36    if (hwif->no_lba48_dma && lba48 && dma)
    37    {
    38      if (block + rq->nr_sectors > 1ULL << 28)
    39        dma = 0;
    40      else
    41        lba48 = 0;
    42    }
    43 
    44    if (!dma)
    45    {
    46      ide_init_sg_cmd(drive, rq);
    47      ide_map_sg(drive, rq);
    48    }
    49 
    50    if (IDE_CONTROL_REG)
    51      hwif->OUTB(drive->ctl, IDE_CONTROL_REG);
    52 
    53    if (drive->select.b.lba)
    54    {
    55      if (lba48) //48位LBA
    56      {
    57        ...
    58      }
    59      else
    60      {
    61        //LBA方式,写入要读写的位置信息到IDE寄存器
    62        hwif->OUTB(0x00, IDE_FEATURE_REG);
    63        hwif->OUTB(nsectors.b.low, IDE_NSECTOR_REG);
    64        hwif->OUTB(block, IDE_SECTOR_REG);
    65        hwif->OUTB(block >>= 8, IDE_LCYL_REG);
    66        hwif->OUTB(block >>= 8, IDE_HCYL_REG);
    67        hwif->OUTB(((block >> 8) &0x0f) | drive->select.all,IDE_SELECT_REG);
    68      }
    69    }
    70    else
    71    {
    72      unsigned int sect, head, cyl, track;
    73      track = (int)block / drive->sect;
    74      sect = (int)block % drive->sect + 1;
    75      hwif->OUTB(sect, IDE_SECTOR_REG);
    76      head = track % drive->head;
    77      cyl = track / drive->head;
    78 
    79      pr_debug("%s: CHS=%u/%u/%u/n", drive->name, cyl, head, sect);
    80      //CHS方式,写入要读写的位置信息到IDE寄存器
    81      hwif->OUTB(0x00, IDE_FEATURE_REG);
    82      hwif->OUTB(nsectors.b.low, IDE_NSECTOR_REG);
    83      hwif->OUTB(cyl, IDE_LCYL_REG);
    84      hwif->OUTB(cyl >> 8, IDE_HCYL_REG);
    85      hwif->OUTB(head | drive->select.all, IDE_SELECT_REG);
    86    }
    87 
    88    if (dma)  //DMA方式
    89    {
    90      if (!hwif->dma_setup(drive))   //设置DMA成功
    91      {
    92        if (rq_data_dir(rq))
    93        {
    94          command = lba48 ? WIN_WRITEDMA_EXT : WIN_WRITEDMA;
    95          if (drive->vdma)
    96            command = lba48 ? WIN_WRITE_EXT : WIN_WRITE;
    97        }
    98        else
    99        {
    100         command = lba48 ? WIN_READDMA_EXT : WIN_READDMA;
    101         if (drive->vdma)
    102           command = lba48 ? WIN_READ_EXT : WIN_READ;
    103       }
    104       hwif->dma_exec_cmd(drive, command);
    105       hwif->dma_start(drive);
    106       return ide_started;
    107     }
    108     /* 回到PIO模式 */
    109     ide_init_sg_cmd(drive, rq);
    110   }
    111
    112   if (rq_data_dir(rq) == READ) //数据传输方向是读
    113   {
    114     if (drive->mult_count)
    115     {
    116       hwif->data_phase = TASKFILE_MULTI_IN;
    117       command = lba48 ? WIN_MULTREAD_EXT : WIN_MULTREAD;
    118     }
    119     else
    120     {
    121       hwif->data_phase = TASKFILE_IN;
    122       command = lba48 ? WIN_READ_EXT : WIN_READ;
    123     }
    124     //执行读命令
    125     ide_execute_command(drive, command, &task_in_intr, WAIT_CMD, NULL);
    126     return ide_started;
    127   }
    128   else  //数据传输方向是写
    129   {
    130     if (drive->mult_count)
    131     {
    132       hwif->data_phase = TASKFILE_MULTI_OUT;
    133       command = lba48 ? WIN_MULTWRITE_EXT : WIN_MULTWRITE;
    134     }
    135     else
    136     {
    137       hwif->data_phase = TASKFILE_OUT;
    138       command = lba48 ? WIN_WRITE_EXT : WIN_WRITE;
    139     }
    140
    141     //写IDE命令寄存器写入写命令
    142     hwif->OUTB(command, IDE_COMMAND_REG);
    143
    144     return pre_task_out_intr(drive, rq);
    145   }
    146 }
    从代码清单13.23可知,真正开始执行I/O操作的是其22行引用的__ide_do_rw_disk()函数。这个函数会根据不同的操作模式,将要读写的LBA或CHS信息写入IDE寄存器内,并给其命令寄存器写入读、写命令。
    为了进行硬盘读写操作,第61~67和80~85行将参数写入地址寄存器和特性寄存器,如果是读,第125行调用的ide_execute_command()会将读命令写入命令寄存器;如果是写,第142行将写命令写入IDE命令寄存器IDE_COMMAND_REG。
    真正调用ide_driver_t结构体中do_request()成员函数即ide_do_rw_disk()的是ide-io.c文件中的start_request()函数,这个函数会过滤掉一些请求,最终将读写I/O操作请求传递给ide_do_rw_disk()函数,如代码清单13.24。
    代码清单13.24 开始执行1个IDE请求的start_request()函数
    1  static ide_startstop_t start_request(ide_drive_t *drive, struct request *rq)
    2  {
    3    ide_startstop_t startstop;
    4    sector_t block;

    6    BUG_ON(!(rq->flags &REQ_STARTED));

    8    /* 超过了最大失败次数 */
    9    if (drive->max_failures && (drive->failures > drive->max_failures))
    10   {
    11     goto kill_rq;
    12   }
    13
    14   block = rq->sector; //要传输的下1个扇区
    15   if (blk_fs_request(rq) && (drive->media == ide_disk || drive->media ==
    16     ide_floppy)) //是文件系统请求,是IDE盘
    17   {
    18     block += drive->sect0;
    19   }
    20   /* 如果将0扇区重映射到1扇区 */
    21   if (block == 0 && drive->remap_0_to_1 == 1)
    22     block = 1;
    23
    24   if (blk_pm_suspend_request(rq) && rq->pm->pm_step ==
    25     ide_pm_state_start_suspend)
    26     drive->blocked = 1;
    27   else if (blk_pm_resume_request(rq) && rq->pm->pm_step ==
    28     ide_pm_state_start_resume)
    29   {
    30     /* 醒来后的第1件事就是等待BSY位不忙 */
    31     int rc;
    32
    33     rc = ide_wait_not_busy(HWIF(drive), 35000);
    34     if (rc)
    35       printk(KERN_WARNING "%s: bus not ready on wakeup/n", drive->name);
    36     SELECT_DRIVE(drive);
    37     HWIF(drive)->OUTB(8, HWIF(drive)->io_ports[IDE_CONTROL_OFFSET]);
    38     rc = ide_wait_not_busy(HWIF(drive), 10000);
    39     if (rc)
    40       printk(KERN_WARNING "%s: drive not ready on wakeup/n", drive->name);
    41   }
    42
    43   SELECT_DRIVE(drive);
    44   if (ide_wait_stat(&startstop, drive, drive->ready_stat, BUSY_STAT | DRQ_STAT,
    45     WAIT_READY))  //等待驱动器READY
    46   {
    47     printk(KERN_ERR "%s: drive not ready for command/n", drive->name);
    48     return startstop;
    49   }
    50   if (!drive->special.all)
    51   {
    52     ide_driver_t *drv;
    53
    54     //其它非读写请求
    55     if (rq->flags &(REQ_DRIVE_CMD | REQ_DRIVE_TASK))
    56       return execute_drive_cmd(drive, rq);
    57     else if (rq->flags &REQ_DRIVE_TASKFILE)
    58       return execute_drive_cmd(drive, rq);
    59     else if (blk_pm_request(rq))
    60     {
    61       startstop = ide_start_power_step(drive, rq);
    62       if (startstop == ide_stopped && rq->pm->pm_step == ide_pm_state_completed)
    63         ide_complete_pm_request(drive, rq);
    64       return startstop;
    65     }
    66
    67     drv = *(ide_driver_t **)rq->rq_disk->private_data;
    68     return drv->do_request(drive, rq, block); //处理IO操作请求
    69   }
    70   return do_special(drive);
    71   kill_rq: ide_kill_rq(drive, rq);
    72   return ide_stopped;
    73 }
    IDE硬盘驱动对I/O请求结束处理进行了针对IDE的整理,并填充在ide_driver_t结构体的end_request成员中,对应的函数为ide_end_request(),如代码清单13.25。
    代码清单13.25 IDE I/O请求结束处理
    1  /* ide_end_request:完成了一个IDE请求 
    2    参数: nr_sectors 被完成的扇区数量
    3  */
    4  int ide_end_request (ide_drive_t *drive, int uptodate, int nr_sectors)
    5  {
    6   struct request *rq;
    7   unsigned long flags;
    8   int ret = 1;

    10  spin_lock_irqsave(&ide_lock, flags);  //获得自旋锁
    11  rq = HWGROUP(drive)->rq;
    12
    13  if (!nr_sectors)
    14   nr_sectors = rq->hard_cur_sectors;
    15
    16  ret = __ide_end_request(drive, rq, uptodate, nr_sectors); //具体的结束请求处理
    17
    18  spin_unlock_irqrestore(&ide_lock, flags);  //释放获得自旋锁
    19  return ret;
    20 }
    21
    22 static int __ide_end_request(ide_drive_t *drive, struct request *rq, int
    23   uptodate, int nr_sectors)
    24 {
    25   int ret = 1;
    26
    27   BUG_ON(!(rq->flags &REQ_STARTED));
    28
    29   /*  如果对请求设置了failfast,立即完成整个请求 */
    30   if (blk_noretry_request(rq) && end_io_error(uptodate))
    31     nr_sectors = rq->hard_nr_sectors;
    32
    33   if (!blk_fs_request(rq) && end_io_error(uptodate) && !rq->errors)
    34     rq->errors =  - EIO;
    35
    36   /* 决定是否再使能DMA,如果DMA超过3次,采用PIO模式 */
    37   if (drive->state == DMA_PIO_RETRY && drive->retry_pio <= 3)
    38   {
    39     drive->state = 0;
    40     HWGROUP(drive)->hwif->ide_dma_on(drive);
    41   }
    42
    43   //结束请求
    44   if (!end_that_request_first(rq, uptodate, nr_sectors))
    45   {
    46     add_disk_randomness(rq->rq_disk);   //给系统的随机数池贡献熵
    47     blkdev_dequeue_request(rq);
    48     HWGROUP(drive)->rq = NULL;
    49     end_that_request_last(rq, uptodate);
    50     ret = 0;
    51   }
    52
    53   return ret;
    54 }
    13.8.4在内核中增加对新系统IDE设备的支持
    尽管IDE的驱动非常复杂,但是由于其访问方式符合ATA标准,因而内核提供的I/O操作方面的代码是通用的,为了使内核能找到新系统中的IDE硬盘,工程师只需编写少量的针对特定硬件平台的底层代码。
    使用ide_register_hw()函数可注册IDE硬件接口,其原型为:
    int ide_register_hw(hw_regs_t *hw, ide_hwif_t **hwifp);
    这个函数接收的2个参数对应的数据结构为hw_regs_s和ide_hwif_t,其定义如代码清单13.26。
    代码清单13.26 hw_regs_s和ide_hwif_t结构体的定义
    1  typedef struct hw_regs_s
    2  {
    3    unsigned long io_ports[IDE_NR_PORTS]; /* task file寄存器 */
    4    int irq; /* 中断号 */
    5    int dma; /* DMA入口 */
    6    ide_ack_intr_t *ack_intr; /* 确认中断 */
    7    hwif_chipset_t chipset;
    8    struct device *dev;
    9  } hw_regs_t;
    10
    11 typedef struct hwif_s
    12 {
    13   ...
    14
    15   char name[6]; /* 接口名,如"ide0" */
    16
    17   hw_regs_t hw; /* 硬件信息 */
    18   ide_drive_t drives[MAX_DRIVES]; /* 驱动器信息 */
    19
    20   u8 major; /* 主设备号 */
    21   u8 index; /* 索引,如0对应ide0,1对应ide1 */
    22   ...
    23   //DMA操作
    24   int(*dma_setup)(ide_drive_t*);
    25   void(*dma_exec_cmd)(ide_drive_t *, u8);
    26   void(*dma_start)(ide_drive_t*);
    27   int(*ide_dma_end)(ide_drive_t *drive);
    28   int(*ide_dma_check)(ide_drive_t *drive);
    29   int(*ide_dma_on)(ide_drive_t *drive);
    30   int(*ide_dma_off_quietly)(ide_drive_t *drive);
    31   int(*ide_dma_test_irq)(ide_drive_t *drive);
    32   int(*ide_dma_host_on)(ide_drive_t *drive);
    33   int(*ide_dma_host_off)(ide_drive_t *drive);
    34   int(*ide_dma_lostirq)(ide_drive_t *drive);
    35   int(*ide_dma_timeout)(ide_drive_t *drive);
    36   //寄存器访问
    37   void(*OUTB)(u8 addr, unsigned long port);
    38   void(*OUTBSYNC)(ide_drive_t *drive, u8 addr, unsigned long port);
    39   void(*OUTW)(u16 addr, unsigned long port);
    40   void(*OUTL)(u32 addr, unsigned long port);
    41   void(*OUTSW)(unsigned long port, void *addr, u32 count);
    42   void(*OUTSL)(unsigned long port, void *addr, u32 count);
    43
    44   u8(*INB)(unsigned long port);
    45   u16(*INW)(unsigned long port);
    46   u32(*INL)(unsigned long port);
    47   void(*INSW)(unsigned long port, void *addr, u32 count);
    48   void(*INSL)(unsigned long port, void *addr, u32 count);
    49
    50   ...
    51 } ____cacheline_internodealigned_in_smp ide_hwif_t;
    hw_regs_s结构体描述了IDE接口的寄存器、用到的中断号和DMA入口,是对寄存器和硬件资源的描述,而ide_hwif_t是对IDE接口硬件访问方法的描述。
    因此,为使得新系统支持IDE并被内核侦测到,工程师只需要初始化hw_regs_s和ide_hwif_t这2个结构体并使用ide_register_hw()注册IDE接口即可。代码清单13.27给出了H8/300系列单片机IDE驱动的适配器注册代码。
    代码清单13.27 H8/300系列单片机IDE接口注册
    1   /* 寄存器操作函数 */
    2   static void mm_outw(u16 d, unsigned long a)
    3   {
    4    __asm__("mov.b %w0,r2h/n/t"
    5     "mov.b %x0,r2l/n/t"
    6     "mov.w r2,@%1"
    7     :
    8     :"r"(d),"r"(a)
    9     :"er2");
    10  }
    11 
    12  static u16 mm_inw(unsigned long a)
    13  {
    14   register u16 r __asm__("er0");
    15   __asm__("mov.w @%1,r2/n/t"
    16    "mov.b r2l,%x0/n/t"
    17    "mov.b r2h,%w0"
    18    :"=r"(r)
    19    :"r"(a)
    20    :"er2");
    21   return r;
    22  }
    23 
    24  static void mm_outsw(unsigned long addr, void *buf, u32 len)
    25  {
    26   unsigned short *bp = (unsigned short *)buf;
    27   for (; len > 0; len--, bp++)
    28    *(volatile u16 *)addr = bswap(*bp);
    29  }
    30 
    31  static void mm_insw(unsigned long addr, void *buf, u32 len)
    32  {
    33   unsigned short *bp = (unsigned short *)buf;
    34   for (; len > 0; len--, bp++)
    35    *bp = bswap(*(volatile u16 *)addr);
    36  }
    37 
    38  #define H8300_IDE_GAP (2)
    39 
    40  /* hw_regs_t结构体初始化 */
    41  static inline void hw_setup(hw_regs_t *hw)
    42  {
    43   int i;
    44 
    45   memset(hw, 0, sizeof(hw_regs_t));
    46   for (i = 0; i <= IDE_STATUS_OFFSET; i++)
    47    hw->io_ports[i] = CONFIG_H8300_IDE_BASE + H8300_IDE_GAP*i;
    48   hw->io_ports[IDE_CONTROL_OFFSET] = CONFIG_H8300_IDE_ALT;
    49   hw->irq = EXT_IRQ0 + CONFIG_H8300_IDE_IRQ;
    50   hw->dma = NO_DMA;
    51   hw->chipset = ide_generic;
    52  }
    53 
    54  /* ide_hwif_t结构体初始化 */
    55  static inline void hwif_setup(ide_hwif_t *hwif)
    56  {
    57   default_hwif_iops(hwif);
    58 
    59   hwif->mmio  = 2;
    60   hwif->OUTW  = mm_outw;
    61   hwif->OUTSW = mm_outsw;
    62   hwif->INW   = mm_inw;
    63   hwif->INSW  = mm_insw;
    64   hwif->OUTL  = NULL;
    65   hwif->INL   = NULL;
    66   hwif->OUTSL = NULL;
    67   hwif->INSL  = NULL;
    68  }
    69 
    70  /* 注册IDE适配器 */
    71  void __init h8300_ide_init(void)
    72  {
    73   hw_regs_t hw;
    74   ide_hwif_t *hwif;
    75   int idx;
    76 
    77    /* 申请内存区域 */
    78   if (!request_region(CONFIG_H8300_IDE_BASE, H8300_IDE_GAP*8, "ide-h8300"))
    79    goto out_busy;
    80   if (!request_region(CONFIG_H8300_IDE_ALT, H8300_IDE_GAP, "ide-h8300")) {
    81    release_region(CONFIG_H8300_IDE_BASE, H8300_IDE_GAP*8);
    82    goto out_busy;
    83   }
    84 
    85   hw_setup(&hw); //初始化hw_regs_t
    86 
    87   /* 注册IDE接口 */
    88   idx = ide_register_hw(&hw, &hwif);
    89   if (idx == -1) {
    90    printk(KERN_ERR "ide-h8300: IDE I/F register failed/n");
    91    return;
    92   }
    93 
    94   hwif_setup(hwif);   //设置ide_hwif_t
    95   printk(KERN_INFO "ide%d: H8/300 generic IDE interface/n", idx);
    96   return;
    97 
    98  out_busy:
    99   printk(KERN_ERR "ide-h8300: IDE I/F resource already used./n");
    100 }
    第1~36行定义了寄存器读写函数,41行的hw_setup()函数用于初始化hw_regs_t结构体,55行的hwif_setup()函数用于初始化ide_hwif_t结构体,71行的h8300_ide_init()函数中使用ide_register_hw()注册了这个接口。
    13.9总结
    块设备的I/O操作方式与字符设备存在较大的不同,因而引入了request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿于始终的就是“请求”,字符设备的I/O操作则是直接进行不绕弯,块设备的I/O操作会排队和整合。
    驱动的任务是处理请求,对请求的排队和整合由I/O调度算法解决,因此,块设备驱动的核心就是请求处理函数或“制造请求”函数。
    尽管在块设备驱动中仍然存在block_device_operations结构体及其成员函数,但其不再包含读写一类的成员函数,而只是包含打开、释放及I/O控制等与具体读写无关的函数。
    块设备驱动的结构相当复杂的,但幸运的是,块设备不像字符设备那么包罗万象,它通常就是存储设备,而且驱动的主体已经由Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及到的工作往往只是编写少量的与硬件直接交互的代码。
     
    13.1 调整块设备I/O操作的顺序

    图13.1 调整块设备I/O操作的顺序

    13.2 request队列、requestbiobio_vec结构体之间的关系

    (a) requestbio


    (a) request与bio
     

    (b) requestbiobio_vec

     


    (b) request、bio和bio_vec
    (c)biobio_vec

    (c)bio与bio_vec
    13.3 遍历1个请求队列

    图13.3 遍历1个请求队列
    13.4 块设备驱动模板与RAMDISK设备驱动的映射

    图13.4 块设备驱动模板与RAMDISK设备驱动的映射
    展开全文
  • Linux块设备驱动总结

    千次阅读 2016-03-26 08:36:06
    Linux块设备驱动程序接口使得块设备可以发挥其最大的功效,但是其复杂程序又是编程者必须面对的一 个问题 一个数据块指的是固定大小的数据,而大小的值由内核确定 数据块的大小通常是4096个字节,

    《Linux设备驱动程序》第十六章 块设备驱动程序读书笔记


    简介
    一个块设备驱动程序主要通过传输固定大小的随机数据来访问设备
    Linux内核视块设备为与字符设备相异的基本设备类型
    Linux块设备驱动程序接口使得块设备可以发挥其最大的功效,但是其复杂程序又是编程者必须面对的一


    个问题
    一个数据块指的是固定大小的数据,而大小的值由内核确定
    数据块的大小通常是4096个字节,但是可以根据体系结构和所使用的文件系统进行改变
    与数据块对应的是扇区,它是由底层硬件决定大小的一个块,内核所处理的设备扇区大小是512字节
    如果要使用不同的硬件扇区大小,用户必须对内核的扇区数做相应的修改
    注册
    注册块设备驱动程序
    <linux/fs.h>
    int register_blkdev(unsigned int major, const char *name);
    如果需要的话分配一个动态的主设备号
    在/proc/devices中创建一个入口项
    int unregister_blkdev(unsigned int major, const char *name);
    注册磁盘
    struct block_device_operations
    int (*open) (struct inode *inode, struct file *filp);
    int (*release) (struct inode *inode, struct file *filp);
    int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
    int (*media_changed) (struct gendisk *gd);
    int (*revalidate_disk) (struct gendisk *gd);
    struct module *owner;
    gendisk结构
    <linux/genhd.h>
    struct gendisk
    int major;
    int first_minor;
    int minors;
    常取16
    char disk_name[32]
    显示在/proc/partitions和sysfs中
    struct block_device_operations *fops;
    struct request_queue *queue;
    int flags;
    sector_t capacity;
    void *private_data;
    struct gendisk *alloc_disk(int minors);
    void del_gendisk(struct gendisk *gd);
    void add_disk(struct gendisk *gd);
    块设备操作
    open和release函数
    对于那些操作实际硬件设备的驱动程序,open和release函数可以设置驱动程序和硬件的状态。这些操作


    包括使磁盘开始或者停止旋转,锁住可移动介质的仓门以及分配DMA缓存等
    有一些操作能够让块设备在用户空间内被直接打开,这些操作包括给磁盘分区,或者在分区上创建文件


    系统,或者运行文件系统检查程序
    对可移动介质的支持
    调用media_changed函数以检查介质是否被改变
    在介质改变后将调用revalideate函数
    ioctl函数
    高层的块设备子系统在驱动程序获得ioctl命令前,已经截取了大量的命令
    实际上在一个现代驱动程序中,许多ioctl命令根本就不用实现
    请求处理
    每个块设备驱动程序的核心是它的请求函数
    request函数介绍
    void request(request_queue_t *queue);
    当内核需要驱动程序处理读取、写入以及其他对设备的操作时,就会调用该函数
    每个设备都有一个请求队列
    dev->queue = blk_init_queue(test_request, &dev->lock);
    对request函数的调用是与用户空间进程中的动作完全异步的
    一个简单的request函数
    struct request * elv_next_request(request_queue_t queue);
    void end_request(struct request *req, int succeeded);
    struct request
    sector_t secotr;
    unsigned long nr_sectors;
    char *buffer
    rq_data_dir(struct request *req);
    请求队列
    一个块设备请求队列可以这样描述:包含块设备I/O请求的序列
    请求队列跟踪未完成的块设备的I/O请求
    请求队列还实现了插件接口
    I/O调度器还负责合并邻近的请求
    请求队列拥有request_queue或request_queue_t结构类型
    <linux/blkdev.h>
    队列的创建与删除
    request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
    void blk_cleanup_queue(request_queue_t *queue);
    队列函数
    struct request *elv_next_request(request_queue_t *queue);
    void blkdev_dequeue_request(struct request *req);
    void elv_requeue_request(request_queue_t *queue, struct request *req);
    队列控制函数
    void blk_stop_queue(request_queue_t *queue);
    void blk_start_queue(request_queue_t *queue);
    void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
    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 short max);
    void blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
    void blk_queue_dma_alignment(request_queue_t *queue, int mask);
    void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);
    请求过程剖析
    从本质上讲,一个request结构是作为一个bio结构的链表实现的
    bio结构
    bio结构包含了驱动程序执行请求的全部信息,而不必与初始化这个请求的用户空间的进程相关联
    <linux/bio.h>
    struct bio
    sector_t bi_sector;
    unsigned int bi_size;
    以字节为单位所需要传输的数据大小
    unsigned long bi_flags;
    unsigned short bio_phys_segments;
    unsigned short bio_hw_segments;
    struct bio_vec *bi_io_vec
    struct bio_vec
    struct page *vb_page;
    unsigned int bv_len;
    unsigned int bv_offset;
    example
    int segno;
    struct bio_vec *bvec;
    bio_for_each_segment(bvec, bio, segno)
    {
    /* 使用该段进行一定的操作 */
    }
    char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
    void __bio_kunmap_atomic(char *buffer, enum km_type type):
    struct page *bio_page(struct bio *bio);
    int bio_offset(struct bio *bio);
    int bio_cur_sectors(struct bio *bio);
    char *bio_data(struct bio *bio);
    char *bio_kmap_irq(struct bio *bio, unsigned long *flags);
    void bio_kunmap_irq(char *buffer, unsigned long *flags);
    request结构成员
    struct request
    sector_t hard_sector;
    unsigned long hard_nr_sectors;
    unsigned int hard_cur_sectors;
    struct bio *bio;
    char *buffer;
    unsigned short nr_phys_segments;
    struct list_head queuelist;
    屏障请求
    在驱动程序接收到请求前,块设备层重新组合了请求以提高I/O性能
    出于同样的目的,驱动程序也可以重新组合请求
    但在无限制重新组合请求时面临了一个问题:一些应用程序的某些操作,要在另外一些操作开始前完成
    2.6版本的块设备层使用屏障(barrier)请求来解决这个问题
    如果一个请求被设置了REQ_HARDBARRER标志,那么在其他后续请求被初始化前,它必须被写入驱动器
    void blk_queue_ordered(request_queue_t *queue, int flag);
    int blk_barrier_rq(sruct request *req);
    如果返回一个非零值,该请求是一个屏障请求
    不可重试请求
    int blk_noretry_request(struct request *req);
    请求完成函数
    int end_that_request_first(struct request *req, int success, int count);
    void end_that_request_last(struct request *req);
    example
    void end_request(struct request *req, int uptodate)
    {
    if (!end_that_request(req, uptodate, req->hard_cur_sectors)
    {
    add_disk_randomness(req->rq_disk);
    blkdev_dequeue_request(req);
    end_that_request_last(req);
    }
    }
    使用bio
    example
    struct request *req
    struct bio *bio;
    rq_for_each_bio(bio, req)
    {
    /* 使用该bio结构进行一定的操作 */
    }
    块设备请求和DMA
    int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);
    clear_bit(QUEUE_FLAG_CLEAR, &queue->queue_flags);
    不使用请求队列
    typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
    void bio_endio(struct bio *bio, unsigned int bytes, int error);
    request_queue_t *blk_alloc_queue(int flags);
    并未真正地建立一个保存请求的队列
    void blk_queue_make_request(request_queue_t *queue, make_request_fn *func);
    drivers/block/ll_rw_block.c
    其他一些细节
    命令预处理
    typedef int (prep_rq_fn) (request_queue_t *queue, struct request *req);
    该函数要能返回下面的值之一
    BLKPREP_OK
    BLKPREP_KILL
    BLKPREP_DEFER
    void blk_queue_prep_rq(request_queue_t *queue, prep_rq_fn *func);
    标记命令队列
    同时拥有多个活动请求的硬件通常支持某种形式的标记命令队列(Tagged Command Queueing, TCQ)
    TCQ只是为每个请求添加一个整数(标记)的技术,这样当驱动器完成它们中的一个请求后,它就可以告


    诉驱动程序完成的是哪个
    int blk_queue_int_tags(request_queue_t *queue, int depth, struct blk_queue_tag *tags);
    int blk_queue_resize_tags(request_queue_t *queue, int new_depth);
    int blk_queue_start_tag(request_queue_t *queue, struct request *req);
    void blk_queue_end_tag(request_queue_t *queue, struct request *req);
    struct request *blk_queue_find_tag(request_queue_t *queue, int tag);
    void blk_queue_invalidate_tags(request_queue_t *queue);
    ========

    Linux设备驱动--块设备(一)之概念和框架



    基本概念 
     块设备(blockdevice)
    --- 是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时


    的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
    字符设备(Character device)
    ---是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据


    流。他不具备缓冲区,所以对这种设备的读写是实时的。
     
    扇区(Sectors):任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512byte。(对设备而


    言)
    块  (Blocks):由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组


    成。(对Linux操作系统而言)
    段(Segments):由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。
    页、段、块、扇区之间的关系图如下:


     
    块设备驱动整体框架


     块设备的应用在Linux中是一个完整的子系统。
    在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用request结


    构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求


    添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内


    核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责


    提交  I/O 请求,  调度程序将磁盘资源分配给系统中所有挂起的块 I/O  请求,其工作是管理块设备


    的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。
    由通用块层(Generic Block Layer)负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。


    在通用块层中,通常用一个bio结构体来对应一个I/O请求。
    Linux提供了一个gendisk数据结构体,用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行


    访问。在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,是


    block_device_operations结构体。
    当多个请求提交给块设备时,执行效率依赖于请求的顺序。如果所有的请求是同一个方向(如:写数据


    ),执行效率是最大的。内核在调用块设备驱动程序例程处理请求之前,先收集I/O请求并将请求排序,


    然后,将连续扇区操作的多个请求进行合并以提高执行效率(内核算法会自己做,不用你管),对I/O请


    求排序的算法称为电梯算法(elevator algorithm)。电梯算法在I/O调度层完成。内核提供了不同类型


    的电梯算法,电梯算法有
    1 noop(实现简单的FIFO,基本的直接合并与排序),
    2 anticipatory(延迟I/O请求,进行临界区的优化排序),
    3 Deadline(针对anticipatory缺点进行改善,降低延迟时间),
    4 Cfq(均匀分配I/O带宽,公平机制)
    PS:其实IO调度层(包括请求合并排序算法)是不需要用户管的,内核已经做好
    相关数据结构
    block_device:      描述一个分区或整个磁盘对内核的一个块设备实例 
    gendisk:               描述一个通用硬盘(generic hard disk)对象。
    hd_struct:             描述分区应有的分区信息 
    bio:                        描述块数据传送时怎样完成填充或读取块给driver
    request:                描述向内核请求一个列表准备做队列处理。 
    request_queue:  描述内核申请request资源建立请求链表并填写BIO形成队列。
    ========

    Linux设备驱动--块设备(二)之相关结构体



    上回最后面介绍了相关数据结构,下面再详细介绍
    块设备对象结构 block_device
    内核用结构block_device实例代表一个块设备对象,如:整个硬盘或特定分区。如果该结构代表一个分


    区,则其成员bd_part指向设备的分区结构。如果该结构代表设备,则其成员bd_disk指向设备的通用硬


    盘结构gendisk
    当用户打开块设备文件时,内核创建结构block_device实例,设备驱动程序还将创建结构gendisk实例,


    分配请求队列并注册结构block_device实例。
    块设备对象结构block_device列出如下(在include/linux/fs.h中)
    [cpp] view plain copy print?
    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实例,装载分区表,分配请求


    队列并填充结构的其他域。
    支持分区的块驱动程序必须包含 <linux/genhd.h> 头文件,并声明一个结构gendisk,内核还维护该结


    构实例的一个全局链表gendisk_head,通过函数add_gendisk、del_gendisk和get_gendisk维护该链表。
    结构gendisk列出如下(在include/linux/genhd.h中):
    [cpp] view plain copy print?
    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;                 /*私有数据*/  
        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
    gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备:
    void add_disk(struct gendisk *gd);
    特别要注意的是对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 操作结构使它们的操作对系统可用. 一个类似的结构用在块设备上是 


    struct block_device_operations,
    定义在 <linux/fs.h>. 
    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 方法


    相当短.
    PS:在block_device_operations中没有实际读或写数据的函数. 在块 I/O 子系统, 这些操作由请求函


    数处理
    请求结构request
    结构request代表了挂起的I/O请求,每个请求用一个结构request实例描述,存放在请求队列链表中,由


    电梯算法进行排序,每个请求包含1个或多个结构bio实例


    struct request {  
        //用于挂在请求队列链表的节点,使用函数blkdev_dequeue_request访问它,而不能直接访  
    问  
        struct list_head queuelist;   
        struct list_head donelist;  /*用于挂在已完成请求链表的节点*/  
        struct request_queue *q;   /*指向请求队列*/  
        unsigned int cmd_flags;    /*命令标识*/  
        enum rq_cmd_type_bits cmd_type;  /*命令类型*/  
        /*各种各样的扇区计数*/  
       /*为提交i/o维护bio横断面的状态信息,hard_*成员是块层内部使用的,驱动程序不应该改变 
    它们*/  
        sector_t sector;     /*将提交的下一个扇区*/  
        sector_t hard_sector;        /* 将完成的下一个扇区*/  
        unsigned long nr_sectors;  /* 整个请求还需要传送的扇区数*/  
        unsigned long hard_nr_sectors; /* 将完成的扇区数*/  
     /*在当前bio中还需要传送的扇区数 */  
        unsigned int current_nr_sectors;  
        /*在当前段中将完成的扇区数*/  
        unsigned int hard_cur_sectors;  
        struct bio *bio;     /*请求中第一个未完成操作的bio*、 
        struct bio *biotail; /*请求链表中末尾的bio*、 
        struct hlist_node hash;  /*融合 hash */  
        /* rb_node仅用在I/O调度器中,当请求被移到分发队列中时, 
    请求将被删除。因此,让completion_data与rb_node分享空间*/      
        union {  
            struct rb_node rb_node;   /* 排序/查找*/  
            void *completion_data;  
        };  
     request结构体的主要成员包括:
     sector_t hard_sector; 
    unsigned long hard_nr_sectors; 
    unsigned int hard_cur_sectors; 
    上述3个成员标识还未完成的扇区,hard_sector是第1个尚未传输的扇区,hard_nr_sectors是尚待完成


    的扇区数,hard_cur_sectors是并且当前I/O操作中待完成的扇区数。这些成员只用于内核块设备层,驱


    动不应当使用它们。


     sector_t sector; 
    unsigned long nr_sectors; 
    unsigned int current_nr_sectors; 
    驱动中会经常与这3个成员打交道,这3个成员在内核和驱动交互中发挥着重大作用。它们以512字节大小


    为1个扇区,如果硬件的扇区大小不是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()。
    请求队列结构request_queue
    每个块设备都有一个请求队列,每个请求队列单独执行I/O调度,请求队列是由请求结构实例链接成的双


    向链表,链表以及整个队列的信息用结构request_queue描述,称为请求队列对象结构或请求队列结构。


    它存放了关于挂起请求的信息以及管理请求队列(如:电梯算法)所需要的信息。结构成员request_fn


    是来自设备驱动程序的请求处理函数。
    请求队列结构request_queue列出如下(在/include/linux/blk_dev.h中)
    太长了,此处略,其实也看不懂,- -#
    Bio结构
    通常1个bio对应1个I/O请求,IO调度算法可将连续的bio合并成1个请求。所以,1个请求可以包含多个


    bio。
    内核中块I/O操作的基本容器由bio结构体表示,定义 在<linux/bio.h>中,该结构体代表了正在现场的


    (活动的)以片段(segment)链表形式组织的块I/O操作。一个片段是一小 块连续的内存缓冲区。这样


    的好处就是不需要保证单个缓冲区一定要连续。所以通过片段来描述缓冲区,即使一个缓冲区分散在内


    存的多个位置上,bio结构体也 能对内核保证I/O操作的执行,这样的就叫做聚散I/O.
    bio为通用层的主要数据结构,既描述了磁盘的位置,又描述了内存的位置,是上层内核vfs与下层驱动


    的连接纽带


    struct bio {  
    sector_t        bi_sector;//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置  
    struct bio        *bi_next;    //请求链表  
    struct block_device    *bi_bdev;//相关的块设备  
    unsigned long        bi_flags//状态和命令标志  
    unsigned long        bi_rw; //读写  
    unsigned short        bi_vcnt;//bio_vesc偏移的个数  
    unsigned short        bi_idx;    //bi_io_vec的当前索引  
    unsigned short        bi_phys_segments;//结合后的片段数目  
    unsigned short        bi_hw_segments;//重映射后的片段数目  
    unsigned int        bi_size;    //I/O计数  
    unsigned int        bi_hw_front_size;//第一个可合并的段大小;  
    unsigned int        bi_hw_back_size;//最后一个可合并的段大小  
    unsigned int        bi_max_vecs;    //bio_vecs数目上限  
    struct bio_vec        *bi_io_vec;    //bio_vec链表:内存的位置  
    bio_end_io_t        *bi_end_io;//I/O完成方法  
    atomic_t        bi_cnt; //使用计数  
    void            *bi_private; //拥有者的私有方法  
    bio_destructor_t    *bi_destructor;    //销毁方法  
    };  


    内存数据段结构bio_vec
           结构bio_vec代表了内存中的一个数据段,数据段用页、偏移和长度描
    述。I/O需要执行的内存位置用段表示,结构bio指向了一个段的数组。
    结构bio_vec列出如下(在include/linux/bio.h中):
    struct bio_vec {
           struct page     *bv_page;   /*数据段所在的页*/
           unsigned short  bv_len;     /*数据段的长度*/
           unsigned short  bv_offset;  /*数据段页内偏移*/
    };
    块设备各个结构体间关系


    ========

    Linux设备驱动--块设备(三)之程序设计



     块设备驱动注册与注销
    块设备驱动中的第1个工作通常是注册它们自己到内核,完成这个任务的函数是 register_blkdev(),其


    原型为:
    int register_blkdev(unsigned int major, const char *name);


    major 参数是块设备要使用的主设备号,name为设备名,它会在/proc/devices中被显示。 如果major为


    0,内核会自动分配一个新的主设备号register_blkdev()函数的返回值就是这个主设备号。如果返回1个


    负值,表明发生了一个错误。
    与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:
    int unregister_blkdev(unsigned int major, const char *name);
    这里,传递给register_blkdev()的参数必须与传递给register_blkdev()的参数匹配,否则这个函数返


    回-EINVAL。
    块设备的请求队列操作
    标准的请求处理程序能排序请求,并合并相邻的请求,如果一个块设备希望使用标准的请求处理程序,


    那它必须调用函数blk_init_queue来初始化请求队列。当处理在队列上的请求时,必须持有队列自旋锁


    。初始化请求队列
    request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);


    该函数的第1个参数是请求处理函数的指针,第2个参数是控制访问队列权限的自旋锁,这个函数会发生


    内存分配的行为,故它可能会失败,函数调用成
    功时,它返回指向初始化请求队列的指针,否则,返回NULL。这个函数一般在块设备驱动的模块加载函


    数中调用。清除请求队列
    void blk_cleanup_queue(request_queue_t * q);


    这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
     
    提取请求
    struct request *elv_next_request(request_queue_t *queue); 
    上述函数用于返回下一个要处理的请求(由 I/O 调度器决定),如果没有请求则返回NULL。
    去除请求
    void blkdev_dequeue_request(struct request *req); 
    上述函数从队列中去除1个请求。如果驱动中同时从同一个队列中操作了多个请求,它必须以这样的方式


    将它们从队列中去除。
     
    分配“请求队列”
    request_queue_t *blk_alloc_queue(int gfp_mask);
    对于FLASH、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上


    述函数分配1个“请求队列”,并使用如下函数来绑定“请求队列”和“制造请求”函数。
    void blk_queue_make_request(request_queue_t * q, 
    make_request_fn * mfn);
    void blk_queue_hardsect_size(request_queue_t *queue, 
    unsigned short max); 
    该函数用于告知内核块设备硬件扇区的大小,所有由内核产生的请求都是这个大小的倍数并且被正确对


    界。但是,内核块设备层和驱动之间的通信还是以512字节扇区为单位进行。
     
    步骤:
    在块设备驱动的模块加载函数中通常需要完成如下工作:
    ① 分配、初始化请求队列,绑定请求队列和请求函数。
    ② 分配、初始化gendisk,给gendisk的major、fops、queue等成
    员赋值,最后添加gendisk。
    ③ 注册块设备驱动。
    在块设备驱动的模块卸载函数中通常需要与模块加载函数相反的工作:
    ① 清除请求队列。
    ② 删除gendisk和对gendisk的引用。
    ③ 删除对块设备的引用,注销块设备驱动。
    总结:
    块设备的I/O操作方式与字符设备存在较大的不同,因而引入了
    request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿于始终的就是“请


    求”,字符设备的I/O操作则是直接进行不绕弯,
    块设备的I/O操作会排队和整合。
    驱动的任务是处理请求,对请求的排队和整合由I/O调度算法解决,因此,块设备驱动的核心就是请求处


    理函数或“制造请求”函数。
    尽管在块设备驱动中仍然存在block_device_operations结构体及其成员函数,但其不再包含读写一类的


    成员函数,而只是包含打开、释放及I/O控制等
    与具体读写无关的函数。块设备驱动的结构相当复杂的,但幸运的是,块设备不像字符设备那么包罗万


    象,它通常就是存储设备,而且驱动的主体已经
    由Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及到的工作往往只是编写少量的与硬件直


    接交互的代码。


    #include <linux/init.h>    
    #include <linux/module.h>    
    #include <linux/kernel.h>    
    #include <linux/fs.h>  
    #include <asm/uaccess.h>  
    #include <linux/spinlock.h>  
    #include <linux/sched.h>  
    #include <linux/types.h>  
    #include <linux/fcntl.h>  
    #include <linux/hdreg.h>  
    #include <linux/genhd.h>  
    #include <linux/blkdev.h>  
      
    #define MAXBUF 1024   
      
      
    #define BLK_MAJOR 253  
      
    char blk_dev_name[]="blk_dev";  
    static char flash[1024*16];  
      
      
    int major;  
    spinlock_t lock;  
    struct gendisk *gd;  
      
      
      
    /*块设备数据传输*/  
    static void blk_transfer(unsigned long sector, unsigned long nsect, char *buffer, int 


    write)  
    {  
        int read = !write;  
        if(read)  
        {  
            memcpy(buffer, flash+sector*512, nsect*512);  
        }  
        else  
        {  
            memcpy(flash+sector*512, buffer, nsect*512);  
        }  
    }  
      
    /*块设备请求处理函数*/  
    static void blk_request_func(struct request_queue *q)  
    {  
        struct request *req;  
        while((req = elv_next_request(q)) != NULL)    
        {  
            if(!blk_fs_request(req))  
            {  
                end_request(req, 0);  
                continue;  
            }  
              
            blk_transfer(req->sector, req->current_nr_sectors, req->buffer, rq_data_dir(req));  
            /*rq_data_dir从request获得数据传送的方向*/  
            /*req->current_nr_sectors 在当前段中将完成的扇区数*/  
            /*req->sector 将提交的下一个扇区*/  
            end_request(req, 1);  
        }  
    }  
      
    /*strcut block_device_operations*/  
    static  int blk_ioctl(struct block_device *dev, fmode_t no, unsigned cmd, unsigned long 


    arg)  
    {  
           return -ENOTTY;  
    }  
      
    static int blk_open (struct block_device *dev , fmode_t no)  
    {  
        printk("blk mount succeed\n");  
        return 0;  
    }  
    static int blk_release(struct gendisk *gd , fmode_t no)  
    {  
        printk("blk umount succeed\n");  
        return 0;  
    }  
    struct block_device_operations blk_ops=  
    {  
        .owner = THIS_MODULE,  
        .open = blk_open,  
        .release = blk_release,  
        .ioctl = blk_ioctl,  
    };  
      
    //-----------------------------------------------  
      
    static int __init block_module_init(void)  
    {  
          
          
        if(!register_blkdev(BLK_MAJOR, blk_dev_name)) //注册一个块设备  
        {  
            major = BLK_MAJOR;    
            printk("regiser blk dev succeed\n");  
        }  
        else  
        {  
            return -EBUSY;  
        }  
        gd = alloc_disk(1);  //分配一个gendisk,分区是一个  
        spin_lock_init(&lock); //初始化一个自旋锁  
        gd->major = major;  
        gd->first_minor = 0;   //第一个次设备号  
        gd->fops = &blk_ops;   //关联操作函数  
      
        gd->queue = blk_init_queue(blk_request_func, &lock); //初始化请求队列并关联到gendisk  
      
        snprintf(gd->disk_name, 32, "blk%c", 'a');    
        blk_queue_hardsect_size(gd->queue, 512);  //设置扇区大小512字节  
        set_capacity(gd, 32);  //设置块设备大小 512*32=16K  
        add_disk(gd);  
        printk("gendisk init success!\n");  
        return 0;  
    }  
    static void __exit block_module_exit(void)  
    {  
        blk_cleanup_queue(gd->queue);  
        del_gendisk(gd);   
        unregister_blkdev(BLK_MAJOR, blk_dev_name);  
        printk("block module exit succeed!\n");  
    }  
      
    module_init(block_module_init);  
    module_exit(block_module_exit);  
      
    MODULE_LICENSE("GPL");  
    MODULE_AUTHOR("gec"); 
    ========

    linux之块设备驱动



        Linux操作系统有两类主要的设备文件:
    1.字符设备:以字节为单位进行顺序I/O操作的设备,无需缓冲区且被直接读写。
    2.块设备:只能以块单位接收输入返回,对于I/O请求有对应的缓冲区,可以随机访问,块设备的访问位


    置必须能够在介质的不同区间前后移动。在块设备中,最小的可寻址单元是扇区,扇区的大小一般是2的


    整数倍,常见的大小为512个字节。


       上图是一个块设备操作的分层实现图
    1.当一个进程被Read时,内核会通过VFS层去读取要读的文件块有没有被cache了,这个cache由一个


    buffer_head结构读取。如果要读取的文件块还没有被cache,则就要从文件系统中去读取,通过一个


    address_space结构来引用,如果调用文件系统读取函数去读取一个扇区的数据。当它从磁盘读出数据时


    ,将数据页连入到cache中,当下一次在读取时,就不需要从磁盘去读取了。Read完后将请求初始化成一


    个bio结构,并提交给通用块层。
    2.它通过submit_bio()去完成,通用层在调用相应的设备IO调度器,这个调度器的调度算法,将这个bio


    合并到已经存在的request中,或者创建一个新的request,并将创建的插入到请求队列中,最后就剩下


    块设备驱动层来完成后面的所有工作。
    内核中块得I/O操作的是由bio结构表示的


    点击(此处)折叠或打开
    struct bio {
        sector_t        bi_sector;    /*该BIO结构所要传输的第一个(512字节)扇区*/
        struct bio        *bi_next;    /*请求链表*/
        struct block_device    *bi_bdev;/*相关的块设备*/
        unsigned long        bi_flags;    /*状态和命令的标志*/
        unsigned long        bi_rw;        /*读写*/


        unsigned short        bi_vcnt;    /* bio_vec的偏移个数 */
        unsigned short        bi_idx;        /* bvl_vec */


        unsigned short        bi_phys_segments;


        /* Number of segments after physical and DMA remapping
         * hardware coalescing is performed.
         */
        unsigned short        bi_hw_segments;


        unsigned int        bi_size;    /* residual I/O count */


        /*
         * To keep track of the max hw size, we account for the
         * sizes of the first and last virtually mergeable segments
         * in this bio
         */
        unsigned int        bi_hw_front_size;
        unsigned int        bi_hw_back_size;


        unsigned int        bi_max_vecs;    /* max bvl_vecs we can hold */


        struct bio_vec        *bi_io_vec;    /* the actual vec list */


        bio_end_io_t        *bi_end_io;
        atomic_t        bi_cnt;        /* pin count */


        void            *bi_private;


        bio_destructor_t    *bi_destructor;    /* destructor */
    };
    此结构体的目的主要是正在执行的I/O操作,其中的bi_io_vecs、bi_vcnt、bi_idx三者都可以相互找到


    。bio_vec描述一个特定的片段,片段所在的物理页,块在物理页中的偏移页,整个bio_io_vec结构表示


    一个完整的缓冲区。
    点击(此处)折叠或打开
    struct bio_vec {
        struct page    *bv_page;
        unsigned int    bv_len;
        unsigned int    bv_offset;
    };
    当一个块被调用内存时,要储存在一个缓冲区,每个缓冲区与一个块对应,所以每一个缓冲区独有一个


    对应的描述符,该描述符用buffer_head结构表示
    点击(此处)折叠或打开
    struct buffer_head {
        unsigned long b_state;        /* buffer state bitmap (see above) */
        struct buffer_head *b_this_page;/* circular list of page's buffers */
        struct page *b_page;        /* the page this bh is mapped to */


        sector_t b_blocknr;        /* start block number */
        size_t b_size;            /* size of mapping */
        char *b_data;            /* pointer to data within the page */


        struct block_device *b_bdev;
        bh_end_io_t *b_end_io;        /* I/O completion */
         void *b_private;        /* reserved for b_end_io */
        struct list_head b_assoc_buffers; /* associated with another mapping */
        struct address_space *b_assoc_map;    /* mapping this buffer is
                             associated with */
        atomic_t b_count;        /* users using this buffer_head */
    };
    下面来看看块设备的核心ll_rw_block函数
    点击(此处)折叠或打开
    void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
    {
        int i;


        for (i = 0; i < nr; i ) {
            struct buffer_head *bh = bhs[i];


            if (!trylock_buffer(bh))
                continue;
            if (rw == WRITE) {
                if (test_clear_buffer_dirty(bh)) {
                    bh->b_end_io = end_buffer_write_sync;
                    get_bh(bh);
                    submit_bh(WRITE, bh);
                    continue;
                }
            } else {
                if (!buffer_uptodate(bh)) {
                    bh->b_end_io = end_buffer_read_sync;
                    get_bh(bh);
                    submit_bh(rw, bh);
                    continue;
                }
            }
            unlock_buffer(bh);
        }
    }
    请求块设备驱动将多个物理块读出或者写到块设备,块设备的读写都是在块缓冲区中进行。
    点击(此处)折叠或打开
    int submit_bh(int rw, struct buffer_head * bh)
    {
        struct bio *bio;
        int ret = 0;


        BUG_ON(!buffer_locked(bh));
        BUG_ON(!buffer_mapped(bh));
        BUG_ON(!bh->b_end_io);
        BUG_ON(buffer_delay(bh));
        BUG_ON(buffer_unwritten(bh));


        /*
         * Only clear out a write error when rewriting
         */
        if (test_set_buffer_req(bh) && (rw & WRITE))
            clear_buffer_write_io_error(bh);


        /*
         * from here on down, it's all bio -- do the initial mapping,
         * submit_bio -> generic_make_request may further map this bio around
         */
        bio = bio_alloc(GFP_NOIO, 1);


        bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);
        bio->bi_bdev = bh->b_bdev;
        bio->bi_io_vec[0].bv_page = bh->b_page;
        bio->bi_io_vec[0].bv_len = bh->b_size;
        bio->bi_io_vec[0].bv_offset = bh_offset(bh);


        bio->bi_vcnt = 1;
        bio->bi_idx = 0;
        bio->bi_size = bh->b_size;


        bio->bi_end_io = end_bio_bh_io_sync;
        bio->bi_private = bh;


        bio_get(bio);
        submit_bio(rw, bio);


        if (bio_flagged(bio, BIO_EOPNOTSUPP))
            ret = -EOPNOTSUPP;


        bio_put(bio);
        return ret;
    这个函数主要是调用submit_bio,最终调用generic_make_request去完成将bio传递给驱动去处理。
    点击(此处)折叠或打开
    void generic_make_request(struct bio *bio)
    {
        struct bio_list bio_list_on_stack;


        if (!generic_make_request_checks(bio))
            return;




        if (current->bio_list) {
            bio_list_add(current->bio_list, bio);
            return;
        }


        BUG_ON(bio->bi_next);
        bio_list_init(&bio_list_on_stack);
        current->bio_list = &bio_list_on_stack;
        do {
            struct request_queue *q = bdev_get_queue(bio->bi_bdev);


            q->make_request_fn(q, bio);


            bio = bio_list_pop(current->bio_list);
        } while (bio);
        current->bio_list = NULL; /* deactivate */
    }
    这个函数主要是取出块设备相应的队列中的每个设备,在调用块设备驱动的make_request,如果没有指


    定make_request就调用内核默认的__make_request,这个函数主要作用就是调用I/O调度算法将bio合并


    ,或插入到队列中合适的位置中去。
    整个流程为:


    那么request_fn指向那个函数呢?在内核中搜搜request_fn,发现


    request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
    {
        return blk_init_queue_node(rfn, lock, -1);
    }
    EXPORT_SYMBOL(blk_init_queue);


    request_queue_t *
    blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id)
    {
        request_queue_t *q = blk_alloc_queue_node(GFP_KERNEL, node_id);


        if (!q)
            return NULL;


        q->node = node_id;
        if (blk_init_free_list(q)) {
            kmem_cache_free(requestq_cachep, q);
            return NULL;
        }




        if (!lock) {
            spin_lock_init(&q->__queue_lock);
            lock = &q->__queue_lock;
        }


        q->request_fn        = rfn;
        q->prep_rq_fn        = NULL;
        q->unplug_fn        = generic_unplug_device;
        q->queue_flags        = (1 << QUEUE_FLAG_CLUSTER);
        q->queue_lock        = lock;


        blk_queue_segment_boundary(q, 0xffffffff);


        blk_queue_make_request(q, __make_request);
        blk_queue_max_segment_size(q, MAX_SEGMENT_SIZE);


        blk_queue_max_hw_segments(q, MAX_HW_SEGMENTS);
        blk_queue_max_phys_segments(q, MAX_PHYS_SEGMENTS);


        q->sg_reserved_size = INT_MAX;


        /*
         * all done
         */
        if (!elevator_init(q, NULL)) {
            blk_queue_congestion_threshold(q);
            return q;
        }


        blk_put_queue(q);
        return NULL;
    }
    原来request_queue的make_request_fn指向__make_request()函数,这个函数复杂I/O调度,并对bio做


    些合并等。下面来看看__make_request()做了些什么?


    由上面可以分析得出,其中有一个很重要的结构体


    struct request {
        struct list_head queuelist;//连接这个请求到请求队列. 
    //追踪请求硬件完成的扇区的成员. 第一个尚未被传送的扇区被存储到 hard_sector, 已经传送的扇区


    总数在 ha//rd_nr_sectors, 并且在当前 bio 中剩余的扇区数是 hard_cur_sectors. 这些成员打算只


    用在块子系统; 驱动//不应当使用它们.
        struct request_queue *q;
        sector_t hard_sector; 
        unsigned long hard_nr_sectors; 
        unsigned int hard_cur_sectors;
        struct bio *bio;//bio 是给这个请求的 bio 结构的链表. 你不应当直接存取这个成员; 使用 


    rq_for_each_bio(后面描述) 代替.
        unsigned short nr_phys_segments;//被这个请求在物理内存中占用的独特段的数目, 在邻近页已


    被合并后
        char *buffer;//随着深入理解,可见到这个成员仅仅是在当前 bio 上调用 bio_data 的结果.
    };
    request_queue只是一个请求队列,通过可以找到requeue,然后通过bio结构体对应的page读取物理内存


    中的信息。
    下面看看内核使用的块设备的例子


    static int jsfd_init(void)
    {
        static DEFINE_SPINLOCK(lock);
        struct jsflash *jsf;
        struct jsfd_part *jdp;
        int err;
        int i;


        if (jsf0.base == 0)
            return -ENXIO;


        err = -ENOMEM;
    //1. 分配gendisk: alloc_disk
        for (i = 0; i < JSF_MAX; i++) {
            struct gendisk *disk = alloc_disk(1);
            if (!disk)
                goto out;
            jsfd_disk[i] = disk;
        }
    //2. 设置,分配/设置队列: request_queue_t  // 它提供读写能力
        if (register_blkdev(JSFD_MAJOR, "jsfd")) {
            err = -EIO;
            goto out;
        }


        jsf_queue = blk_init_queue(jsfd_do_request, &lock);
        if (!jsf_queue) {
            err = -ENOMEM;
            unregister_blkdev(JSFD_MAJOR, "jsfd");
            goto out;
        }
    //3. 设置gendisk其他信息             // 它提供属性: 比如容量
        for (i = 0; i < JSF_MAX; i++) {
            struct gendisk *disk = jsfd_disk[i];
            if ((i & JSF_PART_MASK) >= JSF_NPART) continue;
            jsf = &jsf0;    /* actually, &jsfv[i >> JSF_PART_BITS] */
            jdp = &jsf->dv[i&JSF_PART_MASK];


            disk->major = JSFD_MAJOR;
            disk->first_minor = i;
            sprintf(disk->disk_name, "jsfd%d", i);
            disk->fops = &jsfd_fops;
            set_capacity(disk, jdp->dsize >> 9);
            disk->private_data = jdp;
            disk->queue = jsf_queue;
    //4 注册: add_disk
            add_disk(disk);
            set_disk_ro(disk, 1);
        }
        return 0;
    out:
        while (i--)
            put_disk(jsfd_disk[i]);
        return err;
    }
    ========

    Linux 块设备驱动 实例

     
    任务:用一片虚拟地址连续的内存空间模拟一个块设备,并为其写一个驱动


    /*
     * Sample disk driver, from the beginning.
     */


    #include <linux/autoconf.h>
    #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>


    MODULE_LICENSE("Dual BSD/GPL");


    static int sbull_major = 0;
    module_param(sbull_major, int, 0);
    static int hardsect_size = 512;
    module_param(hardsect_size, int, 0);
    static int nsectors = 25600;    /* How big the drive is */
    module_param(nsectors, int, 0);
    static int ndevices = 1;
    module_param(ndevices, int, 0);


    /*
     * The different "request modes" we can use.
     */
    enum {
        RM_SIMPLE  = 0,    /* The extra-simple request function */
        RM_FULL    = 1,    /* The full-blown version */
        RM_NOQUEUE = 2,    /* Use make_request */
    };
    //static int request_mode = RM_FULL;
    static int request_mode = RM_SIMPLE;
    //static int request_mode = RM_SIMPLE;
    module_param(request_mode, int, 0);


    /*
     * Minor number and partition management.
     */
    #define SBULL_MINORS    16
    #define MINOR_SHIFT    4
    #define DEVNUM(kdevnum)    (MINOR(kdev_t_to_nr(kdevnum)) >> MINOR_SHIFT


    /*
     * We can tweak our hardware sector size, but the kernel talks to us
     * in terms of small sectors, always.
     */
    #define KERNEL_SECTOR_SIZE    512


    /*
     * After this much idle time, the driver will simulate a media change.
     */
    #define INVALIDATE_DELAY    60*HZ


    /*
     * The internal representation of our device.
     */
    struct sbull_dev {
            int size;                       /* Device size in sectors */
            // data 是本程序模拟的块设备,是一片连续的虚拟空间
            // 在初始化函数里分配的虚拟地址连续的内存空间
            u8 *data;                       /* The data array */
            short users;                    /* How many users */
            short media_change;             /* Flag a media change? */
            spinlock_t lock;                /* For mutual exclusion */
            struct request_queue *queue;    /* The device request queue */
            struct gendisk *gd;             /* The gendisk structure */
            struct timer_list timer;        /* For simulated media changes */
    };


    static struct sbull_dev *Devices = NULL;


    /*
     * Handle an I/O request.
     */
    static void sbull_transfer(struct sbull_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;
        }
        // 实际的读写操作
        // 由于本程序是用一片连续的内存空间模拟块设备
        // 所以这里对硬件(内存空间)的读写操作,就是复制内存
        // 在具体点,就是下面的memcpy
        // 具体的项目,需修改为具体的接口函数
        if (write)
            // 写
            memcpy(dev->data + offset, buffer, nbytes);
        else
            // 读
            memcpy(buffer, dev->data + offset, nbytes);
    }


    /*The simple form of the request function.*/


    static void sbull_request(struct request_queue *q)
    {
        struct request *req;


        // 服务完队列上的所有请求
        while ((req = elv_next_request(q)) != NULL) {  // elv_next_request :从队列上去一个下来
            struct sbull_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;
            }
            sbull_transfer(dev, req->sector, req->current_nr_sectors,
                    req->buffer, rq_data_dir(req));
            end_request(req, 1);
        }
    }




    /*
     * Transfer a single BIO.
     */
    static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
    {
        int i;
        struct bio_vec *bvec;
        sector_t sector = bio->bi_sector;


        /* Do each segment independently. */
        bio_for_each_segment(bvec, bio, i) {
            char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);
            sbull_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; /* Always "succeed" */
    }


    /*
     * Transfer a full request.
     */
    static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
    {
        struct bio *bio;
        int nsect = 0;


        // steps through each bio that makes up a request.
        // 遍历
        __rq_for_each_bio(bio, req) {
            sbull_xfer_bio(dev, bio);
            nsect += bio->bi_size/KERNEL_SECTOR_SIZE;
        }
        return nsect;
    }




    /*
     * Smarter request function that "handles clustering".
     */
    static void sbull_full_request(struct request_queue *q)
    {
        struct request *req;
        int sectors_xferred;
        struct sbull_dev *dev = q->queuedata;


        printk("<0>""in %s\n",__FUNCTION__);
        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 = sbull_xfer_request(dev, req);
            __blk_end_request(req,0,sectors_xferred<<9);//add by lht for 2.6.27
        }
    }


     


    //The direct make request version
    static int sbull_make_request(struct request_queue *q, struct bio *bio)
    {
        struct sbull_dev *dev = q->queuedata;
        int status;


        status = sbull_xfer_bio(dev, bio);
        //bio_endio(bio, bio->bi_size, status);
        bio_endio(bio, status);
        return 0;
    }




    /*
     * Open and close.
     */


    static int sbull_open(struct inode *inode, struct file *filp)
    {
        struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
        //printk("<0>" "fdfjdlksjfdlkj\n");    
        del_timer_sync(&dev->timer);
        filp->private_data = dev;
        spin_lock(&dev->lock);
        if (! dev->users) 
            check_disk_change(inode->i_bdev);
        dev->users++;
        spin_unlock(&dev->lock);
        return 0;
    }


    static int sbull_release(struct inode *inode, struct file *filp)
    {
        struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;


        spin_lock(&dev->lock);
        dev->users--;


        if (!dev->users) {
            dev->timer.expires = jiffies + INVALIDATE_DELAY;
            add_timer(&dev->timer);
        }
        spin_unlock(&dev->lock);


        return 0;
    }


    /*
     * Look for a (simulated) media change.
     */
    int sbull_media_changed(struct gendisk *gd)
    {
        struct sbull_dev *dev = gd->private_data;
        
        return dev->media_change;
    }


    /*
     * Revalidate.  WE DO NOT TAKE THE LOCK HERE, for fear of deadlocking
     * with open.  That needs to be reevaluated.
     */
    int sbull_revalidate(struct gendisk *gd)
    {
        struct sbull_dev *dev = gd->private_data;
        
        if (dev->media_change) {
            dev->media_change = 0;
            memset (dev->data, 0, dev->size);
        }
        return 0;
    }


    /*
     * The "invalidate" function runs out of the device timer; it sets
     * a flag to simulate the removal of the media.
     */
    void sbull_invalidate(unsigned long ldev)
    {
        struct sbull_dev *dev = (struct sbull_dev *) ldev;


        spin_lock(&dev->lock);
        if (dev->users || !dev->data) 
            printk (KERN_WARNING "sbull: timer sanity check failed\n");
        else
            dev->media_change = 1;
        spin_unlock(&dev->lock);
    }


    /*
     * The ioctl() implementation
     */


    int sbull_ioctl (struct inode *inode, struct file *filp,
                     unsigned int cmd, unsigned long arg)
    {
        long size;
        struct hd_geometry geo;
        struct sbull_dev *dev = filp->private_data;


        switch(cmd) {
            case HDIO_GETGEO:
                /*
             * Get geometry: since we are a virtual device, we have to make
             * up something plausible.  So we claim 16 sectors, four heads,
             * and calculate the corresponding number of cylinders.  We set the
             * start of data at sector four.
             */
            //printk("<0>""-------------size=%d\n",size);
            /****************for early version************/
            //size = dev->size*(hardsect_size/KERNEL_SECTOR_SIZE);
            //printk("<0>""-------------size=%d\n",size);
            //geo.cylinders = (size & ~0x3f) >> 6;
            //geo.cylinders=2000;
            //geo.heads = 4;
            //geo.sectors = 16;
            //geo.sectors=2560;
            //geo.start = 0;
            //if (copy_to_user((void __user *) arg, &geo, sizeof(geo)))
            //    return -EFAULT;
            return 0;
        }


        return -ENOTTY; /* unknown command */
    }


    static int sbull_getgeo(struct block_device *bdev, struct hd_geometry *geo)
    {
        unsigned long size;
        struct sbull_dev *pdev = bdev->bd_disk->private_data;


        size = pdev->size;
        geo->cylinders = (size & ~0x3f) >> 6;
        geo->heads    = 4;
        geo->sectors = 16;
        geo->start = 0;
        return 0;
    }




    /*
     * The device operations structure.
     */
    static struct block_device_operations sbull_ops = {
        .owner           = THIS_MODULE,
        .open              = sbull_open,
        .release      = sbull_release,
        .media_changed   = sbull_media_changed,
        .revalidate_disk = sbull_revalidate,
        .ioctl             = sbull_ioctl,
        .getgeo            = sbull_getgeo,
    };




    /*
     * Set up our internal device.
     */
    // 初始化设备结构体 static struct sbull_dev *Devices中的成员
    static void setup_device(struct sbull_dev *dev, int which)
    {
        /*
         * Get some memory.
         */
        memset (dev, 0, sizeof (struct sbull_dev));
        dev->size = nsectors*hardsect_size;
        // 分配一片虚拟地址连续的内存空间,作为块设备。
        dev->data = vmalloc(dev->size);   
        if (dev->data == NULL) {
            printk (KERN_NOTICE "vmalloc failure.\n");
            return;
        }
        spin_lock_init(&dev->lock);
        
        /*
         * The timer which "invalidates" the device.
         */
        init_timer(&dev->timer);
        dev->timer.data = (unsigned long) dev;
        dev->timer.function = sbull_invalidate;
        
        /*
         * The I/O queue, depending on whether we are using our own
         * make_request function or not.
         */
        switch (request_mode) {
            case RM_NOQUEUE:
            dev->queue = blk_alloc_queue(GFP_KERNEL);
            if (dev->queue == NULL)
                goto out_vfree;
            blk_queue_make_request(dev->queue, sbull_make_request);
            break;


            case RM_FULL:
            dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
            if (dev->queue == NULL)
                goto out_vfree;
            break;


            default:
            printk(KERN_NOTICE "Bad request mode %d, using simple\n", request_mode);
                /* fall into.. */
        
            case RM_SIMPLE:
            dev->queue = blk_init_queue(sbull_request, &dev->lock);
            if (dev->queue == NULL)
                goto out_vfree;
            break;
        }
        blk_queue_hardsect_size(dev->queue, hardsect_size);
        dev->queue->queuedata = dev;
        /*
         * And the gendisk structure.
         */
        dev->gd = alloc_disk(SBULL_MINORS);
        if (! dev->gd) {
            printk (KERN_NOTICE "alloc_disk failure\n");
            goto out_vfree;
        }
        dev->gd->major = sbull_major;
        dev->gd->first_minor = which*SBULL_MINORS;
        dev->gd->fops = &sbull_ops;
        dev->gd->queue = dev->queue;
        dev->gd->private_data = dev;
        snprintf (dev->gd->disk_name, 32, "sbull%c", which + 'a');
        set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
        add_disk(dev->gd);
        return;


      out_vfree:
        if (dev->data)
            vfree(dev->data);
    }


     


    static int __init sbull_init(void)
    {
        int i;
        /*
         * Get registered.
         */
        //    printk("<0>" "add by lht\n");
        sbull_major = register_blkdev(sbull_major, "sbull");
        if (sbull_major <= 0) {
            printk(KERN_WARNING "sbull: unable to get major number\n");
            return -EBUSY;
        }
        /*
         * Allocate the device array, and initialize each one.
         */
        Devices = kmalloc(ndevices*sizeof (struct sbull_dev), GFP_KERNEL);
        if (Devices == NULL)
            goto out_unregister;
        for (i = 0; i < ndevices; i++) 
            setup_device(Devices + i, i);
        
        return 0;


      out_unregister:
        unregister_blkdev(sbull_major, "sbd");
        return -ENOMEM;
    }


    static void sbull_exit(void)
    {
        int i;


        for (i = 0; i < ndevices; i++) {
            struct sbull_dev *dev = Devices + i;


            del_timer_sync(&dev->timer);
            if (dev->gd) {
                del_gendisk(dev->gd);
                put_disk(dev->gd);
            }
            if (dev->queue) {
                if (request_mode == RM_NOQUEUE)
                //    blk_put_queue(dev->queue);
                kobject_put(&(dev->queue)->kobj);
                else
                    blk_cleanup_queue(dev->queue);
            }
            if (dev->data)
                vfree(dev->data);
        }
        unregister_blkdev(sbull_major, "sbull");
        kfree(Devices);
    }
        
    module_init(sbull_init);
    module_exit(sbull_exit);


     


    测试方法:


    # Makefile
    ifeq ($(KERNELRELEASE),)


    #KERNELDIR ?= /home/lht/kernel2.6/linux-2.6.14


    KERNELDIR ?= /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    PWD := $(shell pwd)


    modules:
            $(MAKE) -C $(KERNELDIR) M=$(PWD) modules


    modules_install:
            $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install


    clean:
            rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions


    .PHONY: modules modules_install clean


    else
        obj-m := sbull.o
    endif


    将模块插入内核(2.6.27)
    root@linuxidc:/source/workplace/test/sbull_linuxidc# insmod sbull.ko 
    用lsmod查看模块是否成功插入内核
    root@linuxidc:/source/workplace/test/sbull_linuxidc# lsmod | grep sbu
    sbull                  13452  0 
    出现上面结果,说明成功了
    用ls查看/dev下是否有sbull设备
    root@linuxidc:/source/workplace/test/sbull_linuxidc# ls /dev | grep sbu
    sbulla
    出现上面结果,说明有了,如果没有,用命令
    mknod /dev/sbulla b 254 0
    手动创建
    至此,已经有一个块设备了
    下面用fdisk对虚拟块设备分区
    root@linuxidc:/source/workplace/test/sbull_linuxidc# fdisk /dev/sbulla 
    Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
    Building a new DOS disklabel with disk identifier 0x14d0973f.
    Changes will remain in memory only, until you decide to write them.
    After that, of course, the previous content won't be recoverable.


    Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)


    Command (m for help): n        这里选择n,新建
    Command action
       e   extended
       p   primary partition (1-4)  这里选p,主分区
    p
    Partition number (1-4): 1   这里选1,第一个分区
    First cylinder (1-400, default 1): 1
    Last cylinder, +cylinders or +size{K,M,G} (1-400, default 400): 
    Using default value 400


    Command (m for help): w  这里选w,保存并推出
    The partition table has been altered!


    Calling ioctl() to re-read partition table.
    Syncing disks.
    接着将其格式化为ext2


    root@linuxidc:/source/workplace/test/sbull_linuxidc# mkfs.ext2 /dev/sbulla1 
    mke2fs 1.41.3 (12-Oct-2008)
    Filesystem label=
    OS type: Linux
    Block size=1024 (log=0)
    Fragment size=1024 (log=0)
    3200 inodes, 12792 blocks
    639 blocks (5.00%) reserved for the super user
    First data block=1
    Maximum filesystem blocks=13107200
    2 block groups
    8192 blocks per group, 8192 fragments per group
    1600 inodes per group
    Superblock backups stored on blocks: 
            8193


    Writing inode tables: done                            
    Writing superblocks and filesystem accounting information: done


    This filesystem will be automatically checked every 29 mounts or
    180 days, whichever comes first.  Use tune2fs -c or -i to override.
    新建一个文件夹,作为此处模拟的块设备的挂载点
    root@linuxidc:/source/workplace/test/sbull_linuxidc# ls /mnt/
    hgfs  initrd
    root@linuxidc:/source/workplace/test/sbull_linuxidc# mkdir /mnt/sbulla1
    挂载
    root@linuxidc:/source/workplace/test/sbull_linuxidc# mount /dev/sbulla1 /mnt/sbulla1
    进入目录,新建一个文件,测试一下
    root@linuxidc:/source/workplace/test/sbull_linuxidc# cd /mnt/sbulla1/
    root@linuxidc:/mnt/sbulla1# ls
    lost+found
    root@linuxidc:/mnt/sbulla1# echo hi > hello.c
    root@linuxidc:/mnt/sbulla1# ls
    hello.c  lost+found
    root@linuxidc:/mnt/sbulla1# cat hello.c 
    hi
    root@linuxidc:/mnt/sbulla1#
    ========

    linux下的块设备驱动(一)

    块设备的驱动比字符设备的难,这是因为块设备的驱动和内核的联系进一步增大,但是同时块设备的访


    问的几个基本结构和字符还是有相似之处的。


    有一句话必须记住:对于存储设备(硬盘~~带有机械的操作)而言,调整读写的顺序作用巨大,因为读


    写连续的扇区比分离的扇区快。


    但是同时:SD卡和U盘这类设备没有机械上的限制,所以像上面说的进行连续扇区的调整显得就没有必要


    了。


     


    先说一下对于硬盘这类设备的简单的驱动。


    在linux的内核中,使用gendisk结构来表示一个独立的磁盘设备或者分区。这个结构中包含了磁盘的主


    设备号,次设备号以及设备名称。


    在国嵌给的历程中,对gendisk这个结构体的填充是在simp_blkdev_init函数中完成的。在对gendisk这


    个结构填充之前要对其进行分配空间。具体代码如下:


    simp_blkdev_disk = alloc_disk(1);
            if (!simp_blkdev_disk) {
                    ret = -ENOMEM;
                    goto err_alloc_disk;
            }
    这里的alloc_disk函数是在内核中实现的,它后面的参数1代表的是使用次设备号的数量,这个数量是不


    能被修改的。


    在分配好了关于gendisk的空间以后就开始对gendisk里面的成员进行填充。具体代码如下:


    strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); //宏定义simp_blkdev
            simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR; //主设备号
            simp_blkdev_disk->first_minor = 0; //次设备号
            simp_blkdev_disk->fops = &simp_blkdev_fops; //主要结构
            simp_blkdev_disk->queue = simp_blkdev_queue;
            set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9); //宏定义(16*1024*1024),实际


    上就是这个结构体。
    在填充好gendisk这个结构以后向内核中 注册这个磁盘设备。具体代码如下:


    add_disk(simp_blkdev_disk);
    在LDD中说,想内核中注册设备的必须在gendisk这个结构体已经填充好了以后,我们以前的字符设备的


    时候也是这么做的,不知道为什么LDD在这里强调了这个。


    当不需要一个磁盘的时候要释放gendisk,释放部分的代码在函数simp_blkdev_exit中实现的。具体的释


    放代码如下:


    del_gendisk(simp_blkdev_disk);
    在simp_blkdev_exit中同时还有put_disk(simp_blkdev_disk),这个是用来进行操作gendisk的引用计数


    。simp_blkdev_exit还实现了blk_cleanup_queue清除请求队列的这个函数。终于说到请求队列了。


     


    在说等待队列之前先要明确几个概念:


    ①用户希望对硬盘数据做的事情叫做请求,这个请求和IO请求是一样的,所以IO请求来自于上层。
    ②每一个IO请求对应内核中的一个bio结构。


    ③IO调度算法可以将连续的bio(也就是用户的对硬盘数据的相邻簇的请求)合并成一个request。
    ④多个request就是一个请求队列,这个请求队列的作用就是驱动程序响应用户的需求的队列。


     请求队列在国嵌的程序中的simp_blkdev_queue


    下面先说一下硬盘这类带有机械的存储设备的驱动。


    这类驱动中用户的IO请求对应于硬盘上的簇可能是连续的,可能是不连续的,连续的当然好,如果要是


    不连续的,那么IO调度器就会对这些BIO进行排序(例如老谢说的电梯调度算法),合并成一个request


    ,然后再接收请求,再合并成一个request,多个request之后那么我们的请求队列就形成了,然后就可


    以向驱动程序提交了。


    在硬盘这类的存储设备中,请求队列的初始化代码如下:


    simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
    老谢说,在这种情况下首先调用的是内核中的make_requst函数,然后再调用自己定义的


    simp_blkdev_do_request。追了一下内核代码,会发现make_requst内核代码如下所示:


    static int make_request(struct request_queue *q, struct bio * bio)
     


    具体的就不贴了,不过可以知道这个make_request的作用就是使用IO调度器对多个bio的访问顺序进行了


    优化调整合并为一个request。也就是在执行完成了这个函数之后才去正式的执行内核的请求队列。


    合并后的request其实还是一个结构,这个结构用来表征IO的请求,这个结构在内核中有具体的定义。


    请求是一个结构,同时请求队列也是一个结构,这个请求队列在内核中的结构定义如下:


    struct request_queue
    {
    /*
    * Together with queue_head for cacheline sharing
    */
    struct list_head queue_head;
    struct request *last_merge;
    struct elevator_queue *elevator;


    /*
    * the queue request freelist, one for reads and one for writes
    */
    struct request_list rq;


    request_fn_proc *request_fn;
    make_request_fn *make_request_fn;
    prep_rq_fn *prep_rq_fn;
    unplug_fn *unplug_fn;
    merge_bvec_fn *merge_bvec_fn;
    prepare_flush_fn *prepare_flush_fn;
    softirq_done_fn *softirq_done_fn;
    rq_timed_out_fn *rq_timed_out_fn;
    dma_drain_needed_fn *dma_drain_needed;
    lld_busy_fn *lld_busy_fn;


    /*
    * Dispatch queue sorting
    */
    sector_t end_sector;
    struct request *boundary_rq;


    /*
    * Auto-unplugging state
    */
    struct timer_list unplug_timer;
    int unplug_thresh; /* After this many requests */
    unsigned long unplug_delay; /* After this many jiffies */
    struct work_struct unplug_work;


    struct backing_dev_info backing_dev_info;


    /*
    * The queue owner gets to use this for whatever they like.
    * ll_rw_blk doesn't touch it.
    */
    void *queuedata;


    /*
    * queue needs bounce pages for pages above this limit
    */
    gfp_t bounce_gfp;


    /*
    * various queue flags, see QUEUE_* below
    */
    unsigned long queue_flags;


    /*
    * protects queue structures from reentrancy. ->__queue_lock should
    * _never_ be used directly, it is queue private. always use
    * ->queue_lock.
    */
    spinlock_t __queue_lock;
    spinlock_t *queue_lock;


    /*
    * queue kobject
    */
    struct kobject kobj;


    /*
    * queue settings
    */
    unsigned long nr_requests; /* Max # of requests */
    unsigned int nr_congestion_on;
    unsigned int nr_congestion_off;
    unsigned int nr_batching;


    void *dma_drain_buffer;
    unsigned int dma_drain_size;
    unsigned int dma_pad_mask;
    unsigned int dma_alignment;


    struct blk_queue_tag *queue_tags;
    struct list_head tag_busy_list;


    unsigned int nr_sorted;
    unsigned int in_flight[2];


    unsigned int rq_timeout;
    struct timer_list timeout;
    struct list_head timeout_list;


    struct queue_limits limits;


    /*
    * sg stuff
    */
    unsigned int sg_timeout;
    unsigned int sg_reserved_size;
    int node;
    #ifdef CONFIG_BLK_DEV_IO_TRACE
    struct blk_trace *blk_trace;
    #endif
    /*
    * reserved for flush operations
    */
    unsigned int ordered, next_ordered, ordseq;
    int orderr, ordcolor;
    struct request pre_flush_rq, bar_rq, post_flush_rq;
    struct request *orig_bar_rq;


    struct mutex sysfs_lock;


    #if defined(CONFIG_BLK_DEV_BSG)
    struct bsg_class_device bsg_dev;
    #endif
    };


    LDD说,请求队列实现了一个插入接口,这个接口允许使用多个IO调度器,大部分IO调度器批量累计IO请


    求,并将它们排列为递增或者递减的顺序提交给驱动。


    多个连续的bio会合并成为一个request,多个request就成为了一个请求队列,这样bio的是直接的也是


    最基本的请求,bio这个结构的定义如下:


    struct bio { sector_t            bi_sector;
           struct bio          *bi_next;    /* request queue link */
           struct block_device *bi_bdev; /* target device */
           unsigned long       bi_flags;    /* status, command, etc */ unsigned long       


    bi_rw;       /* low bits: r/w, high: priority */
           unsigned int bi_vcnt;     /* how may bio_vec's */
           unsigned int bi_idx; /* current index into bio_vec array */
           unsigned int bi_size;     /* total size in bytes */
           unsigned short bi_phys_segments; /* segments after physaddr coalesce*/ unsigned 


    short bi_hw_segments; /* segments after DMA remapping */ unsigned int bi_max;     /* max 


    bio_vecs we can hold used as index into pool */ struct bio_vec   *bi_io_vec;  /* the actual 


    vec list */
           bio_end_io_t *bi_end_io;  /* bi_end_io (bio) */
           atomic_t bi_cnt;     /* pin count: free when it hits zero */ void           


      *bi_private;
           bio_destructor_t *bi_destructor; /* bi_destructor (bio) */ };


    需要注意的是,在bio这个结构中最重要的是bio.vec这个结构。同时还有许多操作bio的宏,这些都是内


    核给实现好了的。


    请求队列的实现:


    首先使用 while ((req = elv_next_request(q)) != NULL)进行循环检测,看看到底传来的IO请求是个


    什么。


    然后进行读写区域的判定:


    if ((req->sector + req->current_nr_sectors) << 9
                            > SIMP_BLKDEV_BYTES) {
                            printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                    ": bad request: block=%llu, count=%u\n",
                                    (unsigned long long)req->sector,
                                    req->current_nr_sectors);
    //结束本次请求。
                            end_request(req, 0);
                            continue;
                    }
    在进行读写区域的判定的时候涉及到了很多linux的编程习惯。


    sector表示要访问的第一个扇区。


    current_nr_sectors表示预计访问扇区的数目。


    这里的左移九位其实就是乘以512。


    这样((req->sector + req->current_nr_sectors) << 9就计算出可以预计要访问的扇区的大小。进行了


    一次判断。


    如果上面的判断没有超出范围,那么就可以对请求的这一部分块设备进行操作了。


    simp_blkdev_disk = alloc_disk(1);
            if (!simp_blkdev_disk) {
                    ret = -ENOMEM;
                    goto err_alloc_disk;
            }


            strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
            simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
            simp_blkdev_disk->first_minor = 0;
            simp_blkdev_disk->fops = &simp_blkdev_fops;
            simp_blkdev_disk->queue = simp_blkdev_queue;
            set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
            add_disk(simp_blkdev_disk);


            return 0;
    关于gendisk结构的内存分配和成员的填充和硬盘类块设备是一样的。


    由于SD卡和U盘属于一类非机械类的设备,所以我们不需要那么复杂的调度算法,也就是不需要把io请求


    进行排序,所以我们需要自己为自己分配一个请求队列。具体代码如下:


     


    simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
    需要注意一下的是在硬盘类块设备的驱动中这个函数的原型是blk_init_queue


    (simp_blkdev_do_request, NULL);


    在这种情况下其实并没有调用内核的make_request(这个函数的功能上文说过),也就是说我们接下来


    要绑定的simp_blkdev_make_request这个函数和make_request是同一级别的函数。这里就没有涉及到算


    法调度的问题了。


    然后需要进行的工作是:绑定制造请求函数和请求队列。具体代码如下:


    blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
    把gendisk的结构成员都添加好了以后就可以执行add_disk(simp_blkdev_disk);这个函数把这块分区添


    加进内核了。


    整体列出制造请求部分的函数。


    //这个条件是在判断当前正在运行的内核版本。
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                    bio_endio(bio, 0, -EIO);
    #else
                    bio_endio(bio, -EIO);
    #endif
                    return 0;
            }


            dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);

    //遍历
            bio_for_each_segment(bvec, bio, i) {
                    void *iovec_mem;


                    switch (bio_rw(bio)) {
                    case READ:
                    case READA:
                            iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                            memcpy(iovec_mem, dsk_mem, bvec->bv_len);
                            kunmap(bvec->bv_page);
                            break;
                    case WRITE:
                            iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                            memcpy(dsk_mem, iovec_mem, bvec->bv_len);
                            kunmap(bvec->bv_page);
                            break;
                    default:
                            printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                    ": unknown value of bio_rw: %lu\n",
                                    bio_rw(bio));
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                            bio_endio(bio, 0, -EIO);
    #else
                            bio_endio(bio, -EIO);
    #endif
                            return 0;
                    }
                    dsk_mem += bvec->bv_len;
            }


    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
            bio_endio(bio, bio->bi_size, 0);
    #else
            bio_endio(bio, 0);
    #endif


            return 0;
    }
    ========

     Linux块设备驱动程序原理

     
    1.4  块设备驱动程序


    1.4.1  Linux块设备驱动程序原理(1)


    顾名思义,块设备驱动程序就是支持以块的方式进行读写的设备。块设备和字符设备最大的区别在于读


    写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的


    基本单元为字节。从实现角度来看,字符设备的实现比较简单,内核例程和用户态API一一对应,这种映


    射关系由字符设备的file_operations维护。块设备接口则相对复杂,读写API没有直接到块设备层,而


    是直接到文件系统层,然后再由文件系统层发起读写请求。


    block_device结构代表了内核中的一个块设备。它可以表示整个磁盘或一个特定的分区。当这个结构代


    表一个分区时,它的bd_contains成员指向包含这个分区的设备,bd_part成员指向设备的分区结构。当


    这个结构代表一个块设备时,bd_disk成员指向设备的gendisk结构。


    struct block_device {  
        dev_t           bd_dev;  
        struct inode *  bd_inode;   /*分区结点*/  
        int         bd_openers;  
        struct semaphore    bd_sem; /*打开/关闭锁*/  
        struct semaphore    bd_mount_sem;   /* 加载互斥锁*/  
        struct list_head    bd_inodes;  
        void *      bd_holder;  
        int         bd_holders;  
        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;  
    };  
    gendisk是一个单独的磁盘驱动器的内核表示。内核还使用gendisk来表示分区。
    struct gendisk {  
        int major;          //主设备号  
        int first_minor;     
        int minors;         //最大的次设备号数量,如果设备不能分区,该值为1                     


                  
        char disk_name[32]; //主设备名  
        struct hd_struct **part;    //分区信息,有minors个  
        struct block_device_operations *fops;//设备操作  
        struct request_queue *queue;    //设备管理I/O请求  
        void *private_data;  
        sector_t capacity;  
        int flags;  
        char devfs_name[64];  
        int number;  
        struct device *driverfs_dev;  
        struct kobject kobj;  
        struct timer_rand_state *random;  
        int policy;  
        atomic_t sync_io;     
        unsigned long stamp, stamp_idle;  
        int in_flight;  
    #ifdef  CONFIG_SMP  
        struct disk_stats *dkstats;  
    #else  
        struct disk_stats dkstats;  
    #endif  
    };  
    gendisk结构的操作函数包括以下几个:


    struct gendisk *alloc_disk(int minors);     //分配磁盘  
    void add_disk(struct gendisk *disk);        //增加磁盘信息  
    void unlink_gendisk(struct gendisk *disk)   //删除磁盘信息  
    void delete_partition(struct gendisk *disk, int part);  //删除分区  
    void add_partition(struct gendisk *disk, int part, sector_t start, sector_t len, int flags)


    ;//添加分区  
    1.4.1  Linux块设备驱动程序原理(2)


    block_device_operations结构是块设备对应的操作接口,是连接抽象的块设备操作与具体块设备操作之


    间的枢纽。


    struct block_device_operations {  
        int (*open) (struct inode *, struct file *);  
        int (*release) (struct inode *, struct file *);  
        int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);  
        long (*unlocked_ioctl) (struct file *, unsigned, unsigned long);  
        long (*compat_ioctl) (struct file *, unsigned, unsigned long);  
        int (*direct_access) (struct block_device *, sector_t, unsigned long *);  
        int (*media_changed) (struct gendisk *);  
        int (*revalidate_disk) (struct gendisk *);  
        int (*getgeo)(struct block_device *, struct hd_geometry *);  
        struct module *owner;  
    };  
    block_device_operations并不能完全提供文件操作全部的API,实际上只提供了open、release等函数,


    其他的文件操作依赖于def_blk_fops:
    const struct file_operations def_blk_fops = {  
        .open   = blkdev_open,  
        .release    = blkdev_close,  
        .llseek = block_llseek,  
        .read       = do_sync_read,  
        .write  = do_sync_write,  
        .aio_read   = generic_file_aio_read,  
        .aio_write= generic_file_aio_write_nolock,  
        .mmap   = generic_file_mmap,  
        .fsync  = block_fsync,  
        .unlocked_ioctl = block_ioctl,  
    #ifdef CONFIG_COMPAT  
        .compat_ioctl   = compat_blkdev_ioctl,  
    #endif  
        .splice_read        = generic_file_splice_read,  
        .splice_write   = generic_file_splice_write,  
    };  
    系统对块设备进行读写操作时,通过块设备通用的读写操作函数将一个请求保存在该设备的操作请求队


    列(request queue)中,然后调用这个块设备的底层处理函数,对请求队列中的操作请求进行逐一执行


    。request_queue结构描述了块设备的请求队列,该结构定义如下:
    struct request_queue  
    {  
        struct list_head    queue_head;  
        struct request      *last_merge;  
        elevator_t      elevator;  
        /*请求队列列表*/  
        struct request_list     rq;  
        request_fn_proc     *request_fn;  
        merge_request_fn    *back_merge_fn;  
        merge_request_fn    *front_merge_fn;  
        merge_requests_fn   *merge_requests_fn;  
        make_request_fn     *make_request_fn;  
        prep_rq_fn          *prep_rq_fn;  
        unplug_fn           *unplug_fn;  
        merge_bvec_fn       *merge_bvec_fn;  
        activity_fn         *activity_fn;  
        /*自动卸载状态*/  
        struct timer_list   unplug_timer;  
        int         unplug_thresh;    
        unsigned long       unplug_delay;   /*自动卸载延时*/  
        struct work_struct  unplug_work;  
        struct backing_dev_info backing_dev_info;  
        void                *queuedata;  
        void                *activity_data;  
        unsigned long       bounce_pfn;  
        int             bounce_gfp;  
        unsigned long       queue_flags;//各种队列标志  
        /*保护队列结构,避免重入*/  
        spinlock_t          *queue_lock;  
        /* 请求的核心结构*/  
        struct kobject kobj;  
        /*请求的配置*/  
        unsigned long       nr_requests;    /* 请求的最大数*/  
        unsigned int        nr_congestion_on;  
        unsigned int        nr_congestion_off;  
        unsigned short      max_sectors;  
        unsigned short      max_phys_segments;  
        unsigned short      max_hw_segments;  
        unsigned short      hardsect_size;  
        unsigned int        max_segment_size;  
        unsigned long       seg_boundary_mask;  
        unsigned int        dma_alignment;  
        struct blk_queue_tag    *queue_tags;  
        atomic_t        refcnt;  
        unsigned int        in_flight;  
        /*sg 参数配置*/  
        unsigned int        sg_timeout;  
        unsigned int        sg_reserved_size;  
    };  
    请求队列相关的处理函数包括:
    //创建队列时提供了一个自旋锁。  
    request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);  
    //获得队列中第一个未完成的请求。  
    struct request *elv_next_request(request_queue_t *q);  
    void end_request(struct request *req, int uptodate);//请求完成  
    void blk_stop_queue(request_queue_t *queue); //停止请求  
    void blk_start_queue(request_queue_t *queue); //开始请求  
    void blk_cleanup_queue(request_queue_t *);//清除请求队列 
    1.4.2  简单的块设备驱动程序实例


    向内核注册和注销一个块设备可使用如下函数:


    int register_blkdev(unsigned int major, const char *name);  
    int unregister_blkdev(unsigned int major, const char *name); 
    例1.10  简单的块设备驱动程序实例


    代码见光盘\src\1drivermodel\1-10block。核心代码如下所示:


    static struct request_queue *Queue;  
    //自定义块设备结构  
    static struct simpleblockdevice   
    {  
        unsigned long size;  
        spinlock_t lock;  
        u8 *data;  
        struct gendisk *gd;  
    } Device;  
    //处理I/O请求  
    static void simpleblocktransfer(struct simpleblockdevice *dev, unsigned long sector,  
                                    unsigned long nsect, char *buffer, int write)  
    {  
        unsigned long offset = sector*hardsect_size;  
        unsigned long nbytes = nsect*hardsect_size;  
        //判断I/O请求是否超出范围  
        if ((offset + nbytes) > dev->size)  
        {  
            printk (KERN_NOTICE "sbd: Beyond-end write (%ld %ld)\n", offset, nbytes);  
            return;  
        }  
        if (write)  
            memcpy(dev->data + offset, buffer, nbytes);  
        else  
            memcpy(buffer, dev->data + offset, nbytes);  
    }  
    //简单请求处理  
    static void simpleblockrequest(struct request_queue *q)  
    {  
        struct request *req;  
        //获取下一个请求  
        while ((req = elv_next_request(q)) != NULL)   
        {  
            if (! blk_fs_request(req))   
            {  
                printk (KERN_NOTICE "Skip non-CMD request\n");  
                end_request(req, 0);  
                continue;  
            }  
            simpleblocktransfer(&Device, req->sector, req->current_nr_sectors,  
                req->buffer, rq_data_dir(req));  
            end_request(req, 1);  
        }  
    }  
    //简单的块设备ioctl函数  
    int simpleblockioctl (struct inode *inode, struct file *filp,unsigned int cmd, unsigned 


    long arg)  
    {  
        long size;  
        struct hd_geometry geo;  
        switch(cmd)   
        {  
        //获取磁盘信息  
        case HDIO_GETGEO:  
            size = Device.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 *) arg, &geo, sizeof(geo)))  
                return -EFAULT;  
            return 0;  
        }  
        return -ENOTTY; /* 未知命令 */  
    }  
    //设备操作结构  
    static struct block_device_operations simpleblockops = {  
        .owner           = THIS_MODULE,  
        .ioctl       = simpleblockioctl 
    };  
    static int __init simpleblockinit(void)  
    {  
        Device.size = nsectors*hardsect_size;  
        spin_lock_init(&Device.lock);  
        Device.data = vmalloc(Device.size);  
        if (Device.data == NULL)  
            return -ENOMEM;  
        //初始化请求队列,配置处理函数为sbd_request  
        Queue = blk_init_queue(simpleblockrequest, &Device.lock);  
        if (Queue == NULL)  
            goto out;  
        blk_queue_hardsect_size(Queue, hardsect_size);  
        //注册块设备  
        major_num = register_blkdev(major_num, "sbd");  
        if (major_num <= 0) {  
            printk(KERN_WARNING "sbd: unable to get major number\n");  
            goto out;  
        }  
        Device.gd = alloc_disk(16);  
        if (! Device.gd)  
            goto out_unregister;  
        Device.gd->major = major_num;  
        Device.gd->first_minor = 0;  
        Device.gd->fops = &simpleblockops;  
        Device.gd->private_data = &Device;  
        strcpy (Device.gd->disk_name, "sbd0");  
        //配置容量  
        set_capacity(Device.gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));  
        Device.gd->queue = Queue;  
        add_disk(Device.gd);  
        return 0;  
    out_unregister:  
        unregister_blkdev(major_num, "sbd");  
    out:  
        vfree(Device.data);  
        return -ENOMEM;  
    }  
    static void __exit simpleblockexit(void)  
    {  
        del_gendisk(Device.gd);  
        put_disk(Device.gd);  
        unregister_blkdev(major_num, "sbd");  
        blk_cleanup_queue(Queue);  
        vfree(Device.data);  
    }  
    module_init(simpleblockinit);  
    module_exit(simpleblockexit);  
    运行结果如下:
    [root@/home]#cat /proc/filesystems  
    nodev   sysfs  
    nodev   rootfs  
    nodev   bdev  
    nodev   proc  
    nodev   binfmt_misc  
    nodev   debugfs  
    nodev   securityfs  
    nodev   sockfs  
    nodev   usbfs  
    nodev   pipefs  
    nodev   anon_inodefs  
    nodev   futexfs  
    nodev   tmpfs  
    nodev   inotifyfs  
            ext3  
            cramfs  
    nodev   ramfs  
            msdos  
            vfat  
            iso9660  
    nodev   nfs  
    nodev   nfs4  
    nodev   mqueue  
    nodev   rpc_pipefs  
    [root@/home]#insmod demo.ko  
     sbd0: unknown partition table  
    [root@/home]#mknod /dev/sbd b 253 0  
    [root@/home]#./mkfs.ext3 /dev/sbd  
    mke2fs 1.40.9 (27-Apr-2008)  
    Filesystem label=  
    OS type: Linux  
    Block size=1024 (log=0)  
    Fragment size=1024 (log=0)  
    1280 inodes, 5120 blocks  
    256 blocks (5.00%) reserved for the super user  
    First data block=1 
    Maximum filesystem blocks=5242880 
    1 block group  
    8192 blocks per group, 8192 fragments per group  
    1280 inodes per group  
     
    Writing inode tables: done  
    Creating journal (1024 blocks): done  
    Writing superblocks and filesystem accounting information: done  
     
    This filesystem will be automatically checked every 39 mounts or  
    180 days, whichever comes first.  Use tune2fs -c or -i to override.  
    [root@/home]#mount -t ext3 /dev/sbd /mnt/u  
    kjournald starting.  Commit interval 5 seconds  
    EXT3 FS on sbd0, internal journal  
    EXT3-fs: mounted filesystem with ordered data mode.  
    [root@/home]#df  
    Filesystem           1k-blocks      Used Available Use% Mounted on  
    rootfs                 2063504   1191136    767548  61% /  
    /dev/root              2063504   1191136    767548  61% /  
    /dev/sbd                  4955      1063      3636  23% /mnt/u  
    [root@/home]#cd /mnt/u  
    [root@/mnt/u]#ls  
    lost+found  
    ========
    展开全文
  • Linux 块设备驱动 (1)

    万次阅读 2016-06-04 18:16:12
    Sampleblk 是一个用于学习目的的 Linux 块设备驱动项目。其中 day1 的源代码实现了一个最简的块设备驱动,源代码只有 200 多行。本文主要围绕这些源代码,讨论 Linux 块设备驱动开发的基本知识。

    1. 背景

    Sampleblk 是一个用于学习目的的 Linux 块设备驱动项目。其中 day1 的源代码实现了一个最简的块设备驱动,源代码只有 200 多行。本文主要围绕这些源代码,讨论 Linux 块设备驱动开发的基本知识。

    开发 Linux 驱动需要做一系列的开发环境准备工作。Sampleblk 驱动是在 Linux 4.6.0 下开发和调试的。由于在不同 Linux 内核版本的通用 block 层的 API 有很大变化,这个驱动在其它内核版本编译可能会有问题。开发,编译,调试内核模块需要先准备内核开发环境,编译内核源代码。这些基础的内容互联网上随处可得,本文不再赘述。

    此外,开发 Linux 设备驱动的经典书籍当属 Device Drivers, Third Edition 简称 LDD3。该书籍是免费的,可以自由下载并按照其规定的 License 重新分发。

    2. 模块初始化和退出

    Linux 驱动模块的开发遵守 Linux 为模块开发者提供的基本框架和 API。LDD3 的 hello world 模块提供了写一个最简内核模块的例子。而 Sampleblk 块驱动的模块与之类似,实现了 Linux 内核模块所必需的模块初始化和退出函数,

    module_init(sampleblk_init);
    module_exit(sampleblk_exit);
    

    与 hello world 模块不同的是,Sampleblk 驱动的初始化和退出函数要实现一个块设备驱动程序所必需的基本功能。本节主要针对这部分内容做详细说明。

    2.1 sampleblk_init

    归纳起来,sampleblk_init 函数为完成块设备驱动的初始化,主要做了以下几件事情,

    2.1.1 块设备注册

    调用 register_blkdev 完成 major number 的分配和注册,函数原型如下,

    int register_blkdev(unsigned int major, const char *name);
    

    Linux 内核为块设备驱动维护了一个全局哈希表 major_names这个哈希表的 bucket 是 [0..255] 的整数索引的指向 blk_major_name 的结构指针数组。

    static struct blk_major_name {
        struct blk_major_name *next;
        int major;
        char name[16];
    } *major_names[BLKDEV_MAJOR_HASH_SIZE];
    

    register_blkdevmajor 参数不为 0 时,其实现就尝试在这个哈希表中寻找指定的 major 对应的 bucket 里的空闲指针,分配一个新的blk_major_name,按照指定参数初始化 majorname。假如指定的 major 已经被别人占用(指针非空),则表示 major 号冲突,反回错误。

    major 参数为 0 时,则由内核从 [1..255] 的整数范围内分配一个未使用的反回给调用者。因此,虽然 Linux 内核的主设备号 (Major Number) 是 12 位的,不指定 major 时,仍旧从 [1..255] 范围内分配。

    Sampleblk 驱动通过指定 major 为 0,让内核为其分配和注册一个未使用的主设备号,其代码如下,

    sampleblk_major = register_blkdev(0, "sampleblk");
    if (sampleblk_major < 0)
        return sampleblk_major;
    
    2.1.2 驱动状态数据结构的分配和初始化

    通常,所有 Linux 内核驱动都会声明一个数据结构来存储驱动需要频繁访问的状态信息。这里,我们为 Sampleblk 驱动也声明了一个,

    struct sampleblk_dev {
        int minor;
        spinlock_t lock;
        struct request_queue *queue;
        struct gendisk *disk;
        ssize_t size;
        void *data;
    };
    

    为了简化实现和方便调试,Sampleblk 驱动暂时只支持一个 minor 设备号,并且可以用以下全局变量访问,

    struct sampleblk_dev *sampleblk_dev = NULL;
    

    下面的代码分配了 sampleblk_dev 结构,并且给结构的成员做了初始化,

    sampleblk_dev = kzalloc(sizeof(struct sampleblk_dev), GFP_KERNEL);
    if (!sampleblk_dev) {
        rv = -ENOMEM;
        goto fail;
    }
    
    sampleblk_dev->size = sampleblk_sect_size * sampleblk_nsects;
    sampleblk_dev->data = vmalloc(sampleblk_dev->size);
    if (!sampleblk_dev->data) {
        rv = -ENOMEM;
        goto fail_dev;
    }
    sampleblk_dev->minor = minor;
    
    2.1.3 Request Queue 初始化

    使用 blk_init_queue 初始化 Request Queue 需要先声明一个所谓的策略 (Strategy) 回调和保护该 Request Queue 的自旋锁。然后将该策略回调的函数指针和自旋锁指针做为参数传递给该函数。

    在 Sampleblk 驱动里,就是 sampleblk_request 函数和 sampleblk_dev->lock

    spin_lock_init(&sampleblk_dev->lock);
    sampleblk_dev->queue = blk_init_queue(sampleblk_request,
        &sampleblk_dev->lock);
    if (!sampleblk_dev->queue) {
        rv = -ENOMEM;
        goto fail_data;
    }
    

    策略函数 sampleblk_request 用于执行块设备的 read 和 write IO 操作,其主要的入口参数就是 Request Queue 结构:struct request_queue。关于策略函数的具体实现我们稍后介绍。

    当执行 blk_init_queue 时,其内部实现会做如下的处理,

    1. 从内存中分配一个 struct request_queue 结构。
    2. 初始化 struct request_queue 结构。对调用者来说,其中以下部分的初始化格外重要,
      • blk_init_queue 指定的策略函数指针会赋值给 struct request_queuerequest_fn 成员。
      • blk_init_queue 指定的自旋锁指针会赋值给 struct request_queuequeue_lock 成员。
      • 与这个request_queue 关联的 IO 调度器的初始化。

    Linux 内核提供了多种分配和初始化 Request Queue 的方法,

    • blk_mq_init_queue 主要用于使用多队列技术的块设备驱动
    • blk_alloc_queueblk_queue_make_request 主要用于绕开内核支持的 IO 调度器的合并和排序,使用自定义的实现。
    • blk_init_queue 则使用内核支持的 IO 调度器,驱动只专注于策略函数的实现。

    Sampleblk 驱动属于第三种情况。这里再次强调一下:如果块设备驱动需要使用标准的 IO 调度器对 IO 请求进行合并或者排序时,必需使用 blk_init_queue 来分配和初始化 Request Queue.

    2.1.4 块设备操作函数表初始化

    Linux 的块设备操作函数表 block_device_operations 定义在 include/linux/blkdev.h 文件中。块设备驱动可以通过定义这个操作函数表来实现对标准块设备驱动操作函数的定制。
    如果驱动没有实现这个操作表定义的方法,Linux 块设备层的代码也会按照块设备公共层的代码缺省的行为工作。

    Sampleblk 驱动虽然声明了自己的 open, release, ioctl 方法,但这些方法对应的驱动函数内都没有做实质工作。因此实际的块设备操作时的行为是由块设备公共层来实现的,

    static const struct block_device_operations sampleblk_fops = {
        .owner = THIS_MODULE,
        .open = sampleblk_open,
        .release = sampleblk_release,
        .ioctl = sampleblk_ioctl,
    };
    
    2.1.5 磁盘创建和初始化

    Linux 内核使用 struct gendisk 来抽象和表示一个磁盘。也就是说,块设备驱动要支持正常的块设备操作,必需分配和初始化一个 struct gendisk

    首先,使用 alloc_disk 分配一个 struct gendisk

    disk = alloc_disk(minor);
    if (!disk) {
        rv = -ENOMEM;
        goto fail_queue;
    }
    sampleblk_dev->disk = disk;
    

    然后,初始化 struct gendisk 的重要成员,尤其是块设备操作函数表,Rquest Queue,和容量设置。最终调用 add_disk 来让磁盘在系统内可见,触发磁盘热插拔的 uevent。

    disk->major = sampleblk_major;
    disk->first_minor = minor;
    disk->fops = &sampleblk_fops;
    disk->private_data = sampleblk_dev;
    disk->queue = sampleblk_dev->queue;
    sprintf(disk->disk_name, "sampleblk%d", minor);
    set_capacity(disk, sampleblk_nsects);
    add_disk(disk);
    

    2.2 sampleblk_exit

    这是个 sampleblk_init 的逆过程,

    • 删除磁盘

      del_gendiskadd_disk 的逆过程,让磁盘在系统中不再可见,触发热插拔 uevent。

      del_gendisk(sampleblk_dev->disk);

    • 停止并释放块设备 IO 请求队列

      blk_cleanup_queueblk_init_queue 的逆过程,但其在释放 struct request_queue 之前,要把待处理的 IO 请求都处理掉。

      blk_cleanup_queue(sampleblk_dev->queue);

      blk_cleanup_queue 把所有 IO 请求全部处理完时,会标记这个队列马上要被释放,这样可以阻止 blk_run_queue 继续调用块驱动的策略函数,继续执行 IO 请求。Linux 3.8 之前,内核在 blk_run_queueblk_cleanup_queue 同时执行时有严重 bug。最近在一个有磁盘 IO 时的 Surprise Remove 的压力测试中发现了这个 bug (老实说,有些惊讶,这个 bug 存在这么久一直没人发现)。

    • 释放磁盘

      put_diskalloc_disk 的逆过程。这里 gendisk 对应的 kobject 引用计数变为零,彻底释放掉 gendisk

      put_disk(sampleblk_dev->disk);

    • 释放数据区

      vfreevmalloc 的逆过程。

      vfree(sampleblk_dev->data);

    • 释放驱动全局数据结构。

      freekzalloc 的逆过程。

      kfree(sampleblk_dev);

    • 注销块设备。

      unregister_blkdevregister_blkdev 的逆过程。

      unregister_blkdev(sampleblk_major, “sampleblk”);

    3. 策略函数实现

    理解块设备驱动的策略函数实现,必需先对 Linux IO 栈的关键数据结构有所了解。

    3.1 struct request_queue

    块设备驱动待处理的 IO 请求队列结构。如果该队列是利用blk_init_queue 分配和初始化的,则该队里内的 IO 请求( struct request )需要经过 IO 调度器的处理(排序或合并),由 blk_queue_bio 触发。

    当块设备策略驱动函数被调用时,request 是通过其 queuelist 成员链接在 struct request_queuequeue_head 链表里的。一个 IO 申请队列上会有很多个 request 结构。

    3.2 struct bio

    一个 bio 逻辑上代表了上层某个任务对通用块设备层发起的 IO 请求。来自不同应用,不同上下文的,不同线程的 IO 请求在块设备驱动层被封装成不同的 bio 数据结构。

    同一个 bio 结构的数据是由块设备上从起始扇区开始的物理连续扇区组成的。由于在块设备上连续的物理扇区在内存中无法保证是物理内存连续的,因此才有了段 (Segment)的概念。在 Segment 内部的块设备的扇区是物理内存连续的,但 Segment 之间却不能保证物理内存的连续性。Segment 长度不会超过内存页大小,而且总是扇区大小的整数倍。

    下图清晰的展现了扇区 (Sector),块 (Block) 和段 (Segment) 在内存页 (Page) 内部的布局,以及它们之间的关系(注:图截取自 Understand Linux Kernel 第三版,版权归原作者所有),

    Segment block sector layout in a page

    因此,一个 Segment 可以用 [page, offset, len] 来唯一确定。一个 bio 结构可以包含多个 Segment。而 bio 结构通过指向 Segment 的指针数组来表示了这种一对多关系。

    struct bio 中,成员 bi_io_vec 就是前文所述的“指向 Segment 的指针数组” 的基地址,而每个数组的元素就是指向 struct bio_vec 的指针。

    struct bio {
    
        [...snipped..]
    
        struct bio_vec      *bi_io_vec; /* the actual vec list */
    
        [...snipped..]
    }
    

    struct bio_vec 就是描述一个 Segment 的数据结构,

    struct bio_vec {
        struct page *bv_page;       /* Segment 所在的物理页的 struct page 结构指针 */
        unsigned int    bv_len;     /* Segment 长度,扇区整数倍 */
        unsigned int    bv_offset;  /* Segment 在物理页内起始的偏移地址 */
    };
    

    struct bio 中的另一个成员 bi_vcnt 用来描述这个 bio 里有多少个 Segment,即指针数组的元素个数。一个 bio 最多包含的 Segment/Page 数是由如下内核宏定义决定的,

    #define BIO_MAX_PAGES       256
    

    多个 bio 结构可以通过成员 bi_next 链接成一个链表。bio 链表可以是某个做 IO 的任务 task_struct 成员 bio_list 所维护的一个链表。也可以是某个 struct request 所属的一个链表(下节内容)。

    下图展现了 bio 结构通过 bi_next 链接组成的链表。其中的每个 bio 结构和 Segment/Page 存在一对多关系 (注:图截取自 Professional Linux Kernel Architecture,版权归原作者所有),

    bio list and page vectors

    3.3 struct request

    一个 request 逻辑上代表了块设备驱动层收到的 IO 请求。该 IO 请求的数据在块设备上是从起始扇区开始的物理连续扇区组成的。

    struct request 里可以包含很多个 struct bio,主要是通过 bio 结构的 bi_next 链接成一个链表。这个链表的第一个 bio 结构,则由 struct requestbio 成员指向。
    而链表的尾部则由 biotail 成员指向。

    通用块设备层接收到的来自不同线程的 bio 后,通常根据情况选择如下两种方案之一,

    • bio 合并入已有的 request

      blk_queue_bio 会调用 IO 调度器做 IO 的合并 (merge)。多个 bio 可能因此被合并到同一个 request 结构里,组成一个 request 结构内部的 bio 结构链表。由于每个 bio 结构都来自不同的任务,因此 IO 请求合并只能在 request 结构层面通过链表插入排序完成,原有的 bio 结构内部不会被修改。

    • 分配新的 request

      如果 bio 不能被合并到已有的 request 里,通用块设备层就会为这个 bio 构造一个新 request 然后插入到 IO 调度器内部的队列里。待上层任务通过 blk_finish_plug 来触发 blk_run_queue 动作,块设备驱动的策略函数 request_fn 会触发 IO 调度器的排序操作,将 request 排序插入块设备驱动的 IO 请求队列。

    不论以上哪种情况,通用块设备的代码将会调用块驱动程序注册在 request_queuerequest_fn 回调,这个回调里最终会将合并或者排序后的 request 交由驱动的底层函数来做 IO 操作。

    3.4 策略函数 request_fn

    如前所述,当块设备驱动使用 blk_run_queue 来分配和初始化 request_queue 时,这个函数也需要驱动指定自定义的策略函数 request_fn 和所需的自旋锁 queue_lock。驱动实现自己的 request_fn 时,需要了解如下特点,

    • 当通用块层代码调用 request_fn 时,内核已经拿了这个 request_queuequeue_lock。因此,此时的上下文是 atomic 上下文。在驱动的策略函数退出 queue_lock 之前,需要遵守内核在 atomic 上下文的约束条件。

    • 进入驱动策略函数时,通用块设备层代码可能会同时访问 request_queue。为了减少在 request_queuequeue_lock 上的锁竞争, 块驱动策略函数应该尽早退出 queue_lock,然后在策略函数返回前重新拿到锁。

    • 策略函数是异步执行的,不处在用户态进程所对应的内核上下文。因此实现时不能假设策略函数运行在用户进程的内核上下文中。

    Sampleblk 的策略函数是 sampleblk_request,通过 blk_init_queue 注册到了 request_queuerequest_fn 成员上。

    static void sampleblk_request(struct request_queue *q)
    {
        struct request *rq = NULL;
        int rv = 0;
        uint64_t pos = 0;
        ssize_t size = 0;
        struct bio_vec bvec;
        struct req_iterator iter;
        void *kaddr = NULL;
    
        while ((rq = blk_fetch_request(q)) != NULL) {
            spin_unlock_irq(q->queue_lock);
    
            if (rq->cmd_type != REQ_TYPE_FS) {
                rv = -EIO;
                goto skip;
            }
    
            BUG_ON(sampleblk_dev != rq->rq_disk->private_data);
    
            pos = blk_rq_pos(rq) * sampleblk_sect_size;
            size = blk_rq_bytes(rq);
            if ((pos + size > sampleblk_dev->size)) {
                pr_crit("sampleblk: Beyond-end write (%llu %zx)\n", pos, size);
                rv = -EIO;
                goto skip;
            }
    
            rq_for_each_segment(bvec, rq, iter) {
                kaddr = kmap(bvec.bv_page);
    
                rv = sampleblk_handle_io(sampleblk_dev,
                    pos, bvec.bv_len, kaddr + bvec.bv_offset, rq_data_dir(rq));
                if (rv < 0)
                    goto skip;
    
                pos += bvec.bv_len;
                kunmap(bvec.bv_page);
            }
    skip:
    
            blk_end_request_all(rq, rv);
    
            spin_lock_irq(q->queue_lock);
        }
    }
    

    策略函数 sampleblk_request 的实现逻辑如下,

    1. 使用 blk_fetch_request 循环获取队列中每一个待处理 request
      内核函数 blk_fetch_request 可以返回 struct request_queuequeue_head 队列的第一个 request 的指针。然后再调用 blk_dequeue_request 从队列里摘除这个 request
    2. 每拿到一个 request,立即退出锁 queue_lock,但处理完每个 request,需要再次获得 queue_lock
    3. REQ_TYPE_FS 用来检查是否是一个来自文件系统的 request。本驱动不支持非文件系统 request
    4. blk_rq_pos 可以返回 request 的起始扇区号,而 blk_rq_bytes 返回整个 request 的字节数,应该是扇区的整数倍。
    5. rq_for_each_segment 这个宏定义用来循环迭代遍历一个 request 里的每一个 Segment: 即 struct bio_vec。注意,每个 Segment 即 bio_vec 都是以 blk_rq_pos 为起始扇区,物理扇区连续的的。Segment 之间只是物理内存不保证连续而已。
    6. 每一个 struct bio_vec 都可以利用 kmap 来获得这个 Segment 所在页的虚拟地址。利用 bv_offsetbv_len 可以进一步知道这个 segment 的确切页内偏移和具体长度。
    7. rq_data_dir 可以获知这个 request 的请求是 read 还是 write。
    8. 处理完毕该 request 之后,必需调用 blk_end_request_all 让块通用层代码做后续处理。

    驱动函数 sampleblk_handle_io 把一个 request的每个 segment 都做一次驱动层面的 IO 操作。调用该驱动函数前,起始扇区地址 pos长度 bv_len, 起始扇区虚拟内存地址 kaddr + bvec.bv_offset,和 read/write 都做为参数准备好。由于 Sampleblk 驱动只是一个 ramdisk 驱动,因此,每个 segment 的 IO 操作都是 memcpy 来实现的,

    /*
     * Do an I/O operation for each segment
     */
    static int sampleblk_handle_io(struct sampleblk_dev *sampleblk_dev,
            uint64_t pos, ssize_t size, void *buffer, int write)
    {
        if (write)
            memcpy(sampleblk_dev->data + pos, buffer, size);
        else
            memcpy(buffer, sampleblk_dev->data + pos, size);
    
        return 0;
    }
    

    4. 试验

    4.1 编译和加载

    • 首先,需要下载内核源代码,编译和安装内核,用新内核启动。

      由于本驱动是在 Linux 4.6.0 上开发和调试的,而且块设备驱动内核函数不同内核版本变动很大,最好去下载 Linux mainline 源代码,然后 git checkout 到版本 4.6.0 上编译内核。编译和安装内核的具体步骤网上有很多介绍,这里请读者自行解决。

    • 编译好内核后,在内核目录,编译驱动模块。

      $ make M=/ws/lktm/drivers/block/sampleblk/day1

    • 驱动编译成功,加载内核模块

      $ sudo insmod /ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko

    • 驱动加载成功后,使用 crash 工具,可以查看 struct smapleblk_dev 的内容,

      crash7> mod -s sampleblk /home/yango/ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko
      MODULE NAME SIZE OBJECT FILE
      ffffffffa03bb580 sampleblk 2681 /home/yango/ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko

      crash7> p *sampleblk_dev
      $4 = {
      minor = 1,
      lock = {
      {
      rlock = {
      raw_lock = {
      val = {
      counter = 0
      }
      }
      }
      }
      },
      queue = 0xffff880034ef9200,
      disk = 0xffff880000887000,
      size = 524288,
      data = 0xffffc90001a5c000
      }

    注:关于 Linux Crash 的使用,请参考延伸阅读。

    4.2 模块引用问题解决

    问题:把驱动的 sampleblk_request 函数实现全部删除,重新编译和加载内核模块。然后用 rmmod 卸载模块,卸载会失败, 内核报告模块正在被使用。

    使用 strace 可以观察到 /sys/module/sampleblk/refcnt 非零,即模块正在被使用。

    $ strace rmmod sampleblk
    execve("/usr/sbin/rmmod", ["rmmod", "sampleblk"], [/* 26 vars */]) = 0
    
    ................[snipped]..........................
    
    openat(AT_FDCWD, "/sys/module/sampleblk/holders", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
    getdents(3, /* 2 entries */, 32768)     = 48
    getdents(3, /* 0 entries */, 32768)     = 0
    close(3)                                = 0
    open("/sys/module/sampleblk/refcnt", O_RDONLY|O_CLOEXEC) = 3    /* 显示引用数为 3 */
    read(3, "1\n", 31)                      = 2
    read(3, "", 29)                         = 0
    close(3)                                = 0
    write(2, "rmmod: ERROR: Module sampleblk i"..., 41rmmod: ERROR: Module sampleblk is in use
    ) = 41
    exit_group(1)                           = ?
    +++ exited with 1 +++
    

    如果用 lsmod 命令查看,可以看到模块的引用计数确实是 3,但没有显示引用者的名字。一般情况下,只有内核模块间的相互引用才有引用模块的名字,所以没有引用者的名字,那么引用者来自用户空间的进程。

    那么,究竟是谁在使用 sampleblk 这个刚刚加载的驱动呢?利用 module:module_get tracepoint,就可以得到答案了。重新启动内核,在加载模块前,运行 tpoint 命令。然后,再运行 insmod 来加载模块。

    $ sudo ./tpoint module:module_get
    Tracing module:module_get. Ctrl-C to end.
    
       systemd-udevd-2986  [000] ....   196.382796: module_get: sampleblk call_site=get_disk refcnt=2
       systemd-udevd-2986  [000] ....   196.383071: module_get: sampleblk call_site=get_disk refcnt=3
    

    可以看到,原来是 systemd 的 udevd 进程在使用 sampleblk 设备。如果熟悉 udevd 的人可能就会立即恍然大悟,因为 udevd 负责侦听系统中所有设备的热插拔事件,并负责根据预定义规则来对新设备执行一系列操作。而 sampleblk 驱动在调用 add_disk 时,kobject 层的代码会向用户态的 udevd 发送热插拔的 uevent,因此 udevd 会打开块设备,做相关的操作。
    利用 crash 命令,可以很容易找到是哪个进程在打开 sampleblk 设备,

    crash> foreach files -R /dev/sampleblk
    PID: 4084   TASK: ffff88000684d700  CPU: 0   COMMAND: "systemd-udevd"
    ROOT: /    CWD: /
     FD       FILE            DENTRY           INODE       TYPE PATH
      8 ffff88000691ad00 ffff88001ffc0600 ffff8800391ada08 BLK  /dev/sampleblk1
      9 ffff880006918e00 ffff88001ffc0600 ffff8800391ada08 BLK  /dev/sampleblk1
    

    由于 sampleblk_request 函数实现被删除,则 udevd 发送的 IO 操作无法被 sampleblk 设备驱动完成,因此 udevd 陷入到长期的阻塞等待中,直到超时返回错误,释放设备。上述分析可以从系统的消息日志中被证实,

    messages:Apr 23 03:11:51 localhost systemd-udevd: worker [2466] /devices/virtual/block/sampleblk1 is taking a long time
    messages:Apr 23 03:12:02 localhost systemd-udevd: worker [2466] /devices/virtual/block/sampleblk1 timeout; kill it
    messages:Apr 23 03:12:02 localhost systemd-udevd: seq 4313 '/devices/virtual/block/sampleblk1' killed
    

    注:tpoint 是一个基于 ftrace 的开源的 bash 脚本工具,可以直接下载运行使用。它是 Brendan Gregg 在 github 上的开源项目,前文已经给出了项目的链接。

    重新把删除的 sampleblk_request 函数源码加回去,则这个问题就不会存在。因为 udevd 可以很快结束对 sampleblk 设备的访问。

    4.3 创建文件系统

    虽然 Sampleblk 块驱动只有 200 行源码,但已经可以当作 ramdisk 来使用,在其上可以创建文件系统,

    $ sudo mkfs.ext4 /dev/sampleblk1
    

    文件系统创建成功后,mount 文件系统,并创建一个空文件 a。可以看到,都可以正常运行。

    $sudo mount /dev/sampleblk1 /mnt
    $touch a
    

    至此,sampleblk 做为 ramdisk 的最基本功能已经实验完毕。

    5. 延伸阅读

    展开全文
  • Linux 块设备驱动 (3)

    千次阅读 2016-11-12 16:26:48
    本文将继续之前系列文章中的实验,围绕这个简单的 fio 测试,探究 Linux 块设备驱动和文件 IO 的运作机制。除非特别指明,本文中所有 Linux 内核源码引用都基于 4.6.0。其它内核版本可能会有较大差异。
  • Linux块设备驱动程序原理

    千次阅读 2012-11-20 10:19:33
    1.4.1 Linux块设备驱动程序原理(1) 顾名思义,块设备驱动程序就是支持以块的方式进行读写的设备。块设备和字符设备最大的区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个...
  • linux块设备驱动之实例

    千次阅读 2015-07-13 00:39:18
    前两篇blog已经基本熟悉了块设备的相关结构,这里来总结下ldd3中提到的一些块设备驱动例题; 注册: sbull_major = register_blkdev(sbull_major, "sbull"); if (0 >= sbull_major){ printk(KERN_WARNING "sbull:...
  • Linux块设备驱动之内存模拟块设备

    千次阅读 2015-10-06 15:44:04
    今天我们开始接触块设备驱动程序了,一下程序的各个模块我讲的很详细,想头文件这些我没有加上来,因为不同 内核版本头文件有可能不一样,驱动的程序的狂街我在下面写出来了,希望给大家带来帮助。   用内存代替块...
  • Linux块设备驱动(一) _驱动模型

    千次阅读 2017-03-21 21:10:04
    块设备是Linux三大设备之一,其驱动模型主要针对磁盘,Flash等存储类设备,本文以3.14为蓝本,探讨内核中的块设备驱动模型 框架 下图是Linux中的块设备模型示意图,应用层程序有两种方式访问一个块设备:/dev和...
  • Linux块设备驱动(一)gendisk结构体

    千次阅读 2018-10-15 22:28:33
    Linux内核中,用gendisk结构体表示一个磁盘设备或分区,块设备驱动程序的设计主要就是围绕gendisk这个数据结构展开的。一个简单的块设备驱动程序框架如下: 在init函数中分配、设置、添加一个gendisk; 设计gendisk...
  • Linux块设备驱动之NOR FLASH

    千次阅读 2015-10-07 11:36:45
    块设备驱动之NOR FLASH /*  * 参考 drivers\mtd\maps\physmap.c  */   static struct map_info *s3c_nor_map; static struct mtd_info *s3c_nor_mtd; static struct mtd_partition s3c_nor_parts[] = { [0] = {
  • 通用块层是块设备驱动的核心部分,这部分主要包含块设备驱动程序的通过代码部分。 1、通用块层 通用块层是一个内核组件,他处理来自系统其他组件发出的块设备请求。换句话说,通用块层包含了块设备操作的一些...
  • 前一篇blog中已经贴出这些相关的结构体,但是...其中struct gendisk 表示的是一个块设备,而struct block_device表示是一个分区设备(也可以表示块设备),struct hd_struct表示的是一个分区信息;它们的关系如下面。
  • <linux 块设备驱动架构图>  a:架构分析 1)struct bio ------当一个进程被Read时,首先读取cache 中有没有相应的文件,这个cache由一个buffer_head结构读取。如果没有,文件系统就会利用块设备驱动去...
  • 块设备的结构及磁盘的结构 1、扇区 磁盘上的每个磁道被等分成若干个弧段,这些弧段便是磁盘的扇区。磁盘驱动器在向磁盘读写数据时,都是以扇区为单位。一般为512个字节,但也有1024或者2048个字节的。注意,即使...
  • Linux块设备驱动的模块加载与卸载

    千次阅读 2013-12-18 18:01:27
    块设备驱动的模块家在函数中通常需要完成如下工作: 1. 分配、初始化请求队列,绑定请求队列和请求函数。 2. 分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。 3. 注册块设备...
  • 块设备的 I/O 操作特点 字符设备与块设备 I/O 操作的不同如下。 (1)块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。大多数设备是字符 设备,因为它们不需要缓冲而且不以固定块大小进行...
  • Linux 内核中,使用gendisk(通用磁盘)结构体来表示1 个独立的磁盘设备(或分区)。 gendisk 结构体 1 struct gendisk 2 { 3 int major; /* 主设备号 */ 4 int first_minor; /*第1 个次设备号*/ 5 int ...
  • 块设备驱动之二(从用户空间的read、write到实际设备物理操作整体架构分析)linux驱动由浅入深系列:块设备驱动之三(块设备驱动结构分析,以mmc为例)前一篇文章介绍了块设备驱动linux框架张的位置关系,本文来...
  • Linux块设备驱动程序分析

    千次阅读 2014-07-04 10:39:11
    基于《Linux设备驱动程序》书中的sbull程序以对Linux块设备驱动总结分析。 开始之前先来了解这个块设备中的核心数据结构: struct sbull_dev {  int size; /* Device size in sectors */  u8 *data; 
  • 本课程是linux驱动开发的第10个课程,主要内容是linux块设备驱动的介绍,首先详细讲了块设备驱动和字符设备驱动的核心差异,然后以一个内存模拟的块设备驱动源码为案例演示了块设备驱动如何使用,后对源码进行了...
  • linux 块设备驱动管理程序 理解块设备访问请求管理及其驱动程序 main.c ll_rw_block.c hd.c ramdisk.c floppy.c hdreg.h fdreg.h system_call.s
  • 块设备驱动程序
  • 块设备驱动程序
  • Linux块设备方式的nvram驱动实例

    千次阅读 2017-03-18 11:55:03
    一般可以通过字符设备驱动的方式访问nvram,在这里,通过块设备驱动方式去驱动nvram,以了解和熟悉块设备驱动编写。
  • 参考文章: 写一个块设备驱动 参考文章中有一些函数在如今的内核版本中已经被删除了,下面写的是调试好的不适用请求队列的简单块设备驱动。 上传了完整项目文档说明和代码:操作系统课程设计-简单字符设备和...
  • Linux2.6块设备驱动程序

    千次阅读 2012-06-09 16:21:57
    VFS和块设备的关系见Linux2.6设备管理 系统调用的服务例程调用一个合适的VFS函数,将文件描述符和文件内的偏移量传递给它。 2.磁盘高速缓存 VFS函数确定所请求的数据是否已经存在磁盘高速缓存中,若存在,就...
  • Linux设备驱动工程师之路之——块设备驱动 K-Style 转载请注明来自于衡阳师范学院08电2 K-Style http://blog.csdn.net/ayangke,QQ:843308498 邮箱:yangkeemail@qq.com   一、重要知识点 1.块设备和字符设备...
  • 增加一个驱动程序(使用内存模拟设备),使用模块编译方式 动态加载和卸载新的驱动 通过程序或命令行使用该驱动。 (至少能通过该驱动保存1MB的数据,还能将这些数据读取出来。可以模仿ramdisk的实现方式。) 1....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 92,376
精华内容 36,950
关键字:

linux块设备驱动

linux 订阅