NIC数据包的接收与发送
一、NIC基本知识
1.基本概念
网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。由于其拥有 MAC 地址,因此属于 OSI 模型的第 1 层和 2 层之间。
网卡上面装有处理器和存储器(包括 RAM 和 ROM)。网卡和局域网之间的通信:
- 有线连接时,通过电缆或双绞线以串行传输方式进行;
- 无线连接时,通过无线信号(如 Wi-Fi 信号等电磁波)进行。
而网卡和计算机之间的通信则通过计算机主板上的 I/O 总线以并行传输方式进行。因此,网卡的一个重要功能是进行串行 / 并行转换。由于网络与计算机总线上的数据率不同,网卡中需装有对数据缓存的存储芯片。
2.NIC主要功能
数据的封装与解封、链路管理、数据编码与译码。
数据的封装与解封
- 发送数据时,网卡将计算机要传输的数据封装成符合网络协议(如以太网协议)的帧格式,添加头部(如 MAC 地址、协议类型等)和尾部(校验信息等),使数据能在网络中正确传输。
- 接收数据时,网卡解析接收到的帧,拆除封装的头部和尾部,提取有效数据,交给计算机上层处理。
链路管理
- 负责控制网卡与网络链路的连接状态,例如监测链路是否连通、协商通信参数(如双工模式)。
- 处理链路层的错误检测与恢复,如检测到传输错误时,通过重发机制确保数据可靠传输。
数据编码与译码
- 发送数据时,将计算机的数字信号转换为适合传输介质的信号形式(如电信号、光信号),完成编码(如曼彻斯特编码)。
- 接收数据时,将传输介质中的信号还原为计算机能识别的数字信号,实现译码。
3.网卡驱动
END设备(以太网网络设备)驱动程序的装载、启动END设备、网络数据包的接收以及网络数据包的发送。
END 设备驱动程序的装载
- 操作系统启动或网卡接入时,加载对应的网卡驱动程序。驱动程序包含网卡硬件的控制指令和通信协议,是操作系统与网卡交互的 “桥梁”。
启动 END 设备
- 驱动程序初始化网卡硬件,配置寄存器参数(如中断号、I/O 地址),激活网卡使其进入工作状态,准备接收和发送数据。
网络数据包的接收与发送
- 接收:网卡捕获网络中的数据包后,驱动程序将数据从网卡缓存读取,按协议解析后交给操作系统网络层。
- 发送:操作系统将待发送的数据交给驱动程序,驱动程序控制网卡完成数据封装、编码,通过物理介质发送到网络。
4.NIC分类
根据网卡所支持的物理层标准与主机接口的不同,网卡可以分为不同的类型,如以太网卡和令牌环网卡等。根据网卡与主板上总线的连接方式、网卡的传输速率和网卡与传输介质连接的接口的不同,网卡分为不同的类型。
按物理层标准与主机接口划分
- 以太网卡:遵循以太网标准(如 IEEE 802.3),是最常见的网卡,支持双绞线、光纤等介质,广泛用于局域网。
- 令牌环网卡:基于令牌环网络标准(IEEE 802.5),通过令牌传递控制数据传输,曾用于企业网络,现逐渐被以太网取代。
按总线连接方式、传输速率、接口划分
- 总线连接方式:如 PCI 接口网卡(连接到计算机 PCI 总线)、PCIe 接口网卡(更高速的 PCI Express 总线,主流现代网卡接口)。
- 传输速率:分为 10M 网卡、100M 网卡、1000M(千兆)网卡、万兆网卡等,满足不同网络带宽需求。
- 接口类型:如 RJ45 接口(用于双绞线)、光纤接口(SC/LC 等,用于光纤传输)、无线网卡的无线信号接口(如 Wi-Fi)。
二、数据包的发送与接收
1.数据包发送
一、socket 层
1. application
应用层是整个网络通信流程的发起者。在实际应用中,像浏览器、即时通讯软件这类应用程序,当用户有发送数据的需求时,就会触发数据发送操作。例如,在浏览器中输入网址并回车后,浏览器需要向服务器发送 HTTP 请求,这个操作就是由应用层发起的。应用层通过调用底层提供的网络接口函数来实现数据的传输,这些接口函数将应用层的数据传递给内核网络子系统进行后续处理。
2. socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
- 功能:此函数的核心作用是创建一个 UDP 类型的套接字。套接字可以理解为应用程序与网络之间的通信端点,它为应用程序提供了一种与网络进行交互的方式。
- 参数:
AF_INET
:明确指定使用 IPv4 协议族。在互联网中,IPv4 是目前广泛使用的网络协议版本,它为网络中的设备分配唯一的 IP 地址。SOCK_DGRAM
:指定套接字的类型为数据报套接字,也就是使用 UDP 协议进行通信。UDP 是一种无连接的传输协议,它不保证数据的可靠传输,但具有传输速度快、开销小的特点。IPPROTO_UDP
:进一步明确使用 UDP 协议。这个参数可以确保内核正确配置套接字以使用 UDP 协议进行数据传输。- 作用:为应用层分配一个网络通信的套接字资源。当调用这个函数成功后,内核会返回一个唯一的套接字描述符,应用程序可以使用这个描述符来进行后续的发送和接收操作。
3. sendto(....)
- 功能:这是应用层调用的用于发送数据的接口函数。在调用这个函数时,应用程序需要指定目标地址(包括目标 IP 地址和端口号)、要发送的数据以及数据的长度等信息。
- 作用:它的主要作用是将应用层产生的数据传递给内核网络子系统。当应用程序调用
sendto
函数后,内核会接管这些数据,并根据目标地址等信息进行后续的处理,从而触发内核协议栈的一系列操作。例如,在一个简单的 UDP 聊天程序中,用户输入的聊天消息会通过sendto
函数发送给目标用户。4. inet_sendmsg(....)
- 功能:作为处理 UDP 发送消息的核心函数,它会对
sendto
函数传递过来的发送参数进行解析。例如,它会解析目标地址、端口号、数据长度等信息,并根据这些信息组织相应的数据结构。- 作用:主要负责接收和解析应用层传递的参数,如目标地址、端口、发送数据等。它会进行一些通用的处理,比如检查套接字状态、自动绑定本地地址和端口(调用
inet_autobind
)等。此外,它还会根据套接字类型(如 UDP 或 TCP)来选择合适的协议处理函数,为后续的协议层处理做好准备。5. inet_autobind(..)
- 功能:该函数的主要功能是自动绑定本地未分配的 IP 地址和端口。在某些情况下,应用程序可能没有显式地绑定本地地址和端口,这时内核就会通过
inet_autobind
函数来分配可用的本地地址信息。- 场景:例如,在一个简单的 UDP 客户端程序中,如果开发者没有手动为客户端指定本地地址和端口,那么当程序调用
sendto
函数时,内核会调用inet_autobind
函数为客户端自动分配一个可用的本地 IP 地址和端口,以确保 UDP 通信能够正常发起。二、UDP 层
1. udp_sendmsg
- 核心处理:
- 调用 网络层的ip_route_output_flow 获取路由信息:
udp_sendmsg
函数会调用ip_route_output_flow
函数,传递目标 IP 地址等信息。ip_route_output_flow
函数会根据这些信息查找路由表,确定数据发送的出接口(即网卡)以及下一跳的地址等路由信息。例如,如果目标 IP 地址是另一个子网的地址,ip_route_output_flow
函数会找到合适的网关作为下一跳地址。- 调用 ip_make_skb 构造 skb:获取到路由信息后,
udp_sendmsg
函数会调用ip_make_skb
函数。ip_make_skb
函数会分配并初始化一个套接字缓冲区(skb),然后将 IP 头、UDP 头以及应用层的数据填充到这个缓冲区中(初步填充ip头信息,如版本号、首部长度、承载的上层协议、源ip和目的ip等)。skb 是内核协议栈处理数据的基础单元,它包含了数据包的所有信息。- 将网卡信息与 skb 提交:最后,
udp_sendmsg
函数会将获取到的网卡信息和构造好的 skb 提交给后续的处理流程,为 IP 层的处理做好准备。2. ip_route_output_flow
- 功能:根据目标 IP 地址查找路由表,确定数据发送的出接口(网卡)、下一跳等路由信息。路由表是内核维护的一个数据结构,它记录了不同网络地址对应的出接口和下一跳地址等信息。
- 作用:解决了 “数据从哪个网卡发出” 和 “发送路径” 的问题。例如,在一个多网卡的服务器上,不同的网卡可能连接到不同的网络,
ip_route_output_flow
函数会根据目标 IP 地址选择最合适的网卡来发送数据。3. ip_make_skb
- 功能:分配并初始化 skb 缓冲区,填充 IP 头、UDP 头及应用数据。在分配 skb 缓冲区时,内核会为其分配足够的内存空间,然后根据路由信息和应用层数据填充 IP 头和 UDP 头。IP 头包含了源 IP 地址、目标 IP 地址、协议类型等信息,UDP 头包含了源端口号、目标端口号、数据长度等信息。
- 意义:skb 是内核协议栈处理数据的基础单元,它将不同层次的协议信息和应用层数据封装在一起,方便内核进行统一的处理和传输。
4. udp_send_skb
- 功能:将构造好的 skb 传递给 IP 层,触发 IP 层的发送流程。当
udp_send_skb
函数接收到ip_make_skb
函数构造好的 skb 后,会将其传递给 IP 层的ip_send_skb
函数,从而启动 IP 层的处理流程。- 衔接作用:完成 UDP 层的处理后,
udp_send_skb
函数将数据无缝地移交至 IP 层继续处理,确保数据能够在不同的协议层之间顺利传输。三、IP 层
1. ip_send_skb
- 定位:IP 层发送数据包的入口函数。当 UDP 层的
udp_send_skb
函数将 skb 传递过来后,ip_send_skb
函数会被调用。- 作用:它的主要作用是简单调用后续的处理函数,作为 IP 层处理的起点。它会将 skb 传递给
__ip_local_out_sk
函数进行进一步的处理。2. __ip_local_out_sk
- 功能:处理本地生成的数据包输出(区别于转发包),为数据包添加 IP 头信息(完善ip头部信息,如生存时间、首部校验和标志和片偏移等)。在这个函数中,内核会根据路由信息和数据包的内容,生成正确的 IP 头,包括源 IP 地址、目标 IP 地址、协议类型、TTL(生存时间)等信息。
3. NF_INET_LOCAL_OUT
- 定位:Netfilter 框架的钩子点。Netfilter 是 Linux 内核中的一个强大的网络过滤和数据包处理框架,
NF_INET_LOCAL_OUT
是其中的一个钩子点,用于处理本地输出的数据包。- 作用:允许防火墙等安全模块在此处对本地输出的数据包进行过滤、修改等操作。例如,防火墙可以根据预设的规则,对某些 IP 地址或端口的数据包进行拦截或修改,从而提高网络的安全性。
4. dst_output_sk
- 功能:根据路由信息(dst 结构)选择输出函数,决定数据包后续的处理路径。dst 结构是内核中用于存储路由信息的数据结构,
dst_output_sk
函数会根据这个结构中的信息,选择合适的输出函数,如ip_output
函数,来继续处理数据包。5. ip_finish_output 与 ip_finish_output2
- 功能:这两个函数主要完成 IP 层数据包的封装工作。它们会调用
NF_INET_POST_ROUTING
钩子(供防火墙等模块处理),对数据包进行最后的检查和修改。最终,将处理好的数据包移交至网络设备子系统进行发送。6. ip_output 与 neigh.resolve.output
- ip_output:执行 IP 层数据包的发送操作,调用路由相关的处理函数。在这个函数中,会根据路由信息对数据包进行进一步的处理,如分片等操作。
- neigh.resolve.output:解析邻居(MAC 地址),将 IP 地址映射为链路层地址,为数据链路层的发送做准备。在以太网中,数据的传输需要知道目标设备的 MAC 地址,
neigh.resolve.output
函数会通过 ARP(地址解析协议)等机制,将目标 IP 地址解析为对应的 MAC 地址。7. dev_queue_xmit
- 定位:IP 层的出口,连接到链路层的接口。当 IP 层的处理完成后,会将数据包传递给
dev_queue_xmit
函数。- 作用:将数据包提交给网络设备队列,触发后续的网卡发送流程。它会将数据包放入网络设备的发送队列中,等待网卡进行发送。
四、netdevice 子系统
1. dev_queue_xmit
- 处理逻辑:
- 首先检查设备发送队列(txq)。如果该队列不存在,说明网络设备没有配置发送队列,此时会直接调用
dev_hard_start_xmit
函数启动网卡的发送流程。- 如果发送队列存在,会将数据包送入
traffic control
模块进行流量控制和优先级处理。2. traffic control
- 功能:对数据包进行过滤、流量整形、优先级调度(如 QoS 策略)等操作。通过这些操作,可以确保网络带宽的合理使用,提高网络的性能和可靠性。例如,对于实时性要求较高的视频流数据,可以给予较高的优先级,确保其能够及时传输。
3. dev_hard_start_xmit
- 功能:启动网卡硬件的发送流程,将数据包从内核缓冲区推向网卡硬件。在这个过程中,内核会将 skb 中的数据复制到网卡的发送缓冲区中,然后通知网卡开始发送数据。
- 触发:调用后触发
NET_TX_SOFTIRQ
软中断,异步处理发送完成后的清理工作。软中断是内核中一种轻量级的中断处理机制,用于处理一些可以在中断上下文之外进行的操作。4. net_tx_action
- 定位:软中断处理函数,处理网卡发送完成后的回调操作。当网卡发送完成后,会触发
NET_TX_SOFTIRQ
软中断,调用net_tx_action
函数。- 作用:在这个函数中,会进行一些清理工作,如释放 skb 资源、更新统计信息等。例如,记录发送的数据包数量、字节数等信息,以便进行网络性能的监控和分析。
5. ndo_start_xmit
- 本质:网卡驱动提供的发送函数指针。不同的网卡有不同的驱动程序,
ndo_start_xmit
指向了具体网卡驱动中的发送函数。- 流程:
- 将 skb 存入网卡发送队列:将内核传递过来的 skb 存入网卡的发送队列中,等待网卡进行发送。
- 通知网卡发送数据:调用网卡的硬件接口,通知网卡开始发送队列中的数据。
- 网卡发送完成后通过中断通知 CPU:当网卡发送完成后,会向 CPU 发送一个中断信号,告知 CPU 发送操作已经完成。
- 处理中断,清理 skb 资源,完成整个发送流程:CPU 接收到中断信号后,会调用相应的中断处理函数,清理 skb 资源,释放内存,完成整个数据发送流程。
函数之间的关系
1. socket 层函数关系
应用层调用
socket
函数创建套接字,得到套接字描述符后,使用该描述符调用sendto
函数。sendto
函数将应用层数据、目标地址等信息传递给内核,内核调用inet_sendmsg
函数进行处理。inet_sendmsg
函数在处理过程中,如果发现应用程序没有显式绑定本地地址和端口,会调用inet_autobind
函数进行自动绑定。绑定完成后,inet_sendmsg
函数将处理后的数据传递给 UDP 层的udp_sendmsg
函数。2. UDP 层函数关系
udp_sendmsg
函数首先调用ip_route_output_flow
函数,传递目标 IP 地址等信息,获取路由信息。然后根据路由信息调用ip_make_skb
函数,传递路由信息和应用层数据,构造 skb 缓冲区。构造完成后,udp_sendmsg
函数将 skb 传递给udp_send_skb
函数,udp_send_skb
函数再将 skb 传递给 IP 层的ip_send_skb
函数。3. IP 层函数关系
ip_send_skb
函数接收 UDP 层传递过来的 skb 后,调用__ip_local_out_sk
函数为数据包添加 IP 头信息。添加完成后,数据包经过NF_INET_LOCAL_OUT
钩子点,供防火墙等安全模块进行处理。处理后的数据包传递给dst_output_sk
函数,dst_output_sk
函数根据路由信息选择输出函数,如ip_output
函数。ip_output
函数执行 IP 层数据包的发送操作,调用neigh.resolve.output
函数解析邻居 MAC 地址。最后,数据包经过ip_finish_output
和ip_finish_output2
函数的封装和处理后,传递给dev_queue_xmit
函数。4. netdevice 子系统函数关系
dev_queue_xmit
函数接收 IP 层传递过来的数据包后,检查设备发送队列。如果队列不存在,直接调用dev_hard_start_xmit
函数;如果队列存在,将数据包送入traffic control
模块进行处理。处理后的数据包传递给dev_hard_start_xmit
函数,该函数启动网卡发送流程,并触发NET_TX_SOFTIRQ
软中断。软中断调用net_tx_action
函数进行发送完成后的清理工作。net_tx_action
函数最终调用网卡驱动的ndo_start_xmit
函数,完成数据包的实际发送和清理操作。
2.数据包接收
数据包如何从网卡到应用程序(进程)。网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册金网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
一、NIC 到内存阶段
1. 数据包接收与 DMA 传输
- packet → NIC → Memory
- 过程:网络接口卡(NIC)从物理网络接收数据包(packet),通过 DMA(直接内存访问) 技术,无需 CPU 参与,直接将数据包传输到内存(Memory),存储为 packet1、packet2 等。DMA 技术绕过 CPU 直接与内存交互,显著提升数据搬运效率,减少 CPU 用于数据拷贝的开销,使其能够专注于更复杂的任务。
- 意义:实现硬件层数据的快速采集,为后续软件处理提供原始数据,是数据包接收流程的物理基础。
- NIC 触发硬件中断
- Raise IRQ:NIC 完成数据包接收后,通过硬件中断(Raise IRQ)通知 CPU 数据到达。
- Disable IRQ:为避免频繁中断导致的性能损耗,NIC 在触发中断后会暂时关闭中断,采用批量处理模式,累积一定数量的数据包后再统一处理,从而减少中断次数,提升系统整体效率。
2. CPU 与 NIC 驱动交互
- CPU 响应中断
- 中断处理:CPU 接收到中断信号后,根据中断向量表调用已注册的中断处理函数。该函数会触发 NIC 驱动(NIC Driver),执行硬件相关操作,如读取网卡接收缓冲区状态、清理已处理的数据包空间等。
- 驱动角色:作为硬件与内核的桥梁,NIC 驱动负责将硬件层面的信号(如数据包到达、错误状态等)转换为软件可识别的数据包结构,为内核网络模块提供处理基础。
- 触发软中断(Raise soft IRQ)
- 软中断触发:NIC 驱动完成硬件操作后,会主动触发软中断(soft IRQ)。软中断是内核设计的一种轻量级中断机制,用于将耗时较长的操作(如数据包解析、协议处理)放到中断上下文之外执行,避免阻塞硬件中断处理,确保系统响应的实时性。
二、内核网络模块处理
1. 软中断与数据读取
- ksoftirqd 处理软中断
- 软中断守护进程:内核中专门处理软中断的守护进程 ksoftirqd 响应软中断事件。它会调用 net_rx_action 函数,启动内核网络模块对数据包的处理流程。
- net_rx_action 函数:作为软中断处理的核心函数,net_rx_action 遍历系统中所有注册的网络接收队列,触发 napi_gro_receive 函数,实现数据包的批量接收与处理。通过批量处理而非逐个处理数据包,显著减少内核函数调用开销,提升数据处理效率。
2. NAPI 机制与数据包接收
- NAPI(New API)机制
- 设计思想:融合中断驱动和轮询驱动的优点。当有数据包到达时,通过中断唤醒内核处理;后续采用轮询方式批量处理已接收的数据包,避免频繁中断带来的性能损耗,同时保证数据包处理的实时性。
- napi_gro_receive 函数
- 功能:
- 批量接收数据包:从内存中批量读取 NIC 接收的数据包(如 packet1、packet2 等),减少函数调用次数,降低 CPU 开销。
- GRO(Generic Receive Offload)处理:执行通用接收卸载,将同一数据流的多个小数据包合并为一个较大的数据包(例如合并同一 IP 数据包的分片),减少协议栈处理次数,进一步提升处理效率。
3. 数据包入队与分类处理
- enqueue to backlog
- 操作:将处理后的数据包存入 CPU 的输入队列(input_pkt_queue)。这一设计在多 CPU 环境下实现了数据包的负载均衡,每个 CPU 仅处理自己队列中的数据包,避免了多个 CPU 同时处理同一数据包导致的竞争冲突。
- 意义:确保数据包以有序、高效的方式传递给后续处理流程,同时平衡系统资源利用,提升整体吞吐量。
- __netif_receive_skb_core 函数
- 协议类型标记:分析数据包的协议类型(如
AF_PACKET
表示链路层数据包),为后续将数据包传递给正确的协议栈(如 IP 层、ARP 层)做准备。- 数据包分发:根据协议类型,将数据包传递给对应的协议层处理函数。例如,对于 IP 数据包,触发 IP 层的入口函数 ip_rcv,启动 IP 层的处理流程。
4. 内核网络模块的整体协作
内核网络模块通过以下协作逻辑,将网卡驱动接收到的数据包高效传递给协议栈:
- 软中断触发:NIC 驱动触发软中断后,ksoftirqd 守护进程调用 net_rx_action。
- NAPI 批量处理:net_rx_action 利用 NAPI 机制,通过 napi_gro_receive 批量接收并处理数据包,合并分片(GRO 处理)。
- 数据包入队与分类:处理后的数据包存入 CPU 输入队列(enqueue to backlog),再经 __netif_receive_skb_core 标记协议类型,最终传递给协议栈(如 IP 层)进行后续解析与传输控制。
通过这一系列处理,内核网络模块实现了从硬件驱动到协议栈的高效数据传递,是网络数据包接收流程中的关键枢纽。
三、协议栈(IP 层)处理
1. 数据包初步检查与路由
- ip_rcv 入口处理
- 入口函数:IP 层的入口函数 ip_rcv 对数据包进行初步检查,包括 IP 头校验和验证、分片检查等。若数据包是分片,ip_rcv 会调用 ip_defragment 函数进行分片组装。
- 钩子调用:调用 NF_INET_PRE_ROUTING 钩子,允许防火墙等安全模块在路由前对数据包进行过滤或修改。
- 路由决策
- routing:根据数据包的目标 IP 地址查找路由表,判断数据包是本地交付还是需要转发。
- 本地交付:调用 ip_local_deliver,将数据包交给 UDP 层或其他上层协议。
- 转发:调用 ip_forward,经 NF_INET_FORWARD 钩子处理后,通过 dst_output_skb 选择合适的输出路径进行转发。
2. IP 分片组装(关键补充)
在 IP 层接收流程中,ip_defragment 函数负责将分片的 IP 数据包重新组装为完整的数据包。具体流程如下:
- 分片检测:ip_rcv 检查数据包的分片标志(如 “更多分片” 标志)和片偏移,判断是否为分片数据包。
- 分片重组:若为分片,ip_rcv 调用 ip_defragment,根据数据包的标识(Identification)、源 IP、目标 IP 等信息,将属于同一原始数据包的分片缓存并组装。
- 组装逻辑:ip_defragment 维护一个分片重组队列,等待所有分片到达后,按片偏移顺序合并数据,恢复原始数据包。若分片超时未到达,则丢弃已接收的分片。
3. 本地交付处理
- ip_local_deliver → NF_INET_LOCAL_IN
- ip_local_deliver:将组装完整的数据包交付给上层协议(如 UDP 层),在此之前调用 NF_INET_LOCAL_IN 钩子,允许本地防火墙对数据包进行最后的检查或修改。
四、UDP 层处理
1. 套接字查找与数据存储
- udp_rcv 核心处理
- 接收入口:UDP 层的入口函数 udp_rcv 调用 _udp4_lib_lookup_skb,根据数据包的源 / 目标 IP 地址和端口号查找匹配的 UDP 套接字。
- 数据包入队
- sock_queue_rcv_skb:将查找到的数据包存入对应套接字的接收队列(调用 __skb_queue_tail)。若套接字设置了过滤器(sk_filter),需先对数据包进行过滤。
- 通知应用层数据就绪
- sk_data_ready:标记套接字数据就绪,唤醒通过 recvfrom 或 epoll/select 监听该套接字的应用层进程,使其能够读取数据。
五、socket 层(应用层接收)
- 应用层获取数据
- recvfrom 方式:应用层调用 recvfrom 函数,阻塞等待并读取套接字接收队列中的数据包。
- epoll/select 方式:应用层通过 epoll/select 监听套接字,当 sk_data_ready 触发数据就绪事件后,调用 recvfrom 非阻塞地读取数据包,实现异步数据接收。