2. Linux 网络协议栈分层模型
理解网卡驱动发包流程,首先需要掌握网络协议栈的分层架构。分层设计是计算机网络与操作系统内核网络子系统的核心思想,它将复杂的网络通信过程分解为多个职责清晰的层次,每一层只关注特定的功能,并通过标准接口与相邻层交互。
2.1 OSI 七层模型与 TCP/IP 四层模型
在理论教学与工程实践中,最常引用的两个分层模型是 OSI(开放系统互连)七层模型 和 TCP/IP 四层模型。OSI 模型是国际标准化组织制定的概念性框架,而 TCP/IP 模型则是实际互联网所采用的协议栈。
学习提示: 在 Linux 内核开发中,我们主要关注 TCP/IP 四层模型,但 OSI 模型的“会话层”和“表示层”功能在应用层协议(如 HTTP、TLS)中实现,理解 OSI 有助于建立完整的网络知识体系。
| OSI 七层模型 | TCP/IP 四层模型 | 核心功能 | Linux 内核对应模块 |
|---|---|---|---|
| 7. 应用层 | 应用层 | 为用户应用提供网络服务(HTTP、FTP、DNS) | 用户空间(socket 接口之上) |
| 6. 表示层 | 数据格式转换、加密、压缩 | 用户空间库(如 OpenSSL) | |
| 5. 会话层 | 建立、管理、终止会话连接 | 用户空间应用逻辑 | |
| 4. 传输层 | 传输层 | 端到端可靠传输(TCP)、不可靠传输(UDP) | TCP/IP 协议栈(tcp_ipv4.c, udp.c) |
| 3. 网络层 | 网络层 | 路由选择、分组转发、IP 寻址 | IP 协议栈(ip_output.c, ip_forward.c) |
| 2. 数据链路层 | 网络接口层 | 帧封装、MAC 寻址、差错检测 | 网络设备子系统(dev.c, net/core/) |
| 1. 物理层 | 网络接口层 | 比特流传输、电气信号、介质访问 | 网卡驱动(drivers/net/) |
关键理解: 在 Linux 内核中,TCP/IP 四层模型被进一步细化为内核网络子系统。物理层和数据链路层的功能由网卡硬件和其驱动程序共同完成;网络层和传输层由内核协议栈软件实现;应用层则运行在用户空间。
2.2 Linux 内核网络子系统的分层架构
Linux 内核网络子系统在 TCP/IP 模型的基础上,进一步划分出清晰的软件层次。从用户态应用发送数据到网卡发出数据帧,数据依次经过以下核心层次:
- 系统调用层(System Call Interface):用户程序通过
send()、write()等系统调用进入内核。 - Socket 层:通用 socket 接口,负责管理 socket 数据结构,将操作分发到具体的协议族(如 AF_INET)。
- 传输层(TCP/UDP):处理分段、可靠传输、流量控制、校验和计算。
- 网络层(IP):路由查找、分片、IP 头封装、邻居子系统(ARP)交互。
- 网络设备子系统(Net Device Subsystem):核心调度层,管理
sk_buff队列,调用驱动发送接口。 - 网卡驱动层(NIC Driver):将
sk_buff映射为 DMA 描述符,触发硬件发送。 - 物理层(PHY / Hardware):网卡硬件通过 DMA 读取数据,经 MAC/PHY 发送到网线。
常见误区: 很多开发者认为数据从用户态到网卡是“直线下降”的。实际上,Linux 网络子系统在每一层都可能将数据包入队(如 qdisc 队列、backlog 队列),存在多次调度和排队过程,这正是本课程后续章节要深入剖析的“队列调度”核心。
2.3 各层核心数据结构概览
在 Linux 内核网络子系统中,每一层都有其核心的数据结构,这些结构是数据包在内核中流转的载体。理解这些结构是掌握驱动发包流程的前提。
| 层次 | 核心数据结构 | 定义位置(内核源码) | 关键字段 / 作用 |
|---|---|---|---|
| Socket 层 | struct socket |
include/linux/net.h |
file(关联文件描述符)、ops(协议操作表) |
| Socket 层 | struct sock |
include/net/sock.h |
sk_receive_queue、sk_write_queue、sk_prot(协议函数指针) |
| 传输层 | struct tcp_sock |
include/linux/tcp.h |
tcp_header_len、write_seq、snd_cwnd(拥塞窗口) |
| 网络层 | struct iphdr |
include/uapi/linux/ip.h |
version、ihl、saddr、daddr、protocol |
| 网络层 | struct rtable |
include/net/route.h |
路由缓存条目,包含下一跳、输出设备指针 |
| 邻居子系统 | struct neighbour |
include/net/neighbour.h |
ha(硬件 MAC 地址)、dev(输出设备)、ops |
| 设备子系统 | struct net_device |
include/linux/netdevice.h |
name、mtu、flags、netdev_ops(驱动操作表) |
| 设备子系统 | struct Qdisc |
include/net/sch_generic.h |
enqueue、dequeue 函数指针,流量控制核心 |
| 通用数据包 | struct sk_buff |
include/linux/skbuff.h |
贯穿所有层次的核心数据包结构(详见后续章节) |
| 驱动层 | struct net_device_ops |
include/linux/netdevice.h |
ndo_start_xmit(驱动发送入口)、ndo_open、ndo_stop |
| 驱动层 | struct skb_shared_info |
include/linux/skbuff.h |
管理 sk_buff 的 frags(分散/聚合 I/O 片段) |
核心要点:
struct sk_buff 是贯穿整个网络子系统的“通用货币”。从用户数据到驱动发送,每一层都在操作 sk_buff 的头部指针(head、data、tail、end)来添加或剥离协议头。驱动层的 ndo_start_xmit 函数接收的就是一个完整的、已封装好所有协议头的 sk_buff。
2.4 数据包在内核中的封装过程
当用户通过 socket 发送数据时,内核协议栈会逐层为原始数据添加协议头部。这个过程称为“封装”(Encapsulation)。理解封装顺序对于调试驱动发包问题至关重要。
调试技巧: 使用
ftrace 或 bpftrace 跟踪 skb_push() 和 skb_put() 的调用,可以精确观察每一层如何修改 sk_buff 的指针。这是分析协议栈行为的利器。
典型 TCP 数据包封装流程(从用户数据到网卡帧):
- 应用层数据:用户缓冲区中的原始数据(如 HTTP POST 内容)。
- Socket 层:将用户数据拷贝到内核
sk_buff的数据区(skb_put())。 - 传输层(TCP):
skb_push()添加 TCP 头部(20~60 字节),包含源端口、目的端口、序列号、确认号、窗口大小等。 - 网络层(IP):
skb_push()添加 IP 头部(20~60 字节),包含源 IP、目的 IP、协议号(TCP=6)、TTL 等。 - 邻居子系统:通过 ARP 或邻居缓存获取目标 MAC 地址,填充到
skb->data之前(通过skb_push()添加以太网头)。 - 网络设备子系统:将
sk_buff入队到 qdisc 队列,等待调度。 - 网卡驱动:从 qdisc 出队
sk_buff,将其 DMA 映射到硬件描述符,触发发送。