网络缓冲区和内存管理

作者:Alan Cox

Linux 操作系统实现了行业标准的 Berkeley 套接字 API,该 API 起源于 BSD Unix 的开发(4.2/4.3/4.4 BSD)。在本文中,我们将研究现有 Linux 内核下网络层和网络设备驱动程序的内存管理和缓冲的实现方式,并解释随着时间的推移,某些事情是如何以及为何发生变化的。

核心概念

网络层在其设计中相当面向对象,Linux 内核的许多部分也是如此。网络代码的核心结构可以追溯到 Ross Biro 和 Orest Zborowski 分别对网络和套接字的初始实现。关键对象是

  • 设备或接口: 网络接口是用于发送和接收数据包的编程代码。通常,接口用于物理设备,例如以太网卡;但是,某些设备仅是软件,例如,用于向自身发送数据的环回设备。

  • 协议: 每种协议实际上都是一种不同的网络语言。某些协议的存在纯粹是因为供应商选择使用专有的网络方案,而另一些协议则是为特殊目的而设计的。在 Linux 内核中,每种协议都是一个单独的代码模块,为套接字层提供服务。

  • 套接字: 套接字是网络中的一个连接,它提供 Unix 文件 I/O,并作为用户程序的文件描述符存在。在内核中,每个套接字都是一对结构,分别代表高级套接字接口和低级协议接口。

  • sk_buff: 网络层使用的所有缓冲区都是 sk_buff。这些缓冲区的控制由核心低级库例程提供,这些例程可供所有网络系统使用。sk_buff 提供网络协议所需的通用缓冲和流量控制功能。

sk_buff 的实现

sk_buff 例程的主要目标是为所有网络层提供一致且高效的缓冲区处理方法,并通过保持一致性,使向所有协议提供更高级别的 sk_buff 和套接字处理功能成为可能。

sk_buff 是一个控制结构,附带一块内存块。sk_buff 库中提供了两组主要功能。第一组包含操作 sk_buff 双向链表的例程;第二组包含控制附加内存的函数。缓冲区保存在链表上,这些链表针对常见的网络操作(追加到末尾和从开头删除)进行了优化。由于如此多的网络功能发生在中断期间,因此这些例程被编写为使用原子内存。由此产生的小额外开销非常值得它在错误查找方面节省的精力。

我们使用列表操作来管理从网络到达以及发送到物理接口的数据包组。我们使用内存操作例程以标准化且高效的方式处理数据包的内容。

在其最基本的级别上,缓冲区列表使用如下函数进行管理

void append_frame(char *buf, int len)
{
  struct sk_buff *skb=alloc_skb(len, GFP_ATOMIC);
  if(skb==NULL)
    my_dropped++;
  else
  {
    skb_put(skb,len);
    memcpy(skb->data,data,len);
    skb_append(&my_list, skb);
  }
}
void process_queue(void)
{
  struct sk_buff *skb;
  while((skb=skb_dequeue(&my_list))!=NULL)
  {
    process_data(skb);
    kfree_skb(skb, FREE_READ);
  }
}

这两段相当简单的代码实际上相当准确地演示了接收数据包的机制。append_frame() 函数类似于设备驱动程序接收数据包时从中断调用的代码,process_frame() 类似于调用以将数据馈送到协议的代码。如果您查看 net/core/dev.c 中的 netif_rx()net_bh(),您会看到它们以类似的方式管理缓冲区。它们要复杂得多,因为它们必须将数据包馈送到正确的协议并管理流量控制,但是基本操作是相同的。如果您查看从协议代码到用户应用程序的缓冲区,情况也是如此。

该示例还显示了数据控制函数之一 skb_put() 的用法。这里它用于在缓冲区中为我们希望向下传递的数据保留空间。

让我们看看 append_frame()alloc_skb() 函数获取一个长度为 len 字节的缓冲区(图 1),它由以下部分组成:

  • 缓冲区头部 0 字节的空余空间

  • 0 字节的数据,以及

  • 数据末尾 len 字节的空余空间。

skb_put() 函数(图 4)通过缓冲区末尾的可用空间向上增长 数据 区域,从而为 memcpy() 保留空间。许多发送数据包的网络操作在每次执行发送时都会在帧的开头添加空间,以便可以将标头添加到数据包中。因此,提供了 skb_push() 函数(图 5),以便在已保留足够的空间来完成此操作的情况下,可以将数据帧的开头向下移动到内存中。

图 1 “alloc_skb 之后”

图 2 “skb_reserve 之后”

图 3 “包含数据的 sk_buff”

图 4 “在缓冲区上调用 skb_put 之后”

图 5 “在上一个缓冲区上发生 skb_push 之后”

在缓冲区分配之后,所有可用空间都在末尾。另一个函数 skb_reserve()图 2)可以在添加数据之前调用。此函数允许您指定某些空间应位于缓冲区的开头。因此,许多发送例程都以如下代码开头:

    skb=alloc_skb(len+headspace, GFP_KERNEL);
    skb_reserve(skb, headspace);
    skb_put(skb,len);
    memcpy_fromfs(skb->data,data,len);
    pass_to_m_protocol(skb);

在 BSD Unix 等系统中,您不需要预先知道您将需要多少空间,因为它使用小缓冲区 (mbufs) 链作为其网络缓冲区。Linux 选择使用线性缓冲区并预先节省空间(通常浪费几个字节以适应最坏的情况),因为线性缓冲区使许多其他操作更快。

Linux 提供了以下用于操作列表的函数

  • skb_dequeue() 从列表中获取第一个缓冲区。如果列表为空,则返回 NULL 指针。此函数用于从队列中拉取缓冲区。缓冲区通过例程 skb_queue_head()skb_queue_tail() 添加。

  • skb_queue_head() 将缓冲区放置在列表的开头。与所有列表操作一样,它是原子的。

  • skb_queue_tail() 将缓冲区放置在列表的末尾,是最常用的函数。几乎所有队列都通过一组例程使用此函数对数据进行排队来处理,而另一组例程使用 skb_dequeue() 从同一队列中删除项目。

  • skb_unlink() 从包含它的任何列表中删除缓冲区。缓冲区不会被释放,只是从列表中删除。为了使某些操作更容易,您无需知道缓冲区在哪个列表中,并且始终可以为不在任何列表中的缓冲区调用 skb_unlink()。此函数使网络代码能够将缓冲区从使用中拉出,即使网络协议不知道当前谁在使用该缓冲区。提供了一个单独的锁定机制,以便设备驱动程序当前正在使用的缓冲区无法删除。

  • 一些更复杂的协议(如 TCP)按顺序保存帧,并在接收到数据时重新排序其输入。存在两个函数 skb_insert()skb_append(),允许用户将 sk_buff 放置在列表中的特定缓冲区之前或之后。

  • alloc_skb() 创建一个新的 sk_buff 并对其进行初始化。返回的缓冲区已准备好使用,但假定您将填写一些字段以指示应如何释放缓冲区。通常,这是通过 skb->free=1 完成的。可以通过 kfree_skb() 将缓冲区标记为不可释放(请参阅下文)。

  • kfree_skb() 释放缓冲区,如果设置了 skb->sk,则会降低套接字 (sk) 的内存使用计数。套接字和协议级例程负责增加这些计数,并避免释放具有未完成缓冲区的套接字。内存计数非常重要,因为内核网络层需要知道每个连接占用了多少内存,以防止远程机器或本地进程使用过多内存。

  • skb_clone() 创建 sk_buff 的副本,但不复制数据区域,数据区域必须被视为只读。

  • 有时需要数据副本进行编辑,skb_copy() 提供与 skb_clone 相同的功能,但也复制数据(因此开销更高)。

图 6 数据包的流动

更高级别的支持例程

为套接字分配和排队缓冲区的语义还涉及流量控制规则,以及发送与信号和可选设置(如非阻塞)的整个交互列表。设计了两个例程,使大多数协议都能轻松实现这一点。

sock_queue_rcv_skb() 函数用于处理传入数据流量控制,通常以以下形式使用

    sk=my_find_socket(whatever);
    if(sock_queue_rcv_skb(sk,skb)==-1)
    {
        myproto_stats.dropped++;
        kfree_skb(skb,FREE_READ);
        return;
    }

此函数使用套接字读取队列计数器来防止大量数据排队到套接字。达到限制后,数据将被丢弃。应用程序需要足够快地读取数据,或者像 TCP 中那样,协议需要通过网络进行流量控制。当 TCP 无法再对数据进行排队时,实际上会告诉发送机器闭嘴。

在发送端,sock_alloc_send_skb() 处理信号处理、非阻塞标志以及所有阻塞语义,直到发送队列中有空间为止,这样您就不会被为慢速接口排队的数据占用所有内存。许多协议发送例程都使用此函数完成几乎所有工作

    skb=sock_alloc_send_skb(sk,....)
    if(skb==NULL)
        return -err;
    skb->sk=sk;
    skb_reserve(skb, headroom);
    skb_put(skb,len);
    memcpy(skb->data, data, len);
    protocol_do_something(skb);

我们之前已经遇到过其中的大部分内容。非常重要的一行是 skb->sk=sksock_alloc_send_skb() 已将缓冲区的内存计入套接字。通过设置 skb->sk,我们告诉内核,任何对缓冲区执行 kfree_skb() 的人都应将缓冲区的内存计入套接字。因此,当设备发送缓冲区并释放它时,用户可以发送更多数据。

网络设备

所有 Linux 网络设备都遵循相同的接口,但并非所有设备都需要该接口中提供的许多功能。使用了面向对象的思维方式,每个设备都是一个对象,其中一系列方法填充到一个结构中。每个方法都以设备本身作为第一个参数调用,以绕过 C 语言中缺少 C++ 的 this 概念的问题。

文件 drivers/net/skeleton.c 包含网络设备驱动程序的骨架。从最新的内核中查看或打印副本,并在本文的其余部分中继续阅读。

基本结构

图 7 Linux 网络设备的结构

每个网络设备都完全处理从协议到物理介质的网络缓冲区传输,以及接收和解码硬件生成的响应。传入帧被转换为网络缓冲区,通过协议识别并传递给 netif_rx()。然后,此函数将帧传递到协议层以进行进一步处理。

每个设备都提供了一组附加方法,用于处理数据包的停止、启动、控制和物理封装。所有控制信息都收集在用于管理每个设备的设备结构中。

命名

所有 Linux 网络设备都有一个唯一的名称,该名称与设备可能具有的文件系统名称没有任何关系。实际上,网络设备通常没有文件系统表示形式,尽管您可以创建一个绑定到设备驱动程序的设备。传统上,名称仅指示设备的类型,而不是其制造商。同一类型的多个设备从 0 开始向上编号;因此,以太网设备被称为 “eth0”、“eth1”、“eth3” 等。命名方案很重要,因为它允许用户以 “以太网卡” 而不是担心电路板的制造商的方式编写程序或系统配置,并在更换电路板时强制重新配置。

以下名称当前用于通用设备

  • ethn 以太网控制器,包括 10 和 100Mbit/秒

  • trn 令牌环设备

  • sln SLIP 设备和 AX.25 KISS 模式

  • pppn PPP 设备,包括异步和同步

  • plipn PLIP 单元;编号与打印机端口匹配

  • tunln IPIP 封装隧道

  • nrn NetROM 虚拟设备

  • isdnn 由 isdn4linux 处理的 ISDN 接口 (*)

  • dummyn 空设备

  • lo 环回设备

(*) 至少一个 ISDN 接口是以太网模拟器 — Sonix PC/Volante 驱动程序的行为在所有方面都如同以太网而不是 ISDN;因此,它使用 “eth” 设备名称。如果可能,新设备应选择反映现有实践的名称。当您添加全新的物理层类型时,您应该寻找其他从事此类项目的人员并使用通用的命名方案。

某些物理层在一个介质上呈现多个逻辑接口。ATM 和帧中继都具有此属性,业余无线电环境中的多点 KISS 也是如此。在这种情况下,每个活动通道都需要一个驱动程序。Linux 网络代码的结构使其可以管理,而无需过多的额外代码。此外,名称注册方案允许您在通道出现和消失时几乎随意创建和删除接口。此类名称的提议约定仍在讨论中,因为 “sl0a”、“sl0b”、“sl0c” 的简单方案适用于多点 KISS 等基本设备,但不适用于虚拟通道可以跨物理板移动的多个帧中继连接。

注册设备

每个设备都是通过填写 struct device 对象并将其传递给 register_netdev(struct device *) 调用来创建的。这会将您的设备结构链接到内核网络设备表中。由于您传入的结构由内核使用,因此您必须在通过 void unregister_netdev(struct device *) 调用卸载设备之前不要释放它。这些调用通常在启动时或模块加载/卸载时完成。

如果创建多个同名设备,内核不会反对,但会崩溃。因此,如果您的驱动程序是可加载模块,则应使用 struct device *dev_get(const char *name) 调用来确保该名称尚未被使用。如果正在使用,则应选择另一个名称,否则您的新驱动程序将失败。如果您发现冲突,则不得使用 unregister_netdev() 来注销使用该名称的其他设备!

典型的注册代码序列是

int register_my_device(void)
{
  int i=0;
  for(i=0;i<100;i++)
  {
    sprintf(mydevice.name,"mydev%d",i);
    if(dev_get(mydevice.name)==NULL)
    {
      if(register_netdev(&mydevice)!=0)
        return -EIO;
      return 0;
    }
  }
  printk(
"100 mydevs loaded. Unable to load more.\n");
  return -ENFILE;
}
设备结构

每个网络设备的所有通用信息和方法都保存在设备结构中。要创建设备,您需要提供包含以下讨论的大部分数据的结构。本节介绍应如何设置设备。

命名

首先,name 字段保存一个字符串指针,指向先前讨论的格式的设备名称。name 字段也可以是 " " (四个空格),在这种情况下,内核会自动为其分配一个 ethn 名称。不应使用此特殊功能。在 Linux 2.0 之后,我们计划为此目的添加一个形式为 dev_make_name("eth") 的简单支持函数。

总线接口参数

下一组参数用于维护设备在架构的设备地址空间中的位置。irq 字段保存设备正在使用的中断 (IRQ),通常在启动时或由初始化函数设置。如果未使用中断、当前未知或未分配,则应使用值零。可以通过多种方式设置中断。可以使用内核的自动 IRQ 功能来探测设备中断,也可以在加载网络模块时设置中断。网络驱动程序通常使用名为 irq 的全局 int 来实现此目的,以便用户可以使用 insmod mydevice irq=5 样式命令加载模块。最后,可以使用 ifconfig 命令动态设置 IRQ 字段,这将导致调用您的设备,我们稍后将讨论该设备。

base_addr 字段是设备所在的基址 I/O 空间地址。如果设备不使用 I/O 位置或在没有 I/O 空间概念的系统上运行,则应将此字段设置为零。当此地址可由用户设置时,通常由名为 io 的全局变量设置。接口 I/O 地址也可以使用 ifconfig 设置。

为诸如 ISA 总线共享内存以太网卡之类的事物定义了两个硬件共享内存范围。就当前目的而言,rmem_startrmem_end 字段已过时,应加载为 0。mem_startmem_end 地址应加载为此设备使用的共享内存块的起始地址和结束地址。如果未使用共享内存块,则应存储值 0。允许用户设置内存基址的那些设备使用名为 mem 的全局变量,然后自行设置 mem_end 地址。

dma 变量保存设备正在使用的 DMA 通道。Linux 允许自动探测 DMA(如中断)。如果未使用 DMA 通道或 DMA 通道尚未设置,则使用值 0。此选项可能需要更改,因为最新的 PC 板允许硬件板使用 ISA 总线 DMA 通道 0,而不仅仅将其绑定到内存刷新。如果用户可以设置 DMA 通道,则使用全局变量 dma

重要的是要认识到,物理信息是为控制和用户查看(以及驱动程序的内部功能)而提供的,并且不注册这些区域以防止它们被重用。因此,设备驱动程序还必须分配和注册它希望使用的 I/O、DMA 和中断线,使用与任何其他设备驱动程序相同的内核函数。[有关编写字符设备驱动程序的最新 Kernel Korner 文章,请参阅第 23、24、25、26 和 28 期,或访问新的 Linux Kernel Hackers' Guide,网址为 www.redhat.com:8080/HyperNews/get/khg.html,以获取有关必要功能的更多信息 — ED]

if_port 字段保存多媒体设备(如组合以太网卡)的物理介质类型。

协议层变量

为了使网络协议层

以合理的方式执行,设备必须提供一组功能标志和变量,这些标志和变量也保存在设备结构中。

mtu 是可以通过此接口发送的最大有效负载,即不包括设备本身将提供的任何底层标头的最大数据包大小。协议层(如 IP)使用此数字来选择合适的数据包大小进行发送。每个协议都有最小限制。没有 576 字节或更大的帧大小,设备不能用于 IPX。IP 至少需要 72 字节,并且在低于约 200 字节的情况下无法正常运行。是否与您的设备合作由协议层决定。

family 始终设置为 AF_INET,表示设备正在使用的协议族。Linux 允许设备同时使用多个协议族,并且仅维护此信息以使其更像标准的 BSD 网络 API。

接口硬件 type 字段取自物理介质类型表。ARP 协议使用的值(请参阅 RFC1700)由那些支持 ARP 的介质使用,并且为其他物理层分配了附加值。每当需要时,都会向内核和 net-tools(包含诸如 ifconfig 之类的程序包,这些程序包需要能够解码此字段)添加新值。截至 Linux pre2.0.5 定义的字段是

From RFC1700:
ARPHRD_NETROM   NET/ROM™ devices
ARPHRD_ETHER         10 and 100Mbit/second Ethernet
ARPHRD_EETHER   Experimental Ethernet (not used)
ARPHRD_AX25          AX.25 level 2 interfaces
ARPHRD_PRONET        PROnet token ring (not used)
ARPHRD_CHAOS         ChaosNET (not used)
ARPHRD_IEE802        802.2 networks notably token ring
ARPHRD_ARCNET        ARCnet interfaces
ARPHRD_DLCI          Frame Relay DLCI
Defined by Linux:
ARPHRD_SLIP          Serial Line IP protocol
ARPHRD_CSLIP         SLIP with VJ header compression
ARPHRD_SLIP6         6bit encoded SLIP
ARPHRD_CSLIP6        6bit encoded header compressed SLIP
ARPHRD_ADAPT         SLIP interface in adaptive mode
ARPHRD_PPP      PPP interfaces (async and sync)
ARPHRD_TUNNEL   IPIP tunnels
ARPHRD_TUNNEL6  IPv6 over IP tunnels
ARPHRD_FRAD          Frame Relay Access Device
ARPHRD_SKIP          SKIP encryption tunnel
ARPHRD_LOOPBACK Loopback device
ARPHRD_LOCALTLK Localtalk apple networking device
ARPHRD_METRICOM Metricom Radio Network

那些标记为未使用的接口是已定义的类型,但在现有的 net-tools 上没有任何当前支持。Linux 内核为使用以太网和令牌环的设备提供了额外的通用支持例程。

当接口启动时,pa_addr 字段用于保存 IP 地址。接口应以清除此变量的状态启动。pa_brdaddr 用于保存配置的广播地址,pa_dstaddr 是点对点链路的目标,pa_mask 是接口的 IP 网络掩码。所有这些都可以初始化为零。pa_alen 字段保存地址的长度(在本例中为 IP 地址),应初始化为 4。

链路层变量

hard_header_len 是设备在传递给它的网络缓冲区开头需要的字节数。此值不必等于将添加的物理标头的字节数,尽管通常使用此数字。设备可以使用此值在每个缓冲区的开头为自己提供一个暂存区。

在 1.2.x 系列内核中,skb->data 指针将指向缓冲区开头,您必须避免发送您的暂存区。这也意味着对于具有可变长度标头的设备,您需要分配 max_size+1 字节,并在开头保留一个长度字节,以便您知道标头实际开始的位置(标头应与数据连续)。Linux 1.3.x 使生活变得更加简单。它确保您在缓冲区开头至少有与您请求的空间一样多的可用空间。您需要适当地使用 skb_push(),正如我们在关于网络缓冲区的章节中讨论的那样。

物理介质地址(如果有)分别保存在 dev_addrbroadcast 中,并且是字节数组。小于数组大小的地址从左侧开始存储。addr_len 字段用于保存硬件地址的长度。对于许多介质,没有硬件地址,在这种情况下,此字段应设置为零。对于某些其他接口,地址必须由用户程序设置。ifconfig 工具允许设置接口硬件地址。在这种情况下,最初不必设置它,但打开代码应注意不要在地址设置之前允许设备开始传输。

标志

一组标志用于维护接口属性。其中一些是 “兼容性” 项目,因此不是直接有用的。标志是

  • IFF_UP 接口当前处于活动状态。在 Linux 中,IFF_RUNNING 和 IFF_UP 标志基本上作为一对处理,出于兼容性原因而作为两个项目存在。当接口未标记为 IFF_UP 时,可以将其删除。与 BSD 不同,未设置 IFF_UP 的接口永远不会接收数据包。

  • IFF_BROADCAST 接口具有广播功能。设备地址中将存储接口的有效 IP 地址。

  • IFF_DEBUG 指示需要调试。当前未使用。

  • IFF_LOOPBACK 环回接口 (lo) 是唯一设置了此标志的接口。在其他接口上设置它既未定义也不是一个好主意。

  • IFF_POINTOPOINT 此接口是点对点链路(如 SLIP 或 PPP)。没有广播功能。设备结构中的远程点对点地址有效。通常,点对点链路没有网络掩码或广播,但如果需要,可以启用它。

  • IFF_NOTRAILERS 比历史兼容性标志更史前。未使用。

  • IFF_RUNNING 请参阅 IFF_UP

  • IFF_NOARP 接口不执行 ARP 查询。此类接口必须具有地址转换的静态表,或者不需要执行映射。NetROM 接口就是一个很好的例子。这里的所有条目都是手动配置的,因为 NetROM 协议无法执行 ARP 查询。

  • IFF_PROMISC 如果可能,接口将听到网络上的所有数据包。此标志通常用于网络监视,尽管它也可以用于桥接。一个或两个接口(如 AX.25 接口)始终处于混杂模式。

  • IFF_ALLMULTI 接收所有多播数据包。无法执行此操作但可以接收所有数据包的接口将在被要求执行此任务时进入混杂模式。

  • IFF_MULTICAST 指示接口支持多播 IP 流量,这与支持物理多播不同。例如,AX.25 通过物理广播支持 IP 多播。诸如 SLIP 之类的点对点协议通常支持 IP 多播。

数据包队列

数据包由内核协议代码为接口排队。在每个设备中,buffs[] 是每个内核优先级级别的数据包队列数组。这些完全由内核代码维护,但必须在启动时由设备本身初始化。使用的初始化代码是

int ct=0;
while(ct<DEV_NUMBUFFS)
{
    skb_queue_head_init(&dev->buffs[ct]);
    ct++;
}

所有其他字段应初始化为 0。

设备可以通过将字段 dev->tx_queue_len 设置为内核应为设备排队的最大帧数来选择其需要的队列长度。通常,以太网约为 100,串行线路约为 10。设备可以动态修改此值,尽管其效果会略微滞后于更改。

网络设备方法

每个网络设备都必须提供一组实际函数(方法)用于基本低级操作。它还应提供一组支持函数,将协议层与它提供的链路层的协议要求连接起来。

设置

当设备初始化并注册到系统时,会调用 init 方法,以执行任何必要的低级验证和检查。如果设备不存在、区域无法注册或设备无法继续,则返回错误代码。如果 init 方法返回错误,则 register_netdev() 调用将返回错误代码,并且不会创建设备。

帧传输

所有设备都必须提供传输功能。设备可能存在但无法传输。在这种情况下,设备需要一个传输函数,该函数只需释放传递给它的缓冲区。虚拟设备在传输时正好具有此功能。

调用 dev->hard_start_xmit() 函数是为了向驱动程序提供其自己的设备指针和网络缓冲区(sk_buff),用于传输。如果您的设备无法接受缓冲区,则应返回 1 并将 dev->tbusy 设置为非零值。此操作会将缓冲区排队以供稍后重试,尽管不能保证会发生重试。如果协议层决定释放驱动程序已拒绝的缓冲区,则该缓冲区将不会返回给设备。如果设备知道缓冲区在不久的将来无法传输(例如,由于不良拥塞),则可以调用 dev_kfree_skb() 来转储缓冲区并返回 0,指示缓冲区已被处理。

如果有空间,则应处理缓冲区。传递下来的缓冲区已经包含所有标头,包括必要的链路层标头,只需要加载到硬件中进行传输即可。此外,缓冲区已锁定,这意味着设备驱动程序拥有缓冲区的绝对所有权,直到它选择放弃它为止。sk_buff 的内容保持只读,但保证下一个/上一个指针是空闲的,因此您可以使用 sk_buff 列表原语来构建缓冲区的内部链。

当缓冲区已加载到硬件中,或者在某些 DMA 驱动设备的情况下,当硬件指示传输完成时,驱动程序必须通过调用 dev_kfree_skb(skb, FREE_WRITE) 来释放缓冲区。一旦发出此调用,有问题的 sk_buff 可能会自发消失,设备驱动程序不应再次引用它。

帧标头

高级协议有必要在将每个帧排队以进行传输之前,将低级标头附加到每个帧。协议显然也不希望预先知道如何为所有可能的帧类型附加低级标头。因此,协议层调用设备,并提供一个缓冲区,该缓冲区在缓冲区的开头至少有 dev->hard_header_len 字节的可用空间。然后,由网络设备正确调用 skb_push() 并使用 dev->hard_header() 方法将标头放在数据包上。没有链路层标头的设备(如 SLIP)可以将此方法指定为 NULL。

该方法通过提供相关的缓冲区、设备的指针、其协议标识、指向源和目标硬件地址的指针以及要发送的数据包的长度来调用。由于该例程可以在协议层完全组装之前被调用,因此至关重要的是该方法使用长度参数,而不是缓冲区长度。

源地址可以为 NULL,表示“使用此设备的默认地址”,目标地址可以为 NULL,表示“未知”。如果由于目标地址未知而无法完成标头,则应分配空间,并应填充任何可以填充的字节。然后,该函数必须返回添加到标头的字节数的负数。此功能目前仅供 IP 在必须进行 ARP 处理时使用。如果标头已完全构建,则该函数必须返回添加到缓冲区开头的标头字节数。

当标头无法完成时,协议层将尝试解析必要的地址。在这种情况下,将调用 dev->rebuild_header() 方法,参数包括标头所在的地址、相关设备、目标 IP 地址和网络缓冲区指针。如果设备能够通过任何可用手段(通常是 ARP)解析地址,则它会填充物理地址并返回 1。如果标头无法解析,则返回 0,并且当协议层有理由相信可以进行解析时,将再次尝试该缓冲区。

接收

网络设备中没有接收方法,因为是由设备调用此类事件的处理。对于典型的设备,中断通知处理程序已完成的数据包已准备好接收。设备使用 dev_alloc_skb() 分配适当大小的缓冲区,并将来自硬件的字节放入缓冲区。接下来,设备驱动程序分析帧以确定数据包类型。驱动程序将 skb->dev 设置为接收帧的设备。它将 skb->protocol 设置为帧表示的协议,以便可以将帧提供给正确的协议层。链路层标头指针存储在 skb->mac.raw 中,链路层标头使用 skb_pull() 移除,以便协议无需了解它。最后,为了保持链路和协议隔离,设备驱动程序必须将 skb->pkt_type 设置为以下之一

  • PACKET_BROADCAST 链路层广播

  • PACKET_MULTICAST 链路层多播

  • PACKET_SELF 发给我们的帧

  • PACKET_OTHERHOST 发给另一个单主机的帧

最后一种类型通常是在接口以混杂模式运行时报告的。

最后,设备驱动程序调用 netif_rx() 以将缓冲区传递到协议层。缓冲区被排队,以便在中断处理程序返回后由网络协议处理。以这种方式延迟处理显著减少了禁用中断的时间,并提高了整体响应能力。一旦调用 netif_rx(),缓冲区就不再是设备驱动程序的属性,不能再被更改或引用。

协议在两个级别上对接收到的数据包应用流控制。首先,netif_rx() 可以处理的最大数据量是有限制的。其次,系统上的每个套接字都有一个队列,用于限制挂起的数据量。因此,所有流控制都由协议层应用。在发送端,每个设备的变量 dev->tx_queue_len 用作队列长度限制器。队列的大小通常为 100 帧,这足够大,可以在通过快速链路发送大量数据时保持队列充分填充。在慢速链路(如 SLIP 链路)上,队列通常设置为大约 10 帧,因为即使发送 10 帧也是几秒钟的排队数据。

对于大多数现有设备的接收操作,以及如果可能您应该实现的一项神奇之处是,在缓冲区的头部保留必要的字节,以使 IP 标头落在长字边界上。因此,现有的以太网驱动程序会执行以下操作

skb=dev_alloc_skb(length+2);
if(skb==NULL)
    return;
skb_reserve(skb,2);
/* then 14 bytes of ethernet hardware header */

将 IP 标头对齐到 16 字节边界,这也是缓存行的开始,有助于提高性能。在 SPARC 或 DEC Alpha 上,这些改进非常明显。

可选功能

每个设备都可以选择向协议层提供额外的功能和设施。不实现这些功能会导致通过接口提供的服务降级,但不会阻止操作。这些操作分为两类——配置和激活/关闭。

激活和关闭

当设备被激活(即,标志 IFF_UP 被设置)时,如果设备提供了 dev->open() 方法,则会调用该方法。此调用允许设备执行任何操作,例如启用接口,这是在使用接口时需要的。从此函数返回错误会导致设备保持关闭状态,并导致用户的激活请求失败,并返回 dev->open() 返回的错误。

dev->open() 函数也可以用于作为模块加载的任何设备。这里有必要防止设备在打开时被卸载;因此,必须在 open 方法中使用 MOD_INC_USE_COUNT 宏。

当设备准备好配置关闭时,会调用 dev->close() 方法,该方法应以最小化机器负载的方式关闭硬件(例如,通过禁用接口或其生成中断的能力)。它也可以用于允许模块设备在关闭后被卸载。内核的其余部分以这样一种方式构建,即当设备关闭时,通过指针对其的所有引用都会被删除,以确保设备可以从运行的系统中安全卸载。close 方法不允许失败。

配置和统计信息

一组函数提供了查询和设置操作参数的能力。其中第一个也是最基本的是 get_stats 例程,当调用时,它会为接口返回一个 enet_statistics 结构块。此块允许用户程序(如 ifconfig)查看接口的负载和任何记录的问题帧。不提供此块意味着将没有统计信息可用。

每当超级用户进程发出 SIOCSIFHWADDR 类型的 ioctl 以更改设备的物理地址时,都会调用 dev->set_mac_address() 函数。对于许多设备,此函数没有意义,对于其他设备,则不支持此函数。在这些情况下,将此函数指针设置为 NULL。某些设备只有在接口关闭时才能执行物理地址更改。对于这些设备,请检查 IFF_UP 标志,如果已设置,则返回 -EBUSY

当用户输入类似 ifconfig eth0 irq 11 的命令时,SIOCSIFMAP 函数会调用 dev->set_config() 函数。然后,它传递一个 ifmap 结构,其中包含所需的 I/O 和其他接口参数。对于大多数接口,此函数不是很有用,您可以返回 NULL。

最后,每当在您的接口上使用 SIOCDEVPRIVATESIOCDEVPRIVATE+15 范围内的 ioctl 时,都会调用 dev->do_ioctl() 调用。所有这些 ioctl 调用都接受一个 ifreq 结构,该结构在调用您的处理程序之前复制到内核空间,并在结束时复制回来。为了最大的灵活性,任何用户都可以进行这些调用,并且由您的代码在适当时检查超级用户状态。例如,PLIP 驱动程序使用这些调用来设置并行端口超时速度,以便用户可以针对他的机器调整 plip 设备。

多播

某些物理介质类型(如以太网)在物理层支持多播帧。多播帧被网络上的一组主机(不一定是全部)听到,而不是从一个主机到另一个主机。

以太网卡的功能差异很大。大多数分为以下三类之一

  • 无多播过滤器。网卡要么接收所有多播,要么不接收任何多播。在具有大量多播流量的网络(如群组视频会议)上,此类网卡可能很麻烦。

  • 哈希过滤器。一个表被加载到网卡上,为所需的多播提供条目的掩码。此方法过滤掉一些不需要的多播,但不是全部。

  • 完美过滤器。大多数支持完美过滤器的网卡都将此选项与上述 1 或 2 结合使用,因为完美过滤器通常具有 8 或 16 个条目的长度限制。

以太网接口被编程为支持多播尤其重要。几种以太网协议(特别是 Appletalk 和 IP 多播)依赖于以太网多播。幸运的是,大部分工作由内核为您完成(请参阅 net/core/dev_mcast.c)。

内核支持代码维护您的接口应允许用于多播的物理地址列表。如果设备无法进行完美过滤,则设备驱动程序可能会返回与请求的多播列表之外的多播匹配的帧。

每当多播地址列表更改时,都会调用设备驱动程序的 dev->set_multicast_list() 函数。然后,驱动程序可以重新加载其物理表。通常看起来像这样

if(dev->flags&IFF_PROMISC)
    SetToHearAllPackets();
else if(dev->flags&IFF_ALLMULTI)
    SetToHearAllMulticasts();
else
{
    if(dev->mc_count<16)
    {
        LoadAddressList(dev->mc_list);
        SetToHearList();
    }
    else
        SetToHearAllMulticasts();
}

少数网卡只能进行单播或混杂模式。在这种情况下,驱动程序在收到多播请求时必须进入混杂模式。如果这样做,驱动程序必须自己在 dev->flags 中设置 IFF_PROMISC 标志。

为了帮助驱动程序编写者,多播列表始终保持有效。这简化了许多驱动程序,因为驱动程序中错误情况的重置通常必须重新加载多播地址列表。

以太网支持例程

以太网可能是可以处理的最常见的物理接口类型。内核提供了一组通用的以太网支持例程,此类驱动程序可以使用这些例程。

eth_header()dev-hard_header 例程的标准以太网处理程序,可用于任何以太网驱动程序。与用于重建例程的 eth_rebuild_header() 结合使用,它提供了将以太网标头放在 IP 数据包上的所有 ARP 查找。

eth_type_trans() 例程期望接收原始以太网数据包。它分析标头并设置 skb->pkt_typeskb->mac 本身,并返回 skb->protocol 的建议值。此例程通常从以太网驱动程序接收中断处理程序中调用,以对数据包进行分类。

eth_copy_and_sum(),最终的以太网支持例程在内部非常复杂,但为内存映射卡提供了显著的性能改进。它提供了将数据从网卡复制并校验和到 sk_buff 中的单次传递支持。当使用时,这种单次内存传递几乎消除了校验和计算的成本,并提高了 IP 吞吐量。

Alan Cox 自 0.95 版本以来一直在从事 Linux 工作,当时他安装 Linux 是为了进一步研究 AberMUD 游戏。他现在管理 Linux 网络、SMP 和 Linux/8086 项目,自 1993 年 11 月以来就再也没有从事过 AberMUD 的工作。

加载 Disqus 评论