• IO栈整体认知


    https://blog.csdn.net/m0_45922575/article/details/106262920

    IO栈整体认知
    简化后的IO栈如下图:

    1.用户空间:除了用户自己的APP之外,也隐含了所有的库,例如常见的C库。我们常用的IO函数,例如open()/read()/write()是系统调用,由内核直接提供功能实现,而fopen()/fread()/fwrite()则是C库实现的函数,通过封装系统调用实现更高级的功能。

    2.虚拟文件系统:屏蔽具体文件系统的差异,向用户空间提供统一的入口。具体的文件系统通过register_filesystem()向虚拟文件系统注册挂载钩子,在用户挂载具体的文件系统时,通过回调挂载钩子实现文件系统的初始化。虚拟文件系统提供了inode来记录文件的元数据,dentry记录了目录项。对用户空间,虚拟文件系统注册了系统调用,例如SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)注册了open()的系统调用。

    3.具体的文件系统:文件系统要实现存储空间的管理,换句话说,其规划了哪些空间存储了哪些文件的数据,就像一个个收纳盒,A文件保存在这个块,B文件则放在哪个块。不同的管理策略以及其提供的不同功能,造就了各式各样的文件系统。除了类似于vfat、ext4、btrfs等常见的块设备文件系统之外,还有sysfs、procfs、pstorefs、tempfs等构建在内存上的文件系统,也有yaffs,ubifs等构建在Flash上的文件系统。

    4.页缓存:可以简单理解为一片存储着磁盘数据的内存,不过其内部是以页为管理单元,常见的页大小是4K。这片内存的大小不是固定的,每有一笔新的数据,则申请一个新的内存页。由于内存的性能远大于磁盘,为了提高IO性能,我们就可以把IO数据缓存在内存,这样就可以在内存中获取要的数据,不需要经过磁盘读写的漫长的等待。申请内存来缓存数据简单,如何管理所有的页缓存以及如何及时回收缓存页才是精髓。

    5.通用块层:通用块层也可以细分为bio层和request层。页缓存以页为管理单位,而bio则记录了磁盘块与页之间的关系,一个磁盘块可以关联到多个不同的内存页中,通过submit_bio()提交bio到request层。一个request可以理解为多个bio的集合,把多个地址连续的bio合并成一个request。多个request经过IO调度算法的合并和排序,有序地往下层提交IO请求。

    6.设备驱动与块设备:不同块设备有不同的使用协议,而特定的设备驱动则是实现了特定设备需要的协议以正常驱使设备。对块设备而言,块设备驱动需要把request解析成一个个设备操作指令,在协议的规范下与块设备通信来交换数据。

    发起一次IO读请求的过程:
    用户空间通过虚拟文件系统提供的统一的IO系统调用,从用户态切到内核态。虚拟文件系统通过调用具体文件系统注册的回调,把需求传递到 具体的文件系统中。紧接着 具体的文件系统根据自己的管理逻辑,换算到具体的磁盘块地址,从页缓存寻找块设备的缓存数据。读操作一般是同步的,如果在 页缓存没有缓存数据,则向通用块层发起一次磁盘读。 通用块层合并和排序所有进程产生的的IO请求,经过 设备驱动从 块设备读取真正的数据。最后是逐层返回。读取的数据既拷贝到用户空间的buffer中,也会在页缓存中保留一份副本,以便下次快速访问。
    如果 页缓存没命中,同步读会一路通到块设备,而对于异步写,则是把数据放到页缓存后返回,由内核回刷进程在合适时候回刷到块设备。
    优化的核心思路是尽可能多的使用内存缓存数据,尽可能减小不必要的开销。

    交换分区
    交换分区的存在,可以让内核在内存压力大时,把内核认为一些不常用的内存置换到交换分区,以此腾出更多的内存给系统。
    在物理内存容量不足且运行吃内存的应用时,交换分区的作用效果是非常明显的。
    1
    2

    服务器总内存达到15G,从内存的使用情况来看,绝大部分内存都是被cache/buff占用,是可丢弃的文件缓存,因此内存是充足的,不需要通过交换分区扩大虚拟内存。
    交换分区也是磁盘的空间,从交换分区置入置出数据可也是要占用IO资源的,如果单纯考虑优化IO资源是需要取消swap分区。
    通过如下截图命令可查看系统及swap使能状态:

    文件系统
    用户发起了一次读写,经过虚拟文件系统(VFS)后交给了实际文件系统。
    分区挂在情况:
    1
    2

    此服务器主要有两个块设备,分别是 sda和 vda。sda 是常见的 SCSI/IDE 设备,我们个人PC上如果使用的机械硬盘,往往就会是 sda 设备节点。vda 是 virtio 磁盘设备。由于本服务器是 KVM 提供的虚拟机,不管是 sda 还是 vda,其实都是虚拟设备,差别在于前者是完全虚拟化的块设备,后者是半虚拟化的块设备。从网上找到的资料来看,使用半虚拟化的设备,可以实现Host与Guest更高效的协作,从而实现更高的性能。在此例子中,sda 作为根文件系统使用,vda 则是用于存储用户数据,在编译时,主要看得是 vda 分区的IO情况。
    vda 使用 ext4 文件系统。ext4 是目前常见的Linux上使用的稳定的文件系统,查看其超级块信息:

    使用的默认参数格式化的分区,为其分配了块大小为4K,inode数量达到19660万个且使能了日志。块大小设为4K,适用于当前源文件偏小的情况,这里为了更紧凑的空间降低块大小,空闲 inode 达到 14522万,空闲占比达到 73.86%;当前 74% 的空间使用率,inode只使用了26.14%。一个inode占256B,那么10000万个inode占用23.84G,inode 太多造成大量的空间浪费。inode数量在格式化时指定,后期无法修改,这种情况不能简单粗暴地重新格式化,要考虑从日志和挂载参数下手。

    日志是为了保证掉电时文件系统的一致性,(ordered日志模式下)通过把元数据写入到日志块,在写入数据后再修改元数据,如果此时掉电,通过日志记录可以回滚文件系统到上一个一致性的状态,即保证元数据与数据是匹配的(服务器有备用电源确定没有掉电风险可以取消日志)。

    由于时刻有任务在执行,直接umount或者-o remount,ro,无法在挂载时取消日志(修改挂在参数,减少日志损耗)。

    ext4挂在参数:data

    ext4有3种日志模式:
       1. jorunal:把元数据与数据一并写入到日志块,性能差不多折半,因为数据写了两次,但最安全
       2. writeback:把元数据写入日志块,数据不写入日志块,但不保证数据先落盘;性能最高,但由于不保证元数据与数据的顺序,也是掉电最不安全的
       3. ordered:与writeback相似,但会保证数据先落盘,再是元数据;折中性能以保证足够的安全,这是大多数PC上推荐的默认的模式
    1
    2
    3
    4
    在不需要担心掉电的服务器环境,我们完全可以使用writeback的日志模式,以获取最高的性能。

    不能动态修改,需要写入/etc/fstab中,重启生效。

    ext4挂在参数:noatime
    Linux上对每个文件都记录了3个时间戳

    编译执行的Make可以根据修改时间来判断是否要重新编译,atime记录的访问时间其实在很多场景下都是多余的,不记录atime可以大量减少读造成的元数据写入量,而元数据的写入往往产生大量的随机IO,因此noatime应运而生。

    ext4挂在参数:nobarrier
    主要是决定在日志代码中是否使用写屏障(write barrier),对日志提交进行正确的磁盘排序,使易失性磁盘写缓存可以安全使用,但会带来一些性能损失。从功能来看,跟writeback和ordered日志模式非常相似,禁用写屏障毫无疑问能提高写性能。

    ext4挂在参数:delalloc
    delalloc是 delayed allocation 的缩写,如果使能,则ext4会延缓申请数据块直至超时。*为什么要延缓申请呢?*在inode中采用多级索引的方式记录了文件数据所在的数据块编号,如果出现大文件,则会采用 extent 区段的形式,分配一片连续的块,inode中只需要记录开始块号与长度即可,不需要索引记录所有的块。这除了减轻inode的压力之外,连续的块可以把随机写改为顺序写,加快写性能。连续的块也符合局部性原理,在预读时可以加大命中概率,进而加快读性能。

    ext4挂载参数:inode_readahead_blks
    ext4从inode表中预读的indoe block最大数量。访问文件必须经过inode获取文件信息、数据块地址,如果需要访问的inode都在内存中命中,就不需要从磁盘中读取,毫无疑问能提高读性能。其默认值是32,表示最大预读 32 × block_size 即 64K 的inode数据,在内存充足的情况下,我们毫无疑问可以进一步扩大,让其预读更多。

    # mount -o ...inode_readahead_blks=4096... /home
    1
    ext4挂载参数:journal_async_commit
    commit块可以不等待descriptor块,直接往磁盘写。这会加快日志的速度。

    ext4挂载参数:commit
    ext4一次缓存多少秒的数据。默认值是5,表示如果此时掉电,你最多丢失5s的数据量。设置更大的数据,就可以缓存更多的数据。

    # mount -o ...commit=1000... /home
    1
    相对的掉电也有可能丢失更多的数据,在不怕掉电的情况,把数值加大可以提高性能。

    页缓存
    页缓存在FS与通用块层之间,其实也可以归到通用块层中。为了提高IO性能,减少真实的从磁盘读写的次数,Linux内核设计了一层内存缓存,把磁盘数据缓存到内存中。由于内存以4K大小的**页**为单位管理,磁盘数据也以页为单位缓存,因此也称为页缓存。在每个缓存页中,都包含了部分磁盘信息的副本。

    如果因为之前读写过或者被预读加载进来,要读取数据刚好在缓存中命中,就可以直接从缓存中读取,不需要深入到磁盘。不管是同步写还是异步写,都会把数据copy到缓存,差别在于异步写只是copy且把页标识脏后直接返回,而同步写还会调用类似fsync()的操作等待回写,详细可以看内核函数generic_file_write_iter(),异步写产生的脏数据会在“合适”的时候被内核工作队列writeback进程回刷。

    那么,什么时候是合适的时候呢?最多能缓存多少数据呢?毫无疑问延迟回刷可以在频繁的删改文件中减少写磁盘次数,缓存更多的数据可以更容易合并随机IO请求,有助于提升性能。

    在/proc/sys/vm中有以下文件与回刷脏数据密切相关:

    对上述的配置文件,有几点要补充的:

    1. XXX_ratio 和 XXX_bytes 是同一个配置属性的不同计算方法,优先级 XXX_bytes > XXX_ratio
      2. 可用内存并不是系统所有内存,而是free pages + reclaimable pages
      3. 脏数据超时表示内存中数据标识脏一定时间后,下次回刷进程工作时就必须回刷
      4. 回刷进程既会定时唤醒,也会在脏数据过多时被动唤醒
      5. dirty_background_XXX与dirty_XXX的差别在于前者只是唤醒回刷进程,此时应用依然可以异步写数据到Cache,当脏数据比例继续增加,触发dirty_XXX的条件,不再支持应用异步写
    1
    2
    3
    4
    5
    更完整的功能介绍,可以看内核文档Documentation/sysctl/vm.txt。

    Request层
    在异步写的场景中,当脏页达到一定比例,就需要通过通用块层把页缓存里的数据回刷到磁盘中。bio层记录了磁盘块与内存页之间的关系,在request层把多个物理块连续的bio合并成一个request,然后根据特定的IO调度算法对系统内所有进程产生的IO请求进行合并、排序。那么都有什么IO调度算法呢?

    请参考IO调度算法,本文不在赘述;

    这里简述下Block Layer ,对其描述形象阐述单队列与多队列的差异:

    单队列的架构,一个块设备只有一个全局队列,所有请求都要往这个队列里面塞,这在多核高并发的情况下,尤其像服务器动则32个核的情况下,为了保证互斥而加的锁就导致了非常大的开销。此外,如果磁盘支持多队列并行处理,单队列的模型不能充分发挥其优越的性能。

    多队列的架构下,创建了Software queues和Hardware dispatch queues两级队列。Software queues是每个CPU core一个队列,且在其中实现IO调度。由于每个CPU一个单独队列,因此不存在锁竞争问题。Hardware Dispatch Queues的数量跟硬件情况有关,每个磁盘一个队列,如果磁盘支持并行N个队列,则也会创建N个队列。在IO请求从Software queues提交到Hardware Dispatch Queues的过程中是需要加锁的。理论上,多队列的架构的效率最差也只是跟单队列架构持平。

    在/sys/block/vda/queue中有两个可写的文件nr_requests和read_ahead_kb,前者是配置块层最大可以申请的request数量,后者是预读最大的数据量,在无法修改IO算法的情况下可以考虑修改这两个参数,例如加大Request队列的长度,加大预读的数据量。
    ————————————————
    版权声明:本文为CSDN博主「@tiger_mu」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/m0_45922575/article/details/106262920

  • 相关阅读:
    剑指Offer(Java版)第三十七题:输入一个正整数数组,把数组里所有数字拼接起来排成一个数, 打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321}, 则打印出这三个数字能排成的最小数字为321323。
    剑指Offer(Java版)第三十六题:从1到非负整数n中1出现的次数 求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数? 为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次, 但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化, 可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
    剑指Offer(Java版)第三十五题:给一个数组,返回它的最大连续子序列的和
    剑指Offer(Java版)第三十四题:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
    剑指Offer(Java版)第三十三题:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。 由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
    剑指Offer(Java版)第三十二题:输入一个字符串,按字典序打印出该字符串中字符的所有排列。 例如输入字符串abc, 则打印出由字符a,b,c所能排列出来的 所有字符串abc,acb,bac,bca,cab和cba。
    剑指Offer(Java版)第三十一题:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。 要求不能创建任何新的结点,只能调整树中结点指针的指向。
    剑指Offer(Java版)第三十题(有难度):输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点, 另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。
    剑指Offer(Java版)第二十九题:输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。 路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
    剑指Offer(Java版)第二十八题:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。 如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
  • 原文地址:https://www.cnblogs.com/cute/p/15919898.html
Copyright © 2020-2023  润新知