龙空技术网

简述 Linux I/O 原理及零拷贝(下)—网络 I/O

零声教育 347

前言:

目前同学们对“复制文件过程中突然中断”可能比较关注,同学们都需要剖析一些“复制文件过程中突然中断”的相关内容。那么小编在网络上收集了一些对于“复制文件过程中突然中断””的相关资讯,希望你们能喜欢,大家快快来了解一下吧!

简述

这已经是 Linux I/O 系列的第二篇文章。之前我们讨论了“磁盘 I/O 及磁盘 I/O 中的部分零拷贝技术” 本篇开始讨论“Linux 网络 I/O 的结构”以及大家关心的零拷贝技术。

socket 发送和接收的过程

socket 是 Linux 内核对 TCP/UDP 的抽象,在这里我们只讨论大家最关心的 TCP。

TCP 如何发送数据

图-1

图-2

程序调用了 write/send,进入内核空间。内核根据发送数据创建 sk_buff 链表,sk_buff 中最多会包含 MSS 字节。相当于用 sk_buff 把数据切割了。这个 sk_buff 形成的链表,就是常说的 socket 发送缓冲区。 *另外 ,有关MSS 的具体内容我们需要另外写一篇文章讨论,这里我们只要理解为网卡的限制即可检查堵塞窗口和接收窗口,判断接收方是否可以接收新数据。 创建数据包(packet,或者叫 TCP 分段 TCP segment);添加 TCP 头,进行 TCP 校验。执行 IP 路由选择,添加 IP 头,进行 IP 校验。 通过 QDisc(排队规则)队列将数据包缓存起来,用来控制网络收发的速度。经过排队,数据包被发送到驱动,被放入 Ring Buffer(Tx.ring)输出队列。网卡驱动调用 DMA engine 将数据从系统内存中拷贝到它自己的内存中。NIC 会向数据包中增加帧间隙(Inter-Frame Gap,IFG),同步码(preamble)和 CRC 校验。当 NIC 发送了数据包,NIC 会在主机的 CPU 上产生中断,使内核确认已发送。相关视频推荐

LinuxC++零拷贝的实现 用户态协议栈 ntytcp

面试中正经“八股文”网络原理tcp/udp,网络编程epoll/reactor

6种epoll的设计,让你吊打面试官,而且他不能还嘴

LinuxC++后台服务器开发架构师免费学习地址C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!后台私信“1”(需要自取)

TCP 如何接收数据

图-3

图-4

(从下往上看)

当收到报文时,NIC 把数据包写入它自身的内存。 NIC 通过 CRC 校验检查数据包是否有效,之后调用 DMA 把数据包发送到主机的内存缓冲区,这是驱动程序提前向内核申请好的一块内存区域。(sk_buff 线性的数据缓冲区,后面会讲)数据包的实际大小、checksum 和其他信息会保存在独立的 Ring Buffer(Rx.ring) 中,Ring Buffer 接收之后,NIC 会向主机发出中断,告知内核有新的数据到达。收到中断,驱动会把数据包包装成指定的数据结构(sk_buff)并发送到上一层。链路层会检查数据包是否有效并且解析出上层的协议(网络协议)。IP 层同样会检查数据包是否有效。检查 IP checksum。TCP 层检查数据包是否有效。检查 TCP checksum。根据 TCP 控制块中的端口号信息,找到对应的 socket,数据会被增加到 socket 的接收缓冲区,socket 接收缓冲区的大小就是 TCP 接收窗口。当应用程序调用 read 系统调用时,程序会切换到内核区,并且会把 socket 接收缓冲区中的数据拷贝到用户区,拷贝后的数据会从 socket 缓冲区中移除。

1 各层的关键结构

1.1 Socket 层的 Socket Buffer

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。

那么数据写入到哪里了?又是从哪里读出来的呢?这就要进入一个抽象的概念“Socket Buffer”。

1.1.1 逻辑上的概念

Socket Buffer 是发送缓冲区和接收缓冲区的统称。

发送缓冲区 进程调用 send() 后,内核会将数据拷贝进入 socket 的发送缓冲区之中。不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP 协议负责的。接收缓冲区 接收缓冲区被 TCP 和 UDP 用来缓存网络上来的数据,一直保存到应用进程读走为止。recv(),就是把接收缓冲区中的数据拷贝到应用层用户的内存里面,并返回。1.1.2 SKB数据结构(线性buffer)

Socket Buffer 的设计应该符合两个要求

保持实际在网络中传输的数据。数据在各协议层传输的过程中,尽量减少拷贝。

怎么才能做到呢?

图-5

图-6

图-7

每个 socket 被创建后,内核都会为其分配一个 Socket Buffer(其实是抽象的)。Socket Buffer 指的是 sk_buff 链表,初始时只是一个空的指针。所以初始时 sk_buff_head 的 next 和 prev 都是空。write 和 receive 的过程就是 sk_buff 链表 append 的过程。sk_buff 是内核对TCP数据包的一个抽象表示,所以最大不能超过最大传输量MSS,或者说长度是固定的。sk_buff 的结构设计是为了方便数据的跨层传递。skb 通过 alloc_skb 和 skb_reserve 申请和释放,因此 skb 是有个池的概念的及“线性的数据缓冲区”。

1.1.3 总结(重要,关系到零拷贝的理解)只在两种情况下创建 sk_buff:应用程序给 socket 写入数据时。当数据包到达 NIC 时。数据只会拷贝两次:用户空间与内核空间之间的拷贝(socket 的 read、write)。sk_buff 与 NIC 之间的拷贝。1.1.4 误区

根据《Unix网络编程V1, 2.11.2》中的描述:

TCP 的 socket 中包含发送缓冲区和接收缓冲区。UDP 的 socket 中只有一个接收缓冲区,没有发送缓冲区。

UDP 如果没有发送缓冲区,怎么实现多层协议之间的交换数据呢?

参考 man 手册:udpwmemmin 和 udprmemmin 不就是送缓冲区和接收缓冲区吗?

1.2 QDisc

QDisc(排队规则)是 queueing discipline 的简写。位于 IP 层和网卡的 Ring Buffer 之间,是 IP 层流量控制(traffic control)的基础。QDisc 的队列长度由 txqueuelen 设置,和网卡关联。

内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的Qdisc(排队规则)把数据包加入队列。然后,内核会尽可能多地从 Qdisc 里面取出数据包,把它们交给网络适配器驱动模块。

说白了,物理设备发送数据是有上限的,IP 层需要约束传输层的行为,避免数据大量堆积,平滑数据的发送。

1.3 Ring Buffer

1.3.1 简介

环形缓冲区 Ring Buffer,用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,其本质是个 FIFO 的队列,是为解决某些特殊情况下的竞争问题提供了一种免锁的方法,可以避免频繁的申请/释放内存,避免内存碎片的产生。

本文中讲的 Ring Buffer,特指 NIC 的驱动程序队列(driver queue),位于 NIC 和协议栈之间。

它的存在有两个重要作用:

可以平滑生产者和消费者的速度。通过 NAPI 的机制,合并以减少 IRQ 次数。

图-8

NIC (network interface card) 在系统启动过程中会向系统注册自己的各种信息,系统会分配 Ring Buffer 队列及一块专门的内核内存区用于存放传输上来的数据包。每个 NIC 对应一个R x.ring 和一个 Tx.ring。一个 Ring Buffer 上同一个时刻只有一个 CPU 处理数据。

Ring Buffer 队列内存放的是一个个描述符(Descriptor) ,其有两种状态:ready 和 used。初始时 Descriptor 是空的,指向一个空的 sk_buff,处在 ready 状态。当有数据时,DMA 负责从 NIC 取数据,并在 Ring Buffer 上按顺序找到下一个 ready 的 Descriptor,将数据存入该 Descriptor 指向的 sk_buff 中,并标记槽为 used。

Ring Buffer 可能被占满,占满之后再来的新数据包会被自动丢弃。为了提高并发度,支持多队列的网卡 driver 里,可以有多个 Rx.ring 和 Tx.ring。

1.3.2 Ring Buffer 误区

虽然名字中带 Buffer,但它其实是个队列,不会存储数据,因此不会发生数据拷贝。

2 关于网络 I/O 结构的总结网络 I/O 中,在内核空间只有一个地方存放数据,那就是 Socket Buffer。Socket Buffer 就是 sk_buff 链表,只有在 Socket 写入或者数据到达 NIC 时创建。sk_buff 是一个线性的数据缓冲区,是通过 alloc_skb 和 skb_reserve 申请和释放的。每个sk_buff 是固定大小的,这与 MTU 有关。数据只有两次拷贝:用户空间与 sk_buff 和 sk_buff 与 NIC。3 网络 I/O 中的零拷贝3.1 DPDK

网络 I/O 中没有没有类似 Direct I/O 的技术呢?答案是 DPDK

我们上面讲了,处理数据包的传统方式是 CPU 中断方式。网卡驱动接收到数据包后通过中断通知 CPU 处理,数据通过协议栈,保存在 Socket Buffer,最终用户态程序再通过中断取走数据,这种方式会产生大量 CPU 中断性能低下。

DPDK 则采用轮询方式实现数据包处理过程。DPDK 在用户态重载了网卡驱动,该驱动在收到数据包后不中断通知 CPU,而是通过 DMA 直接将数据拷贝至用户空间,这种处理方式节省了 CPU 中断时间、内存拷贝时间。

为了让驱动运行在用户态,Linux 提供 UIO(Userspace I/O)机制,使用 UIO 可以通过 read 感知中断,通过 mmap 实现和网卡的通讯。

图-93.1.1 DPDK 缺点

需要程序员做的事情太多,开发量太大,相当于程序员要把整个 IP 协议底层实现一遍。

4 跨越磁盘 I/O 和网络 I/O 的零拷贝

通过三篇文章,我们了解了 Linux I/O 的系统结构和基本原理。对于零拷贝,网上的文章很多,我们只要简单解读一下就可以了。

*另外强调一下,下面的解读,掺杂大量个人观点,并不权威,需要读者自行判断真伪。当然,如有理解错误之处也欢迎指正。

4.1 read + write

图-10

网上的总结

4次上下文切换,2次 CPU 拷贝和2次 DMA 拷贝。

解读

read 和 write,两次系统调用,每次从用户态切换到内核态,再从内核态切换回用户态,所以4次上下文切换。数据由 Page Cache 拷贝到用户空间,再由用户空间拷贝到 socket buffer,2次 CPU 拷贝。现在的磁盘和网卡都是支持 DMA 的,所以从磁盘到内存,从网卡到内存的数据都是 DMA 拷贝。4.2 mmap + write

图-11

网上的总结

4次上下文切换,1次 CPU 拷贝。针对大文件性能高,针对小文件需要内存对齐,所以浪费内存。

解读

mmap 和 write,两次系统调用,4次上下文切换,没问题。MMU 支持下,数据从虚拟地址到 socket buffer 的拷贝,实际是PageCache 到 socket buffer 的拷贝,所以1次 CPU 拷贝,也没问题。mmap 读取过程中,会触发多次缺页异常,造成上下文切换,所以越大的文件性能越差。不存在浪费内存,mmap 本质是 Buffer I/O,本来也是 page 对齐的。

补充

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

4.3 sendfile

图-12网上的总结2次上下文切换,1次CPU拷贝。针对大文件性能高,针对小文件需要内存对齐,所以浪费内存。

解读

sendfile,一次系统调用,2次上下文切换。数据从 Page Cache 拷贝到 socket buffer,所以1次 CPU 拷贝。sendfile才是更适合处理大文件,所有工作都是内核来完成的,效率一定高。

补充

Kafka采用的是sendfile这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。

4.3.1 sendfile,splice,tee 区别

sendfile 在内核态从 in_fd 中读取数据到一个内部 pipe,然后从 pipe 写入 out_fd 中; in_fd 不能是 socket 类型,因为根据函数原型,必须提供随机访问的语义。splice 类似 sendfile 但更通; 需要 fd_in 或者 fd_out 中,至少有一个是 pipe。vmsplice fd_in 必须为 pipe; 如果是写端则把 iov 部分数据挂载到这个 pipe 中(不拷贝数据),并通知 reader 有数据需要读取; 如果是读端,则从 pipe 中 copy 数据到 userspace。tee 需要 fd_in 和 fd_out 都必须为 pipe,从 fd_in pipe 中读取数据并挂载到 fd_out 中。

4.3.2 sendfile 是否可以用于 https 传输

我认为,基本上很难实现。http,https 在七层协议中,属于应用层,是在用户空间的。http 可以在用户空间写入 http 头信息,文件内容的拷贝由内核空间完成。https 的加密,解密工作是必须在用户空间完成的,除非内核支持,否则必须进行数据拷贝。

4.4 sendfile + DMA gather copy

传说中的,跨越磁盘 I/O 和网络 I/O 的零次 CPU 拷贝的技术。

图-13

网上的总结

在硬件的支持下,sendfile 拷贝方式不再从内核缓冲区的数据拷贝到 socket 缓冲区,取而代之的仅仅是缓冲区文件描述符和数据长度的拷贝,这样 DMA 引擎直接利用 gather 操作将页缓存中数据打包发送到网络中即可,本质就是和虚拟内存映射的思路类似。

解读

本人才疏学浅,认为这不太可能。DMA gather 是指 DMA 允许在一次单一的DMA处理中传输数据到多个内存区域,说白了就是支持批量操作,不会有太大差异。

Socket Buffer 结构是很复杂的,它担负着数据跨层传递的作用,如果传递过程中 Page Cache 中的数据被回收了怎么办?我觉得能说得过去的至少是图-14这种方式,而且内核需要有明确的 API 支持 socket_readfile。据我所知,Linux 并没有提供这种 API。

标签: #复制文件过程中突然中断 #socket接收缓冲设置大小