Linux下,I/O处理的层次可分为4层:

  1. 系统调用层,应用程序使用系统调用指定读写哪个文件,文件偏移是多少

   2. 文件系统层,写文件时将用户态中的buffer拷贝到内核态下,并由cache缓存该部分数据

3. 块层,管理块设备I/O队列,对I/O请求进行合并、排序

4. 设备层,通过DMA与内存直接交互,将数据写到磁盘

下图清晰地说明了Linux I/O层次结构:

写文件过程

写文件的过程包含了读的过程,文件先从磁盘载入内存,存到cache中,磁盘内容与物理内存页间建立起映射关系。用于写文件的write函数的声明如下:

ssize_t write(int fd, const void *buf, size_t count);

其中fd对应进程的file结构, buf指向写入的数据。内核从cache中找出与被写文件相应的物理页,write决定写内存的第几个页面,例如"echo 1 > a.out"(底层调用write)写入的是a.out文件的第0个位置,write将写相应内存的第一页。

write函数修改内存内容之后,相应的内存页、inode被标记为dirty,此时write函数返回。注意至此尚未往磁盘写数据,只是cache中的内容被修改。

那什么时候内存中的内容会刷到磁盘中呢?

把脏数据刷到磁盘的工作由内核线程flush完成,flush搜寻内存中的脏数据,按设定将脏数据写到磁盘,我们可以通过sysctl命令查看、设定flush刷脏数据的策略:

linux # sysctl -a | grep centi
vm.dirty_writeback_centisecs =
vm.dirty_expire_centisecs =
linux # sysctl -a | grep background_ratio
vm.dirty_background_ratio =  

以上数值单位为1/100秒,“dirty_writeback_centisecs = 500”指示flush每隔5秒执行一次,“dirty_expire_centisecs = 3000” 指示内存中驻留30秒以上的脏数据将由flush在下一次执行时写入磁盘,“dirty_background_ratio = 10”指示若脏页占总物理内存10%以上,则触发flush把脏数据写回磁盘。

flush找出了需要写回磁盘的脏数据,那存储脏数据的物理页又与磁盘的哪些扇区对应呢?

物理页与扇区的对应关系由文件系统定义,文件系统定义了一个内存页(4KB)与多少个块对应,对应关系在格式化磁盘时设定,运行时由buffer_head保存对应关系:

linux # cat /proc/slabinfo | grep buffer_head
buffer_head : tunables : slabdata

文件系统层告知块I/O层写哪个设备,具体哪个块,执行以下命令后,我们可以在/var/log/messages中看到文件系统层下发到块层的读写请求:

linux # echo  > /proc/sys/vm/block_dump
linux # tail -n /var/log/messages
Aug :: linux-q62c kernel: [ 7523.602144] bash(): READ block on sda1
Aug :: linux-q62c kernel: [ 7523.622857] bash(): dirtied inode (tail) on sda1
Aug :: linux-q62c kernel: [ 7523.623213] tail(): READ block on sda1

块I/O层使用struct bio记录文件系统层下发的I/O请求,bio中主要保存了需要往磁盘刷数据的物理页信息,以及对应磁盘上的扇区信息。

块I/O层为每一个磁盘设备维护了一条I/O请求队列,请求队列在内核中由struct request_queue表示。每一个读或写请求都需经过submit_bio函数处理,submit_bio将读写请求放入相应I/O请求队列中。该层起到最主要的作用就是对I/O请求进行合并和排序,这样减少了实际的磁盘读写次数和寻道时间,达到优化磁盘读写性能的目的。

使用crash解析vmcore文件,执行"dev -d"命令,可以看到块设备请求队列的相关信息:

crash > dev -d
MAJOR GENDISK NAME REQUEST QUEUE TOTAL ASYNC SYNC DRV
0xffff880119e85800 sda 0xffff88011a6a6948
0xffff880119474800 sdb 0xffff8801195632d0

执行"struct request_queue 0xffff88011a6a6948",可对以上sda设备相应的request_queue请求队列结构进行解析。

执行以下命令,可以查看sda设备的请求队列大小:

linux # cat /sys/block/sda/queue/nr_requests

如何对I/O请求进行合并、排序,那就是I/O调度算法完成的工作,Linux支持多种I/O调度算法,通过以下命令可以查看:

linux # cat /sys/block/sda/queue/scheduler
noop anticipatory deadline [cfq]

块I/O层的另一个作用就是对I/O读写情况进行统计,执行iostat命令,看到的就是该层提供的统计信息:

linux # iostat -x -k -d
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
sda 0.00 9915.00 1.00 90.00 4.00 34360.00 755.25 11.79 120.57 6.33 57.60

其中rrqm/s、wrqm/s分别指示了每秒写请求、读请求的合并次数。

task_io_account_read函数用于统计各个进程发起的读请求量, 由该函数得到的是进程读请求量的准确值。而对于写请求,由于数据写入cache后write调用就返回,因而在内核的层面无法统计到一个进程发起的准确写请求量,读时进程会等buff可用,而写则写入cache后返回,读是同步的,写却不一定同步,这是读写实现上的最大区别。

再往下就是设备层,设备从队列中取出I/O请求,scsi的scsi_request_fn函数就是完成取请求并处理的任务。scsi层最终将处理请求转化为指令,指令下发后进行DMA(direct memory access)映射,将内存的部分cache映射到DMA,这样设备绕过cpu直接操作主存。

设备层完成内存数据到磁盘拷贝后,该消息将一层层上报,最后内核去除原脏页的dirty位标志。

以上为写磁盘的大致实现过程,对于读磁盘,内核首先在缓存中查找对应内容,若命中则不会进行磁盘操作。若进程读取一个字节的数据,内核不会仅仅返回一个字节,其以页面为单位(4KB),最少返回一个页面的数据。另外,内核会预读磁盘数据,执行以下命令可以看到能够预读的最大数据量(以KB为单位):

linux # cat /sys/block/sda/queue/read_ahead_kb

下面我们通过一段systemtap代码,了解内核的预读机制:

//test.stp
probe kernel.function("submit_bio") {
  if(execname() == "dd" && __bio_ino($bio) == )
  {
    printf("inode %d %s on %s %d bytes start %d\n",
    __bio_ino($bio),
    bio_rw_str($bio),
    __bio_devname($bio),
    $bio->bi_size,
    $bio->bi_sector)
  }
}

以上代码指示当dd命令读写inode号为5234的文件、经过内核函数submit_bio时,输出inode号、操作方式(读或写)、文件所在设备名、读写大小、扇区号信息。执行以下代码安装探测模块:

stap test.stp &

之后我们使用dd命令读取inode号为5234的文件(可通过stat命令取得文件inode号):

dd if=airport.txt of=/dev/null bs= count=

以上命令故意将bs设为1,即每次读取一个字节,以此观察内核预读机制。执行该命令的过程中,我们在终端中可以看到以下输出:

inode  R on sda2  bytes start
inode R on sda2 bytes start
inode R on sda2 bytes start
inode R on sda2 bytes start
inode R on sda2 bytes start
inode R on sda2 bytes start

由以上输出可知,预读从16384字节(16KB)逐渐增大,最后变为524288字节(512KB),可见内核会根据读的情况动态地调整预读的数据量。

由于读、写磁盘均要经过submit_bio函数处理,submit_bio之后读、写的底层实现大致相同。

直接I/O

当我们以O_DIRECT标志调用open函数打开文件时,后续针对该文件的read、write操作都将以直接I/O(direct I/O)的方式完成;对于裸设备,I/O方式也为直接I/O。

直接I/O跳过了文件系统这一层,但块层仍发挥作用,其将内存页与磁盘扇区对应上,这时不再是建立cache到DMA映射,而是进程的buffer映射到DMA。进行直接I/O时要求读写一个扇区(512bytes)的整数倍,否则对于非整数倍的部分,将以带cache的方式进行读写。

使用直接I/O,写磁盘少了用户态到内核态的拷贝过程,这提升了写磁盘的效率,也是直接I/O的作用所在。而对于读操作,第一次直接I/O将比带cache的方式快,但因带cache方式后续再读时将从cache中读,因而后续的读将比直接I/O快。有些数据库使用直接I/O,同时实现了自己的cache方式。

异步I/O

Linux下有两种异步I/O(asynchronous I/O)方式,一种是aio_read/aio_write库函数调用,其实现方式为纯用户态的实现,依靠多线程,主线程将I/O下发到专门处理I/O的线程,以此达到主线程异步的目的。

另一种是io_submit,该函数是内核提供的系统调用,使用io_submit也需要指定文件的打开方式为O_DIRECT,并且读写需按扇区对齐。

Reference: Chapter 14 - The Block I/O Layer, Linux kernel development.3rd.Edition

kernel笔记——块I/O的更多相关文章

  1. LINUX kernel笔记系列 :IO块参数 图

      Linux下,I/O处理的层次可分为4层: 系统调用层,应用程序使用系统调用指定读写哪个文件,文件偏移是多少 文件系统层,写文件时将用户态中的buffer拷贝到内核态下,并由cache缓存该部分数 ...

  2. kernel笔记——VFS

    vfs(the virtual filesystem, virtual file switch)为应用程序访问文件提供了统一的接口,如read.write.open等. 下面我们看加载文件系统模块.格 ...

  3. web兼容学习分析笔记--块级、内联、内联块级元素

    一.块级.内联.内联块级元素 (1)块级元素:block **独占一行 **可设置width,height,margin,padding **内部可包含块级或内联元素 (3)内联(行内)元素:inli ...

  4. HTML 学习笔记(块 和 类)

    HTML <div> 和 <span> 可以通过<div>和<span>将HTML元素组合起来. HTML块元素 大多数HTML元素被定义为块级元素或者 ...

  5. HTML学习笔记——块级标签、行级标签、图片标签

    1>块级标签.行级标签 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "htt ...

  6. kernel笔记——内核编译与进程管理

    内核与操作系统 由于一些商业操作系统设计上的缺陷以及日益庞杂,“操作系统”的概念对很多人而言变得含糊不清.在进一步讨论Linux内核的话题前,我们先区分“内核”与“操作系统”这两个概念. 操作系统:指 ...

  7. kernel笔记——库文件与系统调用

    库文件 先从我们熟悉的c库入手,理解系统调用(system call).c代码中调用printf函数,经历了以下调用过程:   最终输出的功能由内核中write调用完成,c库封装了系统调用. 对于以下 ...

  8. kernel笔记——进程调度

    调度器完成以下任务: 时钟中断(或类似的定时器)时间内刷新进程的时间片,设置进程调度标志 系统调用返回或中断完成时检查调度标志 schedule函数 内核代码中完成进程调度的函数为schedule() ...

  9. kernel笔记——中断

    cpu与磁盘.网卡.键盘等外围设备(相对于cpu和内存而言)交互时,cpu下发I/O请求到这些设备后,相对cpu的处理能力而言,磁盘.网卡等设备需要较长时间完成请求处理. 那么在请求发出到处理完成这段 ...

随机推荐

  1. ES6躬行记(16)——Set

    ES6引入了两种新的数据结构:Set和Map.Set是一组值的集合,其中值不能重复:Map(也叫字典)是一组键值对的集合,其中键不能重复.Set和Map都由哈希表(Hash Table)实现,并可按添 ...

  2. kubernetes学习01—kubernetes介绍

    本文收录在容器技术学习系列文章总目录 一.简介 1.Kubernetes代码托管在GitHub上:https://github.com/kubernetes/kubernetes/. 2.Kubern ...

  3. 2.计算机组成-数字逻辑电路 门电路与半加器 异或运算半加器 全加器组成 全加器结构 反馈电路 振荡器 存储 D T 触发器 循环移位 计数器 寄存器 传输门电路 译码器 晶体管 sram rom 微处理 计算机

    现代计算机的各个部件到底是如何通过逻辑电路构成的呢   半加器 我们说过了门电路 看似简单的三种门电路却是组成了整个逻辑电路的根基 真值表--其实就是根据输入输出状态枚举罗列出来的所有可能 比如有一台 ...

  4. Spring Boot 2.x(十三):你不知道的PageHelper

    PageHelper 说起PageHelper,使用过Mybatis的朋友可能不是很陌生,作为一款国人开发的分页插件,它基本上满足了我们的日常需求.但是,我想去官方文档看看这个东西配合Spring B ...

  5. 痞子衡嵌入式:并行接口NAND标准(ONFI)及SLC Raw NAND简介

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是ONFI标准及SLC Raw NAND. NAND Flash是嵌入式世界里常见的存储器,对于嵌入式开发而言,NAND主要分为两大类:S ...

  6. #7 找出数组中第k小的数

    「HW面试题」 [题目] 给定一个整数数组,如何快速地求出该数组中第k小的数.假如数组为[4,0,1,0,2,3],那么第三小的元素是1 [题目分析] 这道题涉及整数列表排序问题,直接使用sort方法 ...

  7. Redis数据类型使用场景及有序集合SortedSet底层实现详解

    Redis常用数据类型有字符串String.字典dict.列表List.集合Set.有序集合SortedSet,本文将简单介绍各数据类型及其使用场景,并重点剖析有序集合SortedSet的实现. Li ...

  8. Linux系统命令行整理

    1.pwd  查看当前所在目录 2.cd /  跳往根目录 3.ls  查看当前目录所有子目录或文件 4.ls -l  列出当前目录详细信息 5.ls -lh  h=human 人性化列出当前目录详细 ...

  9. [转]在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查

    本文转自:https://www.cnblogs.com/kongxianghai/p/5582661.html Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用Ja ...

  10. Flask 系列之 优化项目结构

    说明 操作系统:Windows 10 Python 版本:3.7x 虚拟环境管理器:virtualenv 代码编辑器:VS Code 实验目标 完善环境配置,添加 异常请求 处理 实现 400.404 ...