linux块设备驱动简介和实例

1、块设备是linux3大设备之一。其驱动模型主要针对磁盘,Flash等存储类设备。

2、块设备为什么要缓存?

     针对带磁头设备,需读取扇区,分布可能随机。需优化读取顺序,减小磁头机械运转次数。

     针对不带磁头设备,暂时先扇区(或者块)读取到ram中缓存,只修改ram中相应位置数据,在写回到块设备,减小擦写次数。

3、框架

linux块设备驱动简介和实例_第1张图片

  1. read()系统调用最终会调用一个适当的VFS函数(read()-->sys_read()-->vfs_read()),将文件描述符fd和文件内的偏移量offset传递给它。
  2. VFS会判断这个SCI的处理方式,如果访问的内容已经被缓存在RAM中(磁盘高速缓存机制),就直接访问,否则从磁盘中读取
  3. 为了从物理磁盘中读取,内核依赖映射层mapping layer,即上图中的磁盘文件系统
    1. 确定该文件所在文件系统的块的大小,并根据文件块的大小计算所请求数据的长度。本质上,文件被拆成很多块,因此内核需要确定请求数据所在的块
    2. 映射层调用一个具体的文件系统的函数,这个层的函数会访问文件的磁盘节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。
  4. 内核利用通用块层(generic block layer)启动IO操作来传达所请求的数据,通常,一个IO操作只针对磁盘上一组连续的块。
  5. IO调度程序根据预先定义的内核策略将待处理的IO进行重排和合并
  6. 块设备驱动程序向磁盘控制器硬件接口发送适当的指令,进行实际的数据操作

4、函数调用流程

linux块设备驱动简介和实例_第2张图片

linux块设备驱动简介和实例_第3张图片

4、重要数据结构

bio结构简介

bio结构就是kernel中block I/O的基础容器,它是在”linux/bio.h”中被定义的。这个结构将当前激活的block I/O操作表示为一个segment的list。segment就是内存中一块连续的buffer。因此,个别的buffer在内存中不需要连续。为了使buffer可以组成块,bio结构可以将内存多处组成一个buffer,并使kernel在该buffer上进行block I/O操作。这样的I/O被称为scatter-gather I/O。

struct bio {
    sector_t             bi_sector;         /* associated sector on disk */
    struct bio           *bi_next;          /* list of requests */
    struct block_device  *bi_bdev;          /* associated block device */
    unsigned long        bi_flags;          /* status and command flags */
    unsigned long        bi_rw;             /* read or write? */
    unsigned short       bi_vcnt;           /* number of bio_vecs off */
    unsigned short       bi_idx;            /* current index in bi_io_vec */
    unsigned short       bi_phys_segments;  /* number of segments after coalescing */
    unsigned short       bi_hw_segments;    /* number of segments after remapping */
    unsigned int         bi_size;           /* I/O count */
    unsigned int         bi_hw_front_size;  /* size of the first mergeable segment */
    unsigned int         bi_hw_back_size;   /* size of the last mergeable segment */
    unsigned int         bi_max_vecs;       /* maximum bio_vecs possible */
    struct bio_vec       *bi_io_vec;        /* bio_vec list */
    bio_end_io_t         *bi_end_io;        /* I/O completion method */
    atomic_t             bi_cnt;            /* usage counter */
    void                 *bi_private;       /* owner-private method */
    bio_destructor_t     *bi_destructor;    /* destructor method */
};

bio结构的主要作用就是表示一个正在使用的block I/O操作。因此,在这个结构中最重要的属性是与日常管理相关的,分别是bi_io_vecsbi_vcntbi_idx。下图中展示了struct bio, strcut bio_vec与struct page之间的联系:

linux块设备驱动简介和实例_第4张图片

其中,bi_io_vecs属性指向了一个bio_vec结构的数组。这些结构用于表示一列特殊block I/O操作的特定segments。每一个bio_vec都是一个”page, offset, len”形式的向量,描述了一个特定的segment。其中的page表示这个segment所在的物理页,offset表示这个block相对于所在页的偏移量,len则是这个block从该偏移量开始的长度。由这些向量组成的整个数组表示了这个完整的buffer。 
bio_vec结构在”linux/bio.h”中是这样被定义的:

struct bio_vec {
    /* pointer to the physical page on which this buffer resides */
    struct page     *bv_page;

    /* the length in bytes of this buffer */
    unsigned int    bv_len;

    /* the byte offset within the page where the buffer resides */
    unsigned int    bv_offset;
};

---------------------------------------------------------------------

gendisk对应一个磁盘实物,结构体如下:

struct gendisk {
        int major;                   //设备主设备号,等于register_blkdev()函数里的major 
        int first_minor;             //起始次设备号,等于0,则表示此设备号从0开始的 
        int minors;              //分区(次设备)数量,当使用alloc_disk()时,就会自动设置该成员
        char disk_name[32];            //块设备名称, 等于register_blkdev()函数里的name
 
        struct hd_struct **part;    /*分区表的信息*/
        int part_uevent_suppress;
        struct block_device_operations *fops; //块设备操作函数
        struct request_queue *queue;  //请求队列,用于管理该设备IO请求队列的指针*
        void *private_data;                    /*私有数据*/
        sector_t capacity;               /*扇区数,512字节为1个扇区,描述设备容量*/
        ....
    };

request结构体如下:

struct request {  
    //用于挂在请求队列链表的节点,使用函数elv_next_request()访问它,而不能直接访问  
    struct list_head queuelist;   
    struct list_head donelist;  /*用于挂在已完成请求链表的节点*/  
    struct request_queue *q;   /*指向请求队列*/  

    unsigned int cmd_flags;    /*命令标识*/  

    enum rq_cmd_type_bits cmd_type;  //读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写
 
    sector_t sector;                       //要提交的下一个扇区偏移位置(offset)
    ... ...
    unsigned int current_nr_sectors;   //当前需要传送的扇区数(长度) 
    ... ...

    char *buffer;        //当前请求队列链表的申请里面的数据,用来读写扇区数据(源地址)
    ... ...
  };

步骤如下:

3.1在入口函数中:

  • 1)使用register_blkdev()创建一个块设备
  • 2) blk_init_queue()使用分配一个申请队列,并赋申请队列处理函数
  • 3)使用alloc_disk()分配一个gendisk结构体 
  • 4)设置gendisk结构体的成员
  •   ->4.1)设置成员参数(major、first_minor、disk_name、fops)
  •   ->4.2)设置queue成员,等于之前分配的申请队列
  •   ->4.3)通过set_capacity()设置capacity成员,等于扇区数
  •  
  • 5)使用kzalloc()来获取缓存地址,用做扇区
  • 6)使用add_disk()注册gendisk结构体

3.2在申请队列的处理函数中

  • 1) while循环使用elv_next_request()获取申请队列中每个未处理的申请
  • 2)使用rq_data_dir()来获取每个申请的读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写
  • 3)使用memcp()来读或者写扇区(缓存)
  • 4)使用end_request()来结束获取的每个申请

3.3在出口函数中

  • 1)使用put_disk()和del_gendisk()来注销,释放gendisk结构体
  • 2)使用kfree()释放磁盘扇区缓存
  • 3)使用blk_cleanup_queue()清除内存中的申请队列
  • 4)使用unregister_blkdev()卸载块设备

代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 

static DEFINE_SPINLOCK(memblock_lock);                //定义自旋锁
static request_queue_t * memblock_request;                //申请队列
static struct gendisk   *memblock_disk;                  //磁盘结构体
static int memblock_major;

#define BLOCKBUF_SIZE               (1024*1024)              //磁盘大小
#define SECTOR_SIZE                   (512)                    //扇区大小
static unsigned char   *block_buf;                              //磁盘地址


static int memblock_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{    
       geo->heads =2;                                         // 2个磁头分区
       geo->cylinders = 32;                                   //一个磁头有32个柱面
       geo->sectors = BLOCKBUF_SIZE/(2*32*SECTOR_SIZE);      //一个柱面有多少个扇区    
    return 0;
}

static struct block_device_operations memblock_fops = {
       .owner    = THIS_MODULE,
       .getgeo   =  memblock_getgeo,                //几何,保存磁盘的信息(柱头,柱面,扇区)
};

    /*申请队列处理函数*/
static void do_memblock_request (request_queue_t * q)
{
        struct request *req;
        unsigned long offset;
        unsigned long len; 
        static unsigned long r_cnt = 0;
        static unsigned long w_cnt = 0;
              
        while ((req = elv_next_request(q)) != NULL)        //获取每个申请
        {
        offset=req->sector*SECTOR_SIZE;                     //偏移值
        len=req->current_nr_sectors*SECTOR_SIZE;            //长度    
                      
        if(rq_data_dir(req)==READ)
        {            
            memcpy(req->buffer,block_buf+offset,len);       //读出缓存
        }
        else
        {              
            memcpy(block_buf+offset,req->buffer,len);     //写入缓存
        }
        end_request(req, 1);                                            //结束获取的申请
        }    
}

    /*入口函数*/
static int memblock_init(void)
{
     /*1)使用register_blkdev()创建一个块设备*/
     memblock_major=register_blkdev(0, "memblock");     
     
     /*2) blk_init_queue()使用分配一个申请队列,并赋申请队列处理函数*/
     memblock_request=blk_init_queue(do_memblock_request,&memblock_lock);
    
     /*3)使用alloc_disk()分配一个gendisk结构体*/
     memblock_disk=alloc_disk(16);                        //不分区
    
     /*4)设置gendisk结构体的成员*/
     /*->4.1)设置成员参数(major、first_minor、disk_name、fops)*/           
     memblock_disk->major = memblock_major;
     memblock_disk->first_minor = 0;
     sprintf(memblock_disk->disk_name, "memblock");
     memblock_disk->fops = &memblock_fops;
        
     /*->4.2)设置queue成员,等于之前分配的申请队列*/
     memblock_disk->queue = memblock_request;
      
     /*->4.3)通过set_capacity()设置capacity成员,等于扇区数*/
     set_capacity(memblock_disk,BLOCKBUF_SIZE/SECTOR_SIZE);
   
     /*5)使用kzalloc()来获取缓存地址,用做扇区*/
     block_buf=kzalloc(BLOCKBUF_SIZE, GFP_KERNEL);
 
     /*6)使用add_disk()注册gendisk结构体*/
     add_disk(memblock_disk);   
     return  0;
}
static void memblock_exit(void)
{        
      /*1)使用put_disk()和del_gendisk()来注销,释放gendisk结构体*/
      put_disk(memblock_disk);
      del_gendisk(memblock_disk);
      /*2)使用kfree()释放磁盘扇区缓存   */ 
      kfree(block_buf);
      /*3)使用blk_cleanup_queue()清除内存中的申请队列    */
      blk_cleanup_queue(memblock_request);
      
      /*4)使用unregister_blkdev()卸载块设备               */
      unregister_blkdev(memblock_major,"memblock");
}

module_init(memblock_init);
module_exit(memblock_exit);
MODULE_LICENSE("GPL");

测试运行:

insmod ramblock.ko                      //挂载memblock块设备
mkdosfs /dev/memblock                   //将memblock块设备格式化为dos磁盘类型
mount /dev/ memblock   /tmp/            //挂载块设备到/tmp目录下
cat /dev/memblock > /mnt/memblock.bin   //在/mnt目录下创建.bin文件,然后将块设备里面的文件追加到.bin里面
cd /; umount /tmp/                      //退出/mp,卸载,同时之前读写的文件也会消失

你可能感兴趣的:(linux设备驱动理论)