📖Linux|IO

2024-11-10|2025-1-11
黎明
黎明
type
status
date
slug
summary
tags
category
icon
password
进度
 
 

专业术语 📖

  • 轮训 Polling
  • 阻塞 Blocking
  • 非阻塞 Non-Blocking
  • 复用 Multiplexing ˈməltēˌpleks
  • 同步 Synchronous ˈsiNGkrənəs
  • 异步 Asynchronous āˈsiNGkrənəs
  • 发起方 Initiator
  • 接受方 Receiver
  • SG DMA Scatter-Gather Direct Memory Access
Scatter: 连续内存空间 Gather:非连续地址空间。SG DMA允许在不需要CPU参与的情况下,将数据从多个源地址传输到多个目的地地址。常用于网络IO、磁盘IO等高速传输场景。
  • 机械硬盘 HDD Hard Disk Drive
  • 固态硬盘 SSD Solid State Drive
  • 非易失性内存主机控制器接口规范 NVMe
  • 串行高级技术附件 SATA
  • Epoll Event Poll

IO

Linux系统主要IO有磁盘IO、网络IO、内存IO、外设IO、GPUIO等。所谓IO就是对存储设备中数据读取和写入(输入和输出)。
关键点:IO设备是否准备完成;若读取数据时,数据未准备完成,该如何处理?等待 or 返回?
 

IO模型

Blocking IO

为完成功能,发起方发起一个调用。若功能无法立即完成,则调用不返回,一直等待。
notion image
  • 发起系统调用后,内核读取IO设备数据。若数据未准备好,则一直等待。
  • 内核读取IO设备数据是同步的
 
  1. 用户进程发起IO操作,用户进程被挂起(wait for data)
  1. 内核发起实际IO操作,等待IO操作完成(wait for data)
  1. 实际IO操作完成,copy数据到用户空间(copy data from kernel to user)
  1. 唤醒用户进程执行
 
用户进程被挂起,直到实际IO操作完成。因此是阻塞IO。
 

Non-Blocking IO

为完成功能,发起方发起一个调用。若功能无法立即完成,但立即返回特定错误。发起方轮训。
notion image
  • 发起方轮训,直到服务方数据Reader
 
  1. 用户进程发起IO操作,设置non-blocking参数
  1. 内核数据未Ready,返回特定错误。同时内核发起实际IO操作。用户进程轮训(wait for data)
  1. 内核数据Ready,用户进程将被挂起,内核复制数据到用户空间(copy data from kernel to user)
  1. 唤醒用户进程继续执行
 
相比BIO,NIO在数据non-ready时未挂起,而是轮训。
 

IO Multiplexing

IO多路复用表示多个IO操作复用一个进程。
notion image
  • 内核提供多路复用的系统调用
select poll epoll
由内核负责监听提交的fd信息。当监听事件发生时,返回fd。
  1. 用户进程调用select系统调用,内核监听fd数据non-ready,挂起用户进程(wait for data)
  1. 内核监听的fd的数据ready时,唤醒可读的fd。(wait for data)
  1. 用户进程发起读取系统调用,内核复制数据到用户空间(copy data from kernel to user)
 
IO多路复用的优势就是单进程可以同时处理大量连接。
 

Asynchronous I/O

notion image
  • AIO执行IO操作时,数据non-ready时直接返回,不挂起用户进程。
  • 内核等待数据ready,内核复制数据到用户空间
  • 重点:复制完毕通知用户进程处理(一种回调机制)
 
  1. 用户进程发起IO操作,执行系统调用,直接返回,同时内核准备数据。(wait for data)
  1. 数据Ready,内核复制数据到用户空间(发起时的AIO控制块)。(copy data from kernel to user)
  1. 复制完毕,发送信号回调到信号处理器。
 

Signal-Driven I/O 

要点:向内核注册<信号,信号处理函数>
notion image
  • 注册 <single, single handler>
  • 数据ready发送信号
     
    1. 用户进程调用sigaction系统调用来注册信号处理程序,信号为SIGIO,不挂起用户进程,立即返回。
    1. 数据Ready,发送SIGIO
    1. 用户发起IO操作,准备读取数据,内核挂起用户进程,复制数据到用户进程。
    1. 复制完毕,唤醒用户进程。
     
    notion image
     
    BIO, NIO, IO Multiplexing, SIGIO 在第1阶段不同,第2阶段都是阻塞的。(看图品品)
     

    流程化交互

    同步

    一个流程包含多个功能,顺序执行,完成一个任务之后才可以执行下个任务。
    特点:
    操作流程虽然简单,但是效率相对低
     

    异步

    一个流程包含多个功能,无序执行,无论上个任务是否完成,都可以执行下一个任务。
    特点:
    操作流程相对复杂,但是效率相对高
     
     
    IO模型
    理解
    BIO
    同步阻塞IO
    NIO
    异步阻塞IO
    IO Multiplexing
    异步阻塞IO
    SIGIO
    异步阻塞IO
    AIO
    异步非阻塞IO
     
    notion image
    CPU、Mem的访问耗时是ns级,相对而言IO设备慢很多。
    notion image
    • 2次CPU拷贝,2次DMA拷贝。该传输方式效率太低,目标是将磁盘数据传输到网卡中,但是却将数据搬运4次。
    • 4次上下文切换(上下文耗时十几纳秒或者几微秒,高并发场景,上下文时间会积累和放大,负载越高耗时也会增大,形成螺旋)。
    🤔IO操作为啥需要缓存区?当然是提效啦!
    Linux IO设备主要有字符设备(键盘、鼠标、打印机)、块设备(磁盘、UDB、SSD)和网络设备(网卡)。耗时如下:
    设备类型
    类型
    耗时估计
    块设备
    HDD
    5-20ms
    SATA SSD
    0.1-1ms
    NVMe SSD
    0.02-0.1ms
    字符设备
    串口
    1-10ms
    键盘鼠标
    1-10ms
    显示器/显卡
    1-10ms
    网络设备
    广域网
    10-100ms
    局域网
    0.1-1ms
    Wi-Fi
    1-10ms
    4G/5G
    20-50ms
     

    零拷贝

    如果可以直接磁盘数据转发到网络,则完全不需要CPU参与Copy,性能则大大提高。
    • mmp + write
    • sendfile
    notion image
    1. mmp + write (1次CPU Copy,4次上下文切换)
      1. mmp 系统调用将内核缓存地址和用户缓存区共享,减少一次CPU Copy
      2. write 系统调用(实际是从PageCache拷贝到Socket Buffer)
    1. sendfile (1次CPU Copy,2次上下文切换)
      1. sendfile 将数据从PageCache拷贝到SocketBuffer
      2. 缺点是用户进程读取不到数据,无法编辑
    1. sendfile + SG-DMA (0次CPU Copy,1次上下文切换)
      1. sendfile 由SG-DMA直接将数据搬运到网络设备
    性能比较:
    File size
    Normal file transfer (ms)
    transferTo (ms)
    7MB
    156
    45
    21MB
    337
    128
    63MB
    843
    387
    98MB
    1320
    617
    200MB
    2124
    1150
    350MB
    3631
    1762
    700MB
    13498
    4422
    1GB
    18399
    8537
     

    磁盘高速缓存(PageCache

    内存访问速度比磁盘快,容量比磁盘小。为提高读写吞吐量,想法是【读写磁盘】替换成【读写内存】,但是注定只能拷贝一部分数据,利于数据局部性原理,将相邻的数据(预读)。
    Linux PageCache单位: 4KB
    块存储大小基本单位 扇区: 512B
    示例:Linux read 32KB 数据时,但是内核会将0-32KB,32KB-64KB数据读取出来加载到PageCache。如果下次使用到32KB-64KB间的数据,则提升效率。
     
    优点:
    1. 缓存最近访问的数据
    1. 预读
    notion image
     
    notion image
     
    notion image
    🤔 PageCache 一定是好的吗?
    大文件传输(GB):
    notion image
    notion image
    采用PageCache:
    1. 注意用户进程是阻塞的
    1. PageCache 预读会多一次DMA Copy
    1. 大文件拷贝,导致PageCache耗尽和热点数据减少,反而无法发挥PageCache性能

    绕过PageCache,直接读更好;
     

    优化如下:
    1. 异步IO,用户进程不阻塞
    1. 直接 I/O
     
     
    高并发文件传输时:需要根据文件大小采取不同策略
    1. 传输大文件 → 异步IO + 直接IO
    1. 传输小文件 → 零拷贝
     
    🤔 多少字节算大文件?多少字节算小文件?
    一般标准
    小文件
    中文件
    大文件
    超大文件
    大小
    <1MB
    1MB~100MB
    100MB~1GB
    >1GB
    应用
    快速编辑 快速传输
    高分辨图片 短视频 音频 简单文档
    超高清图片 长视频 复杂文档
    高清长视频 游戏包 数据库
     

    系统调用

     
     

    open

    open负责在内核生成与文件相对应的struct file元数据结构,并且与文件系统中该文件的struct inode进行关联,装载对应文件系统的操作回调函数,然后返回一个int fd给用户进程。后续用户对该文件的相关操作,会涉及到其相关的struct filestruct inodeinode->i_opinode->i_fopinode->i_mapping->a_ops等。
    notion image
     

    write

    write的写逻辑路径有好几条,最常使用的就是利用pagecache延迟写的这条路径,所以主要分析这个。在write调用的调用、返回之间,其负责分配新的pagecache,将数据写入pagecache,同时根据系统参数,判断pagecache中的脏数据占比来确定是否要触发回写逻辑。其详细的代码分析可以参考:《Linux 内核写文件过程》《Linux 内核延迟写机制》
    notion image
     

    read

    read的读逻辑中包含预期readahead的逻辑,其可以通过与fadvise的配合达到文件预取的效果。这部分的代码分析可以参考:《Linux 内核读文件过程》
    notion image
     

    fsync

    fsyncfdatasync主要逻辑流程基本相同。其通过触发对应文件的pagecache脏页回写,并且阻塞等待到回写逻辑完成,以达到同步数据的目的。
    notion image
     

    mmap

    用户调用mmap将文件映射到内存时,内核进行一系列的参数检查,然后创建对应的vma,然后给该vma绑定vma_ops。当用户访问到mmap对应的内存时,CPU 会触发page fault,在page fault回调中,将申请pagecache中的匿名页,读取文件到其物理内存中,然后将pagecache中所属的物理页与用户进程的vma进行映射。
    notion image
     
     

    io_uring

     

    epoll

    重点:一次监听多个 fd 的可读/可写状态
    notion image
    1. 调用 epoll_create() 函数创建并初始化一个 eventpoll 对象。
    1. 调用 epoll_ctl() 函数把被监听的文件句柄封装成 epitem 对象并且添加到 eventpoll 对象的红黑树中进行管理。
      1. op:
          • EPOLL_CTL_ADD:表示要进行添加操作。
          • EPOLL_CTL_DEL:表示要进行删除操作。
          • EPOLL_CTL_MOD:表示要进行修改操作。
      2. events
        1. 水平触发
          边缘触发
          EPOLLIN 可读
          EPOLLIN | EPOLLET
          EPOLLOUT 可写
          EPOLLOUT | EPOLLET
          EPOLLERR 异常
          EPOLLERR | EPOLLET
          EPOLLHUP 挂起
          EPOLLHUP | EPOLLET
    LT和ET在处理 I/O 事件方面的不同语义:
    • LT 模式下,是否通知用户程序取决于状态。比如上面的 rfd,它还有数据没读完,所以其状态依然是"可读" readable,也就是还有 I/O 事件没处理完,那么 LT 就会一直通知,直到所有的 I/O 事件被处理完,也就是 rfd 中的所有数据都被读取出来了,状态变为"不可读" unreadable。
    • ET 模式下,是否通知并取决于状态变化。还是以上面的 rfd 为例,ET 模式下的第 3 步的 epoll_wait(2) 之所以会通知用户程序,就是因为 pipe 中写入了数据,所以 rfd 的状态从一开始的 unreadble 变成了 readable,所以 ET 会触发通知;同理,第 5 步之所以不会触发通知,是因为自从第一次状态变化之后 rfd 的状态并没有再次发生转变,一开始是 readable,现在还是 readable,所以 ET 就不再通知了。
    正如Linus所说的那样3
    This is literally an epoll() confusion about what an "edge" is.
    An edge is not "somebody wrote more data". An edge is "there was no data, now there is data".
    And a level triggered event is also not "somebody wrote more data". A level-triggered signal is simply "there is data".
    Notice how neither edge nor level are about "more data". One is about the edge of "no data" -> "some data", and the other is just a "data is available".
     
    1. 当被监听的文件状态发生改变时,会把文件句柄对应 epitem 对象添加到 eventpoll 对象的就绪队列 rdllist 中。并且把就绪队列的文件列表复制到 epoll_wait() 函数的 events 参数中。
    1. 唤醒调用 epoll_wait() 函数被阻塞(睡眠)的进程。(伏笔)(nginx accept 惊群)
     
     
     
     
     
     
    Linux|网络工具|Vim IDE
    Loading...