Linux内核网络驱动框架

Linux内核通用网络驱动基础内容:

  • struct net_device
  • alloc_netdev / free_netdev
  • register_netdev
  • netdev_ops
  • ndo_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

必须做:

  1. 硬件使能:时钟、电源、复位释放
  2. 初始化 MAC:配置帧长度、模式、过滤
  3. 初始化 PHY:自动协商、获取链路状态
  4. 申请中断 request_irq
  5. 初始化 NAPI(收包用)
  6. 启动发送队列 netif_start_queue(ndev)

完成后,网卡真正可以收发数据


四、发包核心流程(ndo_start_xmit)

协议栈下发一个 skb,驱动必须:

  1. 校验 skb 合法性

  2. 映射数据到 DMA(或拷贝到芯片 FIFO)

  3. 启动硬件发送

  4. 发送完成后:

    • 释放 skb
    • 唤醒发送队列
  5. 出错返回错误码

    ndo_start_xmit
    ↳ 封装帧
    ↳ 写入硬件
    ↳ 启动发送
    ↳ 返回 NETDEV_TX_OK


五、收包核心流程(中断 + NAPI)

  1. 硬件收到帧 → 触发中断

  2. 中断函数只做一件事:关闭中断,调度 NAPI

  3. NAPI poll 函数真正收包:

    • 从硬件 FIFO/DMA 读数据
    • 分配 skb、拷贝数据
    • 填充协议类型、时间戳
    • 上交协议栈:netif_receive_skb(skb)
  4. 收完一轮重新开中断

    中断
    ↳ 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 = 内核眼中的一张网卡

系统里看到的 eth0wlan0ens33 本质都是它。

一、定义在哪里?

复制代码
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_netdev

WiFi

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;

总结三句话(背下来)

  1. alloc_netdev(sizeof_priv) 时,私有数据就已经分配好了
  2. netdev_priv(ndev) 只是获取这块内存的地址
  3. 获取后,你自己读写里面的内容

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


它是干嘛的?

做三件事:

  1. 停止收发数据
  2. 释放资源(中断、关闭 NAPI、停止队列)
  3. 关闭硬件

目的:让网卡进入安静、低功耗、安全的关闭状态。


标准固定流程(背这个就够)

复制代码
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 件事

  1. **netif_stop_queue(dev)**停止发送队列,防止协议栈继续发包。

  2. 关闭硬件收发告诉芯片别再收包、发包。

  3. **napi_disable**关闭收包轮询。

  4. **free_irq**释放中断,避免中断处理函数还在跑。

  5. 硬件断电 / 复位进入低功耗状态。


和 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;    // 整个缓冲区结束地址
};

最重要的两个!

  1. skb->data 指向数据包真正内容的指针(以太网头 + IP 头 + 数据)
  2. **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下的网络子系统-CSDN博客

网络数据包的发送

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)

驱动做三件事:

  1. skb->data 取数据

  2. 写入硬件(FIFO/DMA)

  3. 释放 skb

    dev_kfree_skb(skb);


6. 硬件发送

  • 硬件从内存取数据
  • 发到网线上 / 空气中

7. 发送完成(中断)

硬件发完产生中断,驱动:

  • 统计计数
  • 唤醒队列(如果之前满了)

三、极简流程图(背这个)

复制代码
应用 send()
   ↓
协议栈封装成 skb
   ↓
进入发送队列
   ↓
dev_queue_xmit()
   ↓
驱动 ndo_start_xmit()
   ↓
把 skb->data 发给硬件
   ↓
dev_kfree_skb(skb)
   ↓
硬件发送

四、关键点(必须懂)

  1. 发包的核心是 skb整个内核只认 skb。

  2. 驱动只负责搬运不关心协议,只把数据拷给硬件。

  3. skb 必须由驱动释放 谁最后处理 skb,谁负责 dev_kfree_skb

  4. 不能阻塞 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)(...);       // 发包函数
    // ... 状态、缓存、定时器
};

它在发包时干了啥?

  1. 给你查邻居表(相当于 ARP 缓存)
  2. 如果已经有 MAC → 直接填到以太网头→ 调用 neigh->output() 最终走到 dev_queue_xmit
  3. 如果没有 MAC → 触发 ARP 请求→ 包先缓冲,等拿到 MAC 再发

对应内核文件

  • include/linux/neighbour.h
  • net/core/neighbour.c

IPv4 对应的是 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 个关键点

  1. 收包不在中断里,只在 NAPI poll 里
  2. 驱动只负责构建 skb,交给内核就完事
相关推荐
大神的风范2 小时前
QT部署YOLO11实时检测
驱动开发·深度学习·qt·目标检测·计算机视觉
taxunjishu2 小时前
智能仓储无人化管控 Profinet转MODBUS TCP全流程互联
网络·网络协议·自动化
亚空间仓鼠3 小时前
OpenEuler系统常用服务(三)
linux·运维·服务器·网络
vortex53 小时前
从应用层到内核层:SOCKS 代理与 TUN 模式全解析
网络·网络安全·渗透测试
信工 18023 小时前
rk3568-Linux应用程序和驱动程序接口
linux·驱动开发·rk3568
运维儿3 小时前
2.二层网络为什么存在冲突?如何解决冲突和冲突域?
网络·网络协议·linux 网络·云计算网络
REDcker3 小时前
OpenSSL:C 语言 TLS 客户端完整示例
c语言·网络·数据库
上海云盾-小余3 小时前
服务器被入侵后如何快速止损?从排查到加固的应急处置全流程
网络·网络协议·tcp/ip·安全·web安全
倒酒小生3 小时前
4月7日算法学习小结
linux·服务器·学习
木子欢儿3 小时前
KasmVNC 指南:高性能网页原生 Linux 远程桌面方案
linux·运维·服务器