块驱动的IO操作特点
- 块设备对于io请求有缓冲区。为什么会有缓冲区呢,因为块设备的最终都是要读写磁盘的扇区的,而读写操作,都是要移动磁臂这个物理操作,所以连续读写要比分散读写快的多。所以需要有缓冲区,然后在实际读写磁盘的时候可以优化自己的操作顺序,提高工作效率。
- linux的块设备子系统示意图如下:
块设备驱动结构
- 块设备结构体
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;
};
- 上面这些接口被用来实例化操作块设备的。
- 打开和释放函数
// 设备打开和关闭时将调用它们
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
- io控制
// 用于块设备的标准请求,当64位系统内32位进程调用ioctl时,底层会调用compat_iocatl
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
- 介质改变
被内核调用,用来检查驱动器中的介质是否已经改变;若改变,则返回一个非0值。
int (*media_changed) (struct gendisk *);
- 使介质有效
这个函数被调用,用来响应一个介质改变,它给驱动器一个机会进行必要的工作,使其新设备准备好。
int (*revalidate_disk) (struct gendisk *);
- 获得驱动器信息
int (*getgeo)(struct block_device *, struct hd_geometry *);
- 该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry结构体包含磁头、扇区、柱面等信息,其定义于include/linux/hdreg.h头文件中。
- 模块指针
struct module *owner;
- 一个指向拥有这个结构体的模块的指针,它通常被初始化为THIS_MODULE。
gendisk结构体
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;
int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
如何操作这个gendisk结构体呢?
- 分配gendisk
gendisk结构体是一个动态分配的结构体,可以使用下列函数来分配gendisk
struct gendisk *alloc_disk(int minors);
- minors参数是这个磁盘使用的分区数量,确定后,参数不能再被修改。
- 增加gendisk
gendisk结构体被分配后,系统还不能使用这个结构体,需要调用如下函数来注册这个磁盘设备。
void add_disk(struct gendisk *disk);
- 对add_disk的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。
- 释放gendisk
当不再需要磁盘时,我们应当调用如下函数进行释放gendisk
void del_gendisk(struct gendisk *gp);
- gendisk引用计数
通过get_disk和put_disk函数操作gendisk的引用计数,这两个函数的原型
struct kobject *get_disk(struct gendisk *disk);
void put_disk(struct gendisk *disk);
- 前者最终会调用kobject_get函数,后者最终会调用kobject_put函数。
bio、request和request_queue
通常一个bio对应上层传递给块层的IO请求。每个bio结构体实例及其包含的bvec_iter、bvec_vec结构体实例描述了该IO请求的开始扇区、数据方向(读或者写)、数据放入的页,其结构体如下所示:
struct bvec_iter {
/* device address in 512 byte sectors */
sector_t bi_sector;
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
/* number of bytes completed in current bvec */
unsigned int bi_bvec_done;
};
/*
* main unit of I/O for the block layer and lower layers (ie drivers and
* stacking drivers)
*/
struct bio {
sector_t bi_sector; /* device address in 512 byte
sectors */
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short bi_idx; /* current index into bvl_vec */
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
unsigned int bi_size; /* residual I/O count */
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
unsigned int bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
bio_destructor_t *bi_destructor; /* destructor */
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};
- 与bio对应的数据每次存放的内存不一定是连续的,bio_vec结构体用来描述这个bio请求对应的所有的内存,它可能不总是在一个页面里面,因此需要一个向量,向量中的每一个元素实际是一个[page, offset, len],我们也称它为一个片段。其结构如下所示:
/*
* was unsigned short, but we might as well be ready for > 64kB I/O pages
*/
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
- io调度算法可将连续的bio合并成一个请求。请求是经IO调度金子那个整理后的结果。因此每个请求可能包含多个bio。每个块设备或者块设备分区都对应有自身的request_queue,从IO调度器合并和排序出来的请求会被分发到设备级的requ_queue.下图为上面描述的示意图:
下面看一下驱动编程相关的api。
- 初始化请求队列
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
- 上述这个函数一般在块设备驱动的初始化过程中调用。
- 分配请求队列
request_queue_t *blk_alloc_queue(int gfp_mask);
- 提取请求
struct request * blk_peek_request(struct request_queue *q);
- 上述函数用于返回下一个要处理的请求,如果没有请求,则返回NULL。
- 启动请求
void blk_start_request(struct request *req);
- 遍历bio和片段
// 遍历一个请求的所有bio
#define __rq_for_each_bio(_bio, rq)
\
if ((rq->bio))
\
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
// 遍历一个bio的所有bio_vec
#define __bio_for_each_segment(bvl, bio, iter, start)
for (iter = (start);
(iter).bi_size &&
((bvl = bio_iter_iovec((bio), (iter))), 1);
bio_advance_iter((bio), &(iter), (bvl).bv_len))
#define bio_for_each_segment(bvl, bio, iter)
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
- 报告完成
void __blk_end_request_all(struct request *rq, int error);
void blk_end_request_all(struct request *rq, int error);
IO调度器
Linux 2.6以后的内核包含4个I/O调度器,它们分别是Noop I/O调度器、Anticipatory I/O调度器、Deadline I/O调度器与CFQ I/O调度器。
- Noop I/O调度器是一个简化的调度程序,该算法实现了一个简单FIFO队列,它只进行最基本的合并,比较适合基于Flash的存储器。
- Anticipatory I/O调度器算法推迟I/O请求,以期能对它们进行排序,获得最高的效率。在每次处理完读请求之后,不是立即返回,而是等待几个微秒。在这段时间内,任何来自临近区域的请求都被立即执行。超时以后,继续原来的处理。
- Deadline I/O调度器是针对Anticipatory I/O调度器的缺点进行改善而得来的,它试图把每次请求的延迟降至最低,该算法重排了请求的顺序来提高性能。它使用轮询的调度器,简洁小巧,提供了最小的读取延迟和尚佳的吞吐量,特别适合于读取较多的环境(比如数据库)。
- CFQ I/O调度器为系统内的所有任务分配均匀的I/O带宽,提供一个公平的工作环境,在多媒体应用中,能保证音、视频及时从磁盘中读取数据。
- 内核4.0-rc1block目录中的noop-iosched.c、deadline-iosched.c和cfq-iosched.c文件分别实现了IOSCHED_NOOP、IOSCHED_DEADLINE和IOSCHED_CFQ调度算法。as-iosched.c这个文件目前已经不再存在。当前情况下,默认的调度器是CFQ。
- 选择IO调度算法
kernel elevator=deadline
或者
echo SCHEDULER > /sys/block/DEVICE/queue/scheduler
块设备初始化
- 在块设备的注册和初始化阶段,与字符设备驱动类似,块设备驱动要注册它们自己到内核,申请设备号,完成这个任务的函数是register_blkdev,其原型为:
int register_blkdev(unsigned int major, const char *name);
- major是块设备使用的主设备号,name为设备名,他会显示在虚拟文件系统/proc/devices中。如果major为0,内核会自动分配一个新的主设备号,register_blkdev的返回值就是这个主设备号,如果返回了一个负值,则表明发生了错误。
- 注销函数unregister_blkdev的原型为:
int unregister_blkdev(unsigned int major, const char *name);
- 这里传递给注销函数的参数必须与注册的时候匹配,否则返回-EINVAL。
- 块设备在驱动初始化过程中,通常需要完成分配、初始化请求队列绑定请求队列请求处理函数的工作,并且可能会分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。
- 块设备驱动的初始化模板:
static int xxx_init(void)
{
/* 块设备驱动注册 */
if (register_blkdev(XXX_MAJOR, "xxx")) {
err = -EIO;
goto out;
}
/* 请求队列初始化 */
xxx_queue = blk_init_queue(xxx_request, xxx_lock);
if (!xxx_queue)
goto out_queue;
blk_queue_max_hw_sectors(xxx_queue, 255);
blk_queue_logical_block_size(xxx_queue, 512);
/* gendisk 初始化 */
xxx_disks->major = XXX_MAJOR;
xxx_disks->first_minor = 0;
xxx_disks->fops = &xxx_op;
xxx_disks->queue = xxx_queue;
sprintf(xxx_disks->disk_name, "xxx%d", i);
set_capacity(xxx_disks, xxx_size *2);
add_disk(xxx_disks); /* 添加 gendisk */
return 0;
out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
out: put_disk(xxx_disks);28 blk_cleanup_queue(xxx_queue);
return -ENOMEM;
}
- 上述代码中的blk_queue_max_hw_sectors用于通知通用块层和IO调度器,该请求队列支持的每个请求中能够包含的最大扇区数。
- 上述代码中的blk_queue_logical_block_size该请求队列的逻辑块大小。
- 块设备卸载的过程与其注册的过程是相反的。
块设备的打开与释放
- 块设备的驱动函数open与字符设备的open不太相似,前者不以相关的inode和file结构体指针作为参数。在open中我们可以通过block_device的参数bdev获取private_data,在release中通过gendisk的参数disk获取private_data。
static int xxx_open(struct block_device *bdev, fmode_t mode)
{
struct xxx_dev *dev = bdev->bd_disk->private_data;
...
return 0;
}
static void xxx_release(struct gendisk *disk, fmode_t mode)
{
struct xxx_dev *dev = disk->private_data;
...
}
块设备的ioctl函数
- 与字符设备驱动一样,块设备可以包含一个ioctl函数以提供对设备的IO控制能力。实际上高层的块设备层处理了绝大多数的IO控制,因此,在具体的块设备驱动中通常只需要实现与设备相关的的特定ioctl命令。源码文件drivers/block/floppy.c实现了与软驱相关的命令,可以参考学习其步骤流程。
- linux的mmc子系统支持一个ioctl命令MMC_IOC_CMD,drivers/mmc/card/block.c实现了这个命令的处理:
static int mmc_blk_ioctl(struct block_device *bdev, fmode_t mode,
unsigned int cmd, unsigned long arg)
{
int ret = -EINVAL;
if (cmd == MMC_IOC_CMD)
ret = mmc_blk_ioctl_cmd(bdev, (struct mmc_ioc_cmd __user *)arg);
return ret;
}
块设备驱动的IO请求处理
使用请求队列
- 块设备驱动在使用请求队列的场景下,会用blk_init_queue初始化request_queue,而该函数的第一个参数就是请求处理函数的指针。request_queue会作为参数传递给我们在调用blk_init_queue时指定的请求处理函数,块设备驱动请求处理函数的原型:
static void xxx_req(struct request_queue *q)
- 这个函数由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写操作时,它才调用这个函数。
- 该函数的主要工作就是发起与request对应的块设备IO动作
不使用请求队列
- 使用请求队列对于一个机械硬盘设备而言有助于提高系统的性能,但对于像RAMDISK、ZRAM等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。
- 对于这些设备,块层支持“无队列”的操作模式,为了使用这个模式,驱动必须提供一个“制造请求”函数,而不是一个请求处理函数,“制造请求”函数的原型为:
static void xxx_make_request(struct request_queue *queue, struct bio *bio);
实例:vmem_disk驱动
/**
** This file is part of the LinuxTrainningHome project.
** Copyright(C) duanzhonghuan Co., Ltd.
** All Rights Reserved.
** Unauthorized copying of this file, via any medium is strictly prohibited
** Proprietary and confidential
**
** Written by ZhongHuan Duan <15818411038@163.com>, 2019-06-15
**/
#include "linux/init.h"
#include "linux/module.h"
#include "linux/kdev_t.h"
#include "linux/cdev.h"
#include "linux/fs.h"
#include "linux/slab.h"
#include "linux/uaccess.h"
#include "linux/mutex.h"
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#define DEVICE_NUM (4)
static int vmemdisk_major = 0;
#define NSECTORS (1024)
#define HARDSECTOR_SIZE (512)
#define KRNSECTOR_SIZE (512)
#define VMEMDISK_MINIORS (6)
#define vmemdisk_debug
/**
* @brief The vmemdisk_dev struct - the description of the virtual memory disk device
*/
struct vmemdisk_dev
{
// device size in sectors
int size;
// the data array
u8 *data;
// for mutual exclusion
spinlock_t lock;
// the device request queue
struct request_queue *queue;
// the gendisk structure
struct gendisk *gd;
};
static struct vmemdisk_dev *devices = NULL;
static int vmemdisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
struct vmemdisk_dev *dev = bdev->bd_disk->private_data;
long size = dev->size * (HARDSECTOR_SIZE / KRNSECTOR_SIZE);
geo->cylinders = (size & ~0x3f) >> 6;
geo->heads = 4;
geo->sectors = 16;
geo->start = 4;
return 0;
}
static struct block_device_operations vmemdisk_ops = {
.getgeo = vmemdisk_getgeo,
};
static void vmemdisk_transfer(struct vmemdisk_dev *dev, unsigned long sector,
unsigned long nsector, char *buff, int write)
{
unsigned long offset = sector * KRNSECTOR_SIZE;
unsigned long nbytes = nsector * KRNSECTOR_SIZE;
if (dev->size < offset + nbytes)
{
return;
}
if (write)
{
memcpy(dev->data, buff, nbytes);
}
else
{
memcpy(buff, dev->data, nbytes);
}
}
static void vmemdisk_xfer_bio(struct vmemdisk_dev *dev, struct bio *bio)
{
struct bio_vec bvec;
struct bvec_iter iter;
sector_t sector = bio->bi_iter.bi_sector;
bio_for_each_segment(bvec, bio, iter){
char *buffer = __bio_kmap_atomic(bio, iter, 0);
vmemdisk_transfer(dev, sector, bio_cur_bytes(bio) >> 9,
buffer, bio_data_dir(bio) == WRITE);
sector += bio_cur_bytes(bio) >> 9;
__bio_kunmap_atomic(buffer, 0);
}
}
void vmemdisk_make_request(struct request_queue *q, struct bio *bio)
{
struct vmemdisk_dev *dev = q->queuedata;
if (dev)
{
vmemdisk_xfer_bio(dev, bio);
}
}
/**
* @brief setup_device - setup the virtual memory device
* @param device: the virtual memory device
* @param which: which way to setup
*/
static void setup_device(struct vmemdisk_dev* device, int which)
{
// fill the data into the @device structure
memset(device, 0, sizeof(struct vmemdisk_dev));
device->size = NSECTORS * HARDSECTOR_SIZE;
device->data = kmalloc(device->size, GFP_KERNEL);
spin_lock_init(&device->lock);
device->queue = blk_alloc_queue(GFP_KERNEL);
device->gd = alloc_disk(VMEMDISK_MINIORS);
// fill the device structure into the queue data
device->queue->queuedata = device;
blk_queue_make_request(device->queue, vmemdisk_make_request);
// fill the data into the gendisk structre
device->gd->major = vmemdisk_major;
device->gd->first_minor = 0;
device->gd->fops = &vmemdisk_ops;
device->gd->private_data = device;
device->gd->queue = device->queue;
set_capacity(device->gd, NSECTORS * (HARDSECTOR_SIZE / KRNSECTOR_SIZE));
add_disk(device->gd);
printk("success to setup : %s\n", __FILE__);
}
/**
* @brief vmemdisk_init - the function of initializing the virtual memory disk
* @return: the status of initializing the virtual memory disk
*/
static int __init vmemdisk_init(void)
{
int ret = 0;
int i;
// register a new block device
vmemdisk_major = register_blkdev(vmemdisk_major, "vmemdisk");
if (vmemdisk_major <= 0)
{
printk("Error code = %d Fail to register the @vmemdisk new block device.\n", ret);
return -EBUSY;
}
devices = kmalloc(DEVICE_NUM * sizeof(struct vmemdisk_dev), GFP_KERNEL);
if (devices == NULL)
{
printk("Fail to malloc.\n");
ret = -EINVAL;
goto unregister;
}
for (i = 0; i < DEVICE_NUM; i++)
{
setup_device(devices + i * sizeof(struct vmemdisk_dev), 0);
}
unregister:
unregister_blkdev(vmemdisk_major, "vmemdisk");
return ret;
}
/**
* @brief vmemdisk_exit - exit the glboalmem device
*/
static void __exit vmemdisk_exit(void)
{
unregister_blkdev(vmemdisk_major, "vmemdisk");
kfree(devices);
printk("exit success: %s\n", __FILE__);
}
module_init(vmemdisk_init)
module_exit(vmemdisk_exit)
// the declaration of the author
MODULE_AUTHOR("ZhongHuan Duan <15818411038@163.com>");
// the declaration of the licence
MODULE_LICENSE("GPL v2");
- 加载vmemdisk.ko后,在使用默认模块参数的情况下,系统会增加4个块设备节点。
- sudo mkfs.ext2 /dev/vmemdiska可以得到一些信息。
- 它将/dev/vmemdiska格式化为EXT2文件系统。之后我们可以mount这个分区并在中进行文件读写。
总结
一塌糊涂,,,