深入理解Linux网络技术内幕(八)——设备注册和初始化


前言

经过前面所学,我们已经知道内核如何验证NIC,以及内核所做的初始化,使得NIC得以和其他设备驱动程序对话。下面我们将讨论初始化的其他步骤:

  • 网络设备何时以及如何在内核注册

  • 网络设备如何利用网络设备数据库注册,并指派一个net_device结构的实例。

  • net_device结构如何组织到hash表和列表,以便于做各种查询。

  • net_device实例如何初始化,一部分由内核核心函数完成,一部分由其设备驱动程序完成。

  • 就注册而言,虚拟设备和真实设备有何差别。

NIC可用之前,其相关联的net_device数据结构必须先初始化,添加至内核网络设备数据库、配置并开启。不要把注册/除名以及开启/开关混淆是十分重要的,这是两种不同的概念:

  • 如果把加载设备驱动程序模块的动作排除的话,注册和除名是独立于用户之外的,是由内核驱动的。仅仅注册了的设备还不能运转。会在后面说明注册和除名的时间。

  • 开启和关闭设备都需要用户参与。一旦设备已由内核注册,用户就可通过用户命令看到设备,配置并予以开启。

设备注册之时

网络设备的注册发生下列几种情况之下:

加载NIC设备驱动程序

如果NIC设备驱动内建至内核中,则在引导期间初始化;否则,如果以模块加载,就会在运行期间初始化。每当发生初始化时,该驱动程序所控制的NIC都会别注册。

插入可热插拔网络设备

当用户把可热插拔NIC设备插入进来时,内核会通知其驱动程序,而驱动程序再注册该设备。

第一种情况下这种模型适用于所有总线类型,无论注册函数最后被总线基础架构还是被模块初始化程序调用,都是一样的。比如,加载PCI设备驱动程序如何导致pci_driver_probe函数的执行。此函数是由驱动程序提供,并由其负责设备的注册。

设备除名之时

有两种主要的情况会触发设备的除名:

卸载NIC设备驱动程序

当然,这仅针对那些以模块加载的驱动程序,不适于那些内建至内核的驱动程序。当管理员卸载NIC设备驱动程序时,所有相关联的NIC都必须除名。

例如,卸载PCI设备驱动程序时会导致pci_driver_remove函数的执行,此函数负责设备的除名。此函数会由PCI层启用,针对正在卸载的驱动程序,每个已注册设备都会执行一次。

删除可热插拔网络设备

当用户从系统(其运行的内核支持可热插拔设备)删除可热插拔NIC时,则网络设备就会被除名。

分配net_device结构

网络设备利用net_device结构定义。因为在内核代码中通常将其称为dev,所以我们下面也用dev来表示dev_device。这些数据结构的定义在net/core/dev.c中的alloc_netdev分配,需要三个输入参数:

  • 私有数据结构的大小

    • net_device数据结构可由设备驱动程序扩充,阿基上一个私有数据区块,以存储驱动程序的参数。此参数指定该区块的大小。
  • 设备名称

    • 这可能是一部分名称,再由内核通过某种规则,将其完成,以确保独一无二的设备名称。
  • 设置函数

    • 这个函数用于初始化net_device的部分字段。后面的设备初始化一节会详细分析

返回值是指向已分配的net_device结构的指针,但如果发生错误,就是NULL

每个设备都会根据其设备类型被分配一个名称,而且为了唯一性还会包含一个数字,该数字的分派是按已注册的同类型设备数量而连续指定。例如,Ethernet设备就依序称为eth0eth1等等。根据设备注册的次序,一台设备的名称可能会有所不同。例如,如果你有两块不同模块的处理卡,则设备名称会取决于这两个模块加载的次序。可热插拔设备名称的变化更是深不可测。

因为用户空间配置工具会引用内核所分派的设备名称,因此设备注册的次序就很重要。因为这是用户空间的细节,我们不会深入,只是简单提到。如net-tools套件的nameif,它允许你分派固定名称给基于MAC地址的接口。

当传给alloc_netdev的设备名称为name%d的形式时(例如,"eth%d"),内核就会使用函数dev_alloc_name以完成该名称。此函数会把%d换成该设备类型中头一个尚未反派的数字。

内核也提供一组内含alloc_netdev的包裹含函数,如下表,可用于为一组通用设备类型提供正确参数给alloc_netdev。例如,alloc_etherdev用于Ethernet设备,因此,建立的设备名称形式就是字符串eth再接上一个唯一的数字;第二个变量将ether_setup指定为设置函数,它可以把net_device结构的一部分初始化为所有Ethernet设备通用的值。

在这里插入图片描述

NIC注册和除名的架构

在这里插入图片描述

如上图所示,a为NIC设备驱动程序利用网络代码注册的通用方案。b为相反的操作,负责除名。虽然这里以PCI Ethernet NIC为例,但这样的方案对其他设备类型而言也是一样的,只是所用的函数名称不同,或者函数启用方式有别,可能依赖于总线代码如何实现而定。

此函数首先使用alloc_etherdev分配net_device结构。alloc_etherdev也会为那些所有Ethernet设备都通用的参数做初始化。接着,驱动程序初始化net_device结构的另一部分,然后调用register_netdev函数为设备注册。

注意以下几点:

  • 驱动程序会调用适当的内含alloc_netdev的包裹函数,而且值提供其私有数据区块的大小,此例中为alloc_etherdev

  • 包裹函数会使用驱动程序所提供的参数以及另外两个参数(设备名称和初始化函数)来调用alloc_netdev

  • alloc_netdev所分配的内存区块大小包括net_device结构、驱动程序的私有区块以及为了强制对齐所补的一些空白空间。

  • 有些驱动程序会调用netdev_bot_setup_check,以检查加载内核时用户是否提供任何引导期间参数。

  • 新的net_device实例会利用register_netdevice插入至设备数据库。此外,这里我们用的术语是“数据库“,但是在其他部分,则指数据结构的松散组合,在内核需要时提供便利存取途径,供内核访问信息。

设备的除名如图中b所示,除名时总是会调用unregister_netdevicefree_netdev。有时会显式地调用free_netdev,但有时则是间接通过dev->destructor函数。设备驱动程序也必须释放设备所使用的的任何资源(IRQ、内存映像等等)。

设备初始化

通过前面所学,我们已经知道了应该初始化什么东西,内核才能与NIC通信。下面我们将着眼于跟高层的初始化任务。

net_device结构相当大,其字段会与一批一批由不同函数予以初始化,而每个函数都负责一组不同的字段子集。特别是:

设备驱动程序

如IRQ,I/O内存以及I/O端口,其值取决于硬件配置,这些参数是由设备驱动程序负责的。

设备类型

对同一设备类型系列的所有设备所通用字段的初始化,有xxx_setup函数负责。例如,Ethernet设备所使用的是ether_setup

各种功能

强制和可选的功能也必须初始化。。例如,队列规则(也就是QoS),是在register_netdevice中初始化的,如register_netdevice函数一节所述。其他功能的初始化则是在相关联的模块接到通知,说明有新设备注册之时。

设备类型初始化属于设备驱动程序初始化的一部分(即xxx_setup是由xxx_probe调用)。设备类型特定的事以及设备模型特定的事。注意,并非所有设备驱动程序都遵循下表所做的区分。例如,有些情况下xxx_setup函数并不初始化任何函数指针(其中一例就是net/irda/irda_device.c中的irda_device_setup),而其他情况下则会全部予以初始化(其中一例就是drivers/net/wireless/airo.c中的wifi_setup)。

在这里插入图片描述

在这里插入图片描述

设备驱动程序初始化

由设备驱动程序所初始化的net_device字段通常是由xxx_probe函数负责。

有些驱动程序可以处理不同的设备模型,所以相同参数可根据设备模型和能力而初始化为不同的值。下列片段取自drivers/net/3c59x.c驱动程序,显示出函数hard_start_xmit的初始化会根据设备的能力而有不同的值:

if(vp->capabilities & CapBusMaster){
    vp->full_bus_master_tx = 1;
    ... ... ...
}
... ... ...
if(vp->full_bus_master_tx){
    dev->hard_start_xmit = boomerang_start_xmit;
    ... ... ...
}else{
    dev->hard_start_xmit = vortext_start_xmit;
}

设备类型初始化:xxx_setup函数

就最常见网络设备类型而言,xxx_setup函数可以对net_device结构中相同类型的所有设备通用的字段做初始化(包括参数和函数指针),例如所有Ethernet卡。

各种alloc_xxxdev函数如何把正确的xxx_setup函数传给alloc_netdev(作为第三个输入参数)。以下是ether_setup函数,也就是Ethenet设备所用的xxx_setup函数:

void ether_setup(struct net_device *dev)
{
    dev->change_mtu            = eth_change_mtu;
    dev->hard_header           = eth_header;
    dev->rebuild_header        = eth_rebuild_header;
    dev->set_mac_address       = eth_mac_addr;
    dev->hard_header_cache     = eth_header_cache;
    dev->header_cache_update   = eth_header_cache_update;
    dev->hard_header_parse     = eth_header_parse;

    dev->type                  = ARPHRD_ETHER;
    dev->hard_header_len       = ETH_HLEN;
    dev->mtu                   =1500;
    dev->addr_len              = ETH_ALEN;
    dev->tx_queue_len          = 1000;
    dev->flags                 = IFF_BROADCAST | IFF_MULTICAST;

    memset(dev->broadcast, 0xFF, ETH_ALEN)}

此函数只针对那些可由任何Ethernet卡共享的字段和函数指针做初始化,如1,500MTU、链接层广播地址FF:FF:FF:FF:FF:FF、长度为1000个封包的出口队列等等。

使用通用的分配包裹函数以及xxx_setup函数是最常见的方法。然而:

  • 有些类型的设备定义了Setup函数,但是没有提供类似前面介绍的通用包裹函数。例如,ARCENT设备和IrDA设备就是如此。

  • 通用xxx_setup可能也会被不属于指定种类的设备所用。ether_setup就是一例:非Ethernet设备也会使用它。当特定xxx_setup函数的多数初始化工作满足某个设备驱动程序所需时,则该驱动程序也许会使用那个xxx_setup函数,然后改写一些不使用它的初始化工作。但这种做法不常见。

  • Ethernet驱动程序可以使用由ether_setup(由alloc_etherdev间接调用)提供的默认初始化,但是要改写掉一些初始化。例如,3c59x.c驱动程序不使用ether_setup所设置的net->device->mtu值,而是以另一个局部变量予以改写。此变量的初始值也是由ether_setup所设的相同默认值,但是,遇到可以处理更大值的NIC模型时,驱动程序就可以设成更大的值。

可选的初始化和特殊情况

有些情况下,因为对该种类的设备而言没有意义,有些net_device参数就不会初始化;先关的函数指针或值就没初始化,因此就是NULL。

要避免NULL指针的引用,内核总是要在启动前先确定选用的函数指针被初始化,如下面取自register_netdevice的范例所示:

if(dev->init && dev->init(dev)!=0){
    ...
}

值得注意的是,外在因素也会影响到上表的字段在何处初始化以及如何初始化。其中一例与net_device->mtu字段有关。虚拟设备通常会从其关联的真实设备哪里继承配置参数,然后在需要时予以调整。例如,IP-over-IP协议所建的虚拟隧道接口,会从其相关联的真实设备哪里继承dev->mtu(这不是自动化过程,虚拟设备驱动程序要负责)。然而,由于IP-over-IP协议所需的额外IP报头的缘故,MTU也必须据此调低。(参见net/ipv4/ipip.c中的ipip_tuunel_xmit,假设底层为Ethernet设备)。

net_device结构的组织

net_device结构有些值得注意的方面,如下所述:

  • 调用alloc_netdev以分配net_device结构时,会把驱动程序的私有数据区块的大小传进去(其大小依赖驱动程序而定,有些甚至不使用私有数据)。alloc_netdev会把私有数据附加到net_device结构。

  • 下图显示出net_device数据结构和选用的驱动程序私有数据结构之间的关系。通常,第二部分的分配和第一部分一起完成,使得调用kmalloc一次就已足够,但是,有些情况下驱动程序可能宁可自行分配其私有区块

  • 驱动程序私有区块的大小及其内容不仅会随设备类型而变,而且对同一类型的不同设备而言也是如此。

  • dev_basenet_device中的next指针指向net_device结构的开头,而非已分配区块的开头。然而,开头补空白空间的大小则存储在dev->padded,使得内核得以在时机到来时释放整个内存区块。

在这里插入图片描述

net_device数据结构插入在一个全局列表和两张hash表中如下图。这些不同的结构可让内核按需求浏览或查询net_device数据库。以下是其细节:

在这里插入图片描述

  • dev_base

    • 内含所有net_device实例的全局列表能够让内核轻易浏览设备。例如,取得某些统计数据,因为用户命令的结果而必须改变所有设备的某项配置,或者找出吻合特定搜寻准则的设备。因为每个驱动程序对私有数据结构都由其自己的定义。net_device结构全局列表所链接的元素可能会有不同的大小。
  • dev_name_head

    • 这是一张hash表,以设备名称为索引。例如,要通过ioctl接口应用某些配置变更时,就有其用处。老一代配置工具通过ioctl接口与内核对话,通常会以设备名称引用此设备。
  • dev_inidex_head

    • 这是一张hash表,以设备IDdev->ifindex为索引。对net_device结构做交叉引用时,通常会存储设备ID或指向net_device结构的指针。dev_index_head存储设备ID供交叉引用。此外,新一代配置工具ip(来自IPROTE2套件)会通过Netlink套接字与内核对话,通常就是以设备ID引用设备。

查询

最常见的查询都是通过设备名称或设备ID进行。这两种查询的实现是由dev_get_by_namedev_get_by_index负责,其所用的就是前一节所讨论的两张hash表。也有可能根据设备类型和MAC地址搜寻net_device实例。这种查询用的是dev_base列表。

所有查询,包括dev_base列表和那两张hash表,都会由dev_base_lock锁保护。所有查询函数都定义在net/core/dev.c中。

设备状态

net_device结构有各种字段可定义设备当前状态,如下所示:

  • flags

    • 用于存储各种标识的位数。多数标识都代表设备的能力。然而,其中之一的IFF_UP是用于指出该设备是否开启或关闭。可以在include/linux/if.h中找到IFF_XXX标识列表。
  • reg_state

    • 设备注册状态。“注册状态”一节列出可以分派给此字段的一些值,以及何时该值可以变更。
  • state

    • 和其队列规则有关的设备状态

你会发现这些变量有时会重叠。例如。每次IFF_UPflags中被设置时,__LINK_STATE_START也会处于被设置的状态,反之亦然。这两个标识都是由dev_opendev_close予以设置和清楚的,然而其作用域则不同。此外,编写模块化程序时,有时会发生一些重叠。

队列规则状态

每个网络设备都会被分派一种队列规则,流量控制以实现其QoS机制。net_devicestate字段是流量控制所用的结构字段之一。state是位域,而下列是可以设置的标识,定义在include/linux/netdevice.h中。

  • __LINK_STATE_START

    • 设备开启。此标识可以由netif_running检查。
  • __LINK_STATE_PRESENT

    • 设备存在。此标识看来可能是多余的,但是,要想到可热插拔设备可以暂时删除。当系统进入挂起模式然后再重新继续时,此标识会被清楚然后再取回其值。茨表示可以由netif_device_present检查
  • __LINK_STATE_NOCARRIER

    • 没有载波(carrier)。此标识可以由netif_carrier_ok检查。
  • __LINK_STATE_LINKWATCH_EVENT

    • 设备的链接状态已变更。
  • __LINK_STATE_XOFF

  • __LINK_STATE_SHED

  • __LINK_STATE_RX_SCHED

    • 这三个标识由负责管理设备的入口和出口流量的代码所使用。

注册状态

设备和网络协议栈之间的注册状态存储在net_device结构中的reg_state字段内。此字段所取的NETREG_XXX值都定义在include/linux/netdevice.h中(放在net_device结构定义中)。简述如下:

  • NETREG_UNINITALIZED

    • 定义成0.当net_device数据结构已分配而且其内容都清0时,此值代表的就是dev->reg_state中的0.
  • NETREG_REGISTERING

    • net_device结构已添加到前面提及的那些数据结构内,但是,内核依然要在/sys文件系统中添加一个项目。
  • NETREG_REGISTERED

    • 设备已完成注册
  • NETREG_UNREGISTERING

    • net_device结构已从提及到的数据结构中删除掉了。
  • NETREG_UNREGISTERED

    • 设备已完全除名(包括/sys中的项目),但是net_device结构还没释放掉。
  • NETREG_RELEASED

    • 所有对net_device结构的引用都已释放。从网络代码的观点来看,此数据结构已释放。然而,由/sys来决定是够这样做。

设备的注册和除名

网络设备通过register_netdevunregister_ntetdev在内核注册和除名。这两个只是简单的包裹函数,负责上锁,然后分别启用register_netdeviceunregister_netdevice函数。他们定义在net/core/dev.c中。
在这里插入图片描述

上图显示了net_device可以设成的状态,以及前面提及过的函数在此图中的那些位置,同时也显示了其他关键函数在何处受调用。这些函数都会在后面加以说明。但要特别注意以下几点:

  • 状态的改变会用到界于NETREG_UNINITALIZEDNETREG_REGISTERED之间的状态。这些进程是由netdev_run_todo处理,下面一节会加以说明。

  • 为设备注册和除名时,设备驱动程序可以使用两个net_device虚拟函数inituninit,分别对私有数据做初始化以及清理工作。

  • 对设备除名时,除非对相关联的net_device数据结构的引用都已全部释放,否则无法完成:netdev_wait_allrefs不会返回,直到条件满足。

  • 设备的注册和除名都是由netdev_run_todo完成的。下面一节我们就会知道,register_netdeviceunregister_netdevice如何与netdev_run_todo交互。

切割操作:netdev_run_todo

register_netdevice会负责一部分的注册,然后在让netdev_run_todo予以完成。一开始,只看程序代码可能看不清楚发生了什么。通过上图来进行说明:

net_device结构做修改时,会受到通过rtnl_locketnl_unlock操作的Routing Netlink信号量的保护;这也就是以为什么register_netdev在开始之时会取得该锁(信号量),然后在返回前予以释放。一旦register_netdevice完成了份内工作,就会以net_set_toto把新net_device结构添加到net_todo_list。这份列表所含的设备。其注册必须完成才行。这份列表不会由另一个内核线程处理,或者通过定时器处理,而是有register_netdev在释放锁时以间接方式予以处理。

因此rtnl_unlock不仅释放该锁,也会调用netdev_run_todonetdev_run_todo会浏览net_todo_list数组,然后完成其全部的net_device实例注册。

任何时刻,只有一个CPU可以执行net_run_todo。串行化是通过net_todo_run_mutex互斥强制实施的。

设备的除名处理方式也相同,参加下图
在这里插入图片描述

netdev_run_todo完成设备的注册或除名所做之事后面会予以说明。

注意,由于netdev_run_todo所处理的注册和除名任务不用持有该锁,此函数可以安全地进入休眠状态,使信号量可用。后面会介绍这样做的好处。

根据上图的模型可以看出,每次调用netdev_run_todo时,内核都不能在net_todo_list中放置一个以上的net_device实例。如果register_netdevunregister_netdev只添加一个netdev_device实例到该列表中,然后释放该锁时就立刻予以处理,又如何会有一个以上的元素呢?举例来说,设备驱动程序有可能使用下列的循环,一次把其所有设备都除名(参见drivers/net/tun.c中的tun_cleanup):

rtnl_lock();
循环(此驱动程序所驱动的每个设备){
    ... ... ...
    unregister_netdevice(dev);
    .... ... ...
}
rtnl_unlock();

这种做法比下列做法要好,即在循环的每一轮中取得和释放该锁,然后处理net_todo_list

循环(此驱动程序所驱动的每个设备)
{
    ... ... ...
    unregister_netdev(dev);
    ... ... ...
}

设备注册状态通知

内核组件和用户空间应用程序可能都想知道何时发生网络设备注册、除名、关闭或者开启之事。这类事件的通知是通过两种通道传送的“

  • netdev_chain

    • 内核组件可以注册此通知链
  • Netlink的RTMGRP_LINK多播群组

    • 用户空间应用程序可以注册RtnetlinkRTMGRP_LINK多播群组。

netdev_chain通知链

前面已经谈论过通知链的定义及其用法。设备的注册和除名在各个阶段的进展都是通过netdev_chain通知链告知的。此链定义在net/core/dev.c中,而对此类事件感兴趣的内核组件可以通过register_netdevice_notifierunregister_netdevice_notifier分别对该链注册或除名。

所有由netdev_chain报告的NETDEV_XXX事件都列在include/linux/notifier.h中。下面是我们看到过的几种事件以及触发这些事件的条件:

NETDEV_UP

NETDEV_GOING_DOWN

NETDEV_DOWN

送出NETDEV_UP以报告设备已开启,而且此事件由dev_open产生。

当设备要关闭时,就会送出NETDEV_GOING_DOWN。当设备已关闭时,就会送出NETDEV_DOWN。这些事件都是由dev_close产生的。

NETDEV_REGISTER

设备已注册。此事件由register_netdevice产生。

NETDEV_UNREGISTER

设备已除名。此事件有unregister_netdevice产生。

此外还有一些事件:

NETDEV_REBOOT

因为硬件失败,设备已重启,目前没用。

NETDEV_CHANGEADDR

设备硬件地址(或相关联的广播地址)已改变。

NETDEV_CHANGENAME

设备已改变其名称。

NETDEV_CHANGE

设备的状态或配置已经改变。此事件会用在NETDEV_CHANGEADDRNETDEV_CHANGENAME没包括在内的所有情况下。目前,当dev->flags中有些改变时,就会用到此事件。

产生NETDEV_CHANGEXXX通知信息通常是为了响应用户配置变更。

注意,向链注册时,register_netdevice_notifier也会(仅针对新注册者)重放当前系统已注册设备所有过去的NETDEV_REGISTERNETDEV_UP通知信息。这样就能给新注册者有关已注册设备当前状态的清晰图像。

RTnetlink链接通知

当设备的状态或配置中有变更时,就会用rtmsg_ifinfo把通知信息传送给Link多播群组RTMGRP_LINK。其中一些通知信息如下:

  • netdev_chain通知链接收到一个通知信息时,RTnetlink会注册其前面提及的netdev_chain结构,然后重放其接收到的通知信息。

  • 当一个已关闭的设备开启时或者处于相反过程

  • net_device->flag中的一个标识有变动时,例如,通过用户配置命令修改。

netplugd是守护进程,属于`net-utils套件,会监听这些通知信息,然后根据用户配置文件而反应。

设备注册

设备注册不是简单地把net_device结构插入到全局列表和hash表中就行了。设备注册还涉及net_device结构中一些参数的初始化、产生广播通知信息以通知其他内核组件有关此次注册,以及其他任务。设备以register_netdev注册,而register_netdev只是简单的包裹函数,其内含有register_netdevice。此包裹函数注册时负责上锁以及完成名称。该锁会保护dev_base已注册的设备列表。

register_netdevice函数

register_netdevice会着手进行设备的注册,然后调用net_set_todo,而net_set_todo最终会要求netdev_run_todo完成注册。

以下是register_netdevice所进行的主要任务:

  • net_device的某些字段做初始化,包括用于上锁的那些字段

  • 当内核支持Divert功能时,就分配一个该功能所需的配置区块,然后链接至dev->divert。这是由alloc_divert_blk负责完成的。

  • 如果设备驱动程序已对dev->init做了初始化,那就执行该函数。

  • dev_new_index分派一个独一无二的识别码给设备。此识别码产生是通过一个计数器,每次一个新的设备新增至系统时就递增一次。此计数器是32位变量,所以dev_new_index包含一个if子句,用以处理环绕问题,此外还有另一个if子句,用以处理该变量遇到一个已经分派过的值的可能情况。

  • net_device附加到全局列表dev_base,然后插入到两张hash表。虽然把该结构添加到dev_base头部会比较快,但是,内核还是有机会浏览整个列表以检查有无重复的设备名称。设备名称也会通过dev_vaild_name检查是否为无效名称。

  • 检查功能标识是否有无效组合。例如

    • 不支持L4硬件校验和时,散播/聚集-DMA无用,因此,这种情况下应该关闭。

    • TCP节段卸载(TSO)需要散播/聚集-DMA,因此,当后者不支持时就要关闭。

  • 设置dev->state中的__LINK_STATE_PRESENT标识,让设备能为系统所用(可见可用)。例如,当可热插拔设备拔出时,或者当支持电源管理的系统进入挂起模式时,该标识就会清楚。此标识的初始化不会触发任何动作,相反,其值会经过缜密的检查,以过滤出非法的请求或者取得设备状态。

  • 设备的队列规则通过dev_init_scheduler做初始化,由流量控制用于实现QoS。队列规则定义出口封包如何排入出口队列,以及如何退出出口队列、定义开始丢掉封包前有多少封包可以排入队列中等等。

  • 通过netdev_chain通知链,通知所有对此设备注册感兴趣的子系统。

netdev_run_todo被调用完成注册时,只会更新dev->reg_state,然后在sysfs文件系统中注册该设备。

除了内存分配的问题外,只有当设备名称无效或重复,或者因某些原因使得dev->init失败时,设备注册才会失败。

设备除名

要把设备除名,内核和相关联的设备驱动程序必须复原注册期间所执行过的所有操作和其他事项:

  • dev_close关闭设备

  • 释放所有已分配的资源

  • 从全局列表和两张hash表中把net_device结构、该驱动程序的私有数据结构,以及其他任何相链接的内存区块。net_device结构的释放是通过free_netdev完成。当内核被编译成支持sysfs时,free_netdev就能使sysfs释放该结构。

  • 删除曾经添加到/proc/sys文件系统的文件。

注意,每当设备之间存在依赖性而把其中一个设备除名时,就会强制其他所有设备除名。

net_device(用dev表示)中的三个函数指针在设备除名时会派上用场:

  • dev->stop

    • 此函数指针将由设备驱动程序初始化为其自有函数之一。关闭设备时,就会启用dev->stop。这里所处理的常见任务包括以netif_stop_queue停止出口队列、释放硬件资源以及停止设备驱动程序使用任何定时器等等。
  • dev->uninit

    • 此函数指针也是由设备驱动程序初始化为其自有函数之一。虽然很少,但目前隧道虚拟设备会对此函数指针初始化:指向一个函数,而此函数主要负责引用计数。
  • dev->destructor

    • 当使用该指针时,通常是初始化为free_netdev或者内含free_netdev的包裹函数。然而,destructor通常不做初始化,只有少数虚拟设备使用。多数设备驱动程序在unregister_netdevice之后都会直接调用free_netdev

unregister_netdevice函数

unregister_netdevice接受一个参数,也就是要删除的net_device结构的指针:

int unregister_netdevice(struct net_device *dev)

后面我们会详细说明网络代码如何使用软件中断以及处理封包的传输和接收。就目前而言,你可以把这些函数视为设备驱动程序和上层协议之间的接口。两次调用synchronize_net是用于让unregister_netdevice能和接收引擎同步,使其被unregister_netdevice更新后不会访问旧数据。

其他由unregister_netdevice负责的任务包括:

  • 如果设备没关闭,就需要先以dev_close予以关闭

  • 接着,net_device实例会从全局列表dev_base和那两张hash表中删除。注意,这样并不足以禁止内核子系统使用该设备:内核子系统依然持有net_device数据结构指针。这就是为什么net_device会使用引用计数以记录对该结构的引用数目还剩多少。

  • 所有和该设备相关联的队列规则实例都会使用dev_shutdown销毁。

  • NETDEV_UNREGISTER通知信息会送往netdev_chain通知链,让其内核组件知晓此事。

  • 用户空间也必须得知有关除名之事。例如,系统中有两块NIC,可用于访问因特网,而此通知信息就可用于启动第二个设备。

  • 任何链接至net_device结构的数据区块都会被释放。例如,多播数据dev->mc_list会使用dev_mc_discard删除,而Divert区块会使用free_divert_blk删除等等。没有在unregister_netdevice中明确删除的东西,都应该由上一点中提到的负责处理通知信息的函数处理例程予以删除。

  • 无论是register_netdevice中的dev->init做了什么,这里devv->uninit都要予以复原。

  • 诸如绑定这类功能可让你把一组设备群集起来,将其视为有特殊性质的单一虚拟设备。在这些设备中,通常会有一个设备被选为主设备,因为主设备会在此群组内扮演特殊角色。显然,要删除的设备应该释放任何对主设备的引用:此时,dev->master的值不为NULL就是bug了。在以绑定为例,由于早先的几行程序代码所送出的NETDEV_UNREGISTER通知信息,dev->master的引用就会被清除。

最后,调用net_set_todo使net_run_todo完成除名,。此外,引用计数的值会以dev_put递减。net_run_todo将使设备从sysfs中除名,把dev->reg_state改成NETREG_UNREGISTERED,等待所有引用都消失,然后调用dev->destructor以完成除名。

引用计数

net_device结构无法释放,除非对该结构的所有引用都已释放。该结构的引用计数放在dev_refcnt中,每次以dev_holddev_put新增或删除引用时,其值就会更新一次。

当设备以register_netdevice注册时,dev->refcnt的初值就设为1.因此,这第一个引用就会由负责网络设备数据库的内核代码所持有。只有调用unregister_netdevice,这个引用就会被释放。也就是说,除非设备要除名了,否则dev->refcnt绝不会降到0.因此,和其他内核对象(当引用计数减到零时,就会由xxx_put函数与已释放)不同的是,除非你把设备从内核除名,否则net_device结构就不会被释放。

总之,在unregister_netdevice结束时调用dev_put不足以使nnet_device实例有删除的资格:内核仍然必须等待,直到所有引用都释放为止。但是,因为该设备除名后就不能再使用,内核必须通知所有引用持有者,使其能释放其引用。其做法是送出一个NETDEV_UNREGISTER通知信息给netdev_chain通知链。也就是说,引用持有者应该对通知链注册,否则就无法接受这类通知信息而据此采取行动了。

unregister_netdevice会着手进行除名流程,然后让netdev_run_todo予以完成。netdev_run_todo会调用netdev_wait_allrefs,一直等待下去,直到所有对net_device的引用都被释放为止。

函数netdev_wait_allrefs

在这里插入图片描述

如上图所示,netdev_wait_allrefs由一个循环组成,只有当dev->refcnt减至零时才会结束。此函数每秒都会送出一个NETDEV_UNREGISTER通知信息,而每10秒都会在控制台上打印出一条警告。剩余时间都在休眠。此函数不会放弃,知道对输入net_device结构的所有引用都已释放为止。

有两种常见情况需要传送一个以上的通知信息:

bug

例如,有段代码持有对net_device结构的引用,但是因为没有在netdev_chain通知链注册,或者因为没有正确处理通知信息,使其无法释放引用。

未决的定时器

例如,假设当定时器到期时要执行的那个函数必须访问的数据中,包含了对net_device结构的引用。这种情况下,你必须等待知道定时器到期,而且处理函数有望会释放其引用。

当设备正被除名时,内核得知设备上有一链接状态变更事件时不需要做任何事。当当前设备状态指出该设备即将被删除时,处理链接状态变更事件列表时和那个正被删除的设备相关联的事件都会被关联为空操作,所以,结果就是该事件列表会被清楚,而实际上只有其他设备的事件将会得到处理。这是简单的方式,把链接状态变更队列中和即将消失的设备相关联的事件清理掉。

开启和关闭网络设备

设备一旦注册就可用了,但是,除非由用户(或应用程序)明确开启,否则还是无法传输和接收数据流。开启设备的请求由定义在net/core/dev.c中的dev_open负责。

开启设备之时,有下列任务要做:

  • 如果有定义的话,调用dev->open。并非所有设备驱动程序都初始化此函数。

  • 设置dev->state中的__LINK_STATE_START标识。把设备标识为开启和运行中。

  • 设置dev->flags中的IFF_UP标识,把设备标识为开启。

  • 调用dev_activate以初始化由流量控制使用的出口对列规则,然后启动看门狗定时器。如果流量控制没有用户配置,就指定默认的FIFO对列。

  • 传送NETDEV_UP通知信息给netdev_chain通知链,已通知感兴趣的内核组件,该设备现在已经开启了。

当设备必须被显式地开启时,也可以由用户命令显式地关闭或由其他事件隐式地关闭。例如,设备除名前,首先得关闭。网络设备是用dev_close关闭的。关闭设备时,有下列任务要做:

  • 传送NETDEV_GOING_DOWN通知信息给netdev_chain通知链,以通知感兴趣的内核组件该设备即将关闭。

  • 调用dev_deactivate以关闭出口队列规则,使得该设备再也无法用于传输,然后因为不再需要,停止看门狗定时器。

  • 清楚dev->state中的__LINK_STATE_START标识,把设备标识关闭。

  • 如果一个轮询动作被调度,以读取设备上的入口封包,就要等待该动作完成。因为__LINK_STATE_START标识已被清除,该设备上已不能再为其他接收的轮询动作进行调度了,但是在该标识清除前可能有一个轮询动作未决。

  • 如果有定义,调用dev->stop。并非所有设备驱动程序都初始化此函数。

  • 清楚dev->flag中的IFF_UP标识,把设备标识为关闭。

  • 传送NETDEV_DOWN通知链给netdev_chain通知链,以通知刚兴趣的内核组件该设备现在已关闭。

更新设备队列规则状态

从“队列规则状态”一节可知,dev->state中可设置一些标识,以定义设备队列规则转态。这里我们说明如何用其中的两个标识处理电源管理和链接状态变更。

与电源管理之间的交互

当内核支持电源管理时,只要系统进入挂起模式或者重新继续,NIC设备驱动程序就可接到通知。pci_driver结构的suspendresume函数指针如何根据内核是否支持电源管理而进行初始化。例如,以下是drivers/net/3c59x.c设备驱动程序初始化其pci_driver实例的方式:

static struct pci_driver vortex_driver = {
    .name      = "3c59x",
    .probe     = vortex_init_one,
    .remove    = __devexit_p(vortex_remove_one),
    .id_table  = vortex_pci_tbl,
#ifdef CONFIZG_PM
    .suspend   = vortex_suspend,
    .resume    = vortex_resume,
#endif 
};

当系统进入挂起模式时,就会执行设备驱动程序所提供的suspend函数,让驱动程序据此采取动作。电源管理状态变更不会影响注册状态dev->reg_state,但是设备状态dev->state必须变更。

挂起设备

当设备挂起时,举例言之,其设备驱动程序会调用pci_driver为PCI设备准备的suspend函数,以处理此事件。除了驱动程序特定行动外,每个设备驱动程序还必须执行其他一些动作:

  • 消除dev->state中的__LINK_STATE_PRESENT标识,因为该设备暂时不能操作。

  • 如果设备已开启,就用netif_stop_queue关闭出口队列,以防止该设备再被用于传输其他任何封包。注意,已注册的设备不一定必须开启:当设备能被识别时,将由内核为该设备分派设备驱动程序,因而得到注册。然而,除非用户配置明确予以请求,否则该设备不会被开启。

这些任务是由netif_device_detach实现的:

static inline void netif_device_detach(struct net_device *dev)
{
    if(test_and_clear_bit(__LINK_STATE_PRESENT, &dev->state) &&
        netif_running(dev)){
        netif_stop_queue(dev)}
}

使设备重新继续

当设备重新继续时,举例言之,其设备驱动程序会调用pci_driver为PCI设备准备的resume函数以处理此事件:

  • 设置dev->state中的__LINK_STATE_PRESENT标识,因为该设备现在又可用了。

  • 如果该设备在挂起前以开启,就用netif_wake_queue重新开启出口队列,然后重启流量控制所用的看门狗定时器。

这些任务是由netif_device_attach实现的:

static inline void netif_device_attach(struct net_device *dev)
{
    if(!test_and_set_bit(__LINK_STATE_PRESENT, &dev->state) && 
        netif_running(dev)){
        netif_wake_queue(dev);
        __netdev_watchdog_up(dev);
    }
}

链接状态变更侦测

当NIC设备驱动程序侦测载波或信号是否存在时,也许由NIC通知,或者对NIC读取配置寄存器以明确做检查,可以分别利用netif_carrier_onnetif_carrier_off通知内核。当载波状态有变时,就可以调用这些函数;因此,当这些函数的启用不恰当时,就什么也不会做。

以下是可能导致链接状态变更的一些常见情况:

  • 电缆线插入NIC,或从NIC中拔除。

  • 电缆线另一端的设备电源关掉或关闭了。这类设备有Hub、桥接器、路由器以及PC NIC等。

当设备驱动程序侦测到其设备之一上有载波而调用netif_carrier_on时,才函数会:

  • 清楚dev->state中的__LINK_STATE_NOCARRIER标识。

  • 产生一个链接状态变更事件,并将其交付给linkwatch_fire_event进行处理。

  • 如果设备已开启,就启动看门狗定时器。此定时器由流量控制使用,以侦测传输是否失败因而受阻。

static inline netif_carrier_on(struct net_device *dev)
{
    if(test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state))
        linkwatch_fire_event(dev);
    if(netif_running(dev))
        __netdev_watchdog_up(dev);
}

当设备驱动程序侦测到其设备之一上遗失载波而调用netif_carrier_off时,此函数会:

  • 设置dev->state中的__LINK_STATE_NOCARRIER标识。

  • 产生一个链接状态变更事件,并将其交付给linkwatch_fire_event进行处理。

注意,这两个函数都会产生一个链接状态变更事件,然后交付给linkwatch_fire_event处理,描述如下:

static inline netif_carrier_off(struct net_device *dev)
{
    if(!test_and_set_bit(__LINK_STATE_NOCARRIER, &dev->state))
        linkwatch_fire_event(dev);
}

调度并处理链接转态变更事件

链接状态变更事件用lw_event结构定义。此结构比较简单:只包含一个指向相关联的net_device结构的指针,以及另一个用于把此结构链接至全局列表的lweventlist的字段,该全局列表内含未决的链接状态变更事件。

注意,lw_event结构并没有包括任何区分侦测到载波和载波遗失这两种情况的参数,这是因为不需要区分。内核所需知道的就是链接转态有了变更,所以,拥有对该设备的引用就够了。对任何设备而言,lweventlist列表中都不会有一个以上的lw_event实例。因为没必要记录历史或记下前前后后的变更:该链接不是能操作,就是不能操作,因此链接状态不是关就是开。两次状态变更等于没变更,三次等于一次。以此类推,所以当设备已有一个未决链接状态变更事件时,新事件就不需要排入队列。此情况的侦测可以通过检查dev->stae中的__LINK_STATE_LINKWATCH_PENDING标识,如下所示:

在这里插入图片描述

一旦lw_event数据结构初始化,取得正确net_device实例的引用,而且也已添加到lweventlist列表,此外,dev->state中的__LINK_STATE_LINKWATCH_PENDING标识也已设置,则linkwatch_fire_event就必须启用能实际处理lweventlist列表元素的函数。函数linkwatch_event并非直接调用,而是交付请求给keventd_wq内核线程调度执行:有一个work_struct数据结构会初始化指向linkwatch_event函数的引用,然后交付给kwvwntd_wq

为了避免处理函数linkwatch_event太过频繁执行,其执行频率会限制在每秒一次。

linkwatch_eventlinkwatch_run_queuertnl锁的保护下处理lweventlist列表的元素。处理lw_event实例仅包括如下内容:

  • 清楚dev->state上的__LINK_STATE_LINKWATCH_PENDING标识

  • 传送一个NETDEV_CHANGE通知信息到netdev_chain通知链上。

  • 传送RTM_NEWLINK通知信息到RTMGRP_LINK TRnetlink群组

这两条通知信息用netdev_state_change传送,但是,只有当设备开启时才会传送(dev->flags & IFF_UP):没人关心已关闭设备上的链接状态变更。

链接监看标识

net/core/linkwatch.c中的代码定义了两个标识,可以在全局变量linkwatch_flags中设置

LW_RUNNING

当此标识设置时,linkwatch_event已进入执行调度。此标识由linkwatch_event本身清除。

LW_SE_USED

因为lweventlist通常最多只有一个元素,代码会对lw_event数据结构的分配做最优化,只静态分配一个,然后总是以其作为列表中的第一个元素。只有当内核必须记录一个以上未决事件时,才会分配其他lw_event结构,否则,就只是循环利用同一个结构。

从用户空间配置设备相关信息

有多种工具可用于配置或转储网络设备的媒介和硬件参数的当前状态,其中一些如下所示:

  • ifconfig和mii-tool,来自net-tools套件

  • ethtool,来自ethtool条件

  • ip link,来自IPROUTE2套件

后面会一一介绍。语法细节参考man手册

Ethtool

此节对ethtool提出一个概括说明,并说明与其mii-tool以及net_device中的do_ioctl函数指针之间的关系。

net_device数据结构中有一个指向类型为ethtool-ops的VFT的指针。ethtool_ops结构是一组函数指针,可用于读取和初始化net_device结构上的许多参数,或者用以触发一种行为。(也就是重启自动协商)

目前,并非所有设备驱动程序都支持此功能;但那些支持此功能的驱动程序不一定会支持所有函数。一般而言,dev->ethtool_ops的初始化都是在probe函数中完成的。

用户空间和这些函数间的接口就是老旧的ioctl系统调用。如下图所示
在这里插入图片描述

用户空间命令ethtool最后启用内核端的dev_ethtool的方式。此图也显示出dev_ethtool的架构,以及此函数如何与通用的MII内核链库接口。(后面一节说明)。

不支持ethtool的驱动程序

dev_ethtool被调用要处理传给设备的命令时,设备的驱动程序不支持Ethtool,就尝试由驱动程序通过dev->do_ioctl函数处理命令。驱动程序也有可能不支持dev->do_ioctl函数。这种情况下,dev_ethtool返回-EOPNOTSUPP

do_ioctl也有可能对dev_ethtool发出回调请求:例如,虚拟设备如果只想让关联的真实设备的驱动程序负责处理命令,就是如此。

媒介独立接口(MII)

MII(Media Independent Interface, 媒体独立接口)是一种IEEE标准规范,描述网络控制芯片和实例媒介芯片之间的接口。例如,有了这种接口,用户可以开启,关闭以及配置自动协商。但并非所有NIC都支持。

Linux最常用的与MII交互的工具就是mii-tools。如同ethtool一样,也是通过ioctl与内核交互。内核提供一组ioctl命令以处理MII。这些命令主要是读写特定的NIC寄存器。

ioctl命令传给驱动程序所提供的dev->do_ioctl函数。此函数可以采用如下两种方式之一予以处理:

  • 只识别MII的三个ioctl命令,然后用设备驱动代码予以处理,这是最常见的情况。

  • 依赖内核MII链接库drivers/net/mii.c,用generic_mii_ioctl处理输入命令。

也有可能,尤其针对虚拟设备而言,用dev->do_ioctl函数去识别和处理MII以外的其他命令。

下列是dev->do_ioctl函数,针对那些依赖内核MII链接库,而且不实现特殊命令的驱动程序的通用模型:

if(!netif_running(dev)){
    return -EINVAL
}
<lock private data structure>
err = generic_mii_ioctl(...);
<unlock private data structure>
return err;

虚拟设备

第五篇中“虚拟设备”一节我们已经知道,就初始化而言虚拟设备和真实设备之间的差异所在。就注册而言,虚拟设备也必须像真实设备那样注册以及开启次啊能予以使用,然而,还是有差别的:

  • 虚拟设备偶尔会调用register_netdeviceunregister_netdevice而不是其包裹函数,而且自行负责上锁。虚拟设备可能需要处理上锁,以便于持有该锁的时间能比真实设备更长。采用这种方法,此锁可能因为保护另一段代码(除了register_netdev以外)而遭到误用,使得持有的时间超过所需的时间。

  • 真实设备不能以用户命令除名(也就是销毁),只能被关闭。真实设备会在其驱动程序卸载时除名。相反,虚拟设备可能通过用户命令创建和除名。

虚拟设备和多数真实设备在使用dev->init、dev->uinit以及dev->destructor时有所不同。因为多数虚拟设备会在真实设备上实现某种或多或少有点复杂的逻辑,因此,会使用dev->init和dev->uinit负责额外的初始化工作和清理工作,dev->destructtor通常会初始化为free_netdev使得驱动程序不需要在除名后明确地调用free_netdev

虚拟设备驱动程序会注册到netdev_chain通知链上,因为多数虚拟设备都是定义在真实设备之上,所以对真实设备的变更也会影响虚拟设备。我们来看两个范例:

  • Bonding

    • Bonding是虚拟设备,允许你绑定一组接口,使其看起来看单一接口。流量可以通过各种算法而分散在这组接口之间吗,其中一种简单循环法。参考下图,当eth0宕掉时,绑定接口bond0在真实设备间分配流量时必须知道此事,把这件事考虑进来。加上eth1也宕掉了,bond0就得关闭,因为没有任何科工作的真实设备存在。
  • VLAN界面

    • Linux支持802.1Q协议,允许定义VLAN(Virtual LAN)接口。参考下图,用户已在eth0上定义了两个VLAN接口。当eth0宕掉时,所有虚拟设备接口也必须宕掉。

在这里插入图片描述

上锁

dev_base列表以及dev_name和dev_name_index两张hash表由dev_base_list锁保护。然而,该锁只用于对列表和hash表的访问予以串行化,而不是对net_device数据结构内容的变更予以串行化。net_device内容的变更是有Routing Netlink信号量(rtnl_sem)负责;而此锁的取得和释放分别通过rtnl_lock和etnl_unlock完成。此信号量可用于net_device实例变更的串行化,而这些变更来自于:

  • 运行期间事件

    • 例如,当链接状态变更时(例如,网络电缆插入或拔出),内核也必须通过修改dev->flags改变设备状态。
  • 配置变更

    • 当用户应用来自net-tools套件的ifconfig和route命令。或者来自IPROUTE2套件的ip命令改变配置时,内核会通过ioctl命令和Netlink套接字接收到通知。由这些启用的函数都必须使用锁。

net_device数据结构包括一些可用于上锁的字段,其中一些如下所示:

  • ingress_lock

  • queue_lock

    • 当处理入口和出口流量调度时,分别由流量控制使用。
  • xmit_lock

  • xmit_lock_owner

    • 用于同步设备驱动程序hard_start_xmit函数的访问。

通过/proc文件系统调整

/proc里没有可用于调整设备注册和除名任务的文件。

本章涉及的函数和变量

在这里插入图片描述
在这里插入图片描述

本章涉及的文件和目录

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jacky~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值