网络编程--零拷贝文件传输,网络编程范式
零拷贝1.为什么要有 DMA 技术?在没有 DMA 技术前I/O 的过程是这样的CPU 发出对应磁盘控制器收到指令后于是就开始准备数据会把数据放入到磁盘控制器的内部缓冲区中然后产生一个中断CPU 收到中断信号后停下手头的工作利用中断处理函数完成接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器然后再把寄存器里的数据写入到内存。什么是 DMA 技术在进行 I/O 设备和内存的数据传输的时候数据搬运的工作全部交给 DMA 控制器。早期 DMA 只存在在主板上如今由于 I/O 设备越来越多数据传输的需求也不尽相同所以每个 I/O 设备里面都有自己的 DMA 控制器。传统的文件传输有多糟糕代码通常如下一般会需要两个系统调用代码很简单虽然就两行代码但是这里面发生了不少的事情。期间共发生了 4 次用户态与内核态的上下文切换一个系统调用涉及用户态-内核态内核态-用户态算两次。上下文切换到成本并不小一次切换需要耗时几十纳秒到几微秒虽然时间看上去很短但是在高并发的场景下这类时间容易被累积和放大从而影响系统的性能。发生了 4 次数据拷贝第一次拷贝把磁盘上的数据拷贝到操作系统内核的缓冲区里这个拷贝的过程是通过 DMA 搬运的。第二次拷贝把内核缓冲区的数据拷贝到用户的缓冲区里这个拷贝到过程是由 CPU 完成的。第三次拷贝把刚才拷贝到用户的缓冲区里的数据再拷贝到内核的 socket 的缓冲区里这个过程依然还是由 CPU 搬运的。第四次拷贝把内核的 socket 缓冲区里的数据拷贝到网卡的缓冲区里这个过程又是由 DMA 搬运的。要想提高网络文件传输的性能就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。如何实现零拷贝零拷贝技术实现的方式通常有 2 种mmap writesendfilemmap writemmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间这样操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。减少了一次数据拷贝。sendfile在 Linux 内核版本 2.1 中提供了一个专门发送文件的系统调用函数 sendfile()函数形式如下减少了一次系统调用和一次数据拷贝。如果网卡支持 SG-DMAThe Scatter-Gather Direct Memory Access技术和普通的 DMA 有所不同我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。从 Linux 内核 2.4 版本开始起对于支持网卡支持 SG-DMA 技术的情况下 sendfile() 系统调用的过程发生了点变化具体过程如下第一步通过 DMA 将磁盘上的数据拷贝到内核缓冲区里第二步缓冲区描述符和数据长度传到 socket 缓冲区这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里。减少了一次系统调用两次数据拷贝。大文件传输用什么方式实现先来看看最初的例子当调用 read 方法读取文件时进程实际上会阻塞在 read 方法调用因为要等待磁盘数据的返回如下图对于阻塞的问题可以用异步 I/O 来解决它工作方式如下图通常对于磁盘异步 I/O 只支持直接 I/O。此时异步 I/O 并没有涉及到 PageCache。直接 I/O 应用场景常见的两种应用程序已经实现了磁盘数据的缓存那么可以不需要 PageCache 再次缓存减少额外的性能损耗。在 MySQL 数据库中可以通过参数设置开启直接 I/O默认是不开启由于直接 I/O 绕过了 PageCache就无法享受内核的这两点的优化内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中最后「合并」成一个更大的 I/O 请求再发给磁盘这样做是为了减少磁盘的寻址操作内核也会「预读」后续的 I/O 请求放在 PageCache 中一样是为了减少对磁盘的操作网络编程1.select将已连接的 Socket 都放到一个文件描述符集合然后调用 select 函数将文件描述符集合拷贝到内核里让内核来检查是否有网络事件产生。检查的方式很粗暴就是通过遍历文件描述符集合的方式当检查到有事件产生后将此 Socket 标记为可读或可写。接着再把整个文件描述符集合拷贝回用户态里。用户态还需要再通过遍历的方法找到可读或可写的 Socket然后再对其处理。需要 2 次「遍历」文件描述符集合2 次「拷贝」文件描述符集合。select 使用固定长度的 BitsMap表示文件描述符集合而且所支持的文件描述符的个数是有限制的在 Linux 系统中由内核中的 FD_SETSIZE 限制 默认最大值为 1024只能监听 0~1023 的文件描述符。2.poll过程类型。需要 2 次「遍历」文件描述符集合2 次「拷贝」文件描述符集合。poll 不再用 BitsMap 来存储所关注的文件描述符取而代之用动态数组以链表形式来组织突破了 select 的文件描述符个数限制当然还会受到系统文件描述符限制。3.epollepoll 通过两个方面很好解决了 select/poll 的问题。第一点epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里。这样避免了select/poll每次执行检测需要全量拷贝问题。第二点 epoll 使用事件驱动的机制内核里维护了一个链表来记录就绪事件当某个 socket 有事件发生时通过回调函数内核会将其加入到这个就绪事件列表中当用户调用 epoll_wait() 函数时只会返回有事件发生的文件描述符的个数。避免了select/poll每次检测需要全量扫描问题。边缘触发和水平触发epoll 支持两种事件触发模式分别是边缘触发edge-triggeredET和水平触发level-triggeredLT。这两个术语还挺抽象的其实它们的区别还是很好理解的。使用边缘触发模式时当被监控的 Socket 描述符上有可读事件发生时服务器端只会从 epoll_wait 中苏醒一次。使用水平触发模式时当被监控的 Socket 上有可读事件发生时服务器端不断地从 epoll_wait 中苏醒直到内核缓冲区数据被 read 函数读完才结束。

相关新闻