Linux内核通用网络驱动基础内容:
struct net_devicealloc_netdev/free_netdevregister_netdevnetdev_opsndo_start_xmit- skb 基础
- NAPI 原理
一、一句话概括以太网驱动 = 找到硬件 → 初始化 MAC/PHY → 注册网卡 → 实现发包 + 收包 → 提供状态统计
二、完整标准流程(按执行顺序)
1. 设备匹配与 probe(驱动入口)
- 驱动通过 platform / PCI / USB 总线注册
- 硬件被枚举后,内核调用
probe函数- 做基础资源申请:
地址映射
ioremap复位硬件
读取芯片 ID 校验
probe
↳ 资源映射、复位、ID检测
2. 分配 net_device 结构体
ndev = alloc_netdev(sizeof(priv), "eth%d", ether_setup);
- 分配内核标准网卡对象
- 附带一块私有数据区 priv
- 初始化以太网默认参数
3. 初始化私有数据 & 硬件基本配置
初始化锁、队列、定时器
读取 / 设置 MAC 地址
设置网卡基本特性:广播、多播、校验和等
priv = netdev_priv(ndev);
eth_hw_addr_set(ndev, mac_addr);
4. 填充 netdev_ops(驱动最重要的接口)
内核靠这组函数操作你的网卡:
static const struct net_device_ops drv_netdev_ops = { .ndo_open = drv_open, .ndo_stop = drv_stop, .ndo_start_xmit = drv_start_xmit, .ndo_set_mac = eth_mac_addr, // 通用或自己实现 .ndo_get_stats = drv_get_stats, };这是驱动的灵魂。
5. 注册网卡到内核
register_netdev(ndev);执行后:
- 系统出现
eth0- 可被
ip link看到- 可被 NetworkManager 管理
三、网卡启用流程(ip link set up → ndo_open)
用户
ifconfig eth0 up时内核调用ndo_open必须做:
- 硬件使能:时钟、电源、复位释放
- 初始化 MAC:配置帧长度、模式、过滤
- 初始化 PHY:自动协商、获取链路状态
- 申请中断
request_irq- 初始化 NAPI(收包用)
- 启动发送队列
netif_start_queue(ndev)完成后,网卡真正可以收发数据。
四、发包核心流程(ndo_start_xmit)
协议栈下发一个
skb,驱动必须:
校验 skb 合法性
映射数据到 DMA(或拷贝到芯片 FIFO)
启动硬件发送
发送完成后:
- 释放 skb
- 唤醒发送队列
出错返回错误码
ndo_start_xmit
↳ 封装帧
↳ 写入硬件
↳ 启动发送
↳ 返回 NETDEV_TX_OK
五、收包核心流程(中断 + NAPI)
硬件收到帧 → 触发中断
中断函数只做一件事:关闭中断,调度 NAPI
NAPI poll 函数真正收包:
- 从硬件 FIFO/DMA 读数据
- 分配 skb、拷贝数据
- 填充协议类型、时间戳
- 上交协议栈:
netif_receive_skb(skb)收完一轮重新开中断
中断
↳ napi_schedule
↳ napi_poll
↳ 读硬件 → 组skb → 上交协议栈
六、网卡停用流程(ip link set down → ndo_stop)
- 停止发送队列
netif_stop_queue- 关闭中断
- 停止 NAPI
- 关闭 MAC/PHY
- 断电 / 复位硬件
七、卸载流程(remove)
unregister_netdev(ndev); free_netdev(ndev); iounmap(...);
八、极简流程图(最核心,建议记住)
probe ↓ alloc_netdev ↓ 填充 netdev_ops ↓ register_netdev → 出现 eth0 ↓ ndo_open(up时调用) ↳ 初始化MAC/PHY ↳ 申请中断 ↳ 启动NAPI ↳ 启动发送队列 ↓ ndo_start_xmit(发包) ↓ NAPI poll(收包) ↓ ndo_stop(down时调用) ↓ unregister_netdev ↓ free_netdev
九、和 WiFi 驱动的关系(非常重要)
以太网驱动 = 纯网络设备驱动 WiFi 驱动 = 以太网驱动 + 无线协议层(cfg80211/mac80211)
- 发包:都是
ndo_start_xmit- 收包:都是 NAPI + skb
- 设备管理:都是 net_device
学会以太网,WiFi 驱动你就已经学会 60%
接下来,逐个讲解各个核心内容。
struct net_device
这是 Linux 网络驱动最核心的结构体 ,以太网、WiFi、4G/5G 全部用它。
你必须彻底看懂它,才能真正懂网络驱动。
0. 一句话定位
struct net_device= 内核眼中的一张网卡系统里看到的
eth0、wlan0、ens33本质都是它。一、定义在哪里?
include/linux/netdevice.h如下所示:
这是 Linux 网络驱动最重要头文件。
二. 核心组成(必须记住)
我把它分成 5 大块,驱动开发只需要关心这些:
① 设备身份信息
char name[IFNAMSIZ]; // 网卡名 "eth0" "wlan0" unsigned char dev_addr[]; // MAC 地址 unsigned int flags; // 设备状态(UP、RUNNING、广播...)② 最重要:操作函数集合 netdev_ops
驱动必须提供,内核靠它调用驱动
const struct net_device_ops *netdev_ops;里面包含:
.ndo_open // 启用网卡 .ndo_stop // 停止网卡 .ndo_start_xmit // 发送数据(核心!) .ndo_set_mac // 设置MAC地址③ 收发包相关
struct netdev_queue *tx_queue; // 发送队列 unsigned int mtu; // 最大传输单元 1500 unsigned int needed_headroom; // 帧头空间④ 私有数据(驱动自己用)
unsigned long priv[]; // 私有数据区获取方式:
struct my_priv *priv = netdev_priv(ndev);存放:硬件地址、状态、队列、统计、寄存器等
⑤ 统计信息
struct rtnl_link_stats64 stats;
- 收发包数量
- 丢包数
- 错误数
三. 它从哪里来?
由内核函数分配:
struct net_device *ndev; ndev = alloc_netdev(...); // 分配 register_netdev(ndev); // 让系统看见WiFi 驱动是封装版:
ieee8011_alloc_hw(...) → 内部自动 alloc_netdev
四. 内核如何使用 net_device?
非常简单:
协议栈 ↓ 找到 net_device ↓ 调用 netdev_ops 里的函数 ↓ 驱动操作硬件内核不关心硬件是什么,只认识 net_device
五. 以太网 vs WiFi 里的 net_device
以太网
直接用:
alloc_netdev → register_netdevWiFi
由
mac80211层帮你创建管理:
ieee80211_alloc_hw → 内部创建 net_device但最终对内核来说,都是 net_device
几个常见操作接口
一、分配 / 释放相关(创建网卡)
1. 分配 net_device
struct net_device *alloc_netdev( int sizeof_priv, const char *name, void (*setup)(struct net_device *) );
作用:创建一张网卡
常用简化版:
struct net_device *ndev = alloc_etherdev(sizeof(struct my_priv));
三个参数含义
sizeof_priv 驱动私有数据大小 驱动自己定义的
priv结构体多大,就传多大。name网卡名模板,比如:
"eth%d""wlan%d"``%d会被内核自动编号(0、1、2...)setup 函数指针,内核用来初始化网卡默认参数 以太网一般传
ether_setup
①最常用写法(以太网)
struct net_device *ndev; // 分配 ndev = alloc_netdev(sizeof(struct my_priv), "eth%d", ether_setup);更简单的宏(以太网专用)
ndev = alloc_etherdev(sizeof(struct my_priv));内部就是封装了 alloc_netdev + ether_setup
②分配后得到什么?
- 一个完整的
struct net_device- 后面跟着一块私有数据区(priv)
- 基础参数被
ether_setup初始化好你可以直接:
struct my_priv *priv = netdev_priv(ndev);
③执行成功 vs 失败
成功:返回非 NULL 指针
失败:返回 NULL所以必须判断:
ndev = alloc_etherdev(sizeof(*priv));
if (!ndev)
return -ENOMEM;
④它和 register_netdev 的关系
alloc_netdev // 创造网卡(内核里有了) ↓ register_netdev // 把网卡暴露给系统(ip link 能看到)alloc 只是创建对象,register 才让它出现!
⑤内存布局(非常重要)
alloc_netdev分配的内存是:
[ struct net_device ] [ 私有数据 priv ]连续一块内存,所以才能用
netdev_priv()快速获取私有数据。
2. 释放 net_device
void free_netdev(struct net_device *ndev);
- 只有未注册的设备才能用这个释放。
二、注册 / 注销相关(让系统看见网卡)1. 注册网卡
int register_netdev(struct net_device *ndev);
- 执行后:
ip link看到 eth0/wlan0- 失败返回 -errno
2. 注销网卡
void unregister_netdev(struct net_device *ndev);
- 卸载驱动前必须调用
三、获取私有数据(驱动最常用)
void *netdev_priv(const struct net_device *ndev);用法:
struct my_priv *priv = netdev_priv(ndev);本质就是:
计算出 ndev 后面那块私有数据的地址,并返回给你。
就这么简单!
一般使用流程:
// 1. 分配:net_device + 私有数据 一起分配 ndev = alloc_netdev(sizeof(struct my_priv), ...); // 2. 获取:拿到已经分配好的私有数据地址 priv = netdev_priv(ndev); // 3. 使用:你自己往里面填值 priv->xxx = 123; priv->data = 456;总结三句话(背下来)
alloc_netdev(sizeof_priv)时,私有数据就已经分配好了netdev_priv(ndev)只是获取这块内存的地址- 获取后,你自己读写里面的内容
flags网卡状态
struct net_device里的 flags 是网卡的运行状态标志,在内核中定义在:
include/uapi/linux/if.h下面只列驱动里最常见、必须认识的,不用记全部。
一、最常用的 flags(驱动高频出现)
1.
IFF_UP
- 网卡被启用
ip link set eth0 up时内核会置位- 驱动不能随便改,由内核管理
2.
IFF_RUNNING
表示链路已连通(网口插好、载波检测到)
以太网:插上网线 → 置位
WiFi:关联 AP 成功 → 驱动要自己置位
netif_carrier_on(ndev); // 内部会设置 IFF_RUNNING
3.
IFF_BROADCAST
- 支持以太网广播
- 几乎所有网卡都有
4.
IFF_MULTICAST
- 支持组播
- 以太网、WiFi 必带
5.
IFF_ALLMULTI
- 接收所有组播包
- 驱动在处理多播列表时会用到
6.
IFF_PROMISC
- 混杂模式
tcpdump -i eth0 promisc时开启- 驱动要在
ndo_set_rx_mode里处理7.
IFF_LOOPBACK
- 环回网卡
lo专用8.
IFF_POINTOPOINT
- 点对点链路(PPP、VPN 等)
二、驱动里最常用的组合(默认以太网)
ndev->flags = IFF_BROADCAST | IFF_MULTICAST;
三、简单记忆口诀
IFF_UP:网卡启用了IFF_RUNNING:链路通了IFF_BROADCAST:支持广播IFF_MULTICAST:支持组播IFF_PROMISC:开了抓包模式
struct net_device_ops
struct net_device_ops是 网络驱动的 "回调函数集合" ,也就是:内核要操作网卡时,调用驱动里的哪些函数。
1. 它是干嘛的?
内核协议栈本身不知道你具体硬件怎么操作 ,所以驱动必须提供一套标准接口,告诉内核:
- 网卡启用时调用哪个函数
- 停止时调用哪个
- 要发数据包时调用哪个
- 设置 MAC 地址时调用哪个
这一套接口,就是
netdev_ops。
2. 定义在哪
include/linux/netdevice.h结构大概长这样(只列关键):
struct net_device_ops { netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev); int (*ndo_open)(struct net_device *dev); int (*ndo_stop)(struct net_device *dev); int (*ndo_set_mac)(struct net_device *dev, void *addr); void (*ndo_set_rx_mode)(struct net_device *dev); struct rtnl_link_stats64 * (*ndo_get_stats64)(struct net_device *dev, struct rtnl_link_stats64 *stats); };
3. 几个必实现、必认识的成员
①
ndo_open
- 触发时机:
ip link set eth0 up- 驱动里要做:
- 硬件上电、初始化 MAC/PHY
- 申请中断
- 初始化 NAPI
- 启动发送队列
netif_start_queue
②
ndo_stop
- 触发时机:
ip link set eth0 down- 驱动里要做:
- 停止队列
- 关闭中断
- 停止 NAPI
- 硬件下电
③
ndo_start_xmit(核心中的核心)
- 触发时机:内核要发一个数据包
- 传入:
struct sk_buff *skb- 驱动要做:
- 把 skb 数据发给硬件
- 启动发送
- 发送完成后释放 skb
- 返回:
NETDEV_TX_OK
④
ndo_set_mac
- 触发时机:
ip link set eth0 address xx:xx:xx:xx:xx:xx- 作用:设置网卡 MAC 地址
⑤
ndo_set_rx_mode
- 触发时机:设置混杂模式、组播、广播时
- 驱动要配置硬件接收过滤器
⑥
ndo_get_stats64
- 用于
ip -s link展示收发包、丢包统计
4. 驱动里标准用法(极简示例)
// 1. 定义你自己的实现函数 static netdev_tx_t my_drv_start_xmit(struct sk_buff *skb, struct net_device *dev) { // 发包逻辑 dev_kfree_skb(skb); return NETDEV_TX_OK; } static int my_drv_open(struct net_device *dev) { // 初始化硬件、中断、NAPI netif_start_queue(dev); return 0; } // 2. 填充 netdev_ops static const struct net_device_ops my_netdev_ops = { .ndo_open = my_drv_open, .ndo_start_xmit = my_drv_start_xmit, // 其他... }; // 3. 把 netdev_ops 挂到 net_device ndev->netdev_ops = &my_netdev_ops;
5. 一句话总结
netdev_ops就是驱动给内核的 "操作手册": 内核叫你干嘛,你就实现对应的 ndo_xxx 函数。
ndo_open→ 开机ndo_stop→ 关机ndo_start_xmit→ 发数据这三个是以太网驱动灵魂。
ndo_open
ndo_open 就是:网卡被启用(up)时,驱动要做的所有初始化工作。
用户敲命令:
ip link set eth0 up内核就会调用你驱动里的 ndo_open 函数。
1. ndo_open 是干嘛的?
当网卡从关闭 → 启动 时,内核调用它。它的使命只有一个:把硬件准备好,让网卡可以收发包!
2. 标准 ndo_open 必须做哪几件事?(背会这 5 步)
一个合格的
ndo_open必须做:① 硬件初始化
- 打开时钟
- 释放复位
- 初始化 MAC 控制器
- 初始化 PHY(网口)
② 申请中断
request_irq(irq, my_interrupt_handler, ...);收包靠中断。
③ 初始化 NAPI(收包核心)
napi_enable(&priv->napi);④ 启动发送队列
netif_start_queue(dev);告诉内核:我准备好了,可以发包了!
⑤ 打开硬件收发功能
- 启动 RX
- 启动 TX
3. 最简标准模板(背会它,走遍天下以太网都不怕)
static int my_ndo_open(struct net_device *dev) { struct my_priv *priv = netdev_priv(dev); // 1. 硬件初始化 hw_init(priv); // 2. 申请中断 request_irq(priv->irq, my_isr, ..., dev->name, dev); // 3. 使能NAPI napi_enable(&priv->napi); // 4. 启动发送队列 netif_start_queue(dev); // 5. 启动硬件收发 hw_start_tx_rx(priv); return 0; // 成功 }
4. 成功返回什么?失败返回什么?
- 成功:return 0
- 失败:return -errno 例如:
- return -ENXIO
- return -EINVAL
- return -ENOMEM
5. ndo_open 成功后意味着什么?
意味着:✅ 网卡 up✅ 硬件就绪✅ 中断开启✅ 队列启动✅ 可以收发包了!
ndo_stop
ndo_stop就是网卡被down时的 "清理函数" ,和ndo_open完全反过来。用户执行:
ip link set eth0 down # 或者老命令 ifconfig eth0 down内核就会调用驱动里的
ndo_stop。
它是干嘛的?
做三件事:
- 停止收发数据
- 释放资源(中断、关闭 NAPI、停止队列)
- 关闭硬件
目的:让网卡进入安静、低功耗、安全的关闭状态。
标准固定流程(背这个就够)
static int my_ndo_stop(struct net_device *dev) { struct my_priv *priv = netdev_priv(dev); // 1. 先停止队列:不让内核再发数据包下来 netif_stop_queue(dev); // 2. 关闭硬件收发 hw_disable_tx_rx(priv); // 3. 关闭 NAPI napi_disable(&priv->napi); // 4. 释放中断 free_irq(priv->irq, dev); // 5. 硬件下电/复位 hw_deinit(priv); return 0; }
必须做的 5 件事
**
netif_stop_queue(dev)**停止发送队列,防止协议栈继续发包。关闭硬件收发告诉芯片别再收包、发包。
**
napi_disable**关闭收包轮询。**
free_irq**释放中断,避免中断处理函数还在跑。硬件断电 / 复位进入低功耗状态。
和 ndo_open 对比记忆
ndo_open(up) ndo_stop(down) 硬件初始化 硬件反初始化 request_irq free_irq napi_enable napi_disable netif_start_queue netif_stop_queue 启动收发 关闭收发 就是一对逆操作。
返回值
- 成功:
return 0- 失败:
return -EXXX(一般很少失败)
ndo_start_xmit
网络驱动最重要、最核心、必须掌握的函数:没有之一!
我用最直白、最标准、驱动开发必懂的方式讲,看完你就懂发包流程。
1. 一句话定位
ndo_start_xmit = 内核让驱动 "发一个数据包"
- 内核协议栈有数据要发
- 找到你的网卡
- 调用你的 ndo_start_xmit 函数
- 把数据包交给你
2. 函数原型(必须记住)
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev);两个参数:
struct sk_buff *skb
- 内核发给你的数据包(装着要发的数据)
- 这是内核网络数据的标准格式。
struct net_device *dev
- 你的网卡设备。
3. 驱动里必须做什么?(核心 4 步)
你的驱动拿到
skb后,只需要干这 4 件事:① 从 skb 里拿出数据
数据在
skb->data,长度是skb->len。② 把数据发给硬件
通过寄存器、FIFO、DMA 或 USB/SDIO,把数据拷贝到网卡芯片。
③ 释放 skb
数据发出去后,内核不管了,驱动必须释放内存!
dev_kfree_skb(skb);④ 返回成功状态
return NETDEV_TX_OK;
4. 最简标准模板(背会它)
static netdev_tx_t my_ndo_start_xmit(struct sk_buff *skb, struct net_device *dev) { // 1. 获取私有数据 struct my_priv *priv = netdev_priv(dev); // 2. 把数据写到硬件(核心操作) // 比如:memcpy(priv->tx_buffer, skb->data, skb->len); // 比如:write_reg(TX_START, 1); // 3. 释放数据包(极其重要!) dev_kfree_skb(skb); // 4. 告诉内核:发送成功 return NETDEV_TX_OK; }
5. 关键注意点(面试 / 踩坑必问)
❌ 绝对不能做的事:
- 不能在这个函数里睡眠 (不能调用
msleep,mutex_lock)
- 它运行在原子上下文 / 软中断,睡了内核直接崩溃。
- 不能丢 skb
- 要么发出去,要么失败释放,必须 dev_kfree_skb。
✅ 硬件忙了怎么办?
如果硬件满了,发不了:
netif_stop_queue(dev); // 告诉内核:别发了,我满了! return NETDEV_TX_BUSY;等硬件空了,再唤醒:
netif_wake_queue(dev);更多待补充。
网络数据收发
在讲网络数据收发之前,我们先熟悉下sk_buff和NAPI
sk_buff
这是 Linux 网络最核心的数据结构 ,全称 Socket Buffer。
我用最简单、最形象、驱动必懂的方式讲,看完你再也不会懵。
1. 它是干嘛的?
sk_buff = 内核网络的 "数据包裹 / 快递盒"
- 不管是发送还是接收
- 不管是 TCP、UDP、IP
- 不管是以太网、WiFi、4G/5G
所有网络数据,在内核里全都是用 sk_buff 来传递的!
你在
ndo_start_xmit里收到的就是它,收包时上交给内核的也是它。
2. 核心结构(记住这 4 个)
不用记几千行结构体,驱动开发只需要懂这 4 个成员:
struct sk_buff { unsigned char *head; // 整个缓冲区起始地址 unsigned char *data; // **当前有效数据的起始地址(最重要)** unsigned int len; // **有效数据长度(最重要)** unsigned char *tail; // 数据尾部 unsigned char *end; // 整个缓冲区结束地址 };最重要的两个!
skb->data指向数据包真正内容的指针(以太网头 + IP 头 + 数据)- **
skb->len**数据包总长度
3. 驱动怎么用它?(发包 + 收包)
① 发包时(ndo_start_xmit)
内核把包给你,你直接读数据发给硬件:
// 数据地址:skb->data // 数据长度:skb->len // 拷贝到硬件发送 memcpy(hw_tx_buf, skb->data, skb->len);② 收包时(硬件收到数据)
你构造 skb,把数据填进去,交给内核:
struct sk_buff *skb = dev_alloc_skb(packet_len); memcpy(skb->data, hw_rx_buf, packet_len); skb->len = packet_len; netif_receive_skb(skb); // 上交协议栈
4. 几个必备操作函数(驱动高频用)
1) 分配一个 skb(收包用)
struct sk_buff *skb = dev_alloc_skb(buflen);2) 释放一个 skb(发包完成必须做!)
dev_kfree_skb(skb);3) 往 data 前面加头部(常用来写帧头)
skb_push(skb, bytes);4) 往 data 后面加数据
skb_put(skb, bytes);
5. 最简单的记忆法
skb = 装网络数据的标准盒子
- 你要发数据:内核给你一个装满的盒子
- 你要收数据:你造一个盒子装满,交给内核
驱动只做搬运工,不关心盒子里是什么协议!
6. 和 AIC8800/WiFi 驱动的关系
完全一样!
- WiFi 驱动收发的也是 sk_buff
- 只是数据内容从以太网帧 变成 802.11 无线帧
- 结构、API、操作方式 100% 通用
NAPI
直接参考:
网络数据包的发送
Linux 网络数据包完整发送流程不讲芯片、不讲 WiFi,只讲通用原理,看完你看任何驱动都秒懂。
一、一句话概括发包流程
应用程序发数据 → 协议栈封装 → 进入队列 → 调用驱动 ndo_start_xmit → 硬件发送 → 释放 skb
二、完整标准流程(从上到下)
1. 应用层发包
send(socket, data, len, 0);用户态把数据丢给内核。
2. 协议栈封装(TCP/UDP → IP → 以太网头)
内核依次加上头部:
- TCP/UDP 头
- IP 头
- 以太网头(WiFi 是 802.11 头)
最终封装成一个 struct sk_buff。
3. 进入发送队列(qdisc)
内核把包放入网卡的发送队列,做流量控制、排队。
4. 调用驱动的发包函数
内核从队列取出包,调用:
dev_queue_xmit(skb) ↳ 调用驱动的 ndo_start_xmit
5. 驱动处理(ndo_start_xmit)
驱动做三件事:
从
skb->data取数据写入硬件(FIFO/DMA)
释放 skb
dev_kfree_skb(skb);
6. 硬件发送
- 硬件从内存取数据
- 发到网线上 / 空气中
7. 发送完成(中断)
硬件发完产生中断,驱动:
- 统计计数
- 唤醒队列(如果之前满了)
三、极简流程图(背这个)
应用 send() ↓ 协议栈封装成 skb ↓ 进入发送队列 ↓ dev_queue_xmit() ↓ 驱动 ndo_start_xmit() ↓ 把 skb->data 发给硬件 ↓ dev_kfree_skb(skb) ↓ 硬件发送
四、关键点(必须懂)
发包的核心是 skb整个内核只认 skb。
驱动只负责搬运不关心协议,只把数据拷给硬件。
skb 必须由驱动释放 谁最后处理 skb,谁负责
dev_kfree_skb。不能阻塞
ndo_start_xmit运行在软中断,不能休眠。
以TCP IP为例,说明下从send到最后发送出去的整个调用过程,具体到某个文件的某个函数?
超详细!TCP 数据包从 send () → 网卡发出去 完整内核调用链
(具体到 内核函数名 + 核心路径,不绕弯、不抽象,学网络驱动必看)
我直接给你标准 Linux 5.x/6.x 内核的真实调用路径,你可以对应去源码里找。
0. 整体总览(一句话)
应用 send () → 系统调用 entry → TCP 层 → IP 层 → 邻居子系统 → 队列调度 → 驱动 ndo_start_xmit → 硬件发送
1. 应用层:
send()函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);→ 这是用户态 C 库函数 ,最终触发系统调用。
2. 系统调用入口(内核态)
文件:
net/socket.c
SYSCALL_DEFINE4(sendto, int, fd, void __user *, buf, size_t, len, unsigned int, flags, struct sockaddr __user *, addr, int, addr_len) { // ... sock_sendmsg(...) }→ 最终走到:
sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
3. 协议层分发(TCP 进入)
文件:
net/ipv4/af_inet.c
inet_sendmsg(...)根据 socket 类型(SOCK_STREAM)调用 TCP 发送函数:
4. TCP 层核心发送函数
文件:
net/ipv4/tcp.c
tcp_sendmsg(...)做什么:
- 拷贝用户数据
- 构建 TCP 段
- 加入发送队列
- 触发 TCP 输出
→ 调用:
文件:
net/ipv4/tcp_output.c
tcp_write_xmit(sk, mss_now, ...); ↳ tcp_transmit_skb(sk, skb, ...);
tcp_transmit_skb会构建 TCP 头 + IP 头
5. IP 层发送
文件:
net/ipv4/ip_output.c
ip_local_out(skb); ↳ __ip_local_out(skb); ↳ dst_output(skb);
dst_output是路由出口,最终调用:
ip_finish_output(skb); ↳ ip_finish_output2(skb);这里会:
- 查路由
- 查 ARP(获取下一跳 MAC)
- 构造以太网头
6. 邻居子系统(ARP + 以太网头)
文件:
net/core/neighbour.c
neigh_output(neigh, skb); ↳ dev_queue_xmit(skb);到这里,数据包已经变成: 以太网头 + IP 头 + TCP 头 + 数据
7. 队列调度(流量控制)
文件:
net/core/dev.c
dev_queue_xmit(struct sk_buff *skb) { // 选择发送队列 // 流量控制 qdisc // 最终调用: __dev_queue_xmit(skb, NULL); }
8. 最终调用到:驱动的发包函数!
文件:
net/core/dev.c
sch_direct_xmit(skb, q, dev, txq, ...); ↳ dev_hard_start_xmit(skb, dev, txq, ...);然后
dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, ...) { // 这里就是!!! const struct net_device_ops *ops = dev->netdev_ops; ops->ndo_start_xmit(skb, dev); }
9. 驱动层:你的 ndo_start_xmit
my_ndo_start_xmit(struct sk_buff *skb, struct net_device *dev) { // 1. 从 skb->data 读数据 // 2. 写入硬件寄存器/DMA // 3. 发送 // 4. dev_kfree_skb(skb); return NETDEV_TX_OK; }
10. 硬件发送
- DMA 从内存读数据
- 物理层发送到网口 / 空气
🔥 完整终极调用链(背会它,你就通透了)
send() ↓ sock_sendmsg() // net/socket.c ↓ inet_sendmsg() // net/ipv4/af_inet.c ↓ tcp_sendmsg() // net/ipv4/tcp.c ↓ tcp_transmit_skb()// net/ipv4/tcp_output.c ↓ ip_local_out() // net/ipv4/ip_output.c ↓ ip_finish_output2() ↓ neigh_output() // net/core/neighbour.c ↓ dev_queue_xmit() // net/core/dev.c ↓ dev_hard_start_xmit() ↓ ops->ndo_start_xmit(skb, dev) // 你的驱动!
一句话总结
TCP 数据从 send () 出发,穿过 TCP → IP → 邻居 → 队列, 最终一定调用你的驱动:ndo_start_xmit!
邻居子系统是啥?简单说:Linux 邻居子系统(neighbour subsystem),就是专门管 "下一跳 MAC 地址" 的模块。
对应你刚才的发送路径:
ip_finish_output2 ↓ neigh_output // 这就是邻居子系统入口 ↓ dev_queue_xmit
一句话定位
IP 地址 → 找对应 MAC 地址 → 封装以太网帧 这件事,全部由邻居子系统统一负责。
为什么需要它?
- 网络层(IP)只知道 下一跳 IP
- 但网卡发包必须要有 目的 MAC
- 从 IP → MAC 的映射,就是邻居要干的事
典型例子:ARP
核心结构
struct neighbour { unsigned char ha[ETH_ALEN]; // MAC 地址 struct net_device *dev; // 从哪个网卡发 void (*output)(...); // 发包函数 // ... 状态、缓存、定时器 };
它在发包时干了啥?
- 给你查邻居表(相当于 ARP 缓存)
- 如果已经有 MAC → 直接填到以太网头→ 调用
neigh->output()最终走到dev_queue_xmit- 如果没有 MAC → 触发 ARP 请求→ 包先缓冲,等拿到 MAC 再发
对应内核文件
include/linux/neighbour.hnet/core/neighbour.cIPv4 对应的是 ARP:
net/ipv4/arp.c
和你驱动的关系
- 驱动不用管 ARP
- 驱动只管:邻居子系统把 ** 完整以太网帧(带 MAC)** 构好→ 扔给
dev_queue_xmit→ 进你的ndo_start_xmit
极简总结
邻居子系统 = 内核的 "IP→MAC" 翻译官 + ARP 管理器它把三层(IP)转成二层(以太网帧),然后才交给你的网卡驱动发送。
网络数据的接收
直接按真实内核流程 + 关键函数讲,和前面发包流程对应起来,你立刻就能串成完整闭环。
一、一句话总结收包
硬件收到帧 → 中断 → NAPI 轮询 → 驱动组 skb → 上交协议栈 → IP → TCP → 应用 recv ()
二、完整流程(从上到下按真实顺序)
1. 物理层:数据包从网线上来
PHY 芯片接收到电信号 → 解码成以太网帧 → 放入 MAC 的 FIFO/DMA 内存
2. 硬件产生接收中断
- 帧就绪
- MAC 触发中断请求 IRQ
3. 驱动中断处理函数被调用
irqreturn_t my_interrupt(int irq, void *dev_id) { // 读中断状态,判断是 RX 中断 if (rx_interrupt) { // 禁止再触发中断 disable_irq_nosync(irq); // 调度 NAPI 去收包(**真正收包不在中断里!**) napi_schedule(&priv->napi); } return IRQ_HANDLED; }**关键点:**中断只做一件事:
关掉中断 + 启动 NAPI 轮询 真正收包、拷贝数据不在中断上下文。
4. NAPI poll 函数(真正收包的地方)
NAPI 是软中断上下文,运行
napi_struct->poll
static int my_napi_poll(struct napi_struct *napi, int budget) { struct my_priv *priv = container_of(napi, struct my_priv, napi); int rx_count = 0; // 循环收包,直到包取完 or 达到预算 budget while (rx_count < budget && 硬件有包) { // 1. 从硬件读一包数据 len = hw_read_rx_packet(priv, pkt_data); // 2. 分配 skb struct sk_buff *skb = dev_alloc_skb(len); skb_put(skb, len); // 把 len 挂到 skb // 3. 把数据拷贝进 skb memcpy(skb->data, pkt_data, len); // 4. 给 skb 设置网卡、协议类型、时间戳 skb->dev = priv->dev; skb->protocol = eth_type_trans(skb, priv->dev); // 5. **上交协议栈** napi_gro_receive(napi, skb); rx_count++; } // 如果收完了 if (硬件无更多包) { napi_complete_done(napi, rx_count); enable_irq(priv->irq); // 重新开中断 } return rx_count; }
5. 上交协议栈:napi_gro_receive
文件:
net/core/dev.c
napi_gro_receive(napi, skb);它会:
- GRO 合并小包(提高性能)
- 进入内核网络接收队列
最终进入:
netif_receive_skb(skb);
6. 进入二层处理:协议分发
netif_receive_skb ↓ __netif_receive_skb ↓ sch_handle_ingress ↓ ip_rcv(如果是 IP 包)根据以太网类型:
- IPv4 →
ip_rcv- ARP →
arp_rcv
7. IP 层处理
文件:
net/ipv4/ip_input.c
ip_rcv(skb) ↓ ip_rcv_core(skb) ↓ ip_local_deliver(skb) ↓ tcp_v4_rcv(skb) // 如果是 TCP
8. TCP 层处理
文件:
net/ipv4/tcp_input.c
tcp_v4_rcv(skb) ↓ tcp_validate_incoming ↓ tcp_data_queue ↓ 把数据放入 socket 接收队列
9. 应用层 recv () 拿到数据
recv() ↓ sys_recvfrom ↓ sock_recvmsg ↓ tcp_recvmsg从 socket 接收队列把数据拷贝到用户态。
三、极简收包调用链(背这个)
硬件收到帧 ↓ 中断触发 ↓ 中断函数:napi_schedule ↓ NAPI poll 函数 ↓ dev_alloc_skb → 填数据 ↓ napi_gro_receive / netif_receive_skb ↓ ip_rcv ↓ tcp_v4_rcv ↓ 数据放入 socket 队列 ↓ 应用 recv() 读取
四、和发包流程对比(完美闭环)
发送:
send → tcp_sendmsg → ip_local_out → neigh_output → dev_queue_xmit → ndo_start_xmit → 硬件接收:
硬件 → 中断 → NAPI → skb → netif_receive_skb → ip_rcv → tcp_v4_rcv → recv一去一回,结构完全对称!
五、你必须记住的 3 个关键点
- 收包不在中断里,只在 NAPI poll 里
- 驱动只负责构建 skb,交给内核就完事
