9. Linux内核网络子系统¶
Linux内核网络子系统是操作系统实现网络数据收发、协议解析、设备管理、流量调度的核心子系统,是支撑Linux服务器、嵌入式设备、网络设备联网能力的底层基石。 与用户态网络编程不同,内核网络子系统运行于内核态,直接对接硬件网卡设备、实现完整TCP/IP协议栈、负责数据包的内存管理、中断处理、流量控制、协议解析与数据转发,具有高性能、高并发、低延迟的特性。
9.1. 网络相关基础知识¶
9.1.1. 网络分层模型¶
网络通信通常采用分层模型,常见的模型有两种:OSI七层模型和TCP/IP四层模型,TCP/IP五层模型较为少见。Linux网络协议栈更接近TCP/IP四层模型,但在实现上可以映射到多个层次。
9.1.1.1. OSI七层模型¶
OSI七层网络模型是国际标准化组织制定的标准化网络通信 参考模型 ,将网络数据传输、交互、处理逻辑拆分为七个独立层级:
应用层:直接面向用户应用,提供各类网络服务接口。如HTTP、FTP、SMTP、DNS。
表示层:处理数据的“表示方式”:格式转换、加密解密、压缩解压缩,保证异构系统能互相读懂数据。如TLS、JPEG、ASCII。
会话层:建立、管理、终止两个进程之间的“会话”,提供对话控制(全双工/半双工)与同步断点。实际中功能大多被并入应用层。
传输层:提供端到端(进程到进程)的传输服务,通过端口号区分应用;负责分段/重组、可靠交付、流量与拥塞控制。典型协议:TCP、UDP。
网络层:负责逻辑寻址(IP地址)、路由选择、分组转发与拥塞控制,把数据包从源主机送跨网络送达目的主机。典型协议/设备:IP、ICMP、路由器。
数据链路层:把比特流组装成“帧”,负责物理寻址(MAC地址)、差错检测(CRC)、流量控制,保证相邻节点间链路上的可靠传输。典型协议/设备:以太网、交换机、网桥。
物理层:在传输介质上传输原始比特流,规定电压、接口形状、引脚定义、速率等机械/电气规范。典型设备:集线器、中继器;如RJ-45、光纤。
9.1.1.2. TCP/IP四层模型¶
TCP/IP四层模型是互联网通信的 标准模型 ,摒弃了OSI七层模型的理论化冗余分层,采用轻量化的四层架构,面向实际工程实现,是互联网真正在跑的模型。
应用层:合并了OSI的会话层、表示层、应用层,包含所有面向应用的协议:HTTP、HTTPS、DNS、FTP、SMTP、SSH等。
传输层:与OSI传输层相同,TCP提供面向连接的可靠传输,UDP提供无连接的尽最大努力交付。
网络层:核心是IP协议,负责编址、路由、转发,屏蔽底层网络差异,实现“异构网络互联”;配套ICMP、ARP、IGMP等。
网络接口层:合并了OSI的物理层和数据链路层,负责把IP数据报封装成帧、通过具体网络(以太网、Wi-Fi、光纤、PPP等)发送出去,处理MAC寻址、电信号等硬件细节。TCP/IP对此层不做硬性规定,从而兼容一切现有网络技术。
9.1.2. 内核网络数据流转模型¶
内核网络数据分为发送流程(TX)与接收流程(RX):
发送流程:用户态Socket发送数据 -> 内核协议栈逐层封装头部(TCP/UDP+IP+以太网帧) -> 网络设备驱动入队 -> 硬件网卡发送至链路。
接收流程:网卡硬件接收帧 -> 硬件中断触发 -> NAPI轮询接管 -> 驱动获取数据包 -> 逐层解封装校验 -> 协议栈处理 -> 推送至用户态Socket。
9.1.3. 网络常见协议¶
9.1.3.1. ARP地址解析协议¶
ARP(Address Resolution Protocol,地址解析协议)工作在网络层,核心作用是实现三层IP地址到二层MAC硬件地址的映射解析。 以太网通信依赖MAC地址寻址,主机仅知晓目标IP时,可通过ARP广播查询全网设备,获取对应IP的MAC地址,构建ARP缓存表,为单播数据帧收发提供二层寻址依据。
报文字段:
长度(位) |
16 |
16 |
8 |
8 |
16 |
48 |
32 |
48 |
32 |
长度(字节) |
2 |
2 |
1 |
1 |
2 |
6 |
4 |
6 |
4 |
数据类型 |
硬件类型 |
上层协议类型 |
硬件地址长度 |
协议地址长度 |
操作类型 |
源硬件地址 |
源协议地址 |
目标硬件地址 |
目标协议地址 |
内核收到ARP请求报文后,校验目标IP是否为本机地址,匹配成功则组装ARP应答报文,交换收发地址并回复;收到应答报文则更新本地ARP缓存。
9.1.3.2. IPv4网络协议¶
IP(Internet Protocol,网际协议)是网络层核心协议,负责跨网段逻辑寻址、路由转发、数据包分片与重组。IP协议为无连接、不可靠传输协议,不保证报文有序、不丢包,仅负责规划传输路径、转发报文,可靠性由上层TCP协议保障。
报文字段:
长度(位) |
4+4 |
8 |
16 |
16 |
3+13 |
8 |
8 |
16 |
32 |
32 |
长度(字节) |
1 |
1 |
2 |
2 |
2 |
1 |
1 |
2 |
4 |
4 |
数据类型 |
版本 + 首部长度 |
服务类型(DSCP/ECN) |
总长度 |
标识 |
标志 + 片偏移 |
TTL |
协议号 |
首部校验和 |
源IP |
目的IP |
内核接收IP报文后,校验版本、TTL、校验和合法性,根据目标IP路由匹配,通过协议号区分上层协议,分发至TCP/UDP/ICMP处理模块;报文转发、修改后必须重新计算首部校验和。
9.1.3.3. ICMP网络控制报文协议¶
ICMP(Internet Control Message Protocol,网络控制报文协议)隶属于网络层,用于网络链路探测、故障诊断、报文反馈,无端口概念。日常ping命令、网络超时、路由不可达等探测功能均基于ICMP实现。
报文字段:
长度(位) |
8 |
8 |
16 |
16 |
16 |
可变 |
长度(字节) |
1 |
1 |
2 |
2 |
2 |
可变 |
数据类型 |
报文类型 |
代码 |
校验和 |
标识 |
序列号 |
数据载荷 |
内核收到ICMP请求报文后,校验合法性,自动组装对应应答报文回复。
9.1.3.4. TCP传输控制协议¶
TCP(Transmission Control Protocol,传输控制协议)是传输层面向连接、可靠传输协议,通过三次握手建立连接、四次挥手断开连接,具备超时重传、流量控制、拥塞控制、乱序重排、应答校验机制,适用于文件传输、网页访问、远程登录等对数据完整性要求高的场景。
报文字段:
长度(位) |
16 |
16 |
32 |
32 |
4+6+6 |
16 |
16 |
16 |
长度(字节) |
2 |
2 |
4 |
4 |
2 |
2 |
2 |
2 |
数据类型 |
源端口 |
目的端口 |
序列号 |
确认号 |
数据偏移+保留+标志位 |
窗口大小 |
校验和 |
紧急指针 |
内核TCP模块维护连接状态机,完成连接握手、数据校验、序号整理、超时重传、拥塞调控,保障端到端可靠通信,是内核协议栈最复杂的传输层协议。
9.1.4. 以太网帧结构¶
以太网帧是TCP/IP网络接口层的核心数据单元,是数据在物理链路中传输的最小完整数据帧。 Linux内核网卡驱动收发数据、skb报文解析、帧校验、协议匹配均基于标准以太网帧格式实现,目前网络设备通用以太网II帧(Ethernet II)格式,由帧头、数据载荷、帧尾校验位三部分组成:
长度(位) |
48 |
48 |
16 |
368~12000 |
32 |
长度(字节) |
6 |
6 |
2 |
46~1500 |
4 |
数据类型 |
目的MAC地址 |
源MAC地址 |
上层协议类型/长度 |
数据载荷 |
FCS帧校验序列 |
上层协议类型常见取值:0x0800为IPv4报文、0x0806为ARP报文、0x86DD为IPv6报文、0x8035为RARP报文。
9.1.5. NAPI中断轮询机制¶
传统网卡收包流程依赖硬中断:每收到一个包就触发一次中断。在高速网络下,这种方式会导致频繁中断上下文切换,性能下降。 Linux引入NAPI(New API)机制来缓解这个问题。
NAPI采用中断触发+轮询处理的混合模式:
空闲状态:依赖硬件中断,有数据包时触发一次中断;
繁忙状态:关闭硬件中断,启动软件轮询,批量处理队列中所有数据包;
处理完毕后重新开启中断,回归空闲监听状态。
这种机制可大幅减少硬件中断次数、批量处理数据包、降低CPU开销、极大提升高吞吐场景下的网络性能。 同时NAPI支持budget预算机制,限制单次轮询最大处理包数,避免CPU被网络任务独占。
9.1.6. 网络字节序¶
网络通信统一采用大端字节序(Big-Endian),即高位字节存储在低地址,低位字节存储在高地址。 而X86、ARM等主流主机CPU默认使用小端字节序,因此所有内核协议栈组装、解析、修改报文时,必须完成主机字节序与网络字节序的转换,否则会出现字段解析错乱、通信异常问题。
9.2. 网络子系统整体架构¶
Linux内核网络子系统采用四层分层架构,从上至下依次为用户态接口层、内核协议栈层、网络设备抽象层、硬件适配层,各层级职责清晰、接口标准化,支持物理网卡、虚拟网卡等多类型网络设备统一管理。
用户态接口层
面向应用程序与运维工具,提供标准化交互接口,不参与数据核心处理。 核心组件:Socket接口、sysfs调试文件系统、ethtool工具、ip/ifconfig命令、网络状态统计接口。 主要用于网络参数配置、状态查询、调试开关控制、用户态数据收发。
内核协议栈层
网络子系统核心,完全内核态实现,是数据解析、封装、转发的中心。 核心组件:TCP/UDP传输层协议、IP/ARP网络层协议、数据包校验模块、路由转发模块、流量控制模块、连接管理模块。 负责完成所有网络协议逻辑处理、数据包合法性校验、端到端通信管理。
网络设备抽象层
内核提供的标准化设备抽象层,屏蔽不同硬件、虚拟设备的差异,为上层协议栈提供统一调用接口。 核心组件:net_device设备结构体、net_device_ops设备操作集、NAPI轮询框架、skb队列管理、网络统计模块。 所有网卡均遵循该层标准接口,实现协议栈与硬件驱动的解耦。
硬件适配驱动层
最底层硬件适配层,对接具体网卡硬件或虚拟设备。 物理网卡驱动实现硬件寄存器读写、中断注册、DMA收发;虚拟网卡驱动纯软件模拟数据收发,无硬件依赖。 核心职责:完成数据包硬件收发、中断处理、NAPI调度、硬件状态同步。
9.3. 网络子系统核心结构体¶
9.3.1. 数据包结构体¶
数据包结构体(struct sk_buff)是Linux内核网络子系统的基石,全称Socket Buffer,用于统一管理所有网络数据包的内存空间、协议头部、数据长度、所属设备、状态标志等信息。 内核所有网络收发、协议解析、数据转发均基于该结构体实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | struct sk_buff {
// 队列链表节点,用于挂载设备收发队列
union {
struct {
struct sk_buff *next; // 下一个skb节点
struct sk_buff *prev; // 上一个skb节点
struct net_device *dev; // 绑定当前数据包所属网络设备
};
};
struct sock *sk; /* 关联的socket,部分场景使用 */
union {
ktime_t tstamp; // 数据包收发时间戳
};
// 数据包长度信息
unsigned int len; // 数据包总长度(头部+载荷)
unsigned int data_len; // 非线性分片数据长度
__u16 mac_len; // 二层MAC头部长度
__u16 hdr_len; // 整体基础头部总长度
__u16 queue_mapping; // 数据包绑定的设备队列编号
// 数据包状态标记位
__u8 cloned:1; // 标记skb是否为克隆副本
__u8 nohdr:1; // 标记是否无协议头部
__u8 peeked:1; // 标记数据是否被预读取
// 协议处理核心状态
__u8 pkt_type:3; // 数据包类型(单播/广播/组播/回包)
__u8 ip_summed:2; // 校验和状态标记(硬件校验/软件校验/无需校验)
__u8 csum_valid:1; // 标记校验和是否合法
__wsum csum; // 数据包校验和数值
__u32 priority; // 数据包转发优先级
int skb_iif; // 数据包入设备接口索引
__u32 hash; // 数据包哈希值,用于负载分发
// 各层协议头部偏移
__be16 protocol; // 二层上层协议类型(IP/ARP等)
__u16 transport_header; // 四层传输层头部偏移
__u16 network_header; // 三层网络层头部偏移
__u16 mac_header; // 二层链路层头部偏移
// 数据缓冲区指针与边界
sk_buff_data_t tail; // 数据尾部指针
sk_buff_data_t end; // 缓冲区结束指针
unsigned char *head; // 缓冲区起始指针
unsigned char *data; // 有效数据起始指针
unsigned int truesize; // skb整体实际占用内存大小
refcount_t users; // 原子引用计数,管理skb生命周期
/* 其他成员省略 */
};
|
9.3.2. 队列头管理结构体¶
队列头管理结构体(struct sk_buff_head)专门用于管理sk_buff双向链表队列的头部结构体,所有网卡收发队列、协议栈缓存队列均基于该结构体实现。 其定义与sk_buff头部前两个成员对齐,保证队列遍历、入队出队操作高效统一,是skb批量管理、并发缓存的基础结构。
1 2 3 4 5 6 7 8 | struct sk_buff_head {
// 双向链表头尾指针,与sk_buff头部对齐
struct sk_buff *next; // 队列首个skb节点
struct sk_buff *prev; // 队列末尾skb节点
__u32 qlen; // 当前队列中skb节点总数量
spinlock_t lock; // 队列并发操作自旋锁
};
|
9.3.3. 网络设备结构体¶
网络设备结构体(struct net_device)是Linux内核所有网络设备的统一抽象结构体,无论是物理网卡、虚拟网卡、回环设备,均基于该结构体实例化。 该结构体存储设备名称、MAC地址、MTU、链路状态、操作回调、硬件特性、统计信息,是内核管理网络设备的核心载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | struct net_device {
char name[IFNAMSIZ]; // 网络设备名称,如eth0、lo
// 硬件IO资源信息
unsigned long mem_end; // 设备内存空间结束地址
unsigned long mem_start; // 设备内存空间起始地址
unsigned long base_addr; // 设备IO物理基地址
unsigned long state; // 设备全局状态
// 设备内核链表节点
struct list_head dev_list; // 全局网络设备链表
struct list_head napi_list; // NAPI设备管理链表
struct list_head unreg_list; // 设备注销处理链表
// 设备基础属性标志
unsigned int flags; // 设备基础状态标志位
unsigned long long priv_flags; // 设备私有功能标志位
// 核心操作回调接口
const struct net_device_ops *netdev_ops; // 网络设备操作集
const struct ethtool_ops *ethtool_ops; // ethtool工具调试查询回调集
const struct header_ops *header_ops; // 二层协议头部操作接口
int ifindex; // 设备全局唯一索引编号
unsigned short gflags; // 设备通用标志位
unsigned short hard_header_len; // 二层硬件头部固定长度
// MTU与数据收发预留空间
unsigned int mtu; // 最大传输单元,以太网默认1500
unsigned short needed_headroom; // 报文头部预留内存空间
unsigned short needed_tailroom; // 报文尾部预留内存空间
unsigned int min_mtu; // 设备支持的最小MTU
unsigned int max_mtu; // 设备支持的最大MTU
unsigned short type; // 设备网络类型
unsigned char addr_len; // 硬件地址长度,以太网固定6字节
// 链路状态统计
struct net_device_stats stats; // 设备基础收发、丢包统计
atomic_t carrier_up_count; // 链路上线次数统计
atomic_t carrier_down_count; // 链路下线次数统计
// 设备运行状态
unsigned char operstate; // 设备运行状态(开启/关闭/异常)
unsigned char link_mode; // 链路工作模式(全双工/半双工)
// MAC地址相关信息
unsigned char perm_addr[MAX_ADDR_LEN]; // 设备永久固化MAC地址
const unsigned char *dev_addr; // 设备当前生效MAC地址
// 网络协议层私有指针
struct in_device __rcu *ip_ptr; // IPv4协议设备管理信息
struct inet6_dev __rcu *ip6_ptr; // IPv6协议设备管理信息
// 接收队列配置
struct netdev_rx_queue *_rx; // 接收队列数组
unsigned int num_rx_queues; // 接收队列数量
rx_handler_func_t __rcu *rx_handler; // 自定义收包处理回调函数
// 发送队列配置
struct netdev_queue *_tx ____cacheline_aligned_in_smp; // 发送队列数组
unsigned int num_tx_queues; // 发送队列数量
struct Qdisc __rcu *qdisc; // 流量控制队列规则
unsigned int tx_queue_len; // 发送队列最大缓存长度
spinlock_t tx_global_lock; // 发送队列并发保护锁
/* 其他成员省略 */
};
|
9.3.4. 网络设备操作集结构体¶
网络设备操作集结构体(struct net_device_ops)是网络设备的核心回调函数集,定义了设备生命周期所有核心操作接口,驱动必须实现对应回调函数,内核在对应场景自动调用,完成设备初始化、数据收发、状态配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | struct net_device_ops {
// 设备初始化与卸载
int (*ndo_init)(struct net_device *dev); // 设备初始化回调
void (*ndo_uninit)(struct net_device *dev); // 设备资源释放回调
// 设备启停生命周期
int (*ndo_open)(struct net_device *dev); // 设备启动(ifup)回调
int (*ndo_stop)(struct net_device *dev); // 设备关闭(ifdown)回调
// 核心数据收发
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
struct net_device *dev); // 数据包发送核心回调
// 设备特性与队列管理
netdev_features_t (*ndo_features_check)(struct sk_buff *skb,
struct net_device *dev,
netdev_features_t features); // 校验设备硬件特性合法性
u16 (*ndo_select_queue)(struct net_device *dev,
struct sk_buff *skb,
struct net_device *sb_dev); // 选择数据包发送队列
// 设备参数配置
int (*ndo_set_mac_address)(struct net_device *dev,
void *addr); // 手动设置设备MAC地址
int (*ndo_validate_addr)(struct net_device *dev); // 校验MAC地址格式合法性
int (*ndo_change_mtu)(struct net_device *dev,
int new_mtu); // 修改设备MTU最大传输单元
// 异常与状态处理
void (*ndo_tx_timeout)(struct net_device *dev,
unsigned int txqueue); // 发包超时异常处理
int (*ndo_change_carrier)(struct net_device *dev,
bool new_carrier); // 修改链路载波状态(上线/下线)
// 设备信息统计
void (*ndo_get_stats64)(struct net_device *dev,
struct rtnl_link_stats64 *storage); // 获取设备收发统计信息
// 硬件特性配置
netdev_features_t (*ndo_fix_features)(struct net_device *dev,
netdev_features_t features); // 修正设备软硬件特性
int (*ndo_set_features)(struct net_device *dev,
netdev_features_t features); // 开启/关闭设备硬件卸载特性
/* 其他成员省略 */
};
|
9.3.5. 设备统计结构体¶
设备统计结构体(struct net_device_stats)用于记录网卡收发报文、字节、错误、丢包、冲突等运行数据,是ethtool、ip -s、ifconfig等工具展示网络统计信息的底层数据源,所有物理/虚拟网卡均依赖该结构体维护基础状态统计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | struct net_device_stats {
NET_DEV_STAT(rx_packets); // 接收报文总数量
NET_DEV_STAT(tx_packets); // 发送报文总数量
NET_DEV_STAT(rx_bytes); // 接收数据总字节数
NET_DEV_STAT(tx_bytes); // 发送数据总字节数
NET_DEV_STAT(rx_errors); // 接收报文总错误数
NET_DEV_STAT(tx_errors); // 发送报文总错误数
NET_DEV_STAT(rx_dropped); // 接收端丢包总数
NET_DEV_STAT(tx_dropped); // 发送端丢包总数
NET_DEV_STAT(multicast); // 接收组播报文数量
NET_DEV_STAT(collisions); // 以太网报文冲突次数
NET_DEV_STAT(rx_length_errors); // 接收报文长度错误
NET_DEV_STAT(rx_over_errors); // 接收缓冲区溢出错误
NET_DEV_STAT(rx_crc_errors); // 接收报文CRC校验错误
NET_DEV_STAT(rx_frame_errors); // 接收帧格式错误
NET_DEV_STAT(rx_fifo_errors); // 接收FIFO队列溢出错误
NET_DEV_STAT(rx_missed_errors); // 遗漏未接收报文错误
NET_DEV_STAT(tx_aborted_errors); // 发送中止错误
NET_DEV_STAT(tx_carrier_errors); // 发送链路载波错误
NET_DEV_STAT(tx_fifo_errors); // 发送FIFO队列溢出错误
NET_DEV_STAT(tx_heartbeat_errors);// 发送心跳异常错误
NET_DEV_STAT(tx_window_errors); // 发送滑动窗口异常错误
NET_DEV_STAT(rx_compressed); // 接收压缩报文数量
NET_DEV_STAT(tx_compressed); // 发送压缩报文数量
};
|
9.3.6. 网卡调试配置回调结构体¶
网卡调试配置回调结构体(struct ethtool_ops)是Linux网卡设备调试与参数配置的核心回调结构体,为ethtool工具提供底层接口支撑。 所有网卡硬件信息查询、链路参数配置、收发队列调节、功耗调试、统计信息获取等操作,均通过该结构体绑定的回调函数实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | struct ethtool_ops {
// 驱动基础信息获取
void (*get_drvinfo)(struct net_device *, struct ethtool_drvinfo *); // 获取驱动版本、厂商信息
u32 (*get_link)(struct net_device *); // 获取网卡物理链路状态
// 链路与速率配置
int (*get_link_ksettings)(struct net_device *, struct ethtool_link_ksettings *); // 获取速率/双工模式
int (*set_link_ksettings)(struct net_device *, const struct ethtool_link_ksettings *); // 设置速率/双工模式
// 中断聚合参数配置
int (*get_coalesce)(struct net_device *, struct ethtool_coalesce *,
struct kernel_ethtool_coalesce *, struct netlink_ext_ack *); // 获取中断聚合参数
int (*set_coalesce)(struct net_device *, struct ethtool_coalesce *,
struct kernel_ethtool_coalesce *, struct netlink_ext_ack *); // 设置中断聚合参数
// 收发队列参数配置
void (*get_ringparam)(struct net_device *, struct ethtool_ringparam *,
struct kernel_ethtool_ringparam *, struct netlink_ext_ack *); // 获取队列缓冲区参数
int (*set_ringparam)(struct net_device *, struct ethtool_ringparam *,
struct kernel_ethtool_ringparam *, struct netlink_ext_ack *); // 设置队列缓冲区参数
// 网卡统计与自测
void (*get_ethtool_stats)(struct net_device *, struct ethtool_stats *, u64 *); // 获取网卡收发统计数据
void (*self_test)(struct net_device *, struct ethtool_test *, u64 *); // 网卡硬件自检测试
// 多队列通道配置
void (*get_channels)(struct net_device *, struct ethtool_channels *); // 获取网卡多队列通道数
int (*set_channels)(struct net_device *, struct ethtool_channels *); // 设置网卡多队列通道数
// 休眠唤醒配置
void (*get_wol)(struct net_device *, struct ethtool_wolinfo *); // 获取网卡唤醒配置
int (*set_wol)(struct net_device *, struct ethtool_wolinfo *); // 设置网卡唤醒功能
/* 其他成员省略 */
};
|
9.3.7. NAPI轮询结构体¶
NAPI轮询结构体(struct napi_struct)是内核NAPI高性能收包机制的核心结构体,用于管理轮询状态、收包预算、处理函数、队列挂载。 所有支持NAPI的网卡设备均需初始化该结构体,实现中断+轮询的混合收包模式。
1 2 3 4 5 6 7 8 9 10 11 | struct napi_struct {
struct list_head poll_list; // NAPI全局轮询链表节点
unsigned long state; // NAPI状态位图(调度/空闲/工作中)
int weight; // 单次轮询最大处理数据包预算
int (*poll)(struct napi_struct *, int); // 核心收包轮询回调
struct net_device *dev; // 绑定所属网络设备
struct sk_buff *skb; // 临时缓存待处理skb报文
struct list_head rx_list; // 待处理普通收包报文链表
int rx_count; // rx_list链表中报文数量
unsigned int napi_id; // NAPI唯一设备ID
};
|
9.3.8. 网络相关头部结构体¶
9.3.8.1. 以太网头部结构体¶
以太网头部结构体(struct ethhdr)用于映射二层以太网帧头格式,是驱动解析、封装、修改二层报文的核心载体。 所有网卡收发的以太网数据帧,其源目MAC、上层协议类型等字段均通过该结构体直接操作。
1 2 3 4 5 | struct ethhdr {
unsigned char h_dest[ETH_ALEN]; // 目标MAC地址
unsigned char h_source[ETH_ALEN]; // 源端MAC地址
__be16 h_proto; // 上层协议类型(网络大端序)
} __attribute__((packed));
|
9.3.8.2. ARP协议头部结构体¶
ARP协议头部结构体(struct arphdr)用于映射网络层ARP报文格式,是实现IP与MAC地址解析、ARP请求/应答报文封装与解析的核心数据结构。 驱动处理ARP报文翻转、协议栈解析ARP数据时,均通过该结构体直接操作报文字段。
1 2 3 4 5 6 7 | struct arphdr {
__be16 ar_hrd; // 硬件地址类型,以太网固定为0x0001
__be16 ar_pro; // 协议地址类型,IPv4固定为0x0800
unsigned char ar_hln; // 硬件地址长度,MAC地址固定6字节
unsigned char ar_pln; // 协议地址长度,IPv4固定4字节
__be16 ar_op; // ARP操作码,1=请求,2=应答
}
|
9.3.8.3. IPv4协议头部结构体¶
IPv4协议头部结构体(struct iphdr)用于映射标准IPv4报文头部布局,是网络层IP报文解析、封装、修改、转发的核心载体。 该结构体通过大小端位域适配不同CPU架构,兼容大小端主机,涵盖IP版本、头部长度、TTL、上层协议、源目IP、校验和等所有核心字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4, // IP头部长度(4bit)
version:4; // IP协议版本(4bit)
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4, // IP协议版本(4bit)
ihl:4; // IP头部长度(4bit)
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; // 服务类型字段
__be16 tot_len; // IP报文总长度
__be16 id; // 报文唯一标识,用于分片重组
__be16 frag_off; // 分片标志+分片偏移量
__u8 ttl; // 报文生存时间(路由跳数)
__u8 protocol; // 上层传输协议号
__sum16 check; // IP头部校验和
__struct_group(/* no tag */, addrs, /* no attrs */,
__be32 saddr; // 源IP地址
__be32 daddr; // 目的IP地址
);
};
|
9.4. 网络子系统核心函数¶
9.4.1. 网络设备分配与释放¶
9.4.1.1. alloc_netdev函数¶
alloc_netdev函数用于动态分配一个网络设备结构体内存,同时预留设备私有数据空间,初始化设备基础参数,是所有网络设备创建的入口。
函数原型:
1 2 3 | struct net_device *alloc_netdev(size_t priv_size, const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *));
|
参数说明:
priv_size:设备私有数据结构体大小,内核自动分配私有内存;
name:设备名称模板,如eth%d,内核自动填充序号;
name_assign_type:设备名称分配类型,NET_NAME_UNKNOWN为通用模式;
setup:设备初始化回调函数,用于自定义设备参数、绑定回调。
返回值:成功返回net_device指针,失败返回NULL。
9.4.1.2. free_netdev函数¶
与alloc_netdev配对使用,用于释放动态分配的网络设备结构体内存及设备私有数据内存,需在设备注销完成后调用,彻底释放内核设备资源,避免内存泄漏。
函数原型:
1 | void free_netdev(struct net_device *dev);
|
参数说明:
dev:需要释放的网络设备结构体指针,必须是alloc_netdev分配且完成初始化的合法设备。
9.4.2. 网络设备注册与注销¶
9.4.2.1. register_netdev函数¶
register_netdev函数用于将初始化完成、参数配置完毕的网络设备结构体,正式注册到Linux内核网络子系统。 完成注册后,设备会被内核识别,可在用户态通过ip/ifconfig命令查看、配置、启停设备,正式纳入内核网络设备管理体系。
函数原型:
1 | int register_netdev(struct net_device *dev);
|
参数说明:
dev:需要注册的网络设备结构体指针,必须是alloc_netdev分配且完成初始化的合法设备。
返回值:注册成功返回0;注册失败返回负错误码。
9.4.2.2. unregister_netdev函数¶
unregister_netdev函数用于将已注册的网络设备从内核网络子系统中下线注销,是register_netdev的反向操作。 注销后设备从内核设备列表移除,用户态无法再识别、操作该设备。
函数原型:
1 | void unregister_netdev(struct net_device *dev);
|
参数说明:
dev:需要下线注销的网络设备结构体指针,必须是已成功注册的合法设备。
9.4.3. 设备私有数据获取¶
9.4.3.1. netdev_priv函数¶
netdev_priv函数用于获取alloc_netdev分配网络设备时预留的设备私有数据内存指针。 网络设备的自定义私有参数、资源结构体均存放于该私有内存空间,驱动通过该函数快速读取/修改私有数据。
函数原型:
1 | static inline void *netdev_priv(const struct net_device *dev);
|
参数说明:
dev:合法的网络设备结构体指针,必须为alloc_netdev动态分配的设备。
返回值:void* 类型私有数据内存指针,可强制转换为驱动自定义的私有结构体类型,进而操作设备私有资源。
9.4.4. 以太网设备初始化¶
9.4.4.1. ether_setup函数¶
ether_setup函数用于快速初始化以太网类型网络设备的默认参数,批量填充net_device结构体的以太网标准默认配置,避免手动初始化大量冗余的参数。
函数原型:
1 | void ether_setup(struct net_device *dev);
|
参数说明:
dev:待初始化的网络设备结构体指针,必须是alloc_netdev分配完成的合法设备,仅适配以太网设备场景。
9.4.5. 随机MAC地址初始化¶
9.4.5.1. eth_hw_addr_random函数¶
eth_hw_addr_random函数用于为以太网网络设备生成并设置合法的随机本地MAC硬件地址。
函数原型:
1 | void eth_hw_addr_random(struct net_device *dev);
|
参数说明:
dev:待配置随机MAC的网络设备结构体指针,必须是已完成ether_setup初始化的合法以太网设备。
9.4.6. NAPI注册与注销¶
9.4.6.1. netif_napi_add函数¶
netif_napi_add函数用于为网络设备初始化NAPI结构体,绑定轮询回调函数,注册到内核NAPI框架。
函数原型:
1 2 3 | void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int));
|
参数说明:
dev:绑定的网络设备;
napi:待初始化的NAPI结构体;
poll:NAPI轮询回调函数。
9.4.6.2. netif_napi_del函数¶
与netif_napi_add配对使用,用于注销、删除已注册的NAPI实例,从内核NAPI框架中移除设备的轮询回调能力,释放NAPI调度资源。
函数原型:
1 | void netif_napi_del(struct napi_struct *napi);
|
参数说明:
napi:待注销的NAPI结构体指针,必须是已通过netif_napi_add初始化注册完成的合法NAPI实例。
9.4.7. 设备发送队列启动与停止¶
9.4.7.1. netif_start_queue函数¶
netif_start_queue函数用于激活设备发送队列,告知内核协议栈当前设备可正常接收、发送数据包。 设备初始化完成、链路上线后必须调用该函数,否则内核不会向下下发TX报文,直接导致网卡无法发包。
函数原型:
1 | void netif_start_queue(struct net_device *dev);
|
参数说明:
dev:待启动发送队列的网络设备结构体指针。
9.4.7.2. netif_stop_queue函数¶
netif_stop_queue函数用于暂停设备发送队列调度,告知内核协议栈当前设备暂时无法处理发包请求,停止向下投递TX报文。 常用于设备关闭、链路断开、队列拥堵、硬件异常场景,避免无效发包导致报文丢失、内核报错。
函数原型:
1 | void netif_stop_queue(struct net_device *dev);
|
参数说明:
dev:需要停止发送队列的网络设备结构体指针。
9.4.8. 链路载波开启与关闭¶
9.4.8.1. netif_carrier_on函数¶
netif_carrier_on函数用于标记网络设备链路为上线状态,开启设备载波信号。 内核通过载波状态判断设备链路可用性,载波开启后,系统识别网卡链路正常,允许进行网络收发、路由匹配、协议交互。
函数原型:
1 | void netif_carrier_on(struct net_device *dev);
|
参数说明:
dev:需要开启链路载波的网络设备指针。
9.4.8.2. netif_carrier_off函数¶
netif_carrier_off函数用于标记网络设备链路为断开状态,关闭设备载波信号。 调用后内核判定设备链路异常,停止路由转发、终止报文收发、阻塞新的网络请求,同时同步更新设备链路统计状态。
函数原型:
1 | void netif_carrier_off(struct net_device *dev);
|
参数说明:
dev:需要断开链路载波的网络设备指针。
9.4.9. NAPI启用与禁用¶
9.4.9.1. napi_enable函数¶
napi_enable函数用于启用已初始化的NAPI实例,将NAPI状态置为可调度状态,允许内核调度器触发轮询收包回调。 NAPI通过netif_napi_add完成结构体初始化后,默认处于禁用状态,必须调用该函数才能正式开启NAPI收包能力,是网卡设备启动、开启收包链路的关键步骤。
函数原型:
1 | void napi_enable(struct napi_struct *napi);
|
参数说明:
napi:已通过netif_napi_add初始化完成的合法NAPI结构体指针。
9.4.9.2. napi_disable函数¶
与napi_enable成对匹配使用,用于禁用已启用的NAPI实例,阻塞新的NAPI调度请求,等待当前正在执行的轮询流程结束,保证NAPI资源安全退出。
函数原型:
1 | void napi_disable(struct napi_struct *napi);
|
参数说明:
napi:已完成初始化、曾启用的合法NAPI结构体指针。
9.4.10. NAPI调度触发¶
9.4.10.1. napi_schedule函数¶
napi_schedule函数用于触发NAPI轮询机制,调度内核线程执行poll收包函数,批量处理队列数据包。 通常在收到新数据包、skb队列有待处理报文时调用,唤醒NAPI轮询逻辑。
函数原型:
1 | void napi_schedule(struct napi_struct *napi);
|
参数说明:
napi:已完成初始化、启用的合法NAPI结构体指针。
9.4.11. NAPI轮询收尾完成¶
9.4.11.1. napi_complete_done函数¶
napi_complete_done函数用于结束单次NAPI调度轮询流程、更新NAPI工作状态、根据处理预算判断是否重新开启硬件中断。 在NAPI poll回调函数处理完数据包后必须调用,用于告知内核本轮轮询处理结束,同时平衡CPU占用与收包吞吐,避免NAPI死循环、中断永久关闭问题。
函数原型:
1 | bool napi_complete_done(struct napi_struct *napi, int work_done);
|
参数说明:
napi:当前正在执行轮询的NAPI结构体指针。
work_done:本轮poll回调实际处理的数据包个数。
返回值:布尔值,true 表示本轮未耗尽预算、队列仍有数据包,内核可再次调度NAPI轮询;false 表示本轮处理完毕、队列空闲或耗尽预算,正式结束轮询并恢复硬件中断。
9.4.12. 网络栈收包¶
9.4.12.1. netif_receive_skb函数¶
netif_receive_skb函数用于驱动处理完数据包后,将合法skb推送至内核上层协议栈,完成二层解封装、三层协议解析,是驱动向上交付数据的核心API。
函数原型:
1 | int netif_receive_skb(struct sk_buff *skb);
|
参数说明:
skb:合法的sk_buff数据包结构体指针。
返回值:返回0表示数据包成功投递至内核协议栈;返回非负数值代表报文投递异常。
9.4.13. 以太网协议类型解析¶
9.4.13.1. eth_type_trans函数¶
eth_type_trans函数用于解析skb报文的二层以太网协议类型,自动剥离以太网头部、校准skb数据指针、赋值skb->protocol网络协议字段,完成二层帧到三层协议的转接适配。
函数原型:
1 | __be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev);
|
参数说明:
skb:待解析的以太网帧skb数据包指针;
dev:当前收包的网络设备结构体指针。
返回值:网络大端序16位协议类型值,返回二层承载的上层协议标识,常见取值:0x0800(IPv4)、0x0806(ARP)、0x86DD(IPv6),可直接赋值给skb->protocol字段。
9.4.14. SKB队列初始化¶
9.4.14.1. skb_queue_head_init函数¶
skb_queue_head_init函数用于对sk_buff_head队列头结构体做标准化初始化,清零双向链表头尾指针、队列长度计数器、并发自旋锁,将队列置为合法空闲状态。 所有自定义skb收发队列、缓存队列,在使用入队/出队/清空接口前,必须通过该函数完成初始化。
函数原型:
1 | static inline void skb_queue_head_init(struct sk_buff_head *list);
|
参数说明:
list:待初始化的sk_buff_head队列头结构体指针。
9.4.15. SKB队列长度获取¶
9.4.15.1. skb_queue_len函数¶
skb_queue_len函数用于快速获取skb队列当前缓存的数据包总个数。
函数原型:
1 | static inline __u32 skb_queue_len(const struct sk_buff_head *list);
|
参数说明:
list:合法的skb队列头结构体指针。
返回值:无符号32位整型,返回队列中当前缓存的skb报文总数量,队列为空时返回0。
9.4.16. SKB队列入队与出队¶
9.4.16.1. __skb_queue_tail函数¶
__skb_queue_tail函数用于实现skb数据包尾部入队操作,将单个skb数据包挂载到skb队列尾部,完成数据包缓存存储。 该函数为无锁基础入队接口,本身不做并发保护,多线程、中断上下文调用时必须手动加自旋锁,保障队列操作并发安全
函数原型:
1 | void __skb_queue_tail(struct sk_buff_head *list, struct sk_buff *skb);
|
参数说明:
list:skb队列头结构体指针。
skb:待入队的sk_buff数据包指针。
9.4.16.2. __skb_dequeue函数¶
__skb_dequeue函数用于实现skb数据包头部出队操作,从skb队列头部取出首个数据包,实现先进先出的队列读取逻辑。 与入队函数对应,该函数为无锁基础出队接口,并发场景下需配合自旋锁使用,保证队列读取安全。
函数原型:
1 | struct sk_buff *__skb_dequeue(struct sk_buff_head *list);
|
参数说明:
list:skb队列头结构体指针。
返回值:成功出队返回队列头部合法skb数据包指针;队列为空时返回NULL。
9.5. 内核网络子系统源码解析¶
以RK3588S(基于Synopsys DWMAC IP) + RTL8211F PHY进行解析,分析是如何协同完成网卡注册、链路建立以及数据收发。整个链路分为5个阶段:平台Probe与资源解析、MDIO扫描与PHY绑定、网卡Open与链路协商、数据发送(TX)、数据接收(RX)。
选取瑞芯微6.1.99版本内核进行说明,相关源码如下:
板卡设备树:内核源码/arch/arm64/boot/dts/rockchip/rk3588s-lubancat-4.dts
SoC通用设备树:内核源码/arch/arm64/boot/dts/rockchip/rk3588s.dtsi
MAC核心层驱动:内核源码/drivers/net/ethernet/stmicro/stmmac/
瑞芯微MAC平台胶水层驱动:内核源码/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
PHY驱动:内核源码/drivers/net/phy/realtek.c
MDIO驱动:内核源码/drivers/net/mdio/
9.5.1. MAC与PHY¶
在以太网架构中,网卡控制器通常分为MAC(媒体访问控制层)和PHY(物理层)两部分:
MAC (RK3588S GMAC1):负责数据链路层功能,如以太网帧的封装/解封装、MAC地址过滤、校验和卸载以及通过DMA与系统内存交换数据,由stmmac驱动管理。
PHY (RTL8211F):负责物理层功能,如数字信号与模拟信号的转换、自协商速率/双工模式、链路状态检测,由realtek.c驱动管理。
MAC与PHY的通信接口:
数据通道:RGMII(Reduced Gigabit Media Independent Interface),用于MAC和PHY之间高速传输以太网帧。
控制通道:MDIO/MDC总线,用于MAC读写PHY的内部寄存器,如配置延迟、读取链路状态。
9.5.2. 设备树解析¶
SoC通用设备树定义了RK3588S芯片内部GMAC1控制器的硬件属性和资源,与具体外接什么PHY无关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | gmac1: ethernet@fe1c0000 {
compatible = "rockchip,rk3588-gmac", "snps,dwmac-4.20a";
reg = <0x0 0xfe1c0000 0x0 0x10000>;
interrupts = <GIC_SPI 234 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 233 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "macirq", "eth_wake_irq";
rockchip,grf = <&sys_grf>;
rockchip,php_grf = <&php_grf>;
clocks = <&cru CLK_GMAC_125M>, <&cru CLK_GMAC_50M>,
<&cru PCLK_GMAC1>, <&cru ACLK_GMAC1>,
<&cru CLK_GMAC1_PTP_REF>;
clock-names = "stmmaceth", "clk_mac_ref",
"pclk_mac", "aclk_mac",
"ptp_ref";
resets = <&cru SRST_A_GMAC1>;
reset-names = "stmmaceth";
power-domains = <&power RK3588_PD_GMAC>;
snps,mixed-burst;
snps,tso;
snps,axi-config = <&gmac1_stmmac_axi_setup>;
snps,mtl-rx-config = <&gmac1_mtl_rx_setup>;
snps,mtl-tx-config = <&gmac1_mtl_tx_setup>;
status = "disabled";
mdio1: mdio {
compatible = "snps,dwmac-mdio";
#address-cells = <0x1>;
#size-cells = <0x0>;
};
gmac1_stmmac_axi_setup: stmmac-axi-config {
snps,wr_osr_lmt = <4>;
snps,rd_osr_lmt = <8>;
snps,blen = <0 0 0 0 16 8 4>;
};
gmac1_mtl_rx_setup: rx-queues-config {
snps,rx-queues-to-use = <1>;
queue0 {};
};
gmac1_mtl_tx_setup: tx-queues-config {
snps,tx-queues-to-use = <1>;
queue0 {};
};
};
|
关键说明:
compatible:”rockchip,rk3588-gmac”用来匹配Rockchip的平台驱动dwmac-rk.c,”dwmac-4.20a”匹配stmmac核心驱动。
reg:GMAC寄存器的物理基地址和映射长度。内核通过devm_platform_ioremap_resource将其映射为虚拟地址priv->ioaddr。
interrupts:定义了主中断(macirq)和唤醒中断(eth_wake_irq)。内核在stmmac_request_irq中申请这些IRQ资源。
clocks/resets:定义了MAC运行所需的5路时钟(主时钟、PTP时钟等)和复位信号。驱动在Probe阶段会开启时钟并解除复位。
snps,tso:开启TCP分段卸载(TSO)硬件支持。内核解析后设置plat->tso_en = true,发包时若遇到超大TCP 包,会走stmmac_tso_xmit硬件切分路径。
snps,axi-config:指向AXI总线配置子节点。配置DMA引擎在AXI总线上的Burst长度(snps,blen)和Outstanding读写限制,优化内存带宽利用率。
mdio1: mdio:声明该MAC内部集成了一个MDIO总线控制器,用于通过MDC/MDIO引脚与外部PHY芯片进行寄存器读写通信。
snps,mtl-*-config:指向MTL(MAC 传输层)多队列配置。此处配置了1个RX队列和1个TX队列(snps,rx-queues-to-use = <1>),即单队列模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | &gmac1 {
/* Use rgmii-rxid mode to disable rx delay inside Soc */
phy-mode = "rgmii-rxid";
clock_in_out = "output";
snps,reset-gpio = <&gpio3 RK_PB2 GPIO_ACTIVE_LOW>;
snps,reset-active-low;
/* Reset time is 20ms, 100ms for rtl8211f */
snps,reset-delays-us = <0 20000 100000>;
pinctrl-names = "default";
pinctrl-0 = <&gmac1_miim
&gmac1_tx_bus2
&gmac1_rx_bus2
&gmac1_rgmii_clk
&gmac1_rgmii_bus>;
tx_delay = <0x1b>;
/* rx_delay = <0x00>; */
jl2xxx,tx-delay = <0x01>;
jl2xxx,rx-delay = <0x01>;
rtl8211f,tx-delay = <0x1b>;
/* rtl8211f,rx-delay = <0x01>; */
phy-handle = <&rgmii_phy1>;
status = "okay";
};
|
关键说明:
phy-mode = “rgmii-rxid”:这是RGMII接口最关键的配置。rxid表示RX延迟由PHY内部提供(RX Internal Delay),而TX延迟必须由MAC侧提供。
tx_delay = <0x1b>:由于RX延迟交给了PHY,MAC侧需要补偿TX延迟。0x1b是Rockchip GRF(通用寄存器文件)中的特定值,通常对应约1.5ns ~ 2.0ns的时钟相位偏移,以满足RGMII的时序要求。
snps,reset-gpio:定义了PHY的硬件复位引脚(GPIO3_B2,低电平有效)。
时序控制:<0 20000 100000>表示:拉低前等待0us -> 保持拉低状态20ms -> 拉高后等待100ms再开始MDIO通信,确保PHY内部PLL锁相环稳定。
phy-handle = <&rgmii_phy1>:在MAC节点中将MAC与这个PHY节点绑定,建立phylink关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | &mdio1 {
rgmii_phy1: phy@0 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <0x0>;
jl2xxx,led-enable = <(JL2XXX_LED_STATIC_OP_EN | JL1XXX_LED_MODE_EN)>;
jl2xxx,led-mode = <(JL2XXX_LED1_LINK10 | \
JL2XXX_LED1_LINK100 | \
JL2XXX_LED1_LINK1000 | \
JL2XXX_LED2_LINK10 | \
JL2XXX_LED2_LINK100 | \
JL2XXX_LED2_LINK1000 | \
JL2XXX_LED2_ACTIVITY )>; //JL PHY
realtek,led-data = <0x6d60>; //8211F phy
};
};
|
关键说明:
compatible:”ethernet-phy-ieee802.3-c22”匹配内核驱动of_mdio.c。
realtek,led-data:自定义属性,用于配置RTL8211F的LED引脚行为,控制LED闪烁状态。
9.5.3. 瑞芯微MAC平台胶水层驱动解析¶
stmmac是Synopsys的通用以太网MAC IP核驱动,用于所有使用dwmac IP的SoC。 但每家SoC厂商围绕这个IP核做了不同的封装——时钟配置、GRF寄存器、PHY接口选择、延迟配置等都不同。 dwmac-rk.c就是瑞芯微平台的胶水层,负责把这些RK特有配置接入stmmac通用框架。
9.5.3.1. 驱动注册入口¶
设备树compatible中:”rockchip,rk3588-gmac”用来匹配rk_gmac_dwmac_match表,绑定rk3588_ops。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static const struct of_device_id rk_gmac_dwmac_match[] = {
{ .compatible = "rockchip,rk3588-gmac", .data = &rk3588_ops },
// 其他RK芯片...
};
static struct platform_driver rk_gmac_dwmac_driver = {
.probe = rk_gmac_probe, // 探测函数
.remove = rk_gmac_remove, // 移除函数
.driver = {
.name = "rk_gmac-dwmac",
.pm = &rk_gmac_pm_ops, // 电源管理
.of_match_table = rk_gmac_dwmac_match, // 设备树匹配表
},
};
module_platform_driver(rk_gmac_dwmac_driver);
|
匹配成功后后续通过of_device_get_match_data()取出RK3588专用的操作集。
9.5.3.2. RK平台操作集¶
rk_gmac_ops及其在RK3588上的实例rk3588_ops,是驱动中最核心的操作集,用于屏蔽RK3588底层特有的硬件寄存器差异,向上层通用的STMMAC核心驱动提供统一的硬件配置接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | struct rk_gmac_ops {
void (*set_to_rgmii)(struct rk_priv_data *bsp_priv,
int tx_delay, int rx_delay);
void (*set_to_rmii)(struct rk_priv_data *bsp_priv);
void (*set_to_sgmii)(struct rk_priv_data *bsp_priv);
void (*set_to_qsgmii)(struct rk_priv_data *bsp_priv);
void (*set_rgmii_speed)(struct rk_priv_data *bsp_priv, int speed);
void (*set_rmii_speed)(struct rk_priv_data *bsp_priv, int speed);
void (*set_clock_selection)(struct rk_priv_data *bsp_priv, bool input,
bool enable);
void (*integrated_phy_power)(struct rk_priv_data *bsp_priv, bool up);
bool regs_valid;
u32 regs[];
};
static const struct rk_gmac_ops rk3588_ops = {
.set_to_rgmii = rk3588_set_to_rgmii, // 配置RGMII模式
.set_to_rmii = rk3588_set_to_rmii, // 配置RMII模式
.set_rgmii_speed = rk3588_set_gmac_speed, // 设置RGMII速率
.set_rmii_speed = rk3588_set_gmac_speed, // 设置RMII速率
.set_clock_selection = rk3588_set_clock_selection, // 时钟输入/输出选择
.regs_valid = true,
.regs = { // gmac0/gmac1基地址
0xfe1b0000, /* gmac0 */
0xfe1c0000, /* gmac1 */
0x0, /* sentinel */
},
};
|
9.5.3.3. probe函数¶
probe函数负责把SoC特有的硬件资源准备好,然后打包交给STMMAC核心驱动,最终完成网卡的注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | static int rk_gmac_probe(struct platform_device *pdev)
{
struct plat_stmmacenet_data *plat_dat;
struct stmmac_resources stmmac_res;
const struct rk_gmac_ops *data;
int ret;
// 从设备树匹配当前SOC对应的平台操作集,即取出rk3588_ops
data = of_device_get_match_data(&pdev->dev);
if (!data) {
dev_err(&pdev->dev, "no of match data provided\n");
return -EINVAL;
}
// 解析平台设备硬件资源,获取MAC基地址、中断号等资源
ret = stmmac_get_platform_resources(pdev, &stmmac_res);
if (ret)
return ret;
// 解析设备树通用stmmac属性,phy-mode、phy-handle、clocks、resets等
plat_dat = stmmac_probe_config_dt(pdev, stmmac_res.mac);
if (IS_ERR(plat_dat))
return PTR_ERR(plat_dat);
/* If the stmmac is not already selected as gmac4,
* then make sure we fallback to gmac.
*/
if (!plat_dat->has_gmac4)
plat_dat->has_gmac = true;
plat_dat->sph_disable = true;
// 绑定rk平台特有的回调函数
plat_dat->fix_mac_speed = rk_fix_speed; // 速率变更回调
plat_dat->get_eth_addr = rk_get_eth_addr; // MAC地址获取回调
plat_dat->integrated_phy_power = rk_integrated_phy_power; // PHY电源控制回调
// 初始化rk私有数据,解析tx_delay/rx_delay,获取GRF句柄
plat_dat->bsp_priv = rk_gmac_setup(pdev, plat_dat, data);
if (IS_ERR(plat_dat->bsp_priv)) {
ret = PTR_ERR(plat_dat->bsp_priv);
goto err_remove_config_dt;
}
// 初始化芯片时钟安全单元
rk_gmac_csu_init(plat_dat);
// 获取并配置所有时钟
ret = rk_gmac_clk_init(plat_dat);
if (ret)
goto err_remove_config_dt;
// 以太网硬件上电,完成PHY初始化、网口模式配置、硬件复位
ret = rk_gmac_powerup(plat_dat->bsp_priv);
if (ret)
goto err_remove_config_dt;
// 调用内核STMMAC通用驱动,完成网卡设备内核注册、收发链路初始化
ret = stmmac_dvr_probe(&pdev->dev, plat_dat, &stmmac_res);
if (ret)
goto err_gmac_powerdown;
// 创建自定义sysfs调试节点,支持用户态网口参数调试
ret = dwmac_rk_create_loopback_sysfs(&pdev->dev);
if (ret)
goto err_gmac_powerdown;
return 0;
}
|
9.5.3.4. 硬件资源与设备树解析¶
rk_gmac_setup的任务是从设备树中提取所有与MAC/PHY相关的硬件配置信息,填充到rk_priv_data结构体中,为后续的接口初始化、时钟配置等操作提供数据基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | static struct rk_priv_data *rk_gmac_setup(struct platform_device *pdev,
struct plat_stmmacenet_data *plat,
const struct rk_gmac_ops *ops)
{
struct rk_priv_data *bsp_priv;
struct device *dev = &pdev->dev;
struct resource *res;
int ret;
const char *strings = NULL;
int value;
// 分配rk_priv_data结构体
bsp_priv = devm_kzalloc(dev, sizeof(*bsp_priv), GFP_KERNEL);
if (!bsp_priv)
return ERR_PTR(-ENOMEM);
// 从dts的phy-mode属性读取接口模式
of_get_phy_mode(dev->of_node, &bsp_priv->phy_iface);
// 保存芯片特定的操作集指针,如rk3588_ops
bsp_priv->ops = ops;
/* Some SoCs have multiple MAC controllers, which need
* to be distinguished.
*/
// 获取寄存器资源,对比设备的物理基地址与rk3588_ops.regs[]数组,确定当前Probe的是哪个网口
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res && ops->regs_valid) {
int i = 0;
while (ops->regs[i]) {
if (ops->regs[i] == res->start) {
bsp_priv->id = i;
break;
}
i++;
}
}
// 获取PHY电源管理,后续在rk_gmac_powerup中给PHY上电
bsp_priv->regulator = devm_regulator_get(dev, "phy");
if (IS_ERR(bsp_priv->regulator)) {
ret = PTR_ERR(bsp_priv->regulator);
dev_err_probe(dev, ret, "failed to get phy regulator\n");
return ERR_PTR(ret);
}
// 解析时钟方向,设置input参考时钟由PHY产生并输入给MAC,设置output参考时钟由SoC内部产生并输出给PHY
ret = of_property_read_string(dev->of_node, "clock_in_out", &strings);
if (ret) {
dev_err(dev, "Can not read property: clock_in_out.\n");
bsp_priv->clock_input = true;
} else {
dev_info(dev, "clock input or output? (%s).\n",
strings);
if (!strcmp(strings, "input"))
bsp_priv->clock_input = true;
else
bsp_priv->clock_input = false;
}
// 解析tx_delay延迟值
ret = of_property_read_u32(dev->of_node, "tx_delay", &value);
if (ret) {
bsp_priv->tx_delay = -1;
dev_err(dev, "Can not read property: tx_delay.");
dev_err(dev, "set tx_delay to 0x%x\n",
bsp_priv->tx_delay);
} else {
dev_info(dev, "TX delay(0x%x).\n", value);
bsp_priv->tx_delay = value;
}
// 解析rx_delay延迟值
ret = of_property_read_u32(dev->of_node, "rx_delay", &value);
if (ret) {
bsp_priv->rx_delay = -1;
dev_err(dev, "Can not read property: rx_delay.");
dev_err(dev, "set rx_delay to 0x%x\n",
bsp_priv->rx_delay);
} else {
dev_info(dev, "RX delay(0x%x).\n", value);
bsp_priv->rx_delay = value;
}
// 获取SYS_GRF(系统通用寄存器),用来配置延迟使能、延迟值
bsp_priv->grf = syscon_regmap_lookup_by_phandle(dev->of_node,
"rockchip,grf");
// 获取PHP_GRF(PCIe/PHY子系统通用寄存器),用来配置接口模式、时钟分频
bsp_priv->php_grf = syscon_regmap_lookup_by_phandle(dev->of_node,
"rockchip,php_grf");
bsp_priv->xpcs = syscon_regmap_lookup_by_phandle(dev->of_node,
"rockchip,xpcs");
if (!IS_ERR(bsp_priv->xpcs)) {
struct phy *comphy;
comphy = devm_of_phy_get(&pdev->dev, dev->of_node, NULL);
if (IS_ERR(comphy))
dev_err(dev, "devm_of_phy_get error\n");
ret = phy_init(comphy);
if (ret)
dev_err(dev, "phy_init error\n");
}
if (plat->phy_node) {
bsp_priv->integrated_phy = of_property_read_bool(plat->phy_node,
"phy-is-integrated");
if (bsp_priv->integrated_phy) {
unsigned char *efuse_buf;
struct nvmem_cell *cell;
size_t len;
// 获取PHY复位控制
bsp_priv->phy_reset = of_reset_control_get(plat->phy_node, NULL);
if (IS_ERR(bsp_priv->phy_reset)) {
dev_err(&pdev->dev, "No PHY reset control found.\n");
bsp_priv->phy_reset = NULL;
}
...
}
}
dev_info(dev, "integrated PHY? (%s).\n",
bsp_priv->integrated_phy ? "yes" : "no");
bsp_priv->pdev = pdev;
return bsp_priv;
}
|
9.5.3.5. GRF寄存器配置¶
RK3588的MAC控制器不只有MMIO寄存器,还有大量配置通过GRF(General Register File) 寄存器完成。
9.5.3.5.1. 设置RGMII模式¶
通过操作GRF寄存器选择RGMII接口模式、配置RGMII时钟模式、配置TX/RX延迟,,从而让MAC控制器能够以正确的时序通过RGMII总线与外部PHY进行以太网通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | static void rk3588_set_to_rgmii(struct rk_priv_data *bsp_priv,
int tx_delay, int rx_delay)
{
struct device *dev = &bsp_priv->pdev->dev;
u32 offset_con, id = bsp_priv->id;
if (IS_ERR(bsp_priv->grf) || IS_ERR(bsp_priv->php_grf)) {
dev_err(dev, "Missing rockchip,grf or rockchip,php_grf property\n");
return;
}
// 根据网口ID选择延迟值寄存器
offset_con = bsp_priv->id == 1 ? RK3588_GRF_GMAC_CON9 :
RK3588_GRF_GMAC_CON8;
// 选择PHY接口为RGMII
regmap_write(bsp_priv->php_grf, RK3588_GRF_GMAC_CON0,
RK3588_GMAC_PHY_INTF_SEL_RGMII(id));
// 设置时钟为RGMII模式
regmap_write(bsp_priv->php_grf, RK3588_GRF_CLK_CON1,
RK3588_GMAC_CLK_RGMII_MODE(id));
// 使能TX/RX延迟
regmap_write(bsp_priv->grf, RK3588_GRF_GMAC_CON7,
DELAY_ENABLE_BY_ID(RK3588, tx_delay, rx_delay, id));
// 设置延迟值
regmap_write(bsp_priv->grf, offset_con,
DELAY_VALUE(RK3588, tx_delay, rx_delay));
}
|
9.5.3.5.2. 设置速率¶
当网线插上、PHY自协商完成后,内核根据协商出的速率(10M/100M/1000M),调用此函数切换MAC侧的时钟分频器,使MAC控制器输出的参考时钟频率与PHY芯片在当前速率下所要求的时钟匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | static void rk3588_set_gmac_speed(struct rk_priv_data *bsp_priv, int speed)
{
struct device *dev = &bsp_priv->pdev->dev;
unsigned int val = 0, id = bsp_priv->id;
switch (speed) {
case 10: // 10Mbps
if (bsp_priv->phy_iface == PHY_INTERFACE_MODE_RMII)
val = RK3588_GMA_CLK_RMII_DIV20(id); // RMII: 50MHz / 20 = 2.5MHz
else
val = RK3588_GMAC_CLK_RGMII_DIV50(id); // RGMII: 125MHz / 50 = 2.5MHz
break;
case 100: // 100Mbps
if (bsp_priv->phy_iface == PHY_INTERFACE_MODE_RMII)
val = RK3588_GMA_CLK_RMII_DIV2(id); // RMII: 50MHz / 2 = 25MHz
else
val = RK3588_GMAC_CLK_RGMII_DIV5(id); // RGMII: 125MHz / 5 = 25MHz
break;
case 1000:// 1000Mbps
if (bsp_priv->phy_iface != PHY_INTERFACE_MODE_RMII)
val = RK3588_GMAC_CLK_RGMII_DIV1(id); // RGMII: 125MHz / 1 = 125MHz
else
goto err; // RMII不支持1000M
break;
default:
goto err; // 不支持的速率
}
regmap_write(bsp_priv->php_grf, RK3588_GRF_CLK_CON1, val);
return;
err:
dev_err(dev, "unknown speed value for GMAC speed=%d", speed);
}
|
9.5.4. MAC核心层驱动解析¶
MAC核心层驱动向上对接网络协议栈,向下通过硬件抽象层操作寄存器,横向通过phylink管理PHY。
9.5.4.1. 设备树解析¶
stmmac_probe_config_dt函数是STMMAC平台驱动的设备树解析核心函数,它从设备树中提取MAC控制器的所有硬件配置参数, 包括PHY接口模式、FIFO深度、DMA突发长度、时钟、复位信号等,并将这些信息填充到plat_stmmacenet_data结构体中,供后续核心驱动使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | struct plat_stmmacenet_data *
stmmac_probe_config_dt(struct platform_device *pdev, u8 *mac)
{
struct device_node *np = pdev->dev.of_node;
struct plat_stmmacenet_data *plat;
struct stmmac_dma_cfg *dma_cfg;
int phy_mode;
void *ret;
int rc;
// 分配平台数据结构体,使用devm_管理的内存,驱动卸载时自动释放
plat = devm_kzalloc(&pdev->dev, sizeof(*plat), GFP_KERNEL);
if (!plat)
return ERR_PTR(-ENOMEM);
// 从dts中读取MAC地址
rc = of_get_mac_address(np, mac);
if (rc) {
if (rc == -EPROBE_DEFER)
return ERR_PTR(rc);
eth_zero_addr(mac);
}
// 读取phy-mode属性,对于RK3588对应rgmii-rxid
phy_mode = device_get_phy_mode(&pdev->dev);
if (phy_mode < 0)
return ERR_PTR(phy_mode);
plat->phy_interface = phy_mode;
plat->interface = stmmac_of_get_mac_mode(np);
if (plat->interface < 0)
plat->interface = plat->phy_interface;
// 解析phy-handle属性,获取PHY设备节点引用
plat->phy_node = of_parse_phandle(np, "phy-handle", 0);
// 保存PHYLINK节点,PHYLINK框架会自动解析phy-handle
plat->phylink_node = np;
// 获取最大速率限制
of_property_read_u32(np, "max-speed", &plat->max_speed);
...
// 读取发送FIFO深度
of_property_read_u32(np, "tx-fifo-depth", &plat->tx_fifo_size);
of_property_read_u32(np, "rx-fifo-depth", &plat->rx_fifo_size);
// 获取DMA描述符环大小
of_property_read_u32(np, "tx-dma-size", &plat->dma_tx_size);
of_property_read_u32(np, "rx-dma-size", &plat->dma_rx_size);
...
if (of_device_is_compatible(np, "snps,dwmac-4.00") ||
of_device_is_compatible(np, "snps,dwmac-4.10a") ||
of_device_is_compatible(np, "snps,dwmac-4.20a") || // 匹配dts中的compatible属性,RK3588的GMAC对应snps,dwmac-4.20a
of_device_is_compatible(np, "snps,dwmac-5.10a")) {
plat->has_gmac4 = 1;
plat->has_gmac = 0;
plat->pmt = 1;
plat->tso_en = of_property_read_bool(np, "snps,tso"); //读取snps,tso属性,RK3588 dts中配置了此属性,启用TSO
}
...
dma_cfg = devm_kzalloc(&pdev->dev, sizeof(*dma_cfg),
GFP_KERNEL);
if (!dma_cfg) {
stmmac_remove_config_dt(pdev, plat);
return ERR_PTR(-ENOMEM);
}
plat->dma_cfg = dma_cfg;
of_property_read_u32(np, "snps,pbl", &dma_cfg->pbl);
if (!dma_cfg->pbl)
dma_cfg->pbl = DEFAULT_DMA_PBL;
of_property_read_u32(np, "snps,txpbl", &dma_cfg->txpbl);
of_property_read_u32(np, "snps,rxpbl", &dma_cfg->rxpbl);
dma_cfg->pblx8 = !of_property_read_bool(np, "snps,no-pbl-x8");
dma_cfg->aal = of_property_read_bool(np, "snps,aal");
dma_cfg->fixed_burst = of_property_read_bool(np, "snps,fixed-burst");
dma_cfg->mixed_burst = of_property_read_bool(np, "snps,mixed-burst"); // RK3588 dts中配置了此属性,启用混合突发传输
plat->force_thresh_dma_mode = of_property_read_bool(np, "snps,force_thresh_dma_mode");
if (plat->force_thresh_dma_mode && plat->force_sf_dma_mode) {
plat->force_sf_dma_mode = 0;
dev_warn(&pdev->dev,
"force_sf_dma_mode is ignored if force_thresh_dma_mode is set.\n");
}
of_property_read_u32(np, "snps,ps-speed", &plat->mac_port_sel_speed);
if (of_property_read_u32(np, "snps,flow-ctrl", &plat->flow_ctrl))
plat->flow_ctrl = FLOW_AUTO;
plat->axi = stmmac_axi_setup(pdev); // 解析snps,axi-config子节点,配置AXI总线参数
rc = stmmac_mtl_setup(pdev, plat); // 解析snps,mtl-rx-config/snps,mtl-tx-config子节点,配置多队列参数
if (rc) {
stmmac_remove_config_dt(pdev, plat);
return ERR_PTR(rc);
}
/* clock setup */
if (!of_device_is_compatible(np, "snps,dwc-qos-ethernet-4.10")) {
plat->stmmac_clk = devm_clk_get(&pdev->dev, // 获取主时钟,对应dts中的stmmaceth
STMMAC_RESOURCE_NAME);
if (IS_ERR(plat->stmmac_clk)) {
dev_warn(&pdev->dev, "Cannot get CSR clock\n");
plat->stmmac_clk = NULL;
}
clk_prepare_enable(plat->stmmac_clk);
}
plat->pclk = devm_clk_get_optional(&pdev->dev, "pclk_mac"); // 获取APB总线时钟,对应pclk_mac
if (IS_ERR(plat->pclk)) {
ret = plat->pclk;
goto error_pclk_get;
}
clk_prepare_enable(plat->pclk);
/* Fall-back to main clock in case of no PTP ref is passed */
plat->clk_ptp_ref = devm_clk_get(&pdev->dev, "ptp_ref"); // 获取PTP参考时钟
if (IS_ERR(plat->clk_ptp_ref)) {
plat->clk_ptp_rate = clk_get_rate(plat->stmmac_clk);
plat->clk_ptp_ref = NULL;
dev_info(&pdev->dev, "PTP uses main clock\n");
} else {
plat->clk_ptp_rate = clk_get_rate(plat->clk_ptp_ref);
dev_dbg(&pdev->dev, "PTP rate %d\n", plat->clk_ptp_rate);
}
plat->stmmac_rst = devm_reset_control_get_optional(&pdev->dev, // 获取复位控制器,对应stmmaceth复位信号
STMMAC_RESOURCE_NAME);
if (IS_ERR(plat->stmmac_rst)) {
ret = plat->stmmac_rst;
goto error_hw_init;
}
plat->stmmac_ahb_rst = devm_reset_control_get_optional_shared(
&pdev->dev, "ahb");
if (IS_ERR(plat->stmmac_ahb_rst)) {
ret = plat->stmmac_ahb_rst;
goto error_hw_init;
}
return plat;
}
|
9.5.4.2. 平台资源获取¶
stmmac_get_platform_resources函数用于获取MAC控制器的硬件中断和内存资源。对应RK3588 dts中的interrupts和reg属性。 中断号macirq是MAC的主中断(DMA中断),eth_wake_irq是WoL(Wake-on-LAN)唤醒中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | int stmmac_get_platform_resources(struct platform_device *pdev,
struct stmmac_resources *stmmac_res)
{
memset(stmmac_res, 0, sizeof(*stmmac_res));
// 获取dts中interrupt-names = "macirq" 对应的中断号,RK3588为GIC_SPI 234
stmmac_res->irq = platform_get_irq_byname(pdev, "macirq");
if (stmmac_res->irq < 0)
return stmmac_res->irq;
// 获取唤醒中断,RK3588为GIC_SPI 233,若不存在则回退到主中断
stmmac_res->wol_irq =
platform_get_irq_byname_optional(pdev, "eth_wake_irq");
if (stmmac_res->wol_irq < 0) {
if (stmmac_res->wol_irq == -EPROBE_DEFER)
return -EPROBE_DEFER;
dev_info(&pdev->dev, "IRQ eth_wake_irq not found\n");
stmmac_res->wol_irq = stmmac_res->irq;
}
// 获取LPI低功耗空闲中断
stmmac_res->lpi_irq =
platform_get_irq_byname_optional(pdev, "eth_lpi");
if (stmmac_res->lpi_irq < 0) {
if (stmmac_res->lpi_irq == -EPROBE_DEFER)
return -EPROBE_DEFER;
dev_info(&pdev->dev, "IRQ eth_lpi not found\n");
}
// 将dts中reg = <0x0 0xfe1c0000 0x0 0x10000>映射为内核虚拟地址
stmmac_res->addr = devm_platform_ioremap_resource(pdev, 0);
return PTR_ERR_OR_ZERO(stmmac_res->addr);
}
|
9.5.4.3. probe函数¶
stmmac_dvr_probe函数是STMMAC驱动的核心探测函数,由瑞芯微平台胶水层驱动dwmac-rk.c的rk_gmac_probe调用。 它完成以下工作:分配网络设备、初始化硬件、注册MDIO总线、设置PHYLINK、注册网卡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | int stmmac_dvr_probe(struct device *device,
struct plat_stmmacenet_data *plat_dat,
struct stmmac_resources *res)
{
struct net_device *ndev = NULL;
struct stmmac_priv *priv;
u32 rxq;
int i, ret = 0;
// 分配net_device和私有数据stmmac_priv
ndev = devm_alloc_etherdev_mqs(device, sizeof(struct stmmac_priv),
MTL_MAX_TX_QUEUES, MTL_MAX_RX_QUEUES);
if (!ndev)
return -ENOMEM;
SET_NETDEV_DEV(ndev, device);
priv = netdev_priv(ndev);
priv->device = device;
priv->dev = ndev;
stmmac_set_ethtool_ops(ndev);
priv->pause = pause;
priv->plat = plat_dat;
priv->ioaddr = res->addr; // 保存MAC寄存器基地址,RK3588 GMAC1为0xfe1c0000
priv->dev->base_addr = (unsigned long)res->addr;
priv->plat->dma_cfg->multi_msi_en = priv->plat->multi_msi_en;
priv->dev->irq = res->irq; // MAC的主中断
priv->wol_irq = res->wol_irq; // wol唤醒中断
priv->lpi_irq = res->lpi_irq; // lpi低功耗空闲中断
priv->sfty_ce_irq = res->sfty_ce_irq;
priv->sfty_ue_irq = res->sfty_ue_irq;
for (i = 0; i < MTL_MAX_RX_QUEUES; i++)
priv->rx_irq[i] = res->rx_irq[i];
for (i = 0; i < MTL_MAX_TX_QUEUES; i++)
priv->tx_irq[i] = res->tx_irq[i];
if (!is_zero_ether_addr(res->mac))
eth_hw_addr_set(priv->dev, res->mac); // 设备树MAC地址
dev_set_drvdata(device, priv->dev);
/* Verify driver arguments */
stmmac_verify_args();
priv->af_xdp_zc_qps = bitmap_zalloc(MTL_MAX_TX_QUEUES, GFP_KERNEL);
if (!priv->af_xdp_zc_qps)
return -ENOMEM;
/* Allocate workqueue */
priv->wq = create_singlethread_workqueue("stmmac_wq");
if (!priv->wq) {
dev_err(priv->device, "failed to create workqueue\n");
ret = -ENOMEM;
goto error_wq_init;
}
INIT_WORK(&priv->service_task, stmmac_service_task);
/* Initialize Link Partner FPE workqueue */
INIT_WORK(&priv->fpe_task, stmmac_fpe_lp_task);
/* Override with kernel parameters if supplied XXX CRS XXX
* this needs to have multiple instances
*/
if ((phyaddr >= 0) && (phyaddr <= 31))
priv->plat->phy_addr = phyaddr;
if (priv->plat->stmmac_rst) {
ret = reset_control_assert(priv->plat->stmmac_rst); // 执行MAC硬件复位
reset_control_deassert(priv->plat->stmmac_rst);
/* Some reset controllers have only reset callback instead of
* assert + deassert callbacks pair.
*/
if (ret == -ENOTSUPP)
reset_control_reset(priv->plat->stmmac_rst);
}
ret = reset_control_deassert(priv->plat->stmmac_ahb_rst);
if (ret == -ENOTSUPP)
dev_err(priv->device, "unable to bring out of ahb reset: %pe\n",
ERR_PTR(ret));
/* Wait a bit for the reset to take effect */
udelay(10);
/* Init MAC and get the capabilities */
ret = stmmac_hw_init(priv); //初始化MAC硬件接口,读取硬件能力寄存器
if (ret)
goto error_hw_init;
/* Only DWMAC core version 5.20 onwards supports HW descriptor prefetch.
*/
if (priv->synopsys_id < DWMAC_CORE_5_20)
priv->plat->dma_cfg->dche = false;
stmmac_check_ether_addr(priv);
ndev->netdev_ops = &stmmac_netdev_ops; // 绑定网络设备操作集,open/xmit/stop等
ndev->hw_features = NETIF_F_SG | NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM | // 设置硬件特性
NETIF_F_RXCSUM;
ret = stmmac_tc_init(priv, priv);
if (!ret) {
ndev->hw_features |= NETIF_F_HW_TC;
}
if ((priv->plat->tso_en) && (priv->dma_cap.tsoen)) {
ndev->hw_features |= NETIF_F_TSO | NETIF_F_TSO6; // RK3588配置了snps,tso,启用TSO硬件卸载
if (priv->plat->has_gmac4)
ndev->hw_features |= NETIF_F_GSO_UDP_L4;
priv->tso = true;
dev_info(priv->device, "TSO feature enabled\n");
}
if (priv->dma_cap.sphen && !priv->plat->sph_disable) {
ndev->hw_features |= NETIF_F_GRO;
priv->sph_cap = true;
priv->sph = priv->sph_cap;
dev_info(priv->device, "SPH feature enabled\n");
}
/* Ideally our host DMA address width is the same as for the
* device. However, it may differ and then we have to use our
* host DMA width for allocation and the device DMA width for
* register handling.
*/
if (priv->plat->host_dma_width)
priv->dma_cap.host_dma_width = priv->plat->host_dma_width;
else
priv->dma_cap.host_dma_width = priv->dma_cap.addr64;
if (priv->dma_cap.host_dma_width) {
ret = dma_set_mask_and_coherent(device, // 设置DMA掩码
DMA_BIT_MASK(priv->dma_cap.host_dma_width));
if (!ret) {
dev_info(priv->device, "Using %d/%d bits DMA host/device width\n",
priv->dma_cap.host_dma_width, priv->dma_cap.addr64);
/*
* If more than 32 bits can be addressed, make sure to
* enable enhanced addressing mode.
*/
if (IS_ENABLED(CONFIG_ARCH_DMA_ADDR_T_64BIT))
priv->plat->dma_cfg->eame = true;
} else {
ret = dma_set_mask_and_coherent(device, DMA_BIT_MASK(32));
if (ret) {
dev_err(priv->device, "Failed to set DMA Mask\n");
goto error_hw_init;
}
priv->dma_cap.host_dma_width = 32;
}
}
ndev->features |= ndev->hw_features | NETIF_F_HIGHDMA;
ndev->watchdog_timeo = msecs_to_jiffies(watchdog);
#ifdef STMMAC_VLAN_TAG_USED
/* Both mac100 and gmac support receive VLAN tag detection */
ndev->features |= NETIF_F_HW_VLAN_CTAG_RX | NETIF_F_HW_VLAN_STAG_RX;
if (priv->dma_cap.vlhash) {
ndev->features |= NETIF_F_HW_VLAN_CTAG_FILTER;
ndev->features |= NETIF_F_HW_VLAN_STAG_FILTER;
}
if (priv->dma_cap.vlins) {
ndev->features |= NETIF_F_HW_VLAN_CTAG_TX;
if (priv->dma_cap.dvlan)
ndev->features |= NETIF_F_HW_VLAN_STAG_TX;
}
#endif
priv->msg_enable = netif_msg_init(debug, default_msg_level);
/* Initialize RSS */
rxq = priv->plat->rx_queues_to_use;
netdev_rss_key_fill(priv->rss.key, sizeof(priv->rss.key));
for (i = 0; i < ARRAY_SIZE(priv->rss.table); i++)
priv->rss.table[i] = ethtool_rxfh_indir_default(i, rxq);
if (priv->dma_cap.rssen && priv->plat->rss_en)
ndev->features |= NETIF_F_RXHASH;
/* MTU range: 46 - hw-specific max */
ndev->min_mtu = ETH_ZLEN - ETH_HLEN;
if (priv->plat->has_xgmac)
ndev->max_mtu = XGMAC_JUMBO_LEN;
else if ((priv->plat->enh_desc) || (priv->synopsys_id >= DWMAC_CORE_4_00))
ndev->max_mtu = JUMBO_LEN;
else
ndev->max_mtu = SKB_MAX_HEAD(NET_SKB_PAD + NET_IP_ALIGN);
/* Will not overwrite ndev->max_mtu if plat->maxmtu > ndev->max_mtu
* as well as plat->maxmtu < ndev->min_mtu which is a invalid range.
*/
if ((priv->plat->maxmtu < ndev->max_mtu) &&
(priv->plat->maxmtu >= ndev->min_mtu))
ndev->max_mtu = priv->plat->maxmtu;
else if (priv->plat->maxmtu < ndev->min_mtu)
dev_warn(priv->device,
"%s: warning: maxmtu having invalid value (%d)\n",
__func__, priv->plat->maxmtu);
if (flow_ctrl)
priv->flow_ctrl = FLOW_AUTO; /* RX/TX pause on */
/* Setup channels NAPI */
stmmac_napi_add(ndev); // 为每个队列注册NAPI轮询函数
mutex_init(&priv->lock);
/* If a specific clk_csr value is passed from the platform
* this means that the CSR Clock Range selection cannot be
* changed at run-time and it is fixed. Viceversa the driver'll try to
* set the MDC clock dynamically according to the csr actual
* clock input.
*/
if (priv->plat->clk_csr >= 0)
priv->clk_csr = priv->plat->clk_csr;
else
stmmac_clk_csr_set(priv);
stmmac_check_pcs_mode(priv);
pm_runtime_get_noresume(device);
pm_runtime_set_active(device);
if (!pm_runtime_enabled(device))
pm_runtime_enable(device);
if (priv->hw->pcs != STMMAC_PCS_TBI &&
priv->hw->pcs != STMMAC_PCS_RTBI) {
/* MDIO bus Registration */
ret = stmmac_mdio_register(ndev); // 注册MDIO总线,使内核能扫描到RTL8211F PHY
if (ret < 0) {
dev_err_probe(priv->device, ret,
"%s: MDIO bus (id: %d) registration failed\n",
__func__, priv->plat->bus_id);
goto error_mdio_register;
}
}
if (priv->plat->speed_mode_2500)
priv->plat->speed_mode_2500(ndev, priv->plat->bsp_priv);
if (priv->plat->mdio_bus_data && priv->plat->mdio_bus_data->has_xpcs) {
ret = stmmac_xpcs_setup(priv->mii);
if (ret)
goto error_xpcs_setup;
}
ret = stmmac_phy_setup(priv); // 创建PHYLINK实例,建立MAC与PHY的关联
if (ret) {
netdev_err(ndev, "failed to setup phy (%d)\n", ret);
goto error_phy_setup;
}
ret = phy_register_fixup_for_uid(RTL_8211F_PHY_ID, 0xffffffff, phy_rtl8211f_led_fixup); // 注册RTL8211F的LED配置,PHY匹配后自动调用
if (ret) {
pr_warn("Cannot register 8211f PHY board fixup.\n");
}
ret = register_netdev(ndev); // 向内核注册网卡,此时ifconfig可看到ethX设备
if (ret) {
dev_err(priv->device, "%s: ERROR %i registering the device\n",
__func__, ret);
goto error_netdev_register;
}
#ifdef CONFIG_DEBUG_FS
stmmac_init_fs(ndev);
#endif
if (priv->plat->dump_debug_regs)
priv->plat->dump_debug_regs(priv->plat->bsp_priv);
/* Let pm_runtime_put() disable the clocks.
* If CONFIG_PM is not enabled, the clocks will stay powered.
*/
pm_runtime_put(device);
return ret;
}
|
9.5.4.4. PHYLINK实例创建¶
stmmac_phy_setup函数用于创建PHYLINK实例,PHYLINK是Linux内核中管理MAC与PHY之间链路状态的标准框架。 它根据dts中的phy-mode(RK3588为rgmii-rxid)设置支持的接口模式,并根据硬件能力设置MAC支持的最大速率。 PHYLINK会自动解析phy-handle属性并在后续phylink_start时连接PHY。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | static int stmmac_phy_setup(struct stmmac_priv *priv)
{
struct stmmac_mdio_bus_data *mdio_bus_data = priv->plat->mdio_bus_data;
struct fwnode_handle *fwnode = of_fwnode_handle(priv->plat->phylink_node);
int max_speed = priv->plat->max_speed;
int mode = priv->plat->phy_interface;
struct phylink *phylink;
priv->phylink_config.dev = &priv->dev->dev; // 关联网络设备
priv->phylink_config.type = PHYLINK_NETDEV; // 表示这是一个网络设备类型的PHYLINK
if (priv->plat->mdio_bus_data)
priv->phylink_config.ovr_an_inband =
mdio_bus_data->xpcs_an_inband;
if (!fwnode)
fwnode = dev_fwnode(priv->device);
/* Set the platform/firmware specified interface mode */
__set_bit(mode, priv->phylink_config.supported_interfaces); // 设置支持的接口模式,RK3588为PHY_INTERFACE_MODE_RGMII_RXID
/* If we have an xpcs, it defines which PHY interfaces are supported. */
if (priv->hw->xpcs)
xpcs_get_interfaces(priv->hw->xpcs,
priv->phylink_config.supported_interfaces);
priv->phylink_config.mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE |
MAC_10 | MAC_100;
if (!max_speed || max_speed >= 1000)
priv->phylink_config.mac_capabilities |= MAC_1000; // RK3588 GMAC支持千兆
if (priv->plat->has_gmac4) {
if (!max_speed || max_speed >= 2500)
priv->phylink_config.mac_capabilities |= MAC_2500FD;
} else if (priv->plat->has_xgmac) {
if (!max_speed || max_speed >= 2500)
priv->phylink_config.mac_capabilities |= MAC_2500FD;
if (!max_speed || max_speed >= 5000)
priv->phylink_config.mac_capabilities |= MAC_5000FD;
if (!max_speed || max_speed >= 10000)
priv->phylink_config.mac_capabilities |= MAC_10000FD;
if (!max_speed || max_speed >= 25000)
priv->phylink_config.mac_capabilities |= MAC_25000FD;
if (!max_speed || max_speed >= 40000)
priv->phylink_config.mac_capabilities |= MAC_40000FD;
if (!max_speed || max_speed >= 50000)
priv->phylink_config.mac_capabilities |= MAC_50000FD;
if (!max_speed || max_speed >= 100000)
priv->phylink_config.mac_capabilities |= MAC_100000FD;
}
/* Half-Duplex can only work with single queue */
if (priv->plat->tx_queues_to_use > 1)
priv->phylink_config.mac_capabilities &=
~(MAC_10HD | MAC_100HD | MAC_1000HD);
priv->phylink_config.mac_managed_pm = true;
phylink = phylink_create(&priv->phylink_config, fwnode, // 创建PHYLINK实例,绑定MAC操作回调
mode, &stmmac_phylink_mac_ops);
if (IS_ERR(phylink))
return PTR_ERR(phylink);
priv->phylink = phylink;
return 0;
}
|
9.5.4.5. PHY连接¶
stmmac_init_phy函数负责将PHY设备连接到PHYLINK。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | static int stmmac_init_phy(struct net_device *dev)
{
struct stmmac_priv *priv = netdev_priv(dev);
struct fwnode_handle *phy_fwnode;
struct fwnode_handle *fwnode;
int ret;
if (!phylink_expects_phy(priv->phylink)) // 检查是否需要外部PHY
return 0;
if (priv->plat->integrated_phy_power)
ret = priv->plat->integrated_phy_power(priv->plat->bsp_priv, true);
fwnode = of_fwnode_handle(priv->plat->phylink_node);
if (!fwnode)
fwnode = dev_fwnode(priv->device);
if (fwnode)
phy_fwnode = fwnode_get_phy_node(fwnode); // 从phy-handle属性获取PHY节点
else
phy_fwnode = NULL;
/* Some DT bindings do not set-up the PHY handle. Let's try to
* manually parse it
*/
if (!phy_fwnode || IS_ERR(phy_fwnode)) {
int addr = priv->plat->phy_addr;
struct phy_device *phydev;
if (addr < 0) {
netdev_err(priv->dev, "no phy found\n");
return -ENODEV;
}
phydev = mdiobus_get_phy(priv->mii, addr);
if (!phydev) {
netdev_err(priv->dev, "no phy at addr %d\n", addr);
return -ENODEV;
}
ret = phylink_connect_phy(priv->phylink, phydev); // 备选路径,通过MDIO地址直接连接
} else {
fwnode_handle_put(phy_fwnode);
ret = phylink_fwnode_phy_connect(priv->phylink, fwnode, 0); // 通过fwnode连接PHY,触发PHY驱动的probe和config_init
}
if (!priv->plat->pmt) {
struct ethtool_wolinfo wol = { .cmd = ETHTOOL_GWOL };
phylink_ethtool_get_wol(priv->phylink, &wol);
device_set_wakeup_capable(priv->device, !!wol.supported);
device_set_wakeup_enable(priv->device, !!wol.wolopts);
}
return ret;
}
|
对于RK3588S + RTL8211F方案,DTS中配置了phy-handle = <&rgmii_phy1>,因此走phylink_fwnode_phy_connect路径。 连接成功后,PHY驱动的config_init(即rtl8211f_config_init)会在链路启动时被调用。
9.5.4.6. 链路建立回调¶
stmmac_mac_link_up函数是PHYLINK框架在链路协商完成后回调的函数。当RTL8211F自协商完成,PHYLINK调用此函数配置MAC硬件。 它设置MAC控制寄存器中的速率位,并调用平台层的fix_mac_speed回调(即dwmac-rk.c中的rk_fix_speed)来配置GRF时钟分频。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | static void stmmac_mac_link_up(struct phylink_config *config,
struct phy_device *phy,
unsigned int mode, phy_interface_t interface,
int speed, int duplex,
bool tx_pause, bool rx_pause)
{
struct stmmac_priv *priv = netdev_priv(to_net_dev(config->dev));
u32 old_ctrl, ctrl;
old_ctrl = readl(priv->ioaddr + MAC_CTRL_REG); // 读取当前MAC控制寄存器
ctrl = old_ctrl & ~priv->hw->link.speed_mask;
if (interface == PHY_INTERFACE_MODE_USXGMII) {
switch (speed) {
case SPEED_10000:
ctrl |= priv->hw->link.xgmii.speed10000;
break;
case SPEED_5000:
ctrl |= priv->hw->link.xgmii.speed5000;
break;
case SPEED_2500:
ctrl |= priv->hw->link.xgmii.speed2500;
break;
default:
return;
}
} else if (interface == PHY_INTERFACE_MODE_XLGMII) {
switch (speed) {
case SPEED_100000:
ctrl |= priv->hw->link.xlgmii.speed100000;
break;
case SPEED_50000:
ctrl |= priv->hw->link.xlgmii.speed50000;
break;
case SPEED_40000:
ctrl |= priv->hw->link.xlgmii.speed40000;
break;
case SPEED_25000:
ctrl |= priv->hw->link.xlgmii.speed25000;
break;
case SPEED_10000:
ctrl |= priv->hw->link.xgmii.speed10000;
break;
case SPEED_2500:
ctrl |= priv->hw->link.speed2500;
break;
case SPEED_1000:
ctrl |= priv->hw->link.speed1000; // 设置千兆速率位
break;
default:
return;
}
} else {
switch (speed) {
case SPEED_2500:
ctrl |= priv->hw->link.speed2500;
break;
case SPEED_1000:
ctrl |= priv->hw->link.speed1000;
break;
case SPEED_100:
ctrl |= priv->hw->link.speed100;
break;
case SPEED_10:
ctrl |= priv->hw->link.speed10;
break;
default:
return;
}
}
priv->speed = speed;
if (priv->plat->fix_mac_speed)
priv->plat->fix_mac_speed(priv->plat->bsp_priv, speed); // 触发rk_fix_speed,配置PHP_GRF时钟分频
if (!duplex)
ctrl &= ~priv->hw->link.duplex;
else
ctrl |= priv->hw->link.duplex;
/* Flow Control operation */
if (rx_pause && tx_pause)
priv->flow_ctrl = FLOW_AUTO;
else if (rx_pause && !tx_pause)
priv->flow_ctrl = FLOW_RX;
else if (!rx_pause && tx_pause)
priv->flow_ctrl = FLOW_TX;
else
priv->flow_ctrl = FLOW_OFF;
stmmac_mac_flow_ctrl(priv, duplex); // 配置流控
if (ctrl != old_ctrl)
writel(ctrl, priv->ioaddr + MAC_CTRL_REG); // 写入新的MAC控制寄存器
stmmac_mac_set(priv, priv->ioaddr, true); // 使能MAC收发
if (phy && priv->dma_cap.eee) {
priv->eee_active =
phy_init_eee(phy, !priv->plat->rx_clk_runs_in_lpi) >= 0;
priv->eee_enabled = stmmac_eee_init(priv);
priv->tx_lpi_enabled = priv->eee_enabled;
stmmac_set_eee_pls(priv, priv->hw, true);
}
if (priv->dma_cap.fpesel)
stmmac_fpe_link_state_handle(priv, true);
}
|
9.5.4.7. 网络设备操作集¶
stmmac_netdev_ops定义了STMMAC网卡驱动向内核网络子系统暴露的所有操作接口。 当用户执行ifconfig eth0 up时调用ndo_open,发送数据时调用ndo_start_xmit,ethtool命令通过ndo_eth_ioctl处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static const struct net_device_ops stmmac_netdev_ops = {
.ndo_open = stmmac_open, // ifconfig up / ip link set up时调用
.ndo_start_xmit = stmmac_xmit, // 发送报文时调用
.ndo_stop = stmmac_release, // ifconfig down / ip link set down时调用
.ndo_change_mtu = stmmac_change_mtu, // 修改MTU时调用
.ndo_fix_features = stmmac_fix_features,
.ndo_set_features = stmmac_set_features,
.ndo_set_rx_mode = stmmac_set_rx_mode,
.ndo_tx_timeout = stmmac_tx_timeout, // 发送超时时调用
.ndo_eth_ioctl = stmmac_ioctl, // ethtool/ioctl
.ndo_setup_tc = stmmac_setup_tc,
.ndo_select_queue = stmmac_select_queue, // 多队列时选择发送队列
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = stmmac_poll_controller,
#endif
.ndo_set_mac_address = stmmac_set_mac_address, // 设置MAC地址
.ndo_vlan_rx_add_vid = stmmac_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = stmmac_vlan_rx_kill_vid,
.ndo_bpf = stmmac_bpf,
.ndo_xdp_xmit = stmmac_xdp_xmit,
.ndo_xsk_wakeup = stmmac_xsk_wakeup,
};
|
9.5.4.8. 打开网卡¶
stmmac_open函数是ifconfig up的入口。它分配DMA描述符环,然后调用__stmmac_open函数完成连接PHY、初始化硬件、启动PHYLINK、请求中断、使能NAPI和DMA。对于RK3588S + RTL8211F,stmmac_init_phy会连接RTL8211F,phylink_start会触发PHY自协商。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | static int stmmac_open(struct net_device *dev)
{
struct stmmac_priv *priv = netdev_priv(dev);
struct stmmac_dma_conf *dma_conf;
int ret;
// 根据MTU分配DMA描述符环和接收缓冲区
dma_conf = stmmac_setup_dma_desc(priv, dev->mtu);
if (IS_ERR(dma_conf))
return PTR_ERR(dma_conf);
// 调用__stmmac_open
ret = __stmmac_open(dev, dma_conf);
if (ret)
free_dma_desc_resources(priv, dma_conf);
kfree(dma_conf);
return ret;
}
static int __stmmac_open(struct net_device *dev,
struct stmmac_dma_conf *dma_conf)
{
struct stmmac_priv *priv = netdev_priv(dev);
int mode = priv->plat->phy_interface;
u32 chan;
int ret;
ret = pm_runtime_resume_and_get(priv->device);
if (ret < 0)
return ret;
if (priv->hw->pcs != STMMAC_PCS_TBI &&
priv->hw->pcs != STMMAC_PCS_RTBI &&
(!priv->hw->xpcs ||
xpcs_get_an_mode(priv->hw->xpcs, mode) != DW_AN_C73)) {
// 连接PHY
ret = stmmac_init_phy(dev);
if (ret) {
netdev_err(priv->dev,
"%s: Cannot attach to PHY (error: %d)\n",
__func__, ret);
goto init_phy_error;
}
}
/* Extra statistics */
memset(&priv->xstats, 0, sizeof(struct stmmac_extra_stats));
priv->xstats.threshold = tc;
priv->rx_copybreak = STMMAC_RX_COPYBREAK;
buf_sz = dma_conf->dma_buf_sz;
for (int i = 0; i < MTL_MAX_TX_QUEUES; i++)
if (priv->dma_conf.tx_queue[i].tbs & STMMAC_TBS_EN)
dma_conf->tx_queue[i].tbs = priv->dma_conf.tx_queue[i].tbs;
memcpy(&priv->dma_conf, dma_conf, sizeof(*dma_conf));
stmmac_reset_queues_param(priv);
if (priv->plat->serdes_powerup) {
ret = priv->plat->serdes_powerup(dev, priv->plat->bsp_priv);
if (ret < 0) {
netdev_err(priv->dev, "%s: Serdes powerup failed\n",
__func__);
goto init_error;
}
}
// 初始化MAC硬件
ret = stmmac_hw_setup(dev, true);
if (ret < 0) {
netdev_err(priv->dev, "%s: Hw setup failed\n", __func__);
goto init_error;
}
stmmac_init_coalesce(priv);
// 启动PHYLINK状态机,触发PHY自协商,协商完成后回调stmmac_mac_link_up
phylink_start(priv->phylink);
// 通知PHYLINK链路可能已就绪
phylink_speed_up(priv->phylink);
// 请求中断,DMA中断、WoL中断等
ret = stmmac_request_irq(dev);
if (ret)
goto irq_error;
// 使能所有NAPI队列
stmmac_enable_all_queues(priv);
// 通知协议栈可以开始发包
netif_tx_start_all_queues(priv->dev);
// 使能DMA中断
stmmac_enable_all_dma_irq(priv);
return 0;
}
|
9.5.4.9. 关闭网卡¶
stmmac_release函数是ifconfig down的入口,执行与stmmac_open相反的操作:停止PHYLINK、断开PHY、禁用NAPI、释放中断、停止DMA、释放描述符资源、关闭MAC。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | static int stmmac_release(struct net_device *dev)
{
struct stmmac_priv *priv = netdev_priv(dev);
u32 chan;
if (device_may_wakeup(priv->device))
phylink_speed_down(priv->phylink, false);
// 停止PHYLINK状态机
phylink_stop(priv->phylink);
// 断开PHY连接
phylink_disconnect_phy(priv->phylink);
if (priv->plat->integrated_phy_power)
priv->plat->integrated_phy_power(priv->plat->bsp_priv, false);
// 禁用所有NAPI
stmmac_disable_all_queues(priv);
for (chan = 0; chan < priv->plat->tx_queues_to_use; chan++)
hrtimer_cancel(&priv->dma_conf.tx_queue[chan].txtimer);
// 停止所有发送队列
netif_tx_disable(dev);
/* Free the IRQ lines */
stmmac_free_irq(dev, REQ_IRQ_ERR_ALL, 0);
if (priv->eee_enabled) {
priv->tx_path_in_lpi_mode = false;
del_timer_sync(&priv->eee_ctrl_timer);
}
// 停止所有DMA通道
stmmac_stop_all_dma(priv);
/* Release and free the Rx/Tx resources */
free_dma_desc_resources(priv, &priv->dma_conf);
// 关闭MAC收发
stmmac_mac_set(priv, priv->ioaddr, false);
/* Powerdown Serdes if there is */
if (priv->plat->serdes_powerdown)
priv->plat->serdes_powerdown(dev, priv->plat->bsp_priv);
// 通知协议栈链路断开
netif_carrier_off(dev);
stmmac_release_ptp(priv);
pm_runtime_put(priv->device);
if (priv->dma_cap.fpesel)
stmmac_fpe_stop_wq(priv);
return 0;
}
|
9.5.4.10. 发送入口¶
stmmac_xmit函数是发送路径的核心函数,由协议栈通过ndo_start_xmit调用。 它将sk_buff的数据进行DMA映射,填充TX描述符环,设置OWN位将描述符交给DMA引擎,最后触发DMA发送。 对于TSO(TCP分段卸载)报文,会转到stmmac_tso_xmit处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | static netdev_tx_t stmmac_xmit(struct sk_buff *skb, struct net_device *dev)
{
unsigned int first_entry, tx_packets, enh_desc;
struct stmmac_priv *priv = netdev_priv(dev);
unsigned int nopaged_len = skb_headlen(skb);
int i, csum_insertion = 0, is_jumbo = 0;
u32 queue = skb_get_queue_mapping(skb);
int nfrags = skb_shinfo(skb)->nr_frags;
int gso = skb_shinfo(skb)->gso_type;
struct dma_edesc *tbs_desc = NULL;
struct dma_desc *desc, *first;
struct stmmac_tx_queue *tx_q;
bool has_vlan, set_ic;
int entry, first_tx;
dma_addr_t des;
tx_q = &priv->dma_conf.tx_queue[queue]; // 获取对应队列的TX队列配置结构
first_tx = tx_q->cur_tx; // 记录本次发送操作起始的描述符索引,用于后续统计
if (priv->tx_path_in_lpi_mode && priv->eee_sw_timer_en)
stmmac_disable_eee_mode(priv);
// 检测是否为TSO报文,RK3588支持TSO
if (skb_is_gso(skb) && priv->tso) {
if (gso & (SKB_GSO_TCPV4 | SKB_GSO_TCPV6))
return stmmac_tso_xmit(skb, dev);
if (priv->plat->has_gmac4 && (gso & SKB_GSO_UDP_L4))
return stmmac_tso_xmit(skb, dev);
}
// 检查TX描述符环是否有足够空间
if (unlikely(stmmac_tx_avail(priv, queue) < nfrags + 1)) {
if (!netif_tx_queue_stopped(netdev_get_tx_queue(dev, queue))) {
netif_tx_stop_queue(netdev_get_tx_queue(priv->dev, // 空间不足时停止发送队列
queue));
/* This is a hard error, log it. */
netdev_err(priv->dev,
"%s: Tx Ring full when queue awake\n",
__func__);
}
// 返回NETDEV_TX_BUSY,通知网络栈稍后重试发送
return NETDEV_TX_BUSY;
}
/* Check if VLAN can be inserted by HW */
has_vlan = stmmac_vlan_insert(priv, skb, tx_q);
entry = tx_q->cur_tx; // 获取当前可用的描述符索引
first_entry = entry; // 记录第一个描述符的索引,后续需要对其设置OWN位
WARN_ON(tx_q->tx_skbuff[first_entry]);
csum_insertion = (skb->ip_summed == CHECKSUM_PARTIAL);
// 根据描述符类型获取第一个描述符的内存指针
if (likely(priv->extend_desc))
desc = (struct dma_desc *)(tx_q->dma_etx + entry);
else if (tx_q->tbs & STMMAC_TBS_AVAIL)
desc = &tx_q->dma_entx[entry].basic;
else
desc = tx_q->dma_tx + entry;
first = desc; // 保存第一个描述符的指针,后续统一设置OWN位以触发DMA
if (has_vlan)
stmmac_set_desc_vlan(priv, first, STMMAC_VLAN_INSERT);
enh_desc = priv->plat->enh_desc;
/* To program the descriptors according to the size of the frame */
if (enh_desc)
is_jumbo = stmmac_is_jumbo_frm(priv, skb->len, enh_desc);
if (unlikely(is_jumbo)) {
entry = stmmac_jumbo_frm(priv, tx_q, skb, csum_insertion);
if (unlikely(entry < 0) && (entry != -EINVAL))
goto dma_map_err;
}
// 遍历skb的所有分片,为每个分片映射DMA并设置对应的描述符
for (i = 0; i < nfrags; i++) {
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
int len = skb_frag_size(frag);
bool last_segment = (i == (nfrags - 1));
// 计算下一个描述符的索引
entry = STMMAC_GET_ENTRY(entry, priv->dma_conf.dma_tx_size);
WARN_ON(tx_q->tx_skbuff[entry]);
// 获取当前分片对应的描述符指针
if (likely(priv->extend_desc))
desc = (struct dma_desc *)(tx_q->dma_etx + entry);
else if (tx_q->tbs & STMMAC_TBS_AVAIL)
desc = &tx_q->dma_entx[entry].basic;
else
desc = tx_q->dma_tx + entry;
// 将分片数据映射为DMA物理地址
des = skb_frag_dma_map(priv->device, frag, 0, len,
DMA_TO_DEVICE);
if (dma_mapping_error(priv->device, des))
goto dma_map_err; /* should reuse desc w/o issues */
// 在TX队列的DMA信息数组中保存映射后的DMA地址
tx_q->tx_skbuff_dma[entry].buf = des;
// 将DMA地址写入硬件描述符
stmmac_set_desc_addr(priv, desc, des);
// 设置DMA映射元数据:标记为页映射、记录长度、是否为最后一段、缓冲区类型为SKB
tx_q->tx_skbuff_dma[entry].map_as_page = true;
tx_q->tx_skbuff_dma[entry].len = len;
tx_q->tx_skbuff_dma[entry].last_segment = last_segment;
tx_q->tx_skbuff_dma[entry].buf_type = STMMAC_TXBUF_T_SKB;
// 填充描述符,地址、长度、校验和卸载标志等
stmmac_prepare_tx_desc(priv, desc, 0, len, csum_insertion,
priv->mode, 1, last_segment, skb->len);
}
/* Only the last descriptor gets to point to the skb. */
// 只有最后一个描述符对应的数组位置保存skb指针,用于发送完成后在TX Clean中释放skb
tx_q->tx_skbuff[entry] = skb;
tx_q->tx_skbuff_dma[entry].buf_type = STMMAC_TXBUF_T_SKB;
/* According to the coalesce parameter the IC bit for the latest
* segment is reset and the timer re-started to clean the tx status.
* This approach takes care about the fragments: desc is the first
* element in case of no SG.
*/
tx_packets = (entry + 1) - first_tx;
tx_q->tx_count_frames += tx_packets;
if ((skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP) && priv->hwts_tx_en)
set_ic = true;
else if (!priv->tx_coal_frames[queue])
set_ic = false;
else if (tx_packets > priv->tx_coal_frames[queue])
set_ic = true;
else if ((tx_q->tx_count_frames %
priv->tx_coal_frames[queue]) < tx_packets)
set_ic = true;
else
set_ic = false;
if (set_ic) {
if (likely(priv->extend_desc))
desc = &tx_q->dma_etx[entry].basic;
else if (tx_q->tbs & STMMAC_TBS_AVAIL)
desc = &tx_q->dma_entx[entry].basic;
else
desc = &tx_q->dma_tx[entry];
tx_q->tx_count_frames = 0;
stmmac_set_tx_ic(priv, desc);
priv->xstats.tx_set_ic_bit++;
}
/* We've used all descriptors we need for this skb, however,
* advance cur_tx so that it references a fresh descriptor.
* ndo_start_xmit will fill this descriptor the next time it's
* called and stmmac_tx_clean may clean up to this descriptor.
*/
// 推进cur_tx索引,使其指向下一个空闲的描述符
entry = STMMAC_GET_ENTRY(entry, priv->dma_conf.dma_tx_size);
tx_q->cur_tx = entry;
if (netif_msg_pktdata(priv)) {
netdev_dbg(priv->dev,
"%s: curr=%d dirty=%d f=%d, e=%d, first=%p, nfrags=%d",
__func__, tx_q->cur_tx, tx_q->dirty_tx, first_entry,
entry, first, nfrags);
netdev_dbg(priv->dev, ">>> frame to be transmitted: ");
print_pkt(skb->data, skb->len);
}
// 检查剩余可用描述符数量,如果小于等于最大分片数加 1,则必须停止队列
if (unlikely(stmmac_tx_avail(priv, queue) <= (MAX_SKB_FRAGS + 1))) {
netif_dbg(priv, hw, priv->dev, "%s: stop transmitted packets\n",
__func__);
// 停止网络栈对该队列的发送,直到TX清理函数释放足够的描述符后再唤醒
netif_tx_stop_queue(netdev_get_tx_queue(priv->dev, queue));
}
// 累加网卡设备的发送字节数统计
dev->stats.tx_bytes += skb->len;
if (priv->sarc_type)
stmmac_set_desc_sarc(priv, first, priv->sarc_type);
skb_tx_timestamp(skb);
/* Ready to fill the first descriptor and set the OWN bit w/o any
* problems because all the descriptors are actually ready to be
* passed to the DMA engine.
*/
// 如果不是巨型帧,则处理线性数据部分
if (likely(!is_jumbo)) {
bool last_segment = (nfrags == 0);
// 将skb的线性数据部分映射为DMA物理地址
des = dma_map_single(priv->device, skb->data,
nopaged_len, DMA_TO_DEVICE);
if (dma_mapping_error(priv->device, des))
goto dma_map_err;
// 保存DMA地址、缓冲区类型、映射方式
tx_q->tx_skbuff_dma[first_entry].buf = des;
tx_q->tx_skbuff_dma[first_entry].buf_type = STMMAC_TXBUF_T_SKB;
tx_q->tx_skbuff_dma[first_entry].map_as_page = false;
// 将DMA地址写入第一个描述符
stmmac_set_desc_addr(priv, first, des);
// 保存长度和是否为最后一段的标志
tx_q->tx_skbuff_dma[first_entry].len = nopaged_len;
tx_q->tx_skbuff_dma[first_entry].last_segment = last_segment;
if (unlikely((skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP) &&
priv->hwts_tx_en)) {
/* declare that device is doing timestamping */
skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
stmmac_enable_tx_timestamp(priv, first);
}
/* Prepare the first descriptor setting the OWN bit too */
stmmac_prepare_tx_desc(priv, first, 1, nopaged_len,
csum_insertion, priv->mode, 0, last_segment,
skb->len);
}
if (tx_q->tbs & STMMAC_TBS_EN) {
struct timespec64 ts = ns_to_timespec64(skb->tstamp);
tbs_desc = &tx_q->dma_entx[first_entry];
stmmac_set_desc_tbs(priv, tbs_desc, ts.tv_sec, ts.tv_nsec);
}
// 设置OWN位,将描述符所有权交给DMA硬件
stmmac_set_tx_owner(priv, first);
// 通知网络栈已入队的数据包字节数,用于动态队列限流
netdev_tx_sent_queue(netdev_get_tx_queue(dev, queue), skb->len);
// 使能DMA发送
stmmac_enable_dma_transmission(priv, priv->ioaddr);
// 更新TX尾指针寄存器,触发DMA引擎开始搬运
stmmac_flush_tx_descriptors(priv, queue);
// 启动TX清理定时器
stmmac_tx_timer_arm(priv, queue);
return NETDEV_TX_OK;
dma_map_err:
netdev_err(priv->dev, "Tx DMA map failed\n");
dev_kfree_skb(skb);
priv->dev->stats.tx_dropped++;
return NETDEV_TX_OK;
}
|
9.5.4.11. 发送完成清理¶
stmmac_tx_clean函数由NAPI轮询函数stmmac_napi_poll_tx调用,负责回收已完成发送的TX描述符和skb。 它遍历从dirty_tx到cur_tx的描述符,检查DMA是否已完成(OWN位是否已清除),然后解除DMA映射、释放skb、更新统计信息。 如果之前队列被停止且现在有足够空间,则唤醒队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | static int stmmac_tx_clean(struct stmmac_priv *priv, int budget, u32 queue)
{
struct stmmac_tx_queue *tx_q = &priv->dma_conf.tx_queue[queue];
unsigned int bytes_compl = 0, pkts_compl = 0;
unsigned int entry, xmits = 0, count = 0;
// 获取TX队列的锁,并禁用软中断
__netif_tx_lock_bh(netdev_get_tx_queue(priv->dev, queue));
// 增加TX清理操作的统计计数
priv->xstats.tx_clean++;
tx_q->xsk_frames_done = 0;
// 从脏描述符索引(dirty_tx)开始遍历,这是上次清理停止的位置
entry = tx_q->dirty_tx;
/* Try to clean all TX complete frame in 1 shot */
// 循环遍历描述符环,直到追上当前发送指针(cur_tx)或达到环的最大大小
while ((entry != tx_q->cur_tx) && count < priv->dma_conf.dma_tx_size) {
struct xdp_frame *xdpf;
struct sk_buff *skb;
struct dma_desc *p;
int status;
// 根据缓冲区类型判断是XDP帧还是普通的SKB
if (tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_XDP_TX ||
tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_XDP_NDO) {
xdpf = tx_q->xdpf[entry];
skb = NULL;
} else if (tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_SKB) {
xdpf = NULL;
skb = tx_q->tx_skbuff[entry]; // 如果是普通的网络栈SKB,获取sk_buff指针
} else {
xdpf = NULL;
skb = NULL;
}
// 根据硬件配置获取对应的DMA描述符指针
if (priv->extend_desc)
p = (struct dma_desc *)(tx_q->dma_etx + entry);
else if (tx_q->tbs & STMMAC_TBS_AVAIL)
p = &tx_q->dma_entx[entry].basic;
else
p = tx_q->dma_tx + entry;
// 读取描述符的状态字,包含DMA引擎写入的发送结果
status = stmmac_tx_status(priv, &priv->dev->stats,
&priv->xstats, p, priv->ioaddr);
// 检查OWN位,如果DMA仍然拥有该描述符,说明还没发送完,则跳出循环
if (unlikely(status & tx_dma_own))
break;
// 增加已处理的描述符计数
count++;
/* Make sure descriptor fields are read after reading
* the own bit.
*/
dma_rmb();
/* Just consider the last segment and ...*/
// 只有当该描述符是数据包的最后一个分段时,才进行完整的状态统计
if (likely(!(status & tx_not_ls))) {
/* ... verify the status error condition */
if (unlikely(status & tx_err)) {
// 如果有错误,增加设备的TX错误统计
priv->dev->stats.tx_errors++;
if (unlikely(status & tx_err_bump_tc))
stmmac_bump_dma_threshold(priv, queue);
} else {
// 发送成功,增加成功包数统计
priv->dev->stats.tx_packets++;
priv->xstats.tx_pkt_n++;
priv->xstats.txq_stats[queue].tx_pkt_n++;
}
// 如果是SKB且请求了硬件时间戳,则从描述符中提取并传递给网络栈
if (skb)
stmmac_get_tx_hwtstamp(priv, p, skb);
}
// 如果该描述符映射了DMA缓冲区
if (likely(tx_q->tx_skbuff_dma[entry].buf &&
tx_q->tx_skbuff_dma[entry].buf_type != STMMAC_TXBUF_T_XDP_TX)) {
// 根据映射方式解除DMA映射
if (tx_q->tx_skbuff_dma[entry].map_as_page)
dma_unmap_page(priv->device,
tx_q->tx_skbuff_dma[entry].buf,
tx_q->tx_skbuff_dma[entry].len,
DMA_TO_DEVICE);
else
dma_unmap_single(priv->device,
tx_q->tx_skbuff_dma[entry].buf,
tx_q->tx_skbuff_dma[entry].len,
DMA_TO_DEVICE);
// 清空DMA缓冲区相关的元数据
tx_q->tx_skbuff_dma[entry].buf = 0;
tx_q->tx_skbuff_dma[entry].len = 0;
tx_q->tx_skbuff_dma[entry].map_as_page = false;
}
// 清理描述符的扩展字段
stmmac_clean_desc3(priv, tx_q, p);
// 重置该描述符的元数据标志
tx_q->tx_skbuff_dma[entry].last_segment = false;
tx_q->tx_skbuff_dma[entry].is_jumbo = false;
if (xdpf &&
tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_XDP_TX) {
xdp_return_frame_rx_napi(xdpf);
tx_q->xdpf[entry] = NULL;
}
if (xdpf &&
tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_XDP_NDO) {
xdp_return_frame(xdpf);
tx_q->xdpf[entry] = NULL;
}
if (tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_XSK_TX)
tx_q->xsk_frames_done++;
// 如果是普通的SKB发送
if (tx_q->tx_skbuff_dma[entry].buf_type == STMMAC_TXBUF_T_SKB) {
if (likely(skb)) {
// 累加完成的包数和字节数
pkts_compl++;
bytes_compl += skb->len;
dev_consume_skb_any(skb); // 释放SKB内存
tx_q->tx_skbuff[entry] = NULL;
}
}
// 释放描述符,将其重置为初始状态,清除OWN位等,准备下次使用
stmmac_release_tx_desc(priv, p, priv->mode);
// 计算下一个描述符的索引
entry = STMMAC_GET_ENTRY(entry, priv->dma_conf.dma_tx_size);
}
// 更新脏描述符索引,记录下次清理的起点
tx_q->dirty_tx = entry;
// 通知网络栈当前队列已完成的包数和字节数,以动态调整队列限流
netdev_tx_completed_queue(netdev_get_tx_queue(priv->dev, queue),
pkts_compl, bytes_compl);
// 如果该TX队列之前因为环满被停止,且现在可用空间大于阈值,则唤醒队列
if (unlikely(netif_tx_queue_stopped(netdev_get_tx_queue(priv->dev,
queue))) &&
stmmac_tx_avail(priv, queue) > STMMAC_TX_THRESH(priv)) {
netif_dbg(priv, tx_done, priv->dev,
"%s: restart transmit\n", __func__);
// 唤醒网络栈,允许其继续向该队列下发数据包
netif_tx_wake_queue(netdev_get_tx_queue(priv->dev, queue));
}
if (tx_q->xsk_pool) {
bool work_done;
if (tx_q->xsk_frames_done)
xsk_tx_completed(tx_q->xsk_pool, tx_q->xsk_frames_done);
if (xsk_uses_need_wakeup(tx_q->xsk_pool))
xsk_set_tx_need_wakeup(tx_q->xsk_pool);
/* For XSK TX, we try to send as many as possible.
* If XSK work done (XSK TX desc empty and budget still
* available), return "budget - 1" to reenable TX IRQ.
* Else, return "budget" to make NAPI continue polling.
*/
work_done = stmmac_xdp_xmit_zc(priv, queue,
STMMAC_XSK_TX_BUDGET_MAX);
if (work_done)
xmits = budget - 1;
else
xmits = budget;
}
if (priv->eee_enabled && !priv->tx_path_in_lpi_mode &&
priv->eee_sw_timer_en) {
if (stmmac_enable_eee_mode(priv))
mod_timer(&priv->eee_ctrl_timer, STMMAC_LPI_T(priv->tx_lpi_timer));
}
/* We still have pending packets, let's call for a new scheduling */
// 如果还有未清理的描述符(dirty_tx没追上cur_tx),说明DMA还在发送或刚发完还没处理
if (tx_q->dirty_tx != tx_q->cur_tx)
// 启动TX清理定时器,确保即使没有中断也能在稍后继续清理,防止描述符耗尽导致死锁
stmmac_tx_timer_arm(priv, queue);
// 释放TX队列锁,恢复软中断
__netif_tx_unlock_bh(netdev_get_tx_queue(priv->dev, queue));
/* Combine decisions from TX clean and XSK TX */
// 返回本次NAPI poll处理的工作量
return max(count, xmits);
}
|
9.5.4.12. 主中断处理函数¶
stmmac_interrupt函数是STMMAC驱动的主中断服务函数,对应dts中的macirq中断(RK3588为GIC_SPI 234)。它处理安全特性中断、通用中断(链路状态、时间戳等)和DMA中断(收发完成)。DMA中断会触发NAPI调度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static irqreturn_t stmmac_interrupt(int irq, void *dev_id)
{
struct net_device *dev = (struct net_device *)dev_id;
struct stmmac_priv *priv = netdev_priv(dev);
// 若设备已关闭,直接返回
if (test_bit(STMMAC_DOWN, &priv->state))
return IRQ_HANDLED;
// 处理安全特性中断
if (stmmac_safety_feat_interrupt(priv))
return IRQ_HANDLED;
// 处理通用中断
stmmac_common_interrupt(priv);
// 处理DMA中断,调用stmmac_napi_check检查各通道状态并调度NAPI
stmmac_dma_interrupt(priv);
return IRQ_HANDLED;
}
|
9.5.4.13. RX NAPI轮询¶
stmmac_napi_poll_rx函数是RX通道的NAPI轮询函数。DMA接收中断触发后,内核调度此函数在软中断上下文中批量处理接收报文。 它调用stmmac_rx处理报文,若处理量小于budget则调用napi_complete_done退出轮询模式并重新使能RX中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static int stmmac_napi_poll_rx(struct napi_struct *napi, int budget)
{
struct stmmac_channel *ch =
container_of(napi, struct stmmac_channel, rx_napi);
struct stmmac_priv *priv = ch->priv_data;
u32 chan = ch->index;
int work_done;
priv->xstats.napi_poll++;
// 调用接收核心函数,最多处理budget个包
work_done = stmmac_rx(priv, budget, chan);
if (work_done < budget && napi_complete_done(napi, work_done)) { // 若处理量小于预算,说明队列已空,退出NAPI轮询模式
unsigned long flags;
spin_lock_irqsave(&ch->lock, flags);
stmmac_enable_dma_irq(priv, priv->ioaddr, chan, 1, 0); // 重新使能RX DMA中断
spin_unlock_irqrestore(&ch->lock, flags);
}
return work_done;
}
|
9.5.4.14. 接收函数¶
stmmac_rx函数是接收路径的核心函数,它遍历RX描述符环,从DMA已完成的描述符中取出数据,经过XDP处理后构建sk_buff,最终通过napi_gro_receive提交给协议栈。 处理完成后调用stmmac_rx_refill补充接收缓冲区。
注解
XDP(eXpress Data Path,快速数据路径)是内核提供的一种高性能数据包处理框架,它允许把一个eBPF程序挂到网卡驱动收包路径的最前端,在数据包还躺在DMA缓冲区里、内核还没分配sk_buff、还没进入协议栈之前,就对其执行用户自定义的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
{
struct stmmac_rx_queue *rx_q = &priv->dma_conf.rx_queue[queue];
struct stmmac_channel *ch = &priv->channel[queue];
unsigned int count = 0, error = 0, len = 0;
int status = 0, coe = priv->hw->rx_csum;
unsigned int next_entry = rx_q->cur_rx;
enum dma_data_direction dma_dir;
unsigned int desc_size;
struct sk_buff *skb = NULL;
struct xdp_buff xdp;
int xdp_status = 0;
int buf_sz;
// 从页池获取DMA映射方向,通常为DMA_FROM_DEVICE,启用XDP时可能为双向
dma_dir = page_pool_get_dma_dir(rx_q->page_pool);
// 将DMA缓冲区大小向上对齐到页大小,用于XDP缓冲区初始化
buf_sz = DIV_ROUND_UP(priv->dma_conf.dma_buf_sz, PAGE_SIZE) * PAGE_SIZE;
// 限制本次poll最多处理的描述符数量,防止超过环大小
limit = min(priv->dma_conf.dma_rx_size - 1, (unsigned int)limit);
// 如果启用了RX状态调试信息,则打印描述符环内容
if (netif_msg_rx_status(priv)) {
void *rx_head;
netdev_dbg(priv->dev, "%s: descriptor ring:\n", __func__);
if (priv->extend_desc) {
rx_head = (void *)rx_q->dma_erx;
desc_size = sizeof(struct dma_extended_desc);
} else {
rx_head = (void *)rx_q->dma_rx;
desc_size = sizeof(struct dma_desc);
}
// 打印整个RX描述符环
stmmac_display_ring(priv, rx_head, priv->dma_conf.dma_rx_size, true,
rx_q->dma_rx_phy, desc_size);
}
// 主循环,在NAPI预算内尽可能多地处理已接收完成的描述符
while (count < limit) {
unsigned int buf1_len = 0, buf2_len = 0;
enum pkt_hash_types hash_type;
struct stmmac_rx_buffer *buf;
struct dma_desc *np, *p;
int entry;
u32 hash;
// 如果是本次poll的第一次迭代,且上次poll遗留了未完成的包状态,则恢复状态
if (!count && rx_q->state_saved) {
skb = rx_q->state.skb;
error = rx_q->state.error;
len = rx_q->state.len;
// 否则重置状态,开始处理新的数据包
} else {
rx_q->state_saved = false;
skb = NULL;
error = 0;
len = 0;
}
read_again:
// 如果即将达到预算上限且预算大于1,提前退出,避免多段包处理一半导致状态复杂
if ((count >= limit - 1) && limit > 1)
break;
// 重置当前描述符的两个缓冲区长度
buf1_len = 0;
buf2_len = 0;
// 获取当前要处理的描述符索引
entry = next_entry;
// 获取该索引对应的软件缓冲区结构
buf = &rx_q->buf_pool[entry];
// 根据是否使用扩展描述符,获取当前描述符的硬件指针
if (priv->extend_desc)
p = (struct dma_desc *)(rx_q->dma_erx + entry);
else
p = rx_q->dma_rx + entry;
// 读取描述符状态字,包含DMA写入的接收结果、错误标志等
status = stmmac_rx_status(priv, &priv->dev->stats,
&priv->xstats, p);
// 如果OWN位仍为1,说明DMA引擎仍拥有该描述符,数据未收完,退出轮询
if (unlikely(status & dma_own))
break;
// 推进cur_rx到下一个描述符索引
rx_q->cur_rx = STMMAC_GET_ENTRY(rx_q->cur_rx,
priv->dma_conf.dma_rx_size);
next_entry = rx_q->cur_rx;
// 获取下一个描述符指针,用于预取和后续时间戳读取
if (priv->extend_desc)
np = (struct dma_desc *)(rx_q->dma_erx + next_entry);
else
np = rx_q->dma_rx + next_entry;
// 预取下一个描述符到CPU缓存,提升性能
prefetch(np);
// 如果使用扩展描述符,额外读取扩展状态字段,如时间戳等
if (priv->extend_desc)
stmmac_rx_extended_status(priv, &priv->dev->stats,
&priv->xstats, rx_q->dma_erx + entry);
// 如果硬件标记该帧需要丢弃,如严重CRC错误等
if (unlikely(status == discard_frame)) {
// 直接将该内存页归还给页池
page_pool_recycle_direct(rx_q->page_pool, buf->page);
buf->page = NULL;
// 标记错误,后续逻辑会跳过或丢弃该包
error = 1;
// 如果不是为了硬件时间戳而保留错误帧,则增加错误计数
if (!priv->hwts_rx_en)
priv->dev->stats.rx_errors++;
}
// 如果之前有错误,且当前描述符不是最后一段,说明整个多段包都坏了,跳过剩余分段
if (unlikely(error && (status & rx_not_ls)))
goto read_again;
// 如果是单段包错误或多段包的最后一个分段错误,释放已分配的skb并继续下一个包
if (unlikely(error)) {
dev_kfree_skb(skb);
skb = NULL;
count++;
continue;
}
/* Buffer is good. Go on. */
// 预取数据包所在内存页到CPU缓存,加速后续访问
prefetch(page_address(buf->page) + buf->page_offset);
if (buf->sec_page)
prefetch(page_address(buf->sec_page));
// 计算主缓冲区Buf1的有效数据长度并累加到总长度
buf1_len = stmmac_rx_buf1_len(priv, p, status, len);
len += buf1_len;
// 计算辅助缓冲区Buf2的有效数据长度并累加到总长度
buf2_len = stmmac_rx_buf2_len(priv, p, status, len);
len += buf2_len;
/* ACS is disabled; strip manually. */
if (likely(!(status & rx_not_ls))) {
if (buf2_len) {
buf2_len -= ETH_FCS_LEN;
len -= ETH_FCS_LEN;
} else if (buf1_len) {
buf1_len -= ETH_FCS_LEN;
len -= ETH_FCS_LEN;
}
}
// 如果skb为空,说明这是数据包的第一个分段,需要运行XDP程序并可能新建skb
if (!skb) {
unsigned int pre_len, sync_len;
// 将主缓冲区的DMA数据同步到CPU缓存,确保CPU读取到最新数据
dma_sync_single_for_cpu(priv->device, buf->addr,
buf1_len, dma_dir);
// 初始化XDP缓冲区结构
xdp_init_buff(&xdp, buf_sz, &rx_q->xdp_rxq);
// 准备XDP缓冲区,指向页池中的实际数据区域
xdp_prepare_buff(&xdp, page_address(buf->page),
buf->page_offset, buf1_len, false);
// 记录XDP程序运行前的数据长度,用于后续DMA同步范围计算
pre_len = xdp.data_end - xdp.data_hard_start -
buf->page_offset;
// 运行绑定的XDP程序,返回处理结果(PASS/DROP/TX/REDIRECT)
skb = stmmac_xdp_run_prog(priv, &xdp);
/* Due xdp_adjust_tail: DMA sync for_device
* cover max len CPU touch
*/
sync_len = xdp.data_end - xdp.data_hard_start -
buf->page_offset;
sync_len = max(sync_len, pre_len);
/* For Not XDP_PASS verdict */
// 如果XDP程序没有返回PASS,返回值为ERR_PTR
if (IS_ERR(skb)) {
// 解析XDP动作结果
unsigned int xdp_res = -PTR_ERR(skb);
// 如果是XDP_DROP或XDP_ABORTED
if (xdp_res & STMMAC_XDP_CONSUMED) {
// 将内存页归还给页池
page_pool_put_page(rx_q->page_pool,
virt_to_head_page(xdp.data),
sync_len, true);
buf->page = NULL;
// 增加丢包统计
priv->dev->stats.rx_dropped++;
/* Clear skb as it was set as
* status by XDP program.
*/
// 清空skb指针
skb = NULL;
// 如果是多段包,跳过剩余分段
if (unlikely((status & rx_not_ls)))
goto read_again;
count++;
continue;
// 如果是XDP_TX或XDP_REDIRECT
} else if (xdp_res & (STMMAC_XDP_TX |
STMMAC_XDP_REDIRECT)) {
// 记录XDP动作状态,用于后续统一收尾,如刷新TX队列、xdp_do_flush
xdp_status |= xdp_res;
buf->page = NULL;
skb = NULL;
count++;
continue;
}
}
}
// 如果XDP返回PASS,且skb仍未分配,则需要新建skb并拷贝数据
if (!skb) {
/* XDP program may expand or reduce tail */
buf1_len = xdp.data_end - xdp.data;
// 在NAPI上下文中分配线性sk_buff头部空间
skb = napi_alloc_skb(&ch->rx_napi, buf1_len);
if (!skb) {
priv->dev->stats.rx_dropped++;
count++;
goto drain_data;
}
/* XDP program may adjust header */
// 将XDP缓冲区中的数据拷贝到skb线性数据区
skb_copy_to_linear_data(skb, xdp.data, buf1_len);
skb_put(skb, buf1_len);
/* Data payload copied into SKB, page ready for recycle */
// 数据已拷贝到skb,原来的内存页可以直接归还给页池
page_pool_recycle_direct(rx_q->page_pool, buf->page);
buf->page = NULL;
// 如果skb已存在,且buf1有数据,则作为分片追加到skb
} else if (buf1_len) {
// 同步DMA数据到CPU
dma_sync_single_for_cpu(priv->device, buf->addr,
buf1_len, dma_dir);
// 将该内存页作为分片追加到skb的分片列表
skb_add_rx_frag(skb, skb_shinfo(skb)->nr_frags,
buf->page, buf->page_offset, buf1_len,
priv->dma_conf.dma_buf_sz);
/* Data payload appended into SKB */
// 解除页池对该页的追踪,skb接管该页生命周期
page_pool_release_page(rx_q->page_pool, buf->page);
buf->page = NULL;
}
// 如果启用了Split Header且辅助缓冲区有数据,同样作为分片追加
if (buf2_len) {
// 同步辅助页的DMA数据
dma_sync_single_for_cpu(priv->device, buf->sec_addr,
buf2_len, dma_dir);
// 将辅助页作为分片追加到skb
skb_add_rx_frag(skb, skb_shinfo(skb)->nr_frags,
buf->sec_page, 0, buf2_len,
priv->dma_conf.dma_buf_sz);
/* Data payload appended into SKB */
// 解除页池对该页的追踪,skb接管该页生命周期
page_pool_release_page(rx_q->page_pool, buf->sec_page);
buf->sec_page = NULL;
}
drain_data:
// 如果当前描述符不是最后一段,说明大包还没收完,继续读取下一个描述符
if (likely(status & rx_not_ls))
goto read_again;
// 如果skb为空,可能被XDP消耗或分配失败,直接继续下一个包
if (!skb)
continue;
/* Got entire packet into SKB. Finish it. */
// 如果启用了硬件RX时间戳,从描述符中提取时间戳并附加到skb
stmmac_get_rx_hwtstamp(priv, p, np, skb);
stmmac_rx_vlan(priv->dev, skb);
// 解析以太网头部,设置协议类型(如IPv4/IPv6)和MAC地址
skb->protocol = eth_type_trans(skb, priv->dev);
// 如果硬件不支持RX校验和卸载,标记需要软件计算校验和
if (unlikely(!coe))
skb_checksum_none_assert(skb);
else
// 硬件已验证校验和,标记为无需软件重复计算
skb->ip_summed = CHECKSUM_UNNECESSARY; // RK3588支持硬件校验和卸载
if (!stmmac_get_rx_hash(priv, p, &hash, &hash_type))
skb_set_hash(skb, hash, hash_type);
// 记录接收队列索引到skb
skb_record_rx_queue(skb, queue);
// 将数据包提交给NAPI的GRO引擎,然后进入协议栈
napi_gro_receive(&ch->rx_napi, skb);
// 清空skb指针,防止重复释放
skb = NULL;
// 更新设备接收包数和字节数统计
priv->dev->stats.rx_packets++;
priv->dev->stats.rx_bytes += len;
// 增加本次poll处理的包计数
count++;
}
// 如果退出循环时当前包还未接收完或skb仍在构建中,保存状态
if (status & rx_not_ls || skb) {
rx_q->state_saved = true;
rx_q->state.skb = skb;
rx_q->state.error = error;
rx_q->state.len = len;
}
// 完成XDP收尾工作:刷新XDP_TX队列、调用xdp_do_flush等
stmmac_finalize_xdp_rx(priv, xdp_status);
// 重新填充RX描述符环,将新的内存页交还给DMA引擎
stmmac_rx_refill(priv, queue);
// 更新驱动内部扩展统计信息
priv->xstats.rx_pkt_n += count;
priv->xstats.rxq_stats[queue].rx_pkt_n += count;
// 返回本次NAPI poll实际处理的工作量
return count;
}
|
9.5.4.15. 接收缓冲区补充¶
stmmac_rx_refill函数在每次NAPI轮询结束后调用,负责补充已消耗的接收缓冲区。它从Page Pool分配新的页面,设置DMA地址到描述符中,然后设置OWN位将描述符交还给DMA引擎,最后更新RX尾指针寄存器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | static inline void stmmac_rx_refill(struct stmmac_priv *priv, u32 queue)
{
struct stmmac_rx_queue *rx_q = &priv->dma_conf.rx_queue[queue];
int dirty = stmmac_rx_dirty(priv, queue);
unsigned int entry = rx_q->dirty_rx;
gfp_t gfp = (GFP_ATOMIC | __GFP_NOWARN);
// 如果主机DMA地址宽度小于等于32位,限制分配的内存必须在DMA32区域
if (priv->dma_cap.host_dma_width <= 32)
gfp |= GFP_DMA32;
// 循环处理所有需要重新填充的脏描述符
while (dirty-- > 0) {
// 获取当前索引对应的软件缓冲区元数据结构
struct stmmac_rx_buffer *buf = &rx_q->buf_pool[entry];
// 硬件描述符指针
struct dma_desc *p;
bool use_rx_wd;
// 根据是否使用扩展描述符,获取当前描述符的硬件指针
if (priv->extend_desc)
p = (struct dma_desc *)(rx_q->dma_erx + entry);
else
p = rx_q->dma_rx + entry;
// 如果主缓冲区页面为空,从页池分配新的内存页
if (!buf->page) {
buf->page = page_pool_alloc_pages(rx_q->page_pool, gfp);
if (!buf->page)
break;
}
// 如果启用了Split Header且辅助页面为空,分配辅助页
if (priv->sph && !buf->sec_page) {
buf->sec_page = page_pool_alloc_pages(rx_q->page_pool, gfp);
if (!buf->sec_page)
break;
// 获取辅助页的DMA地址
buf->sec_addr = page_pool_get_dma_addr(buf->sec_page);
}
// 计算主缓冲区的DMA地址
buf->addr = page_pool_get_dma_addr(buf->page) + buf->page_offset;
// 将主缓冲区DMA地址写入描述符
stmmac_set_desc_addr(priv, p, buf->addr);
// 如果启用了Split Header,将辅助页DMA地址写入描述符并标记有效
if (priv->sph)
stmmac_set_desc_sec_addr(priv, p, buf->sec_addr, true);
else
// 否则清除辅助地址有效标志
stmmac_set_desc_sec_addr(priv, p, buf->sec_addr, false);
stmmac_refill_desc3(priv, rx_q, p);
rx_q->rx_count_frames++;
rx_q->rx_count_frames += priv->rx_coal_frames[queue];
if (rx_q->rx_count_frames > priv->rx_coal_frames[queue])
rx_q->rx_count_frames = 0;
use_rx_wd = !priv->rx_coal_frames[queue];
use_rx_wd |= rx_q->rx_count_frames > 0;
if (!priv->use_riwt)
use_rx_wd = false;
// 写内存屏障:确保在设置OWN位之前,所有描述符字段都已写入内存,防止DMA引擎读到旧数据
dma_wmb();
// 设置描述符的OWN位,将控制权交还给DMA引擎
stmmac_set_rx_owner(priv, p, use_rx_wd);
// 计算下一个描述符索引
entry = STMMAC_GET_ENTRY(entry, priv->dma_conf.dma_rx_size);
}
// 更新脏描述符索引,记录下次填充的起点
rx_q->dirty_rx = entry;
// 计算新的RX尾指针物理地址
rx_q->rx_tail_addr = rx_q->dma_rx_phy +
(rx_q->dirty_rx * sizeof(struct dma_desc));
// 将尾指针地址写入硬件寄存器,通知DMA引擎有新描述符可用,可以开始接收新数据
stmmac_set_rx_tail_ptr(priv, priv->ioaddr, rx_q->rx_tail_addr, queue);
}
|
9.5.4.16. 收发数据流总结¶
9.5.4.16.1. 发送路径¶
当用户态程序通过socket发送数据时,报文经过内核协议栈(TCP/IP)层层封装后,最终到达网络设备层,调用stmmac_xmit函数。 该函数首先检查TX描述符环是否有足够的空闲空间,若空间不足则停止发送队列并返回NETDEV_TX_BUSY。 空间充足时,驱动对skb的线性数据区和各分片分别执行DMA映射,将物理地址填充到对应的TX描述符中,同时设置校验和卸载标志、VLAN标签插入等硬件特性参数。 所有描述符填充完毕后,驱动设置第一个描述符的OWN位,将所有权移交给DMA引擎,然后通过写TX尾指针寄存器触发DMA传输。 DMA引擎将报文数据从内存搬运到MAC硬件的发送FIFO中,MAC硬件再通过RGMII接口将数据发送给PHY芯片,最终由PHY转换为电信号发送到网线。
发送完成后,DMA引擎产生发送完成中断。 该中断触发NAPI调度,进入软中断上下文执行stmmac_napi_poll_tx,其内部调用stmmac_tx_clean函数。 该函数从dirty_tx指针开始遍历已完成的描述符,检查每个描述符的状态,确认DMA已完成传输后,解除对应的DMA映射,释放skb,更新统计计数。 如果之前发送队列因描述符环满而被停止,且当前可用空间已超过阈值,则唤醒发送队列,允许协议栈继续下发数据。
9.5.4.16.2. 接收路径¶
当网络上的报文到达PHY芯片时,PHY将电信号转换为数字数据,通过RGMII接口传送给MAC硬件。 MAC硬件的DMA引擎将报文数据写入驱动预先分配好的RX缓冲区中(这些缓冲区通过Page Pool机制管理,每个缓冲区对应一个RX描述符,其中记录了缓冲区的物理地址)。 DMA写入完成后,产生接收中断。
接收中断触发NAPI调度,进入软中断上下文执行stmmac_napi_poll_rx,其内部调用stmmac_rx函数。 该函数在budget限制内循环处理RX描述符环:首先读取描述符状态字,判断DMA是否已完成写入(通过检查OWN位); 若报文存在错误则丢弃并计入错误统计;若报文正常,则先执行XDP程序,然后根据XDP的处理结果决定后续动作。 对于XDP_PASS的报文,驱动构建skb:对于小包,分配新的skb并将数据拷贝进去,原页面归还Page Pool; 对于大包,直接将页面作为分片挂载到skb上,实现零拷贝。 构建完skb后,调用eth_type_trans解析以太网头、设置协议类型和包类型,然后调用napi_gro_receive将报文提交给内核协议栈,协议栈再进行GRO聚合、IP层处理、传输层分发等后续操作。
本轮轮询处理完所有已到达的报文后,驱动调用stmmac_rx_refill函数补充已消耗的接收缓冲区:从Page Pool分配新的页面,将页面的DMA地址写入描述符,设置OWN位将描述符归还给DMA引擎,并更新RX尾指针寄存器,确保DMA引擎始终有可用的缓冲区接收后续报文。 若本轮处理的报文数小于budget,说明队列已空,调用napi_complete_done退出轮询模式并重新使能接收中断。
9.5.4.17. 链路管理路径总结¶
当用户执行ip link set eth0 up时,内核调用stmmac_open函数。 该函数首先分配DMA描述符环和接收缓冲区,然后调用stmmac_hw_setup完成MAC硬件初始化,接着调用stmmac_init_phy通过PHYLINK框架连接PHY设备,最后调用phylink_start启动PHYLINK状态机。
PHYLINK启动后,PHY芯片开始自协商过程,与对端设备协商速率(10M/100M/1000M)和双工模式。 自协商完成后,PHYLINK检测到链路状态变化,读取PHY的状态寄存器获取协商结果,然后回调MAC侧的stmmac_mac_link_up函数。 该函数根据协商出的速率设置MAC控制寄存器中对应的速率位,配置流控参数,并使能MAC收发功能。 同时,该函数通过plat_dat->fix_mac_speed回调调用rk_fix_speed函数,进一步调用RK3588平台特有的rk3588_set_gmac_speed,向PHP_GRF配置寄存器写入对应的时钟分频值(千兆时选择125MHz,百兆时选择25MHz,十兆时选择2.5MHz),确保MAC与PHY之间的RGMII时钟频率正确匹配。 至此,链路完全就绪,网卡可以正常收发数据。
9.5.5. PHY驱动解析¶
PHY驱动是内核网络子系统中连接MAC驱动与物理层芯片的桥梁。 它运行在MDIO总线之上,向上对接PHYLINK框架,向下操作PHY芯片寄存器,完成物理层的管理工作。
9.5.5.1. 驱动注册¶
realtek_drvs数组是Realtek PHY驱动的注册表,每个条目对应一款PHY芯片。 module_phy_driver()宏将数组注册到内核PHY驱动框架。 当MDIO总线扫描到PHY ID为0x001cc916的设备时,匹配到RTL8211F条目,调用对应的回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | static struct phy_driver realtek_drvs[] = {
// ... 其他PHY型号
{
PHY_ID_MATCH_EXACT(0x001cc916), // 匹配PHY ID为0x001cc916的设备
.name = "RTL8211F Gigabit Ethernet", // 驱动名称,用于在sysfs文件系统等中显示
.probe = rtl821x_probe, // 探测函数,在设备与驱动匹配成功时调用
.config_init = &rtl8211f_config_init, // 初始化配置函数
.read_status = rtlgen_read_status, // 读取状态函数
.config_intr = &rtl8211f_config_intr, // 中断配置函数
.handle_interrupt = rtl8211f_handle_interrupt, // 中断处理服务程序
.suspend = genphy_suspend, // 挂起函数
.resume = rtl821x_resume, // 恢复函数
.read_page = rtl821x_read_page, // 读取页函数
.write_page = rtl821x_write_page, // 写入页函数
},
// ... 其他PHY型号
};
module_phy_driver(realtek_drvs);
static const struct mdio_device_id __maybe_unused realtek_tbl[] = {
{ PHY_ID_MATCH_VENDOR(0x001cc800) },
{ PHY_ID_MATCH_VENDOR(0x001ccb30) },
{ }
};
MODULE_DEVICE_TABLE(mdio, realtek_tbl);
|
9.5.5.2. PHY探测函数¶
rtl821x_probe函数在MDIO总线扫描到RTL8211F(PHY ID=0x001cc916)时被调用。 它读取PHY硬件当前配置,结合设备树属性,缓存到私有结构体中,供后续config_init使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | static int rtl821x_probe(struct phy_device *phydev)
{
struct device *dev = &phydev->mdio.dev;
struct rtl821x_priv *priv;
u32 phy_id = phydev->drv->phy_id;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
// 读取扩展页面0xa43的PHYCR1寄存器,获取ALDPS省电模式当前配置
ret = phy_read_paged(phydev, 0xa43, RTL8211F_PHYCR1);
if (ret < 0)
return ret;
priv->phycr1 = ret & (RTL8211F_ALDPS_PLL_OFF | RTL8211F_ALDPS_ENABLE | RTL8211F_ALDPS_XTAL_OFF);
// 若DTS中配置了realtek,aldps-enable,则启用ALDPS省电模式
if (of_property_read_bool(dev->of_node, "realtek,aldps-enable"))
priv->phycr1 |= RTL8211F_ALDPS_PLL_OFF | RTL8211F_ALDPS_ENABLE | RTL8211F_ALDPS_XTAL_OFF;
// RTL8211F-VD型号没有PHYCR2寄存器,需特殊处理
priv->has_phycr2 = !(phy_id == RTL_8211FVD_PHYID);
if (priv->has_phycr2) {
ret = phy_read_paged(phydev, 0xa43, RTL8211F_PHYCR2);
if (ret < 0)
return ret;
priv->phycr2 = ret & RTL8211F_CLKOUT_EN;
if (of_property_read_bool(dev->of_node, "realtek,clkout-disable"))
priv->phycr2 &= ~RTL8211F_CLKOUT_EN;
}
// 将私有数据挂载到PHY设备
phydev->priv = priv;
return 0;
}
|
9.5.5.3. RTL8211F 初始化配置¶
rtl8211f_config_init函数是RTL8211F最核心的初始化函数,在PHYLINK启动链路协商时由PHY框架调用, 完成配置ALDPS省电模式、根据phydev->interface配置RGMII TX/RX延迟、配置时钟输出。
因为RK3588S的dts配置了phy-mode = “rgmii-rxid”,因此此函数会在PHY内部启用RX延迟,不启用TX延迟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | static int rtl8211f_config_init(struct phy_device *phydev)
{
struct rtl821x_priv *priv = phydev->priv;
struct device *dev = &phydev->mdio.dev;
u16 val_txdly, val_rxdly;
int ret;
// 配置ALDPS省电模式
ret = phy_modify_paged_changed(phydev, 0xa43, RTL8211F_PHYCR1,
RTL8211F_ALDPS_PLL_OFF | RTL8211F_ALDPS_ENABLE | RTL8211F_ALDPS_XTAL_OFF,
priv->phycr1);
if (ret < 0) {
dev_err(dev, "aldps mode configuration failed: %pe\n",
ERR_PTR(ret));
return ret;
}
// 根据PHY与MAC之间的接口模式,决定TX和RX的内部延迟配置
switch (phydev->interface) {
// 标准RGMII:延迟由MAC端或PCB走线处理,PHY端不加延迟
case PHY_INTERFACE_MODE_RGMII:
val_txdly = 0; // 禁用PHY的TX延迟
val_rxdly = 0; // 禁用PHY的RX延迟
break;
// RGMII_RXID:PHY端在RX方向加延迟
case PHY_INTERFACE_MODE_RGMII_RXID:
val_txdly = 0;
val_rxdly = RTL8211F_RX_DELAY;
break;
// RGMII_TXID:PHY端在TX方向加延迟
case PHY_INTERFACE_MODE_RGMII_TXID:
val_txdly = RTL8211F_TX_DELAY;
val_rxdly = 0;
break;
// RGMII_ID:PHY端在TX和RX方向都加延迟
case PHY_INTERFACE_MODE_RGMII_ID:
val_txdly = RTL8211F_TX_DELAY;
val_rxdly = RTL8211F_RX_DELAY;
break;
default: /* the rest of the modes imply leaving delay as is. */
return 0;
}
// 配置TX延迟寄存器
ret = phy_modify_paged_changed(phydev, 0xd08, 0x11, RTL8211F_TX_DELAY,
val_txdly);
if (ret < 0) {
dev_err(dev, "Failed to update the TX delay register\n");
return ret;
} else if (ret) {
dev_dbg(dev,
"%s 2ns TX delay (and changing the value from pin-strapping RXD1 or the bootloader)\n",
val_txdly ? "Enabling" : "Disabling");
} else {
dev_dbg(dev,
"2ns TX delay was already %s (by pin-strapping RXD1 or bootloader configuration)\n",
val_txdly ? "enabled" : "disabled");
}
// 配置RX延迟寄存器
ret = phy_modify_paged_changed(phydev, 0xd08, 0x15, RTL8211F_RX_DELAY,
val_rxdly);
if (ret < 0) {
dev_err(dev, "Failed to update the RX delay register\n");
return ret;
} else if (ret) {
dev_dbg(dev,
"%s 2ns RX delay (and changing the value from pin-strapping RXD0 or the bootloader)\n",
val_rxdly ? "Enabling" : "Disabling");
} else {
dev_dbg(dev,
"2ns RX delay was already %s (by pin-strapping RXD0 or bootloader configuration)\n",
val_rxdly ? "enabled" : "disabled");
}
// 检查该PHY型号是否有PHYCR2寄存器
if (priv->has_phycr2) {
// 配置CLKOUT时钟输出功能
ret = phy_modify_paged(phydev, 0xa43, RTL8211F_PHYCR2,
RTL8211F_CLKOUT_EN, priv->phycr2);
if (ret < 0) {
dev_err(dev, "clkout configuration failed: %pe\n",
ERR_PTR(ret));
return ret;
}
// 配置完成后,对PHY执行一次软复位,使上述延迟和时钟配置生效
return genphy_soft_reset(phydev);
}
return 0;
}
|
9.5.5.4. 中断配置¶
rtl8211f_config_intr函数用于使能或禁用RTL8211F的链路状态中断。 当PHYLINK框架需要监控链路变化时,会调用此函数开启中断;当不再需要时,如轮询模式,会关闭中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | static int rtl8211f_config_intr(struct phy_device *phydev)
{
u16 val;
int err;
// 判断内核PHY子系统请求的动作,是要启用中断还是禁用中断
if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
// 在开启中断前,先调用ack函数读取一次中断状态寄存器,清除可能遗留的挂起中断
err = rtl8211f_ack_interrupt(phydev);
if (err)
return err;
// 设置中断使能掩码
val = RTL8211F_INER_LINK_STATUS;
// 将配置值写入中断使能寄存器
err = phy_write_paged(phydev, 0xa42, RTL821x_INER, val);
} else {
// 将掩码设置为0
val = 0;
// 写入中断使能寄存器,正式关闭中断
err = phy_write_paged(phydev, 0xa42, RTL821x_INER, val);
if (err)
return err;
// 禁用中断后,再次调用ack函数读取状态寄存器以清除挂起的中断
err = rtl8211f_ack_interrupt(phydev);
}
return err;
}
|
9.5.5.5. 中断处理¶
rtl8211f_handle_interrupt函数是RTL8211F的中断服务函数。 当触发中断时,此函数读取中断状态寄存器,判断是否为链路状态变化中断,若是则触发PHYLINK状态机重新协商。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static irqreturn_t rtl8211f_handle_interrupt(struct phy_device *phydev)
{
int irq_status;
// 读取RTL8211F的中断状态寄存器
irq_status = phy_read_paged(phydev, 0xa43, RTL8211F_INSR);
if (irq_status < 0) {
phy_error(phydev);
return IRQ_NONE;
}
// 检查读取到的状态字中,是否包含了链路状态变化的中断标志位
if (!(irq_status & RTL8211F_INER_LINK_STATUS))
return IRQ_NONE; // 如果该位为0,说明触发的不是链路变化中断,返回IRQ_NONE
// 确认是链路状态变化中断后,触发内核的PHY状态机
phy_trigger_machine(phydev);
// 返回IRQ_HANDLED,告诉内核中断处理完毕
return IRQ_HANDLED;
}
|
9.5.5.6. 链路状态读取¶
rtlgen_read_status函数是Realtek PHY驱动中用于读取并更新PHY当前链路状态的核心回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 | static int rtlgen_read_status(struct phy_device *phydev)
{
int ret;
// 调用内核PHY子系统的通用函数,读取标准IEEE 802.3 MII寄存器
ret = genphy_read_status(phydev);
if (ret < 0)
return ret;
// 调用Realtek特有的私有函数来读取实际速度
return rtlgen_get_speed(phydev);
}
|
标准MII状态寄存器可能无法正确反映实际协商速率, 因此驱动通过读取扩展页面0xa43的寄存器0x12来获取真实链路速率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | static int rtlgen_get_speed(struct phy_device *phydev)
{
int val;
// 检查链路是否已建立
if (!phydev->link)
return 0;
// 从扩展页0xa43的寄存器0x12中读取实际链路速度状态字
val = phy_read_paged(phydev, 0xa43, 0x12);
if (val < 0)
return val;
// 使用RTLGEN_SPEED_MASK掩码提取速度字段,并根据结果设置内核标准速度宏
switch (val & RTLGEN_SPEED_MASK) {
case 0x0000:
phydev->speed = SPEED_10;
break;
case 0x0010:
phydev->speed = SPEED_100;
break;
case 0x0020:
phydev->speed = SPEED_1000;
break;
case 0x0200:
phydev->speed = SPEED_10000;
break;
case 0x0210:
phydev->speed = SPEED_2500;
break;
case 0x0220:
phydev->speed = SPEED_5000;
break;
default:
break;
}
return 0;
}
|
9.6. 虚拟网卡回环实验¶
本实验驱动基于Linux内核网络子系统框架开发,实现一款纯软件虚拟回环网卡,具备报文翻转回环、NAPI异步收包、报文完整解析打印、设备状态管理、sysfs调试配置、网络统计、ethtool信息查询等完整功能。
驱动实现原理:网卡发送的ARP/IPv4/ICMP请求报文,在驱动层完成MAC、IP、ICMP协议字段翻转,将请求伪装成应答报文,重新投递回内核网络栈,实现本地报文自收发回环效果,无需物理网卡和外部网络环境。
本章的示例代码目录为: linux_driver/43_net_subsystem
9.6.1. 驱动代码详解¶
9.6.1.1. 核心定义与数据结构¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #define DRV_NAME "net_loopback" /* 驱动名称 */
#define DRV_VERSION "1.0" /* 驱动版本号 */
#define NAPI_BUDGET 64 /* 每次NAPI轮询最多处理64个包 */
/* 设备私有数据结构体 */
struct net_lb_priv {
struct net_device *ndev; /* 网络设备指针 */
struct napi_struct napi; /* NAPI轮询结构体 */
struct sk_buff_head lb_queue; /* 回环skb队列,TX包翻转后暂存于此 */
spinlock_t lb_lock; /* 保护回环队列的自旋锁 */
struct net_device_stats stats; /* 收发统计信息结构体 */
bool debug; /* 报文详细打印开关 */
};
|
关键说明:
第1-2行:定义驱动名称与版本,用于内核日志、ethtool信息打印。
第4行:定义NAPI处理预算,限制单次NAPI轮询最大处理报文数,避免CPU占用过高。
第8行:保存所属网络设备指针,实现私有数据与网卡设备绑定。
第9行:NAPI结构体,用于内核异步收包机制,替代中断轮询,提升报文处理效率。
第10行:skb报文队列,缓存发送后翻转完成的待回送报文,作为TX/RX数据中转缓冲区。
第11行:自旋锁,保护多线程并发下的报文队列操作。
第12行:设备统计结构体,记录收发包数、字节数、丢包数等流量信息。
第13行:调试开关变量,控制是否打印报文详细日志。
9.6.1.2. 报文翻转处理函数¶
net_lb_transform函数用于实现各类网络报文的协议翻转,将主机发送的请求报文(ARP请求、ICMP请求)伪装为应答报文,通过交换MAC、IP、修改协议操作码、重算校验和,完成报文回环预处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | /*
* 回环报文翻转:将TX报文转换为对端回复再回送RX
*
* 处理逻辑:
* - 以太网层:交换源/目的MAC
* - ARP:交换发送方/目标的IP+MAC,opcode请求变应答
* - IP:交换源/目的IP
* - ICMP:echo-request变echo-reply,并重算校验和
*
* 这样主机发出的请求会被伪装成对端的应答回送上来
*/
static struct sk_buff *net_lb_transform(struct net_lb_priv *priv,
struct sk_buff *skb)
{
/* 以太网头指针 */
struct ethhdr *eth;
/* 新建回送包指针 */
struct sk_buff *nskb;
/* MAC交换临时缓冲 */
unsigned char tmp_mac[ETH_ALEN];
/* IP交换临时变量 */
__be32 tmp_ip;
/* 完整拷贝原skb,拷贝后可安全修改 */
nskb = skb_copy(skb, GFP_ATOMIC);
if (!nskb)
return NULL;
/* 以太网层:交换源/目的 MAC */
eth = (struct ethhdr *)nskb->data; /* 指向数据起始处的以太网头 */
memcpy(tmp_mac, eth->h_source, ETH_ALEN); /* 保存源MAC到临时缓冲 */
memcpy(eth->h_source, eth->h_dest, ETH_ALEN); /* 源MAC <- 目的MAC */
memcpy(eth->h_dest, tmp_mac, ETH_ALEN); /* 目的MAC <- 原源MAC */
/* 判断是否为ARP报文 */
if (eth->h_proto == htons(ETH_P_ARP)) {
/* ARP头 */
struct arphdr *arp = (struct arphdr *)(nskb->data + ETH_HLEN);
/* ARP中的发送方/目标IP指针 */
__be32 *sip, *tip;
/* ARP中的发送方/目标MAC指针 */
u8 *sha, *tha;
/* 仅处理以太网/IPv4 ARP,其他类型跳过 */
if (arp->ar_hrd != htons(ARPHRD_ETHER) || /* 硬件类型非以太网 */
arp->ar_pro != htons(ETH_P_IP)) /* 协议类型非IPv4 */
goto done;
/*
* ARP有效负载布局:
* ARP固定头 arp 8字节
* 发送方MAC sha 6字节
* 发送方IP sip 4字节
* 目标方MAC tha 6字节
* 目标方IP tip 4字节
*/
/* 发送方MAC位于ARP头之后 */
sha = (u8 *)(arp + 1);
/* 发送方IP位于MAC之后 */
sip = (__be32 *)(sha + ETH_ALEN);
/* 目标MAC位于IP之后 */
tha = (u8 *)sip + 4;
/* 目标IP位于MAC之后 */
tip = (__be32 *)(tha + ETH_ALEN);
/* opcode:请求变应答 */
if (arp->ar_op == htons(ARPOP_REQUEST)) /* 若为ARP请求 */
arp->ar_op = htons(ARPOP_REPLY); /* 改为ARP应答 */
/* 交换发送方/目标MAC */
memcpy(tmp_mac, sha, ETH_ALEN); /* 保存发送方MAC */
memcpy(sha, tha, ETH_ALEN); /* 发送方MAC <- 目标MAC */
memcpy(tha, tmp_mac, ETH_ALEN); /* 目标MAC <- 原发送方MAC */
/* 交换发送方/目标 IP */
tmp_ip = *sip; /* 保存发送方IP */
*sip = *tip; /* 发送方IP <- 目标IP */
*tip = tmp_ip; /* 目标IP <- 原发送方IP */
goto done; /* ARP处理完成,跳转返回 */
}
/* 判断是否为IPv4报文 */
if (eth->h_proto == htons(ETH_P_IP)) {
/* IP头 */
struct iphdr *iph = (struct iphdr *)(nskb->data + ETH_HLEN);
/* 交换源/目的IP */
tmp_ip = iph->saddr; /* 保存源IP */
iph->saddr = iph->daddr; /* 源IP <- 目的IP */
iph->daddr = tmp_ip; /* 目的IP <- 原源IP */
/* 重算IP头校验和 */
iph->check = 0; /* 校验和字段清零 */
iph->check = ip_fast_csum(iph, iph->ihl); /* 重算IP头校验和 */
/* 判断是否为ICMP报文 */
if (iph->protocol == IPPROTO_ICMP) {
/* ICMP头位于IP头之后 */
struct icmphdr *icmph = (struct icmphdr *)((u8 *)iph +
(iph->ihl * 4));
/* 仅翻转echo-request */
if (icmph->type == ICMP_ECHO) { /* 若为echo请求 */
icmph->type = ICMP_ECHOREPLY; /* 改为echo应答 */
/* 重算ICMP校验和 */
icmph->checksum = 0; /* 校验和字段清零 */
icmph->checksum = csum_fold( /* 折叠为16位校验和 */
csum_partial(icmph, nskb->len - ETH_HLEN -
iph->ihl * 4, 0)); /* 计算ICMP部分校验和 */
}
}
}
done:
/* 返回转换后的回送包 */
return nskb;
}
|
关键说明:
第25行:调用skb_copy完整复制原始发送报文,避免直接修改原生skb导致内核栈异常。
第30-33行:以太网层处理,交换报文源MAC与目的MAC,实现二层链路反向。
第36行:判断报文是否为ARP协议,针对ARP请求处理翻转。
第45-47行:校验ARP报文为标准以太网+IPv4类型,过滤非法ARP报文。
第59-65行:按照ARP协议帧布局,确认发送方/目标MAC、IP字段指针。
第68-69行:将ARP请求操作码修改为应答操作码,伪装ARP响应报文。
第72-74行:交换ARP报文中的发送方与目标MAC地址。
第77-79行:交换ARP报文中的发送方与目标IP地址,完成ARP报文翻转。
第85行:判断报文为IPv4协议,进入三层IP翻转处理。
第90-92行:交换IP报文源地址与目的地址,实现三层路由反向。
第95-96行:清零IP校验和并重新计算,IP头部字段修改后必须重算校验和,否则报文校验失败被内核丢弃。
第99行:判断IP上层协议为ICMP,处理ping请求报文翻转。
第104-105行:将ICMP回声请求改为回声应答,适配ping命令应答。
第108-111行:清零并重新计算ICMP校验和,保证ICMP报文合法性。
第118行:统一出口,返回翻转完成的新报文,供后续回环投递。
9.6.1.3. 协议名称解析工具函数¶
包含4组协议码转可读字符串的工具函数,分别实现以太网协议、IP上层协议、ICMP类型、ARP操作码的文本解析,用于报文日志打印。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /* 以太网协议号转可读名称 */
static const char *eth_proto_name(u16 proto)
{
/* 根据协议号匹配 */
switch (proto) {
case ETH_P_IP: return "IPv4"; // 宏定义在include/uapi/linux/if_ether.h
case ETH_P_ARP: return "ARP";
case ETH_P_IPV6: return "IPv6";
case ETH_P_RARP: return "RARP";
default: return "Other";
}
}
/* IP上层协议号转可读名称 */
static const char *ip_proto_name(u8 proto)
{
/* 根据协议号匹配 */
switch (proto) {
case IPPROTO_ICMP: return "ICMP"; // 宏定义在include/uapi/linux/in.h
case IPPROTO_TCP: return "TCP";
case IPPROTO_UDP: return "UDP";
default: return "?";
}
}
/* ICMP类型转可读名称 */
static const char *icmp_type_name(u8 type)
{
/* 根据类型匹配 */
switch (type) {
case ICMP_ECHO: return "Echo-Request"; // 宏定义在include/uapi/linux/icmp.h
case ICMP_ECHOREPLY: return "Echo-Reply";
case ICMP_DEST_UNREACH: return "Dest-Unreach";
default: return "Other";
}
}
/* ARP opcode转可读名称 */
static const char *arp_op_name(u16 op)
{
/* 根据操作码匹配 */
switch (op) {
case ARPOP_REQUEST: return "Request"; // 宏定义在include/uapi/linux/if_arp.h
case ARPOP_REPLY: return "Reply";
default: return "Other";
}
}
|
关键说明:
第5-10行:解析二层以太网协议,支持IPv4、ARP、IPv6、RARP解析,未知协议统一归类为Other。
第18-22行:解析IP上层传输层协议,支持ICMP、TCP、UDP协议解析。
第30-34行:解析ICMP报文类型,区分ping请求、应答、目的不可达等常见类型。
第42-45行:解析ARP操作码,区分ARP请求与应答报文类型。
9.6.1.4. 报文字段逐行打印工具函数¶
实现完整报文解析与日志打印,包含报文方向、设备名、长度概要,逐层解析以太网、ARP、IP、ICMP协议信息,逐字段打印原始字节与解码值,支持ICMP负载数据转储,仅在debug开关开启时生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | /* 打印报文详细信息 */
static void net_lb_dump_skb(struct net_device *ndev,
const char *dir,
struct sk_buff *skb)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 指向以太网头 */
struct ethhdr *eth = (struct ethhdr *)skb->data;
/* 网络字节序转主机字节序协议号 */
u16 proto = ntohs(eth->h_proto);
/* 概要缓冲区:仅放标题与各协议单行摘要 */
char hdr[512];
/* 缓冲区已写入偏移量 */
int n = 0;
/* debug开关关闭时不打印 */
if (!priv->debug)
return;
/* 标题行:方向、接口名、报文长度 */
n += scnprintf(hdr + n, sizeof(hdr) - n,
"\n---[%s]-- %s -- %u bytes --\n",
dir, ndev->name, skb->len);
/* 以太网层:目的MAC、源MAC、协议名称 */
n += scnprintf(hdr + n, sizeof(hdr) - n,
"| ETH dst=%pM src=%pM %s\n",
eth->h_dest, eth->h_source, eth_proto_name(proto));
/* ARP报文解析 */
if (proto == ETH_P_ARP) { /* 若为ARP协议 */
struct arphdr *arp = (struct arphdr *)(skb->data + ETH_HLEN); /* ARP 头 */
u8 *sha = (u8 *)(arp + 1); /* 发送方MAC */
__be32 *sip = (__be32 *)(sha + ETH_ALEN); /* 发送方IP */
u8 *tha = (u8 *)sip + 4; /* 目标MAC */
__be32 *tip = (__be32 *)(tha + ETH_ALEN); /* 目标IP */
n += scnprintf(hdr + n, sizeof(hdr) - n, /* 追加ARP层信息 */
"| ARP %s %pI4(%pM) --> %pI4(%pM)\n", /* 格式:操作码 IP(MAC) */
arp_op_name(ntohs(arp->ar_op)), /* ARP操作码名称 */
sip, sha, tip, tha); /* IP和MAC参数 */
}
/* IP报文解析 */
if (proto == ETH_P_IP) { /* 若为IPv4协议 */
struct iphdr *iph = (struct iphdr *)(skb->data + ETH_HLEN); /* IP头 */
n += scnprintf(hdr + n, sizeof(hdr) - n, /* 追加IP层信息 */
"| IP %pI4 --> %pI4 %s ttl=%u len=%u\n", /* 格式:源IP 目的IP */
&iph->saddr, &iph->daddr, /* 源/目的IP */
ip_proto_name(iph->protocol), /* 上层协议名称 */
iph->ttl, ntohs(iph->tot_len)); /* TTL和总长度 */
/* ICMP报文解析 */
if (iph->protocol == IPPROTO_ICMP) { /* 若上层为ICMP */
struct icmphdr *icmph = (struct icmphdr *)((u8 *)iph +
iph->ihl * 4); /* ICMP头位于IP头之后 */
n += scnprintf(hdr + n, sizeof(hdr) - n, /* 追加ICMP层信息 */
"| ICMP %s id=0x%04x seq=%u\n", /* 格式:类型名 id seq */
icmp_type_name(icmph->type), /* ICMP类型名称 */
ntohs(icmph->un.echo.id), /* ICMP标识符 */
ntohs(icmph->un.echo.sequence)); /* ICMP序列号 */
}
}
/* 解析标题 */
n += scnprintf(hdr + n, sizeof(hdr) - n, "--- fields ---\n");
/* 概要一次输出 */
netdev_info(ndev, "%s", hdr);
/* 以下字段行逐行printk输出,避免单次printk过长被截断 */
/* 以太网层字段:目的MAC、源MAC、以太类型 */
{
/* 临时解码值缓冲 */
char val[40];
scnprintf(val, sizeof(val), "%pM", eth->h_dest); /* 目的MAC */
net_lb_field_line(0, "dst-mac", eth->h_dest, ETH_ALEN, val);
scnprintf(val, sizeof(val), "%pM", eth->h_source); /* 源MAC */
net_lb_field_line(ETH_ALEN, "src-mac",
eth->h_source, ETH_ALEN, val);
scnprintf(val, sizeof(val), "0x%04x (%s)", /* 以太类型 */
proto, eth_proto_name(proto));
net_lb_field_line(2 * ETH_ALEN, "ethertype",
(u8 *)ð->h_proto, 2, val);
}
/* ARP报文字段解析 */
if (proto == ETH_P_ARP) {
/* ARP头 */
struct arphdr *arp = (struct arphdr *)(skb->data + ETH_HLEN);
/* 发送方/目标MAC、IP指针 */
u8 *sha = (u8 *)(arp + 1);
__be32 *sip = (__be32 *)(sha + ETH_ALEN);
u8 *tha = (u8 *)sip + 4;
__be32 *tip = (__be32 *)(tha + ETH_ALEN);
/* 解码后的主机序字段值 */
u16 ar_hrd = ntohs(arp->ar_hrd);
u16 ar_pro = ntohs(arp->ar_pro);
u16 ar_op = ntohs(arp->ar_op);
/* 临时解码值缓冲 */
char val[64];
scnprintf(val, sizeof(val), "0x%04x (%s)", ar_hrd, /* 硬件类型 */
ar_hrd == ARPHRD_ETHER ? "Ethernet" : "?");
net_lb_field_line(ETH_HLEN, "hrd-type",
(u8 *)&arp->ar_hrd, 2, val);
scnprintf(val, sizeof(val), "0x%04x (%s)", ar_pro, /* 协议类型 */
ar_pro == ETH_P_IP ? "IPv4" : "?");
net_lb_field_line(ETH_HLEN + 2, "pro-type",
(u8 *)&arp->ar_pro, 2, val);
scnprintf(val, sizeof(val), "%u", arp->ar_hln); /* 硬件地址长度 */
net_lb_field_line(ETH_HLEN + 4, "hrd-len",
&arp->ar_hln, 1, val);
scnprintf(val, sizeof(val), "%u", arp->ar_pln); /* 协议地址长度 */
net_lb_field_line(ETH_HLEN + 5, "pro-len",
&arp->ar_pln, 1, val);
scnprintf(val, sizeof(val), "0x%04x (%s)", ar_op, /* 操作码 */
arp_op_name(ar_op));
net_lb_field_line(ETH_HLEN + 6, "opcode",
(u8 *)&arp->ar_op, 2, val);
scnprintf(val, sizeof(val), "%pM", sha); /* 发送方MAC */
net_lb_field_line(ETH_HLEN + 8, "sender-mac",
sha, ETH_ALEN, val);
scnprintf(val, sizeof(val), "%pI4", sip); /* 发送方IP */
net_lb_field_line(ETH_HLEN + 14, "sender-ip",
(u8 *)sip, 4, val);
scnprintf(val, sizeof(val), "%pM", tha); /* 目标MAC */
net_lb_field_line(ETH_HLEN + 18, "target-mac",
tha, ETH_ALEN, val);
scnprintf(val, sizeof(val), "%pI4", tip); /* 目标IP */
net_lb_field_line(ETH_HLEN + 24, "target-ip",
(u8 *)tip, 4, val);
}
/* IPv4报文字段解析 */
if (proto == ETH_P_IP) {
/* IP头 */
struct iphdr *iph = (struct iphdr *)(skb->data + ETH_HLEN);
/* 临时解码值缓冲 */
char val[64];
scnprintf(val, sizeof(val), "v%u, ihl=%u (%u bytes)", /* 版本/头长 */
iph->version, iph->ihl, iph->ihl * 4);
net_lb_field_line(ETH_HLEN, "ver/ihl", (u8 *)iph, 1, val);
scnprintf(val, sizeof(val), "0x%02x", iph->tos); /* 服务类型 */
net_lb_field_line(ETH_HLEN + 1, "tos", &iph->tos, 1, val);
scnprintf(val, sizeof(val), "%u", ntohs(iph->tot_len)); /* 总长度 */
net_lb_field_line(ETH_HLEN + 2, "tot-len",
(u8 *)&iph->tot_len, 2, val);
scnprintf(val, sizeof(val), "0x%04x", ntohs(iph->id)); /* 标识符 */
net_lb_field_line(ETH_HLEN + 4, "id",
(u8 *)&iph->id, 2, val);
scnprintf(val, sizeof(val), "flags=0x%x frag=%u", /* 标志/分片 */
ntohs(iph->frag_off) >> 13,
ntohs(iph->frag_off) & 0x1fff);
net_lb_field_line(ETH_HLEN + 6, "flags/frag",
(u8 *)&iph->frag_off, 2, val);
scnprintf(val, sizeof(val), "%u", iph->ttl); /* TTL */
net_lb_field_line(ETH_HLEN + 8, "ttl", &iph->ttl, 1, val);
scnprintf(val, sizeof(val), "0x%02x (%s)", iph->protocol, /* 上层协议 */
ip_proto_name(iph->protocol));
net_lb_field_line(ETH_HLEN + 9, "protocol",
&iph->protocol, 1, val);
scnprintf(val, sizeof(val), "0x%04x", ntohs(iph->check)); /* 头校验和 */
net_lb_field_line(ETH_HLEN + 10, "checksum",
(u8 *)&iph->check, 2, val);
scnprintf(val, sizeof(val), "%pI4", &iph->saddr); /* 源IP */
net_lb_field_line(ETH_HLEN + 12, "src-ip",
(u8 *)&iph->saddr, 4, val);
scnprintf(val, sizeof(val), "%pI4", &iph->daddr); /* 目的IP */
net_lb_field_line(ETH_HLEN + 16, "dst-ip",
(u8 *)&iph->daddr, 4, val);
/* ICMP报文字段解析 */
if (iph->protocol == IPPROTO_ICMP) {
/* ICMP头位于IP头之后 */
struct icmphdr *icmph = (struct icmphdr *)((u8 *)iph +
iph->ihl * 4);
/* ICMP字段起始偏移 */
int icmp_off = ETH_HLEN + iph->ihl * 4;
scnprintf(val, sizeof(val), "0x%02x (%s)", icmph->type, /* 类型 */
icmp_type_name(icmph->type));
net_lb_field_line(icmp_off, "type",
&icmph->type, 1, val);
scnprintf(val, sizeof(val), "0x%02x", icmph->code); /* 代码 */
net_lb_field_line(icmp_off + 1, "code",
&icmph->code, 1, val);
scnprintf(val, sizeof(val), "0x%04x", /* 校验和 */
ntohs(icmph->checksum));
net_lb_field_line(icmp_off + 2, "checksum",
(u8 *)&icmph->checksum, 2, val);
scnprintf(val, sizeof(val), "0x%04x", /* 标识符 */
ntohs(icmph->un.echo.id));
net_lb_field_line(icmp_off + 4, "id",
(u8 *)&icmph->un.echo.id, 2, val);
scnprintf(val, sizeof(val), "%u", /* 序列号 */
ntohs(icmph->un.echo.sequence));
net_lb_field_line(icmp_off + 6, "seq",
(u8 *)&icmph->un.echo.sequence, 2, val);
/* ICMP负载数据:逐行转储剩余字节,最多显示64字节 */
{
int payload_off = icmp_off + 8; /* 负载起始偏移 */
int payload_len = skb->len - payload_off; /* 负载总字节 */
int show = min_t(int, payload_len, 64); /* 最多64字节 */
int i;
/* 每16字节一行,逐行输出负载原始字节 */
for (i = 0; i < show; i += 16) {
/* 本行字节数 */
int chunk = min_t(int, show - i, 16);
scnprintf(val, sizeof(val),
"(payload %d-%d/%d bytes)",
i, i + chunk, payload_len);
net_lb_field_line(payload_off + i,
i == 0 ? "payload" : "cont",
skb->data + payload_off + i,
chunk, val);
}
/* 超过显示上限则提示总字节数 */
if (payload_len > show) {
scnprintf(val, sizeof(val),
"(truncated, %d total bytes)",
payload_len);
net_lb_field_line(payload_off + show, "trunc",
skb->data + payload_off + show,
0, val);
}
}
}
}
}
|
关键说明:
第6行:通过netdev_priv获取设备私有数据,读取debug全局调试开关状态。
第18-19行:判断debug开关,关闭时直接返回,不执行任何报文解析与日志输出。
第22-24行:打印报文头部概要,包含收发方向(TX/RX)、网卡设备名、报文总长度。
第27-29行:解析并打印以太网二层信息,包含源/目的MAC地址、二层协议类型。
第32-43行:解析ARP协议报文,提取ARP操作类型、发送方/目标IP与MAC映射关系。
第46-53行:解析IPv4三层报文,输出源目IP、上层传输协议、TTL生存时间、报文总长度。
第56-65行:针对ICMP协议解析,识别ping请求/应答类型,打印报文ID与序列号。
第77-92行:实现以太网字段打印,逐字节解析目的MAC、源MAC、二层协议类型。
第95-148行:ARP报文字段解析,逐一打印硬件类型、协议类型、地址长度、操作码、收发双方MAC/IP所有头部字段。
第151-196行:IPv4报文字段解析,打印IP版本、头长度、服务类型、总长度、分片标志、TTL、校验和、源目IP等标准头部字段。
第199-260行:ICMP报文字段解析与负载转储,解析ICMP类型、代码、校验和、ID、序列号,支持ICMP负载数据分页打印。
9.6.1.5. NAPI异步收包轮询函数¶
实现NAPI异步收包逻辑,从回环报文队列中取出翻转完成的报文,完成报文初始化、日志打印、协议绑定、校验和配置,将报文投递回内核网络栈,同时统计收发流量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | /* NAPI轮询函数:从回环队列取出报文,回送网络栈 */
static int net_lb_napi_poll(struct napi_struct *napi, int budget)
{
/* 取私有数据 */
struct net_lb_priv *priv = container_of(napi, struct net_lb_priv, napi);
/* 待回送的报文指针 */
struct sk_buff *skb;
/* 报文长度 */
unsigned int pkt_len;
/* 已处理报文计数 */
int work_done = 0;
/* 循环处理回环队列 */
while (work_done < budget) { /* 未达预算时继续处理 */
spin_lock(&priv->lb_lock); /* 加锁保护队列操作 */
skb = __skb_dequeue(&priv->lb_queue); /* 从队列头部取出一个skb */
spin_unlock(&priv->lb_lock); /* 解锁 */
/* 取出为空,退出循环 */
if (!skb)
break;
/* 设置报文关联到本网络设备 */
skb->dev = priv->ndev;
/* 打印RX报文详情 */
net_lb_dump_skb(priv->ndev, "RX", skb);
/* 解析以太网协议类型,设置skb->protocol和pkt_type */
skb->protocol = eth_type_trans(skb, priv->ndev);
/* 重置校验和,标记无需硬件校验 */
skb->ip_summed = CHECKSUM_UNNECESSARY;
/* 保存长度到局部变量 */
pkt_len = skb->len;
/* 回送报文到网络协议栈,模拟硬件接收,此后skb由协议栈释放 */
netif_receive_skb(skb);
/* 更新接收统计 */
priv->stats.rx_packets++; /* 接收包数+1 */
priv->stats.rx_bytes += pkt_len; /* 接收字节累加 */
work_done++; /* 已处理数+1 */
}
/* 若处理完毕且队列已空,结束NAPI轮询 */
if (work_done < budget) {
/* 完成NAPI并报告处理量 */
napi_complete_done(napi, work_done);
}
/* 返回实际处理的报文数 */
return work_done;
}
|
关键说明:
第14行:循环处理报文,单次处理数量不超过NAPI_BUDGET预算。
第15-17行:加锁后从队列取出报文,保证队列操作并发安全。
第20-21行:队列为空时直接退出循环,避免无效轮询。
第24行:绑定报文所属网卡设备,保证内核网络栈识别报文来源设备。
第30行:解析二层协议类型,初始化skb协议字段,为上层协议解析提供依据。
第32行:标记报文校验和无需硬件校验,虚拟报文已完成软件校验。
第38行:调用netif_receive_skb将报文投递回内核网络栈,模拟硬件收包流程。
第41-43行:更新网卡接收流量统计数据。
第47-50行:队列处理完毕后结束NAPI轮询,释放CPU资源。
第53行:返回本次轮询实际处理报文数,供内核调度参考。
9.6.1.6. 设备启停回调函数¶
实现网卡设备打开、关闭回调,对应系统ifconfig/ip link命令启停设备操作,负责NAPI启用/禁用、载波状态切换、报文队列清空、设备状态初始化与回收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /* 打开网络设备:ifconfig up/ip link set up时调用 */
static int net_lb_ndo_open(struct net_device *ndev)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 启用NAPI轮询 */
napi_enable(&priv->napi);
/* 开启载波 */
netif_carrier_on(ndev);
/* 启动发送队列 */
netif_start_queue(ndev);
/* 打印打开信息 */
netdev_info(ndev, "虚拟回环网卡已打开\n");
return 0;
}
/* 关闭网络设备:ifconfig down/ip link set down时调用 */
static int net_lb_ndo_stop(struct net_device *ndev)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 停止发送队列 */
netif_stop_queue(ndev);
/* 关闭载波 */
netif_carrier_off(ndev);
/* 禁用NAPI */
napi_disable(&priv->napi);
/* 清空回环队列中残留报文 */
skb_queue_purge(&priv->lb_queue);
/* 打印关闭信息 */
netdev_info(ndev, "虚拟回环网卡已关闭\n");
return 0;
}
|
关键说明:
第8行:启用NAPI异步收包机制,设备上线后开启报文处理能力。
第10行:开启网卡载波状态,告知内核设备链路正常。
第12行:启动网卡发送队列,允许内核下发TX报文。
第27行:停止发送队列,禁止新的报文下发。
第29行:关闭载波,标记设备链路断开。
第31行:禁用NAPI,停止异步收包轮询。
第34行:清空队列残留报文,防止设备关闭后内存泄漏、脏数据残留。
9.6.1.7. 报文发送回调函数¶
网卡发送核心回调,内核下发的所有报文都会进入该函数,完成发送日志打印、流量统计、报文翻转、队列入队、NAPI调度,实现TX报文转RX回环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /* 发送报文:网络栈下发数据包时调用,实现回环 */
static netdev_tx_t net_lb_ndo_start_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 回送用的转换报文 */
struct sk_buff *nskb;
/* debug模式下打印发送报文详情 */
net_lb_dump_skb(ndev, "TX", skb);
/* 更新发送统计 */
priv->stats.tx_packets++; /* 发送包数 +1 */
priv->stats.tx_bytes += skb->len; /* 发送字节累加 */
/* 将TX报文翻转为对端应答用于回送 */
nskb = net_lb_transform(priv, skb);
/* 释放原始发送skb */
dev_kfree_skb(skb);
/* 转换失败则直接返回 */
if (!nskb) {
priv->stats.tx_dropped++; /* 丢包计数+1 */
return NETDEV_TX_OK; /* 返回发送成功 */
}
/* 将回送报文入队 */
spin_lock(&priv->lb_lock); /* 加锁保护队列 */
__skb_queue_tail(&priv->lb_queue, nskb); /* 回送包加入队列尾部 */
spin_unlock(&priv->lb_lock); /* 解锁 */
/* 调度NAPI轮询,触发回送收包 */
napi_schedule(&priv->napi);
/* 返回发送成功 */
return NETDEV_TX_OK;
}
|
关键说明:
第11行:打印TX方向报文日志,记录内核下发的原始报文数据。
第14-15行:更新网卡发送流量统计,记录发包数与字节数。
第18行:调用核心翻转函数,生成反向应答报文。
第20行:释放原始skb,避免内存泄漏,原始报文无需继续使用。
第23-26行:报文翻转失败时统计丢包,返回发送成功,避免内核报错。
第29-31行:加锁将翻转后的报文加入回环队列,保证队列安全。
第34行:调度NAPI轮询,触发异步收包流程,完成报文回环。
第37行:返回TX成功状态,告知内核报文发送处理完成。
9.6.1.8. 设备配置与统计回调函数¶
实现网卡MAC地址设置、网络统计信息获取回调,适配内核标准网络设备接口,支持用户修改网卡MAC、查看设备流量统计数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* 获取网络统计信息 */
static struct net_device_stats *net_lb_ndo_get_stats(struct net_device *ndev)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 返回统计结构指针 */
return &priv->stats;
}
/* 设置MAC地址 */
static int net_lb_ndo_set_mac_address(struct net_device *ndev, void *addr)
{
/* 调用通用以太网MAC设置函数,内部完成校验与dev_addr_set */
return eth_mac_addr(ndev, addr);
}
/* net_device_ops操作集 */
static const struct net_device_ops net_lb_ops = {
.ndo_open = net_lb_ndo_open, /* 打开设备回调 */
.ndo_stop = net_lb_ndo_stop, /* 关闭设备回调 */
.ndo_start_xmit = net_lb_ndo_start_xmit, /* 发送报文回调 */
.ndo_get_stats = net_lb_ndo_get_stats, /* 获取统计回调 */
.ndo_set_mac_address = net_lb_ndo_set_mac_address, /* 设置MAC回调 */
.ndo_validate_addr = eth_validate_addr, /* 校验MAC回调,使用内核通用函数 */
};
|
关键说明:
第8行:返回私有数据中的统计结构体,供内核读取收发、丢包统计数据。
第15行:调用内核通用以太网MAC设置函数,自带MAC合法性校验,无需自定义。
第19-26行:绑定所有网卡核心操作回调,完成设备操作接口注册。
9.6.1.9. ethtool工具适配函数¶
适配ethtool工具,实现驱动信息、链路状态、自定义统计项查询,支持通过ethtool命令查看驱动版本、网卡状态、收发流量、队列长度等详细信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | /* 获取驱动信息 */
static void net_lb_et_get_drvinfo(struct net_device *ndev,
struct ethtool_drvinfo *info)
{
/* 填入驱动名 */
strscpy(info->driver, DRV_NAME, sizeof(info->driver));
/* 填入版本号 */
strscpy(info->version, DRV_VERSION, sizeof(info->version));
/* 填入总线信息 */
strscpy(info->bus_info, "virtual", sizeof(info->bus_info));
}
/* 获取链路状态 */
static u32 net_lb_et_get_link(struct net_device *ndev)
{
/* 载波正常返回1,否则0 */
return netif_carrier_ok(ndev) ? 1 : 0;
}
/* 统计信息字符串表 */
static const char net_lb_stat_strings[][ETH_GSTRING_LEN] = {
"tx_packets", "tx_bytes", "rx_packets", "rx_bytes", /* 发送数据包数、发送字节数、接收数据包数、接收字节数 */
"tx_dropped", "lb_queue_len", /* 丢包数、队列长度 */
};
/* 获取统计字符串集 */
static void net_lb_et_get_strings(struct net_device *ndev, u32 stringset,
u8 *data)
{
/* 若请求统计字符串 */
if (stringset == ETH_SS_STATS)
memcpy(data, net_lb_stat_strings, sizeof(net_lb_stat_strings)); /* 拷贝字符串表 */
}
/* 获取统计项数量 */
static int net_lb_et_get_sset_count(struct net_device *ndev, int sset)
{
/* 若为统计字符串集 */
if (sset == ETH_SS_STATS)
return ARRAY_SIZE(net_lb_stat_strings); /* 返回数组元素个数 */
return 0;
}
/* 获取统计数据 */
static void net_lb_et_get_ethtool_stats(struct net_device *ndev,
struct ethtool_stats *stats,
u64 *data)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
data[0] = priv->stats.tx_packets; /* 发送包数 */
data[1] = priv->stats.tx_bytes; /* 发送字节 */
data[2] = priv->stats.rx_packets; /* 接收包数 */
data[3] = priv->stats.rx_bytes; /* 接收字节 */
data[4] = priv->stats.tx_dropped; /* 发送丢包数 */
data[5] = skb_queue_len(&priv->lb_queue); /* 当前回环队列长度 */
}
/* ethtool操作集 */
static const struct ethtool_ops net_lb_ethtool_ops = {
.get_drvinfo = net_lb_et_get_drvinfo, /* 获取驱动信息 */
.get_link = net_lb_et_get_link, /* 获取链路状态 */
.get_strings = net_lb_et_get_strings, /* 获取统计字符串 */
.get_sset_count = net_lb_et_get_sset_count, /* 获取统计项数 */
.get_ethtool_stats = net_lb_et_get_ethtool_stats, /* 获取统计数据 */
};
|
关键说明:
第6-10行:填充驱动名称、版本、虚拟总线类型,适配ethtool -i查询。
第17行:通过载波状态判断链路是否正常,返回网卡在线状态。
第21-24行:定义6项自定义统计字段,包含收发流量、丢包、队列状态。
第31-32行:向ethtool内核接口拷贝统计字段名称。
第39-40行:返回统计项总数,适配ethtool动态统计查询。
第53-58行:实时读取设备统计数据和当前队列长度,上报给用户态工具。
第62-68行:绑定所有ethtool回调接口,完成工具功能适配。
9.6.1.10. sysfs调试开关¶
基于sysfs文件系统实现用户态debug调试开关,用户可通过读写/sys文件节点,动态开启/关闭报文详细日志打印,无需重新编译驱动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | /* sysfs属性:显示debug开关状态 */
static ssize_t debug_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
/* 从device取net_device */
struct net_device *ndev = to_net_dev(dev);
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 输出0=关 1=开 */
return sprintf(buf, "%d\n", priv->debug ? 1 : 0);
}
/* sysfs属性:设置debug开关 */
static ssize_t debug_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
/* 从device取net_device */
struct net_device *ndev = to_net_dev(dev);
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 解析后的数值 */
unsigned long val;
/* 将字符串解析为无符号长整型 */
if (kstrtoul(buf, 0, &val))
return -EINVAL;
/* 非0即开启,0为关闭 */
priv->debug = val ? true : false;
/* 打印状态 */
netdev_info(ndev, "报文详细打印: %s\n", priv->debug ? "开启" : "关闭");
/* 返回已写入字节数 */
return count;
}
/* 读写属性宏 */
static DEVICE_ATTR_RW(debug);
/* sysfs属性指针数组 */
static struct attribute *net_lb_attrs[] = {
&dev_attr_debug.attr, /* debug开关属性 */
NULL,
};
/* sysfs属性组 */
static const struct attribute_group net_lb_attr_group = {
.name = "loopback", /* 属性组目录名 */
.attrs = net_lb_attrs, /* 属性指针数组 */
};
|
关键说明:
第11行:读取设备私有数据中的debug状态,格式化输出到用户态sysfs文件。
第27-37行:解析用户态写入的字符串数据,转换为内核数值类型,仅0、1可生效,非法输入直接忽略。
第41行:通过DEVICE_ATTR宏创建sysfs属性文件,配置0644权限。
9.6.1.11. 设备初始化与资源初始化函数¶
基于内核通用以太网初始化接口完成虚拟网卡基础配置,统一初始化设备私有资源、并发锁、报文队列、NAPI异步机制、设备回调接口与硬件特性参数,为设备注册上线完成资源初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /* 网络设备初始化 */
static void net_lb_netdev_setup(struct net_device *ndev)
{
/* 获取私有数据指针 */
struct net_lb_priv *priv = netdev_priv(ndev);
/* 初始化以太网标准字段 */
ether_setup(ndev);
/* 保存网络设备指针到私有数据 */
priv->ndev = ndev;
/* 初始化回环队列自旋锁 */
spin_lock_init(&priv->lb_lock);
/* 初始化回环skb队列 */
skb_queue_head_init(&priv->lb_queue);
/* 绑定net_device操作回调 */
ndev->netdev_ops = &net_lb_ops;
/* 绑定ethtool操作回调 */
ndev->ethtool_ops = &net_lb_ethtool_ops;
/* 设置设备特性 */
ndev->features = 0; /* 无硬件特性 */
ndev->hw_features = 0; /* 无可切换硬件特性 */
/* 设置MTU */
ndev->mtu = ETH_DATA_LEN; /* 标准以太网MTU 1500字节 */
/* 设置需要的最小头空间 */
ndev->needed_headroom = NET_IP_ALIGN; /* 2字节对齐头空间 */
/* 生成随机MAC地址 */
eth_hw_addr_random(ndev);
/* 初始化NAPI */
netif_napi_add(ndev, &priv->napi, net_lb_napi_poll);
}
|
关键说明:
第8行:调用内核标准ether_setup接口,批量初始化以太网设备基础字段,避免手动配置冗余参数。
第14行:初始化队列自旋锁,保障多线程并发下报文队列操作安全。
第16行:skb报文队列,构建空的回环报文缓冲队列。
第19-21行:绑定网络设备核心操作回调集与ethtool工具回调接口,完成设备功能接口注册。
第24-25行:清零设备硬件特性与可切换硬件特性,明确当前纯软件虚拟网卡无任何硬件卸载能力。
第28行:采用内核标准宏ETH_DATA_LEN配置MTU(1500字节)。
第31行:设置报文头部最小预留空间为NET_IP_ALIGN(2字节),满足内核IP报文内存对齐要求。
第34行:调用内核标准eth_hw_addr_random接口生成随机合法以太网MAC地址。
第37行:注册NAPI轮询回调函数,初始化异步收包机制,依托NAPI框架实现高效报文轮询处理,替代中断轮询模式。
9.6.1.12. 模块初始化入口函数¶
采用通用alloc_netdev动态创建自定义命名虚拟网卡,依次完成网卡分配、内核注册、sysfs属性组创建,实现设备上线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | /* 模块加载函数 */
static int __init net_lb_init(void)
{
/* 返回值 */
int ret;
/* 打印加载信息 */
pr_info("虚拟回环网卡加载\n");
/* 分配网络设备,命名为lbnetX */
lb_ndev = alloc_netdev(sizeof(struct net_lb_priv), "lbnet%d",
NET_NAME_UNKNOWN, net_lb_netdev_setup);
if (!lb_ndev) {
pr_err("alloc_netdev 失败\n");
return -ENOMEM;
}
/* 注册网络设备到内核 */
ret = register_netdev(lb_ndev);
if (ret) {
pr_err("register_netdev 失败: %d\n", ret);
goto err_free_netdev;
}
/* 创建sysfs属性组,在/sys/class/net/<dev>/loopback/下 */
ret = sysfs_create_group(&lb_ndev->dev.kobj, &net_lb_attr_group);
if (ret) {
pr_err("sysfs 属性组创建失败: %d\n", ret);
goto err_unregister_netdev;
}
return 0;
err_unregister_netdev:
/* 注销已注册的设备 */
unregister_netdev(lb_ndev);
err_free_netdev:
/* 释放设备内存 */
free_netdev(lb_ndev);
/* 置空指针 */
lb_ndev = NULL;
/* 返回错误码 */
return ret;
}
|
关键说明:
第11-16行:调用内核通用alloc_netdev创建网络设备,设备名称自动生成lbnet0、lbnet1格式,同时绑定私有数据内存大小与net_lb_netdev_setup设备初始化回调,设备创建即完成基础参数初始化。
第19-23行:将初始化完成的虚拟网卡注册到内核网络子系统,成功后系统可识别lbnetX设备。
第26-30行:创建独立sysfs属性组,统一挂载在/sys/class/net/<设备名>/loopback/目录下。
9.6.1.13. 模块注销退出函数¶
针对初始化阶段创建的sysfs属性组、网络设备、NAPI机制、设备内存进行逐一释放,避免驱动卸载后的内核资源残留与内存泄漏问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* 模块卸载函数 */
static void __exit net_lb_exit(void)
{
/* 获取私有数据 */
struct net_lb_priv *priv = netdev_priv(lb_ndev);
/* 打印卸载信息 */
pr_info("虚拟回环网卡卸载\n");
/* 移除sysfs属性组 */
sysfs_remove_group(&lb_ndev->dev.kobj, &net_lb_attr_group);
/* 注销网络设备 */
unregister_netdev(lb_ndev);
/* 移除NAPI */
netif_napi_del(&priv->napi);
/* 释放网络设备 */
free_netdev(lb_ndev);
}
|
关键说明:
第11行:释放sysfs文件系统资源,避免用户态文件节点残留。
第14行:调用内核标准接口注销虚拟网络设备,从内核网络子系统中移除lbnetX设备。
第17行:删除设备NAPI异步处理结构体,彻底注销NAPI轮询机制,释放报文异步处理相关资源。
第20行:释放网络设备内存,包含设备本身内存与私有数据内存,彻底回收所有驱动占用资源。
9.6.2. Makefile说明¶
本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、CROSS_COMPILE和obj-m即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
# KERNEL_DIR=/home/guest/LubanCat_Linux_Generic_Full_SDK/kernel-6.1
# KERNEL_DIR=/home/guest/LubanCat_Linux_rk3588_SDK/kernel
#指定目标架构为arm64
ARCH=arm64
#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
# CROSS_COMPILE=/home/guest/LubanCat_Linux_Generic_Full_SDK/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#导出为环境变量
export ARCH CROSS_COMPILE
#指定要编译的内核模块目标文件
obj-m := net_loopback.o
#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
#清理编译生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
|
9.6.3. 编译驱动¶
在实验目录下输入 make 即可编译驱动,编译得到内核模块net_loopback.ko。
9.6.4. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
9.6.4.1. 实验操作¶
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # 加载驱动
sudo insmod net_loopback.ko
# 信息输出如下
[ 158.054235] 虚拟回环网卡加载
[ 158.283334] lbnet0: 虚拟回环网卡已打开
# 查看lbnet0网卡
ifconfig lbnet0
# 信息输出如下
lbnet0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
ether ae:4c:58:a4:f1:82 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
|
可以看到虚拟网卡成功注册,设备名字为lbnet0,网卡状态<UP,BROADCAST,RUNNING,MULTICAST>分别为已启用、支持广播、载波正常、支持多播,mtu 1500和驱动配置一致,MAC地址ae:4c:58:a4:f1:82是随机生成的,每次加载模块可能不同。 因为尚未设置ip和数据收发,所以收发数据包统计都是0。
设置ip并进行ping测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | # 禁用NetworkManager对虚拟网卡控制
sudo nmcli device set lbnet0 managed no
# 手动配置lbnet0虚拟网卡ip和掩码
sudo ip addr add 10.0.0.1/24 dev lbnet0
# 查看lbnet0网卡
ifconfig lbnet0
# 信息输出如下
lbnet0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.0.1 netmask 255.255.255.0 broadcast 0.0.0.0
ether ae:4c:58:a4:f1:82 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
# 查看网关信息
route -n
# 信息输出如下
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 lbnet0
# ping命令测试,ping 10次
ping 10.0.0.2 -c 10
# 信息输出如下
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.184 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.203 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=0.149 ms
64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=0.202 ms
64 bytes from 10.0.0.2: icmp_seq=5 ttl=64 time=0.205 ms
64 bytes from 10.0.0.2: icmp_seq=6 ttl=64 time=0.210 ms
64 bytes from 10.0.0.2: icmp_seq=7 ttl=64 time=0.212 ms
64 bytes from 10.0.0.2: icmp_seq=8 ttl=64 time=0.144 ms
64 bytes from 10.0.0.2: icmp_seq=9 ttl=64 time=0.214 ms
64 bytes from 10.0.0.2: icmp_seq=10 ttl=64 time=0.201 ms
--- 10.0.0.2 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9016ms
rtt min/avg/max/mdev = 0.144/0.192/0.214/0.024 ms
# 查看lbnet0网卡
ifconfig lbnet0
# 信息输出如下
lbnet0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.0.1 netmask 255.255.255.0 broadcast 0.0.0.0
ether ae:4c:58:a4:f1:82 txqueuelen 1000 (Ethernet)
RX packets 11 bytes 868 (868.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 11 bytes 1022 (1022.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
|
设置ip地址和掩码后通过ifconfig和route命令可以看到ip、网关和掩码设置成功。使用ping命令向10.0.0.2发10个包,可以看到全部包发送成功。 由于ping 10.0.0.2时,协议栈在发送第一个ICMP请求之前,必须先知道10.0.0.2的MAC地址。因为此时内核的ARP/邻居表中没有10.0.0.2的记录,协议栈会先发出一个ARP请求,虚拟回环驱动将其翻转为ARP应答回送,因此ping 10次收发统计为11次(1 ARP + 10 ICMP)。
开启debug接口查看报文详细信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | # 开启debug信息打印
sudo sh -c "echo 1 > /sys/class/net/lbnet0/loopback/debug"
# ping命令测试,ping 1次
ping 10.0.0.2 -c 1
# 信息输出如下
[ 2118.226279] lbnet0: 报文详细打印: 开启
[ 2125.248503] lbnet0:
[ 2125.248503] ---[TX]-- lbnet0 -- 98 bytes --
[ 2125.248503] | ETH dst=00:00:00:00:00:00 src=ae:4c:58:a4:f1:82 IPv4
[ 2125.248503] | IP 10.0.0.1 --> 10.0.0.2 ICMP ttl=64 len=84
[ 2125.248503] | ICMP Echo-Request id=0x332c seq=1
[ 2125.248503] --- fields ---
[ 2125.248588] 0000 dst-mac 00 00 00 00 00 00 00:00:00:00:00:00
[ 2125.248603] 0006 src-mac ae 4c 58 a4 f1 82 ae:4c:58:a4:f1:82
[ 2125.248614] 000c ethertype 08 00 0x0800 (IPv4)
[ 2125.248626] 000e ver/ihl 45 v4, ihl=5 (20 bytes)
[ 2125.248637] 000f tos 00 0x00
[ 2125.248647] 0010 tot-len 00 54 84
[ 2125.248657] 0012 id 3e f0 0x3ef0
[ 2125.248666] 0014 flags/frag 40 00 flags=0x2 frag=0
[ 2125.248676] 0016 ttl 40 64
[ 2125.248686] 0017 protocol 01 0x01 (ICMP)
[ 2125.248695] 0018 checksum e7 b6 0xe7b6
[ 2125.248706] 001a src-ip 0a 00 00 01 10.0.0.1
[ 2125.248716] 001e dst-ip 0a 00 00 02 10.0.0.2
[ 2125.248726] 0022 type 08 0x08 (Echo-Request)
[ 2125.248736] 0023 code 00 0x00
[ 2125.248746] 0024 checksum 52 e6 0x52e6
[ 2125.248755] 0026 id 33 2c 0x332c
[ 2125.248765] 0028 seq 00 01 1
[ 2125.248778] 002a payload 7f 5d 8d 6a 00 00 00 00 a4 51 02 00 00 00 00 00 (payload 0-16/56 bytes)
[ 2125.248793] 003a cont 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (payload 16-32/56 bytes)
[ 2125.248808] 004a cont 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f (payload 32-48/56 bytes)
[ 2125.248820] 005a cont 30 31 32 33 34 35 36 37 (payload 48-56/56 bytes)
[ 2125.248854] lbnet0:
[ 2125.248854] ---[RX]-- lbnet0 -- 98 bytes --
[ 2125.248854] | ETH dst=ae:4c:58:a4:f1:82 src=00:00:00:00:00:00 IPv4
[ 2125.248854] | IP 10.0.0.2 --> 10.0.0.1 ICMP ttl=64 len=84
[ 2125.248854] | ICMP Echo-Reply id=0x332c seq=1
[ 2125.248854] --- fields ---
[ 2125.248869] 0000 dst-mac ae 4c 58 a4 f1 82 ae:4c:58:a4:f1:82
[ 2125.248882] 0006 src-mac 00 00 00 00 00 00 00:00:00:00:00:00
[ 2125.248893] 000c ethertype 08 00 0x0800 (IPv4)
[ 2125.248903] 000e ver/ihl 45 v4, ihl=5 (20 bytes)
[ 2125.248913] 000f tos 00 0x00
[ 2125.248922] 0010 tot-len 00 54 84
[ 2125.248931] 0012 id 3e f0 0x3ef0
[ 2125.248941] 0014 flags/frag 40 00 flags=0x2 frag=0
[ 2125.248951] 0016 ttl 40 64
[ 2125.248960] 0017 protocol 01 0x01 (ICMP)
[ 2125.248970] 0018 checksum e7 b6 0xe7b6
[ 2125.248980] 001a src-ip 0a 00 00 02 10.0.0.2
[ 2125.248990] 001e dst-ip 0a 00 00 01 10.0.0.1
[ 2125.249000] 0022 type 00 0x00 (Echo-Reply)
[ 2125.249009] 0023 code 00 0x00
[ 2125.249019] 0024 checksum 5a e6 0x5ae6
[ 2125.249028] 0026 id 33 2c 0x332c
[ 2125.249037] 0028 seq 00 01 1
[ 2125.249050] 002a payload 7f 5d 8d 6a 00 00 00 00 a4 51 02 00 00 00 00 00 (payload 0-16/56 bytes)
[ 2125.249065] 003a cont 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (payload 16-32/56 bytes)
[ 2125.249079] 004a cont 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f (payload 32-48/56 bytes)
[ 2125.249092] 005a cont 30 31 32 33 34 35 36 37 (payload 48-56/56 bytes)
[ 2130.283465] lbnet0:
[ 2130.283465] ---[TX]-- lbnet0 -- 42 bytes --
[ 2130.283465] | ETH dst=00:00:00:00:00:00 src=ae:4c:58:a4:f1:82 ARP
[ 2130.283465] | ARP Request 10.0.0.1(ae:4c:58:a4:f1:82) --> 10.0.0.2(00:00:00:00:00:00)
[ 2130.283465] --- fields ---
[ 2130.283673] 0000 dst-mac 00 00 00 00 00 00 00:00:00:00:00:00
[ 2130.283749] 0006 src-mac ae 4c 58 a4 f1 82 ae:4c:58:a4:f1:82
[ 2130.283811] 000c ethertype 08 06 0x0806 (ARP)
[ 2130.283873] 000e hrd-type 00 01 0x0001 (Ethernet)
[ 2130.283933] 0010 pro-type 08 00 0x0800 (IPv4)
[ 2130.283988] 0012 hrd-len 06 6
[ 2130.284041] 0013 pro-len 04 4
[ 2130.284099] 0014 opcode 00 01 0x0001 (Request)
[ 2130.284160] 0016 sender-mac ae 4c 58 a4 f1 82 ae:4c:58:a4:f1:82
[ 2130.284221] 001c sender-ip 0a 00 00 01 10.0.0.1
[ 2130.284281] 0020 target-mac 00 00 00 00 00 00 00:00:00:00:00:00
[ 2130.284342] 0026 target-ip 0a 00 00 02 10.0.0.2
[ 2130.286467] lbnet0:
[ 2130.286467] ---[RX]-- lbnet0 -- 42 bytes --
[ 2130.286467] | ETH dst=ae:4c:58:a4:f1:82 src=00:00:00:00:00:00 ARP
[ 2130.286467] | ARP Reply 10.0.0.2(00:00:00:00:00:00) --> 10.0.0.1(ae:4c:58:a4:f1:82)
[ 2130.286467] --- fields ---
[ 2130.286649] 0000 dst-mac ae 4c 58 a4 f1 82 ae:4c:58:a4:f1:82
[ 2130.286707] 0006 src-mac 00 00 00 00 00 00 00:00:00:00:00:00
[ 2130.286755] 000c ethertype 08 06 0x0806 (ARP)
[ 2130.286800] 000e hrd-type 00 01 0x0001 (Ethernet)
[ 2130.286845] 0010 pro-type 08 00 0x0800 (IPv4)
[ 2130.286886] 0012 hrd-len 06 6
[ 2130.286927] 0013 pro-len 04 4
[ 2130.286969] 0014 opcode 00 02 0x0002 (Reply)
[ 2130.287017] 0016 sender-mac 00 00 00 00 00 00 00:00:00:00:00:00
[ 2130.287062] 001c sender-ip 0a 00 00 02 10.0.0.2
[ 2130.287110] 0020 target-mac ae 4c 58 a4 f1 82 ae:4c:58:a4:f1:82
[ 2130.287155] 0026 target-ip 0a 00 00 01 10.0.0.1
|
开启debug开关后,执行ping 10.0.0.2 -c 1命令,内核日志中依次打印了四组报文信息,分别对应一次ICMP收发和一次ARP收发的翻转过程。
ICMP部分:
发送方向(TX)的报文总长98字节,其中以太网头14字节、IP头20字节、ICMP头8字节、ICMP负载56字节。
以太网层中,源MAC为虚拟网卡的随机地址ae:4c:58:a4:f1:82,目的MAC为全零(因为10.0.0.2在本地不存在对应的真实邻居表项,协议栈无法填充有效的目的MAC),以太类型为0x0800表示是IPv4报文。
IP层中,源地址为10.0.0.1,目的地址为10.0.0.2,TTL为64,协议字段为0x01即ICMP,IP头校验和为0xe7b6。
ICMP层中,类型为0x08即Echo-Request,代码为0,校验和为0x52e6,标识符为0x332c,序列号为1,负载的56字节由时间戳和递增填充数据组成。
该报文经net_lb_transform()翻转后,在接收方向(RX)以回送包的形式重新进入协议栈:以太网层的源和目的MAC发生互换,IP层的源和目的地址互换为10.0.0.2 -> 10.0.0.1,由于两个地址数值相同只是交换位置,IP头校验和保持0xe7b6不变;ICMP类型从0x08(Echo-Request)变为0x00(Echo-Reply),校验和相应地从0x52e6更新为0x5ae6,标识符和序列号保持0x332c和1不变,56字节的负载数据原封不动地回送。
ARP部分:
发送方向的报文总长42字节,由14字节以太网头和28字节ARP载荷组成。
以太网层的目的MAC同样为全零,源MAC为虚拟网卡地址,以太类型为0x0806表示ARP。
ARP载荷中,硬件类型为以太网(0x0001),协议类型为IPv4(0x0800),硬件地址长度6,协议地址长度4,操作码为0x0001即Request,发送方为10.0.0.1(MAC ae:4c:58:a4:f1:82),目标方为10.0.0.2(MAC 全零)。
翻转后的接收方向报文中,以太网层源和目的MAC互换,ARP操作码从0x0001(Request)变为0x0002(Reply),发送方与目标方的IP和MAC全部互换,即发送方变为10.0.0.2(MAC 全零)、目标方变为10.0.0.1(MAC ae:4c:58:a4:f1:82),从而模拟了对端设备对ARP请求的应答。
整个过程中,所有字段的偏移位置、字节序和翻转均与net_lb_transform()函数的实现一一对应。
使用ethtool工具进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | # 查看驱动信息
ethtool -i lbnet0
# 信息输出如下
driver: net_loopback
version: 1.0
firmware-version:
expansion-rom-version:
bus-info: virtual
supports-statistics: yes
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no
# 查看链路状态
ethtool lbnet0
# 信息输出如下
Settings for lbnet0:
Link detected: yes
# 查看统计信息
ethtool -S lbnet0
# 信息输出如下
NIC statistics:
tx_packets: 13
tx_bytes: 1162
rx_packets: 13
rx_bytes: 980
tx_dropped: 0
lb_queue_len: 0
|
可以看到ethtool可以正常操作lbnet0虚拟网卡,关于ethtool工具使用和信息说明不作过多展开,感兴趣可以自行搜索资料了解。
9.6.5. 实验注意事项¶
虚拟网卡默认名为lbnet0,注意与系统原生lo回环设备区分。
debug开关开启后会输出大量报文日志,测试建议只ping 1个包,调试完成后及时关闭。
驱动仅对ARP、IPv4、ICMP 报文做协议翻转,其他协议仅完成二层回环。
高流量测试时可调整驱动NAPI_BUDGET预算值,平衡处理性能。