3. 内核网络子系统核心数据结构
做网络驱动开发,说白了就是跟几个核心数据结构打交道。我刚开始接触这部分时,看着 struct net_device 那上百个字段,说实话有点懵。但干久了你会发现,真正需要你关心的,其实就那么几个。
今天咱们就把这几个「顶梁柱」掰开揉碎了讲清楚。它们是:struct net_device、struct sk_buff、struct net_device_ops、struct ethtool_ops。
核心观点:这四个结构体,分别代表了「设备本身」、「数据包」、「操作函数集」、「控制接口」。理解它们,你就掌握了网络驱动的骨架。
3.1 struct net_device:网络设备的「身份证」
这个结构体,你可以把它理解成网络设备在内核里的「户口本」。它记录了设备的名字、MAC地址、状态、统计信息等等。每个网卡对应一个 net_device 实例。
我个人习惯,写驱动第一步就是先把这个结构体分配出来。用 alloc_netdev() 或者 alloc_etherdev() 都行。前者更通用,后者专门给以太网设备用的,省得你手动设置一些以太网相关的参数。
// 分配一个以太网设备
struct net_device *ndev;
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev) {
// 我曾经在这里吃过亏,忘了检查返回值,结果后面直接空指针崩溃
return -ENOMEM;
}
// 设置设备名字
strncpy(ndev->name, "eth%d", IFNAMSIZ);
// 设置MAC地址
ndev->dev_addr[0] = 0x00;
ndev->dev_addr[1] = 0x11;
ndev->dev_addr[2] = 0x22;
// ... 实际项目中MAC地址应该从硬件读取或由用户配置
小技巧:我个人习惯在 alloc_etherdev() 的参数中传入私有数据结构的大小,这样设备结构体和私有数据就在一次内存分配中完成了,省事又高效。
net_device 里有个字段叫 flags,控制着设备的状态。比如 IFF_UP 表示设备已启用,IFF_RUNNING 表示链路正常。你想想看,ifconfig 命令看到的那些状态,其实就是读的这个字段。
3.2 struct sk_buff:数据包的「快递箱」
这个结构体太重要了。每个收发的网络数据包,都被装在这个「箱子」里。它包含了数据本身,还有各种控制信息,比如协议类型、网络头指针、传输头指针等等。
我记得刚做驱动时,总搞不清 skb->data、skb->head、skb->tail 的区别。说白了:
head指向分配的内存起始位置data指向当前协议层的数据起始位置tail指向当前数据的结束位置end指向分配的内存结束位置
收包时,驱动需要分配一个 sk_buff,然后把硬件收到的数据拷贝进去。发包时,驱动从 sk_buff 里取出数据,交给硬件发送。
// 收包示例(简化版)
struct sk_buff *skb;
skb = netdev_alloc_skb(ndev, length + 2);
if (!skb) {
// 内存不够时,该丢的包就得丢
ndev->stats.rx_dropped++;
return;
}
skb_reserve(skb, 2); // 预留2字节,让IP头对齐
skb_put(skb, length); // 告诉内核数据长度
// 从硬件读取数据到 skb->data
// ... 硬件相关操作 ...
// 设置协议类型
skb->protocol = eth_type_trans(skb, ndev);
// 把包交给上层协议栈
netif_rx(skb);
注意:我曾经在项目中犯过一个低级错误——收包时忘了调用 skb_reserve(),结果IP头没对齐,导致某些架构上性能暴跌。嗯,这个坑踩过一次就记住了。
3.3 struct net_device_ops:设备的「操作手册」
这个结构体定义了一组函数指针,告诉内核「这个设备该怎么操作」。比如怎么打开设备、怎么关闭设备、怎么发包、怎么修改MAC地址等等。
说白了,你写驱动的大部分工作,就是实现这个结构体里的函数。我一般会先实现 ndo_open 和 ndo_stop,让设备能跑起来,然后再慢慢加其他功能。
| 函数指针 | 作用 | 必须实现? |
|---|---|---|
| ndo_open | 打开设备,申请资源 | 是 |
| ndo_stop | 关闭设备,释放资源 | 是 |
| ndo_start_xmit | 发送数据包 | 是 |
| ndo_set_mac_address | 修改MAC地址 | 否 |
| ndo_do_ioctl | 处理自定义ioctl命令 | 否 |
| ndo_change_mtu | 修改MTU值 | 否 |
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_open,
.ndo_stop = my_stop,
.ndo_start_xmit = my_start_xmit,
.ndo_set_mac_address = my_set_mac,
.ndo_change_mtu = my_change_mtu,
};
// 注册到 net_device
ndev->netdev_ops = &my_netdev_ops;
经验之谈:我建议你在 ndo_open 里申请中断和DMA缓冲区,在 ndo_stop 里释放。这样设备打开关闭时资源管理清晰,不容易泄漏。
3.4 struct ethtool_ops:设备的「控制面板」
ethtool 是用户空间的一个工具,用来查看和修改网卡参数。而 ethtool_ops 就是内核侧对应的实现。比如用户执行 ethtool -S eth0 查看统计信息,内核就会调用你实现的 get_ethtool_stats 函数。
说实话,这个结构体不是必须实现的。但如果你想让你的驱动显得专业,建议至少实现几个常用的回调:
get_drvinfo:返回驱动版本、固件版本等信息get_link:返回链路状态(up/down)get_settings/set_settings:获取/设置速率、双工模式等get_strings和get_ethtool_stats:提供自定义统计信息
static const struct ethtool_ops my_ethtool_ops = {
.get_drvinfo = my_get_drvinfo,
.get_link = my_get_link,
.get_settings = my_get_settings,
.set_settings = my_set_settings,
};
// 注册到 net_device
ndev->ethtool_ops = &my_ethtool_ops;
避坑指南:我曾经实现 get_settings 时,忘了处理 autoneg(自动协商)字段。结果用户用 ethtool 查看时,显示自动协商关闭,但实际上硬件是支持的。这种小细节很容易被忽略,但用户会很在意。
3.5 四个结构体的协作关系
你想想看,这四个结构体是怎么配合的?
当用户执行 ifconfig eth0 up 时,内核找到对应的 net_device,然后调用 net_device_ops 里的 ndo_open。当有数据包到达时,驱动分配一个 sk_buff,填好数据,交给协议栈。当用户执行 ethtool eth0 时,内核调用 ethtool_ops 里的对应函数。
说白了,net_device 是核心,其他三个都是为它服务的。理解了这个关系,你写驱动时就知道该往哪个结构体里填代码了。
个人建议:刚开始写驱动时,先别急着把所有回调都实现。先把 net_device_ops 里的三个必须函数搞定,让设备能收发包。然后再慢慢加 ethtool_ops 和其他高级功能。这样迭代开发,不容易出大问题。
嗯,这四个结构体就讲到这里。记住它们各自的分工,你写网络驱动时就不会迷路了。