草稿!Linux网络系统总结!
网络系统
文件传输
- 磁盘是计算机系统最慢的硬件设备之一,读写速度比内存慢百倍以上,
- 所以优化磁盘的技术非常多
- 零拷贝 直接IO 异步IO等
- 都是为了提升系统吞吐量
- 操作系统内核中还有磁盘高速缓冲区 目的是减少磁盘访问次数
以文件传输,分析io工作方式 以及如何优化传输文件的性能
DMA技术
传统IO过程
- CPU发出对应的指令(IO 请求)给磁盘控制器,然后返回(是指继续手头工作么?)
- 磁盘控制器收到指令/请求后,开始准备数据,会把数据放到一个 磁盘控制器的内部缓冲区,然后产生一个中断信号,发送给cpu
- cpu收到中断信号后,停下手头工作,接着把磁盘控制器的缓冲区数据 一次一个字节地读取进cpu自己地寄存器,最后把寄存器里面地数据写入 内存!
- 数据传输期间cpu无法执行其他任务!
整个数据传输过程,都需要cpu亲自参与,亲自逐个搬运数据,而这个过程,cpu完全不能做其他事情
简单搬运几个字节是ok 但是 如果我们用千兆网卡或者硬盘传输大量数据 都用cpu来搬运 是不行的 cpu资源非常重要!不能浪费在这个简单工作上面!
于是计算机科学家发明了直接内存访问DMA direct Memory access!技术
DMA
在进行io设备和内存的数据传输时 数据搬运的工作全部交给DMA控制器
cpu不再参与 任何与数据搬运有关的工作!只负责发起io请求(并且是对DMA控制器发起这个请求!) 接收数据搬运完毕的信号(从DMA接收!)
cpu不再直接与磁盘交互 数据是DMA搬运的 ! 交互也改为了CPU与DMA~
这样 cpu就可以在DMA处理io任务时去处理其他任务!
具体的过程
涉及到用户进程 操作系统cpu DMA 磁盘 这几个对象
用户进程在调用读取数据的时候 会进入阻塞状态
- 用户进程希望从磁盘读取数据,会调用read方法,向操作系统发送io请求 请求读取数据到用户进程自身的内存缓冲区!发送read后 用户进程会进入阻塞状态,等待读取
- 操作系统cpu收到请求后,进一步将io请求发送给DMA,然后就去执行其他任务
- DMA收到操作系统发来的io请求后 进一步将请求发送给 磁盘
- 磁盘收到DMA发来 的io请求之后
- 先把用户进程请求的数据 放到磁盘控制器自己的缓冲区
- 当磁盘控制器的缓冲区被读满
- 向DMA发送中断信号 告知DMA,磁盘控制器的缓冲区已满
- DMA收到磁盘信号后,
- 将磁盘控制器缓冲区中的数据拷贝到操作系统内核缓冲区
- 此时不占用cpu cpu可以执行其他
- 当DMA读取到足够多的数据 就会发送中断信号给cpu
- cpu收到dma的中断信号后,就知道了数据已经准备够了,于是将内核中的数据拷贝到用户空间的用户进程!最后返回 用户进程也结束阻塞
cpu不再 参与直接的搬运工作 也就是 将数据从磁盘控制器缓冲区 搬运到内核空间的工作 这部分搬运工的工作 全部由dma完成
但是cpu对于io工作依旧是必不可少的 因为用户进程需要什么数据 从哪里搬到哪里 都需要用户进程通过cpu来告诉dma来控制dma!!
传统文件传输极其糟糕的四次切换和四次拷贝!
比如 服务端可以给客户端提供文件传输功能 , 最简单的方式就是两步:
- 从服务端磁盘读文件
- 网络协议发送给客户端
注意 我们只要传输一份数据 从服务端的磁盘 到 网卡 然后再通过网卡发送出去!
但是这么简单的一个过程却发生了四次拷贝 以及 四次 用户态内核态之间的来回切换!!!
比如这个过程的代码是
read(file, tmp_buf, len);// 服务端磁盘读取数据
write(socket, tmp_buf, len);// 数据写入网卡
用到 两个系统调用!read()和write() 每次系统调用都得先从用户态切换内核态,内核完成任务后,再从内核态切换用户态
这样就发生了四次上下文切换!一次切换虽然只需要几十纳秒到几微秒 看上去很短 但是高并发场景下 这会被大大放大!从而拖垮系统性能!
还有四次数据拷贝!
其中两次是DMA的拷贝!另外两次是通过CPU拷贝!
- 第一次拷贝:磁盘数据 经由 DMA搬运 到 操作系统内核的缓冲区
- 第二次拷贝:操作系统内核缓冲区的数据 经由CPU 搬运到 用户缓冲区
- 第三次拷贝:用户态缓冲区数据 经由CPU 拷贝到 内核socket的缓冲区
- 第四次拷贝:内核的socket缓冲区数据 经由DMA 搬运到网卡缓冲区
只是从磁盘发一份数据 !光是从磁盘到网卡都经过了四次拷贝!
磁盘 (DMA) 内核态缓冲区 (CPU)用户态缓冲区 (CPU) socket 缓冲区 (DMA) 网卡缓冲区!
只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源大大降低了系统性能。存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的多了很多不必要的开销,会严重影响系统性能。
要提高文件传输性能 最关键的是 减少用户态与内核态的上下文切换 和 内存拷贝的次数!
如何优化文件传输的性能?
首先思考为什么前面传统传输数据方式这么繁琐!四次拷贝 内核态 用户态搬来搬去?
根本原因 在于 读取磁盘 或者操作网卡 时,用户空间没有权限操作网卡和磁盘!内核权限最高 所以这些操作设备的过程都得操作系统内核完成!
一般需要内核时,我们会使用操作系统提供的系统调用函数!
一次系统调用就必然发生两次上下文切换
先从用户态切换到内核态
待内核态执行完任务 再切换回用户态交由进程代码执行
所以要想减少数据拷贝次数 要减少上下文切换 即内核和用户态切换 那就要减少系统调用次数
传统我文件传输方式的四次数据拷贝中 首先 第一次和第四次
从内核读缓冲区拷贝到用户的缓冲区
用户缓冲区拷贝到socket缓冲区 没必要
因为用户空间中我们不会对数据再次加工!
所以将数据搬运进用户空间就是多此一举!
用户的缓冲区没有存在必要
如何实现零拷贝呢?
两种减少上下文切换和数据拷贝次数的方法!:
- mmap+write
- sendfile
原来传统的方法是:
read(file, tmp_buf, len);// 服务端磁盘读取数据
write(socket, tmp_buf, len);// 数据写入网卡
read()这个系统调用会把内核缓冲区数据拷贝到 用户缓冲区!
为了减少这一步开销!我们用mmap()替换掉 read() 系统调用函数
// read(file, tmp_buf, len); 替换为
buf = mmap(file, len);
write(sockfd, buf, len);
(豆包详细解释这里每个参数 调用 传入传出的东西!)
read()是将内核态缓冲区数据拷贝到用户态缓冲区!
mmap()不在用户态和内核态之间搬运数据 而是
创建了用户态缓冲区和内核态缓冲区 之间的 共享缓冲区
mmap() 系统调用函数 直接将内核缓冲区的数据映射到 用户空间 这样 操作系统内核与用户空间就不需要进行数据拷贝
使用mmap()代替read() 可以减少一次拷贝开销!本质上是减少了 将内核缓冲区数据经由cpu拷贝到用户缓冲区这一步!
具体:
- 应用进程调用mmap() DMA 会把磁盘的数据拷贝到 内核的缓冲区中 。接着应用进程跟操作系统内核会共享这个缓冲区!!
- 应用进程再调用write()时 操作系统直接将内核缓冲区(视为与应用进程共享 )的数据拷贝到socket缓冲区!这一切都发生在内核态 !这一步由CPU搬运
- 最后, 还是把socket中数据搬运到拷贝到网卡缓冲区 这个过程是DMA搬运!
通过mmap() 来代替read() 可以减少一次数据拷贝!
磁盘缓冲区 (DMA)内核缓冲区(与用户共享) (cpu搬运) socket缓冲区 (DMA) 网卡
mmap()这种方式只是减少了一次数据拷贝过程!仍然是cpu拷贝一次占用cpu
仍然是两次系统调用 所以仍需要4次上下文切换!
sendfile()
这是Linux内核版本2.1中 提供的专门发送文件的系统调用函数sendfile()!|
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd in_fd 分别是目的端 源端的文件描述符!
后面的offset count分别是源端的偏移量和复制数据的长度
返回值是实际复制数据长度!
sendfile一个代替了两个系统调用read() 和write()
减少了一次系统调用 减少了两次系统调用!
这样做彻底不再考虑用户态,跳过搬运到用户态缓冲区这一步!直接将内核缓冲区的数据拷贝到socket缓冲区!这样就只有两次上下文切换(因为一次系统调用!)和三次数据拷贝!
但这样仍然开销过大!仍然占用cpu进行第二次拷贝 也就是从内核缓冲区搬运到socket缓冲区!
还有优化空间!如果网卡支持SG-DMA!那么就可以把这一步也节省掉!
SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
我们可以在Linux系统通过这个命令查看网卡是否支持这个SG-DMA(The Scatter-Gather Direct Memory Access)技术!
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
如果支持SG-DMA!?
- 第一步依旧是DMA将磁盘数据拷贝搬运到内核缓冲区!
- 第二步 不再是将全部数据拷贝搬运到 socket缓冲区! 而是 只将缓冲区描述符和数据长度传到socket缓冲区!然后由SG-DMA控制器直接将内核缓冲区的数据拷贝到网卡缓冲区!
- 这个技术不再需要将数据从内核缓冲区拷贝到socket缓冲区!减少了一次CPU的数据拷贝!
所以这个过程只执行了两次数据拷贝 且都由DMA操作 不占用CPU!
这个技术全程没有用cpu搬运数据,因为没有在内存层面拷贝数据 没用cpu所以叫做零拷贝技术!
所有数据都是DMA搬运!
SG-DMA 零拷贝相比传统方式 两次系统调用四次上下文开销 四次拷贝! 只有一次系统调用 两次上下文开销 两次拷贝 而且还是dma拷贝 没占用cpu!所以零拷贝技术至少将文件传输性能提高一倍不止!
使用零拷贝的项目有哪些?(可以深入拓展一点其他的项目!)
一个是kafka 这个开源项目!利用了零拷贝技术!!
大幅度提高了i/o吞吐量!零拷贝利用就是kafka处理海量数据这么快的原因!
看源码它调用了 Java NIO 库里的 transferTo 方法:
如果Linux支持sendfile() 系统调用实际上 transferTo 最后就是使用的sendfile()!
经过实测零拷贝能够缩短65%y以上的文件传输时间!
另外 就是Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率
(简单介绍这两个项目!)
http {
…
sendfile on
…
}
即Nginx可以配置
sendfile
设置为on表示 用零拷贝技术传输文件 : sendfile!这样只需要两次上下文开销(一次sendfile系统开销) 2次数据拷贝(DMA)
设置为off表示 使用传统文件传输技术! read + write!两次系统调用 四次上下文切换 四次拷贝(2次cpu 2次dma)
注意!要用sendfile需要Linux2.1以上版本!
PageCache?
刚刚我们所有的文件传输方法 第一步都是将磁盘缓冲区数据 拷贝到 内核缓冲区!
这个内核缓冲区 实际上就是 磁盘高速缓存PageCache
pagecache磁盘高速缓存(内核缓冲区)技术使得零拷贝性能进一步提升
我们都知道 磁盘读写速度太慢 我们应该 尽量把读写磁盘 替换成 读写内存!
于是我们用DMA把磁盘数据搬运到内存 这样就通过读内存代替读磁盘
但内存空间很小 内存注定只能拷贝磁盘很小一部分数据
那么问题是 选择那些磁盘数据拷贝到内存?更加有利于传输整个文件?
程序运行时具有{局部性}(豆包简要解析这个局部性!)所以通常刚访问的数据短时间内再次被访概率比较高
所以pagecache的主要功能就是缓存最近被访问的数据!
当空间不足时淘汰最久没访问的缓存!
即读取磁盘数据时,优先在pagecache里面找,如果数据存在直接返回,如果没有 从磁盘里面找!找到后还要缓存进pagecache!
pacache还有个预读功能!因为机械磁盘读取数据时候 需要先找到数据所在位置 具体是 通过磁头旋转到数据所在扇区 再开始顺序的读取数据 但是旋转磁头的操作非常耗时
所以pagecache 有预读 这个功能来提升效率!
比如read 每次只会 读取 32KB字节 刚开始read只读取前面0—32KB字节!但是内核会将后面的32—64kb 一同读取到pageCache 这样 后面如果读取32-64KB就可以直接从缓冲区pagecache中得到! 如果在32-64Kb淘汰出pagecache之前 被进程读取到 那收益极大!
所以总的来说pagecache两个大优点
- 缓存最近被访问的数据
- 预读
虽然这两个做法可以大大提高磁盘读写性能!
但是 对于GB级别大文件 pagecache很可能不起作用!