前言

写文章的目的是想通过记录自己的学习过程,以便以后使用到相关的知识点可以回顾和参考。

一、块设备驱动框架

.

1、 block\_device 结构体

linux 内核使用 block\_device 表示块设备, block\_device 为 一 个 结 构 体 , 定 义 在 include/linux/fs.h 文件中,结构体内容如下:

struct block_device {
    dev_t            bd_dev;  /* not a kdev_t - it's a search key */
    int            bd_openers;
    struct inode *        bd_inode;    /* will die */
    struct super_block *    bd_super;
    struct mutex        bd_mutex;    /* open/close mutex */
    struct list_head    bd_inodes;
    void *            bd_claiming;
    void *            bd_holder;
    int            bd_holders;
    bool            bd_write_holder;
#ifdef CONFIG_SYSFS
    struct list_head    bd_holder_disks;
#endif
    struct block_device *    bd_contains;
    unsigned        bd_block_size;
    struct hd_struct *    bd_part;
    /* number of times partitions within this device have been opened. */
    unsigned        bd_part_count;
    int            bd_invalidated;
    struct gendisk *    bd_disk;    /* 描述一个磁盘的结构体gendisk */
    struct request_queue *  bd_queue;
    
    ........
};

对于 block\_device 结构体,重点关注一下 bd\_disk 成员变量,此成员变量为
gendisk 结构体指针类型。内核使用 block\_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd\_disk 就指向通用磁盘结构 gendisk。


. 注册块设备
和字符设备驱动一样,需要向内核注册新的块设备、申请设备号,块设备注册函数为register\_blkdev,函数原型如下:
int register\_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:主设备号。
name:块设备名字。
返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。

. 注销块设备
如果不使用某个块设备了,那么就需要注销掉,函数为unregister\_blkdev,函数原型如下:
void unregister\_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:要注销的块设备主设备号。
name:要注销的块设备名字。
返回值:无。

.

2、 gendisk 结构体

linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:

struct gendisk {
    /* major, first_minor and minors are input parameters only,
     * don't use directly.  Use disk_devt() and disk_max_parts().
     */
    /* 磁盘设备的主设备号 */
    int major;            /* major number of driver */
    /* 磁盘的第一个次设备号 */
    int first_minor;
    /* 磁盘的此设备号数量,也就是磁盘的分区数量 */
    int minors;                     /* maximum number of minors, =1 for
                                         * disks that can't be partitioned. */

    char disk_name[DISK_NAME_LEN];    /* name of major driver */
    char *(*devnode)(struct gendisk *gd, umode_t *mode);

    unsigned int events;        /* supported events */
    unsigned int async_events;    /* async events, subset of all */

    /* Array of pointers to partitions indexed by partno.
     * Protected with matching bdev lock but stat and other
     * non-critical accesses use RCU.  Always access through
     * helpers.
     */
    struct disk_part_tbl __rcu *part_tbl;     /* 磁盘对应的分区表 */
    struct hd_struct part0;
    
    const struct block_device_operations *fops;    /* 块设备操作集 */
    struct request_queue *queue;    /* 磁盘对应的请求队列 */
    void *private_data;    /* 私有数据 */

    .......
};

里面比较重要的成员,我已中文注释在代码中。编写块的设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数,常用的 API 函数如下面这些。

. 申请 gendisk
使用 gendisk 之前要先申请,allo\_disk 函数用于申请一个 gendisk,函数原型如下:
struct gendisk *alloc\_disk(int minors)
函数参数和返回值含义如下:
minors:次设备号数量,也就是 gendisk 对应的分区数量。
返回值:成功:返回申请到的 gendisk,失败:NULL。

. 删除 gendisk
如果要删除 gendisk 的话可以使用函数 del\_gendisk,函数原型如下:
void del\_gendisk(struct gendisk *gp)
函数参数和返回值含义如下:
gp:要删除的 gendisk。
返回值:无。

. 将 gendisk 添加到内核
使用 alloc\_disk 申请到 gendisk 以后系统还不能使用,必须使用 add\_disk 函数将申请到的gendisk 添加到内核中,add\_disk 函数原型如下:
void add\_disk(struct gendisk *disk)
函数参数和返回值含义如下:
disk:要添加到内核的 gendisk。
返回值:无。

. 设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set\_capacity,函数原型如下:
void set\_capacity(struct gendisk *disk, sector\_t size)
函数参数和返回值含义如下:
disk:要设置容量的 gendisk。
size:磁盘容量大小,注意这里是扇区数量。
返回值:无。

. 调整 gendisk 引用计数
内核会通过 get\_disk 和 put\_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get\_disk 是增加 gendisk 的引用计数,put\_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:
truct kobject *get\_disk(struct gendisk *disk)

void put\_disk(struct gendisk *disk)

.

3、 block\_device\_operations 结构体

和字符设备的 file \_operations 一样,块设备也有操作集,为结构体 block\_device\_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:

struct block_device_operations {
    int (*open) (struct block_device *, fmode_t);
    int (*release) (struct gendisk *, fmode_t);
    int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
    int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
    int (*direct_access) (struct block_device *, sector_t,
                        void **, unsigned long *);
    unsigned int (*check_events) (struct gendisk *disk,
                      unsigned int clearing);
    /* ->media_changed() is DEPRECATED, use ->check_events() instead */
    int (*media_changed) (struct gendisk *);
    void (*unlock_native_capacity) (struct gendisk *);
    int (*revalidate_disk) (struct gendisk *);
    int (*getgeo)(struct block_device *, struct hd_geometry *);
    /* this callback is with swap_lock and sometimes page table lock held */
    void (*swap_slot_free_notify) (struct block_device *, unsigned long);
    struct module *owner;
};

可以看出,block\_device\_operations 结构体里面的操作集函数和字符设备的 file\_operations 操作集基本类似,不过块设备中的读写操作不再通过read,write操作函数来完成,因此在上面找不到这两个函数指针。除了open,release,ioctl外,里面有一个叫 getgeo 的也要了解下,接下来的驱动编写实践会用到,它用于获取磁盘信息,包括磁头、柱面和扇区等信息,其中有个参数

struct hd\_geometry *

,它是 hd\_geometry 结构体类型的指针,定义在 include/linux/hdreg.h 文件中,内容如下:

/* 描述一个磁盘的结构 */
struct hd_geometry {
      unsigned char heads;    /* 磁头 */
      unsigned char sectors;    /* 一条磁道的扇区数 */
      unsigned short cylinders;    /* 柱面,即磁道数 */
      unsigned long start;    
};

.

4、块设备 I/O 请求过程

在上面讲到 block\_device\_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引处理块设备驱动中非常重要的 request\_queue、request 和 bio。
它们之间的关系如下图所示:

在这里插入图片描述

内核将对块设备的读写都发送到请求队列 request\_queue 中,request\_queue 中是大量的 request(请求结构体),而 request 又包含了 bio,bio 包含的bvec\_iter、bio\_vec 结构体描述了该I/O请求的开始扇区、数据方向(读、写)、数据放入的页,即bio保存了读写相关数据。bio相关代码定义如下:

. bio结构体

struct bio { 
    struct bio *bi_next; /* 请求队列的下一个 bio */
    struct block_device *bi_bdev; /* 指向块设备 */
    unsigned long bi_flags; /* bio 状态等信息 */
    unsigned long bi_rw; /* I/O 操作,读或写 */
    struct bvec_iter bi_iter; /* I/O 操作,读或写 */
    .......
    struct bio_vec *bi_io_vec; /* bio_vec 列表 */
    struct bio_set *bi_pool;
    struct bio_vec bi_inline_vecs[0];
};

. bvec\_iter结构体

struct bvec_iter { 
    sector_t bi_sector; /* I/O 请求的设备起始扇区(512 字节) */
    unsigned int bi_size; /* 剩余的 I/O 数量 */
    unsigned int bi_idx; /* blv_vec 中当前索引 */
    unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */
};

. bio\_vec结构体

struct bio_vec { 
    struct page *bv_page; /* 页 */
    unsigned int bv_len; /* 长度 */
    unsigned int bv_offset; /* 偏移 */
    };

我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi\_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi\_io\_vec 指向 bio\_vec 数组首地址,bio\_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度。bio、bvec\_iter 以及 bio\_vec 这三个结构体之间的关系如下图所示:
在这里插入图片描述

5、涉及处理 request\_queue、request 和 bio 的API

从上面知道了 request\_queue、request 和 bio 这三者之间的关系,下面来看一下内核提供的涉及处理 request\_queue、request 和 bio 的API函数。

1、初始化请求队列
request\_queue *blk\_init\_queue(request\_fn\_proc *rfn, spinlock\_t *lock)
函数参数和返回值含义如下:
rfn:请求处理函数指针,每个 request\_queue 都要有一个请求处理函数,请求处理函数需要驱动编写人员自行实现。
lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request\_queue 地址。

2、清除请求队列
void blk\_cleanup\_queue(struct request\_queue *q)

3、分配请求队列
struct request\_queue *blk\_alloc\_queue(gfp\_t gfp\_mask)
对于 ramdisk 这种完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,可以直接 “踢开” I/O调度器,使用如下函数来绑定请求队列和 “制造请求” 函数(make\_request\_fn)。

void blk\_queue\_make\_request(struct request\_queue *q, make\_request\_fn *mfn)

通常 blk\_cleanup\_queue() 和 blk\_queue\_make\_request() 都是结合起来使用的,逻辑一般是:
xxx\_queue = blk\_cleanup\_queue(GFP\_KERNEL);
blk\_queue\_make\_request(xxx\_queue, xxx\_make\_request\_fn);

4、提取请求
struct request *blk\_peek\_request(struct request\_queue *q)
上述函数用于返回下一个要处理的请求,如果没有请求则返回NULL。

5、启动请求
void blk\_start\_request(struct request *req)
使用 blk\_peek\_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,使用上述函数启动请求。

6、一步到位处理请求
struct request *blk\_fetch\_request(struct request\_queue *q)
我们可以考虑使用 blk\_fetch\_request() 函数完成请求的提取和启动工作。

7、遍历 bio 和片段
\_\_rq\_for\_each\_bio(\_bio, rq) 遍历一个请求的所有 bio

#define __rq_for_each_bio(_bio, rq) \
 if ((rq->bio)) \
 for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)

bio\_for\_each\_segment 遍历一个 bio 的所有 bio\_vec

#define bio_for_each_segment(bvl, bio, iter) \
 __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)

rq\_for\_each\_segment 迭代遍历一个请求所有 bio 中的所有 bio\_vec

#define rq_for_each_segment(bvl, _rq, _iter)            \
    __rq_for_each_bio(_iter.bio, _rq)            \
        bio_for_each_segment(bvl, _iter.bio, _iter.iter)

8、报告完成
void \_\_blk\_end\_request\_all(struct request *rq, int error)
void blk\_end\_request\_all(struct request *rq, int error)

上述两个函数用于报告请求是否完成,error为0表示完成,小于0表示失败,\_\_blk\_end\_request\_all 需要在持有队列锁的场景下使用。

如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要使用 bio\_endio 函数通知处理完成:

bvoid bio\_endio(struct bio *bio, int error)

.

二、实践:编写 ramdisk 驱动

ramdisk 是一种模拟磁盘,其数据实际上存储在 RAM 中。它通过 kzalloc() 分配出来的内存空间来模拟出一个磁盘,以块设备的方式访问这片内存。

下面的驱动程序通过设置 request\_mode 的值,实现 使用请求队列 或者 不使用请求队列 两种方式来访问ramdisk。

ramdisk.c驱动程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h> 
#include <linux/gpio.h> 
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/bio.h>
#include <linux/hdreg.h>

#define RAMDISK_SIZE    (2 * 1024 * 1024)   
#define RAMDISK_NAME    "ramdisk"
#define RAMDISK_MINORS   (3)   /* 3个分区 */

#define RAMDISK_QUEUE    (1)  /* 使用请求队列 */
#define RAMDISK_NOQUEUE  (2)  /* 不使用请求队列 */

const unsigned char request_mode = RAMDISK_QUEUE;   /* 选择是否使用请求队列 */

/* ramdisk 设备结构体 */
struct ramdisk_dev{
    int major;
    unsigned char *ramdiskbuf;  /* ramdisk 内存空间,用于模拟块设备 */
    spinlock_t lock;
    struct gendisk *gendisk;    /* 描述一个磁盘设备, gendisk 结构体 */
    struct request_queue *queue;    /* 请求队列 */
};

struct ramdisk_dev ramdiskdev;  /* ramdisk 设备 */

static int setup_device(struct ramdisk_dev *dev);


/* 使用请求队列时的传输过程 */
static void ramdisk_transfer(struct ramdisk_dev *dev, struct request *req)
{
    unsigned long start = blk_rq_pos(req) << 9;
    unsigned long len = blk_rq_cur_bytes(req);

    char *buffer = bio_data(req->bio);

    if(rq_data_dir(req) == READ)
        memcpy(buffer, dev->ramdiskbuf + start, len);
    else if(rq_data_dir(req) == WRITE)
        memcpy(dev->ramdiskbuf + start, buffer, len);
}

/* 请求处理函数,处理请求队列中每一个请求 */
static void ramdisk_request_fn(struct request_queue *q)
{
    struct request *req;
    struct bio_vec *bvec;
    struct req_iterator iter;

    req = blk_fetch_request(q);
#if 0
    while(req != NULL){
        struct ramdisk_dev *dev = req->rq_disk->private_data;
       
        ramdisk_transfer(dev, req);   /*  针对请求做具体的传输处理 */

        __blk_end_request_all(req, 0);
        req = blk_fetch_request(q);
    }
#endif
    
     while(req != NULL){
         struct ramdisk_dev *dev = req->rq_disk->private_data;

        rq_for_each_segment(bvec, req, iter){   /* 遍历一个请求所有 bio 中的所有 bio_vec */
            ramdisk_transfer(dev, req);  
        } 
        __blk_end_request_all(req, 0);
        req = blk_fetch_request(q);
    }
    
}

/* 
 * "制造请求"函数,
 * 不使用请求队列时使用 
*/
static void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
{
    struct ramdisk_dev *dev = q->queuedata;
    int offset;
    struct bio_vec *bvec;
    unsigned long len = 0;
    int i = 0;

    offset = (bio->bi_sector) << 9;

    /* 处理 bio 中的所有 bio_vec */
    bio_for_each_segment(bvec, bio, i){
        char *ptr = __bio_kmap_atomic(bio, bio->bi_idx,i);
        len = bvec->bv_len;

        if(bio_data_dir(bio) == READ)
            memcpy(ptr, dev->ramdiskbuf + offset, len);
        else if(bio_data_dir(bio) == WRITE)
            memcpy(dev->ramdiskbuf + offset, ptr, len);

        offset += len;
        __bio_kunmap_atomic(ptr,i);
    }
    set_bit(BIO_UPTODATE, &bio->bi_flags);
    bio_endio(bio, 0);
}


static int ramdisk_open(struct block_device *bdev, fmode_t mode)
{
    //struct ramdisk_dev *dev = bdev->bd_disk->private_data;

    printk(KERN_EMERG "ramdisk open!\r\n");
    return 0;
}

static int ramdisk_release(struct gendisk *disk, fmode_t mode)
{
    //struct ramdisk_dev *dev = disk->private_data;

    printk(KERN_EMERG "ramdisk release!\r\n");

    return 0;
}

static int ramdisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    //struct ramdisk_dev *dev = bdev->bd_disk->private_data;

    geo->heads = 2;         /* 磁头 */
    geo->cylinders = 32;    /* 柱面(即磁道数) */
    geo->sectors = RAMDISK_SIZE / (2*32*512);   /* 每条磁道的扇区数 */

    return 0;
    
}

static struct block_device_operations ramdisk_fops = {
    .owner = THIS_MODULE,
    .open = ramdisk_open,
    .release = ramdisk_release,
    .getgeo = ramdisk_getgeo,
};

/* 驱动入口函数 */
static int __init ramdisk_init(void)
{
    int ret = 0;

    /* 申请 ramdisk 内存 */
    ramdiskdev.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
    if(ramdiskdev.ramdiskbuf == NULL){  
        ret = -EINVAL;  /* 申请内存空间失败 */
        goto ram_fail;
    }

    /* 初始化自旋锁 */
    spin_lock_init(&ramdiskdev.lock);

    /* 注册块设备 */
    ramdiskdev.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配主设备号 */
    if(ramdiskdev.major < 0){
        goto register_blkdev_fail;
    }
    printk(KERN_EMERG "major = %d\r\n",ramdiskdev.major);

    ret = setup_device(&ramdiskdev);
    if(ret != 0)
        return ret;

    return 0;

register_blkdev_fail:
    kfree(ramdiskdev.ramdiskbuf); /* 释放内存 */
ram_fail:
    return ret;
}

/* 驱动出口函数 */
static void __exit ramdisk_exit(void)
{
    put_disk(ramdiskdev.gendisk);      /* 引用计数-1 */
    del_gendisk(ramdiskdev.gendisk);   /* 删除 gendisk */

    blk_cleanup_queue(ramdiskdev.queue);   /* 清除请求队列 */

    unregister_blkdev(ramdiskdev.major, RAMDISK_NAME); /* 注销块设备 */

    kfree(ramdiskdev.ramdiskbuf); /* 释放内存 */
}


static int setup_device(struct ramdisk_dev *dev)
{
    int ret = 0;

    switch(request_mode){
        case RAMDISK_NOQUEUE:   /* 不使用请求队列 */
            dev->queue = blk_alloc_queue(GFP_KERNEL);
            if(dev->queue == NULL){
                ret = -EINVAL;
                goto blk_queue_fail;  
            }
            blk_queue_make_request(dev->queue, ramdisk_make_request_fn);  /* 设置 "制造请求" 函数*/
            break;

        default:    /* 没有指定模式,则默认使用 请求队列  */
            printk(KERN_EMERG "Bad request mode %d, using simple\r\n", request_mode);

        case RAMDISK_QUEUE:  /* 使用请求队列 */
            dev->queue = blk_init_queue(ramdisk_request_fn, &dev->lock);
            if (dev->queue == NULL){
                ret = -EINVAL; 
                goto blk_queue_fail;
            }
            break;
    }

    dev->queue->queuedata = dev;    /* 私有数据 */

    /* 分配并初始化gendisk */
    dev->gendisk = alloc_disk(RAMDISK_MINORS);
    if (dev->gendisk == NULL)
    {
        printk(KERN_EMERG "alloc_disk fail\r\n");
        ret = -EINVAL;  /* 分配 gendisk 失败 */
        goto gendisk_alloc_fail;
    }

    /* 初始化 gendisk ,并添加进内核 */
    dev->gendisk->major = dev->major;
    dev->gendisk->first_minor = 0;
    dev->gendisk->fops = &ramdisk_fops;
    dev->gendisk->private_data = dev;   /* 私有数据 */
    dev->gendisk->queue = dev->queue;
    sprintf(dev->gendisk->disk_name, RAMDISK_NAME);
    set_capacity(dev->gendisk, RAMDISK_SIZE/512);    /* 设置 gendisk 容量 */
    add_disk(dev->gendisk);  /* 将 gendisk 添加到内核 */

    return 0;

blk_queue_fail:
    unregister_blkdev(dev->major, RAMDISK_NAME);
gendisk_alloc_fail:
    put_disk(dev->gendisk);   /* 引用计数-1 */
    kfree(dev->ramdiskbuf);   /* 释放内存 */
    return ret;
}


module_init(ramdisk_init);
module_exit(ramdisk_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzj");

.

测试

加载驱动 insmod
格式化 ramdisk-----mkfs.vfat /dev/ramdisk
Linux块设备驱动-ramdisk模拟磁盘教程
查看所有硬盘及分区信息------fdisk -l
Linux块设备驱动-ramdisk模拟磁盘教程
已经识别出 ramdisk 设备了,容量和磁盘信息跟上面驱动程序中的一样。
Linux块设备驱动-ramdisk模拟磁盘教程
把磁盘挂载到 tmp目录------mount /dev/ramdisk /tmp
Linux块设备驱动-ramdisk模拟磁盘教程
在 tmp 目录中随便新建一个.txt来测试 ramdisk 的访问是否正常,接着卸载。然后重新挂载到 mnt 目录,可以看到 mnt 目录里面有之前在 tmp 目录下创建的.txt,说明 ramdisk驱动正常工作。
在这里插入图片描述

标签: Linux, 磁盘, dev, struct, request, ramdisk, bio, gendisk

相关文章推荐

添加新评论,含*的栏目为必填