Linux文件系统之:通用块处理层 ll_rw_block | +往期文章回顾

/proc 文件系统并使用/proc 进行输入

Linux虚拟文件系统概述

Linux文件系统概述:硬盘驱动>通用块设备层>文件系统>虚拟文件系统(VFS)

Linux内核:一文读懂文件系统、缓冲区高速缓存和块设备、超级块

Linux文件系统:编写一个内核文件系统

Linux系统C语言遍历系统文件系统,并查询其磁盘使用率(有效磁盘)

Linux虚拟文件系统VFS的相关数据结构和操作

Linux文件系统:Linux 内核文件描述符表的演变

Linux的文件系统及文件缓存知识点整理

Linux文件系统之:通用块处理层

目录

概述

通用块读写接口

执行I/O请求


 

概述


由于不同块设备(如磁盘,机械硬盘等)有着不同的设备驱动程序,为了让文件系统有统一的读写块设备接口,Linux实现了一个 通用块层。如下图中的红色部分:

通用块层 的引入为了提供一个统一的接口让文件系统实现者使用,而不用关心不同设备驱动程序的差异,这样实现出来的文件系统就能用于任何的块设备。

通用块层 将对不同块设备的操作转换成对逻辑数据块的操作,也就是将不同的块设备都抽象成是一个数据块数组,而文件系统就是对这些数据块进行管理。如下图:

注意:不同的文件系统可能对逻辑数据块定义的大小不一样,比如 ext2文件系统 的逻辑数据块大小为 4KB。

通过对设备进行抽象后,不管是磁盘还是机械硬盘,对于文件系统都可以使用相同的接口对逻辑数据块进行读写操作。

 

通用块读写接口


通用块层 提供了 ll_rw_block() 函数对逻辑块进行读写操作,ll_rw_block() 函数的原型如下:

void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]);

在分析 ll_rw_block() 函数前,我们先来介绍一下 buffer_head 这个结构,因为要理解 ll_rw_block() 函数必须先了解 buffer_head 结构。

struct buffer_head 结构代表一个要进行读或者写的数据块,其定义如下:

struct buffer_head {
    struct buffer_head *b_next;     /* 用于快速查找数据块缓存 */
    unsigned long b_blocknr;        /* 数据块号 */
    unsigned short b_size;          /* 数据块大小 */
    unsigned short b_list;          /* List that this buffer appears */
    kdev_t b_dev;                   /* 数据块所属设备 */

    atomic_t b_count;               /* 引用计数器 */
    kdev_t b_rdev;                  /* 数据块所属真正设备 */
    ...
};

为了让读者更加清晰,上面忽略了 buffer_head 结构的某些字段,上面比较重要的是 b_blocknr 字段和 b_size 字段, b_blocknr 字段指定了要读写的数据块号,而 b_size 字段指定了数据块的大小。还有就是 b_rdev 字段,其指定了数据块所属的设备。

有了 buffer_head 结构,就可以对数据块进行读写操作。接下来,我们看看 ll_rw_block() 函数的实现:

void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
{
    ...
    for (i = 0; i < nr; i++) {
        struct buffer_head *bh = bhs[i];

        /* 上锁 */
        if (test_and_set_bit(BH_Lock, &bh->b_state))
            continue;

        /* 增加buffer_head的计数器 */
        atomic_inc(&bh->b_count);
        bh->b_end_io = end_buffer_io_sync;
        ...
        submit_bh(rw, bh);
    }
    return;
    ...
}

下面介绍一下 ll_rw_block() 函数各个参数的作用:

  • rw:要进行的读或者写操作,一般可选的值为 READWRITE 或者 READA 等。
  • nrbhs 数组的大小。
  • bhs:要进行读写操作的数据块数组。

ll_rw_block() 函数的实现比较简单,遍历 bhs 数组,并且对所有的 buffer_head 进行上锁和增加其计数器,然后调用 submit_bh() 函数把其提交到 IO调度层 进行I/O操作。

我们接着看看 submit_bh() 函数的实现:

void submit_bh(int rw, struct buffer_head *bh)
{
    int count = bh->b_size >> 9; // 一个数据块需要的扇区数
    ...
    set_bit(BH_Req, &bh->b_state);

    bh->b_rdev = bh->b_dev;
    bh->b_rsector = bh->b_blocknr * count; // 转换成真实的扇区号

    generic_make_request(rw, bh);

    switch (rw) {
    case WRITE:
        kstat.pgpgout += count;
        break;
    default:
        kstat.pgpgin += count;
        break;
    }
}

数据块是 通用块层 的概念,而真实的块设备是以扇区作为读写单元的。所以在进行IO操作前,必须将数据块号转换成真正的扇区号,而代码 bh->b_blocknr * count 就是用于将数据块号转换成扇区号。转换成扇区号后,submit_bh() 函数接着调用 generic_make_request() 进行下一步的操作。

我们接着分析 generic_make_request() 函数的实现:

void generic_make_request(int rw, struct buffer_head * bh)
{
    request_queue_t *q;
    ...
    do {
        q = blk_get_queue(bh->b_rdev); // 获取块设备对应的I/O请求队列
        if (!q) {
            ...
            break;
        }
    } while (q->make_request_fn(q, rw, bh)); // 把I/O请求发送到块设备的I/O请求队列中
}

每一个块设备都有一个类型为 request_queue_t 的I/O请求队列,而 blk_get_queue() 函数用于获取块设备对应的I/O请求队列,然后调用I/O请求队列的 mark_request_fn() 方法把I/O请求添加到队列中。

那么I/O请求队列的 mark_request_fn() 方法到底是什么呢?这个方法由块设备驱动提供,也可以通过调用 blk_init_queue() 函数设置为默认的 __make_request() 方法。我们主要分析默认的 __make_request() 方法:

static int __make_request(request_queue_t *q, int rw, struct buffer_head *bh)
{
    ...
    elevator_t *elevator = &q->elevator;

    count = bh->b_size >> 9; // 要读写的扇区数
    sector = bh->b_rsector;  // 进行读写操作的开始扇区号
    ...
again:
    req = NULL;
    head = &q->queue_head;           // I/O请求队列头部
    spin_lock_irq(&io_request_lock); // 关闭中断并且上自旋锁
    insert_here = head->prev;        // 插入到IO请求队列的最后
    ...
    // 尝试合并I/O请求
    el_ret = elevator->elevator_merge_fn(q, &req, head, bh, rw, max_sectors); 
    switch (el_ret) {
        case ELEVATOR_BACK_MERGE:  // 与其他I/O请求合并成功
            ...
            goto out;
        case ELEVATOR_FRONT_MERGE: // 与其他I/O请求合并成功
            ...
            goto out;
        case ELEVATOR_NO_MERGE:    // 如果不能合并I/O请求
            if (req)
                insert_here = &req->queue;
            break;
        ...
    }

    req = get_request(q, rw); // 获取一个空闲的I/O请求对象
    ...
    req->cmd = rw;
    req->errors = 0;
    req->hard_sector = req->sector = sector;        // 读写操作的开始扇区号
    req->hard_nr_sectors = req->nr_sectors = count; // 要读写多少个扇区
    req->current_nr_sectors = count;
    req->nr_segments = 1;
    req->nr_hw_segments = 1;
    req->buffer = bh->b_data;                       // 读写数据存放的缓冲区
    req->waiting = NULL;
    req->bh = bh;
    req->bhtail = bh;
    req->rq_dev = bh->b_rdev;
    add_request(q, req, insert_here); // 把I/O请求对象添加到I/O请求队列中
out:
    ...
    return 0;
}

__make_request() 函数首先通过调用 elevator->elevator_merge_fn() 方法尝试将当前I/O请求与其他正在排队的I/O请求进行合并,因为如果当前I/O请求与正在排队的I/O请求相邻,那么就可以合并为一个I/O请求,从而减少对设备I/O请求的次数。

如果不能与排队的I/O请求进行合并,那么就调用 get_request() 函数申请一个I/O请求对象,然后初始化此对象各个字段,再通过调用 add_request() 函数把I/O请求对象添加到I/O请求队列中。add_request() 函数实现如下:

static inline void
add_request(request_queue_t *q, struct request *req, struct list_head *insert_here)
{
    drive_stat_acct(req->rq_dev, req->cmd, req->nr_sectors, 1);
    ...
    list_add(&req->queue, insert_here);
}

add_request() 函数的实现非常简单,首先调用 drive_stat_acct() 函数更新统计信息,然后调用 list_add() 函数把I/O请求添加到I/O请求队列中。

 

执行I/O请求


ll_rw_block() 函数只是把I/O请求添加到设备的I/O请求队列中,那么I/O请求队列中的I/O请求什么时候会执行呢?答案就是当调用 run_task_queue(&tq_disk) 函数时。

run_task_queue() 函数是 Linux 用于运行任务队列的入口,而 tq_disk 队列就是块设备I/O的任务队列。当执行 run_task_queue(&tq_disk) 函数时,便会处理 tq_disk 任务队列中的例程。

当调用 ll_rw_block() 函数添加I/O请求时,会触发调用 generic_plug_device() 函数,而 generic_plug_device() 函数会把设备的I/O请求队列添加到 tq_disk 任务队列中, generic_plug_device() 函数实现如下:

static void generic_plug_device(request_queue_t *q, kdev_t dev)
{
    if (!list_empty(&q->queue_head) || q->plugged)
        return;

    q->plugged = 1;
    queue_task(&q->plug_tq, &tq_disk); // 把I/O请求队列添加到 tq_disk 任务队列中
}

通过 Linux 的任务队列机制,设备的I/O请求队列将会被执行。执行I/O请求主要是由块设备驱动完成,在块设备驱动程序初始化时可以通过调用 blk_init_queue() 函数指定处理I/O请求队列的方法。blk_init_queue() 函数原型如下:

void blk_init_queue(request_queue_t *q, request_fn_proc *rfn);

参数 rfn 就是处理I/O请求队列的例程函数。

https://mp.weixin.qq.com/s/tqtBmEdV46FUzOkxttARig

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页