全面解析Linux网络I/O:数据是如何“跑”起来的?
数字化时代,网络已深度融入生活与工作,从日常上网、观影到企业数据传输,都离不开网络支持。而 Linux 内核作为操作系统核心,在网络数据处理中扮演关键角色,其核心功能之一便是管理网络数据的输入输出。Linux 内核因开源、高效、稳定,被广泛应用于服务器、嵌入式设备、超级计算机等场景。
全球超 90% 的超级计算机运行 Linux 系统,谷歌、亚马逊等互联网巨头的服务器集群,也依赖其强大网络处理能力,支撑海量数据传输与高并发请求。那么,Linux 内核如何实现网络数据的输入输出?本文将深入其源码,解析网络数据处理的技术原理,探究从接收、协议解析到发送的全过程,展现 Linux 内核在网络领域的精妙设计。
Part1.Linux网络栈架构总览
Linux 网络栈采用分层架构,这种架构设计就如同建造高楼,每一层都有其独特的功能和职责,层层协作,共同构建起强大的网络通信体系。从下往上,Linux 网络栈主要分为链路层、网络层、传输层和应用层 ,各层紧密配合,实现网络数据的高效传输。
图片
1.1应用层:用户的交互界面
应用层是用户与网络应用程序进行交互的界面,就像是各种商店,为用户提供各种具体的网络服务。它负责处理特定的应用程序细节,如 HTTP(Hypertext Transfer Protocol)协议用于网页浏览、SMTP(Simple Mail Transfer Protocol)协议用于邮件发送、FTP 协议用于文件传输等。应用层通过调用传输层提供的接口,实现数据的传输和交互。
例如,当我们在浏览器中输入网址并按下回车键后,浏览器会使用 HTTP 协议向服务器发送请求,服务器接收到请求后,会根据请求内容返回相应的网页数据,浏览器再将这些数据解析并显示出来,供用户浏览。
(1) Socket应用层的各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的。Linux Socket 是从 BSD Socket 发展而来的,它是 Linux 操作系统的重要组成部分之一,它是网络应用程序的基础。从层次上来说,它位于应用层,是操作系统为应用程序员提供的 API,通过它,应用程序可以访问传输层协议。
socket 位于传输层协议之上,屏蔽了不同网络协议之间的差异socket 是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体在Linux系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件的控制一样方便。图片
图片
1.2传输层:数据的可靠保障
传输层是网络通信的可靠保障,如同快递服务中的包裹跟踪系统,确保数据能够准确无误地从发送方传输到接收方。它主要提供端到端的通信服务,常见的传输层协议有 TCP(Transmission Control Protocol)和 UDP(User Datagram Protocol)。
TCP 协议是一种面向连接的、可靠的传输协议,它通过三次握手建立连接,在数据传输过程中进行流量控制和拥塞控制,确保数据的完整性和有序性;TCP栈的简要过程包括连接建立、数据传输和连接关闭三个主要阶段:
①连接建立(三次握手)客户端选择一个初始序列号 x,发送一个带有 SYN 标志的数据包到服务器,此时客户端进入 SYN_SENT 状态。服务器收到 SYN 包后,如果同意连接,会分配 TCP 资源,选择自己的初始序列号 y,并发送一个 SYN-ACK 包,确认客户端的序列号为 x+1。此时服务器进入 SYN_RCVD 状态。客户端收到 SYN-ACK 包后,发送一个 ACK 包给服务器,确认服务器的序列号为 y+1,客户端进入 ESTABLISHED 状态。服务器收到 ACK 包后,也进入 ESTABLISHED 状态,至此 TCP 连接建立成功。②数据传输应用程序创建要发送的数据,通过系统调用(如 write)将数据从用户空间拷贝到内核空间的发送 socket 缓冲区。TCP 协议从发送缓冲区中取出数据,构造 TCP 段,包括添加 TCP 头部,设置序列号、确认号等字段,并计算 TCP 校验和。TCP 段的 payload(有效载荷)大小受接收窗口、拥塞窗口和最大段大小(MSS)限制。TCP 段被传输到 IP 层,IP 层在 TCP 段头部加上 IP 头信息,用于路由。然后 IP 包被传输到链路层,链路层添加以太网头部信息,并通过 ARP 协议获取下一跳的 MAC 地址,最终将数据包发送到网络中。接收方收到数据包后,从链路层依次向上解封装,到达 TCP 层。TCP 层根据序列号和确认号对数据进行排序和确认,将正确的数据放入接收 socket 缓冲区,供应用程序读取。如果发现数据丢失或错误,接收方会请求发送方重发相应数据,发送方会缓存未收到 ACK 的数据,在超时未收到确认时进行重发。同时,接收方会根据自己的接收能力,通过窗口字段告知发送方自己能接收的最大字节数,进行流控制。发送方还会根据网络状况进行拥塞控制,避免网络拥塞。③连接关闭(四次挥手)当客户端完成数据传输并希望断开连接时,它发送一个 FIN 包,进入 FIN-WAIT-1 状态。服务器收到 FIN 包后,发送一个 ACK 包确认,进入 CLOSE-WAIT 状态。客户端收到 ACK 包后,进入 FIN-WAIT-2 状态。服务器完成数据传输后,发送一个 FIN 包给客户端,请求关闭其端的连接,此时服务器进入 LAST-ACK 状态。客户端收到服务器的 FIN 包后,发送一个 ACK 包进行确认,然后进入 TIME-WAIT 状态。经过 2MSL(最大报文段生存时间的两倍)后,客户端确保服务器接收到最终的 ACK 包,然后关闭连接。服务器收到 ACK 后,也关闭连接。UDP 协议则是一种无连接的、不可靠的传输协议,它不保证数据的可靠传输,但具有传输速度快、开销小的特点,适用于对实时性要求较高但对数据准确性要求相对较低的应用场景,如视频直播、音频通话等。例如,当我们使用 FTP(File Transfer Protocol)进行文件传输时,通常会使用 TCP 协议,以确保文件的完整传输;而在观看在线视频时,由于对实时性要求较高,即使少量数据丢失也不会对观看体验造成太大影响,因此可能会使用 UDP 协议。
UDP 栈的简要过程包括数据发送和数据接收两个主要方面:
①数据发送过程创建套接字:应用层通过调用 Socket API 中的socket函数创建一个 UDP 套接字。该函数会最终调用内核中的sock_create方法,返回一个文件描述符,内核中会为该套接字创建对应的struct socket和struct sock结构体,其中struct sock包含接收队列(rx)、发送队列(tx)和错误队列(err)。构建 UDP 数据报:应用程序调用send、sendto或sendmsg等函数发送数据,这些函数最终会调用内核中的udp_sendmsg函数。udp_sendmsg函数根据目的地址和端口号,将应用层数据封装成 UDP 数据报,添加 UDP 首部,首部包含源端口号、目的端口号、长度和校验和(校验和为可选项)。发送到网络层:UDP 将构建好的数据报通过调用ip_append_data方法或ip_queue_xmit函数,将数据包发送给网络层(IP 层)。网络层处理:IP 层接收到数据包后,添加 IP 首部,进行路由处理,确定下一跳地址。如果数据包长度超过网络最大传输单元(MTU),可能会进行分片处理,最后将处理好的数据包发送到链路层。链路层发送:链路层收到数据包后,添加链路层首部(如以太网首部),包含源 MAC 地址和目的 MAC 地址等信息,并通过物理层将数据发送到网络中。②数据接收过程物理层接收:物理层从网络中接收数据帧,将其传递给链路层。链路层解析:链路层去除链路层首部,根据首部中的协议类型字段,判断出是 UDP 数据报,将其传递给网络层。网络层处理:IP 层检查 IP 首部,进行路由相关处理和校验等,根据协议字段确定上层协议为 UDP 后,去除 IP 首部,将 UDP 数据报传递给 UDP 层。UDP 层解析:UDP 层根据 UDP 首部中的目的端口号,将数据报从接收队列中取出,计算校验和(若有),验证数据完整性。如果校验通过,提取数据载荷部分。交付应用层:UDP 将提取出的数据载荷通过 Socket 接口发送给对应的应用程序,应用程序通过调用recv或recvfrom等函数接收数据,完成数据接收过程。1.3网络层:数据的导航者
网络层如同交通枢纽的调度员,负责数据包在网络中的传输和路由选择。其核心协议是 IP(Internet Protocol)协议,它为每个网络节点分配唯一的 IP 地址,使得数据包能够在不同的网络之间进行传输。当网络层接收到链路层传来的数据包后,会根据数据包中的目的 IP 地址,通过路由表查找最佳的传输路径,然后将数据包转发到下一个网络节点。
此外,网络层还包括 ICMP(Internet Control Message Protocol)协议,用于网络诊断和错误报告;IGMP(Internet Group Management Protocol)协议,用于多播通信。例如,当我们在浏览器中输入一个网址并访问时,网络层会根据目标服务器的 IP 地址,规划出数据从本地计算机到目标服务器的传输路径,确保数据能够准确无误地到达。
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括 (1)路由处理,即选择下一跳 (2)添加 IP header(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错 (4)可能的话,进行 IP 分片(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
IP 栈基本处理过程主要包括数据包的接收、校验、路由选择、分片以及发送等步骤:
步骤一:链路层接收分组并送至网络层:当网卡收到与自己 MAC 地址匹配或广播的以太网帧时,会产生硬件中断,网卡驱动处理该中断,从 DMA 或其他方式获取分组数据写入内存,分配套接字缓冲区 skb,并调用 netif_rx (skb) 函数。
skb 进入到达队列等待 CPU 处理,同时标记 NET_RX_SOFTIRQ 网络接收软中断。CPU 调用 do_softirq () 处理软中断,再调用 net_rx_action (),在 netif_receive_skb 函数中,根据协议将 skb 发送到相关协议处理函数,如 ip_rcv () 用于处理 IP 协议。
步骤二:网络层处理分组(以 IPv4 为例):
ip_rcv 函数校验:skb 被送到 ip_rcv () 函数,验证 IP 分组,检查目的地是否为本机地址、校验和是否正确等。若正确,交给 netfilter 的 NF_IP_ROUTING;否则,丢弃数据包。ip_rcv_finish 函数路由判断:随后分组进入 ip_rcv_finish () 函数,根据 skb 结构中的目的或路由信息进行处理。若 skb->dst 无路由信息,则通过 ip_route_input () 查找路由,若目的地不可达,丢弃数据包。最后执行 dst_input,根据结果决定数据包下一步处理方式:本机分组由 ip_local_deliver 处理;需要转发的数据由 ip_forward () 函数处理;组播数据包由 ip_mr_input () 函数处理。ip_forward 转发数据包:若需转发,ip_forward () 函数会先处理 IP 头选项,记录本地 IP 地址和时间戳等,确认分组可转发后,将 TTL 减 1,若 TTL 为 0 则丢弃。接着根据 MTU 大小和路由信息对数据分组进行分片,最后将数据分组送往外出设备。若转发失败,则回应 ICMP 消息说明原因。之后执行 ip_forward_finish () 函数准备发送,再通过 dst_output (skb) 将分组发到转发的目的主机或本地主机,最后调用 ip_finish_output () 进入邻居子系统。ip_local_deliver 本地处理:对于本机分组,ip_local_deliver 中会对 ip 分片进行重组,经过 LOCAL_IN 钩子点,然后调用 ip_local_deliver_finish。ip_local_deliver_finish 函数处理原始套接字的数据接收,并调用上层协议的包接收函数,将数据包传递到传输层。1.4链路层:网络通信的基石
链路层,如同高楼的地基,是网络通信的基础。它负责处理与物理网络设备的直接交互,包括网卡驱动程序以及数据链路协议的实现。其主要功能是将网络层传来的数据包封装成数据帧,并通过物理网络介质进行传输。在接收数据时,则进行相反的操作,将接收到的数据帧解封装,提取出数据包传递给网络层。常见的链路层协议有以太网协议、PPP(Point-to-Point Protocol)协议等。例如,在以太网中,链路层会在数据包前后添加以太网头部和尾部,形成以太网帧,其中头部包含源 MAC 地址和目的 MAC 地址等信息,用于在局域网内标识数据的发送方和接收方。
功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。
实现上,Linux 提供了一个 Network device 的抽象层,其实现在 linux/net/core/dev.c。具体的物理网络设备在设备驱动中(driver.c)需要实现其中的虚函数。Network Device 抽象层调用具体网络设备的函数。
图片
Part2.数据输入:从网卡到内核的旅程
2.1网卡接收与中断处理
在 Linux 系统中,网络数据的输入旅程始于网卡。网卡,作为计算机与网络之间的物理接口,如同一位勤劳的快递员,时刻监听着网络上的数据传输。当网卡接收到数据包时,它首先会进行初步的筛选。如果目的地址不是该网卡,且该网卡没有开启混杂模式,那么这个数据包就会被网卡无情地丢弃,就好像快递员发现包裹不是送到自己负责的区域,便会直接退回。
图片
对于符合条件的数据包,网卡会通过 DMA(Direct Memory Access)技术,将其写入到预先分配好的内存地址中。DMA 技术就像是一条数据高速公路,允许设备直接与内存进行数据传输,而无需 CPU 的频繁干预,大大提高了数据传输的效率。例如,在一台配备千兆网卡的服务器上,通过 DMA 技术,网卡可以快速地将大量的网络数据包写入内存,为后续的处理做好准备。
在完成数据写入后,网卡会通过硬件中断(IRQ)通知 CPU,就如同快递员按响门铃,告诉主人有新的包裹送达。硬件中断是一种由硬件设备发出的电信号,它会使 CPU 立即暂停当前正在执行的任务,保存现场信息,然后跳转到对应的中断处理程序。在 Linux 系统中,每个硬件设备都有一个唯一的中断号,用于标识该设备发出的中断请求,这样 CPU 就能准确地知道是哪个设备在请求服务。
2.2网络驱动的接力
当 CPU 收到网卡发出的中断信号后,会调用已经注册的中断函数,这个中断函数会进一步调用到网卡驱动程序中相应的函数。网络驱动程序就像是一位翻译官,负责将网卡接收到的原始数据转换为内核能够理解的格式。
以常见的 e1000 网卡驱动为例,其硬中断处理函数 e1000_intr () 会首先禁用网卡的中断,表示驱动程序已经知晓内存中有数据,让网卡下次收到数据包时直接写入内存即可,无需再通知 CPU,以此提高效率,避免 CPU 被频繁中断。接着,启动软中断,将耗时较长的数据处理任务交给软中断处理函数。
在软中断处理函数中,网络驱动会从内存中读取数据包,并将其转换为内核网络模块能识别的 skb(socket buffer)格式。skb 是 Linux 内核中用于表示网络数据包的数据结构,它包含了数据包的各种信息,如数据内容、源地址、目的地址等,就像是一个包裹的详细清单,方便内核后续的处理。完成格式转换后,驱动会调用 napi_gro_receive 函数,该函数会处理 GRO(Generic Receive Offload)相关的内容,即将可以合并的数据包进行合并,这样就只需要调用一次协议栈,减少了系统开销。
2.3网络层的初次校验
当数据包以 skb 格式进入网络层后,首先会调用 ip_rcv 函数进行处理。这个函数就像是一位严格的质检员,会对数据包进行多项严格的检查,以确保数据包的完整性和正确性。
它会检查数据包的 IP 首部长度是否符合要求。IP 首部包含了数据包的各种控制信息,如版本号、首部长度、服务类型、总长度等,如果首部长度不正确,那么数据包可能在传输过程中出现了错误,无法被正确处理。它还会检查数据包的校验和。校验和是一种用于检测数据传输错误的机制,通过对数据包的内容进行特定的算法计算得出一个值,接收方在收到数据包后,会重新计算校验和并与发送方发送的校验和进行比较,如果两者不一致,就说明数据包在传输过程中可能发生了错误。
在进行这些基本检查的同时,ip_rcv 函数还会与 netfilter 的 NF_IP_PRE_ROUTING 钩子点交互。netfilter 是 Linux 内核中的一个框架,它提供了一系列的钩子点,允许用户通过编写规则对网络数据包进行过滤、修改等操作。例如,在一些网络安全场景中,管理员可以通过在 NF_IP_PRE_ROUTING 钩子点设置规则,对进入网络层的数据包进行安全检查,如检测是否存在恶意攻击的特征,如果发现可疑数据包,则可以直接丢弃或进行其他处理,从而保护系统的安全。
2.4路由决策与后续走向
经过网络层的初次校验后,数据包会进入 ip_rcv_finish 函数,在这里,路由决策将决定数据包的后续走向。路由决策就像是为数据包规划一条旅行路线,根据数据包的目的 IP 地址,查找最佳的传输路径。
在进行路由决策时,首先会检查路由缓存项。路由缓存就像是一本常用路线的速查手册,记录了最近使用过的路由信息。如果在缓存中找到了匹配的路由项,那么就可以直接使用该路由项,大大提高了路由查找的效率。如果路由缓存未命中,就需要调用 ip_route_input_slow 函数查找路由表。路由表则是一本详细的地图集,包含了网络中所有可能的路由信息。在查找路由表时,会根据目的 IP 地址、子网掩码等信息,找到最佳的下一跳地址。
根据路由结果,数据包有两种可能的走向。如果目的 IP 地址是本地主机的地址,那么数据包将走向本地交付,即被传递到本地的应用程序进行处理;如果目的 IP 地址是其他网络主机的地址,那么数据包将被转发到下一个网络节点。在不同的走向下,会设置不同的处理函数。对于本地交付,会设置 ip_local_deliver 函数,负责将数据包传递到本地的传输层协议进行进一步处理;对于转发,会设置 ip_forward 函数,负责将数据包转发到合适的网络接口,继续其传输旅程。
2.5本地交付的深入剖析
当数据包被确定为本地交付后,会进入 ip_local_deliver 函数进行处理。在这个函数中,首先会对 IP 报文进行分片处理。在网络传输过程中,由于不同网络链路的最大传输单元(MTU)可能不同,当数据包的大小超过了链路的 MTU 时,就需要将数据包进行分片,将其拆分成多个较小的片段进行传输。例如,在一个以太网链路中,MTU 通常为 1500 字节,如果一个数据包的大小为 2000 字节,那么就需要将其分成两个片段,分别为 1500 字节和 500 字节。
ip_local_deliver 函数会对这些分片进行重组,将它们还原成完整的数据包。它会根据 IP 首部中的分片标识、标志位和片偏移等信息,判断各个分片之间的顺序和关系,然后将它们正确地组合起来。完成分片重组后,数据包会被传递到 netfilter 的 NF_IP_LOCAL_IN 钩子点,在这里,用户可以再次对数据包进行过滤、修改等操作。经过 NF_IP_LOCAL_IN 钩子点的处理后,数据包最终会到达传输层,根据其协议类型(如 TCP、UDP 等),被传递到相应的传输层协议模块进行后续的处理,如 TCP 协议的连接管理、数据确认等。在实际的网络场景中,分片处理非常重要。例如,当我们从互联网上下载一个大型文件时,文件会被分成多个数据包进行传输,这些数据包可能会经过不同的网络链路,由于各链路的 MTU 不同,数据包可能会被分片。如果接收方不能正确地对这些分片进行重组,那么就无法得到完整的文件,导致下载失败。
Part3.数据输出:从内核到网卡的征程
3.1传输层的发起
在 Linux 内核中,网络数据的输出是一个复杂而有序的过程,传输层在其中扮演着重要的发起角色。以常见的 TCP 和 UDP 协议为例,当应用程序调用 send () 或 sendto () 等系统调用发送数据时,数据首先进入传输层。
图片
对于 TCP 协议,以一个 Web 服务器向客户端发送网页数据为例,当 Web 服务器的应用程序调用 send () 函数时,数据会被传递到 TCP 层。TCP 层会为数据添加 TCP 首部,首部中包含源端口号、目的端口号、序列号、确认号等重要信息。这些信息就像是货物运输中的详细清单,用于建立和维护可靠的连接。在这个过程中,TCP 协议会进行一系列的操作,如拥塞控制、流量控制等,以确保数据能够稳定、高效地传输。拥塞控制就像是交通警察,通过调整发送窗口的大小,避免网络拥塞,确保数据能够顺利传输;流量控制则是根据接收方的接收能力,调整发送方的发送速率,防止接收方缓冲区溢出。
而 UDP 协议则相对简单,它不提供可靠的传输机制,也没有拥塞控制和流量控制。在视频直播应用中,为了保证直播的实时性,通常会使用 UDP 协议。当直播服务器调用 sendto () 函数发送视频数据时,UDP 层同样会添加 UDP 首部,首部包含源端口号和目的端口号等信息。由于 UDP 协议不需要建立连接,也不保证数据的可靠传输,所以它的传输效率较高,能够满足视频直播对实时性的要求。
3.2 IP层的处理前奏
当数据从传输层传递到 IP 层后,首先会到达本地发送数据包在 netfilter 的 NF_IP_LOCAL_OUT 钩子点。这个钩子点就像是一个关卡,在这里可以对数据包进行各种自定义的处理,如过滤、修改等。
在一些安全防护场景中,管理员可能会在 NF_IP_LOCAL_OUT 钩子点设置规则,对本地发送的数据包进行安全检查。如果发现数据包中包含恶意代码或违反安全策略的内容,就可以直接丢弃该数据包,从而保护系统的安全。而对于转发的数据包,由于其来源并非本地生成,所以会跳过 NF_IP_LOCAL_OUT 钩子点,直接进入后续的处理流程。
这是因为转发数据包的处理逻辑与本地发送数据包有所不同,转发数据包更关注的是如何快速、准确地将数据转发到下一个网络节点,而不是进行本地的安全检查等操作。钩子点的处理对数据包流向起着关键的控制作用,通过合理设置钩子点的规则,可以实现对网络数据的有效管理和安全防护。
3.3路由与协议设置
在经过 NF_IP_LOCAL_OUT 钩子点的处理后,数据包会进入 ip_output 函数。在这个函数中,会进行一系列重要的设置,以确保数据包能够正确地传输到目标地址。
ip_output 函数会根据数据包的目的 IP 地址,查找路由表,确定数据包应该从哪个网络设备发送出去,以及下一跳的地址。这个过程就像是为数据包规划一条旅行路线,根据目的地的不同,选择最合适的交通路线。它会设置数据包的网络设备和协议类型。以一个企业内部网络为例,当一台计算机要向外部网络发送数据时,ip_output 函数会根据路由表的信息,选择合适的出口网络设备,如企业的网关设备,并将数据包的协议类型设置为 IP 协议。
完成这些设置后,数据包会被传递到 netfilter 的 NF_IP_INET_POST_ROUTING 钩子点。在这个钩子点,同样可以对数据包进行一些最后的处理,如网络地址转换(NAT)等。NAT 技术可以将企业内部网络的私有 IP 地址转换为合法的公网 IP 地址,使得企业内部的计算机能够访问外部网络。这些设置对数据包传输至关重要,它们确保了数据包能够在复杂的网络环境中准确无误地找到自己的传输路径,最终到达目标地址。
3.4数据分片与最终发送
当数据包到达 ip_finish_output 函数时,会对报文长度与 MTU(最大传输单元)进行比较。MTU 是指数据链路层一次能够传输的最大数据量,不同的网络链路类型,其 MTU 值也不同,例如以太网的 MTU 通常为 1500 字节。
如果报文长度超过了 MTU,就需要进行分片处理。这就好比将一个大包裹拆分成多个小包裹进行邮寄,以确保每个小包裹都能顺利通过传输链路。在分片过程中,会为每个分片分配一个标识,以及片偏移等信息,以便在接收端能够正确地重组这些分片。以一个发送大型文件的场景为例,假设文件被封装成一个大小为 3000 字节的数据包,而网络链路的 MTU 为 1500 字节,那么这个数据包就需要被分成两个分片,第一个分片包含 1500 字节的数据,第二个分片包含剩余的 1500 字节数据。每个分片都会有自己的 IP 首部,首部中的标识字段相同,用于表示它们属于同一个原始数据包,片偏移字段则表示该分片在原始数据包中的位置。
经过分片处理后,或者如果报文长度小于等于 MTU,数据包会通过邻居子系统发送到网络设备。邻居子系统负责解析目标 IP 地址对应的 MAC 地址,就像是根据收件人的地址找到对应的门牌号。通过 ARP(地址解析协议)等机制,邻居子系统可以获取到目标 IP 地址对应的 MAC 地址,并将其添加到数据包的链路层首部中。最后,数据包会被发送到网络设备,通过物理网络介质传输到目标地址。在实际的网络传输中,网络带宽是一个重要的因素。如果网络带宽较低,而数据包过大,就容易导致网络拥塞,影响数据传输的效率。因此,分片处理可以将大的数据包拆分成多个小的分片,降低每个分片的大小,从而更有效地利用网络带宽,提高数据传输的成功率。
Part4.实例分析与代码解读
4.1选取典型场景
为了更直观地理解 Linux 内核中网络数据的输入输出流程,我们以 Web 服务器与客户端的通信为例进行分析。在这个场景中,客户端通过浏览器访问 Web 服务器,请求获取网页内容。当我们在浏览器地址栏输入网址并按下回车键后,一系列复杂的网络数据交互就此展开。
首先,客户端的浏览器会解析 URL,提取出服务器的域名和请求的资源路径。接着,通过 DNS 解析将域名转换为对应的 IP 地址。在获取到服务器的 IP 地址后,客户端与服务器之间建立 TCP 连接,这一过程通过著名的三次握手来完成。三次握手确保了双方的通信连接是可靠的,就像在正式开始对话之前,双方先确认彼此都准备好进行交流。
TCP 连接建立成功后,客户端向服务器发送 HTTP 请求,请求中包含了请求方法(如 GET、POST 等)、请求头和请求体等信息。服务器接收到 HTTP 请求后,会根据请求内容进行处理,可能会读取相应的网页文件或调用后端程序生成动态内容。然后,服务器将处理结果封装在 HTTP 响应中返回给客户端。客户端收到 HTTP 响应后,对其进行解析,提取出网页内容,并通过浏览器进行渲染,最终将网页展示在用户面前。当通信结束后,客户端和服务器之间会通过四次挥手来断开 TCP 连接。
4.2关键代码片段解析
结合上述 Web 服务器与客户端通信的场景,我们来看一些 Linux 内核中实现网络数据输入输出的关键代码片段。
在网络数据输入方面,当网卡接收到数据包时,会触发中断处理程序。以 e1000 网卡驱动为例,其硬中断处理函数如下:
在这段代码中,首先禁用网卡中断,以避免在处理当前中断时再次被中断。然后遍历接收队列,调用 e1000_clean_rx_irq 函数清理接收队列中的数据包,并通过 e1000_rx_skb 函数将数据包转换为 skb 格式。接着,使用 netif_receive_skb 函数将 skb 传递给协议栈进行后续处理。最后,启动软中断,并恢复网卡中断。
在网络数据输出方面,当应用程序调用 send () 函数发送数据时,最终会调用到 tcp_sendmsg 函数。下面是 tcp_sendmsg 函数的简化代码:
在这段代码中,首先分配一个 skb,并预留出 TCP 首部的空间。然后将用户数据从应用层拷贝到 skb 中,并设置 TCP 首部的相关信息。最后,调用 ip_queue_xmit 函数将 skb 传递给网络层进行发送。如果发送过程中出现错误,会释放 skb 并返回错误信息。
通过对这些关键代码片段的解析,我们可以更深入地理解 Linux 内核中网络数据输入输出的实际代码逻辑,感受到内核开发者在实现网络通信功能时的精妙设计和严谨思考。