网络设备驱动(Network Device Driver)是操作系统内核与网络硬件(物理网卡或虚拟网卡)之间的软件桥梁,负责屏蔽不同硬件的实现细节,并为上层网络协议栈提供统一的数据收发接口。

网络协议栈:

驱动不属于网络协议栈,驱动属于对接物理层和数据链路层的内容。

网卡与PHY的关系?

网卡由三部分组成:PHY,MAC控制器,变压器与RJ45接口。

1.网络设备驱动

1.1核心架构分层

Linux 网络子系统采用自顶向下的分层架构:

  • 网络协议栈层:负责 TCP/IP、UDP 等协议的处理,通过 sk_buff 结构传递数据包。

  • 网络设备接口层:内核抽象出统一的 net_device 结构体,定义了标准的网卡行为(启动、关闭、发送)。

  • 驱动实现层:特定网卡芯片的驱动代码,负责初始化硬件、配置寄存器、处理 DMA 和硬件中断。

  • 物理总线与硬件层:包含 MAC/PHY 芯片及 PCIe、USB 或 MDIO 等总线架构。

网络协议栈层是操作系统内核中纯逻辑的软件子系统,负责实现 TCP/IP 等网络协议的规则与算法(如数据封包/解包、路由选择、拥塞控制、丢包重传等)。网络协议栈层对上层暴露接口,socket(),connect(),send()等,网络设备驱动层不对外暴露接口。

1.2核心数据结构与接口

  • sk_buff (Socket Buffer):网络数据包在内核中的核心数据载体,包含数据指针、协议头偏移量及套接字信息,在协议栈与驱动间流动时无需拷贝实际数据。

  • net_device:代表一个网络接口实体(如 eth0),包含 MAC 地址、MTU、状态标志以及底层驱动的回调函数指针集。

  • net_device_ops:驱动向内核注册的操作接口集,核心函数包括:

    • ndo_open():打开网卡,申请 DMA 缓冲区和注册中断处理函数。

    • ndo_stop():停止网卡并释放硬件资源。

    • ndo_start_xmit():内核发送数据包的核心入口。

1.3网络数据包收发全流程

数据从应用程序发起,自顶向下逐层封装并推送到物理介质:

[应用层: send()] ➔ [Socket TX Buffer] ➔ [TCP/UDP 传输层] ➔ [IP 网络层] ➔ [TC/邻居层] ➔ [驱动 ndo_start_xmit] ➔ [DMA 搬运至网卡] ➔ [PHY 发送]

1. 应用层与 Socket 接口层

  • 发起调用:应用程序(如 Nginx)调用 send() / write() 系统调用。

  • 内存拷贝:CPU 将数据从用户空间缓冲区拷贝至内核空间的 Socket 发送缓冲区(TX Buffer)

  • 构建控制块:内核为数据分配 sk_buff 结构体,用于在后续协议栈传输中记录头指针、尾指针与协议元数据。

2. 传输层(TCP / UDP)

  • TCP 切片与封装:TCP 根据 MSS(最大报文段长度)对数据进行分段,为每个 sk_buff 添加 TCP 头部(包含源/目的端口、序列号、确认号等)。

  • 状态与重传机制:数据被挂入 TCP 送出队列,启动重传定时器;同时计算 TCP 校验和(若网卡支持 Checksum Offload,则留给硬件计算)。

3. 网络层(IP Layer)

  • 路由查找(Routing Lookup):查询内核路由表,确定出接口(Egress Interface)及下一跳 IP 地址。

  • IP 头部封装:添加 IP 头部(包含源/目的 IP、TTL、协议号等)。

  • 防火墙挂钩(Netfilter/iptables):触发 LOCAL_OUTPOST_ROUTING 链(如执行 SNAT 或数据包过滤)。

  • IP 分片:若数据包尺寸大于 Path MTU 且网卡不支持 TSO(TCP Segmentation Offload),在网络层拆分为多个 IP 分片。

4. 邻居子系统与数据链路层(TC & ARP)

  • MAC 地址解析:邻居子系统(ARP 表)解析下一跳 IP 对应的目标 MAC 地址;构建以太网帧头。

  • 流量控制(Traffic Control / qdisc):数据包进入网卡的排队规则(qdisc)队列,执行 QoS 限速、整形或丢包策略。

5. 网卡驱动层

  • 触发发送:内核调用驱动注册的回调函数 ndo_start_xmit()

  • DMA 映射:驱动将 sk_buff 在主存中的物理地址写入 TX Ring Buffer(TX 描述符环形缓冲区)。

  • 通知硬件:驱动写入网卡寄存器(如 Doorbell/Tail 寄存器),提示网卡有新数据待发送。

6. 物理硬件层

  • DMA 提取:网卡控制器直接通过 DMA 从主存的 TX Ring Buffer 提取帧数据到网卡内部的 TX FIFO 缓冲区。

  • 物理发包:网卡 MAC/PHY 芯片将数字数据转换为电信号/光信号发往网络介质。

  • 清理资源:网卡发送完成后触发 TX 完成中断,CPU 响应硬中断并调度软中断,驱动清理已发送的 sk_buff 并释放 TX 描述符。

简述发送流程:(1)应用层调用send系统调用,CPU将数据从用户空间缓冲区拷贝至内核空间Socket缓冲区(TX-Buffer),内核为这块数据构建sk_buff结构体;(2)进入网络协议栈层,对sk_buff增添TCP包头、IP包头、以太网包头等内容;(3)调用驱动中的ndo_statr_xmit()回调:把sk_buffer映射为物理地址,并将其物理地址和数据长度包装为句柄写入TX Ring Buffer,驱动写入网卡寄存器(如Doorbell)通知网卡有新数据到来,网卡控制器通过DMA将TX Ring Buffer提取帧数据到网卡内部的FIFO缓冲区,然后PHY发包。

2.最小网络设备驱动框架

不依赖真实 PHY / MAC 硬件的最小虚拟网卡驱动,下面这个驱动加载后会注册一个类似 vnet0 的网卡。它不能真正把数据发到网线上,只是看清楚网络驱动结构:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>

static struct net_device *vnet_dev;

/*
 * 打开网卡
 * 对应:
 * ip link set vnet0 up
 */
static int vnet_open(struct net_device *dev)
{
    printk(KERN_INFO "vnet: open\n");

    /* 启动发送队列 */
    netif_start_queue(dev);

    return 0;
}

/*
 * 关闭网卡
 * 对应:
 * ip link set vnet0 down
 */
static int vnet_stop(struct net_device *dev)
{
    printk(KERN_INFO "vnet: stop\n");

    /* 停止发送队列 */
    netif_stop_queue(dev);

    return 0;
}

/*
 * 发送函数
 *
 * 上层有数据需要发送时,
 * 内核最终会调用这里。
 */
static netdev_tx_t vnet_start_xmit(struct sk_buff *skb,
                                   struct net_device *dev)
{
    printk(KERN_INFO "vnet: send packet, len = %u\n", skb->len);

    /*
     * 真正的网卡驱动:
     *
     * skb
     *  ↓
     * DMA descriptor
     *  ↓
     * MAC
     *  ↓
     * PHY
     *  ↓
     * 网线
     *
     * 这里没有真实硬件,
     * 所以直接释放数据包。
     */
    dev_kfree_skb(skb);

    return NETDEV_TX_OK;
}

/*
 * 网络设备操作函数
 */
static const struct net_device_ops vnet_ops = {
    .ndo_open       = vnet_open,
    .ndo_stop       = vnet_stop,
    .ndo_start_xmit = vnet_start_xmit,
};

static int __init vnet_init(void)
{
    int ret;

    /*
     * 分配一个 Ethernet 网络设备
     *
     * 最终名字:
     * vnet0、vnet1 ...
     */
    vnet_dev = alloc_netdev(0,
                            "vnet%d",
                            NET_NAME_UNKNOWN,
                            ether_setup);

    if (!vnet_dev)
        return -ENOMEM;

    /*
     * 注册网络设备操作函数
     */
    vnet_dev->netdev_ops = &vnet_ops;

    /*
     * 设置一个随机 MAC 地址
     */
    eth_hw_addr_random(vnet_dev);

    /*
     * 注册到 Linux 网络子系统
     */
    ret = register_netdev(vnet_dev);
    if (ret) {
        free_netdev(vnet_dev);
        return ret;
    }

    printk(KERN_INFO "vnet: registered as %s\n",
           vnet_dev->name);

    return 0;
}

static void __exit vnet_exit(void)
{
    unregister_netdev(vnet_dev);

    free_netdev(vnet_dev);

    printk(KERN_INFO "vnet: removed\n");
}

module_init(vnet_init);
module_exit(vnet_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("demo");
MODULE_DESCRIPTION("Minimal Linux virtual network driver");

执行

sudo ip link set vnet0 up

会调用 vnet_open();

open中会调用netif_start_queue(dev),意思是告诉 Linux 网络协议栈:这个网卡现在可以开始接收上层下发的待发送数据了。

3.核心架构

3.1net_device结构体

net_device 是 Linux 内核对“网络设备”的统一抽象。像这些:

eth0
eth1
wlan0
lo
vnet0

在内核里基本都对应一个:

struct net_device

net_device结构体中包含内容:

struct net_device
├── 网卡名字
│   └── name,例如 eth0
│
├── MAC 地址
│   └── dev_addr
│
├── MTU
│   └── mtu
│
├── 网络设备操作函数
│   └── netdev_ops
│
├── 发送队列相关信息
│
├── 网卡运行状态
│
├── statistics
│   └── 收发包、错误等统计
│
└── 驱动私有数据
    └── netdev_priv()

其中最重要的还是netdev_ops,如:

static const struct net_device_ops vnet_ops = {
    .ndo_open       = vnet_open,
    .ndo_stop       = vnet_stop,
    .ndo_start_xmit = vnet_start_xmit,
};

3.2net_device注册流程

初始化net_device的步骤是:

分配 net_device
    ↓
获取私有数据
    ↓
设置 net_device 基本属性
    ↓
绑定 net_device_ops
    ↓
初始化驱动私有资源
    ↓
注册 net_device
struct my_priv *priv;
struct net_device *ndev;
int ret;

/* 1. 分配 Ethernet net_device + 私有数据 */
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev)
    return -ENOMEM;

/* 2. 获取驱动私有数据 */
priv = netdev_priv(ndev);

/* 3. 保存关联关系 */
priv->ndev = ndev;

/* 4. 设置网卡操作函数 */
ndev->netdev_ops = &my_netdev_ops;

/* 5. 设置 MAC 地址 */
eth_hw_addr_random(ndev);

/* 也可以设置其它属性 */
ndev->mtu = 1500;

/* 6. 初始化驱动自己的资源 */
priv->irq = irq;
priv->base = base;

/* 例如初始化 NAPI */
netif_napi_add(ndev, &priv->napi, my_poll);

/* 7. 注册到 Linux 网络子系统 */
ret = register_netdev(ndev);
if (ret) {
    free_netdev(ndev);
    return ret;
}

驱动私有数据:

真实驱动不可能只靠 net_device,还需要私有数据,net_device存放通用的数据,对于不同的网卡有不同的硬件细节,需要放到私有数据中,如:

  • PCIe 网卡:需要存 TX/RX DMA 描述符环形缓冲区指针、PCIe 寄存器映射基地址(MMIO)、MSI-X 中断向量表等。

  • USB 网卡:需要存 USB URB(USB Request Block)结构体、控制/数据端点(Endpoint)等。

  • Wi-Fi 网卡:需要存 SSID、信道、加密密钥、信号强度(RSSI)、Beacon 定时器等。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐