avpacket 判断i帧_简述 Linux I/O 原理及零拷贝(下) — 网络 I/O

本文深入探讨Linux网络I/O结构,讲解TCP发送和接收数据的过程,重点阐述socket缓冲区、QDisc和Ring Buffer。同时,文章介绍了零拷贝技术,如DPDK、mmap+write和sendfile在处理大文件时的优势,并对比了不同零拷贝方法的适用场景。
摘要由CSDN通过智能技术生成

点击蓝字

169565eaa65aa02b696a865992f03e47.gif

关注我们

74277a2988da4bcaf211b6fc3e7be910.png

作者简介

冯志明,2019年至今负责搜索算法的相关工作,擅长处理复杂的业务系统,对底层技术有浓厚兴趣。

简述

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

socket 发送和接收的过程

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

TCP 如何发送数据

a23ee72ea8b034ba5cc686bac87cc42d.png

图-1

7a2f3ed7882f0aaa1806130df25b55af.png

图-2

  1. 程序调用了 write/send,进入内核空间。

  2. 内核根据发送数据创建 sk_buff 链表,sk_buff 中最多会包含 MSS 字节。相当于用 sk_buff 把数据切割了。这个 sk_buff 形成的链表,就是常说的 socket 发送缓冲区。

    *另外 ,有关MSS 的具体内容我们需要另外写一篇文章讨论,这里我们只要理解为网卡的限制即可

  3. 检查堵塞窗口和接收窗口,判断接收方是否可以接收新数据。

    创建数据包(packet,或者叫 TCP 分段 TCP segment);添加 TCP 头,进行 TCP 校验。

  4. 执行 IP 路由选择,添加 IP 头,进行 IP 校验。

    通过 QDisc(排队规则)队列将数据包缓存起来,用来控制网络收发的速度。

  5. 经过排队,数据包被发送到驱动,被放入 Ring Buffer(Tx.ring)输出队列。

  6. 网卡驱动调用 DMA engine 将数据从系统内存中拷贝到它自己的内存中。

  7. NIC 会向数据包中增加帧间隙(Inter-Frame Gap,IFG),同步码(preamble)和 CRC 校验。当 NIC 发送了数据包,NIC 会在主机的 CPU 上产生中断,使内核确认已发送。

TCP 如何接收数据

fdf1e869a96a22d72d26ee0cc01d971f.png

图-3

0c7f31c85d47f5ac8145eb6910b11ea4.png

图-4

(从下往上看)

  1. 当收到报文时,NIC 把数据包写入它自身的内存。

    NIC 通过 CRC 校验检查数据包是否有效,之后调用 DMA 把数据包发送到主机的内存缓冲区,这是驱动程序提前向内核申请好的一块内存区域。(sk_buff 线性的数据缓冲区,后面会讲)

  2. 数据包的实际大小、checksum 和其他信息会保存在独立的 Ring Buffer(Rx.ring)  中,Ring Buffer 接收之后,NIC 会向主机发出中断,告知内核有新的数据到达。收到中断,驱动会把数据包包装成指定的数据结构(sk_buff)并发送到上一层。

  3. 链路层会检查数据包是否有效并且解析出上层的协议(网络协议)。

  4. IP 层同样会检查数据包是否有效。检查 IP checksum。

  5. TCP 层检查数据包是否有效。检查 TCP checksum。

  6. 根据 TCP 控制块中的端口号信息,找到对应的 socket,数据会被增加到 socket 的接收缓冲区,socket 接收缓冲区的大小就是 TCP 接收窗口。

  7. 当应用程序调用 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 的设计应该符合两个要求

  1. 保持实际在网络中传输的数据。

  2. 数据在各协议层传输的过程中,尽量减少拷贝。

怎么才能做到呢?

34f8c13a4abda5287e91fc94c7ed170e.png

图-5

b72ef19bd9287c22552d6659ebde2f98.png

图-6

7d28340c0d40d050cca0c445a91681cc.png

图-7

  1. 每个 socket 被创建后,内核都会为其分配一个 Socket Buffer(其实是抽象的)。Socket Buffer 指的是 sk_buff 链表,初始时只是一个空的指针。所以初始时 sk_buff_head 的 next 和 prev 都是空。

  2. write 和 receive 的过程就是 sk_buff 链表 append 的过程。

  3. sk_buff 是内核对TCP数据包的一个抽象表示,所以最大不能超过最大传输量MSS,或者说长度是固定的。

  4. sk_buff 的结构设计是为了方便数据的跨层传递。

  5. skb 通过 alloc_skb 和 skb_reserve 申请和释放,因此 skb 是有个池的概念的及“线性的数据缓冲区”。

960f3c98da5ad1fb1789395b162456c0.png

1.1.3 总结(重要,关系到零拷贝的理解)
  • 只在两种情况下创建 sk_buff:

  1. 应用程序给 socket 写入数据时。

  2. 当数据包到达 NIC 时。

  • 数据只会拷贝两次:

  1. 用户空间与内核空间之间的拷贝(socket 的 read、write)。

  2. sk_buff 与 NIC 之间的拷贝。

1.1.4 误区

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

  • TCP 的 socket 中包含发送缓冲区和接收缓冲区。

  • UDP 的 socket 中只有一个接收缓冲区,没有发送缓冲区。

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

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

https://man7.org/linux/man-pages/man7/udp.7.html

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 和协议栈之间。

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

  1. 可以平滑生产者和消费者的速度。

  2. 通过 NAPI 的机制,合并以减少 IRQ 次数。

3b09be394b4c8dd1ac238ac7beda41e0.png

图-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 结构的总结

  1. 网络 I/O 中,在内核空间只有一个地方存放数据,那就是 Socket Buffer。

  2. Socket Buffer 就是 sk_buff 链表,只有在 Socket 写入或者数据到达 NIC 时创建。

  3. sk_buff 是一个线性的数据缓冲区,是通过 alloc_skb 和 skb_reserve 申请和释放的。

  4. 每个sk_buff 是固定大小的,这与 MTU 有关。

  5. 数据只有两次拷贝:用户空间与 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 实现和网卡的通讯。

d61c8093b8246e3f2781eb86364b510f.png

图-9
3.1.1 DPDK 缺点

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

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

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

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

4.1 read + write

86697ae52beee14f84fba9c95cadbe6d.png

2d9f2b888774e27d52c6971dfdb57eb6.png

图-10

网上的总结

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

解读

  1. read 和 write,两次系统调用,每次从用户态切换到内核态,再从内核态切换回用户态,所以4次上下文切换。

  2. 数据由 Page Cache 拷贝到用户空间,再由用户空间拷贝到 socket buffer,2次 CPU 拷贝。

  3. 现在的磁盘和网卡都是支持 DMA 的,所以从磁盘到内存,从网卡到内存的数据都是 DMA 拷贝。

4.2 mmap + write

234ecb06468d27d0a786b78e8d08e992.png

38d1328836dc7f2c6190d2429f72407f.png

图-11

网上的总结

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

解读

  1. mmap 和 write,两次系统调用,4次上下文切换,没问题。

  2. MMU 支持下,数据从虚拟地址到 socket buffer 的拷贝,实际是PageCache 到 socket buffer 的拷贝,所以1次 CPU 拷贝,也没问题。

  3. mmap 读取过程中,会触发多次缺页异常,造成上下文切换,所以越大的文件性能越差。

  4. 不存在浪费内存,mmap 本质是 Buffer I/O,本来也是 page 对齐的。

补充

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

4.3 sendfile

4e69e392403d68d46d2291a770d6b858.png

2911d1e20ca995963e440344666f8e67.png

图-12
网上的总结
  1. 2次上下文切换,1次CPU拷贝。

  2. 针对大文件性能高,针对小文件需要内存对齐,所以浪费内存。

解读

  1. sendfile,一次系统调用,2次上下文切换。

  2. 数据从 Page Cache 拷贝到 socket buffer,所以1次 CPU 拷贝。

  3. sendfile才是更适合处理大文件,所有工作都是内核来完成的,效率一定高。

补充

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

4.3.1 sendfile,splice,tee 区别

1abc3cde39e723a3e153d7e7fc14243b.png

  • 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 拷贝的技术。

b5c2daeebb099f4200a8a4d3463f979c.png

图-13

网上的总结

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

解读

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

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

fabc2eb126f8c747ab72a848373d47d8.png

图-14

End

5308f4263a82a0b4c55fd913d3f8d8d7.png

f3e3edad7f05f08a75e7877985cda365.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值