linux块设备驱动

设备驱动基础构架

  1. Linux输入\输出设备被分为三类:块设备(仅能以块为单位进行读写的设备,系统的串口设备/dev/cua0和/dev/cua1)和网络设备(通过BSD套接口访问数据)
  2. Linux对每个设备描述是通过主设备和从设备号,其中主设备号描述控制这个设备的驱动程序,也就是说驱动程序和主设备号是一一对应,从设备号是用来区分同一个驱动程序控制不同设备
  3. linux输入/输出系统的层次结构及各层次的功能如下图所示:在这里插入图片描述
    设备无关软件的主要功能:对设备程序的统一的接口、设备保护、设备命名、提供一个独立于设备的块的存储分配和释放独占设备、错误报告
  4. Linux设备驱动程序主要功能:
    -> 对设备进行初始化
    -> 将数据从内核传送到设备
    -> 使设备投入运行和退出服务
    -> 从设备接收数据并将他们传送回内核
  5. 驱动程序属于内核代码;
    -> 为内核提供统一的接口;
    -> 驱动程序的执行属于内核机制并且使用内核服务;
    -> 动态可加载;
    -> 可配置

块设备驱动程序

应用层读写到块设备数据框架图:
在这里插入图片描述

  1. 通用块层提供了一个接口ll_rw_block( )用来对逻辑块进行读写操作。应用层对块设备的读写操作都会调用到ll_rw_block( )函数来执行。
    void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]);
    rw:进行读操作还是写操作,一般可选的值为 READ、WRITE 或者 READA 等;
    nr:bhs数组元素个数;
    bhs:要进行读写操作的数据块数组;
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]) //rw:读写标志位,  nr:bhs[]长度,  bhs[]:要读写的数据数组
{
      int i; 
      for (i = 0; i < nr; i++) {
      struct buffer_head *bh = bhs[i]; //获取nr个buffer_head
      ... ...
      if (rw == WRITE || rw == SWRITE) {
              if (test_clear_buffer_dirty(bh)) {
              ... ...
              submit_bh(WRITE, bh); //提交WRITE写标志的buffer_head   
         continue;
              }
      } else {
              if (!buffer_uptodate(bh)) {
              ... ...
              submit_bh(rw, bh); //提交其它标志的buffer_head
              continue;
              }}
              unlock_buffer(bh); 
       }
}

其中buffer_head结构体,就是我们的缓冲区描述符,存放缓存区的各种信息,结构体如下所示:

struct buffer_head {
    unsigned long b_state; //缓冲区状态标志 
    struct buffer_head *b_this_page; //页面中的缓冲区 
    struct page *b_page; //存储缓冲区位于哪个页面
    sector_t b_blocknr; //逻辑块号
    size_t b_size; //块的大小
    char *b_data; //页面中的缓冲区
    struct block_device *b_bdev; //块设备,来表示一个独立的磁盘设备
    bh_end_io_t *b_end_io; //I/O完成方法 
    void *b_private; //完成方法数据 
    struct list_head b_assoc_buffers; //相关映射链表
    /* mapping this buffer is associated with */
    struct address_space *b_assoc_map;   
    atomic_t b_count; //缓冲区使用计数 
}
  1. 然后进入submit_bh()中, submit_bh()函数如下:
int submit_bh(int rw, struct buffer_head * bh)
{
       struct bio *bio; //定义一个bio(block input output),也就是块设备i/o
       ... ...
       bio = bio_alloc(GFP_NOIO, 1); //分配bio
      /*根据buffer_head(bh)构造bio */
       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;             //设置i/o回调函数
       bio->bi_private = bh;                               //指向哪个缓冲区
       ... ...
       submit_bio(rw, bio);                           //提交bio
       ... ...
}

submit_bh()函数就是通过bh来构造bio,然后调用submit_bio()提交bio

  1. submit_bio()函数如下:
void submit_bio(int rw, struct bio *bio)
{
       ... ...
       generic_make_request(bio);        
}

最终调用generic_make_request(),把bio数据提交到相应块设备的请求队列中,generic_make_request()函数主要是实现对bio的提交处理

  1. generic_make_request()函数如下所示:
void generic_make_request(struct bio *bio)
{
     if (current->bio_tail) {  // current->bio_tail不为空,表示有bio正在提交
              *(current->bio_tail) = bio; //将当前的bio放到之前的bio->bi_next里面
              bio->bi_next = NULL;  //更新bio->bi_next=0;
              //然后将当前的bio->bi_next放到current->bio_tail里,使下次的bio就会放到当前bio->bi_next里面了
              current->bio_tail = &bio->bi_next; 
              return;
      }
BUG_ON(bio->bi_next);
       do {
              current->bio_list = bio->bi_next;
              if (bio->bi_next == NULL)
                     current->bio_tail = &current->bio_list;
        } else {
              bio->bi_next = NULL;
              __generic_make_request(bio);  //调用__generic_make_request()提交bio
              bio = current->bio_list;
       } while (bio);
       current->bio_tail = NULL; /* deactivate */
}

从上面的注释和代码分析到,只有当第一次进入generic_make_request()时, current->bio_tail为NULL,才能调用__generic_make_request().
__generic_make_request()首先由bio对应的block_device获取申请队列q,然后要检查对应的设备是不是分区,如果是分区的话要将扇区地址进行重新计算,最后调用q的成员函数make_request_fn完成bio的递交

  1. __generic_make_request()函数如下所示:
static inline void __generic_make_request(struct bio *bio)
{
   request_queue_t *q;    
   int ret;  
    ... ...
       do {
              q = bdev_get_queue(bio->bi_bdev);  //通过bio->bi_bdev获取申请队列q
              ... ...
              ret = q->make_request_fn(q, bio);             //提交申请队列q和bio
       } while (ret);
}
  1. __make_request()函数,对提交的申请队列q和bio进行设置:
static int __make_request(request_queue_t *q, struct bio *bio)
{
    struct request *req; //块设备本身的队列
    ... ...
    //(1)将之前的申请队列q和传入的bio,通过排序,合并在本身的req队列中
    el_ret = elv_merge(q, &req, bio);
    ... ...
    init_request_from_bio(req, bio);  //合并失败,单独将bio放入req队列
     add_request(q, req);  //单独将之前的申请队列q放入req队列
     ... ...
     __generic_unplug_device(q); //(2) 执行申请队列的处理函数     
 }

在这里插入图片描述

  1. q->request_fn是一个request_fn_proc结构体:
      -> 申请队列q->request_fn:参考自带的块设备驱动程序drivers\block\xd.c
static struct request_queue *xd_queue;             //定义一个申请队列xd_queue
xd_queue = blk_init_queue(do_xd_request, &xd_lock);       //分配一个申请队列

其中blk_init_queue()函数原型如下所示:

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
//  *rfn: request_fn_proc结构体,用来执行申请队列中的处理函数
//  *lock:队列访问权限的自旋锁(spinlock),该锁需要通过DEFINE_SPINLOCK()函数来定义

-> 申请队列的处理函数 do_xd_request()处理:

static void do_xd_request (request_queue_t * q)
{
    struct request *req; 
    if (xdc_busy)
        return;
    while ((req = elv_next_request(q)) != NULL) {  //(1)while获取申请队列中的需要处理的申请  
        int res = 0;
        ... ...
       for (retry = 0; (retry < XD_RETRIES) && !res; retry++) {
           //将获取申请req的buffer成员 读写到disk扇区中,当读写失败返回0,成功返回1   
             res = xd_readwrite(rw, disk, req->buffer, block, count); 
          }
     end_request(req, res);  //申请队列中的的申请已处理结束,当res=0,表示读写失败
      }
}

为什么要while一直获取?
  因为这个q是个申请队列,里面会有多个申请,之前是使用电梯算法elv_merge()函数合并的,所以获取也要通过电梯算法elv_next_request()函数获取

  1. drivers\block\xd.c的入口函数大概流程:
static DEFINE_SPINLOCK(xd_lock);     //定义一个自旋锁,用到申请队列中
static struct request_queue *xd_queue; //定义一个申请队列xd_queue
static int __init xd_init(void)          //入口函数
{
    if (register_blkdev(XT_DISK_MAJOR, "xd"))  //1.创建一个块设备,保存在/proc/devices中
         goto out1;
    xd_queue = blk_init_queue(do_xd_request, &xd_lock);  //2.分配一个申请队列,后面会赋给gendisk结构体的queue成员
    ... ...
    for (i = 0; i < xd_drives; i++) {                   
	    ... ...
	    struct gendisk *disk = alloc_disk(64);  //3.分配一个gendisk结构体, 64:次设备号个数,也称为分区个数
	    /* 4.接下来设置gendisk结构体 */
	    disk->major = XT_DISK_MAJOR;             //设置主设备号
	    disk->first_minor = i<<6;                //设置次设备号
	    disk->fops = &xd_fops;                   //设置块设备驱动的操作函数
	    disk->queue = xd_queue;                  //设置queue申请队列,用于管理该设备IO申请队列
	    ... ...
	    xd_gendisk[i] = disk;
	}
    ... ...
    for (i = 0; i < xd_drives; i++)
        add_disk(xd_gendisk[i]);                   //5.注册gendisk结构体
}

其中gendisk(通用磁盘)结构体是用来存储该设备的硬盘信息,包括请求队列、分区链表和块设备操作函数集等,结构体如下所示:

struct gendisk {
    int major;                        /*设备主设备号*/
    int first_minor;                  /*起始次设备号*/
    int minors;                       /*次设备号的数量,也称为分区数量,如果改值为1,表示无法分区*/
    char disk_name[32];              /*设备名称*/
    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个扇区,描述设备容量*/
    ....
};
  1. 注册一个块设备驱动,需要以下步骤:
    -> 创建一个块设备
    -> 分配一个申请队列
    -> 分配一个gendisk结构体
    -> 设置gendisk结构体的成员
    -> 注册gendisk结构体

*** 以sdram模拟块设备的操作 ***
Ramblock.c

/* 参考:
 * drivers\block\xd.c
 * drivers\block\z2ram.c
 */

#include <linux/module.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>

static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;
static int major;
static DEFINE_SPINLOCK(ramblock_lock);

#define RAMBLOCK_SIZE (1024*1024)
static unsigned char *ramblock_buf;

static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
	/* 容量=heads*cylinders*sectors*512 */
	/*磁头 柱面  扇区*/
	geo->heads     = 2;
	geo->cylinders = 32;
	geo->sectors   = RAMBLOCK_SIZE/2/32/512;
	return 0;
}


static struct block_device_operations ramblock_fops = {
	.owner	= THIS_MODULE,
	.getgeo	= ramblock_getgeo,
};

static void do_ramblock_request(request_queue_t * q)
{
	static int r_cnt = 0;
	static int w_cnt = 0;
	struct request *req;	
	//printk("do_ramblock_request %d\n", ++cnt);

	/*以电梯调度算法取出下一个请求*/
	/*用内存模拟磁盘读写。*/
	while ((req = elv_next_request(q)) != NULL) {
		/* 数据传输三要素: 源,目的,长度 */
		/* 源/目的: */
		unsigned long offset = req->sector * 512;

		/* 目的/源: */
		// req->buffer

		/* 长度: */		
		unsigned long len = req->current_nr_sectors * 512;
		if (rq_data_dir(req) == READ) {
			//printk("do_ramblock_request read %d\n", ++r_cnt);
			memcpy(req->buffer, ramblock_buf+offset, len);
		} else {
			//printk("do_ramblock_request write %d\n", ++w_cnt);
			memcpy(ramblock_buf+offset, req->buffer, len);
		}		
		end_request(req, 1);
	}
}

static int ramblock_init(void)
{
	/* 1. 分配一个gendisk结构体 */
	ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */

	/* 2. 设置 */
	/* 2.1 分配/设置队列: 提供读写能力 */
	ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
	ramblock_disk->queue = ramblock_queue;
	
	/* 2.2 设置其他属性: 比如容量 */
	major = register_blkdev(0, "ramblock");  /* cat /proc/devices */	
	ramblock_disk->major       = major;
	ramblock_disk->first_minor = 0;
	sprintf(ramblock_disk->disk_name, "ramblock");
	ramblock_disk->fops        = &ramblock_fops;
	set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);

	/* 3. 硬件相关操作分配内存*/
	ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);

	/* 4. 注册 */
	add_disk(ramblock_disk);

	return 0;
}

static void ramblock_exit(void)
{
	unregister_blkdev(major, "ramblock");
	del_gendisk(ramblock_disk);
	put_disk(ramblock_disk);
	blk_cleanup_queue(ramblock_queue);
	kfree(ramblock_buf);
}

module_init(ramblock_init);
module_exit(ramblock_exit);

MODULE_LICENSE("GPL");

总结:

  • 块设备驱动程序初始化时调用blk_init_queue接口初始一个i/o请求队列,并为该队列提供一个队列"请求处理函数";
  • 用户程序调用read()/write(),会转换成通用块层ll_rw_block接口的调用;
  • ll_rw_block接口通过调用submit_bh接口提交读写数据块;
  • submit_bh接口构建并设置bio,然后通过submit_bio接口提交bio;
  • 调用块设备队列的"构造请求函数"处理bio,即__make_request接口处理bio;
  • __make_request接口首先尝试和其他i/o请求合并bio以减少对设备I/O请求的次数;
  • 如果合并不成功,则构建一个新的i/o请求;
  • 所有bio处理(和其他i/o请求合并或构建一个新的i/o请求)完成或者用户强制sync时,会处理所有的i/o请求,调用_generic_unplug_device接口,调用队列的"请求处理函数"处理所有请求(读写数据)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值