今天的讨论主要围绕操作系统中的网络功能及其与网卡驱动的关系来展开讲述操作系统是怎么和网络交互的。
网络分为局域网(LAN)和长距离网络(互联网),局域网连接多个主机,通常会通过交换机、以太网或Wi-Fi设备进行通信。每个主机上的应用程序通过网络协议栈进行数据传输。
局域网的规模有限,通常支持几十到几百个主机,而为了连接更大范围的网络,需要通过路由器将多个局域网连接起来。
路由器通过“路由”功能在多个网络之间转发数据包。
互联网通信使用Internet Protocol(IP),而局域网内的通信则通过以太网协议。
一、重要协议
(一) 以太网协议
在局域网(LAN)中,两个主机之间进行通信时,它们通过以太网协议交换数据。这个数据的基本单元叫做数据帧(Frame) 。我们来看这是来自Mit的教学操作系统xv6(以下简称xv6)的关于以太网的代码:
// an Ethernet packet header, from net lab's
// kernel/net.h
#define ETHADDR_LEN 6
struct eth {
uint8 dhost[ETHADDR_LEN];
uint8 shost[ETHADDR_LEN];
uint16 type;
} __attribute__((packed));
#define ETHERTYPE_IP 0x0800 // Internet protocol
#define ETHERTYPE_ARP 0x0806 // Address resolution protocol
数据包结构 以太网数据包由以下部分组成:
- 目的地址:接收方的网卡地址(48位)。
- 源地址:发送方的网卡地址(48位)。
- 类型字段:告知接收方如何处理数据(如0x0800表示IP协议,0x0806表示ARP协议)。
- 数据负载:实际传输的数据内容。
以太网地址(MAC地址)
- 48位地址,前24位由网卡制造商分配,用于标识制造商,后24位由制造商分配给每个网卡,用于唯一标识。
- 只在局域网内唯一,跨局域网通信时依赖IP地址。数据包通过物理链路传输,硬件通过电信号或光信号标识开头和结尾。
数据传输
- 以太网数据包通过物理链路(如网线、Wi-Fi)传输。
- 数据包的开始和结束位置通过硬件标记(如Preamble和FCS)进行识别,操作系统内核不可见。
传输协议
- 在同一局域网内,接收方根据目的地址处理数据包。
- 跨局域网通信时,IP地址用于找到远程主机。
TCP dump工具可以用于分析网络数据包,会显示如下信息如:
- 数据包时间戳
- 包内容(十六进制)
- 源地址、目的地址
- 协议类型(如ARP、IP) 这里对于该工具仅仅提一句,不作额外展示。
(二) ARP协议
IP 地址的高位部分用于标识网络,而低位部分则标识局域网中的具体主机。IP地址可以帮助我们在全球范围内定位目标主机的位置,而以太网地址只是局域网内识别设备的方式。
当数据包通过互联网传输并最终到达某个局域网时,我们需要把32位的IP地址转换成对应的48位以太网地址(MAC地址) 。
如:当一个主机知道目标主机的IP地址时,它可以通过ARP请求找到对应的MAC地址。当数据包的类型字段是0x0806,表示这个数据包使用的是ARP协议。
ARP工作流程
- 假设主机A想要给主机B发送数据包,但它只有主机B的IP地址,而不知道主机B的以太网地址。
- 主机A会广播一个ARP请求,询问网络中的所有主机:“谁拥有这个IP地址?如果你有,请告诉我你的以太网地址(MAC地址)”。
- 如果主机B(也就可能是路由器)存在并且在线,它会收到ARP请求,并回复主机A一个ARP响应,告诉它自己的MAC地址。
- 主机A就可以利用主机B的MAC地址把数据包正确地发送过去。
ARP包的结构
ARP数据包嵌套在以太网数据包的负载部分,包含以下信息:
- 以太网Header:目的MAC地址、源MAC地址和类型字段(0x0806表示ARP)。
- ARP包:包含发送方和接收方的IP地址与MAC地址。
如下给出xv6中ARP的数据结构:
// an ARP packet (comes after an Ethernet header).
struct arp {
uint16 hrd; // format of hardware address
uint16 pro; // format of protocol address
uint8 hln; // length of hardware address
uint8 pln; // length of protocol address
uint16 op; // operation
char sha[ETHADDR_LEN]; // sender hardware address
uint32 sip; // sender IP address
char tha[ETHADDR_LEN]; // target hardware address
uint32 tip; // target IP address
} __attribute__((packed));
#define ARP_HRD_ETHER 1 // Ethernet
enum {
ARP_OP_REQUEST = 1, // requests hw addr given protocol addr
ARP_OP_REPLY = 2, // replies a hw addr given protocol addr
};
以太网包的类型字段(16位)会告诉接收方,数据包的负载部分是ARP包(类型值为0x0806)。ARP包本身包含了:
- 发送方的IP地址(SIP)
- 目的方的IP地址(DIP)
- 发送方的MAC地址(SHA)
- 目的方的MAC地址(THA,可能是全0,表示还不知道)
我们其实可以看到以太网Header和ARP包中重复包含发送方的MAC地址,那么以太网Header中已经包含了发送方的MAC地址,为什么ARP包里还要重复发送这些信息呢?
ARP协议最初设计时是独立于以太网的,它可能会在其他类型的网络上使用。因此,ARP包里包含了自己的MAC地址是为了让它能在不同的网络环境中都能正常工作。如果它只依赖以太网地址,可能会限制其适用性。
(三) IP协议
IP协议与Ethernet协议
- Ethernet协议:用于局域网内的通信,通过MAC地址识别目标主机。
- IP协议:用于跨网络(如互联网)发送数据包,通过IP地址和路由器转发实现远程通信。
IP数据包结构
- Ethernet header:目标MAC地址、源MAC地址、类型字段(0x0800表示IP包)。
- IP header:包含源IP地址、目标IP地址、协议类型等信息。
- IP payload:实际传输的数据部分。
IP协议功能
- 目的IP地址:告诉路由器数据包的目标主机。
- 源IP地址:告诉路由器数据包的来源主机。
- 协议字段:指定数据部分使用的协议类型(如UDP、TCP)。
(四) UDP协议
在网络中,每个主机上可能运行多个不同的应用程序,而每个应用程序通常都需要使用网络进行通信。所以为了让数据包能够传递到正确的应用程序,除了使用IP地址外,我们还需要端口来标识不同的应用程序。
- IP地址帮助我们把数据包送到正确的主机,但它不能告诉我们数据包应该传递给该主机上的哪个应用程序。
- 这个问题通过端口来解决。UDP/TCP 协议就是通过源端口(sport)和目的端口(dport)来区分不同的应用程序。
// a UDP packet header (comes after an IP header).
struct udp {
uint16 sport; // source port
uint16 dport; // destination port
uint16 ulen; // length, including udp header, not including IP header
uint16 sum; // checksum
};
数据传输的时候,如果你知道目标主机的IP地址,但不清楚其MAC地址,发送数据包时,主机会检查目标IP地址,判断目标主机是否在同一局域网内:
- 如果是,它会使用ARP协议(地址解析协议)将IP地址解析成MAC地址,再通过Ethernet发送数据。
- 如果目标主机不在同一局域网,数据包会先发送到路由器,由路由器通过自己的路由表转发到下一个网络。
值得注意的是,每个网络技术都有一个最大传输数据包的大小。例如,传统的以太网最大支持1500字节的包,而一些现代的以太网支持到9000或10000字节。为什么不支持更大的数据包呢?
- 传输时间:发送非常大的数据包需要更多的时间,而网络中存在噪音和干扰,传输过程中可能出现数据错误。如果包太大,错误的检测和修复就变得困难。
- 路由器和主机的缓冲区:传输大数据包需要路由器和主机有足够大的内存来接收和处理数据包。管理一个可变长度的缓冲区比管理一个固定大小的缓冲区要复杂得多,因此网络协议通常规定了数据包的最大大小。
在计算机网络中,当一个数据包(packet)通过网络从源主机到达目标主机时,这个过程是通过网络协议栈来管理的。每一层网络协议栈都有特定的功能,负责处理不同类型的数据,直到数据最终被应用程序使用。下面是如何在主机上通过网络协议栈处理数据的简要说明。
二、数据包的传输过程
网卡是计算机中专门接受和发送网络包的硬件。
(一) 接收数据
接收数据包
当一个数据包从网络传来时,网卡会从网络中接收到。
当网卡收到了一个packet,它会生成一个中断。(因为网卡存储 packet 的内存非常小,所以需要尽快拷贝到计算机的内存中。)系统内核中处理中断的程序会被触发,并从网卡中获取packet。
网卡驱动负责拷贝网卡内存数据到主机内存,但现在基本不存在了,这是因为外设到 CPU 的距离非常长,交互时间过长。
现在网卡接收到 packet ,会不经过 CPU 直接通过 DMA 技术将网络包 copy 到写到指定内存地址,等待驱动来读取。
网卡驱动会将它接收并传递给协议栈。首先,IP层会检查数据包的IP头部,然后将剩余的部分传递到UDP层或TCP层。
逐层解析
UDP/TCP层会进一步解析数据包,去掉UDP/TCP头部,然后将数据传递给socket层, 它负责维护socket与端口号的映射。
在socket层,数据包被存入一个应用层的缓冲区队列中,唤醒用户线程,等待应用程序通过socket读取。
(二) 发送数据包
当应用程序需要发送数据时,数据会从应用层开始,逐层向下添加各个协议的头部。
首先应用程序调用 Socket 发送数据包的接口,由于是系统调用这里会从用户态陷入内核态中的 Socket 层,内核会申请一个内核态的 sk_buff 内存,将要发送的数据拷贝到 sk_buff 内存,并将其加入发送缓冲区。
网络协议栈从 Socket 发送缓冲区取出 sk_buff,进行处理。 sk_buff 后续调用网络层,到网卡发送完成会释放掉。
如果使用的是 TCP 传输协议,会拷贝一份 sk_buff 副本发送出去,在获得对方 ACK 前不会删除这个 sk_buff。
为了在层级之间传递数据时,不发生拷贝,只用 sk_buff 一个结构体来描述所有的网络包,我们会通过调整 sk_buff 中 data 指针实现。
- 接收报文,协议栈层层向上传送数据报,通过增加
skb->data指针,逐步剥离协议首部。 - 发送报文,创建 sk_buff 结构体,数据缓存区头部预留足够空间,用来填充各层首部,在经过各下层协议时,通过减少 skb->data 的值增加协议首部。
运输层完成后,传给网络层。在网络层:选取路由,填充 IP 头,对超过 MTU 大小的数据包分片。
网络接口层紧接着根据 ARP 协议获取下一跳 MAC 地址,填充针头针尾。将 sk_buff 放入网卡的发送队列。会触发软中断,通知网卡驱动程序发送数据包。
驱动程序将发送队列中的 sk_buff 指针挂到 RingBuffer ,紧接着将 sk_buff 数据映射到网卡可以访问 DMA 区域。
最终,数据通过网卡驱动发送到物理网络中。
数据发送完成后触发硬中断释放 sk_buff 内存和清理 RingBuffer 内存。
这里涉及到应用层,Socket 层,传输层(UDP/TCP),网络层(IP),链路层(Ethernet)
在网络协议栈的每一层,都会使用一个缓存区(buffer)来临时存储数据包。比如,在socket层,会有一个队列存储接收到的数据包。这个队列通常是一个链表,允许数据包按照顺序等待处理。
额外补充
这里我想优先强调一下:现在的网卡可以直接访问内存。
在这里存在一些并发的组件以不同的方法调度。中断处理程序由网卡的发送或者接受中断触发。
由于网卡内存有限,数据包需要迅速存入计算机的RAM。数据包处理通常在独立线程或进程中进行:
- 中断处理程序:接收数据包并将其放入队列。
- IP处理线程:它是一个内核线程。它不能与中断处理程序同时运行,中断处理程序优先级最高。
它负责读取队列中的数据包,决定处理方式。若数据包发送给当前主机,向上传递至UDP/TCP层;若需要路由转发,将数据包从一个网卡转发至另一个网卡。通常来说,这里的向上传递实际上就是在同一个线程context下的函数调用。
网卡的发送和接收都会触发中断程序。数据包在不同处理层之间通常会缓存到队列中,特别是在网卡和内存之间。
(三) DMA 技术
现代网卡通过直接内存访问(DMA)技术,实现数据包不经过 CPU, 直接从网卡传输到主机内存,减少CPU负担。工作原理如下:
- DMA Ring:网卡通过DMA将数据包存入指定内存地址-环形缓冲区(RX ring)。
每个元素是 packet 指针,指向一个数据包,接收时更新指针。
- 数据接收:网卡通过DMA将数据包传输至主机内存,并通过中断通知驱动,驱动读取缓冲区并交给上层协议处理。
- 数据发送:发送时,数据包存入发送缓冲区(TX ring),网卡通过DMA发送。
驱动还会设置好发送buffer,也就是TX ring。驱动会将需要网卡传输的packet存储在 TX ring中
多网卡与现代网卡:
- 现代网卡支持多个接收缓冲区(RX rings),可以同时接收来自不同网络的数据,并支持硬件加速如TCP校验和计算。
E1000网卡与现代网卡区别:
- E1000:传统网卡,只有一个RX ring和TX ring,性能较好但较为基础。
- 现代网卡:支持多RX ring、TCP硬件加速,能有效分配流量,减轻CPU负担。
Live Lock 出现的原因
我们通过一个路由器的性能图详细讨论了不同接收速率下,路由器的发送速率如何变化,并探讨一下其中的瓶颈和问题。
- 曲线的上升阶段:
- 初期,路由器能够有效处理每个接收到的packet,并将其转发到发送端网卡,接收速率与发送速率保持一致。
- 这个阶段的上升表明,在达到处理瓶颈之前,路由器能够处理更多的packets。直到达到某个接收速率时,系统资源(如CPU)成为限制因素,处理速率无法继续提高。
CPU的算力并不是无限的,CPU最多每秒执行一定数量的指令。对于每个packet,IP软件会查看packet的header,检查校验和,根据目的地址查找转发表等等,这个过程会消耗数百甚至数千条CPU指令时间来处理一个packet。
- 曲线的转折点:
- 当接收速率增加到一定程度,曲线停止上升,且在5000pps左右达到一个瓶颈。这个转折点反映了CPU的计算能力和处理单个packet的时间(例如200微秒)。
- 这时即使系统的接收速率进一步增加,处理能力已经被CPU限制,无法继续提升发送速率。
- 曲线的下降阶段:
- 在接收速率超过5000pps后,发送速率反而开始下降。原因是系统的中断处理负担过重,尤其是在网络中断和CPU调度的影响下,系统进入了所谓的Livelock状态。(因为中断涉及到CPU将一个packet从网卡拷贝到主机的内存中,中断的优先级大于发送和接收 packet)
- 中断频繁触发且优先级较高,导致CPU时间被中断处理占用,转发packet的任务没有足够的CPU时间,导致转发能力下降,最终发送速率趋近于零。
Livelock现象
- Livelock是指系统中的两个任务(如输入中断和packet转发程序)由于调度问题,导致一个任务(如中断处理)占用了所有CPU资源,另一个任务(如转发程序)无法获得资源执行,形成死循环。
- 这不仅是CPU消耗过高导致的,也可能是其他资源(如内存、DMA)被过度占用引发的。比如: 网卡的DMA耗尽了RAM的处理时间,那么网卡占据了RAM导致CPU不能使用RAM。所以,即使你拥有大量的CPU空闲时间,还是有可能触发Livelock。
丢包情况:
- 对于超过处理能力的packet,系统将无法处理并将其丢弃。丢包发生在队列缓存已满时。每个packet的接收会触发中断,如果队列已满,新到的packet就会被丢弃。
- 在高负载时,丢包成为一种常见的现象,尤其是在缓冲区较小的情况下。
如何解决 Live Lock 呢?
这里提出的解决Livelock问题的方案是通过将传统的中断处理机制转变为轮询模式,以避免因过多的中断处理而导致CPU资源完全被占用,进而无法处理数据包的转发任务。
如果网卡中没有等待处理的packet,那么处理线程会重新打开网卡中断,并进入sleep状态。因为最后打开了中断,当下一个packet到达时,中断处理程序不会从网卡拷贝 packet,而是会唤醒处理packet线程,并且关闭网卡的中断。处理 packet 的线程从sleep状态苏醒后,循环的检查从网卡拉取几个 packet,再去处理它。
最开始的时候,处理线程的主循环会询问每个网卡是否在自己的内存中有待处理的packet。如果有的话,主循环会在主机的RAM中申请缓存,再将packet数据从网卡中拷贝到RAM中的缓存,再处理packet。
这里有几个特点要再次详细说明一下:
- 轮询模式(Polling Scheme)
-
- 在高负载情况下,网卡的中断被关闭,处理数据包的线程进入轮询模式。
- 该线程会循环检查网卡的缓冲区,拉取若干个数据包进行处理。如果网卡中没有待处理的数据包,线程会进入休眠状态,等待下一次中断的触发。
- 数据包处理流程:
-
- 轮询线程会检查网卡的缓冲区,若有待处理数据包,会从网卡中读取若干个数据包(论文中提到是最多5个),并将其复制到主机的内存中处理。
- 如果网卡缓冲区为空,轮询线程会重新启用中断,等待新的数据包到达。
- 中断与轮询模式的切换:
-
- 在低负载的情况下,处理线程会继续等待中断,当有新的数据包到达时,中断会触发处理线程的唤醒。
- 在高负载下,关闭中断,线程持续轮询网卡以确保不被中断占用CPU时间。
- 丢包与缓冲区:
-
- 虽然在轮询模式下处理线程会不断读取数据包,但由于输入速率超过了处理能力,多余的数据包会被丢弃。论文中提到的设计并没有特别强调增加网卡内存容量,因为丢包通常发生在高负载下,增加内存并不会显著减少丢包的情况。
- 在设计中,网卡的缓冲区是有限的,当缓冲区满时,任何新到的包都会被丢弃,而这不消耗CPU时间,从而避免了因额外的中断处理而触发Livelock。
- 应用层的瓶颈:
-
- 即便是轮询模式,也可能在其他地方遭遇瓶颈。比如,当数据包需要传递给本地应用程序时,如果应用程序的socket缓冲区已满,网络线程会暂停从网卡读取数据包,直到缓冲区有空余空间。
- 这种瓶颈可能导致Livelock问题,尤其是在处理线程不断尝试读取数据包,但应用程序无法及时处理时,导致处理线程没有足够的CPU时间来进行其他任务。
值得思考的点:
- 多网卡支持: 处理线程可能需要区分不同网卡的状态,决定是否进入中断模式或轮询模式,确保有效的资源分配。
- 内存与队列: 通过将多个数据包从网卡一次性拉取到主机内存,可以有效避免由于单个数据包处理过于频繁而导致的阻塞输出。网卡的内存容量对于短暂的流量过载有帮助,但在持续的过载情况下,增加内存并不显著减缓丢包。
- Livelock的根本原因: Livelock的根本原因是系统在无法处理的数据包上浪费了大量的时间,导致其他任务(如应用程序处理)无法获得足够的CPU时间。
参考
Linux 系统是如何收发网络包的?xiaolincoding.com/network/1_b…
Mogul, Jeffrey, and K. K. Ramakrishnan. "Eliminating Receive Livelock in an Interrupt-Driven Kernel." Proceedings of the USENIX 1996 Annual Technical Conference, USENIX Association, 1996, San Diego, CA.
MIT6.s081 Lec21 NetWorking