Linux的io

2021/05/01

前言

上次介绍了计算机数据的存储器,这次探究总结一下linux的io传输,先回顾一下上次介绍的一些概念。

磁盘: 用户的持久化存储介质

PageCache-磁盘高速缓存-内核缓冲区: 对磁盘内容的缓存,其储存器是内存,速度快一些

虚拟内存: 提供给应用程序操作的内存,基本单位是page, 由MMU映射到物理内存

接下来我们会再介绍一些概念帮助更好地理解最后的内容,最后会介绍linux的传统拷贝和现在比较主流的先进拷贝(这个先进拷贝名字是我取得….为了和传统拷贝区分,其本质是利用零拷贝技术)

用户态和内核态

操作系统的核心是内核,它是独立于普通应用程序的程序,其可以访问受保护的内存空间,也拥有访问底层硬件设备的权限。为了避免用户进程之间操作内核,为了保证内核的安全,操作系统将虚拟内存划分成两部分,一部分是内核空间,一部分是用户空间。

总结(下面会反复提及):

内核模块运行在内核空间,对应的进程处于内核态;

用户程序运行在用户空间,对应的进程处于用户态。

引入DMA

CPU是电脑的大脑,大大小小的事情如果都需要CPU时时刻刻盯着的话,CPU的效率会非常低。现在CPU的主频都非常高了,计算数据比传输数据快的多,传统情况CPU如果需要计算数据它需要把数据读到缓存当中,可能计算只需要纳秒级别,而读取需要秒级别。当读取的时候,其实CPU是处于等待堵塞状态的,这部分时间如果能让别人来做就好了,CPU只需要少量甚至不需要拷贝,只需要计算就好了。从而引出了DMA(Direct Memory Access,直接存储器访问) ,目前,大部分的计算机都配备了 DMA 控制器,英特尔把DMA控制器集成在南桥里,整个过程无须 CPU 参与,数据直接通过 DMA 控制器进行快速地移动拷贝,节省 CPU 的资源去做其他工作。

传统拷贝

用户态直接io

用户态直接io是指应用进程或运行在用户态下的库函数直接访问硬件设备,数据直接跨过内核传输(内核指的是内核空间中PageCache),其中内核除了进行必要的虚拟存储配置工作之外,不参与任何的工作,这种上下文切换成本低,这些应用通常在进程空间(用户空间)有自己的数据缓存区。这种缺点非常明显犹豫cpu和磁盘io之间的速度差距,会造成大量的资源浪费,解决方案是配合异步io使用。注意这种通常适用于非常大的文件io(通常上GB)

使用PageCache缓存(内核缓冲区)

之前介绍过内核缓冲区(也就是PageCache), 读取小文件的时候,如果每次往内核缓冲区读一点,如果下次命中缓存效率会提高很多。

这也是传统拷贝比较常见的io,我们重点来分析一下这种读取,它的开销(“换”指上下文切换,“拷”指拷贝操作)

  1. 用户进程通过read()函数向内核发起系统调用,上下文从用户态切换成内核态【换-1】
  2. 内核态下CPU利用DMA控制器将数据从硬盘拷贝到内核空间的内核缓冲区【拷-1】
  3. CPU将从内核缓冲区的数据拷到用户缓冲区【拷-2】
  4. read()调用执行返回,上下文从内核态切回用户态【换-2】
  5. 在用户态下用户进程调用write()函数向内核发起系统调用,上下文从用户态切换成内核态【换-3】
  6. CPU将用户缓冲区的数据拷到内核空间的网络缓冲区(socket buffer)【拷-3】
  7. CPU利用DMA控制器将数据从网络缓冲区拷到网卡进行数据传输【拷-4】
  8. write()系统执行返回,上下文从内核态切回用户态【换-4】

可以看到,使用传统使用内核缓冲区拷贝,需要经过四次拷贝(CPU两次+DMA两次)和四次上下文切换, 在高并发场景,因为会进行频繁的io调用和频繁的上下文切换,这是非常影响性能的。

先进拷贝-零拷贝

零拷贝的概念其实并没有非常明确,或者说零拷贝不是一次拷贝都没有,但是零拷贝的核心非常明确-提高io性能。

目前主流核心思想和大致归为以下三类

  • 减少甚至避免用户空间和内核空间之间的数据拷贝:在一些场景下,用户进程在数据传输过程中并不需要对数据进行访问和处理,那么数据在 Linux 的 Page Cache 和用户进程的缓冲区之间的传输就完全可以避免,让数据拷贝完全在内核里进行,甚至可以通过更巧妙的方式避免在内核里的数据拷贝。这一类实现一般是通过增加新的系统调用来完成的,比如 Linux 中的 mmap(),sendfile() 等。

  • 绕过内核的直接 I/O:允许在用户态进程绕过内核直接和硬件进行数据传输,内核在传输过程中只负责一些管理和辅助的工作。这种方式其实和第一种有点类似,也是试图避免用户空间和内核空间之间的数据传输,只是第一种方式是把数据传输过程放在内核态完成,而这种方式则是直接绕过内核和硬件通信,效果类似但原理完全不同。

  • 内核缓冲区和用户缓冲区之间的传输优化:这种方式侧重于在用户进程的缓冲区和操作系统的页缓存之间的 CPU 拷贝的优化。这种方法延续了以往那种传统的通信方式,但更灵活。

    img

图来自github用户Andy Pan(引用3)

mmap() 替换read()

mmap()的思想是对用户缓冲区对内核缓冲区进行映射,这样就可以减少一次cpu拷贝到用户缓冲区,其他都不变,但是需要增加映射的成本。分析过程

  1. 用户进程通过mmap()函数向内核发起系统调用,上下文从用户态切换成内核态【换-1】
  2. 内核态下CPU利用DMA控制器将数据从硬盘拷贝到内核空间的内核缓冲区(此时用户缓冲区已经映射过去了)【拷-1】
  3. mmap()调用执行返回,上下文从内核态切回用户态【换-2】
  4. 在用户态下用户进程调用write()函数向内核发起系统调用,上下文从用户态切换成内核态【换-3】
  5. CPU将用户缓冲区的数据拷到内核空间的网络缓冲区(socket buffer)【拷-3】
  6. CPU利用DMA控制器将数据从网络缓冲区拷到网卡进行数据传输【拷-4】
  7. write()系统执行返回,上下文从内核态切回用户态【换-4】

通过这种方式,有两个优点:一是节省内存空间,因为用户进程上的这一段内存是虚拟的,并不真正占据物理内存,只是映射到文件所在的内核缓冲区上,因此可以节省一半的内存占用;二是省去了一次 CPU 拷贝,对比传统的 Linux I/O 读写,数据不需要再经过用户进程进行转发了,而是直接在内核里就完成了拷贝。所以使用 mmap() 之后的拷贝次数是 2 次 DMA 拷贝,1 次 CPU 拷贝,加起来一共 3 次拷贝操作,比传统的 I/O 方式节省了一次 CPU 拷贝以及一半的内存,不过因为 mmap() 也是一个系统调用,因此用户态和内核态的切换还是 4 次。

sendfile()替换read()

sendfile()的原理是利用linux2.1内核的sendfile()系统调用函数来替换read()和write(),senfile是数据完全屏蔽用户空间,适用于一些不需要用户空间处理的情况,可以使得数据直接在内核空间传输,因此避免了用户空间和内核空间的拷贝,节省了一次拷贝和两次上下文切换。分析过程

  1. 用户进程通过调用sendfile()函数向内核发起系统调用,上下文从用户态切换成内核态【换-1】
  2. 内核态下CPU利用DMA控制器将数据从硬盘拷贝到内核空间的内核缓冲区【拷-1】
  3. 接下来CPU直接把内核缓冲区中的数据拷贝到网络缓冲区(socket buffer) 【拷-2】
  4. CPU利用DMA控制器将网络缓冲区的数据再拷贝到网络【拷-3】
  5. sendfile()系统执行返回, 上下文从内核态切回用户态【换-2】

可以看到使用sendfile可以减少一次拷贝和两次上下文切换,与mmap内存映射方式不同的是,sendfile调用io数据对用户空间是完全不可见的,所以此方式局限于用户空间不需要对数据进行修改的io。

sendfile-linux2.4内核增强

看过程我们可以看到,从磁盘拷入到内核缓冲区,然后又到网络缓冲区,这部分数据的传递是否多余。Linux 2.4 版本的内核对 sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作。它将内核空间的内核缓冲区中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区中,由 DMA 根据内存地址、地址偏移量将数据批量地从读内核冲区拷贝到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作。分析过程

  1. 用户进程通过sendfile()方法向操作系统发起调用, 此时上下文从用户态转向内核态【换-1】

  2. 内核态下, DMA控制器把数据从磁盘读到读缓冲区【拷-1】,
  3. cpu把文件描述符和文件长度直接写到socekt缓冲区,这个不算拷贝
  4. DMA控制器使用scatter和gather把数据从读缓冲拷贝到网卡【拷-2】
  5. sendfile()返回,上下文又重新由内核态切换回用户态【换2】

一些中间件采用的拷贝技术

RocketMQ 选择了 mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输;

而 Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。但是值得注意的一点是,Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。

参考

  1. https://www.bilibili.com/video/BV1cJ411K7HW
  2. https://www.bilibili.com/video/BV16J411p7f1
  3. https://github.com/panjf2000?tab=repositories

(转载本站文章请注明作者和出处 没有气的汽水



┌┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┐
├ 文章已经完啦, 想要第一时间收到文章更新可以关注↓ ┤
└┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┘

Post Directory






下面是评论区,欢迎大家留言探讨或者指出错误哈