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

  1. 系统调用层,应用程序使用系统调用指定读写哪个文件,文件偏移是多少
  2. 文件系统层,写文件时将用户态中的buffer拷贝到内核态下,并由cache缓存该部分数据
  3. 块层,管理块设备I/O队列,对I/O请求进行合并、排序
  4. 设备层,通过DMA与内存直接交互,将数据写到磁盘

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

写文件过程

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

  1. 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刷脏数据的策略:

  1. linux # sysctl -a | grep centi
  2. vm.dirty_writeback_centisecs = 500
  3. vm.dirty_expire_centisecs = 3000
  4. linux # sysctl -a | grep background_ratio
  5. vm.dirty_background_ratio = 10

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

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

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

  1. linux # cat /proc/slabinfo | grep buffer_head
  2. buffer_head 12253 12284 104 37 1 : tunables 120 60 8 : slabdata 332 332 0

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

  1. linux # echo 1 > /proc/sys/vm/block_dump
  2. linux # tail -n 3 /var/log/messages
  3. Aug 7 00:50:31 linux-q62c kernel: [ 7523.602144] bash(5466): READ block 1095792 on sda1
  4. Aug 7 00:50:31 linux-q62c kernel: [ 7523.622857] bash(5466): dirtied inode 27874 (tail) on sda1
  5. Aug 7 00:50:31 linux-q62c kernel: [ 7523.623213] tail(5466): READ block 1095824 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"命令,可以看到块设备请求队列的相关信息:

  1. crash > dev -d
  2. MAJOR            GENDISK NAME            REQUEST QUEUE TOTAL ASYNC SYNC DRV
  3. 8 0xffff880119e85800  sda 0xffff88011a6a6948    10     0     0   10
  4. 8 0xffff880119474800  sdb 0xffff8801195632d0     0     0     0    0

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

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

  1. linux # cat /sys/block/sda/queue/nr_requests
  2. 128

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

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

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

  1. linux # iostat -x -k -d 1
  2. Device: rrqm/s wrqm/s    r/s   w/s rkB/s    wkB/s avgrq-sz avgqu-sz   await svctm %util
  3. 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为单位):

  1. linux # cat /sys/block/sda/queue/read_ahead_kb
  2. 512

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

  1. //test.stp
  2. probe kernel.function("submit_bio") {
  3.   if(execname() == "dd" && __bio_ino($bio) == 5234)
  4.   {
  5.     printf("inode %d %s on %s %d bytes start %d\n",
  6.     __bio_ino($bio),
  7.     bio_rw_str($bio),
  8.     __bio_devname($bio),
  9.     $bio->bi_size,
  10.     $bio->bi_sector)
  11.   }
  12. }

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

  1. stap test.stp &

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

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

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

  1. inode 5234 R on sda2 16384 bytes start 70474248
  2. inode 5234 R on sda2 32768 bytes start 70474280
  3. inode 5234 R on sda2 32768 bytes start 70474352
  4. inode 5234 R on sda2 131072 bytes start 70474416
  5. inode 5234 R on sda2 262144 bytes start 70474672
  6. inode 5234 R on sda2 524288 bytes start 70475184

由以上输出可知,预读从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

LINUX kernel笔记系列 :IO块参数 图的更多相关文章

  1. linux kernel笔记

    文章目录 关于linux内核中的__attribute__关键字 Linux kernel启动参数 gdt / ldt PCB 关于linux内核中的__attribute__关键字 part I: ...

  2. LInux 学习笔记系列

    1.Linux 就该这么学的笔记系列

  3. linux kernel (proc文件系统)参数

    http://blog.csdn.net/guowake/article/details/3279796 Linux Proc文件系统,通过对Proc文件系统进行调整,达到性能优化的目的. 二./pr ...

  4. linux学习笔记之IO

    一.基础知识. 1:普通IO类型. 1,非阻塞IO:发出open/read/write等IO操作,并使这些操作不会永远阻塞.当不能完成时,会立即出错返回. 1)非阻塞的两种标志方式:指定标志:O_NO ...

  5. Linux学习笔记 -- 为 Shell 传递参数

    我们可以在执行 Shell 脚本时,可以向脚本传递参数.脚本内获取参数的格式为:$n.(n 代表一个数字,0为所执行的shell脚本名称,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类 ...

  6. Linux kernel 有关 spi 设备树参数解析

    一.最近做了一个 spi 设备驱动从板级设备驱动升级到设备树设备驱动,这其中要了解 spi 设备树代码的解析. 二. 设备树配置如下: 503 &spi0 { 504 status = &qu ...

  7. Linux 性能优化之 IO 子系统 系列 图

    http://blog.sina.com.cn/s/articlelist_1029388674_11_1.html Linux 性能优化之 IO 子系统(一) 本文介绍了对 Linux IO 子系统 ...

  8. Linux Kernel File IO Syscall Kernel-Source-Code Analysis(undone)

    目录 . 引言 . open() syscall . close() syscall 0. 引言 在linux的哲学中,所有的磁盘文件.目录.外设设备.驱动设备全部被抽象为了"文件" ...

  9. linux kernel学习笔记-5内存管理_转

    void * kmalloc(size_t size, gfp_t gfp_mask); kmalloc()第一个参数是要分配的块的大小,第一个参数为分配标志,用于控制kmalloc()的行为. km ...

随机推荐

  1. 第 2 章 代理模式【Proxy Pattern】

    第 2 章 代理模式[Proxy Pattern] 以下内容出自:24种设计模式介绍与6大设计原则.pdf 什么是代理模式呢?我很忙,忙的没空理你,那你要找我呢就先找我的代理人吧,那代理人总要知道被代 ...

  2. 使AspNetPager控件中文显示分页信息

    在日常的编程过程中,很多学员对于使AspNetPager控件中文显示分页信息不是很清楚,本文将由达内的老师为各位学员介绍一下使AspNetPager控件中文显示分页信息的内容. AspNetPager ...

  3. c#中总是提示“在代码运行时或者在禁用“只要一个进程中断,就中断所有进程”选项时,不允许进行更改。

    但是根据它提示的修改方法,还是提示这个. “此选项可在“工具”->“选项”->“调试”中启用.” 根本不起作用,后来试着,要这样操作: 工具->选项->调试->编辑并继续 ...

  4. 李洪强iOS开发Swift篇—07_函数

    李洪强iOS开发Swift篇—07_函数 一.函数的定义 (1)函数的定义格式 1 func 函数名(形参列表) -> 返回值类型 { 2 // 函数体... 3 4 } (2)形参列表的格式 ...

  5. Ubuntu 安装基础教程

    转自:http://teliute.org/linux/Ubsetup/index.html 1.进入 live cd 桌面  1)设置好启动后,断开网络,然后重启动计算机,可以用硬盘启动,也可以刻成 ...

  6. 115太酷了,居然出了个TV版客户端

    确实,智能电视代表了未来的方向,是智能家居的最重要客户端,TV也能做很多事情呢!!不要忘了这个市场,想想什么服务在TV上是最需要的? http://pc.115.com/tv.html

  7. BroadCastReceiver中耗时操作导致ANR

    現象:廣播接收器中進行耗時的I/O操作導致ANR. 查資料發現每次广播到来时 , 会重新创建 BroadcastReceiver 对象 , 并且调用 onReceive() 方法 , 执行完以后 该对 ...

  8. hud1520Anniversary party(树形DP)

    链接 第一道树形DP 根据左儿子 右兄弟 将多叉树转化成二叉树 结构体里保存取这个节点和不取这个节点的最大值 #include <iostream> #include<cstdio& ...

  9. bzoj1079

    50%的数据很好考虑,基本的dp了 关键到了100%,如果用每种颜色有ci种这种常规的写法,显然5^15会爆空间 考虑到反过来,ci<=5, 15^5是不会爆空间的 又想到,每一种颜色,如果数量 ...

  10. UIImagePickerController 如何显示中文界面

    1: 添加中文的 Localizations Project --> Info --> Localizations  添加 "Chinese(Simplified)" ...