目录
1. 主要数据结构说明 1
2. 添加磁盘和分区到系统 4
2.1磁盘的注册 4
2.1添加磁盘分区到系统 6
3. 请求队列 9
4. IO调度 11
5. 请求提交 13
6. 块设备与文件系统的关联 16
每个分区打开都会创建一个block_device:
struct block_device |
|
dev_t bd_dev |
块设备设备号 |
int bd_openers |
设备打开计数 |
struct super_block * bd_super |
设备如果有挂载文件系统bd_super指向文件系统的草超级块 |
void * bd_holder |
块设备打开的时候指向file |
unsigned bd_block_size |
块大小 |
struct hd_struct * bd_part |
指向块设备在磁盘上所属的分区Gendisk->ptbl->part[partno] |
int bd_invalidated |
指向分区是否有效,如果无效在打开的时候会rescan分区 |
struct gendisk * bd_disk |
指向磁盘描述结构gendisk |
struct request_queue * bd_queue |
指向块设备对应的请求队列 |
struct list_head bd_list |
用于将block_device 添加到全局链表all_bdevs |
unsigned long bd_private |
指向块设备的私有数据 |
struct gendisk用于描述一个磁盘
struct gendisk |
|
int major |
磁盘主设备号 |
int first_minor |
第一个从设备号 |
int minors |
从设备号数,一个分区占有一个从设备号 |
char disk_name[DISK_NAME_LEN] |
磁盘名 |
struct disk_part_tbl __rcu *part_tbl |
磁盘分区表,rescan_partitions之后分配 |
struct hd_struct part0 |
主分区 |
struct block_device_operations *fops |
特定磁盘驱动的操作函数 |
struct request_queue *queue |
请求队列,一个磁盘一个请求队列而不是一个分区一个 |
void *private_data |
磁盘底层私有数据 |
struct hd_struct用于描述分区
struct hd_struct |
|
sector_t start_sect |
分区起始扇区在磁盘上的起始位置 |
sector_t nr_sects |
分区大小 |
struct device __dev |
分区作为device添加到系统 |
int partno |
分区号 |
struct request_queue |
|
struct list_head queue_head |
经过调度器选出后等待处理的请求 |
struct request *last_merge |
上一次合并的请求 |
struct elevator_queue *elevator |
指向调度器队列 |
request_fn_proc *request_fn |
请求处理函数 |
make_request_fn *make_request_fn |
根据bio创建请求 |
struct kobject kobj |
用于将请求队列放到kobject框架中管理 |
unsigned long nr_requests |
最大请求数量 |
struct queue_limits limits |
请求队列的各种参数限制 |
struct request |
|
struct list_head queuelist |
用于链接到请求队列的request_queue ->queue_head |
struct request_queue *q |
请求所在的请求队列 |
req_flags_t rq_flags |
标识读请求写请求等等各种标识 |
unsigned int __data_len |
请求数据长度 |
sector_t __sector |
请求的起始扇区 |
struct bio *bio |
请求中当前处理的bio |
struct bio *biotail |
请求中末尾bio |
unsigned short nr_phys_segments |
在分散聚集操作时候最大的物理段 |
struct bio标识请求磁盘上一段连续的数据区间,struct bio主要分为两个部分:描述请求磁盘扇区部分;和描述数据缓存段的bvec数组。缓存可能是多个内存段比如几个不连续的页,他们由bvec来管理但是bio请求的磁盘区间是连续的所以只用指定起始扇区和请求数据长度就够了
struct bio |
|
struct bio *bi_next |
Bio在请求中组成单项链表 |
struct block_device *bi_bdev |
Bio请求的数据所在的块设备 |
struct bvec_iter bi_iter |
struct bvec_iter { sector_t bi_sector; //bio请求数据在磁盘上的起始扇区编号 unsigned int bi_size; //剩余I/O数量 unsigned int bi_idx; //当前处理的bi_io_vec数组项 unsigned int bi_bvec_done;// 当前处理的bvec数组项完成的bytes }; |
unsigned int bi_phys_segments |
传输数据段数目 |
bio_end_io_t *bi_end_io |
Bio传输完成调用函数bi_end_io做清理工作或者唤醒等待的进程 |
unsigned short bi_vcnt |
当前bvec的数量 |
unsigned short bi_max_vecs |
最大bvec数量 |
struct bio_vec *bi_io_vec |
缓存段数组 |
在函数elevator_init_fn中分配struct elevator_queue,并付给指针 request_queue ->elevator
struct elevator_queue |
|
struct elevator_type *type |
struct elevator_type { union { struct elevator_ops sq; //调度器的一组操作函数 struct elevator_mq_ops mq;//多请求队列的一组操作函数 } ops; bool uses_mq;//是否使用多请求队列 }; |
void *elevator_data |
指向特定调度类型的数据 |
struct kobject kobj |
将调度器放到kobject框架中去管理 |
unsigned int uses_mq:1 |
是否使用多请求队列 |
后面讲解调度器的时候将以iosched_deadline为例,所以这里只说明iosched_deadline相关的结构deadline_data
struct deadline_data |
|
struct rb_root sort_list[2] |
用于'读' '写'的两棵红黑树。按照请求在磁盘上的位置排序,方便合并临近请求 |
struct list_head fifo_list[2] |
用于'读' '写'的两个队列。按照请求提交的时间先后排序,方便查找到期请求 |
struct request *next_rq[2] |
如果一个请求req被处理,这里存放的是req在sort_list中的下一个请求,因为在sort_list中的下一个请求一定是与req请求的磁盘扇区临近,这样可以节省磁盘寻道的时间 |
unsigned int starved |
饥饿计数 |
int fifo_expire[2] |
请求到期时间间隔 |
int writes_starved |
写请求被读请求阻塞的次数 |
int front_merges |
是否固定向前合并 |
我们以mmc驱动作为实例来讲解块设备,代码路径:linux-4.12.3/drivers/mmc/core/block.c,磁盘设备注册流程如下:
gendisk分配与初始化
我们主要关注块设备相关的逻辑,不过多讲解mmc相关的实现:
static struct mmc_blk_data *mmc_blk_alloc_req(struct mmc_card *card,
struct device *parent,
sector_t size,
bool default_ro,
const char *subname,
int area_type)
{
struct mmc_blk_data *md;
int devidx, ret;
......
md->disk = alloc_disk(perdev_minors); //分配磁盘通用管理结构gendisk,后面展开讲解
ret = mmc_init_queue(&md->queue, card, &md->lock, subname); //初始化磁盘的请求队列后续详解
if (ret)
goto err_putdisk;
md->queue.blkdata = md;
md->disk->major = MMC_BLOCK_MAJOR; //MMC块设备的主设备号,179
md->disk->first_minor = devidx * perdev_minors;//设置从设备号起始位置
md->disk->fops = &mmc_bdops; //特定设备相关的一些底层操作函数,
md->disk->private_data = md;
md->disk->queue = md->queue.queue; //设置gendisk->queue指向请求队列
md->parent = parent;
if (mmc_card_mmc(card))
blk_queue_logical_block_size(md->queue.queue,
card->ext_csd.data_sector_size);
else
blk_queue_logical_block_size(md->queue.queue, 512);
set_capacity(md->disk, size); //设置磁盘容量
......
}
前面函数alloc_disk(perdev_minors);调用alloc_disk_node来实现gendisk分配,传入参数perdev_minors,这个参数的值是编译时候配置的,配置项CONFIG_MMC_BLOCK_MINORS
struct gendisk *alloc_disk_node(int minors, int node_id)
{
struct gendisk *disk;
disk = kzalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id); //分配gendisk
if (disk) {
......
disk->node_id = node_id;
// 根据分区数分配disk_part_tbl数据,由指针gendisk->part_tbl指向,这里分配数组只有1个成员
if (disk_expand_part_tbl(disk, 0)) {
free_part_stats(&disk->part0);
kfree(disk);
return NULL;
}
// disk->part0为主分区,gendisk->part_tbl->part[0]指向主分区
disk->part_tbl->part[0] = &disk->part0;
disk->minors = minors; //设置从设备号的个数
rand_initialize_disk(disk);
disk_to_dev(disk)->class = &block_class; //设置设备类型
disk_to_dev(disk)->type = &disk_type; //在前面博文“Sysfs实现原理”中有讲解
device_initialize(disk_to_dev(disk)); //初始化主分区设备gendisk->part0.__dev
}
return disk;
}
gendisk添加到系统
blk_alloc_devt:为主分区disk->part0分配设备号
bdi_register_owner:将disk->queue->backing_dev_info添加到全局链表bdi_list
blk_register_region:根据设备号将gendisk添加到哈希表bdev_map中
register_disk:完成gendisk注册的主要工作blkdev_get会扫描分区并将其添加到系统
blk_register_queue:将request_queue->kobj添加到kobject管理框架中去,并且在sys下创建相关目录文件
static void register_disk(struct device *parent, struct gendisk *disk)
{
struct device *ddev = disk_to_dev(disk);
struct block_device *bdev;
struct disk_part_iter piter;
struct hd_struct *part;
int err;
ddev->parent = parent;
dev_set_uevent_suppress(ddev, 1); //禁止上报uevent事件等rescan之后统一上报
......
bdev = bdget_disk(disk, 0);
if (!bdev)
goto exit;
bdev->bd_invalidated = 1; //设置分区无效标志,blkdev_get中检测到这个标志就会rescan分区
err = blkdev_get(bdev, FMODE_READ, NULL);//主要工作是rescan分区
if (err < 0)
goto exit;
exit:
dev_set_uevent_suppress(ddev, 0); //允许上报uevent事件
kobject_uevent(&ddev->kobj, KOBJ_ADD);
disk_part_iter_init(&piter, disk, 0);
while ((part = disk_part_iter_next(&piter))) //遍历每一个分区上报uevent
kobject_uevent(&part_to_dev(part)->kobj, KOBJ_ADD);
disk_part_iter_exit(&piter);
}
前面函数blkdev_get会调用到__blkdev_get:
static int __blkdev_get(struct block_device *bdev, fmode_t mode, int for_part)
{
struct gendisk *disk;
struct module *owner;
int ret;
int partno;
int perm = 0;
restart:
disk = get_gendisk(bdev->bd_dev, &partno);
if (!disk)
goto out;
if (!bdev->bd_openers) {
bdev->bd_disk = disk;
bdev->bd_queue = disk->queue;
bdev->bd_contains = bdev;
if (!partno) {
ret = -ENXIO;
bdev->bd_part = disk_get_part(disk, partno);
if (!bdev->bd_part)
goto out_clear;
ret = 0;
if (disk->fops->open) {
ret = disk->fops->open(bdev, mode); //调用特定设备的open函数
}
if (bdev->bd_invalidated) { //前面函数register_disk中有设置bd_invalidated
if (!ret)
rescan_partitions(disk, bdev);//重建分区
else if (ret == -ENOMEDIUM)
invalidate_partitions(disk, bdev);
}
......
}
rescan_partitions是建立磁盘分区的核心函数:
int rescan_partitions(struct gendisk *disk, struct block_device *bdev)
{
struct parsed_partitions *state = NULL;
struct hd_struct *part;
int p, highest, res;
rescan:
if (!get_capacity(disk) || !(state = check_partition(disk, bdev)))//检查磁盘存在的分区
return 0;
......
for (p = 1, highest = 0; p < state->limit; p++)
if (state->parts[p].size)
highest = p;
disk_expand_part_tbl(disk, highest);
for (p = 1; p < state->limit; p++) {//将每一个分区添通过函数add_partition加到系统
sector_t size, from;
size = state->parts[p].size;
if (!size)
continue;
from = state->parts[p].from;
......
part = add_partition(disk, p, from, size,
state->parts[p].flags,
&state->parts[p].info);
......
}
return 0;
}
扫描磁盘分区
结构体parsed_partitions用于暂存磁盘分区信息
struct parsed_partitions {
……
struct {
sector_t from; //起始扇区
sector_t size; //分区大小
……
} *parts; //分区信息数组
……
};
struct parsed_partitions *
check_partition(struct gendisk *hd, struct block_device *bdev)
{
struct parsed_partitions *state;
int i, res, err;
state = allocate_partitions(hd); //分配state用于暂时管理分区信息
if (!state)
return NULL;
state->pp_buf[0] = '\0';
state->bdev = bdev;
……
while (!res && check_part[i]) {
memset(state->parts, 0, state->limit * sizeof(state->parts[0]));
res = check_part[i++](state); //获取分区信息并填充state
if (res < 0) {
err = res;
res = 0;
}
}
……
}
check_part是一个指针函数数组,不同的分区划分方式需要不同的函数来解决分区信息。
static int (*check_part[])(struct parsed_partitions *) = {
......
#ifdef CONFIG_CMDLINE_PARTITION
cmdline_partition,// 通过命令行获取分区信息
#endif
#ifdef CONFIG_EFI_PARTITION
efi_partition, //通过MBR获取分区信息
#endif
......
NULL
};
添加分区到系统
struct hd_struct *add_partition(struct gendisk *disk, int partno,
sector_t start, sector_t len, int flags,
struct partition_meta_info *info)
{
struct hd_struct *p;
dev_t devt = MKDEV(0, 0);
struct device *ddev = disk_to_dev(disk);
struct device *pdev;
struct disk_part_tbl *ptbl;
const char *dname;
int err;
err = disk_expand_part_tbl(disk, partno); //扩展磁盘分区
if (err)
return ERR_PTR(err);
ptbl = disk->part_tbl;
p = kzalloc(sizeof(*p), GFP_KERNEL); //分配结构体hd_struct用于描述一个分区
if (!p)
return ERR_PTR(-EBUSY);
pdev = part_to_dev(p);
p->start_sect = start; //分区起始位置
p->nr_sects = len; //分区大小
p->partno = partno;//分区编号
p->policy = get_disk_ro(disk);
dev_set_name(pdev, "%s%d", dname, partno);
device_initialize(pdev); //初始化分区对应的hd_struc->__dev
pdev->class = &block_class; //….
pdev->type = &part_type;//…
pdev->parent = ddev;
err = blk_alloc_devt(p, &devt); //分配分区设备号
if (err)
goto out_free_info;
pdev->devt = devt;
dev_set_uevent_suppress(pdev, 1); //禁止uevent事件
err = device_add(pdev); //添加hd_struc->__dev到系统
if (err)
goto out_put;
dev_set_uevent_suppress(pdev, 0);
rcu_assign_pointer(ptbl->part[partno], p);
if (!dev_get_uevent_suppress(ddev))
kobject_uevent(&pdev->kobj, KOBJ_ADD);
return p;
......
}
这里仍然以mmc驱动为例:
int mmc_init_queue(struct mmc_queue *mq, struct mmc_card *card,
spinlock_t *lock, const char *subname)
{
struct mmc_host *host = card->host;
u64 limit = BLK_BOUNCE_HIGH;
......
//分配请求队列设置请求处理函数为mmc_request_fn
mq->queue = blk_init_queue(mmc_request_fn, lock);
if (!mq->queue)
return -ENOMEM;
......
mq->thread = kthread_run(mmc_queue_thread, mq, "mmcqd/%d%s",
host->index, subname ? subname : ""); //后面再请求处理的时候会介绍
return 0;
}
请求队列处理流程:
blk_alloc_queue_node:分配请求队列结构体request_queue,并初始化相关字段
q->request_fn = rfn:设置请求处理函数,这里实例中函数为mmc_request_fn
blk_init_allocated_queue:请求队列初始化和io调度类的选择和初始化
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
{
q->nr_requests = BLKDEV_MAX_RQ;//设置最大请求数为128
q->make_request_fn = mfn;//用于根据bio创建请求,这里是函数blk_queue_bio
blk_queue_dma_alignment(q, 511); //dma传送时候的对齐限制
blk_queue_congestion_threshold(q);
q->nr_batching = BLK_BATCH_REQ; //
blk_set_default_limits(&q->limits); //设置一些io请求的最大限制
blk_queue_bounce_limit(q, BLK_BOUNCE_HIGH); //I/O操作时候能够访问的最高物理内存
}
int elevator_init(struct request_queue *q, char *name)
{
struct elevator_type *e = NULL;
int err;
......
if (!e) {
if (q->mq_ops) {
......
} else
// 所有的io调度策略都放在elv_list上,elevator_get根据策略名查找相关策略
e = elevator_get(CONFIG_DEFAULT_IOSCHED, false); //
......
}
if (e->uses_mq)
err = blk_mq_init_sched(q, e);
else
err = e->ops.sq.elevator_init_fn(q, e); //分配并初始化 q->elevator
if (err)
elevator_put(e);
return err;
}
内核中提供了多种调度策略,这里以iosched_deadline为例来讲解调度策略的具体实现:
static struct elevator_type iosched_deadline = {
.ops.sq = {
.elevator_merge_fn = deadline_merge, //检查bio是否可以合并到现有请求中
.elevator_merged_fn = deadline_merged_request,//请求和bio合并之后重启插入红黑树中,因为请求的数据量发生变化了
.elevator_merge_req_fn = deadline_merged_requests,//两个请求发生了合并就删除next请求,修改req->fifo_time
.elevator_dispatch_fn = deadline_dispatch_requests,//从请求队列中选择下一个需要调度的请求
.elevator_add_req_fn = deadline_add_request,//添加请求到请求队列
.elevator_former_req_fn = elv_rb_former_request,//返回给定请求的前一个请求
.elevator_latter_req_fn = elv_rb_latter_request,//返回给定请求的后一个请求
.elevator_init_fn = deadline_init_queue,//队列初始化
.elevator_exit_fn = deadline_exit_queue,
},
.elevator_attrs = deadline_attrs,
.elevator_name = "deadline", //调度策略名
.elevator_owner = THIS_MODULE,
};
static int __init deadline_init(void)
{
return elv_register(&iosched_deadline); //将iosched_deadline添加到全局链表elv_list中
}
static int deadline_init_queue(struct request_queue *q, struct elevator_type *e)
{
struct deadline_data *dd; //用于管理请求的各种链表和参数
struct elevator_queue *eq; //请求队列对象
eq = elevator_alloc(q, e);
if (!eq)
return -ENOMEM;
dd = kzalloc_node(sizeof(*dd), GFP_KERNEL, q->node);
if (!dd) {
kobject_put(&eq->kobj);
return -ENOMEM;
}
eq->elevator_data = dd; //
INIT_LIST_HEAD(&dd->fifo_list[READ]);
INIT_LIST_HEAD(&dd->fifo_list[WRITE]);
dd->sort_list[READ] = RB_ROOT; //用于管理读请求的红黑树
dd->sort_list[WRITE] = RB_ROOT;// 用于管理写请求的红黑树
dd->fifo_expire[READ] = read_expire; //请求在提交后到被调度的截止时间,0.5s
dd->fifo_expire[WRITE] = write_expire;// 请求在提交后到被调度的截止时间,5s
dd->writes_starved = writes_starved;//在写请求被调度之前最多连续发起几次读调度,默认为2
dd->front_merges = 1; //向前合并
dd->fifo_batch = fifo_batch;//一次处理请求的数量
spin_lock_irq(q->queue_lock);
q->elevator = eq; //让请求队列的q->elevator指向elevator_queue
spin_unlock_irq(q->queue_lock);
return 0;
}
static void
deadline_add_request(struct request_queue *q, struct request *rq)
{
struct deadline_data *dd = q->elevator->elevator_data;
const int data_dir = rq_data_dir(rq);
deadline_add_rq_rb(dd, rq); //根据请求数据在磁盘上的位置将请求添加到红黑树dd->sort_list中
rq->fifo_time = jiffies + dd->fifo_expire[data_dir];//更新请求到期时间
list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]);//将请求添加到dd->fifo_list
}
static int deadline_dispatch_requests(struct request_queue *q, int force)
{
struct deadline_data *dd = q->elevator->elevator_data;
const int reads = !list_empty(&dd->fifo_list[READ]);
const int writes = !list_empty(&dd->fifo_list[WRITE]);
struct request *rq;
int data_dir;
// next_rq缓存的是最近一次访问的请求在dd->sort_list中的next节点
if (dd->next_rq[WRITE])
rq = dd->next_rq[WRITE];
else
rq = dd->next_rq[READ];
//如果dd->next_rq有缓存请求并且dd->batching小于dd->fifo_batch就直接选择该请求发起io调度
//因为next_rq存放的请求与最近一次处理请求的数据位置接近,可以减少寻道时间
if (rq && dd->batching < dd->fifo_batch)
goto dispatch_request;
//优先处理读请求,但是如果已经连续处理两次读请求为避免饿死写请求,就发起一次读请求调度
if (reads) {
if (writes && (dd->starved++ >= dd->writes_starved))
goto dispatch_writes;
data_dir = READ;
goto dispatch_find_request;
}
//处理写请求
if (writes) {
dispatch_writes:
dd->starved = 0;
data_dir = WRITE;
goto dispatch_find_request;
}
return 0;
dispatch_find_request:
if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
rq = rq_entry_fifo(dd->fifo_list[data_dir].next);//从dd->fifo_list中取出到期的请求
} else {
rq = dd->next_rq[data_dir];
}
dd->batching = 0;
dispatch_request:
dd->batching++;
//1,取出rq 在dd->sort_list中的下一个请求放到dd->next_rq中;2,将当前请求从队列adline_data的各
//删除;3,将请求添加到请求队列的ue ->queue_head准备io调度
deadline_move_request(dd, rq);
return 1;
}
下面讲解请求提交的例子来源于linux-4.12.3/fs/mpage.c
mpage_alloc:分配bio并设置请求的起始扇区bio->bi_iter.bi_sector = first_sector;
bio_add_page:设置bio的数据buffer,buffer可能不是连续的,这些不连续的缓存用一个数组来管理
bio->bi_io_vec,但是一个bio请求的数据在磁盘上是连续的。
mpage_bio_submit:将bio提交到请求队列。
bio->bi_end_io = mpage_end_io:bio处理之后会调用到,遍历bio的每一个页,设置相关标志比如
SetPageUptodate。然后通知等待在这个页上的进程。
blk_qc_t generic_make_request(struct bio *bio)
{
struct bio_list bio_list_on_stack[2];
blk_qc_t ret = BLK_QC_T_NONE;
//如果当前进程正在处理bio提交的流程,再次提交bio就先放到current->bio_list中, 在请求提交的底层机
//制中可能将bio拆分成几个bio重新提交
if (current->bio_list) {
bio_list_add(¤t->bio_list[0], bio);
goto out;
}
bio_list_init(&bio_list_on_stack[0]);
current->bio_list = bio_list_on_stack; //设置current->bio_list用于存放在bio提交流程中再次提交bio
do {
struct request_queue *q = bdev_get_queue(bio->bi_bdev);
if (likely(blk_queue_enter(q, false) == 0)) {//潘丹请求队列是否繁忙
struct bio_list lower, same;
bio_list_on_stack[1] = bio_list_on_stack[0];
bio_list_init(&bio_list_on_stack[0]);
//在第2节请求队列中有设置make_request_fn 为blk_queue_bio,这个函数后面细讲
ret = q->make_request_fn(q, bio);
blk_queue_exit(q);
bio_list_init(&lower);
bio_list_init(&same);
while ((bio = bio_list_pop(&bio_list_on_stack[0])) != NULL)
//将缓存在current->bio_list上的bio取出来,请求队列与当前处理bio相同的放到same,不同的放lower
if (q == bdev_get_queue(bio->bi_bdev))
bio_list_add(&same, bio);
else
bio_list_add(&lower, bio);
//将链表lower,same和bio_list_on_stack[1]拼接起来放到bio_list_on_stack[0]上
bio_list_merge(&bio_list_on_stack[0], &lower);
bio_list_merge(&bio_list_on_stack[0], &same);
bio_list_merge(&bio_list_on_stack[0], &bio_list_on_stack[1]);
} else {
bio_io_error(bio);
}
bio = bio_list_pop(&bio_list_on_stack[0]); //弹出bio提交到请求队列直到bio_list_on_stack[0]为空
} while (bio);
current->bio_list = NULL; /* deactivate */
out:
return ret;
}
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
{
struct blk_plug *plug;
int where = ELEVATOR_INSERT_SORT;
struct request *req, *free;
unsigned int request_count = 0;
unsigned int wb_acct;
//检查bio中page是否满足内存区间限制,如果不满足就clone一个bio将page分配在规定的区间
blk_queue_bounce(q, &bio);
//检查bio是否超过一些边界限制,如果超过了就将bio拆分,并且将拆分后的bio重新提交
blk_queue_split(q, &bio, q->bio_split);
//如果请求队列禁止合并就尝试将bio合并到current->plug中的请求中。在进行大量数据读写的时候如果每
//一次bio都获取q->queue_lock进入请求队列处理效率不高,可以在数据读写前blk_start_plug(&plug);,后
//面的bio就会合并到current->plug中,数据请求结束调用blk_finish_plug(&plug);将对current->plug中的bio
//请求统一处理
if (!blk_queue_nomerges(q)) {
if (blk_attempt_plug_merge(q, bio, &request_count, NULL))
return BLK_QC_T_NONE;
} else
request_count = blk_plug_queued_count(q);
spin_lock_irq(q->queue_lock);
//根据bio->bi_iter.bi_sector和 rq->__sector以及bio和请求的读写方向来判断合并的类型,向前、向后或者
//不能合并
switch (elv_merge(q, &req, bio)) {
case ELEVATOR_BACK_MERGE:
if (!bio_attempt_back_merge(q, req, bio)) //尝试将bio合并到req
break;
elv_bio_merged(q, req, bio);
//因为合并bio之后req大小扩展了,再尝试向后一个请求next合并
free = attempt_back_merge(q, req);
if (free)
__blk_put_request(q, free);//释放掉被合并的请求
else
elv_merged_request(q, req, ELEVATOR_BACK_MERGE); //将req重新插入红黑树
goto out_unlock;
case ELEVATOR_FRONT_MERGE: //向前合并与前面向后合并逻辑相同
if (!bio_attempt_front_merge(q, req, bio))
break;
elv_bio_merged(q, req, bio);
free = attempt_front_merge(q, req);
if (free)
__blk_put_request(q, free);
else
elv_merged_request(q, req, ELEVATOR_FRONT_MERGE);
goto out_unlock;
default:
break;
}
get_rq:
wb_acct = wbt_wait(q->rq_wb, bio, q->queue_lock);
//前面的合并尝试都没有成功,所以需要分配一个req给bio
req = get_request(q, bio->bi_opf, bio, GFP_NOIO);
if (IS_ERR(req)) {
__wbt_done(q->rq_wb, wb_acct);
bio->bi_error = PTR_ERR(req);
bio_endio(bio);
goto out_unlock;
}
wbt_track(&req->issue_stat, wb_acct);
//用bio初始化req,日中req->__sector = bio->bi_iter.bi_sector rq->__data_len和rq->__data_len = bio->bi_iter.bi_size对理解请求提交的逻辑比较重要
blk_init_request_from_bio(req, bio);
//如果设置了current->plug,就将这个请求添加到current->plug中
plug = current->plug;
if (plug) {
if (!request_count || list_empty(&plug->list))
trace_block_plug(q);
else {
struct request *last = list_entry_rq(plug->list.prev);
if (request_count >= BLK_MAX_REQUEST_COUNT ||
blk_rq_bytes(last) >= BLK_PLUG_FLUSH_SIZE) {
blk_flush_plug_list(plug, false);
trace_block_plug(q);
}
}
list_add_tail(&req->queuelist, &plug->list);
blk_account_io_start(req, true);
} else {
spin_lock_irq(q->queue_lock);
// 如果前面的都没有满足就字节插入红黑树,ELEVATOR_INSERT_SORT
add_acct_request(q, req, where);
__blk_run_queue(q); //调用q->request_fn(q)处理请求队列上的请求
out_unlock:
spin_unlock_irq(q->queue_lock);
}
return BLK_QC_T_NONE;
}
前面第2节我们讲了mmc驱动的例子,里面设置q->request_fn为mmc_request_fn,这里再贴一遍代码
int mmc_init_queue(struct mmc_queue *mq, struct mmc_card *card,
spinlock_t *lock, const char *subname)
{
......
//分配请求队列设置请求处理函数为mmc_request_fn
mq->queue = blk_init_queue(mmc_request_fn, lock);
if (!mq->queue)
return -ENOMEM;
......
//请求处理函数mmc_request_fn通过调用函数q->elevator->type->ops.sq.elevator_dispatch_fn从队列上
//选取一个请求,然后唤醒下面内核线程做真正的请求处理
mq->thread = kthread_run(mmc_queue_thread, mq, "mmcqd/%d%s",
host->index, subname ? subname : "");
return 0;
}
struct inode {
umode_t i_mode; //文件类型,例如S_IFCHR(字符设备),S_IFBLK(块设备)
......
dev_t i_rdev; //如果是设备文件表示设备号,普通文件表示文件所在存储设备的设备号
......
const struct file_operations *i_fop; //文件数据操作函数集
union {
struct block_device *i_bdev;//指向块设备对应的block_device
struct cdev *i_cdev; //指向字符设备对应的cdev
......
};
......
};
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops; //如果是字符设备设置i_fop指向def_chr_fops
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
......
}
通用块设备操作函数集如下:
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read_iter = blkdev_read_iter,
.write_iter = blkdev_write_iter,
.mmap = generic_file_mmap,
.fsync = blkdev_fsync,
......
};
bdget:从伪文件系统"bdev"中获取块设备对应的bdev_inode,其中包含一个block_device和inode
bdev->bd_inode->i_mapping:就是def_blk_aops,包含了直接通过块设备而不是文件系统读写数据的方法
__blkdev_get:从根据设备号从bdev_map中获取gendisk,关联block_device和gendisk,bdev->bd_disk = disk;