Understanding Linux Network Internals 第八章 翻译稿:设备注册和初始化

【翻译】设备注册和初始化

 

5章和第6章中,我们了解了内核是如何识别网卡以及内核执行初始化过程以使网卡能够和驱动程序通讯。本章中,我们讨论初始化的其它阶段:

l         网络设备什么时候,如何注册到内核

l         网络设备如何注册到网络设备数据库并分配一个net_device结构实例

l         net_device结构是如何被组织到哈希表和队列中以支持各种不同的查找

l         net_device结构实例是如何初始化的。部分由内核核心函数,部分由它们的设备驱动程序

l         虚拟设备和实际设备的注册过程有何不同

 

本章不是力图指导你如何编写网卡驱动程序,我有时会深入到网卡驱动程序代码细节,但我并不会讲到整个的网卡驱动程序的设计。我们仅对注册以及设备驱动程序,诸如连接状态改变侦测、电源管理等特性之间的接口感兴趣,可以参考Linux Device Drivers (O'Reilly)了解设备驱动程序细节问题。

在网卡能够被使用前,与之相关联net_device数据结构必须被初始化,并被加到网络设备数据库中配置和激活。很重要的一点就是大家不要混淆了注册/注销与激活/禁止,它们是两个不同的概念:

l         如果我们抛开加载设备驱动程序的动作,注册/注销是用户独立的,由内核驱动它们。仅被注册的设备还不能工作。我们将会在“设备什么时候注册”和“设备什么时候注销”两小节中看到设备什么时候被注册与注销。

l         激活/禁止设备需要用户的干涉。一旦内核注册了设备,用户能够通过用户命令看到它,并能够配置、激活它。可以参考后续小节“激活和禁止网络设备”。

 

我们来看看什么事件触发网络设备的注册与注销。

8.1、设备什么时候注册

注册网络设备发生在下列情形:

l         加载网卡驱动程序

       网卡驱动程序如果被编译进内核,则它在启动时被初始化,在运行时被作为模块加载。无论初始化是否发生,所以由驱动程序控制的网卡都被注册。

l         插入可热拔插网络设备

       当用户插入一块热拔插网卡,内核通知其对应的驱动程序以注册设备。(为了简单化,我们假定设备驱动程序已经被加载)

 

第一种情形下应用的注册模式在后面的“网卡注册和注销框架”一节描述,它适用所有的总线类型,不管注册函数是被总线体系结构还是被模块初始化代码调用结束都是一样的。例如:我们在第六章看到PCI设备驱动程序如何加载以致pci_driver->probe函数执行,通常命名有几分象xxx_probe,它由驱动程序提供并兼顾设备注册。本章中,我们看看这些probe函数如何实现。

       用于其它总线类型(USB, PCMCIA等等)的设备驱动程序的注册共用同样的框架。就像第六章我们看到的PCI一样,我们不会考虑这些总线体系结构如何结束调用它们的probe类函数。老的总线可能不能够自动侦测设备的存在,可能需要设备驱动程序自己用缺省参数或用户提供的启动时参数主动搜索特殊的内存地址. [*]我们不会考虑这其中的任何一种情况。

       例如:看看drivers/net/Space.c中的net_olddevs_init。这个函数由第七章中介绍的device_initcall宏标记,它在启动时执行。它还兼顾了环回设备的注册。

 

8.2、设备什么时候注销

两个主要的情形会导致设备注销

 

l         卸载网卡驱动程序

       这只适用与驱动程序作为模块被加载的情形,当然不适于编译进内核的情况。当管理员卸载网卡设备驱动程序时,所有相关网卡的驱动程序都被注销(译者注:比如虚拟网卡是依附在实际网卡上的,一旦实际网卡被卸载,则相关虚拟网卡的设备驱动都将被注销)

l         移除热拔插网络设备

       当用户从正在运行且内核支持热拔插功能的系统中移除热拔插网卡时,网络设备被注销。

 

8.3、分配net_device结构空间

 

网络设备由net_device结构定义。由于在内核代码中它们通常命名为dev,所有我在本章中会频繁的使用这个名字来标识net_device。这些数据结构由net/core/dev.c中定义的alloc_netdev函数分配空间,它需要三个入参:

 

l         私有数据结构大小

       我们将在“net_device结构组织”小节中看到:net_device结构由设备驱动程序用私有数据块扩展以存储驱动程序参数,这个参数指定了数据块的大小。

l         设备名

       这是一个局部名称,内核通过某些策略确保设备名唯一。

l         配置函数:

       这个函数用于初始化net_device结构的部分域,详细信息参考“设备初始化”一节和“设备类型初始化:xxx_setup函数”小节。

 

返回值是alloc_netdev函数分配的net_device的结构指针,如果出错就返回NULL

 

每个设备都根据设备类型分配一个唯一名字,当同样类型的设备注册是,这个名字包含按序递增的数字。例如,以太网设备分配eth0eth1等等。单个的设备由于注册设备的顺序不同也会被分配不同的名字。例如,如果你有两块网卡由不同的模块处理,设备名字则依赖两个模块加载的顺序;而热拔插设备明显地会导致自己的名字变化不可预知。

 

因为用户空间配置工具引用了内核分配的设备名称,所以设备注册顺序相当重要。由于这是用户空间细节,除非谈到net-tools工具包中的诸如nameif(这个工具允许将确定的名字分配给基于MAC地址的网络接口)等工具,我们后面不必关心它。

当设备名以name%d(:eth%d)的形式传递给alloc_netdev,内核用dev_alloc_name函数分配完整的名字,后者根据设备类型将%d改为第一个未分配的数字。

内核也提供了一些封装alloc_netdev功能的函数,表8-1列出了几个用于普通设备类型并能传适当参数给alloc_netdev函数的包裹函数[*]。例如:alloc_etherdev函数用于以太网设备,所以它创建以字符串eth后跟唯一数字形式的设备名;第二点,它指派ether_setup作为配置函数,对于所有以太网卡来说,配置函数均把net_device结构的部分域初始化为公用值。

 

[*]也有例外,就是类似包裹函数并不遵循alloc_xxxdev命名规则。此外,有些设备直接调用alloc_netdev函数注册到内核而不采用包裹函数。

 

8-1 alloc_netdev包裹函数

网络设备类型

封装函数名

说明

以太网

alloc_etherdev

return alloc_netdev(sizeof_priv,"eth%d",ether_setup);

光纤分布式数据接口

alloc_fddidev

return alloc_netdev(sizeof_priv,"fddi%d",fddi_setup);

高性能并行接口

alloc_hippi_dev

return alloc_netdev(sizeof_priv,"hip%d",hippi_setup);

令牌网

alloc_trdev

return alloc_netdev(sizeof_priv,"tr%d",tr_setup);

光纤通道

alloc_fcdev

return alloc_netdev(sizeof_priv,"fc%d",fc_setup);

红外数据联盟

alloc_irdadev

return alloc_netdev(sizeof_priv,"irda%d",irda_device_setup);

 

8.4、网卡注册和注销框架

8-1(a)表明了网络代码中网卡驱动程序注册的通用方式,图8-1(b)表明了注销时发生的调用动作。尽管例子展示的是PCI以太网网卡,但对每种设备类型都是一样的;只是函数名称或函数被调用的方式根据总线代码如何实现而变化。

 

 

8-1(a)  设备注册方式                                                    8-1(b) 设备注销模式

 

 

函数由alloc_etherdev分配net_device结构开始,alloc_etherdev函数也初始化所有以太网设备通用的参数。然后驱动程序初始化net_device结构其它部分,并调用register_netdev函数以结束设备的注册过程。

 

注意:

l         驱动程序调用合适的alloc_netdev包裹函数(如例子中的alloc_etherdev),并提供唯一的私有数据块大小。表8-1列出了几个包裹函数

l         包裹函数用驱动程序提供的参数调用alloc_netdev,并且加上另外两个参数(设备名和初始化配置函数)

l         alloc_netdev分配的内存块包括net_device结构、驱动程序私有数据块、以及强制对齐的填充数据。参考后面章节的图8-2

l         有些驱动程序调用netdev_boot_setup_check函数以检查用户在加载内核时是否提供了启动时参数。参考第七章“用启动选项配置网络设备”小节

l         新的net_device实例由register_netdevice插入设备数据库,(参考后面的小节“设备注册)。顺便提一下,我在这里用了术语数据库,在本书的其它地方,提及的许多数据结构可以在内核需要时通过数据库方便的获取相关信息。

   

8-1(b)展示了设备注销的简单形式,它总是包含unregister_netdevice free_netdev函数,free_net有时明确的调用,有时由dev->destructor函数[*]间接调用,这可以在后面的图8-4中看到。设备驱动程序也必须释放设备使用的所有资源(IRQ、内存映象等等),但是本章对此不作描述。

[*]仅有些虚拟设备的驱动程序采用这种方式(例子参考net/8021q/vlan.c),图8-4的两个调用是互斥的

 

8.5、设备初始化

 

在“设备什么时候注册”一节,我们看到什么需要内核初始化以和网卡通讯,在接下来的章节里,我们看看高级初始化任务。

net_device结构相当大,它的域由不同的函数块初始化。特别的,每个域都负责不同的域子集[*] .

[*]一个值得关注的例外是环回设备,它的初始化是没有限制的,在drivers/net/loopback.c中定义的loopback_dev函数里面

l       设备驱动程序

       诸如IRQI/O内存、I/O端口等参数的值依赖硬件配置,由设备驱动程序提供。参考第五章

l       设备类型

       某个设备类型家族的所有设备都公用一些结构字段,这些字段的初始化由xxx_setup函数实现,例如:以太网设备采用ether_setup函数,参考“设备类型初始化:xxx_setup函数”小节。

l       特性

       有些必需的和可选的属性也需要初始化,例如:网络接口排队算法(也就是QoSqdisc(queuing discipline))register_netdevice函数初始化,这在“register_netdevice函数”小节中描述其它特征在关联模块被通知有新设备注册时初始化(参考“设备注册状态通知”小节)

       设备类型初始化是设备驱动程序初始化的一部分(也就是说,xxx_setupxxx_probe调用),以使驱动程序有机会覆盖设备类型设定的初始值。可以参考“可选的初始化和特殊情况”小节的例子。

       8-2展示了由xxx_setup函数初始化的函数指针及留给设备驱动程序[*](xxx_probe)初始化的函数指针:什么是设备类型特性、什么是设备模型特性。注意,并不是所有的设备驱动程序都遵循表8-2所示的差别,例如:有些xxx_setup函数并不初始化任何函数指针(例如:net/irda/irda_device.c中的irda_device_setup),但另一些会初始化所有的函数指针(如:drivers/net/wireless/airo.c中的wifi_setup)

       [*] 第二章包含了net_device结构的所有参数的细节

 

8-2 xxx_setupxxx_probe初始化的net_device 函数指针

 

初始化函数

函数指针名

xxx_setup

change_mtu

set_mac_address

rebuild_header

hard_header

hard_header_cache

header_cache_update

hard_header_parse

设备驱动程序探测函数

open

stop

hard_start_xmit

tx_timeout

watchdog_timeo

get_stats

get_wireless_stats

set_multicast_list

do_ioctl

init

uninit

poll

ethtool_ops (this is actually an array of routines)

 

8-3和表8-2似,但是它列出的不是函数指针,而是net_device结构的其它一些字段。

 

8-3 xxx_setupxxx_probe初始化的net_device结构字段

 

初始化函数

变量名

xxx_setup

type

hard_header_len

mtu

addr_len

tx_queue_len

broadcast

flags

设备驱动程序探测函数

base_addr

irq

if_port

priv

features

 

参考第二章,可以获得表8-2和表8-3中所示字段的更多的细节。

 

8.5.1 、设备驱动程序初始化

 

       net_device结构中由设备驱动程序初始化的域,通常被第六章“全局轮廓”(The Big Picture)一节介绍且图8-1(a)描绘的xxx_probe函数初始化。

       有些驱动程序能够处理不同的设备模型。因此,同样的参数可以基于设备模型和容量做不同的初始化。下面的代码来自于drivers/net/ 3c 59x.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 = vortex_start_xmit;

    }

8.5.2 、设备类型初始化:xxx_setup函数

对于绝大多数普通的网络设备类型,都有一个xxx_setup函数初始化net_device结构(包括参数和函数指针)的字段,这个结构对所有以太网卡实例而言,对所有同类型设备都是一样的。

       在表8-1中,你看到各种alloc_xxxdev函数传递右边的xxx_setup函数给alloc_netdev函数(作为第三个输入参数)。下面是ether_setup函数,它就是以太网设备所用的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);

}

       正如你看到的,函数仅仅初始化任何以太网卡所共享的字段和函数指针:MTU设为1500,链路层广播地址设为FF:FF:FF:FF:FF:FF,发送队列长度设为1000个包长度,[*]等等。

这是linux选择实现的,并非根据任何协议规范,而是根据发送队列规则配置,这个值有可能不用。

       8-1中表明,普通的分配包裹函数和xxx_setup函数的用法是相当的接近。但是:

l         有几种设备定义setup函数,但是并没有提供与表8-1中函数类似的普通包裹函数。ARCNET[]IrDA[]就是其中之一

[]ARCNET是基于令牌总线设计的局域网(类似802.4),它在工业自动化行业的良好口碑得益于其稳定的性能。LinuxARCNET几其他一些设备驱动提供了一个通用的层。

[]IrDA(Infrared Data Association)是红外线无线通讯标准

l         普通的xxx_setup可能被不属于显示的设备使用,ether_setup就是一个例子,它也被非以太网设备使用,一个特殊的xxx_setup函数的绝大部分初始值都适合设备驱动的需要,后来可以用于xxx_setup函数并能够简单的被不恰当的初始值覆盖。但是这种方法并不常用。

l         以太网驱动能够用ether_setup(alloc_etherdev直接调用)提供的缺省初值,但是要覆盖部分初始值,例如:3c59x.c驱动程序不用ether_setup设置的net_device->mtu值,而是用局部变量覆盖它。变量被ether_setup初始化为同样的缺省值,但是驱动程序能够设置成网卡模型能够处理的更大的值。

 

8.5.3 、可选的初始化和特殊情况

       有种情况就是net_device结构的一些参数并不被简单的初始化,因为这对这种设备类型没有意义,相关的函数指针和字段不被初始化并保持NULL值。

       为了避免NULL指针的应用,内核确保可选的函数指针在调用前被初始化,下面是来自于register_netdevice的一个例子:

    []在第一章中,你可以在VFTs的用法 中看到一些细节

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

    ...

}

       很重要的一点就是要注意到外部因素也能够改变表8-2和表8-3中的字段,有个例子涉及到net_device->mtu字段,虚拟设备通常继承它们关联的实际设备的配置参数,然后如果需要才校正这些参数,例如:p2p协议创建的虚拟的隧道接口从它关联的实际设备继承dev->mtu(这不是自动的,它由虚拟设备驱动程序考虑),但是,由于p2p协议需要额外的IP头,因此MTU需要减少(参看net/ipv4/ipip.c中的ipip_tunnel_xmit,它虚拟了一个底层的以太网设备)

 

8.6net_device结构组织

net_device结构中较重要的方面如下:

l         我们在“分配net_device结构”一节中看到,当调用alloc_netdev分配net_device结构时,驱动程序的私有数据块的长度(此长度依据驱动程序,即使它根本不使用私有数据)被传递给alloc_netdevalloc_netdev函数追加私有数据到net_device结构。图8-1展示了参数如何被传递,图8-2展示了内存分配效果。

l         8-2也表明了net_device数据结构和可选的设备驱动私有数据结构之间的关系。通常,第二部分和第一部分一起分配,以使单个kmalloc函数就足够了,但也有一种情况,就是驱动程序更愿意自己分配它的私有块(参考图8-2driver C)

l         正如图8-2所示例子,驱动程序私有块的长度及其内容不仅会从一个设备类型到另一个类型改变(如令牌网到以太网),而且在同类型的设备之间也会改变(如两个不同的以太网卡)

l         dev_base(本节后续介绍)net_devicenext指针指向net_device结构的开始,而不是指向已分配块的开始。但是,初始填充长度保存在dev->padded字段,这个字段允许内核在适当的时候释放整个内存块。

net_device数据结构被链入一个全局链表,如图8-2所示,还链入两个哈希链表,如图8-3所示。这些不同的结构使内核能够很容易查询、查找需要的net_device数据库。细节如下:

 

dev_base

       所有的net_device实例组成的全局链表允许内核很容易地查询设备,例如,在获取一些统计信息,通过用户命令改变所有设备配置,或者,根据被给的标注查找匹配的设备。

    由于每个设备都有自定义的私有数据结构,net_device结构全局链表可能连接不同长度尺寸的节点(见图8-2)

 

8-2 已注册设备全局链表

 

 

dev_name_head

       这是以设备名为索引的哈希表,它非常有用,如,当由ioctl接口改变一个配置时。早期的配置工具通过ioctl接口与内核交互,通常通过设备名来引用设备。

 

dev_index_head

       这是以设备IDdev->ifindex为索引的哈希表,交叉引用net_device结构通常要么存储设备ID,要么存储net_device结构的指针。dev_index_head对这种情形很有用,新的IP配置工具(来自IPROUTE2)通过Netlink套接字和内核交互,通常通过设备ID引用设备。

 

8-3 用于查找基于设备名和设备ID的设备实例的哈希表

 

 

 

8.6.1 、查找

       绝大多数普通的查找都是基于设备名或设备ID,这两种查找类型由dev_get_by_name dev_get_by_index实现,它们使用了前面讨论的两种哈希表。也有可能基于设备类型、MAC地址等搜索net_device实例,这种搜索采用dev_base链表。

       dev_base链和两个哈希表中的所有的搜索,都由dev_base_lock锁保护

    所有的查找函数都定义在net/core/dev.c

 

8.7、设备状态

       net_device结构包含定义设备当前状态的各种字段,这些包括:

l         flags

       用于存储不同的状态,它们绝大多数表示设备的性能,不过,其中的IFF_UP用于指示设备是激活的(UP)还是关闭的(DOWN),你可以在include/linux/if.h中看到一列IFF_XXX标志。也可参考“使能和禁止网络设备”一节

l         reg_state

       设备注册状态,“注册状态”小节列出了这个字段可能被指派的值

l         state

       关于流量控制的设备状态,参考“排队规则状态”小节

在这些变量之间你可以发现有时有些位是交迭的。例如:每次flag设置成IFF_UPstate设置成__LINK_STATE_START,反之亦然。它们一起被设置和清除,分别由dev_open dev_close,但是,域是不同的,当些模块化代码时,有些重叠位有时会被引入。

 

8.7.1 、排队规则状态

每个网络设备都被指派了排队规则,用于流量控制以实现其质量检测机制。net_device结构的state字段时用于流量控制的结构字段之一,state时位域实现,下面列出了可被设置的标志,它们都被定义在include/linux/netdevice.h

__LINK_STATE_START

       设备时激活的,这个标志可以被netif_running检测。参考“使能和禁止网络设备”一节

__LINK_STATE_PRESENT

       设备是可见的,这个状态似乎是多余的,但是能够只是热插拔设备临时移除。当系统处于挂起模式随后唤醒时,这个标志能够被分别清除和恢复。这个标志能够由netif_device_present检测。参考“register_netdevice函数”小节

__LINK_STATE_NOCARRIER

       没有传递。此标志可由netif_carrier_ok检测,参考“侦测连接状态改变”小节

__LINK_STATE_LINKWATCH_EVENT

       设备连接状态改变。参考“调度处理连接状态改变事件”小节

__LINK_STATE_XOFF

__LINK_STATE_SHED

__LINK_STATE_RX_SCHED

       上面三个标志由代码设置以管理设备的入口和出口流量,我们在第三部分看看它们如何使用。

 

8.7.2 、注册状态

       设备注册在网络堆栈中,其状态保存在net_device结构的reg_state字段中,它可以设置的值形如NETREG_XXX,它们在include/linux/netdevice.h中,并定义在net_device结构内部。在下一节我们将会看到它们之间如何关联,下面是简单的描述:

NETREG_UNINITIALIZED

       定义为0,当net_device数据结构被分配,并且初始化时,dev->reg_state被赋此值,代表着0

NETREG_REGISTERING

       net_device结构被链入“net_device结构组织”一节介绍的结构链表,但内核仍然在/sys文件系统中加入一个入口。

NETREG_REGISTERED

       设备已完全注册

NETREG_UNREGISTERING

       net_device结构从结构链表中移除时

NETREG_UNREGISTERED

       设备完全注销(包括移除/sys文件系统入口),但net_device结构并未被释放

NETREG_RELEASED

       所有对net_device结构的引用都已释放,数据结构亦即释放,但是,从网络代码的角度来看,这由sysfs来处理,参考“引用计数

8.8、注册和注销设备

       内核中网络设备是通过register_netdev unregister_netdev函数来注册与注销的。有很多简单的包裹函数只是加锁,然后分别调用register_netdev unregister_netdev函数,他们都在net/core/dev.c文件中定义,我们已经简要的介绍了这些函数(在图8-1)

       8-4展示了net_device能够设置的状态和前面所述的函数进入图中所示状态的地方,以及其它关键函数调用的地方。它们都将在后面的小节中介绍。特别要注意以下几点:

l         状态的变化可能使用NETREG_UNINITIALIZED NETREG_REGISTERED之间的中间状态。这些环节由“衔接操作:netdev_run_todo”小节描述的netdev_run_todo函数处理。

l         设备驱动程序能够使用net_device的两个虚拟函数init uninit在注册或注销设备时分别初始化和清除私有数据。它们主要用于虚拟设备,可参考“虚拟设备”小节

l         设备的注销直到所有对net_device数据结果的相关引用都释放时才能够完成,netdev_wait_allrefs直到条件满足才会返回,请参考“引用技术”小节

l         设备的注册与注销都以netdev_run_todo函数结束,在“衔接操作:netdev_run_todo”小节中看到register_netdev unregister_netdev函数如何和netdev_run_todo函数交互。

8-4  net_device注册状态图

 

 

8.8.1 、衔接操作:netdev_run_todo

       register_netdevice做部分注册工作,然后由netdev_run_todo结束它。起先,通过看代码可能并不清楚这是如何发生的,那我们在图8-4的帮助下看看它时如何工作的。

       net_device 结构的改变受rtnl_lock rtnl_unlock两个Netlink路由旗语保护。这就是为何register_netdev函数在开始获得锁,然后在返回前释放锁(详情参考“锁”小节)。一旦register_netdevice完成了它的工作,他会通过net_set_todo加新的net_device结构给net_todo_list。这个链表包含了注册(或注销,我们一会就会看到)已结束的设备。链表并不由单独的内核线程或定时器处理,它一直等到释放锁时由register_netdev直接处理。

       因此,rtnl_unlock不仅释放锁,而且还调用netdev_run_todo函数[*],后者扫描net_todo_list数组,并完成其中的所有网络设备实例的注册。

       rtnl_unlock是旗语函数up的简单包装函数,当up被直接调用时,如rtnetlink_rcv中所示,netdev_run_todo被明确调用,可参考“锁”小节。

    在同一时间仅一个cpu能够运行net_run_todo函数,由互斥变量net_todo_run_mutex控制其串行化。

       设备的注销也以同样的方式正常处理,如图8-5(b):

8-5 register_netdev unregister_netdev结构

       netdev_run_todo函数执行以结束设备注册注销在本节后面的“register_netdevice函数”和“unregister_netdevice函数”小节分别描述。

       注意,由于netdev_run_todo函数处理设备注册注销任务时未获取锁,函数能够安全的睡眠和离开可用旗语。我们可以在“引用计数”小节看到为什么这是比较好的处理方法的一个例子。

       8-5所示的模型显示内核在netdev_run_todo被调用时在net_todo_list链表中不会有超过一个的net_device实例。如果在释放锁时register_netdev unregister_netdev仅加一个net_device实例到链表,然后立刻处理后面的元素,那怎样才能够有超过一个的元素呢?例如:对于设备驱动,用象下面在一个槽中所有设备的注销的循环是可能的(例如:drivers/net/tun.c文件中的tun_cleanup)

 

rtnl_lock( );

loop for each device driven by this driver {

    ... ... ...

    unregister_netdevice(dev);

    ... ... ...

}

rtnl_unlock( );

 

       这比下面在每个循环迭代中的net_todo_list获取和释放锁的方法好些:

 

loop for each device driven by this driver {

    ... ... ...

    unregister_netdev(dev);

    ... ... ...

}

 

8.8.2 、设备注册状态通知

       内核组件和用户空间应用程序可能都想知道网络设备什么时候注册、注销、打开、关闭,关于这些事件的通知由两个途径产生:

netdev_chain

       内核组件能够注册通知链,参考下面的“netdev_chain通知链”小节

Netlink RTMGRP_LINK 多播组

       用户空间应用程序诸如监控工具和路由协议能够注册RTnetlinkRTMGRP_LINK多播组,参考“Rtnetlink连接通知”小节内容

 

8.8.2 .1netdev_chain通知链

       我们在第四章明白通知链的概念以及其如何使用,对注册注销设备的各个阶段的处理由netdev_chain通知链报告,这条链在net/core/dev.c文件中被定义。内核组件对由register_netdevice_notifier unregister_netdevice_notifier分别注册、注销的通知链中的事件感兴趣。

       include/linux/notifier.h文件中列出了netdev_chain通知的所有的NETDEV_XXX事件,下面是我们本章列出的几个事件以及触发它们的条件。

NETDEV_UP

NETDEV_GOING_DOWN

NETDEV_DOWN

       NETDEV_UP被触发以报告设备使能。它由dev_open产生。

    NETDEV_GOING_DOWN被触发以报告将被禁止,而NETDEV_DOWN被触发报告设备已被禁止。两者均由dev_close产生。

有关这三个事件的更多信息请参考“使能和禁止网络设备”一节。

NETDEV_REGISTER

       设备已经注册,事件由register_netdevice产生,详见register_netdevice函数”小节

NETDEV_UNREGISTER

       设备已注销,事件由unregister_netdevice产生,详见unregister_netdevice函数”小节

下面还有些其它的事件:

NETDEV_REBOOT

       设备由于硬件错误而重启,目前不用,保留

NETDEV_CHANGEADDR

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

NETDEV_CHANGENAME

       设备的名字改变

NETDEV_CHANGE

       设备状态或设备配置改变,这被用在各种情况,而不被NETDEV_CHANGEADDRNETDEV_CHANGENAME屏蔽(译者注:这里的意思是说不管什么改变,NETDEV_CHANGE

事件一定触发)。它用于dev->flags标志改变时。

       NETDEV_XXX通知通常在响应用户配置改变时产生。

注意:当注册到通知连时,register_netdevice_notifier函数也会将以前的NETDEV_REGISTER NETDEV_UP通知重发给系统中当前注册的设备(新的注册者)。这给新的注册者一个清晰的当前已注册设备状态图。

       相当多的内核组件注册到netdev_chain通知链,其中几个如下:

l        路由:

    例如,路由子系统用通知链增删设备相关的路由入口,可参考“32

l        防火墙:

    例如:防火墙缓存了当前已禁止(关闭)的设备的数据包,它必须丢弃这些包或者根据自己的策略做其它处理。

l        协议代码(如:ARP, IP等等)

       例如,当你改变本地设备的MAC地址时,ARP表必须因此而更新,要了解小节请参考相关协议章节。

l         虚拟设备:

       参考“虚拟设备”一节

l         RTnetlink

       参考下面的“Rtnetlink链接通知

8.8.2 .2Rtnetlink链接通知

       当设备状态或配置有些改变时,通知被发送到连接多播组RTMGRP_LINK(带有rtmsg_ifinfo接口信息)。有几个通知如下:

l         netdev_chain通知链收到一个通知时,RTnetlink登记给前面介绍的netdev_chain链并重它收到的通知。

l         当禁止的设备使能时,反之亦然(参考netdev_state_change)

l         net_device->flags的一个标志位改变时,例如,由用户配置命令(参考dev_change_flags)

Netplugd是一个守护进程,在net-utils工具保中提供,它能够根据用户配置文件监听并作用于这些通知。更多细节请参考netplugs手册页。

8.9、设备注册

       8-1展示了设备注册的基本模型,它并不是简单的把net_device结构插入到全局链表或前面“net_device结构组织”一节介绍的哈希表。它还包括net_device结构的部分参数初始化、产生一个广播通知以告知所有的其它内核组件其注册、等等其它任务。设备由register_netdev函数注册,它仅仅是简单的封装register_netdevice函数。这种封装主要是考虑锁和函数名的因素,这在先前的“分配net_device结构空间”一节中已有描述。锁保护着已注册设备链表dev_base

8.9.1 register_netdevice函数

       如图8-5所示,register_netdevice启动设备注册并调用net_set_todo函数,并最终调用netdev_run_todo完成注册。

       下面是register_netdevice执行的主要任务:

l         net_device的部分域的初始化,包括在“”一节中所列的用于锁操作的域

l         当内核支持转向特性时,分配一个特性需要的配置块,并连接到dev->divert,这是由alloc_divert_blk函数来实现的。

l         如果设备驱动程序提供了初始化函数(dev->init!=NULL),则执行次函数。可参考“虚拟设备”一节。

l         dev_new_index函数给设备分配一个唯一标识符。标识符由一个计数器生产,每次一个新设备加到系统中计数器就会递增.计数器是32位变量,所以dev_new_index函数中有一个if子句用于回绕,还有一个if子句处理变量赋值为已经指派的值.

l         追加net_device到全局链表dev_base,并把它插入到“net_device结构组织一节中介绍的两个哈希表中.虽然把结构加到dev_base链表头很快,但内核偶尔也会扫描整个链表来检测相同的设备名.设备名由dev_valid_name函数检测是否是有效的名字.

l         检测特性标志是否是有效的组合,例如:

1.         Scather/Gather-DMA没有L4(传输层)硬件校验和支持则是无用的,所以在这种情况下被禁止.

2.         TCP Segmentation Offload (TSO)需要Scather/Gather-DMA,所以在后者部支持时也被禁止.

L4层校验和的详细情况请参考“19

l         设置dev->state中的__LINK_STATE_PRESENT标志使设备对系统使可用的(可见和可用).例如,当一个热拔插设备被拔出,或者当支持电源管理的系统进入挂起模式时, 这个设备标志被清除.详情参考“流量控制状态小节.

这个标志位的初始化不被任何动作触发,相反,它的值在定义好的情况下检测以过滤非法请求和获取设备状态.

l        初始化设备队列规则,用流量控制通过dev_init_scheduler实现QoS,队列规则定义了出口包如何入队列和从出口队列出队列,还定义了各种包在开始丢弃它们之前如何排队,详情参考第11章的“流量控制接口”.

l        netdev_chain通知链通知所有对设备注册感兴趣的子系统.通知链在4描述.

netdev_run_todo被调用以结束注册时,它仅更新dev->reg_state并在sysfs文件系统注册设备.

除了内存分配问题外,在设备名无效、或重复以及dev->init由于某种原因调用失败时设备注册也可能失败。

 

8.10、设备注销

       为了注销设备,内核和相关设备驱动程序需要撤销所有注册时执行的操作,以及更多的操作:

l         用“使能和禁止网络设备”一节介绍的dev_close函数禁止设备。

l         释放所有分配的资源(IRQ, I/O内存, I/O端口等等)

l         从全局队列dev_base和“net_device结构组织”一节中介绍的两个哈希链表中移除net_device结构

l         一旦对结构的所有引用都释放了,就释放net_device结构、驱动程序私有数据结构和其它连接到它的内存块(见图8-2). net_device结构由free_netdev函数释放,当内核编译时支持sysfsfree_netdevsysfs负责释放这个结构。

l         移除加到/proc /sys文件系统的任何文件

注意无论设备间是否有依赖,注销它们中的一个可能强制注销其它所有(或部分)的设备,可以参考“虚拟设备”中的例子。

       当注销设备时,在net_device(变量名dev代表)中有三个指针比较令人关注:

dev->stop

    函数指针由设备驱动程序初始化为一个局部函数,当禁止设备时(参考“使能和禁止网络设备”一节)dev_stop调用,这里通常处理的事情包括调用netif_stop_queue禁止出队列,释放硬件资源,删除所有驱动程序使用的定时器等等。

    netif_xxx_queue函数在第11章介绍。

    虚拟设备不需要释放硬件资源,但需要考虑一些其它的高级的问题。参考“虚拟设备”一节。

dev->uninit

    函数指针也由设备驱动程序初始化为一个局部函数,当前仅有少数虚拟设备通道初始化它,这些设备把它指向一个主要处理引用计数的函数。

dev->destructor

       它通常初始化为free_netdev或其封装函数,但是,destructor一般不被初始化;仅有少数虚拟设备使用它。绝大多数设备驱动在unregister_netdevice后直接调用free_netdev函数。

8-4 表明了这三个函数的调用事件和顺序。

 

8.10.1 unregister_netdevice函数

       unregister_netdevice接受一个指向它要移除的net_device结构的参数:

int unregister_netdevice(struct net_device *dev)

       在第9章中我们看到了网络代码用软中断(softirqs)处理包的接收(net_tx_action)与发送(net_rx_action).现在你可以看到这些函数,作为设备驱动与底层协议之间的接口。这两个函数调用synchronize_net函数用于同步unregister_netdevice和接收引擎(net_rx_action),以使由unregister_netdevice更新的旧有的数据不被访问。

       unregister_netdevice处理的其它事情包括:

l        如果设备没有被禁止,则首先由dev_close禁止它(参考“使能和禁止网络设备)

l        然后,net_device实例从全局链表dev_base中移除,同时,“net_device结构组织”一节中介绍的两个哈希表也被移除。注意这样并不能阻止内核子系统使用设备:它们仍然拥有指向net_device结构的指针。这就是net_device结构使用引用计数以获取结构还有多少引用的原因(参考“引用计数”小节)

l        dev_shutdown函数释放所有与设备相关的队列规则实例。

l        NETDEV_UNREGISTER通知被送到netdev_chain通知链使其它内核组件知道此事件发生了,参考“设备注册状态通知”一节。

l        用户空间收到注销通知,例如,在一个用于访问internet网的双网卡的系统中,这个通知可用于启动第二块网卡。参考“设备注册状态通知”一节。

l        释放所有链接到net_device结构的数据块。例如,dev_mc_discard函数释放多播数据dev->mc_listfree_divert_blk函数释放转发块,等等。在unregister_netdevice函数未明确移除的数据块假设被处理在先前提及的通知的函数句柄移除。

l        register_netdevice中的dev->init处理的任何事情在此都不被dev->uninit处理。

l        绑定的特性允许你组合一组设备以欺骗它们作为一个单独的具有特殊功能的虚拟设备。在这些设备之间,其中之一常被选择作为主设备,因为它扮演着组内的特殊角色。由于这个明显的原因,被移除的设备应当释放所有的对主设备的引用:在这中情况下,dev->master非空将是一个bug。如果我们组合这组设备,dev->master引用会被清除,因为NEtdEV_UNREGISTER通知被发送,早期仅许几行代码。

最后,net_set_todo被调用以使net_run_todo结束注销过程且dev_put函数减少引用计数,这在“衔接操作:netdev_run_todo”一节中有描述。net_run_todo函数从sysfs中注销设备,并设置dev->reg_stateNETREG_UNREGISTERED,等到所有的引用都释放后,调用dev->destructor结束注销过程。

 

8.10.2 、引用计数

       net_device结构直到对其的引用都释放后才被释放,结构的引用计数保存在dev->refcnt中,每次增加或移除它都由dev_hold dev_put分别更新一次。

    当设备由register_netdevice注册时,dev->refcnt被初始化为1。因此第一次引用被维护设备数据库的内核代码保存,这次引用也仅在调用unregister_netdevice时释放。这意味着dev->refcnt直到设备注销才减为0,所以,不像其它内核对象在引用计数减为0时由xxx_put函数释放,net_device结构直到你从内核注销设备时才被释放。我们已经在“设备什么时候注销”一节中看到导致设备注销的条件。

       简而言之,在unregister_netdevice结束前调用dev_put并不使net_device实例具备删除条件有效:内核仍然要等到所有引用释放。但是在注销后设备不再可用,内核需要通知所有引用者以便它们能够释放其引用,这是通过发送NEtdEV_UNREGISTER通知给netdev_chain通知链实现的。这也意味着引用者要注册到通知链,否则它们不会收到这样的通知而采用适当的动作。

       正如在“衔接操作:netdev_run_todo”一节中提及的,unregister_netdevice启动注销处理过程,并让netdev_run_todo结束它。netdev_run_todo调用netdev_wait_allrefs函数不确定地等到所有对net_device结构的引用释放,下一小节将深入到neTDev_wait_allrefs.的内部细节。

 

8.10.2 .1netdev_wait_allrefs函数

       如图8-6描绘的,neTDev_wait_allrefs.由一个循环组成,仅当dev->refcnt值减到0时结束。它每秒中发出一个NEtdEV_UNREGISTER通知,没10秒朝控制台发送一个警告。接下来的时间它就睡眠,此函数直到所有对入参net_device结构的引用都释放了才结束。           

有两种情况下需要发送不止一个的通知:

一个bug:

       例如:有些代码能够增加对net_device结构的引用,但是它不会释放它们,因为它并没有注册到通知链,或者没有正确处理通知。

一个未决定时器:

       例如:假设当某些定时器到期需要访问包含对net_device结构引用的数据时,某个函数被执行。在这种情况下,你需要等到定时器到期并且其处理函数有望释放它的引用。

 

    注意,由于neTDev_run_todounregister_netdevice释放其锁时执行,如“衔接操作:netdev_run_todo”小节所述,这意味着不管谁触发注销,设备驱动更有可能睡眠直到neTDev_run_todo完成其工作。

    当函数发送通知时,它也处理未决的连接状态改变事件。连接状态改变事件在“连接状态改变检测”一节中介绍,在此,可以说,在设备注销时,内核不需要做任何事情,去通知设备连接状态改变事件。当当前设备状态是设备将被移除,与设备移除相关的事件与no-ops关联,当链路状态改变事件链表被处理时,因此,结果就是,事件链表被清除,仅有其它设备的事件实际被处理。这正式一种从与设备消失相关事件中清除连接状态队列的一种方法。

 

8.11、使能和禁止网络设备

       一旦设备注册即可使用,但它必须在用户(或用户空间应用程序)使能后才可以收发数据。dev_open处理设备使能的请求,它定义在net/core/dev.c。使能设备有下面几个环节组成:

l         如果dev->open被定义则调用它。并非所有的驱动程序都初始化这个函数。

l         设置dev->state__LINK_STATE_START标志位,标记设备打开并在运行。

8-6 netdev_wait_allrefs函数

 

Understanding Linux Network Internals 第八章 翻译稿:设备注册和初始化_第1张图片

l         设置dev->flagsIFF_UP标志位标记设备启动。

l         调用dev_activate所指函数初始化流量控制用的排队规则,并启动监视定时器.[*]。如果用户没有配置流量控制,则指定缺省的先进先出(FIFO)队列。

监控定时器细节参考第11

l         发送NETDEV_UP通知给netdev_chain通知链以通知对设备使能有兴趣的内核组件。

当设备需要明确的使能时,它能够被用户命令明确地或其它事件隐含地禁止。例如,在设备注销

时,它首先禁止(参考“设备注销”一节),网络设备由dev_close禁止,禁止设备由下列

l         发送NETDEV_GOING_DOWN通知到netdev_chain通知链以通知对设备禁止有兴趣的内核组件。

l         调用dev_deactivate函数禁止出口队列规则,这样确保设备不再用于传输,并停止不再需要的监控定时器。

l         清除dev->state标志的__LINK_STATE_START标志位,标记设备卸载。

l         如果轮询动作被调度在读设备入队列数据包,则等待此动作完成。这是由于__LINK_STATE_START标志位被清除,不再接受其它轮询在设备上调度,但在标志被清除前已有一个轮询正被调度,关于接受轮询的细节请参考第10章。

l         如果dev->stop指针不空则调用它,并非所有的设备驱动都初始化此函数指针。

l         清除dev->flagsIFF_UP标志位标识设备关闭。

l         发送NETDEV_DOWN通知给netdev_chain通知链,通知对设备禁止感兴趣的内核组件。

 

8.12、更新设备排队规则状态

       我们在“排队规则状态”一节中看到,设置dev->state标志位以定义设备排队规则。本节中我们将会看到其中的两个标志位是如何用于处理电源管理和连接状态变化的。

 

8.12.1 、与电源管理交互

       若内核支持电源管理,在系统进入挂起模式或被唤醒时,网卡驱动程序能够获得通知。我们在第6章的“PCI网卡驱动注册实例”一节中看到,pci_driver 结构的suspend resume函数指针根据内核是否支持电源管理来初始化。例如:下面显示了drivers/net/ 3c 59x.c设备驱动如何初始化pci_driver实例:

static struct pci_driver vortex_driver = {

    .name        " 3c 59x",

    .probe        vortex_init_one,

    .remove        _ _devexit_p(vortex_remove_one),

    .id_table    vortex_pci_tbl,

#ifdef CONFIG_PM

    .suspend    vortex_suspend,

    .resume        vortex_resume,

#endif

};

当系统进入挂起模式,设备驱动提供的suspend函数就被执行,以使设备驱动执行挂起操作。电源管理状态改变并不影响注册状态dev->reg_state。但是,设备状态dev->state必须改变。

 

8.12.1 .1挂起设备

       当设备挂起时,其设备驱动处理此事件,例如,通过调用PCI设备的pci_driversuspend函数。此外,每种设备驱动都必须提供驱动程序规范规定的功能和其它一些额外的功能。

l         清除dev->state__LINK_STATE_PRESENT标志位,因为设备临时不能够操作。

l         如果设备用netif_stop_queue[*]使能或禁止其入队列,以防止设备用于发送任何其它的包。注意:已注册的设备不需要使能:当设备被验证后,它获取内核指派的设备驱动并注册;但是,设备直到有明确的用户配置请求时,才使能(因此可用)

[*]用于启动、停止、重启入队列的函数的细节请参考第11

这些功能由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);

    }

}

 

8.12.1 .2、唤醒设备

       当设备唤醒时,其设备驱动处理此事件,例如,通过调用PCI设备的pci_driverresume函数。也有几个功能是所以设备驱动都得提供得:

l         设置dev->state__LINK_STATE_PRESENT标志位,因为设备现在可用了。

l         如果设备在挂起前被使能,用netif_wake_queue函数重新使能其入口队列,并由流量控制重新开启其监视定时器(参考第11章的“监视定时器”一节).

这些功能由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);

    }

}

 

8.12.2 、侦测连接状态改变

       当网卡侦测到信号的出现或未出现时,要麽因为它被网卡通知,要麽它由网卡上的读配置注册程序明确检测到,它能够同netif_carrier_on netif_carrier_off分别通知内核。这些函数在传递状态有变化时被调用,因此,不适当的调用时他们不作任何事情。

    下面是几个常见的导致连接状态改变的的情况:

l         从网卡拔出或插入电缆

l         电缆另一端的设备关闭或禁止。这些设备如hub、网桥、路由器、pc机网卡。

当设备驱动侦测到在其设备上传递信号时,它调用netif_carrier_on函数。函数做如下事情:

l         清除dev->state__LINK_STATE_NOCARRIER标志位

l         生成一个连接状态改变事件并提交给linkwatch_fire_event处理。参考“调度处理连接状态改变事件”小节。

l         如果设备被使能,则启动一个监视定时器。定时器由流量控制使用,以侦测是否发送失败或获得一个时间戳(这种情况定时器超时)。参考第11章的“监视定时器”一节

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函数。函数做如下事情:

l         设置dev->state__LINK_STATE_NOCARRIER标志位

l         生成一个连接状态改变事件并提交给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);

}

 

8.12.2 .1、调度处理连接状态改变事件

       连接状态改变事件在lw_event结构中定义,此结构相当简单:它仅包含一个关联到net_device结构的指针和另一个用于将结构连接到未决连接状态改变事件全局队列的字段lweventlist,这个链表由lweventlist_lock锁保护。

       注意,lw_event结构并不包括任何区分信号传递的检测与丢失的参数。这是因为没有必要区别,内核需要知道的就是在连接状态中有一个变化,因此,对设备的引用旧足够了。对任何设备而言,在lweventlist链表中,绝没有超过一个的lw_event实例,因为没有理由去记录一个变化的历史轨迹:要麽连接可操作,要麽不可以;因此连接状态要麽打开要麽关闭。两次状态改变等于没有变化,三次变化等价于一次改变,等等;因此,当设备已有未决连接状态改变事件时,新事件不入队列。这种情况可以通过检查dev->state__LINK_STATE_LINKWATCH_PENDING标志来检测,如图8-7所示:

 

8-7 linkwatch_fire_event函数

 

Understanding Linux Network Internals 第八章 翻译稿:设备注册和初始化_第2张图片

 

一旦lw_event数据结果被初始化引用一个合适的net_device实例,它旧被加到lweventlist链表,然后dev->state__LINK_STATE_LINKWATCH_PENDING标志位被设置,linkwatch_fire_event必须启动实际处理lweventlist链表元素的函数,此函数linkwatch_event不被直接调用,它通过给内核线程keventd_wq提交一个请求来被调度执行:一个work_struct数据结构被初始化以引用linkwatch_event 函数并被提交给keventd_wq

为了避免处理函数linkwatch_event运行太频繁,他的执行几乎被限制为每秒一次。linkwatch_event处理lweventlist(包含linkwatch_run_queue)中的元素。在“”一节中描述的rtnl锁的保护下,处理lw_event由以下几步组成:

l         清除dev->state__LINK_STATE_LINKWATCH_PENDING标志位

l         发送NEtdEV_CHANGE通知给netdev_chain通知链。

l         发送RTM_NEWLINK通知给RTMGRP_LINK RTnetlink组。参考“Rtnetlink链接通知”小节

这两个通知仅当设备使能时()dev->flags & IFF_UPnetdev_state_change发送:没有

任何禁止的设备关心连接状态的改变。

 

8.12.2 .2Linkwatch标志

       net/core/linkwatch.c中定义了两个用于设置全局变量linkwatch_flags标志:

LW_RUNNING

       当这个标志被设置时,linkwatch_event被调度执行,此标志由linkwatch_event自己清除。

LW_SE_USED

       由于lweventlist通常有不止一个的元素,代码优化静态分配的lw_event数据结构并总是用它作为第一个元素。仅当内核需要明了不止一个的未决事件(事件在不止一个设备),为它分配额外的lw_event结构;否则,它简单的重用同一个结构。

 

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

       不同的工具可用于配置或获取网络设备的媒介状态和硬件参数。它们有:

l         Ifconfigmii-tool,它们来自于net-tools工具包

l         Ethtool,来自于ethtool

l         ip link,来自于IPROUTE2

你可以参考man手册获取这些命令的语法类型细节。“Ethtool”小节描述了Ethtool和内核间的接

口,“媒体独立接口(MII: Media Independent Interface)”小节介绍了mii-tool和内核间的接口,随后一节则回到第三层配置命令ifconfig ip

       8-8是我们本节要说的高级的概览,图并没有展示锁的细节,它是足够说dev_ethtool和调用dev->do_ioctl都被路由Netlink保护(参考“”小节).

 

8.13.1 Ethtool

       本节给了一个ethtoolmii-tool以及net_device中的do_ioctl函数指针之间的关系的概览。net_device数据结构包括指向ethtool_ops类型的vft。后面的数据结构是一组函数指针集合,它们能够用于读或初始化net_device结构的一些参数域或触发一些事件(如重新启动自动流通).

       并非所有的设备驱动程序当前都支持这个特性,即使支持它也并不会支持所有的函数。dev->ethtool_ops的初始化通常在本章开始介绍的probe函数中实现。

    用户空间和函数的接口是就有的ioctl系统调用,图8-8表明了用户空间命令ethtool如何结束调用内核端的dev_ethtool。图中也展示了dev_ethtool框架,以及函数接口如何和媒体独立接口(MII: Media Independent Interface)内核库交互。我们将再最后一小节“媒体独立接口(MII: Media Independent Interface)”讲述此点。

       关于内核如何分发ioctl控制命令给正确的处理函数我们没有讲述更多的细节,我们将说说请求是如何传递给inet_ioctl的,它调用dev_ioctl,而dev_ioctl以调用dev_ethtool结束(你可以浏览一下代码并看下它是如何一步一步工作的,代码相当清晰)

8-8 设备配置ioctl接口

dev_ethtool必须运行在Netlink路由锁下(参考“”一节),函数以几个健康检查开始,然后,基于ifreq数据结构提供的来自与用户空间的命令类型,它调用适当的处理函数ethtool_xxx,它们由包裹了dev->ethtool_ops->xxx虚函数组成。因为支持ethtool的驱动程序不需要支持所有的ethtool_ops函数,则辅助函数返回-EOPNOTSUPP(操作不支持),这在图8-9中没有显示。

       注意,dev_ethtool也会在执行ethtool_xxx函数前后分别调用ethtool_opsbegin函数和complete函数。但是,这些函数都是可选的,因此,仅在驱动程序提供它时才被调用。并非所有的驱动程序到提供它,并且不同的驱动程序也不可能只用其中一个。有些PCI网卡驱动程序在给网卡发送命令之前用它们打开网卡(如果网卡是关闭的),之后再关闭网卡。

    ethtool_xxx帮助函数框架相当简单:把数据从用户空间移到内核空间(或者以“get”命令反向操作),然后调用某给ethtool_ops函数。  

 

8.13.1 .1、不支持Ethtool的驱动程序

       对于不支持Ethtool的驱动程序,当dev_ethtool被调用去处理一个命令时,它试着让驱动程序调用dev->do_ioctl函数处理此命令。可能驱动程序也不支持后者,在这种情况下,dev->do_ioctl返回-EOPNOTSUPP

    do_ioctl也有可能采用回调dev_ethtool(如图8-8中的虚线所示):例如,虚拟设备就是这样做的,它简单的让与实际设备关联的设备驱动处理这些命令(参考net/8021q/vlan_dev.c中的vlan_dev_ioctl函数)

 

8.13.2 、媒体独立接口(MII: Media Independent Interface)

       MII是一个IEEE标准规范,它描述了网络控制芯片和物理介质芯片之间的接口。用户能够使用这些接口,诸如:使能、禁止、协商。并非所有网卡都提供此接口。

       Linux下用于和MII交互的最常见的工具是mii-tools。象ethtool,其和内核交互也是通过ioctl,如图8-8所示。内核提供了一组ioctl命令处理MII,这些命令主要由基于特定网卡注册器的读写函数组成。

       如图8-8所示,ioctl命令被设备驱动传给dev->do_ioctl函数,这个函数能够以如下两种方式处理命令:

l        仅识别三个MII ioctl命令并调用设备驱动代码处理它们。这是最常见的情况。

l         依赖内核MII库驱动程序(drivers/net/mii.c),用generic_mii_ioctl处理输入命令

尤其是虚拟设备,有可能用dev->do_ioctl函数去识别和处理除MII命令之外的其它命令。

下面是这些驱动程序的dev->do_ioclt函数的命令模型,它们依赖内核MII库并没有实现特别的命令:

 

if (!netif_running(dev)) {

    return -EINVAL;

}

<lock private data structure>

err = generic_mii_ioctl(...);

<unlock private data structure>

return err;

 

       注意,在8-8中,ethtool命令可能结束调用一个MII内核库函数(如重启动协商)

 

8.14、虚拟设备

       在第五章的“虚拟设备”一节中,我们看到了虚拟设备与真实设备初始化的不同指出。至于我们关心的注册环节,虚拟设备和真实设备一样,需要注册、使能后方可使用。但是,它们也有不同之处:

l         虚拟设备有时调用register_netdevice unregister_netdevice函数而不是它们的包裹函数,并且自己处理锁问题。它们处理锁问题时需要保持锁的时间比真实设备长一些。这种情况,锁可能被误用且需要持有更长的时间,以保护可以用其它方式保护的额外的代码段。

l         真实设备不能够被用户命令注销或销毁,它们仅仅能够比禁止。真实设备只在设备驱动程序卸载时被注销。与此相对,虚拟设备能够被用户命令创建或注销,而尽管它可能依赖虚拟设备驱动程序的设计。

我们也在“register_netdevice函数”和“设备注销”小节中看到,虚拟设备不像绝大多数真实设

备,它使用dev->init, dev->uninit dev->destructor。因为绝大多数虚拟设备都在真实设备基础上实现某些或多或少复杂的逻辑,它们用dev->init dev->uninit处理各种初始化和清除任务。dev->destructor经常被free_netdev(8-4所示)初始化,以便驱动程序不必在注销时严格调用后者。

    我们在“设备初始化”小节中看到net_device结构的初始化是如何被分隔在设备驱动程序的probe函数和通用的setup函数中实现的。由于虚拟设备没有probe函数,则表8-28-3都不适合于它。

    虚拟设备注册到“设备注册状态通知”小节介绍的neTDev_chain通知链中,因为绝大多数虚拟设备是基于真实设备之上,因此真实设备的变化也会影响到虚拟设备。我们看如下两个例子:

l         胶合:

胶合是指一个虚拟设备允许你绑定一组接口并使它们看起来象一个单独设备。通

过不同的算法将流量分散到这些接口,其中每个都是简单的循环。我们看看图8-9(a)的例子,当eth0关闭时,胶合接口bond0在真实设备间分发流量需要知道并重视这个情况。万一eth1此时也关闭了,bond0将不得不被禁止,因为此时没有任何工作着的真实设备。

l         VLAN接口:

Linux支持802.1Q协议,并运行你定义虚拟局域网(VLAN)接口。来看图8-9(b)的例子,其中用户可以在eth0定义两个VLAN接口。当eth0关闭后,所有VLAN接口都必须关闭。

 

8-9. a) 胶合接口   b) VLAN接口

 

 

8.15、锁

       我们在net_device结构的组织小节中看到,dev_base链表和两个哈希表dev_name_head dev_name_indexdev_base_list锁保护。但是,这个锁仅用于串行访问链表和哈希表,不能够串行改变net_device数据结构的内容。net_device内容由路由netlink旗语rtnl_sem改变,它通过rtnl_lockrtnl_umlock分别获取和释放,[*] 这个旗语通过下述情况来串行改变net_device实例:

    也有其它的函数可用于获取和释放旗语,详情请参考include/linux/rtnetlink.h

l         运行时事件:

       例如,当连接状态改变时(如:网线插上或拔出),内核需要通过修改dev->flags以改变设备状态。

l         配置变化:

当用户通过net-tools工具包的ifconfig route命令以及IPROUTE2工具包的ip命令改变设备配

置时,这些命令通过ioctlnetlink套接字接口分别通知内核。由这些接口调用的函数必须采用锁机制:

       net_device数据结构包含一下几个锁字段:

ingress_lock

queue_lock

       用于分别实现出口和入口流量计划时的流量控制

xmit_lock

xmit_lock_owner

       用于同步访问设备驱动函数hard_start_xmit

更多锁的细节请参考第11章。

8.16、用/proc文件系统配置参数

       /proc文件系统中没有相关设备注册与注销的策略。

8.17、本章相关函数和变量

       8-4 总结了本章介绍的函数、数据结构和变量

 

8-4 本章介绍的函数、数据结构和变量

 

名称

描述

函数

 

alloc_netdev alloc_

分配net_device数据结构空间并初始化部分字段

xxxdev wrappers

free_netdev

释放net_device数据结构

dev_alloc_name

分配一个设备名

register_netdevice

注册和注销网络设备

xxx_neTDev函数封装了xxx_neTDevice函数

register_netdev

unregister_netdevice

unregister_netdev

xxx_setup

协助初始化net_device数据结构的部分字段,这是最常用的接口类型之一

dev_hold

增加和减少net_device数据结构的引用计数

dev_put

netif_carrier_on

当侦测、丢失或读取设备上的信号时分别被调用

netif_carrier_off

netif_carrier_ok

netif_device_attach

当设备被插入或拔出系统时分别被调用,

当系统挂起或唤醒时也会被调用

netif_device_detach

netif_start_queue

在启动、停止或检测设备出队列时被分别调用

netif_stop_queue

netif_queue_stopped

dev_ethtool

处理来自于ethtool工具包的用户空间调用ioctl函数的命令

变量

 

dev_base

dev_base是普通的已注册的网络设备队列

dev_xxx_headnet_device结构的两个哈希表,通过设备名和设备id索引

上述队列或哈希表由dev_base_lock锁保护

dev_name_head

dev_index_head

dev_base_lock

lweventlist

Lweventlist未决的lw_event事件队列.lweventlist_lock保护

lweventlist_lock

数据结构

 

lw_event

连接状态改变事件

 

8.18、本节相关文件和目录

       8-10展示了本章所涉及的文件和目录在内核源代码树中的位置:

8-10  本章的文件和目录结构

 

 

 

你可能感兴趣的:(Understanding Linux Network Internals 第八章 翻译稿:设备注册和初始化)