【翻译】设备注册和初始化
第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。
每个设备都根据设备类型分配一个唯一名字,当同样类型的设备注册是,这个名字包含按序递增的数字。例如,以太网设备分配eth0、eth1等等。单个的设备由于注册设备的顺序不同也会被分配不同的名字。例如,如果你有两块网卡由不同的模块处理,设备名字则依赖两个模块加载的顺序;而热拔插设备明显地会导致自己的名字变化不可预知。
因为用户空间配置工具引用了内核分配的设备名称,所以设备注册顺序相当重要。由于这是用户空间细节,除非谈到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 设备驱动程序
诸如IRQ、I/O内存、I/O端口等参数的值依赖硬件配置,由设备驱动程序提供。参考第五章
l 设备类型
某个设备类型家族的所有设备都公用一些结构字段,这些字段的初始化由xxx_setup函数实现,例如:以太网设备采用ether_setup函数,参考“设备类型初始化:xxx_setup函数”小节。
l 特性
有些必需的和可选的属性也需要初始化,例如:网络接口排队算法(也就是QoS、qdisc(queuing discipline))由register_netdevice函数初始化,这在“register_netdevice函数”小节中描述。其它特征在关联模块被通知有新设备注册时初始化(参考“设备注册状态通知”小节)。
设备类型初始化是设备驱动程序初始化的一部分(也就是说,xxx_setup被xxx_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_setup和xxx_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结构的其它一些字段。