Linux内核编程——网络驱动程序

引言

本章将介绍 Linux 内核中最重要的组成部分之一------网络栈。我们将了解网络栈的组成,从网卡到其复杂的处理流程。此外,还会探讨如何提升网络栈的效率。

结构

本章涵盖以下主题:

  • 网络栈的发展历史
  • 网络架构
  • 协议层
  • 网络模块
  • 网络驱动加载
  • sk_buff(套接字缓冲区)
  • 性能与调优
  • DPDK 项目

目标

通过本章学习,你将能够理解网络栈的工作原理,开发网卡驱动,改进网络栈的运行,或保障其安全性。

为了能够处理数十亿比特每秒(Gbps)级别的重要吞吐量,我们还将介绍开源项目 DPDK 的结构。

网络栈的发展历史

Linux 内核网络层的开发由多位独立程序员共同推动。目标是在保持自由软件理念的前提下,实现至少与其他系统同等高效的网络系统。

其中,TCP/IP 协议由 Ross Biro 通过基本原语完成了初步开发。Orest Zborowski 为 GNU Linux 内核制作了第一个 BSD 套接字接口。

之后,Red Hat 的 Alan Cox 接手了这部分代码的维护。

下图展示了 Linux 网络栈发展的时间轴:

GNU/Linux 网络栈开发的重要阶段如下:

早期网络支持(1991--1995年):

Linux 在 1991 年由 Linus Torvalds 创建时,最初并没有内置的网络支持。但很快,在 1991 年 9 月发布的 0.01 版本中,Linux 获得了基础的网络功能。

最初的网络支持较为初级,提供了如 TCP/IP 网络和以太网支持等基本功能。

内核版本 1.x 和 2.x(1996--2000年):

在 1990 年代中后期,Linux 的网络能力随着每个内核版本的发布显著增强。

  • 1996 年 6 月发布的 2.0 版本,引入了网络栈的重要增强,包括支持更多网络协议、提升性能和更好的可扩展性。
  • 1999 年 1 月发布的 2.2 版本,引入了 IPv6 支持、增强的路由功能和改进的网络设备驱动。

内核版本 2.4 和 2.6(2001--2006年):

  • 2001 年 1 月发布的 2.4 版本,是 Linux 网络发展中的一个重要里程碑。它引入了 Netfilter 框架,通过如 iptables 这样的工具,实现了强大的数据包过滤和处理能力。
  • 2003 年 12 月发布的 2.6 版本,进一步增强了网络栈,包括更好的可扩展性、支持网络命名空间,并引入了如 TCP/IP 栈改进和高效事件通知的 epoll 系统调用等特性。

内核版本 3.x 和 4.x(2011--2019年):

  • 2011 年 7 月发布的 3.0 版本,没有带来重大网络变化,但持续对现有功能进行优化和改进。
  • 2013 年 6 月发布的 3.10 版本,引入了 TCP Fast Open 功能,旨在减少 TCP 连接的建立时间。
  • 2016 年 1 月发布的 4.4 版本,提升了网络栈的性能和可扩展性,增强了网络协议和安全特性。
  • 2018 年 1 月发布的 4.15 版本,引入了 Retpoline Spectre 缓解技术,修补网络栈及内核其他部分的安全漏洞。

近期发展(2020年至今):

近期内核版本重点提升安全性、性能,并支持新兴网络技术,如 5G、物联网(IoT)和软件定义网络(SDN)。

当前工作包括优化低延迟应用的网络栈,增强对 VXLAN、Geneve 等网络虚拟化技术的支持,以及改进网络协议实现以提升性能和安全性。

网络架构

Linux 网络栈是 Linux 内核中的一个复杂系统,负责处理网络的各个方面,包括数据包处理、协议实现、设备驱动以及网络配置。

以下是 GNU/Linux 网络栈的详细技术概述:

数据包处理:

  • 网络接口接收传入的数据包,这些接口由内核中的设备驱动表示。
  • 接收到的数据包被封装进称为套接字缓冲区(socket buffers,sk_buff)的数据结构中,包含了关于数据包的元信息,如协议类型、源地址和目的地址,以及指向数据包内容的指针。
  • 内核的网络栈通过多个协议层处理接收到的数据包,包括链路层(如以太网、Wi-Fi)、网络层(如 IP)、传输层(如 TCP、UDP)和应用层(如套接字)。

协议实现:

  • Linux 内核包含多种网络协议的实现,如 TCP/IP、UDP、ICMP、IPv4 和 IPv6。
  • 每个协议由一组函数实现,负责数据包处理、协议特定操作、错误处理和状态管理。
  • 协议实现通过定义良好的接口和数据结构相互交互,实现模块化和可扩展的网络功能。

设备驱动:

  • Linux 中的网络接口由设备驱动表示,负责控制物理或虚拟网络设备。
  • 设备驱动与硬件交互,处理数据包的发送和接收、中断处理以及设备初始化等任务。
  • 内核的网络子系统为设备驱动提供统一接口,便于它们集成到网络栈中。

套接字层:

  • Linux 的套接字层为应用程序通过网络使用套接字通信提供接口。
  • 套接字是通过 IP 地址、端口号和协议类型(如 TCP、UDP)标识的通信端点。
  • 应用程序通过系统调用如 socket()bind()listen()connect()send()recv() 与套接字交互,这些调用由内核的套接字层实现。

路由与转发:

  • Linux 内核维护路由表,决定网络数据包如何转发到目的地。
  • 路由决策基于目标 IP 地址和内核中配置的路由策略。
  • 内核的路由子系统还支持高级路由功能,如策略路由、源路由和网络地址转换(NAT)。

数据包过滤与防火墙:

  • Linux 包含 Netfilter 框架,提供数据包过滤和防火墙功能。
  • Netfilter 允许管理员根据源地址、目的地址、端口号和数据包内容等条件定义过滤、修改和转发规则。
  • 工具如 iptables 和 nftables 提供用户空间接口,用于配置 Netfilter 规则和管理防火墙策略。

性能与可扩展性:

  • Linux 网络栈设计注重性能和可扩展性,针对多核处理器、高速网络和高效的数据包处理进行了优化。
  • 采用技术包括内核绕过、零拷贝网络和中断调节,旨在减少开销、提升吞吐量。
  • 内核网络子系统持续进行优化和调优,以满足现代网络应用的性能需求。

数据包在 IP 栈中接收或发送时的处理可分为多个层次,示意图如下:

该 IP 处理过程包含四个主要阶段:

  • 帧接收:网络接口接收到的帧进入 IP 层的入口点。
  • 路由选择:确定数据包应通过哪个接口或网络进行转发,或发送到本地目的地。
  • 转发:在进入转发阶段前,对 TTL(生存时间)和 MTU(最大传输单元)进行控制。
  • 传输:待发送或转发的数据包经过此阶段。在离开 IP 层之前,完成以太网头部的封装。

下图详细展示了 Netfilter 在网络栈中可使用的钩子点:

Netfilter 是 Linux 内核中的一个框架,提供数据包过滤、网络地址转换(NAT)和数据包处理(mangling)功能。它包含网络栈中的一组钩子(入口点),内核模块(称为 Netfilter 钩子)可以在这些钩子处拦截和处理网络数据包。

Netfilter 常用于在基于 Linux 的系统中实现防火墙、NAT 网关、数据包过滤和流量控制。每个钩子是 Linux 内核网络栈中的一个入口点,内核模块可以在此注册回调函数来拦截网络数据包。

Netfilter 主要有五个钩子,对应数据包处理路径中的特定位置:

  • NF_INET_PRE_ROUTING:数据包进行路由前调用。适用于数据包过滤和 NAT 操作。
  • NF_INET_LOCAL_IN:数据包目标为本地系统时调用。适用于本地数据包过滤和流量监控。
  • NF_INET_FORWARD:数据包被转发到其他主机时调用。适用于系统转发数据包的过滤。
  • NF_INET_LOCAL_OUT:数据包由本地系统发送时调用。适用于出站数据包过滤和 NAT。
  • NF_INET_POST_ROUTING:数据包完成路由后调用。适用于对出站数据包进行 NAT 操作。

Netfilter 钩子根据数据包在网络栈中的位置,提供了灵活且细粒度的过滤和处理能力。

内核模块通过注册回调函数(称为钩子函数)与 Netfilter 钩子关联,用于拦截和处理网络数据包。每个钩子函数会接收一个指向表示被拦截数据包的 sk_buff 结构体的指针以及其他相关信息。

钩子函数可以检查数据包头部,基于配置的规则进行过滤,修改数据包内容,并执行相应操作,如接受、丢弃或转发数据包。

除了钩子和钩子函数,Netfilter 子系统还包括核心模块(管理钩子注册和数据包遍历)以及多个实现特定数据包过滤和 NAT 功能的内核模块。

常用的 Netfilter 模块包括:

  • iptables:用于数据包过滤和防火墙
  • ip6tables:IPv6 数据包过滤
  • ebtables:以太网帧过滤
  • conntrack:连接跟踪和 NAT

Netfilter 为基于 Linux 的系统构建复杂数据包过滤和防火墙解决方案提供了灵活且可扩展的框架。

以下图展示了 GNU/Linux 内核网络栈不同层次的数据包路由情况:

网络接口卡使用两个链表来管理数据包,分别是接收数据包列表(rx_ring)和发送数据包列表(tx_ring)。因此,可以将发送过程和接收过程区分开来。对于非常旧的内核版本(如 2.2 及以下),帧的处理方式有所不同(例如找不到 tx 和 rx 文件)。

Linux 内核中的 Netfilter 框架用于在 PRE_ROUTING 钩子处拦截数据包并进行相应处理。

以下是实现该功能的主要组件和函数说明:

hook_func_tunnel_in 函数:

该函数作为 Netfilter 钩子 NF_IP_PRE_ROUTING 的回调,拦截路由决策前的数据包。

参数包括钩子编号、指向 sk_buff 结构体的指针、输入和输出网络设备指针,以及指向下一钩子函数的指针。

函数首先获取 sk_buff 结构体并提取 IP 头部。

然后判断数据包是否来自特定的输入设备(如 "eth0"),且目标 IP 是否匹配预定义的 IP 地址(ip1)。

满足条件时,函数通过修改数据包的 MAC 头部、调整包长度,实现数据包的隧道封装,并将其排入另一个设备("eth1")的发送队列。

最后返回 NF_STOLEN 表示已接管数据包的所有权。

init_module 函数:

作为 Netfilter 模块的初始化入口。

设置一个 Netfilter 钩子对象(nfho_tunnel_in),指定钩子函数为 hook_func_tunnel_in,钩子编号为 NF_IP_PRE_ROUTING,协议族为 PF_INET,优先级为 NF_IP_PRI_FIRST

通过 nf_register_hook 将钩子对象注册到 Netfilter 框架,允许其在指定钩子点拦截数据包。

cleanup_module 函数:

作为模块清理的出口。

通过 nf_unregister_hook 注销之前注册的钩子对象 nfho_tunnel_in

编译和安装该 Netfilter 内核模块的步骤:

  1. 将提供的代码保存为文件,例如 nf_hook.c
  2. 在系统上配置 Linux 内核开发环境。
  3. 使用合适的内核构建工具(gcc、make 等)编译模块。
  4. 使用 insmod 将模块加载到内核。
  5. 使用 lsmod 及日志工具(如 dmesgjournalctl)验证模块是否加载和运行正常。

以下是该 Linux 内核模块的代码示例,实现了一个 Netfilter 钩子用于拦截并重定向 IPv4 数据包。它捕获来自 eth0 的特定目标 IP 数据包,修改其 MAC 头部,并通过排队机制将数据包转发至 eth1。模块使用 NF_STOLEN 来处理数据包所有权,确保高效的内核隧道处理:

arduino 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/netdevice.h>

static unsigned int hook_func_tunnel_in(unsigned int hooknum,
                                        struct sk_buff *skb,
                                        const struct net_device *in,
                                        const struct net_device *out,
                                        int (*okfn)(struct sk_buff *)) {
    struct iphdr *ip_header;
    struct net_device *dev;
    struct Qdisc *q;
    int len;

    // 检查数据包是否含有 IP 头部
    if (!skb || !skb_network_header(skb))
        return NF_ACCEPT;

    ip_header = ip_hdr(skb);

    // 仅处理 IPv4 数据包
    if (ip_header->version != 4)
        return NF_ACCEPT;

    // 判断数据包是否来自 eth0 且目标 IP 是 ip1
    if (in && strcmp(in->name, "eth0") == 0 && ip_header->daddr == htonl(ip1)) {
        len = skb->len;

        // 进行隧道封装
        skb_push(skb, ETH_HLEN);
        skb_reset_mac_header(skb);
        memcpy(skb->data, hd_mac2, ETH_ALEN * 2);

        // 将数据包排入 eth1 的发送队列
        dev = dev_get_by_name("eth1");
        if (!dev)
            return NF_ACCEPT;

        q = dev->qdisc;
        spin_lock_bh(&dev->queue_lock);
        q->enqueue(skb, q);
        qdisc_run(dev);
        spin_unlock_bh(&dev->queue_lock);

        return NF_STOLEN; // 表示已接管数据包所有权
    }

    return NF_ACCEPT; // 允许数据包正常处理
}

// Netfilter 钩子结构体
static struct nf_hook_ops nfho_tunnel_in = {
    .hook = hook_func_tunnel_in,
    .hooknum = NF_INET_PRE_ROUTING, // IPv4 使用 NF_INET_PRE_ROUTING
    .pf = NFPROTO_IPV4,             // IPv4 协议族
    .priority = NF_IP_PRI_FIRST,
};

// 模块初始化函数
static int __init nf_hook_init(void) {
    int ret;
    ret = nf_register_hook(&nfho_tunnel_in);
    if (ret < 0) {
        printk(KERN_ERR "Failed to register Netfilter hook\n");
        return ret;
    }
    printk(KERN_INFO "NF_HOOK: Module installed\n");
    return 0;
}

// 模块退出函数
static void __exit nf_hook_exit(void) {
    nf_unregister_hook(&nfho_tunnel_in);
    printk(KERN_INFO "NF_HOOK: Module removed\n");
}

module_init(nf_hook_init);
module_exit(nf_hook_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BPB");
MODULE_DESCRIPTION("Netfilter hook example");

以上代码的主要要点如下:

  • 第1行至第7行:头文件包含
    添加了 Netfilter、skb、IP 头部和网络设备相关的必要头文件。
  • 第20行:函数参数检查
    在访问参数成员前,检查函数参数(如 skb、in)是否为 NULL。
  • 第26行:IPv4 检查
    确保数据包为 IPv4 类型后,才访问其 IP 头部。
  • 第30行:字节序转换
    使用 htonl() 将目标 IP 地址转换为网络字节序,再与 ip1 进行比较。
  • 第52行:返回值修改
    对于未修改的数据包,将返回值由 NF_STOLEN 改为 NF_ACCEPT,避免不必要的处理。
  • 第63行和第86行:模块初始化
    init_module() 重命名为 nf_hook_init(),增强代码清晰度和一致性,并增加注册 Netfilter 钩子的错误处理。
  • 第79行和第87行:模块清理
    cleanup_module() 重命名为 nf_hook_exit(),增强代码清晰度和一致性。
  • 第89行和第91行:模块元信息
    添加了模块许可证、作者和描述等元数据。

编译与安装该 Netfilter 内核模块步骤:

  • 使用合适的内核构建工具(make、gcc 等)编译模块。
  • 使用 insmod 将模块加载到内核。
  • 通过日志工具(如 dmesg)等方式验证模块是否加载且正常工作。

关于 ARP 协议的说明:

在发送时,仅能捕获类型为 ETH_P_ALL 的数据包。需要注意的是,正如内核中数据帧处理路径不同,数据包处理所用的函数也会随内核版本不同而变化。

要捕获 ARP 帧,只需在 ptype 列表(ptype_baseptype_all)中注册一个 packet_type 结构体,并指定 dev_add_pack() 回调函数(定义在 linux/netdevice.h)。

与 Netfilter 钩子类似,当模块从内核卸载时,该数据包处理钩子必须被移除,这可通过调用 dev_remove_pack()(同样定义在 linux/netdevice.h)实现。

通过为 ETH_P_ALL 注册函数,我们的包类型会被添加到 ptype_all 列表中,从而捕获来自网卡驱动的所有进出数据帧。

当我们用此类型钩子获取 sk_buff 时,实际上是捕获的 sk_buff 的一个拷贝。该拷贝在函数末尾必须用 kfree_skb() 释放。

因此,这类钩子不适用于修改捕获的数据包。

协议层

Linux 网络栈及其网络协议按照开放系统互联(OSI)模型或 TCP/IP 模型组织为多个层次。

这些层次促进设备间通过网络交换数据,并为实现网络通信提供了结构化框架。以下是 Linux 网络栈中各层的简介:

链路层(第2层)

链路层负责在直接连接的设备之间通过物理或逻辑链路传输数据帧。

在 Linux 中,链路层由设备驱动实现,包含以太网、Wi-Fi(802.11)和点对点协议(PPP)等协议。

设备驱动负责帧的封装与解封装、错误检测与纠正,以及媒体访问控制(MAC)地址解析。

Linux 中的设备驱动示例有 Intel 网卡的 e1000 驱动和 Atheros 无线网卡的 ath9k 驱动。

网络层(第3层)

网络层负责不同网络之间的数据包路由以及端到端的数据传输保证。

在 Linux 中,网络层主要由 IP(互联网协议)套件实现,包括 IPv4 和 IPv6。

IP 负责地址分配、数据包分片和重组、路由及错误报告。

Linux 内核包含 IPv4 和 IPv6 的协议实现,并支持高级路由功能及网络命名空间。

传输层(第4层)

传输层负责不同主机上运行的应用程序间的端到端通信。

在 Linux 中,传输层包括传输控制协议(TCP)和用户数据报协议(UDP)。

TCP 提供可靠的面向连接的通信,具有流量控制、拥塞控制和错误恢复等功能。

UDP 提供不可靠的无连接通信,开销小,适合流媒体、在线游戏等实时应用。

Linux 内核分别实现了 TCP 和 UDP 协议,并支持基于套接字的通信,提供如 socket()、bind()、connect()、send() 和 recv() 等系统调用。

应用层(第5至7层)

应用层涵盖各种协议和服务,支持特定的网络应用。

在 Linux 中,应用层包括超文本传输协议(HTTP)、文件传输协议(FTP)、简单邮件传输协议(SMTP)和域名系统(DNS)等协议。

Linux 上运行的应用程序通过操作系统或第三方软件提供的库和 API 使用这些协议。

应用层服务示例包括 Apache HTTP 服务器(提供网页服务)、Postfix 邮件服务器(邮件收发)和 BIND DNS 服务器(域名解析)。

GNU/Linux 各版本网络协议实现演进表

内核版本 协议支持
1.x(早期) 基础 TCP/IP 网络(IPv4、TCP、UDP、ICMP)
2.0(1996) IPv4、TCP、UDP、ICMP、以太网、PPP、SLIP
2.2(1999) IPv4(增强版)、ARP、RARP、IP 转发和路由、以太网、PPP、SLIP、DHCP、BOOTP、VLAN 标记
2.4(2001) IPv4(改进版)、IPv6(早期支持)、IPSEC、ARP、RARP、IP 转发和路由、以太网、PPP
2.6(2003) IPv4、IPv6、ARP、RARP、IP 转发和路由、以太网、PPP、VLAN 标记、IGMP、多播
3.x(2011) IPv4、IPv6(改进版)、ARP、RARP、IP 转发和路由、以太网、PPP、VLAN 标记、IGMP、多播
4.x(2015) IPv4、IPv6、ARP、RARP、IP 转发和路由、以太网、PPP、VLAN 标记、IGMP、多播

表 8.1:GNU/Linux 版本协议演进

网络模块

在 GNU/Linux 环境下,网络驱动是一种软件组件,负责实现 Linux 内核与网络硬件设备之间的通信。

这些驱动对于网络接口卡(NIC)、无线适配器及其他网络设备的正常运行至关重要。下面我们详细了解 GNU/Linux 网络驱动的关键组成部分和特性:

设备驱动接口

Linux 中的网络驱动通常作为内核模块实现,或者直接内置于内核中。

它们通过网络设备接口规范(NDIS)与 Linux 内核交互,NDIS 提供了一个标准化接口,供网络驱动注册自身、处理网络设备操作以及与网络栈的上层通信。

硬件抽象

网络驱动抽象底层网络硬件的细节,向 Linux 内核和网络栈提供统一接口。

它们负责硬件相关任务,如设备初始化、硬件参数配置(例如 MAC 地址、MTU)、中断处理以及网络数据包的发送和接收。

数据包处理

网络驱动负责将内核中的出站网络包传输到网络设备,以及接收来自设备的入站包回传给内核。

它们实现数据包的发送和接收逻辑,包括缓冲区管理、数据包排队,以及为高效的数据传输实现直接内存访问(DMA)操作。

中断处理

网络驱动处理网络设备产生的中断,用以通知新数据包到达、发送操作完成或硬件事件发生。

它们向内核注册中断处理程序,以响应中断信号,处理接收的数据包,并执行维持网络连接和性能所需的操作。

错误处理与恢复

网络驱动实现错误检测与恢复机制,处理硬件故障、网络异常及其他特殊情况。

它们监控网络设备状态,发现错误或异常后采取相应措施,如重置设备、重试失败操作,或向内核报告错误以便进一步处理。

配置与管理

网络驱动通过 Linux 的 sysfs 接口暴露配置参数和统计信息,管理员可以动态查询和调整驱动设置。

它们支持链路聚合、VLAN 标记、混杂模式及卸载功能(如校验和卸载、分段卸载)等特性,以优化网络性能和功能。

常见网络驱动示例

GNU/Linux 中的网络驱动包括:

  • Intel 千兆以太网控制器的 e1000e 驱动
  • Atheros 无线局域网适配器的 ath9k 驱动
  • Intel 10 Gigabit 以太网适配器的 ixgbe 驱动

网络驱动是继块设备驱动和字符设备驱动之后的第三类 Linux 驱动。

系统中网络接口的操作与块设备驱动类似:块设备驱动用于内核传输或接收数据块;类似地,网络驱动向内核注册,以便能与外部交换数据包。

但与块设备驱动不同的是,网络驱动不会在专用的设备目录 /dev 下有对应的入口文件,因此无法套用"Unix/GNU Linux 下万物皆文件"的规则。

这两类驱动最重要的区别在于:块设备驱动只有在被调用时才工作,而网络驱动是异步地接收来自外部网络的数据包。

换言之,块设备驱动要求在发送数据前由内核发起操作,而网络驱动则主动推动数据流。

下图展示了 GNU/Linux 内核中不同类别驱动的分类结构:

网络架构使得 Linux 内核能够通过网络连接到其他系统。

支持的硬件种类繁多,协议数量也非常多。

每个网络对象都通过套接字(socket)来表示。套接字与进程相关联,就像文件系统中的 i-node 关联一样。

一个套接字可以被多个进程共享。

网络架构利用 Linux 内核的进程调度器 schedule() 来挂起或恢复处于等待数据状态的进程(由控制和数据流管理系统管理)。

此外,虚拟文件系统(VFS)层提供了逻辑文件系统(如 NFS 的情况;用户态下也可以通过 libfuse 实现类似功能)。

下图展示了与 VFS 和网络栈交互的不同模块的详细信息:

所有网卡都可以通过两种不同方式与内核交互:

  • 轮询(Polling) :内核定期检查设备状态,判断是否有任务需要处理。
  • 中断(Interrupts) :网卡通过中断信号告诉内核有任务需要处理。

硬件层面:网卡接收来自网络接口的入站数据包,并直接将其放入输入队列。

软件层面(NET_RX_SOFTIRQ):执行接收数据包处理,负责协议管理。入站数据包通过该中断处理并排入队列,需转发的数据包则放入输出接口的输出队列。

以下代码示例来自适用于内核版本 2.2.14 的网络驱动源码(drivers/net/3c59x.c)。代码中大量使用了 #ifdef MODULE#if defined(MODULE)

arduino 复制代码
#if defined(MODULE) && LINUX_VERSION_CODE > 0x20115
MODULE_AUTHOR("Donald Becker <becker@cesdis.gsfc.nasa.gov>");
MODULE_DESCRIPTION("3Com 3c590/3c900 series Vortex/Boomerang driver");
MODULE_PARM(debug, "i");
...
#endif
...
#ifdef MODULE
int init_module(void)
{
...
}
#else
int tc59x_probe(struct device *dev)
{
...
}
#endif /* not MODULE */

这种写法是旧式编程方式的代表,通过编译时决定模块是动态模块还是内核映像中的静态模块,从而定义其操作。

arduino 复制代码
static int vortex_scan(struct device *dev, struct pci_id_info pci_tbl[])
{
...
#if defined(CONFIG_PCI) || (defined(MODULE) && !defined(NO_PCI))
...
#ifdef MODULE
if (compaq_ioaddr) {
    vortex_probe1(0, 0, dev, compaq_ioaddr, compaq_irq,
                  compaq_device_id, cards_found++);
    dev = 0;
}
#endif
return cards_found ? 0 : -ENODEV;
}
...
#ifdef MODULE
void cleanup_module(void)
{
    ...
}
#endif

在新版本中,预处理指令不再必要,去除了所有 #ifdef#endif,驱动开发者可通过一套宏(__init__exit__devinitdata)实现更简洁的代码:

csharp 复制代码
static char version[] __devinitdata = DRV_NAME " ... ";

static struct vortex_chip_info {
    ...
} vortex_info_tbl[] __devinitdata = {
    {"3c590 Vortex 10Mbps",
     ...
    }
};

static int __init vortex_init(void)
{
    ...
}

static void __exit vortex_cleanup(void)
{
    ...
}

module_init(vortex_init);
module_exit(vortex_cleanup);

下面是一个可管理网卡的示例代码:

arduino 复制代码
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>

#define DRIVER_NAME "my_ethernet_driver"

struct my_priv_data {
    struct net_device *netdev;
    struct pci_dev *pdev;
    // 添加驱动私有数据
};

static int my_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
    struct my_priv_data *priv;
    struct net_device *netdev;

    // 分配驱动私有数据内存
    priv = kzalloc(sizeof(struct my_priv_data), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    // 初始化 PCI 设备
    if (pci_enable_device(pdev))
        goto err_free_priv;

    // 使能总线主控并配置 PCI
    pci_set_master(pdev);

    // 分配并初始化网络设备结构体
    netdev = alloc_etherdev(sizeof(struct my_priv_data));
    if (!netdev)
        goto err_disable_dev;

    priv->netdev = netdev;
    priv->pdev = pdev;

    // 配置设备相关参数(例如 MAC 地址、MTU)
    // 示例:ether_setup(netdev);

    // 设置驱动私有数据和操作函数
    // 示例:netdev->netdev_ops = &my_netdev_ops;

    // 向内核注册网络设备
    if (register_netdev(netdev))
        goto err_free_netdev;

    // 添加设备相关初始化代码

    return 0;

err_free_netdev:
    free_netdev(netdev);
err_disable_dev:
    pci_disable_device(pdev);
err_free_priv:
    kfree(priv);
    return -ENODEV;
}

static void my_remove(struct pci_dev *pdev)
{
    struct my_priv_data *priv = pci_get_drvdata(pdev);
    struct net_device *netdev = priv->netdev;

    // 注销网络设备
    unregister_netdev(netdev);

    // 释放网络设备结构体
    free_netdev(netdev);

    // 禁用 PCI 设备
    pci_disable_device(pdev);

    // 释放驱动私有数据
    kfree(priv);
}

// PCI 设备 ID 表
static const struct pci_device_id my_pci_tbl[] = {
    { PCI_DEVICE(0x1234, 0x5678) }, // 厂商 ID 和设备 ID
    { 0, },
};

MODULE_DEVICE_TABLE(pci, my_pci_tbl);

// PCI 驱动结构体
static struct pci_driver my_driver = {
    .name = DRIVER_NAME,
    .id_table = my_pci_tbl,
    .probe = my_probe,
    .remove = my_remove,
};

// 模块初始化
static int __init my_init(void)
{
    return pci_register_driver(&my_driver);
}

// 模块卸载
static void __exit my_exit(void)
{
    pci_unregister_driver(&my_driver);
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BPB");
MODULE_DESCRIPTION("My Ethernet Driver");

现在,让我们解释这段代码的主要技术点:

包含文件和定义:

  • 代码包含了 Linux 内核模块开发所需的头文件,如 <linux/module.h><linux/pci.h><linux/netdevice.h><linux/etherdevice.h>
  • 定义了一个宏 DRIVER_NAME,用于表示驱动名称。

驱动数据结构:

  • struct my_priv_data:该结构体保存驱动特定的数据,例如指向网络设备(netdev)和 PCI 设备(pdev)的指针。

探测函数(my_probe):

  • 该函数在驱动加载且检测到匹配的 PCI 设备时调用。
  • 它负责初始化 PCI 设备、分配驱动私有数据内存、设置网络设备,并将设备注册到内核。

卸载函数(my_remove):

  • 当驱动卸载或关联的 PCI 设备被移除时调用。
  • 该函数注销网络设备、释放分配的资源,并禁用 PCI 设备。

PCI 设备 ID 表:

  • my_pci_tbl:列出了该驱动支持的 PCI 设备 ID。

PCI 驱动结构:

  • my_driver:定义了 PCI 驱动的结构体,包括驱动名称、支持的设备 ID 表、探测函数和卸载函数。

模块初始化与清理:

  • my_init:模块加载到内核时调用,负责向内核注册 PCI 驱动。
  • my_exit:模块从内核卸载时调用,负责注销 PCI 驱动。

模块信息:

  • MODULE_LICENSEMODULE_AUTHORMODULE_DESCRIPTION 宏为模块提供许可信息、作者信息和描述信息。

网络驱动加载

当动态加载网络驱动到内核时,Kmod 是 GNU/Linux 内核中负责管理模块的机制。加载过程会初始化要加载的模块名称(argv[0])。如果使用的是 modprobe 而非 insmod,kmod 会检查 /etc/modprobe.conf 配置文件,以确定是否存在任何依赖关系。

下图展示了网络驱动的动态加载过程:

网络驱动中可由宏初始化的内存区段列表:

这些宏大多定义在头文件 include/linux/init.h 中:

宏名称 说明
__init 启动序列中的初始化例程。
__exit 当内核组件从内核卸载时调用。
core_initcallpostcore_initcallarch_initcallsubsys_initcallfs_initcalldevice_initcalllate_initcall 一组例程,用于调整启动序列中虚拟化例程的执行优先级顺序。
__initcall 已废弃的宏。
__exitcall 以前由驱动退出例程在卸载时调用。
_initdata 初始化结构体用于启动序列。
_exitdata 初始化结构体用于启动序列。

表 8.2:部分宏的详细说明

下图描述了网络设备注册的状态机:

当网络驱动加载到 Linux 内核时,会经历多个阶段来初始化驱动并使其正常工作。主要步骤如下:

设备检测与初始化

  • Linux 内核启动时,会扫描系统硬件以检测网络设备。
  • 若检测到网络设备,内核会加载相应的驱动模块或固件以初始化设备。
  • 设备检测可通过多种机制实现,如 PCI 总线枚举、USB 设备检测或平台特定的探测程序。

驱动注册

  • 网络设备检测到且相应驱动模块加载后,驱动向 Linux 内核注册自身。
  • 注册过程包括向内核提供回调函数和数据结构,定义驱动与网络子系统的交互方式。
  • 驱动注册到内核的网络设备接口,负责管理网络设备及其关联驱动。

数据结构初始化

  • 注册时,驱动初始化内部数据结构和处理数据包及设备管理所需的资源。
  • 包括分配用于发送和接收数据包的内存缓冲区,设置 DMA 操作,初始化硬件寄存器等。

设备配置

  • 驱动根据设备能力和系统网络配置对网络设备进行配置。
  • 可能涉及设置设备的 MAC 地址、最大传输单元(MTU)、工作模式(如混杂模式)及中断处理设置。

中断处理设置

  • 如果网络设备通过中断信号指示数据包接收或发送完成等事件,驱动会设置中断处理。
  • 通常注册中断处理程序以响应设备产生的中断。
  • 中断处理程序负责处理入站数据包、管理数据包队列及协调数据包传输。

设备激活

  • 设备初始化和配置完成后,驱动激活网络设备,使其准备好发送和接收网络数据包。
  • 驱动可能开启设备的发送和接收队列,启动 DMA 操作,使设备进入工作状态。

内核通知

  • 驱动完成初始化后,可能通知内核或其他系统组件设备已准备就绪。
  • 通知方式包括发送系统日志消息、通过内核事件子系统发出事件,或更新系统状态指示。

sk_buff

当网络卡需要发送或接收数据包时,会创建一个 sk_buff 结构体。插入网络栈中的数据包直到传递到用户空间或被删除之前,数据包本身不会被复制。

该控制结构主要包含:

  • 表示以太网层、网络层和传输层的结构体
  • 用于管理 sk_buff 链表中缓冲区的指针
  • 输入/输出设备的信息
  • 数据包类型的信息(如广播、多播等)
  • 包含数据包本身的缓冲区

此外,它还描述了与 sk_buff 关联的内存块,一个是空的,另一个是已初始化并包含数据包的内存块。

此头部结构用于双向链表,支持网络栈中数据包的高效管理。下图左侧显示一个未初始化的内存块,右侧则是已初始化的内存块:

关于单链表或双链表的使用方法,将在后续章节中进行讲解。

链表头由以下数据结构定义:

arduino 复制代码
struct sk_buff_head {
     /* 这两个成员必须放在结构体的最前面。 */
     struct sk_buff  *next;  // 指向下一个 sk_buff
     struct sk_buff  *prev;  // 指向上一个 sk_buff

     __u32       qlen;       // 队列长度
     spinlock_t  lock;       // 自旋锁,用于保护链表并发访问
};

它的初始化非常简单:

以下是一些用于操作 sk_buff 结构体的主要宏和函数:

  • 宏:skb_alloc(size, priority, fclone)

    分配一个指定大小的新 sk_buff 结构体。

    • size:要分配的数据缓冲区大小。
    • priority:分配的优先级。
    • fclone:标志,指示是否分配缓冲区的完整克隆(深拷贝)。
      示例:skb_alloc(1500, GFP_KERNEL, 0)
  • 宏:skb_clone(skb, priority)

    创建现有 sk_buff 结构体的浅拷贝。

    • skb:指向原始 sk_buff 结构体的指针。
    • priority:分配的优先级。
      示例:skb_clone(skb_orig, GFP_KERNEL)
  • 函数:skb_put(skb, len)

    调整 sk_buff 结构体的尾指针,为新增数据腾出空间。

    • skb:指向 sk_buff 结构体的指针。
    • len:要添加到缓冲区的字节数。
      返回新增空间的起始指针。
      示例:skb_put(skb, 100)
  • 函数:skb_pull(skb, len)

    sk_buff 结构体的起始处移除数据。

    • skb:指向 sk_buff 结构体的指针。
    • len:要从缓冲区移除的字节数。
      示例:skb_pull(skb, 50)
  • 函数:skb_reserve(skb, len)

    sk_buff 结构体的头部预留空间,用于额外的协议头。

    • skb:指向 sk_buff 结构体的指针。
    • len:要预留的字节数。
      示例:skb_reserve(skb, 32)
  • 函数:skb_copy_expand(skb, new_len, headroom, tailroom)

    复制 sk_buff 结构体内容到一个新的缓冲区,新的缓冲区尺寸更大并有额外的头部空间和尾部空间。

    • skb:指向原始 sk_buff 结构体的指针。
    • new_len:新的缓冲区大小。
    • headroom:预留的头部空间字节数。
    • tailroom:预留的尾部空间字节数。
      返回指向新 sk_buff 结构体的指针。
      示例:skb_copy_expand(skb_orig, new_len, 16, 16)
  • 函数:skb_clone_sk(skb)

    创建 sk_buff 结构体的深拷贝,包括相关的套接字和传输层头部。

    • skb:指向原始 sk_buff 结构体的指针。
      返回指向克隆后的 sk_buff 结构体的指针。
      示例:skb_clone_sk(skb_orig)

性能调优

调优 GNU/Linux 网络栈涉及调整各种参数,以优化网络性能、吞吐量、延迟和资源利用率,针对特定工作负载和环境进行优化。

以下是常用的一组广泛且全面的 GNU/Linux 网络栈调优参数:

TCP/IP 参数:
  • net.ipv4.tcp_mem:定义为 TCP 缓冲区分配的最小、初始和最大内存量。
  • net.ipv4.tcp_window_scaling:启用 TCP 窗口缩放以支持大带宽-延迟乘积。
  • net.ipv4.tcp_timestamps:启用 TCP 时间戳,用于往返时间测量和拥塞控制。
  • net.ipv4.tcp_sack:启用选择性确认(SACK),提高丢包重传效率。
  • net.ipv4.tcp_syncookies:启用 TCP SYN Cookie,防止 SYN 洪泛攻击。
  • net.ipv4.tcp_tw_reusenet.ipv4.tcp_tw_recycle:控制 TCP TIME-WAIT 套接字的重用和回收行为。
  • net.core.rmem_defaultnet.core.wmem_default:定义默认的接收和发送套接字缓冲区大小。
  • net.core.rmem_maxnet.core.wmem_max:定义最大接收和发送套接字缓冲区大小。
  • net.ipv4.tcp_keepalive_timenet.ipv4.tcp_keepalive_intvlnet.ipv4.tcp_keepalive_probes:配置 TCP keepalive 设置。
网络接口参数:
  • net.core.netdev_max_backlog:设置网络接口接收数据包队列的最大长度。
  • net.core.dev_weight:调整网络接口的权重,用于负载均衡。
  • net.ipv4.tcp_mtu_probing:启用 TCP 最大传输单元(MTU)探测,用于路径 MTU 发现。
  • net.ipv4.tcp_congestion_control:指定 TCP 拥塞控制算法(如 cubic、reno、bbr)。
缓冲区和内存管理参数:
  • net.core.optmem_max:定义用于网络相关缓冲区的最大内存量。
  • net.ipv4.tcp_mem:指定为 TCP 缓冲区分配的最小、初始和最大内存量。
  • net.ipv4.udp_mem:指定为 UDP 缓冲区分配的最小、初始和最大内存量。
  • vm.dirty_background_bytesvm.dirty_bytes:分别控制触发后台和同步写操作之前的脏页内存量。
数据包处理参数:
  • net.core.busy_poll:启用忙等待轮询模式,降低网络延迟。
  • net.core.dev_weight:设置网络接口的权重用于负载均衡。
  • net.core.somaxconn:指定监听队列中最大挂起连接数。
  • net.ipv4.tcp_fastopen:启用 TCP Fast Open,减少连接建立延迟。
  • net.ipv4.tcp_fin_timeout:指定关闭非活动 TCP 连接的超时时间。
安全和防火墙参数:
  • net.ipv4.conf.all.accept_redirectsnet.ipv4.conf.all.secure_redirects:控制 IPv4 ICMP 重定向和安全重定向设置。
  • net.ipv4.conf.default.rp_filter:设置默认的反向路径过滤(RPFilter)模式。
  • net.ipv4.conf.all.accept_source_route:控制 IPv4 源路由的接受。
  • net.ipv4.conf.all.log_martians:记录可疑的 IPv4 数据包(martians)。
IPv6 参数:
  • 类似于 IPv4 的参数,用于调优 IPv6 行为。
  • net.ipv6.conf.all.disable_ipv6:禁用所有接口上的 IPv6。
防火墙参数(iptables/nftables):
  • 用于配置数据包过滤、网络地址转换(NAT)和连接跟踪规则的各种参数。
  • 可通过 netfilternft 命令管理防火墙规则和设置。

所有这些参数都可以通过 sysctl 命令进行修改。sysctl 是 Linux 下的命令行工具,用于动态查看、设置和管理内核参数。

它允许管理员配置内核的各种行为和性能,如网络设置、虚拟内存管理和安全选项。

通过 sysctl,用户可以查询当前内核参数的值,临时修改参数,或者通过编辑配置文件实现重启后保持更改。

常见用法包括调整网络缓冲区大小、启用或禁用 IP 转发、调优 TCP 参数以及控制进程调度。

管理员可以使用 sysctl -a 查看所有内核参数及其当前值,也可以用 sysctl -w 修改特定参数。

sysctl 做出的配置更改会立即生效,无需重启系统。但若要在重启后保持更改,需要编辑 /etc/sysctl.conf/etc/sysctl.d/ 目录下的配置文件。

修改内核参数时需谨慎,不正确的设置可能影响系统稳定性、性能和安全性。

sysctl 提供了强大的机制,帮助管理员根据具体工作负载、硬件配置和安全需求对 Linux 内核进行细致调优。

总的来说,sysctl 是系统管理员优化和定制 Linux 内核行为的多功能工具。

以下是一项研究,旨在评估将数据包复制到网络栈中的开销(时间和内存消耗):从网卡到 BSD 套接字的内存复制估计:

零拷贝(或称零拷贝网络)是网络栈设计中的一个重要概念,包括 Linux 的网络栈。它指的是一种数据传输技术,数据从一个内存缓冲区传输到另一个内存缓冲区时,避免了 CPU 的中间拷贝操作。

零拷贝机制不需要在内核空间和用户空间之间,或者不同的内核缓冲区之间进行数据拷贝,而是允许数据直接从源缓冲区传输到目标缓冲区,通常通过 DMA(直接内存访问)等机制实现。

零拷贝网络的重要性体现在以下几个方面:

  • 降低 CPU 开销:消除数据中间拷贝,减少了 CPU 在内存拷贝上的负载,显著提升性能,尤其适合处理大数据量的高速网络应用。
  • 降低延迟:拷贝数据涉及 CPU 处理和内存访问,容易引入延迟。零拷贝绕过这些步骤,使数据能更快通过网络栈传输,降低处理延迟。
  • 提高吞吐量:减少 CPU 负载和延迟,有助于提升整体网络吞吐量,实现更高的数据传输速率和更好的网络应用扩展性。
  • 优化资源利用:减少 CPU 时间和内存带宽的占用,更高效地使用系统资源,提升多任务和多线程应用的性能。
  • 节能效率:降低 CPU 使用率和内存访问次数,有助于提升网络系统的能效,尤其在移动设备和数据中心等功耗受限环境中表现显著。

在 Linux 中,支持零拷贝网络的机制和 API 包括:

  • Socket Buffers(sk_buff) :Linux 的 sk_buff 数据结构支持零拷贝,允许网络数据包在内核和用户空间间处理时无需中间拷贝。
  • 直接 I/O(Direct I/O,DIO) :Linux 提供如 sendfile()splice() 等 DIO 接口,支持文件描述符和网络套接字之间的零拷贝数据传输。
  • 内存映射 I/O(mmap) :Linux 支持内存映射 I/O 操作,使用户空间进程能直接访问内核缓冲区,无需拷贝数据。

总之,零拷贝网络是提升网络系统性能、效率和可扩展性的关键优化技术,是现代网络栈(如 Linux)的重要特性。

接下来,我们将深入探讨这一技术细节。

数据包的接收围绕缓冲环(buffer ring)进行:

下图详细展示了发送栈的流程:

该环形缓冲区可以使用 ethtool 工具进行管理:

如果可能的话,可以更改发送(Tx)和接收(Rx)环形缓冲区的大小:

ruby 复制代码
$ sudo ethtool enp3s0 -G tx 256 rx 256

对 TCP/IP 网络栈层在每秒发送一个 1024 字节的 UDP 和 TCP 数据包时的延迟进行研究,得到以下结果:

  • 总体而言,拷贝 1024 字节所需时间约为 1 微秒,调用 BSD 套接字系统原语约为 2 微秒,UDP 校验和计算时间也约为 2 微秒。
  • 采用 DMA 在设备与内存之间传输数据,从性能角度来看是一个显著优势。
  • 网络驱动传输一个网络包的时间低于 2 微秒,但由于驱动接收模式的复杂性,接收时间增加到约 4 微秒。此外,接收模式存在不可忽视的内存拷贝开销。

具体性能指标如下:

  • UDP:当需要计算校验和时,发送 1024 字节的数据包耗时 18.9 微秒,接收请求耗时 35 微秒。UDP 最大发送能力约为 433 Mbps,接收速率约为 234 Mbps。
  • TCP:TCP 协议下,发送一个 1024 字节包耗时 22.5 微秒,接收耗时 36 微秒。最大发送速率为 364 Mbps,接收速率为 228 Mbps。
  • 当发送端/接收端为本地(localhost)时,最大吞吐量约为 150 Mbps。

在 GNU/Linux 内核中,内存拷贝、校验和计算以及系统调用在 TCP 发送过程中占总体时间的 22%,在 TCP 接收过程中占 16.7%;UDP 情况也类似。

DPDK 项目

DPDK(Data Plane Development Kit,数据平面开发套件)是一个开源项目,提供了一套库和驱动,用于加速基于软件的网络数据平面中的数据包处理任务。DPDK 主要由英特尔开发,旨在优化通用硬件上的数据包处理性能,尤其适用于需要高吞吐量、低延迟和可扩展性的网络应用。

DPDK 是一个革命性的软件模块,能够实现高达数十亿比特的传输速率,展现了真正的技术性能。因此,DPDK 常与一些特定的高性能网卡一起使用,例如 NVIDIA CONNECTX-6 DX 网卡,该网卡配备了两个用于光纤的 SFP 接口。

官方网站:www.dpdk.org

以下是 DPDK 项目的关键特性和组成简介:

数据包处理加速:

DPDK 提供了一个构建高性能数据包处理应用的框架,通过绕过 Linux 内核的网络栈,直接与网络接口和硬件交互。

利用轮询、零拷贝数据包输入输出、无锁数据结构等技术,DPDK 相较于传统的基于内核的网络方法,实现了显著的性能提升。

用户空间网络栈:

DPDK 提供了用户空间网络栈,包括数据包 I/O、内存管理、缓冲池和协议处理的库。

基于 DPDK 开发的应用可以完全在用户空间执行数据包处理,避免内核上下文切换和内存拷贝的开销,从而降低延迟并提高吞吐量。

支持的硬件:

DPDK 支持多种网络接口控制器(NIC)和硬件平台,包括英特尔以太网控制器、Mellanox InfiniBand 适配器及多种虚拟化网络接口。

DPDK 利用硬件特性,例如英特尔提供的专用驱动和库,进一步提升性能和可扩展性。

数据包处理库:

DPDK 包含一系列数据包处理库,提供常见网络任务的优化实现,如数据包分类、过滤、转发和流量管理。

这些库提供构建定制数据包处理流水线和应用的 API,使开发者能够细粒度控制网络流量和性能。

与其他软件集成:

DPDK 可以与其他软件框架和项目集成,加速网络功能和应用,如软件定义网络(SDN)控制器、虚拟网络功能(VNF)及网络监控工具。

DPDK 经常与 Open vSwitch(OVS)、FD.io(向量包处理 VPP)以及独立于协议的数据包处理器(P4)等项目协同使用,提升软件定义网络环境中的数据包处理性能和可扩展性。

社区与生态:

DPDK 拥有活跃的开源社区和生态系统,汇聚了众多公司、组织和个人的贡献。

社区持续开发和维护项目,定期发布更新、修复漏洞、增加新特性,提升性能、功能,并增强对各种硬件平台和软件环境的兼容性。

总体来说,DPDK 是一个功能强大且广泛使用的框架,用于加速基于软件的网络数据平面中的数据包处理任务,提供高性能、灵活性和可扩展性,适用于多种网络应用和场景。

以下是一个使用 DPDK 实现数据包处理的简化示例。该示例演示了使用 DPDK API 进行基本的数据包接收、处理和发送。

请注意,该示例假设您熟悉 C 语言编程和基本的网络概念:

arduino 复制代码
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#define DRIVER_NAME "my_ethernet_driver"

struct my_priv_data {
    struct net_device *netdev;
    struct pci_dev *pdev;
    // 添加任何驱动特有的数据
};

static int my_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
    struct my_priv_data *priv;
    struct net_device *netdev;
    // 为驱动私有数据分配内存
    priv = kzalloc(sizeof(struct my_priv_data), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    // 初始化 PCI 设备
    if (pci_enable_device(pdev))
        goto err_free_priv;
    // 启用总线主控并设置其他 PCI 配置
    pci_set_master(pdev);
    // 分配并初始化网络设备结构
    netdev = alloc_etherdev(sizeof(struct my_priv_data));
    if (!netdev)
        goto err_disable_dev;
    priv->netdev = netdev;
    priv->pdev = pdev;
    // 设置设备特定参数(如 MAC 地址,MTU)
    // 示例:ether_setup(netdev);
    // 设置驱动特定数据和函数
    // 示例:netdev->netdev_ops = &my_netdev_ops;
    // 向内核注册网络设备
    if (register_netdev(netdev))
        goto err_free_netdev;
    // 这里添加设备特定初始化代码
    return 0;

err_free_netdev:
    free_netdev(netdev);
err_disable_dev:
    pci_disable_device(pdev);
err_free_priv:
    kfree(priv);
    return -ENODEV;
}

static void my_remove(struct pci_dev *pdev)
{
    struct my_priv_data *priv = pci_get_drvdata(pdev);
    struct net_device *netdev = priv->netdev;
    // 注销网络设备
    unregister_netdev(netdev);
    // 释放网络设备结构
    free_netdev(netdev);
    // 禁用 PCI 设备
    pci_disable_device(pdev);
    // 释放驱动私有数据
    kfree(priv);
}

// PCI 设备 ID 表
static const struct pci_device_id my_pci_tbl[] = {
    { PCI_DEVICE(0x1234, 0x5678) }, // 厂商和设备 ID
    { 0, },
};
MODULE_DEVICE_TABLE(pci, my_pci_tbl);

// PCI 驱动结构
static struct pci_driver my_driver = {
    .name = DRIVER_NAME,
    .id_table = my_pci_tbl,
    .probe = my_probe,
    .remove = my_remove,
};

// 模块初始化
static int __init my_init(void)
{
    return pci_register_driver(&my_driver);
}

// 模块清理
static void __exit my_exit(void)
{
    pci_unregister_driver(&my_driver);
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BPB");
MODULE_DESCRIPTION("My Ethernet Driver");

以下是上例中一些关键细节说明:

  • 使用 rte_eal_init() 初始化 DPDK 环境,使用 rte_pktmbuf_pool_create() 创建用于数据包缓冲的内存池。
  • 使用 rte_eth_dev_configure()rte_eth_rx_queue_setup()rte_eth_tx_queue_setup()rte_eth_dev_start() 配置并初始化每个网络端口的接收(RX)和发送(TX)操作。
  • 在主数据包处理循环中,不断从第一个网络端口接收数据包(rte_eth_rx_burst()),并转发到第二个网络端口(rte_eth_tx_burst())。
  • 数据包处理循环会持续运行,直到程序终止。

该示例展示了使用 DPDK API 进行网络端口配置、数据包缓冲管理和数据包 I/O 操作的基础转发应用。实际的 DPDK 应用可能会根据需求增加数据包过滤、数据包检测和协议处理等功能。

构建此 DPDK 示例的一般步骤:

  1. 确保系统中已安装 DPDK,可从官网下载:www.dpdk.org/
  2. 按照 DPDK 官方安装说明设置开发环境,包括环境变量(RTE_SDK 和 RTE_TARGET)和 Hugepages 配置。
  3. 新建一个 C 源文件,将上述 DPDK 示例代码复制到文件中。
  4. 使用 DPDK 提供的构建系统编译代码,通常使用 make 命令和合适的参数。
  5. 编写 Makefile,包含 DPDK 库和头文件路径,指定目标架构(RTE_TARGET)。
  6. 运行 make 命令编译生成可执行文件。
  7. 编译成功后,运行生成的可执行文件执行 DPDK 应用,确保有相应权限和配置访问网络接口。

这是一个用于构建 DPDK 示例的简化版 Makefile 示例:

makefile 复制代码
# DPDK 示例的 Makefile

CC := gcc
CFLAGS := -O3 -std=gnu99 -Wall

# DPDK 配置
RTE_SDK := /path/to/dpdk
RTE_TARGET := x86_64-native-linuxapp-gcc

# 包含 DPDK 构建系统
include $(RTE_SDK)/mk/rte.vars.mk

# 应用程序构建选项
APP := dpdk_example
SRCS := dpdk_example.c
OBJS := $(SRCS:.c=.o)

# 编译规则
$(APP): $(OBJS)
	$(CC) $(LDFLAGS) $(OBJS) -o $@ $(LDLIBS)

# 依赖规则
%.o: %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) -c $< -o $@

# 清理规则
clean:
	rm -f $(OBJS) $(APP)

注意:请将 /path/to/dpdk 替换成你实际的 DPDK 安装路径。

如果编译成功,会生成一个名为 dpdk_example 的可执行文件,你可以运行它来执行 DPDK 应用程序。请确保已正确配置系统环境和网络接口,以便 DPDK 应用程序能够访问和处理网络数据包。

总结

本章详细讲解了 GNU/Linux 内核的网络栈工作原理,包括网络卡驱动的开发、性能调优以及安全防护。最后,还介绍了围绕开源项目 DPDK 的重大流量管理的最新进展。

在下一章,我们将深入学习如何保障 Linux 内核的安全。

相关推荐
共享家95271 分钟前
linux-高级IO(上)
java·linux·服务器
Wgllss2 小时前
雷电雨效果:Kotlin+Compose+协程+Flow 实现天气UI
android·架构·android jetpack
归辞...3 小时前
「iOS」————设计架构
ios·架构
bing.shao3 小时前
微服务容错与监控体系设计
微服务·云原生·架构
小米里的大麦3 小时前
022 基础 IO —— 文件
linux
Xの哲學3 小时前
Perf使用详解
linux·网络·网络协议·算法·架构
门前灯3 小时前
Linux系统之iprconfig 命令详解
linux·运维·服务器·iprconfig
自由的疯4 小时前
在 Java IDEA 中使用 DeepSeek 详解
java·后端·架构
tb_first4 小时前
k8sday09
linux·云原生·容器·kubernetes