• linux文件IO全景解析


    本文说明以下内容:

    • 文件系统分层视图
    • 数据在硬盘上是如何组织的?
    • 读写文件时,如何从文件开始寻址到磁盘扇区?
    • read,write内核中完整流程
    • directIO不经过pagecache,落盘之前也需要将用户态数据拷贝到内核态,这个内存与page cache有何区别 (块缓存buffer cache与页缓存 page cache???)

    Buffer cache和page cache的区别 : 简单说,文件系统操作文件都是面向内存的,根本够不着块这一层;directIO或裸设备读写也会分配一些内存(buffer cache)作为用户态内存和磁盘设备之间的媒介;而非directIO因为已经有了page cache,所以直接用了page cache,不用再特别分配buffer cache.
    Linux内核Page Cache和Buffer Cache关系及演化历史

    参考
    宋宝华: 文件读写(BIO)波澜壮阔的一生

    文件系统分层视图

    vfs
    |
    具体文件系统
    |
    page cache (directio 不走page cache)
    |
    通用块层
    |
    IO调度层
    |
    设备驱动程序
    |
    设备控制器 (硬件)

    磁盘数据组织

    磁盘会被划分为多个块组,每个块组都如下述一样组织
    大致分为5块区域
    superblock --- data bitmap --- inode bitmap --- inode --- data

    superblock: 一般在磁盘开始的固定位置上,其中存放了后面各个区域的起始终止位置,例如data bitmap从哪个扇区开始
    data bitmap: 标记data区哪些位置有数据,哪些是空闲的
    inode bitmap:标记inode区域哪些位置有数据,哪些是空闲的
    inode:存放inode
    data: 存放数据

    dumpe2fs 挂载点 可以查看文件系统信息

    vfs 主要构成

    superblock

    整个具体文件系统的描述信息,包含了关键信息包括super_operations(读写inode等操作)

    file

    进程角度看到的文件信息,里面存放了当前读写偏移(f_pos)

    • fd存放在task_struct的fdtable中,通过fd找到file结构 -> file结构中包含dentry结构 -> dentry结构包含inode
    • file结构体中包含了另一个重要信息是file_operations,由各个磁盘文件系统自己实现(如ext4等)

    inode

    • 磁盘上inode超集,里面存放了文件存放在哪些扇区上等信息
    • inode中关键信息有:address_space(将lba映射到page), inode_operations(创建、删除、查找inode等), 指向表示硬盘的i_rdev, i_bdev等结构

    为什么把关于inode的操作一部分放到super_operations里面,一部分放到inode_operations里面呢

    dentry

    • 是文件的逻辑表示,没有对应的磁盘存储
    • 在打开文件时/home/xx/a.txt,会以此访问文件/, /home, /home/xx/(目录也是文件,有对应的inode保存在磁盘上),最后才是/home/xx/a.txt;根据文件路径一层层访问磁盘上inode时,在内存中构建dentry结构,最终形成一个树形结构
    • 存放了dentry_operations(比对文件名等操作就是用这个实现的)

    综上,通过file -> dentry -> inode 就把进程眼中的文件映射到了磁盘上的某些扇区。

    更多阅读:
    Linux文件系统2---VFS的四个主要对象
    计算机底层知识拾遗(四)理解文件系统

    寻址过程

    进程看到的文件地址空间,是连续的、平坦的,从0开始增加;磁盘上是按照扇区存储文件的,可能连续也可能分散

    • 当进程打开一个文件,vfs会创建一个file结构,里面存放了f_pos
    • get_block_t 是一个函数指针类型,用于完成文件逻辑块号到磁盘物理块号的映射
    • address_space address_space_operations包含读写page等, directIO等操作操作
      在io通过submit_bio提交到通用块层之前,
    • lba -> 文件逻辑块号 -> page (ext4_file_operations)
    • page -> 物理块号 (address_space_operations中的readpage/writepage/directIO等 会调用get_block_t完成page到物理块号的映射,并组装成bio,准备提交给通用块层)

    更多阅读
    linux异步IO浅析

    最后,整理一下direct-io异步读操作的处理流程:
    io_submit。对于提交的iocbpp数组中的每一个iocb(异步请求),调用io_submit_one来提交它们;
    io_submit_one。为请求分配一个kiocb结构,并且在对应的kioctx的ring_info中为它预留一个对应的io_event。然后调用aio_rw_vect_retry来提交这个读请求;
    aio_rw_vect_retry。调用file->f_op->aio_read。这个函数通常是由generic_file_aio_read或者其封装来实现的;
    generic_file_aio_read。对于非direct-io,会调用do_generic_file_read来处理请求(见《linux文件读写浅析》)。而对于direct-io,则是调用mapping->a_ops->direct_IO。这个函数通常就是blkdev_direct_IO;
    blkdev_direct_IO。调用filemap_write_and_wait_range将相应位置可能存在的page cache废弃掉或刷回磁盘(避免产生不一致),然后调用direct_io_worker来处理请求;
    direct_io_worker。一次读可能包含多个读操作(对应于类readv系统调用),对于其中的每一个,调用do_direct_IO;
    do_direct_IO。调用submit_page_section;
    submit_page_section。调用dio_new_bio分配对应的bio结构,然后调用dio_bio_submit来提交bio;
    dio_bio_submit。调用submit_bio提交请求。后面的流程就跟非direct-io是一样的了,然后等到请求完成,驱动程序将调用 bio->bi_end_io来结束这次请求。对于direct-io下的异步IO,bio->bi_end_io等于dio_bio_end_aio;
    dio_bio_end_aio。调用wake_up_process唤醒被阻塞的进程(异步IO下,主要是io_getevents的调用者)。然后调用aio_complete;
    aio_complete。将处理结果写回到对应的io_event中;
    

    脏页也是可以读写的
    脏页也是可以读写的

    正在回写中的脏页会被锁上,此时后续的这个页上的IO会被阻塞

    读写流程

    文件系统层 & page cache

    ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
           loff_t *pos)
    {
      if (file->f_op->read)
        return file->f_op->read(file, buf, count, pos);
      else if (file->f_op->read_iter)
        return new_sync_read(file, buf, count, pos);
      else
        return -EINVAL;
    }
    
    
    ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
            loff_t *pos)
    {
      if (file->f_op->write)
        return file->f_op->write(file, p, count, pos);
      else if (file->f_op->write_iter)
        return new_sync_write(file, p, count, pos);
      else
        return -EINVAL;
    }
    
    
    const struct file_operations ext4_file_operations = {
    ......
      .read_iter  = ext4_file_read_iter,
      .write_iter  = ext4_file_write_iter,
    ......
    }
    
    
    ssize_t
    generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
    {
    ......
        if (iocb->ki_flags & IOCB_DIRECT) {
    ......
            struct address_space *mapping = file->f_mapping;
    ......
            retval = mapping->a_ops->direct_IO(iocb, iter);
        }
    ......
        retval = generic_file_buffered_read(iocb, iter, retval);
    }
    
    
    ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
    {
    ......
        if (iocb->ki_flags & IOCB_DIRECT) {
    ......
            written = generic_file_direct_write(iocb, from);
    ......
        } else {
    ......
        written = generic_perform_write(file, from, iocb->ki_pos);
    ......
        }
    }
    
    
    static const struct address_space_operations ext4_aops = {
    ......
      .direct_IO    = ext4_direct_IO,
    ......
    };
    
    
    /*
     * This is a library function for use by filesystem drivers.
     */
    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)
    {......}
    

    ext4_direct_IO 最终会调用到 __blockdev_direct_IO->do_blockdev_direct_IO,这就跨过了缓存层,到了通用块层,最终到了文件系统的设备驱动层。

    写流程

    ssize_t generic_perform_write(struct file *file,
            struct iov_iter *i, loff_t pos)
    {
      struct address_space *mapping = file->f_mapping;
      const struct address_space_operations *a_ops = mapping->a_ops;
      do {
        struct page *page;
        unsigned long offset;  /* Offset into pagecache page */
        unsigned long bytes;  /* Bytes to write to page */
        status = a_ops->write_begin(file, mapping, pos, bytes, flags,
                &page, &fsdata);
        copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
        flush_dcache_page(page);
        status = a_ops->write_end(file, mapping, pos, bytes, copied,
                page, fsdata);
        pos += copied;
        written += copied;
    
    
        balance_dirty_pages_ratelimited(mapping);
      } while (iov_iter_count(i));
    }
    

    这个函数里,是一个 while 循环。我们需要找出这次写入影响的所有的页,然后依次写入。对于每一个循环,主要做四件事情:

    • 对于每一页,先调用 address_space 的 write_begin 做一些准备;调用grab_cache_page_write_begin找到page,并调用get_block_t将文件逻辑块号与磁盘物理快照映射起来;如果write数据没有写整个buffer,需下盘读ll_rw_block
    static const struct address_space_operations ext4_aops = {
    ......
      .write_begin    = ext4_write_begin,
      .write_end    = ext4_write_end,
    ......
    }
    
    • 调用 iov_iter_copy_from_user_atomic,将写入的内容从用户态拷贝到内核态的页中;先将分配好的页面调用 kmap_atomic 映射到内核里面的一个虚拟地址,然后将用户态的数据拷贝到内核态的页面的虚拟地址中,调用 kunmap_atomic 把内核里面的映射删除。
    
    size_t iov_iter_copy_from_user_atomic(struct page *page,
        struct iov_iter *i, unsigned long offset, size_t bytes)
    {
      char *kaddr = kmap_atomic(page), *p = kaddr + offset;
      iterate_all_kinds(i, bytes, v,
        copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
        memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
             v.bv_offset, v.bv_len),
        memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
      )
      kunmap_atomic(kaddr);
      return bytes;
    }
    
    • 调用 address_space 的 write_end 完成写操作;调用 ext4_write_end 完成写入。这里面会调用 ext4_journal_stop 完成日志的写入,会调用 block_write_end->__block_commit_write->mark_buffer_dirty,将修改过的缓存标记为脏页。可以看出,其实所谓的完成写入,并没有真正写入硬盘,仅仅是写入缓存后,标记为脏页。
    • 调用 balance_dirty_pages_ratelimited,看脏页是否太多,需要写回硬盘。所谓脏页,就是写入到缓存,但是还没有写入到硬盘的页面。

    读流程

    vfs_read -> new_sync_read

    • struct iovec记录用户态内存地址和len;构造kiocb记录filp和相关的flag;构造iov_iter将iovec塞进去
      -> ext4_file_read_iter (调用file_operation.read_iter,ext4_file_operation实现为ext4_file_read_iter) -> generic_file_read_iter -> do_generic_file_read
    • find_get_page: 根据address_space、offset检查是否已经在缓存中;
    • 如果在缓存中,检查page是否是uptodate,如果不是,将page锁住(),然后调用ext4_aops.read_page下盘去读这一页数据;数据从磁盘读到后,中断处理中调用bio_endio将page设置为uptodate
    • page是uptodate,然后将数据拷贝到用户态内存,至此IO已经完成
      page dirty表示写请求将数据写到page上,还没有sync到磁盘;page not uptodate一般表示page cache中已经有了这个page,但数据还未从磁盘上读上来
      -> page_cache_sync_readahead(没命中缓存的情景)
    • 调用ext4_map_blocks找到page对应到磁盘上的物理块号
    • 创建bio,将物理块号、page等信息填充进bio,最后调用submit_bio
    
    static ssize_t generic_file_buffered_read(struct kiocb *iocb,
        struct iov_iter *iter, ssize_t written)
    {
      struct file *filp = iocb->ki_filp;
      struct address_space *mapping = filp->f_mapping;
      struct inode *inode = mapping->host;
      for (;;) {
        struct page *page;
        pgoff_t end_index;
        loff_t isize;
        page = find_get_page(mapping, index);
        if (!page) {
          if (iocb->ki_flags & IOCB_NOWAIT)
            goto would_block;
          page_cache_sync_readahead(mapping,
              ra, filp,
              index, last_index - index);
          page = find_get_page(mapping, index);
          if (unlikely(page == NULL))
            goto no_cached_page;
        }
        if (PageReadahead(page)) {
          page_cache_async_readahead(mapping,
              ra, filp, page,
              index, last_index - index);
        }
        /*
         * Ok, we have the page, and it's up-to-date, so
         * now we can copy it to user space...
         */
        ret = copy_page_to_iter(page, offset, nr, iter);
        }
    }
    

    更多阅读
    Ext3文件系统读写过程分析

    通用块层

    block_device 表示逻辑块设备
    gendisk 表示磁盘对象,管理请求队列和磁盘通用操作
    request_queue 包含elevator, softirq_done_fn 等
    buffer_head 磁盘按照block size被划分为一系列连续的块,一个文件也按照block size被划分为一系列块,进程io操作时将lba转化为逻辑块号(文件视角的块号),逻辑块号对应磁盘上哪个物理块号是有buffer_head管理的,它管理了这个映射关系

    https://blog.51cto.com/alanwu/1122077
    bio 是文件系统与通用块层进行IO的容器,bio指向一些page

    submit_bio -> generic_make_request

    • 通过bio上关联的block_device找到gendisk,然后找到request_queue
    • request_queue包含elivator, make_request_fn, request_fn(处理请求), softirq_done_fn(请求被硬件处理完后,软中断中调用)

    更多阅读
    Linux kernel学习-block层
    Linux IO请求处理流程-bio和request

    io调度层

    blk_queue_bio(scsi驱动添加磁盘设备时scsi_alloc_sdev->scsi_alloc_queue->blk_init_queue_node, 这其中会调用elevator_init 初始化elivator)
    调度层有三个队列

    • 线程的unplog list
    • elivator
    • gendisk.request_queue
      bio 并不是直接挂到设备的请求队列上,他首先尝试挂到unplog list,不行再尝试合并到elivator中的request;如果无法合并,新建request放入unplog list

    调度层主要解决两类问题:

    • Bio的合并问题。主要考虑bio是否可以和scheduler中的某个request进行合并。因为从磁盘的角度来看,临近的请求需要合并,所有的IO需要顺序化处理,这样磁头才能往一个方向运行,避免无规则的乱序运行。
    • Request的调度问题。request在何时可以从scheduler中取出,并且送入底层驱动程序继续进行处理?不同的应用可能需要不同的带宽资源,读写请求的带宽、延迟控制也可以不一样,因此,需要解决request的调度处理,从而可以更好的控制IO的QoS。

    如果io塞到了某个request_queue上,函数就对上层响应了,并没有等待驱动层处理结束

    更多阅读
    一个IO的传奇一生(8) -- elevator子系统

    此人的博客.

    35 | 块设备(下):如何建立代理商销售模式?

    计算机底层知识拾遗(五)理解块IO层
    Linux deadline电梯调度算法

    驱动层

    • 把读取数据内存地址、命令等信息提交给设备控制器
    • 设备控制器将驱动发过来的请求翻译成DMA控制器可识别的形式,由DMA控制器执行磁盘到内存的数据传输
    • 传输完成时,由DMA控制器触发中断通知CPU读写完毕

    scsi_request_fn(scsi驱动添加磁盘设备时scsi_alloc_sdev->scsi_alloc_queue->blk_init_queue_node )
    组装scsi cmnd,调用scsi_dispatch_cmd发送给scsi底层驱动,注意scsi cmnd上设置了scsi_done函数

    
    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;
    ......
    }
    

    scsi_dispatch_cmd调用scsi lower level注册的host template模板上的queuecommand将请求下发到scsi底层
    scsi_done是scsi中层中断处理函数,它会在底层处理完毕后被调用,该函数中会调用__blk_complete_request触发BLOCK_SOFTIRQ,最终会调用scsi_softirq_done

    try_to_wake_up
    native_sched_clock
    default_wake_function
    autoremove_wake_function
    wake_bit_function
    __wake_up_common
    __wake_up
    __wake_up_bit
    unlock_page
    mpage_end_io_read
    bio_endio
    req_bio_endio
    blk_update_request
    blk_update_bidi_request
    blk_end_bidi_request
    blk_end_request
    scsi_io_completion
    scsi_finish_command
    scsi_softirq_done
    blk_done_softirq
    __do_softirq
    handle_IRQ_event
    call_softirq
    do_softirq
    irq_exit
    do_IRQ
    ret_from_intr
    acpi_idle_enter_cl
    cpuidle_idle_call
    cpu_idle
    start_secondary
    

    scsi_done可能是在磁盘控制器或者DMA控制器在IO完成后,触发的硬件中断中调用 -> 参考 Linux内核I/O scsi_done()及__blk_complete_request()调用栈信息

    <IRQ> [<ffffffff8162a629>] dump_stack+0x19/0x1b
    [<ffffffff812c96d4>] __blk_complete_request+0x144/0x150
    [<ffffffff812c9701>] blk_complete_request+0x21/0x30
    [<ffffffff81417033>] scsi_done+0x53/0xa0
    [<ffffffffa00ef34b>] _scsih_io_done+0x1ab/0xb60 [mpt3sas]
    [<ffffffffa00e4530>] ? mpt3sas_base_free_smid+0x120/0x240 [mpt3sas]
    [<ffffffffa00e487c>] _base_interrupt+0xbc/0x8c0 [mpt3sas]
    [<ffffffff81087bdc>] ? get_next_timer_interrupt+0xec/0x270
    [<ffffffff81058e96>] ? native_safe_halt+0x6/0x10
    [<ffffffff81114e6e>] handle_irq_event_percpu+0x3e/0x1e0
    [<ffffffff8111504d>] handle_irq_event+0x3d/0x60
    [<ffffffff81117ce7>] handle_edge_irq+0x77/0x130
    [<ffffffff81016ecf>] handle_irq+0xbf/0x150
    [<ffffffff810d9f9a>] ? tick_check_idle+0x8a/0xd0
    [<ffffffff8163676a>] ? atomic_notifier_call_chain+0x1a/0x20
    [<ffffffff8163d1ef>] do_IRQ+0x4f/0xf0
    [<ffffffff8163252d>] common_interrupt+0x6d/0x6d
    <EOI> [<ffffffff81058e96>] ? native_safe_halt+0x6/0x10
    [<ffffffff8101dbcf>] default_idle+0x1f/0xc0
    [<ffffffff8101e4d6>] arch_cpu_idle+0x26/0x30
    [<ffffffff810cf305>] cpu_startup_entry+0x245/0x290
    [<ffffffff8161a347>] rest_init+0x77/0x80
    [<ffffffff81a80057>] start_kernel+0x429/0x44a
    [<ffffffff81a7fa37>] ? repair_env_string+0x5c/0x5c
    [<ffffffff81a7f120>] ? early_idt_handlers+0x120/0x120
    [<ffffffff81a7f5ee>] x86_64_start_reservations+0x2a/0x2c
    [<ffffffff81a7f742>] x86_64_start_kernel+0x152/0x175
    

    一些问题

    scsi_device.request_queue与gendisk.request_queue是什么关系?

    是同一个队列
    IO的请求队列何来何往

    /dev/sdx inode 与 bdev的关系

    文件系统之块设备文件

    什么时候调用sd_probe

    SCSI Upper Layer 与LLD的联系——sd_probe
    Linux那些事儿之我是SCSI硬盘(2)依然probe

    • os起来时,初始化sd driver,会扫描总线上所有的device,进行driver & device绑定,这里面会调用sd_probe,为device创建gendisk等结构
    • 添加磁盘时,会调用sd_probe扫描总线

    裸设备读写

    打开/dev/sdx直接读写场景,IO也区分directIO/非directIO,可以参考init_special_inode中inode.fops赋值的file_operations读写函数。
    /dev/sdx当作一个普通文件读写来理解,那么这里涉及一个问题,在文件逻辑块号映射到磁盘物理块号时,这个文件算是在哪个文件系统上呢?

    更多阅读
    linux驱动子系统--SCSI
    scsi块设备驱动层处理
    scsi命令的执行
    Linux那些事儿 之 我是Block层
    linux kernel的中断子系统之(八):softirq
    Linux内核读取文件流程源码及阻塞点超详解
    《大话存储》——1. 磁盘控制器、驱动器控制电路和磁盘控制器驱动程序
    文章汇总(包括NVMe SPDK vSAN Ceph xfs等)

  • 相关阅读:
    11.ForkJoinPool 分支/合并框架 (工作窃取)
    10.线程池_线程调度
    9.线程八锁
    8.读写锁ReadWriteLock
    7.生产者消费者 案例 (使用Lock 同步锁 方式,使用Condition完成线程之间的通信)
    ScrollView嵌套子View的getDrawingCache为空的解决方法
    装箱与拆箱
    Java核心技术卷一基础知识-第11章-异常、断言、日志和调试-读书笔记
    Java核心技术卷一基础知识-第9章-Swing用户界面组件-读书笔记
    Java核心技术卷一基础知识-第8章-事件处理-读书笔记
  • 原文地址:https://www.cnblogs.com/holidays/p/linuxio2.html
Copyright © 2020-2023  润新知