You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

757 lines
27 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 35 | 块设备(下):如何建立代理商销售模式?
在[文件系统](https://time.geekbang.org/column/article/97876)那一节我们讲了文件的写入到了设备驱动这一层就没有再往下分析。上一节我们又讲了mount一个块设备将block\_device信息放到了ext4文件系统的super\_block里面有了这些基础是时候把整个写入的故事串起来了。
还记得咱们在文件系统那一节分析写入流程的时候对于ext4文件系统最后调用的是ext4\_file\_write\_iter它将I/O的调用分成两种情况
第一是**直接I/O**。最终我们调用的是generic\_file\_direct\_write这里调用的是mapping->a\_ops->direct\_IO实际调用的是ext4\_direct\_IO往设备层写入数据。
第二种是**缓存I/O**。最终我们会将数据从应用拷贝到内存缓存中但是这个时候并不执行真正的I/O操作。它们只将整个页或其中部分标记为脏。写操作由一个timer触发那个时候才调用wb\_workfn往硬盘写入页面。
接下来的调用链为wb\_workfn->wb\_do\_writeback->wb\_writeback->writeback\_sb\_inodes->\_\_writeback\_single\_inode->do\_writepages。在do\_writepages中我们要调用mapping->a\_ops->writepages但实际调用的是ext4\_writepages往设备层写入数据。
这一节,我们就沿着这两种情况分析下去。
## 直接I/O如何访问块设备
我们先来看第一种情况直接I/O调用到ext4\_direct\_IO。
```
static ssize_t ext4_direct_IO(struct kiocb *iocb, struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
struct inode *inode = file->f_mapping->host;
size_t count = iov_iter_count(iter);
loff_t offset = iocb->ki_pos;
ssize_t ret;
......
ret = ext4_direct_IO_write(iocb, iter);
......
}
static ssize_t ext4_direct_IO_write(struct kiocb *iocb, struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
struct inode *inode = file->f_mapping->host;
struct ext4_inode_info *ei = EXT4_I(inode);
ssize_t ret;
loff_t offset = iocb->ki_pos;
size_t count = iov_iter_count(iter);
......
ret = __blockdev_direct_IO(iocb, inode, inode->i_sb->s_bdev, iter,
get_block_func, ext4_end_io_dio, NULL,
dio_flags);
……
}
```
在ext4\_direct\_IO\_write调用\_\_blockdev\_direct\_IO有个参数你需要特别注意一下那就是inode->i\_sb->s\_bdev。通过当前文件的inode我们可以得到super\_block。这个super\_block中的s\_bdev就是咱们上一节填进去的那个block\_device。
\_\_blockdev\_direct\_IO会调用do\_blockdev\_direct\_IO在这里面我们要准备一个struct dio结构和struct dio\_submit结构用来描述将要发生的写入请求。
```
static inline ssize_t
do_blockdev_direct_IO(struct kiocb *iocb, struct inode *inode,
struct block_device *bdev, struct iov_iter *iter,
get_block_t get_block, dio_iodone_t end_io,
dio_submit_t submit_io, int flags)
{
unsigned i_blkbits = ACCESS_ONCE(inode->i_blkbits);
unsigned blkbits = i_blkbits;
unsigned blocksize_mask = (1 << blkbits) - 1;
ssize_t retval = -EINVAL;
size_t count = iov_iter_count(iter);
loff_t offset = iocb->ki_pos;
loff_t end = offset + count;
struct dio *dio;
struct dio_submit sdio = { 0, };
struct buffer_head map_bh = { 0, };
......
dio = kmem_cache_alloc(dio_cache, GFP_KERNEL);
dio->flags = flags;
dio->i_size = i_size_read(inode);
dio->inode = inode;
if (iov_iter_rw(iter) == WRITE) {
dio->op = REQ_OP_WRITE;
dio->op_flags = REQ_SYNC | REQ_IDLE;
if (iocb->ki_flags & IOCB_NOWAIT)
dio->op_flags |= REQ_NOWAIT;
} else {
dio->op = REQ_OP_READ;
}
sdio.blkbits = blkbits;
sdio.blkfactor = i_blkbits - blkbits;
sdio.block_in_file = offset >> blkbits;
sdio.get_block = get_block;
dio->end_io = end_io;
sdio.submit_io = submit_io;
sdio.final_block_in_bio = -1;
sdio.next_block_for_io = -1;
dio->iocb = iocb;
dio->refcount = 1;
sdio.iter = iter;
sdio.final_block_in_request =
(offset + iov_iter_count(iter)) >> blkbits;
......
sdio.pages_in_io += iov_iter_npages(iter, INT_MAX);
retval = do_direct_IO(dio, &sdio, &map_bh);
.....
}
```
do\_direct\_IO里面有两层循环第一层循环是依次处理这次要写入的所有块。对于每一块取出对应的内存中的页page在这一块中有写入的起始地址from和终止地址to所以第二层循环就是依次处理from到to的数据调用submit\_page\_section提交到块设备层进行写入。
```
static int do_direct_IO(struct dio *dio, struct dio_submit *sdio,
struct buffer_head *map_bh)
{
const unsigned blkbits = sdio->blkbits;
const unsigned i_blkbits = blkbits + sdio->blkfactor;
int ret = 0;
while (sdio->block_in_file < sdio->final_block_in_request) {
struct page *page;
size_t from, to;
page = dio_get_page(dio, sdio);
from = sdio->head ? 0 : sdio->from;
to = (sdio->head == sdio->tail - 1) ? sdio->to : PAGE_SIZE;
sdio->head++;
while (from < to) {
unsigned this_chunk_bytes; /* # of bytes mapped */
unsigned this_chunk_blocks; /* # of blocks */
......
ret = submit_page_section(dio, sdio, page,
from,
this_chunk_bytes,
sdio->next_block_for_io,
map_bh);
......
sdio->next_block_for_io += this_chunk_blocks;
sdio->block_in_file += this_chunk_blocks;
from += this_chunk_bytes;
dio->result += this_chunk_bytes;
sdio->blocks_available -= this_chunk_blocks;
if (sdio->block_in_file == sdio->final_block_in_request)
break;
......
}
}
}
```
submit\_page\_section会调用dio\_bio\_submit进而调用submit\_bio向块设备层提交数据。其中参数struct bio是将数据传给块设备的通用传输对象。定义如下
```
/**
* submit_bio - submit a bio to the block device layer for I/O
* @bio: The &struct bio which describes the I/O
*/
blk_qc_t submit_bio(struct bio *bio)
{
......
return generic_make_request(bio);
}
```
## 缓存I/O如何访问块设备
我们再来看第二种情况缓存I/O调用到ext4\_writepages。这个函数比较长我们这里只截取最重要的部分来讲解。
```
static int ext4_writepages(struct address_space *mapping,
struct writeback_control *wbc)
{
......
struct mpage_da_data mpd;
struct inode *inode = mapping->host;
struct ext4_sb_info *sbi = EXT4_SB(mapping->host->i_sb);
......
mpd.do_map = 0;
mpd.io_submit.io_end = ext4_init_io_end(inode, GFP_KERNEL);
ret = mpage_prepare_extent_to_map(&mpd);
/* Submit prepared bio */
ext4_io_submit(&mpd.io_submit);
......
}
```
这里比较重要的一个数据结构是struct mpage\_da\_data。这里面有文件的inode、要写入的页的偏移量还有一个重要的struct ext4\_io\_submit里面有通用传输对象bio。
```
struct mpage_da_data {
struct inode *inode;
......
pgoff_t first_page; /* The first page to write */
pgoff_t next_page; /* Current page to examine */
pgoff_t last_page; /* Last page to examine */
struct ext4_map_blocks map;
struct ext4_io_submit io_submit; /* IO submission data */
unsigned int do_map:1;
};
struct ext4_io_submit {
......
struct bio *io_bio;
ext4_io_end_t *io_end;
sector_t io_next_block;
};
```
在ext4\_writepages中mpage\_prepare\_extent\_to\_map用于初始化这个struct mpage\_da\_data结构。接下来的调用链为mpage\_prepare\_extent\_to\_map->mpage\_process\_page\_bufs->mpage\_submit\_page->ext4\_bio\_write\_page->io\_submit\_add\_bh。
在io\_submit\_add\_bh中此时的bio还是空的因而我们要调用io\_submit\_init\_bio初始化bio。
```
static int io_submit_init_bio(struct ext4_io_submit *io,
struct buffer_head *bh)
{
struct bio *bio;
bio = bio_alloc(GFP_NOIO, BIO_MAX_PAGES);
if (!bio)
return -ENOMEM;
wbc_init_bio(io->io_wbc, bio);
bio->bi_iter.bi_sector = bh->b_blocknr * (bh->b_size >> 9);
bio->bi_bdev = bh->b_bdev;
bio->bi_end_io = ext4_end_bio;
bio->bi_private = ext4_get_io_end(io->io_end);
io->io_bio = bio;
io->io_next_block = bh->b_blocknr;
return 0;
}
```
我们再回到ext4\_writepages中。在bio初始化完之后我们要调用ext4\_io\_submit提交I/O。在这里我们又是调用submit\_bio向块设备层传输数据。ext4\_io\_submit的实现如下
```
void ext4_io_submit(struct ext4_io_submit *io)
{
struct bio *bio = io->io_bio;
if (bio) {
int io_op_flags = io->io_wbc->sync_mode == WB_SYNC_ALL ?
REQ_SYNC : 0;
io->io_bio->bi_write_hint = io->io_end->inode->i_write_hint;
bio_set_op_attrs(io->io_bio, REQ_OP_WRITE, io_op_flags);
submit_bio(io->io_bio);
}
io->io_bio = NULL;
}
```
## 如何向块设备层提交请求?
既然不管是直接I/O还是缓存I/O最后都到了submit\_bio里面那我们就来重点分析一下它。
submit\_bio会调用generic\_make\_request。代码如下
```
blk_qc_t generic_make_request(struct bio *bio)
{
/*
* bio_list_on_stack[0] contains bios submitted by the current
* make_request_fn.
* bio_list_on_stack[1] contains bios that were submitted before
* the current make_request_fn, but that haven't been processed
* yet.
*/
struct bio_list bio_list_on_stack[2];
blk_qc_t ret = BLK_QC_T_NONE;
......
if (current->bio_list) {
bio_list_add(&current->bio_list[0], bio);
goto out;
}
bio_list_init(&bio_list_on_stack[0]);
current->bio_list = bio_list_on_stack;
do {
struct request_queue *q = bdev_get_queue(bio->bi_bdev);
if (likely(blk_queue_enter(q, bio->bi_opf & REQ_NOWAIT) == 0)) {
struct bio_list lower, same;
/* Create a fresh bio_list for all subordinate requests */
bio_list_on_stack[1] = bio_list_on_stack[0];
bio_list_init(&bio_list_on_stack[0]);
ret = q->make_request_fn(q, bio);
blk_queue_exit(q);
/* sort new bios into those for a lower level
* and those for the same level
*/
bio_list_init(&lower);
bio_list_init(&same);
while ((bio = bio_list_pop(&bio_list_on_stack[0])) != NULL)
if (q == bdev_get_queue(bio->bi_bdev))
bio_list_add(&same, bio);
else
bio_list_add(&lower, bio);
/* now assemble so we handle the lowest level first */
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]);
}
......
bio = bio_list_pop(&bio_list_on_stack[0]);
} while (bio);
current->bio_list = NULL; /* deactivate */
out:
return ret;
}
```
这里的逻辑有点复杂我们先来看大的逻辑。在do-while中我们先是获取一个请求队列request\_queue然后调用这个队列的make\_request\_fn函数。
### 块设备队列结构
如果再来看struct block\_device结构和struct gendisk结构我们会发现每个块设备都有一个请求队列struct request\_queue用于处理上层发来的请求。
在每个块设备的驱动程序初始化的时候会生成一个request\_queue。
```
struct request_queue {
/*
* Together with queue_head for cacheline sharing
*/
struct list_head queue_head;
struct request *last_merge;
struct elevator_queue *elevator;
......
request_fn_proc *request_fn;
make_request_fn *make_request_fn;
......
}
```
在请求队列request\_queue上首先是有一个链表list\_head保存请求request。
```
struct request {
struct list_head queuelist;
......
struct request_queue *q;
......
struct bio *bio;
struct bio *biotail;
......
}
```
每个request包括一个链表的struct bio有指针指向一头一尾。
```
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
blk_status_t bi_status;
......
struct bvec_iter bi_iter;
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short 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 */
......
};
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
}
```
在bio中bi\_next是链表中的下一项struct bio\_vec指向一组页面。
![](https://static001.geekbang.org/resource/image/3c/0e/3c473d163b6e90985d7301f115ab660e.jpeg)
在请求队列request\_queue上还有两个重要的函数一个是make\_request\_fn函数用于生成request另一个是request\_fn函数用于处理request。
### 块设备的初始化
我们还是以scsi驱动为例。在初始化设备驱动的时候我们会调用scsi\_alloc\_queue把request\_fn设置为scsi\_request\_fn。我们还会调用blk\_init\_allocated\_queue->blk\_queue\_make\_request把make\_request\_fn设置为blk\_queue\_bio。
```
/**
* scsi_alloc_sdev - allocate and setup a scsi_Device
* @starget: which target to allocate a &scsi_device for
* @lun: which lun
* @hostdata: usually NULL and set by ->slave_alloc instead
*
* Description:
* Allocate, initialize for io, and return a pointer to a scsi_Device.
* Stores the @shost, @channel, @id, and @lun in the scsi_Device, and
* adds scsi_Device to the appropriate list.
*
* Return value:
* scsi_Device pointer, or NULL on failure.
**/
static struct scsi_device *scsi_alloc_sdev(struct scsi_target *starget,
u64 lun, void *hostdata)
{
struct scsi_device *sdev;
sdev = kzalloc(sizeof(*sdev) + shost->transportt->device_size,
GFP_ATOMIC);
......
sdev->request_queue = scsi_alloc_queue(sdev);
......
}
struct request_queue *scsi_alloc_queue(struct scsi_device *sdev)
{
struct Scsi_Host *shost = sdev->host;
struct request_queue *q;
q = blk_alloc_queue_node(GFP_KERNEL, NUMA_NO_NODE);
if (!q)
return NULL;
q->cmd_size = sizeof(struct scsi_cmnd) + shost->hostt->cmd_size;
q->rq_alloc_data = shost;
q->request_fn = scsi_request_fn;
q->init_rq_fn = scsi_init_rq;
q->exit_rq_fn = scsi_exit_rq;
q->initialize_rq_fn = scsi_initialize_rq;
//调用blk_queue_make_request(q, blk_queue_bio);
if (blk_init_allocated_queue(q) < 0) {
blk_cleanup_queue(q);
return NULL;
}
__scsi_init_queue(shost, q);
......
return q
}
```
在blk\_init\_allocated\_queue中除了初始化make\_request\_fn函数我们还要做一件很重要的事情就是初始化I/O的电梯算法。
```
int blk_init_allocated_queue(struct request_queue *q)
{
q->fq = blk_alloc_flush_queue(q, NUMA_NO_NODE, q->cmd_size);
......
blk_queue_make_request(q, blk_queue_bio);
......
/* init elevator */
if (elevator_init(q, NULL)) {
......
}
......
}
```
电梯算法有很多种类型定义为elevator\_type。下面我来逐一说一下。
* **struct elevator\_type elevator\_noop**
Noop调度算法是最简单的IO调度算法它将IO请求放入到一个FIFO队列中然后逐个执行这些IO请求。
* **struct elevator\_type iosched\_deadline**
Deadline算法要保证每个IO请求在一定的时间内一定要被服务到以此来避免某个请求饥饿。为了完成这个目标算法中引入了两类队列一类队列用来对请求按起始扇区序号进行排序通过红黑树来组织我们称为sort\_list按照此队列传输性能会比较高另一类队列对请求按它们的生成时间进行排序由链表来组织称为fifo\_list并且每一个请求都有一个期限值。
* **struct elevator\_type iosched\_cfq**
又看到了熟悉的CFQ完全公平调度算法。所有的请求会在多个队列中排序。同一个进程的请求总是在同一队列中处理。时间片会分配到每个队列通过轮询算法我们保证了I/O带宽以公平的方式在不同队列之间进行共享。
elevator\_init中会根据名称来指定电梯算法如果没有选择那就默认使用iosched\_cfq。
### 请求提交与调度
接下来我们回到generic\_make\_request函数中。调用队列的make\_request\_fn函数其实就是调用blk\_queue\_bio。
```
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
{
struct request *req, *free;
unsigned int request_count = 0;
......
switch (elv_merge(q, &req, bio)) {
case ELEVATOR_BACK_MERGE:
if (!bio_attempt_back_merge(q, req, bio))
break;
elv_bio_merged(q, req, bio);
free = attempt_back_merge(q, req);
if (free)
__blk_put_request(q, free);
else
elv_merged_request(q, req, ELEVATOR_BACK_MERGE);
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:
req = get_request(q, bio->bi_opf, bio, GFP_NOIO);
......
blk_init_request_from_bio(req, bio);
......
add_acct_request(q, req, where);
__blk_run_queue(q);
out_unlock:
......
return BLK_QC_T_NONE;
}
```
blk\_queue\_bio首先做的一件事情是调用elv\_merge来判断当前这个bio请求是否能够和目前已有的request合并起来成为同一批I/O操作从而提高读取和写入的性能。
判断标准和struct bio的成员struct bvec\_iter有关它里面有两个变量一个是起始磁盘簇bi\_sector另一个是大小bi\_size。
```
enum elv_merge elv_merge(struct request_queue *q, struct request **req,
struct bio *bio)
{
struct elevator_queue *e = q->elevator;
struct request *__rq;
......
if (q->last_merge && elv_bio_merge_ok(q->last_merge, bio)) {
enum elv_merge ret = blk_try_merge(q->last_merge, bio);
if (ret != ELEVATOR_NO_MERGE) {
*req = q->last_merge;
return ret;
}
}
......
__rq = elv_rqhash_find(q, bio->bi_iter.bi_sector);
if (__rq && elv_bio_merge_ok(__rq, bio)) {
*req = __rq;
return ELEVATOR_BACK_MERGE;
}
if (e->uses_mq && e->type->ops.mq.request_merge)
return e->type->ops.mq.request_merge(q, req, bio);
else if (!e->uses_mq && e->type->ops.sq.elevator_merge_fn)
return e->type->ops.sq.elevator_merge_fn(q, req, bio);
return ELEVATOR_NO_MERGE;
}
```
elv\_merge尝试了三次合并。
第一次它先判断和上一次合并的request能不能再次合并看看能不能赶上马上要走的这部电梯。在blk\_try\_merge主要做了这样的判断如果blk\_rq\_pos(rq) + blk\_rq\_sectors(rq) == bio->bi\_iter.bi\_sector也就是说这个request的起始地址加上它的大小其实是这个request的结束地址如果和bio的起始地址能接得上那就把bio放在request的最后我们称为ELEVATOR\_BACK\_MERGE。
如果blk\_rq\_pos(rq) - bio\_sectors(bio) == bio->bi\_iter.bi\_sector也就是说这个request的起始地址减去bio的大小等于bio的起始地址这说明bio放在request的最前面能够接得上那就把bio放在request的最前面我们称为ELEVATOR\_FRONT\_MERGE。否则那就不合并我们称为ELEVATOR\_NO\_MERGE。
```
enum elv_merge blk_try_merge(struct request *rq, struct bio *bio)
{
......
if (blk_rq_pos(rq) + blk_rq_sectors(rq) == bio->bi_iter.bi_sector)
return ELEVATOR_BACK_MERGE;
else if (blk_rq_pos(rq) - bio_sectors(bio) == bio->bi_iter.bi_sector)
return ELEVATOR_FRONT_MERGE;
return ELEVATOR_NO_MERGE;
}
```
第二次如果和上一个合并过的request无法合并那我们就调用elv\_rqhash\_find。然后按照bio的起始地址查找request看有没有能够合并的。如果有的话因为是按照起始地址找的应该接在人家的后面所以是ELEVATOR\_BACK\_MERGE。
第三次调用elevator\_merge\_fn试图合并。对于iosched\_cfq调用的是cfq\_merge。在这里面cfq\_find\_rq\_fmerge会调用elv\_rb\_find函数里面的参数是bio的结束地址。我们还是要看能不能找到可以合并的。如果有的话因为是按照结束地址找的应该接在人家前面所以是ELEVATOR\_FRONT\_MERGE。
```
static enum elv_merge cfq_merge(struct request_queue *q, struct request **req,
struct bio *bio)
{
struct cfq_data *cfqd = q->elevator->elevator_data;
struct request *__rq;
__rq = cfq_find_rq_fmerge(cfqd, bio);
if (__rq && elv_bio_merge_ok(__rq, bio)) {
*req = __rq;
return ELEVATOR_FRONT_MERGE;
}
return ELEVATOR_NO_MERGE;
}
static struct request *
cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio)
{
struct task_struct *tsk = current;
struct cfq_io_cq *cic;
struct cfq_queue *cfqq;
cic = cfq_cic_lookup(cfqd, tsk->io_context);
if (!cic)
return NULL;
cfqq = cic_to_cfqq(cic, op_is_sync(bio->bi_opf));
if (cfqq)
return elv_rb_find(&cfqq->sort_list, bio_end_sector(bio));
return NUL
}
```
等从elv\_merge返回blk\_queue\_bio的时候我们就知道应该做哪种类型的合并接着就要进行真的合并。如果没有办法合并那就调用get\_request创建一个新的request调用blk\_init\_request\_from\_bio将bio放到新的request里面然后调用add\_acct\_request把新的request加到request\_queue队列中。
至此我们解析完了generic\_make\_request中最重要的两大逻辑获取一个请求队列request\_queue和调用这个队列的make\_request\_fn函数。
其实generic\_make\_request其他部分也很令人困惑。感觉里面有特别多的struct bio\_list倒腾过来倒腾过去的。这是因为很多块设备是有层次的。
比如我们用两块硬盘组成RAID两个RAID盘组成LVM然后我们就可以在LVM上创建一个块设备给用户用我们称接近用户的块设备为**高层次的块设备**,接近底层的块设备为**低层次**lower**的块设备**。这样generic\_make\_request把I/O请求发送给高层次的块设备的时候会调用高层块设备的make\_request\_fn高层块设备又要调用generic\_make\_request将请求发送给低层次的块设备。虽然块设备的层次不会太多但是对于代码generic\_make\_request来讲这可是递归的调用一不小心就会递归过深无法正常退出而且内核栈的大小又非常有限所以要比较小心。
这里你是否理解了struct bio\_list bio\_list\_on\_stack\[2\]的名字为什么叫stack呢其实将栈的操作变成对于队列的操作队列不在栈里面会大很多。每次generic\_make\_request被当前任务调用的时候将current->bio\_list设置为bio\_list\_on\_stack并在generic\_make\_request的一开始就判断current->bio\_list是否为空。如果不为空说明已经在generic\_make\_request的调用里面了就不必调用make\_request\_fn进行递归了直接把请求加入到bio\_list里面就可以了这就实现了递归的及时退出。
如果current->bio\_list为空那我们就将current->bio\_list设置为bio\_list\_on\_stack后进入do-while循环做咱们分析过的generic\_make\_request的两大逻辑。但是当前的队列调用make\_request\_fn的时候在make\_request\_fn的具体实现中会生成新的bio。调用更底层的块设备也会生成新的bio都会放在bio\_list\_on\_stack的队列中是一个边处理还边创建的过程。
bio\_list\_on\_stack\[1\] = bio\_list\_on\_stack\[0\]这一句在make\_request\_fn之前将之前队列里面遗留没有处理的保存下来接着bio\_list\_init将bio\_list\_on\_stack\[0\]设置为空然后调用make\_request\_fn在make\_request\_fn里面如果有新的bio生成都会加到bio\_list\_on\_stack\[0\]这个队列里面来。
make\_request\_fn执行完毕后可以想象bio\_list\_on\_stack\[0\]可能又多了一些bio了接下来的循环中调用bio\_list\_pop将bio\_list\_on\_stack\[0\]积攒的bio拿出来分别放在两个队列lower和same中顾名思义lower就是更低层次的块设备的biosame是同层次的块设备的bio。
接下来我们能将lower、same以及bio\_list\_on\_stack\[1\] 都取出来放在bio\_list\_on\_stack\[0\]统一进行处理。当然应该lower优先了因为只有底层的块设备的I/O做完了上层的块设备的I/O才能做完。
到这里generic\_make\_request的逻辑才算解析完毕。对于写入的数据来讲其实仅仅是将bio请求放在请求队列上设备驱动程序还没往设备里面写呢。
### 请求的处理
设备驱动程序往设备里面写调用的是请求队列request\_queue的另外一个函数request\_fn。对于scsi设备来讲调用的是scsi\_request\_fn。
```
static void scsi_request_fn(struct request_queue *q)
__releases(q->queue_lock)
__acquires(q->queue_lock)
{
struct scsi_device *sdev = q->queuedata;
struct Scsi_Host *shost;
struct scsi_cmnd *cmd;
struct request *req;
/*
* To start with, we keep looping until the queue is empty, or until
* the host is no longer able to accept any more requests.
*/
shost = sdev->host;
for (;;) {
int rtn;
/*
* get next queueable request. We do this early to make sure
* that the request is fully prepared even if we cannot
* accept it.
*/
req = blk_peek_request(q);
......
/*
* Remove the request from the request list.
*/
if (!(blk_queue_tagged(q) && !blk_queue_start_tag(q, req)))
blk_start_request(req);
.....
cmd = req->special;
......
/*
* Dispatch the command to the low-level driver.
*/
cmd->scsi_done = scsi_done;
rtn = scsi_dispatch_cmd(cmd);
......
}
return;
......
}
```
在这里面是一个for无限循环从request\_queue中读取request然后封装更加底层的指令给设备控制器下指令实施真正的I/O操作。
## 总结时刻
这一节我们讲了如何将块设备I/O请求送达到外部设备。
对于块设备的I/O操作分为两种一种是直接I/O另一种是缓存I/O。无论是哪种I/O最终都会调用submit\_bio提交块设备I/O请求。
对于每一种块设备都有一个gendisk表示这个设备它有一个请求队列这个队列是一系列的request对象。每个request对象里面包含多个BIO对象指向page cache。所谓的写入块设备I/O就是将page cache里面的数据写入硬盘。
对于请求队列来讲还有两个函数一个函数叫make\_request\_fn函数用于将请求放入队列。submit\_bio会调用generic\_make\_request然后调用这个函数。
另一个函数往往在设备驱动程序里实现我们叫request\_fn函数它用于从队列里面取出请求来写入外部设备。
![](https://static001.geekbang.org/resource/image/c9/3c/c9f6a08075ba4eae3314523fa258363c.png)
至此,整个写入文件的过程才算完全结束。这真是个复杂的过程,涉及系统调用、内存管理、文件系统和输入输出。这足以说明,操作系统真的是一个非常复杂的体系,环环相扣,需要分层次层层展开来学习。
到这里,专栏已经过半了,你应该能发现,很多我之前说“后面会细讲”的东西,现在正在一点一点解释清楚,而文中越来越多出现“前面我们讲过”的字眼,你是否当时学习前面知识的时候,没有在意,导致学习后面的知识产生困惑了呢?没关系,及时倒回去复习,再回过头去看,当初学过的很多知识会变得清晰很多。
## 课堂练习
你知道如何查看磁盘调度算法、修改磁盘调度算法以及I/O队列的长度吗
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)