网络设备、内核与您!

简介

以下是关于网络设备的文档的随机集合。它面向驱动程序开发人员。

struct net_device 生命周期规则

即使在模块卸载后,网络设备结构也需要持久存在,并且必须使用 alloc_netdev_mqs() 及其朋友分配。如果设备已成功注册,则将在 free_netdev() 的最后一次使用时释放它。这是干净地处理病态情况所必需的(例如:rmmod mydriver </sys/class/net/myeth/mtu

alloc_netdev_mqs() / alloc_netdev() 为驱动程序私有数据保留额外的空间,这些空间在网络设备释放时会被释放。如果单独分配的数据附加到网络设备 (netdev_priv()),则由模块退出处理程序来释放它。

有两组 API 用于注册 struct net_device。第一组可以在未持有 rtnl_lock 的正常上下文中使用:register_netdev(), unregister_netdev()。第二组可以在已经持有 rtnl_lock 时使用:register_netdevice(), unregister_netdevice(), free_netdevice().

简单驱动程序

大多数驱动程序(尤其是设备驱动程序)在未持有 rtnl_lock 的上下文中处理 struct net_device 的生命周期(例如,驱动程序探测和删除路径)。

在这种情况下,struct net_device 注册是使用 register_netdev()unregister_netdev() 函数完成的。

int probe()
{
  struct my_device_priv *priv;
  int err;

  dev = alloc_netdev_mqs(...);
  if (!dev)
    return -ENOMEM;
  priv = netdev_priv(dev);

  /* ... do all device setup before calling register_netdev() ...
   */

  err = register_netdev(dev);
  if (err)
    goto err_undo;

  /* net_device is visible to the user! */

err_undo:
  /* ... undo the device setup ... */
  free_netdev(dev);
  return err;
}

void remove()
{
  unregister_netdev(dev);
  free_netdev(dev);
}

请注意,在调用 register_netdev() 之后,该设备在系统中可见。用户可以打开它并立即开始发送/接收流量,或者运行任何其他回调,因此所有初始化必须在注册之前完成。

unregister_netdev() 关闭设备并等待所有用户完成操作。 struct net_device 本身的内存可能仍然被 sysfs 引用,但对该设备的所有操作都将失败。

free_netdev() 可以在 unregister_netdev() 返回时或者 register_netdev() 失败时调用。

RTNL 下的设备管理

在已经持有 rtnl_lock 的上下文中注册 struct net_device 需要格外小心。在这些情况下,大多数驱动程序都希望使用 struct net_deviceneeds_free_netdevpriv_destructor 成员来释放状态。

rtnl_lock 下处理 netdev 的示例流程

static void my_setup(struct net_device *dev)
{
  dev->needs_free_netdev = true;
}

static void my_destructor(struct net_device *dev)
{
  some_obj_destroy(priv->obj);
  some_uninit(priv);
}

int create_link()
{
  struct my_device_priv *priv;
  int err;

  ASSERT_RTNL();

  dev = alloc_netdev(sizeof(*priv), "net%d", NET_NAME_UNKNOWN, my_setup);
  if (!dev)
    return -ENOMEM;
  priv = netdev_priv(dev);

  /* Implicit constructor */
  err = some_init(priv);
  if (err)
    goto err_free_dev;

  priv->obj = some_obj_create();
  if (!priv->obj) {
    err = -ENOMEM;
    goto err_some_uninit;
  }
  /* End of constructor, set the destructor: */
  dev->priv_destructor = my_destructor;

  err = register_netdevice(dev);
  if (err)
    /* register_netdevice() calls destructor on failure */
    goto err_free_dev;

  /* If anything fails now unregister_netdevice() (or unregister_netdev())
   * will take care of calling my_destructor and free_netdev().
   */

  return 0;

err_some_uninit:
  some_uninit(priv);
err_free_dev:
  free_netdev(dev);
  return err;
}

如果设置了 struct net_device.priv_destructor,它将在 unregister_netdevice() 之后由内核在某个时间调用,如果 register_netdevice() 失败,它也会被调用。可以持有或不持有 rtnl_lock 调用回调。

没有显式的构造函数回调,驱动程序在分配私有 netdev 状态后和注册之前“构造”它。

设置 struct net_device.needs_free_netdev 使内核在 unregister_netdevice() 之后,当对设备的所有引用都消失时自动调用 free_netdevice()。它仅在成功调用 register_netdevice() 后生效,因此如果 register_netdevice() 失败,驱动程序负责调用 free_netdev()

free_netdev() 在 unregister_netdevice() 之后或 register_netdevice() 失败时,可以在错误路径上安全地调用。 netdev (取消)注册过程的部分发生在释放 rtnl_lock 后,因此在这些情况下,free_netdev() 将推迟部分处理,直到释放 rtnl_lock

从 struct rtnl_link_ops 产生的设备永远不应直接释放 struct net_device

.ndo_init 和 .ndo_uninit

.ndo_init.ndo_uninit 回调在 net_device 注册和取消注册期间,在 rtnl_lock 下调用。驱动程序可以使用它们,例如,当它们的初始化过程的部分需要在 rtnl_lock 下运行时。

.ndo_init 在设备在系统中可见之前运行,.ndo_uninit 在设备关闭后取消注册期间运行,但其他子系统可能仍然具有对 netdevice 的未完成引用。

MTU

每个网络设备都有一个最大传输单元。 MTU 不包括任何链路层协议开销。上层协议不得将套接字缓冲区 (skb) 传递给设备以传输超过 mtu 的数据。 MTU 不包括链路层标头开销,因此例如在以太网上,如果使用标准 MTU 1500 字节,则实际 skb 将包含最多 1514 字节,因为以太网标头。设备应允许 4 字节 VLAN 标头。

分段卸载 (GSO, TSO) 是此规则的例外。上层协议可能会将一个大的套接字缓冲区传递给设备传输例程,并且设备将根据当前的 MTU 将其分解为单独的数据包。

MTU 是对称的,适用于接收和传输。设备必须能够接收至少 MTU 允许的最大尺寸数据包。网络设备可以使用 MTU 作为调整接收缓冲区大小的机制,但设备应允许带有 VLAN 标头的数据包。使用 1500 字节的标准以太网 mtu,设备应允许最多 1518 字节的数据包(1500 + 14 标头 + 4 标签)。设备可以:丢弃、截断或传递超大尺寸的数据包,但最好丢弃超大尺寸的数据包。

struct net_device 同步规则

ndo_open

同步:rtnl_lock() 信号量。此外,如果驱动程序实现了队列管理或整形器 API,则会使用 netdev 实例锁。上下文:进程

ndo_stop

同步:rtnl_lock() 信号量。此外,如果驱动程序实现了队列管理或整形器 API,则会使用 netdev 实例锁。上下文:进程 注意:保证 netif_running() 为 false

ndo_do_ioctl

同步:rtnl_lock() 信号量。

这仅由网络子系统在内部调用,而不是由用户空间调用 ioctl 调用,就像在 linux-5.14 之前一样。

ndo_siocbond

同步:rtnl_lock() 信号量。此外,如果驱动程序实现了队列管理或整形器 API,则会使用 netdev 实例锁。上下文:进程

由绑定驱动程序用于 SIOCBOND 系列 ioctl 命令。

ndo_siocwandev

同步:rtnl_lock() 信号量。此外,如果驱动程序实现了队列管理或整形器 API,则会使用 netdev 实例锁。上下文:进程

由 drivers/net/wan 框架用于处理带有 if_settings 结构的 SIOCWANDEV ioctl。

ndo_siocdevprivate

同步:rtnl_lock() 信号量。此外,如果驱动程序实现了队列管理或整形器 API,则会使用 netdev 实例锁。上下文:进程

这用于实现 SIOCDEVPRIVATE ioctl 助手。不应将这些添加到新驱动程序中,因此请勿使用。

ndo_eth_ioctl

同步:rtnl_lock() 信号量。此外,如果驱动程序实现了队列管理或整形器 API,则会使用 netdev 实例锁。上下文:进程

ndo_get_stats

同步:RCU(可以与统计信息更新路径并发调用)。上下文:原子(不能在 RCU 下休眠)

ndo_start_xmit

同步:__netif_tx_lock 自旋锁。

当驱动程序设置 dev->lltx 时,这将在不持有 netif_tx_lock 的情况下调用。在这种情况下,驱动程序必须在需要时自行锁定。那里的锁定也应该正确地防止 set_rx_mode。警告:不建议使用 dev->lltx。不要将其用于新驱动程序。

上下文:禁用 BH 的进程或 BH(计时器),

将通过 netconsole 禁用中断来调用。

返回代码

  • NETDEV_TX_OK 一切正常。

  • NETDEV_TX_BUSY 无法传输数据包,稍后重试 通常是一个错误,意味着驱动程序中的队列启动/停止流量控制已损坏。注意:驱动程序不得将 skb 放入其 DMA 环中。

ndo_tx_timeout

同步:netif_tx_lock 自旋锁;所有 TX 队列已冻结。上下文:禁用 BH 注意:保证 netif_queue_stopped() 为 true

ndo_set_rx_mode

同步:netif_addr_lock 自旋锁。上下文:禁用 BH

ndo_setup_tc

TC_SETUP_BLOCKTC_SETUP_FT 在 NFT 锁下运行(即没有 rtnl_lock 也没有设备实例锁)。如果驱动程序实现了队列管理或整形器 API,则其余的 tc_setup_type 类型会在 netdev 实例锁下运行。

上面列表中未指定的大多数 ndo 回调都在 rtnl_lock 下运行。此外,如果驱动程序实现了队列管理或整形器 API,则也会使用 netdev 实例锁。

struct napi_struct 同步规则

napi->poll
同步

napi->state 中的 NAPI_STATE_SCHED 位。设备驱动程序的 ndo_stop 方法将在所有 NAPI 实例上调用 napi_disable(),这将对 NAPI_STATE_SCHED napi->state 位执行睡眠轮询,等待所有挂起的 NAPI 活动停止。

上下文

softirq 将通过 netconsole 禁用中断来调用。

netdev 实例锁

从历史上看,所有网络控制操作都由一个称为 rtnl_lock 的单一全局锁保护。目前正在努力用每个网络命名空间的单独锁来替换此全局锁。此外,单个 netdev 的属性越来越多地受到每个 netdev 锁的保护。

对于实现整形或队列管理 API 的设备驱动程序,所有控制操作都将在 netdev 实例锁下执行。驱动程序还可以通过将 request_ops_lock 设置为 true 来显式请求在操作期间持有实例锁。代码注释和文档将操作在实例锁下调用的驱动程序称为“ops locked”。另请参阅 struct net_devicelock 成员的文档。

将来,可以选择让单个驱动程序选择不使用 rtnl_lock,而是直接在 netdev 实例锁下执行其控制操作。

鼓励设备驱动程序尽可能依赖实例锁。

对于需要与核心堆栈交互的(主要是软件)驱动程序,有两组接口:dev_xxx/netdev_xxxnetif_xxx (例如,dev_set_mtunetif_set_mtu)。 dev_xxx/netdev_xxx 函数处理获取实例锁本身,而 netif_xxx 函数假定驱动程序已经获取了实例锁。

struct net_device_ops

对于大多数驱动程序,调用 ndos 时不持有实例锁。

“Ops locked” 驱动程序的大多数 ndos 将在实例锁下调用。

struct ethtool_ops

ndos 类似,实例锁仅对选定的驱动程序持有。对于“ops locked” 驱动程序,所有 ethtool 操作都应在实例锁下调用,没有例外。

struct netdev_stat_ops

对于“ops locked”驱动程序,在实例锁下调用“qstat”操作,对于所有其他驱动程序,在 rtnl_lock 下调用。

struct net_shaper_ops

在持有 netdev 实例锁的同时调用所有 net shaper 回调。可以持有或不持有 rtnl_lock

请注意,支持 net shapers 会自动启用“ops locking”。

struct netdev_queue_mgmt_ops

在持有 netdev 实例锁的同时调用所有队列管理回调。可以持有或不持有 rtnl_lock

请注意,支持 struct netdev_queue_mgmt_ops 会自动启用“ops locking”。

通知程序和 netdev 实例锁

对于实现整形或队列管理 API 的设备驱动程序,某些通知程序 (enum netdev_cmd) 在 netdev 实例锁下运行。

以下 netdev 通知程序始终在实例锁下运行:* NETDEV_XDP_FEAT_CHANGE

对于具有锁定操作的设备,目前只有以下通知程序在锁下运行:* NETDEV_CHANGE * NETDEV_REGISTER * NETDEV_UP

以下通知程序在没有锁的情况下运行:* NETDEV_UNREGISTER

对其余通知程序没有明确的期望。不在列表上的通知程序可以在有或没有实例锁的情况下运行,甚至可能使用和不使用锁从不同的代码路径调用相同的通知程序类型。目标是最终确保所有(或大多数,带有一些记录在案的例外)通知程序都在实例锁下运行。每当您对从通知程序持有的锁进行显式假设时,请扩展此文档。

NETDEV_INTERNAL 符号命名空间

作为 NETDEV_INTERNAL 导出的符号只能在网络核心和仅通过主网络列表和树流动的驱动程序中使用。请注意,反之则不然,NETDEV_INTERNAL 之外的大多数符号也不希望被 netdev 之外的随机代码使用。符号可能缺少指定,因为它们早于命名空间,或者仅仅是由于疏忽。