热插拔

作者:Greg Kroah-Hartman

创建热插拔设备是为了解决许多用户的需求。在笔记本电脑上,PCMCIA 设备的设计允许用户在计算机仍在运行时更换卡。这使得人们可以在不关闭机器的情况下更换网络适配器、存储卡甚至磁盘驱动器。

这种成功促成了 USB 和 IEEE1394 (火线) 总线的创建。这些设计允许外围设备在任何时候连接和移除。它们的创建也是为了尝试将系统从 ISA 总线转移到完全即插即用类型的系统。

从操作系统的角度来看,热插拔设备存在许多问题。过去,操作系统只需要在启动时搜索连接到它的各种设备,一旦看到,设备就永远不会消失。从设备驱动程序的角度来看,它从不期望它试图控制的硬件会消失。但是对于热插拔设备,这一切都改变了。

现在,操作系统必须有一种机制来不断检测是否有新设备出现。这通常由特定于总线的管理器完成。该管理器处理扫描新设备和识别这种消失。它必须能够为新设备创建系统资源,并将控制权传递给特定的驱动程序。热插拔设备的设备驱动程序必须能够在硬件移除时优雅地恢复,并且能够在任何时候将自身绑定到新硬件。不仅内核需要知道设备何时被移除或添加,用户也应该在这种情况发生时得到通知。其他类型的内核事件,例如网络设备的创建或笔记本电脑插入扩展坞,对于用户来说也很有用。

本文介绍了 Linux 内核中用于支持 USB 和其他热插拔设备的新框架。它涵盖了过去 PCMCIA 实现如何加载其驱动程序以及该系统的问题。它介绍了当前加载 USB 和 PCI 驱动程序的方法,以及这个相同的框架如何轻松处理其他类型的用户配置问题。

过去

自 1995 年以来,Linux 就已经支持 PCMCIA。为了使 PCMCIA 核心能够在插入新设备时加载驱动程序,它有一个名为 cardmgr 的用户空间程序。当设备被插入或移除时,cardmgr 程序会收到来自内核 PCMCIA 核心的通知,并使用该信息来加载或卸载该卡的正确驱动程序。它使用位于 /etc/pcmcia/config 的配置文件来确定哪个驱动程序应该用于哪张卡。这个配置文件需要保持更新,以了解哪个驱动程序支持哪张卡或哪些卡范围,并且已经增长到超过 1,500 行。每当驱动程序作者添加对新设备的支持时,他们都必须修改两个不同的文件才能使设备正常工作。

随着 USB 核心代码的成熟,该小组意识到它也需要像 PCMCIA 系统这样的东西,以便能够在设备插入和移除时动态地加载和卸载驱动程序。该小组还注意到,由于 USB 和 PCMCIA 都需要这个系统,并且其他内核热插拔子系统也将使用这样的系统,因此通用的热插拔核心将很有用。David Brownell 发布了一个初始补丁到内核 (marc.theaimsgroup.com/?l=linux-usb-devel&m=96334011602320),使其能够调用名为 /sbin/hotplug 的用户空间程序。这个补丁最终被接受,并且其他子系统也被修改以利用它。

让计算机自行完成

所有 USB 和 PCI 设备都包含一个标识符,该标识符描述了它们支持的功能类型(例如 USB 音频或 USB 大容量存储设备),或者如果它们不支持类规范,则它们包含唯一的供应商和产品标识符。PCMCIA 设备也包含这些相同的标识符。

这些标识符为 PCI 和 USB 内核驱动程序所知,因为它们需要知道它们可以正常工作的设备类型。USB 和 PCI 内核驱动程序向内核注册它们支持的不同类型的设备列表。此列表用于确定哪个驱动程序将控制哪些设备。

内核通过设备总线核心代码(USB、火线、PCI 等)知道何时以及何种类型的设备插入或移除系统。它可以将此信息发送给用户。

将这三个部分结合在一起(设备告诉计算机它们是什么,驱动程序知道它们支持什么设备,内核知道正在发生什么)为我们提供了一个解决方案,让计算机在插入新设备时自动加载正确的驱动程序。

/sbin/hotplug

内核热插拔核心提供了一种方法,让内核通知用户空间发生了某些事情。需要选择 CONFIG_HOTPLUG 配置项才能启用此代码。当内核调用全局变量 hotplug_path 中列出的可执行文件时,会发生通知。当内核启动时,hotplug_path 被设置为 /sbin/hotplug,但用户可以在 /proc/sys/kernel/hotplug 修改该值来更改它。内核函数 call_usermodehelper() 执行 /sbin/hotplug。

截至内核 2.4.14,/sbin/hotplug 方法正在被 PCI、USB、IEEE1394 和网络核心子系统使用。随着时间的推移,更多的子系统将被转换为使用它。补丁可用于 PnP-BIOS(当笔记本电脑插入和移除扩展坞时通知)、热插拔 CPU、SCSI 和 IDE 内核子系统。预计这些补丁将在一段时间后合并到主内核中。

当调用 /sbin/hotplug 时,会根据刚刚发生的动作设置不同的环境变量。

PCI

PCI 设备使用以下参数调用 /sbin/hotplug

argv [0] = hotplug_path
argv [1] = "pci"
argv [2] = 0

并且系统环境设置为以下

HOME=/
PATH=/sbin:/bin:/usr/sbin:/usr/bin
PCI_CLASS=class_code
PCI_ID=vendor:device
PCI_SUBSYS_ID=subsystem_vendor:subsystem_device
PCI_SLOT_NAME=slot_name
ACTION=action
action 设置为 “add” 或 “remove”,具体取决于设备是插入还是从系统中移除。class_code、vendor、subsystem_vendor、subsystem_device 和 slot_name 环境设置表示 PCI 设备信息的数值。
USB

USB 设备使用以下参数调用 /sbin/hotplug

argv [0] = hotplug_path
argv [1] = "usb"
argv [2] = 0

并且系统环境设置为以下

HOME=/
PATH=/sbin:/bin:/usr/sbin:/usr/bin
ACTION=action
PRODUCT=idVendor/idProduct/bcdDevice
TYPE=device_class/device_subclass/device_protocol
action 设置为 “add” 或 “remove”,具体取决于设备是插入还是从系统中移除,并且 idVendor、idProduct、bcdDevice、device_class、device_subclass 和 device_protocol 用来自 USB 设备描述符的信息填充。

如果 USB 设备的 deviceClass 为 0,则环境变量 INTERFACE 设置为

INTERFACE=class/subclass/protocol

这是因为 USB 比 PCI 具有更复杂的设备配置模型。

如果 USB 子系统在启用 usbdevfs 文件系统的情况下编译,则还会设置以下环境变量

DEVFS=/proc/bus/usb
DEVICE=/proc/bus/usb/bus_number/device_number

其中 bus_number 和 device_number 设置为此特定 USB 设备分配的总线号和设备号。

网络

每当网络设备在网络子系统中注册或注销时,网络核心代码都会调用 /sbin/hotplug,并且当从网络核心调用时,/sbin/hotplug 使用以下参数调用

argv [0] = hotplug_path
argv [1] = "net"
argv [2] = 0

并且系统环境设置为以下

HOME=/
PATH=/sbin:/bin:/usr/sbin:/usr/bin
INTERFACE=interface
ACTION=action
action 设置为 “register” 或 “unregister”,具体取决于网络核心中发生的情况,interface 是刚刚应用动作的接口的名称。
CPU

热插拔 CPU 补丁(可在 sourceforge.net/projects/lhcs 获得)在 CPU 从系统中移除或添加到系统后调用 /sbin/hotplug,并且 /sbin/hotplug 使用以下参数调用

argv [0] = hotplug_path
argv [1] = "cpu"
argv [2] = 0

并且系统环境设置为以下

HOME=/
PATH=/sbin:/bin:/usr/sbin:/usr/bin
CPU=cpu_number
ACTION=action
action 设置为 “add” 或 “remove”,具体取决于 CPU 发生的情况,cpu_number 是刚刚应用动作的 CPU 的编号。
示例

如果您只想用 /sbin/hotplug 脚本控制少量设备,则该脚本可以非常简单。例如,如果您有一个 USB 鼠标,并希望在鼠标插入或移除时加载和卸载内核驱动程序,则位于 /sbin/hotplug 的以下脚本就足够了

#!/bin/sh
if [ "$1" = "usb" ]; then
    if [ "$INTERFACE" = "3/1/2" ]; then
        if [ "$ACTION" = "add" ]; then
            modprobe usbmouse
        else
            rmmod usbmouse
        fi
    fi
fi

或者,如果您想在将 USB HandSpring Visor 连接到计算机时自动运行 ColdSync (www.ooblick.com/software/coldsync),则位于 /sbin/hotplug 的以下脚本将很好地工作

#!/bin/sh
USER=gregkh
if [ "$1" = "usb" ]; then
    if [ "$PRODUCT" = "82d/100/0" ]; then
        if [ "$ACTION" = "add" ]; then
            modprobe visor
            su $USER - -c "/usr/bin/coldsync"
        else
            rmmod visor
        fi
    fi
fi
如果您想确保您的网络设备始终连接到正确的以太网卡,则由 Sukadev Bhattiprolu 贡献的以下 /sbin/hotplug 脚本可以做到这一点
#!/bin/sh
if [ "$1" = "network" ]; then
    if [ "$ACTION" = "register" ]; then
        nameif -r $INTERFACE -c /etc/mactab
    fi
fi
清单 1 显示了一个更复杂的示例,它可以自动加载和卸载三个不同 USB 设备的模块。

清单 1. 用于自动加载和卸载三个不同 USB 设备模块的脚本

自动化需求

之前的简单示例显示了被迫手动输入所有不同设备 ID、产品 ID 等的局限性,以便使 /sbin/hotplug 脚本与内核知道的所有不同设备保持同步。相反,内核本身最好以某种方式指定它支持的不同类型的设备,以便任何用户空间工具都可以读取它们。因此诞生了一个名为 MODULE_DEVICE_TABLE() 的宏,该宏被所有 USB 和 PCI 驱动程序使用。这个宏描述了每个特定驱动程序可以支持哪些设备。在编译时,构建过程从驱动程序中提取此信息并构建一个表。该表称为 modules.pcimap 和 modules.usbmap,分别用于所有 PCI 和 USB 设备,并且存在于 /lib/modules/内核版本/ 目录中。

例如,来自 drivers/net/eepro100.c 的以下代码片段

static struct pci_device_id eepro100_pci_tbl[]
__devinitdata = {
    { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82557,
      PCI_ANY_ID, PCI_ANY_ID, },
    { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82562ET,
      PCI_ANY_ID, PCI_ANY_ID, },
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL
          _82559ER, PCI_ANY_ID, PCI_ANY_ID,
},
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL
          _ID1029, PCI_ANY_ID, PCI_ANY_ID,
},
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL
          _ID1030, PCI_ANY_ID, PCI_ANY_ID,
},
        { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL
          _82801BA_7, PCI_ANY_ID, PCI_ANY_ID,
},
        { 0,}
    };
    MODULE_DEVICE_TABLE(pci, eepro100_pci_tbl);

导致以下行添加到 modules.pcimap 文件

eepro100 0x00008086 0x00001229 0xffffffff 0xffffffff
0x00000000 0x00000000 0x00000000
eepro100 0x00008086 0x00001031 0xffffffff 0xffffffff
0x00000000 0x00000000 0x00000000
eepro100 0x00008086 0x00001209 0xffffffff 0xffffffff
0x00000000 0x00000000 0x00000000
eepro100 0x00008086 0x00001029 0xffffffff 0xffffffff
0x00000000 0x00000000 0x00000000
eepro100 0x00008086 0x00001030 0xffffffff 0xffffffff
0x00000000 0x00000000 0x00000000
eepro100 0x00008086 0x00002449 0xffffffff 0xffffffff
0x00000000 0x00000000 0x00000000
如示例所示,PCI 设备可以通过传递给 /sbin/hotplug 程序的任何相同参数来指定。

USB 设备可以指定它只能接受特定设备,例如来自 drivers/usb/mdc800.c 的这个示例

static struct usb_device_id
  mdc800_table [] = {
   { USB_DEVICE(MDC800_VENDOR_ID, MDC800_PRODUCT_ID) },
   { } /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, mdc800_table);

这会导致以下行添加到 modules.usbmap 文件

mdc800 0x0003 0x055f 0xa800 0x0000 0x0000 0x00 0x00
0x00 0x00 0x00 0x00 0x00000000
或者它可以指定它接受任何与特定 USB 类代码匹配的设备,如来自 drivers/usb/printer.c 的这个示例
static struct usb_device_id usblp_ids [] = {
  { USB_INTERFACE_INFO(USB_CLASS_PRINTER, 1, 1) },
  { USB_INTERFACE_INFO(USB_CLASS_PRINTER, 1, 2) },
  { USB_INTERFACE_INFO(USB_CLASS_PRINTER, 1, 3) },
  { }    /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, usblp_ids);
这会导致以下行添加到 modules.usbmap 文件
printer 0x0380 0x0000 0x0000 0x0000 0x0000 0x00 0x00
0x00 0x07 0x01 0x01 0x00000000
printer 0x0380 0x0000 0x0000 0x0000 0x0000 0x00 0x00
0x00 0x07 0x01 0x02 0x00000000
printer 0x0380 0x0000 0x0000 0x0000 0x0000 0x00 0x00
0x00 0x07 0x01 0x03 0x00000000
同样,这些 USB 示例表明 modules.usbmap 文件中的信息与内核提供给 /sbin/hotplug 的信息相匹配,使 /sbin/hotplug 能够确定要加载哪个驱动程序,而无需像 PCMCIA 那样依赖于手动生成的表。
预处理器滥用

宏 MODULE_DEVICE_TABLE 自动创建两个变量。对于示例:MODULE_DEVICE_TABLE (usb, usblp_ids); 变量 __module_usb_device_size 和 __module_usb_device_table 被创建并分别放置到模块的只读数据段和初始化数据段中。变量 __module_usb_device_size 包含 struct usb_id 结构大小的值,__module_usb_device_table 指向 usblp_ids 结构。usblp_ids 变量是一个 usb_id 结构数组,列表末尾有一个终止 NULL 结构。

当 depmod 程序作为内核安装过程的一部分运行时,它会遍历每个模块,查找符号 __module_usb_device_size 是否存在于编译后的模块中。如果找到它,它会将 __module_usb_device_table 符号指向的数据复制到一个结构中,提取所有信息,并将其写入到位于模块根目录中的 modules.usbmap 文件。在创建 modules.pcimap 文件时,它也会执行相同的操作,同时查找 __module_pci_device_size。

通过将内核模块信息导出到文件 modules.usbmap 和 modules.pcimap,我们的 /sbin/hotplug 版本可以如清单 2 所示 [ftp.linuxjournal.com/pub/lj/listings/issue96/5604.tgz]。此示例仅测试 USB 产品 ID 和供应商 ID 的匹配项。Linux-Hotplug 项目创建了一组脚本,涵盖了所有可以调用 /sbin/hotplug 的不同子系统。这使得在将新设备插入系统时可以自动加载驱动程序。它还在看到网络设备时启动网络服务。这些脚本在 GPL 下发布,并且可以在 linux-hotplug.sourceforge.net 获得。几乎所有主要的 Linux 发行版目前都在发布此软件包,因此它可能已经安装在您的机器上。

未来

当前 /sbin/hotplug 子系统需要合并到其他内核系统中,因为它们开发了热插拔功能。SCSI、IDE 和其他系统都有可用于内核支持的热插拔补丁,但需要添加脚本支持、内核宏支持和 modutils depmod 支持,以便为用户提供一致的体验。

当内核启动并发现新设备时,它会尝试生成 /sbin/hotplug,但由于用户空间尚未初始化,因此无法运行。这意味着启动时需要的任何 USB 或 PCI 设备都需要编译到内核中,或者作为模块存在于 initrd RAM 磁盘映像中。在 2.5 开发过程中的某个时候,initrd RAM 磁盘映像将被转换为包含整个小型用户空间树。这将允许在启动过程中运行 /sbin/hotplug 并动态加载模块。一些描述此磁盘映像想法的链接是:lwn.net/2001/0712/kernel.php3 -- marc.theaimsgroup.com/?l=acpi4linux&m=99705696732868 -- marc.theaimsgroup.com/?l=linux-kernel&m=99436439232254marc.theaimsgroup.com/?l=linux-kernel&m=99436253707952

由于此 RAM 磁盘映像的空间需求很小,因此编写了 dietHotplug 程序。它是 Linux-Hotplug bash 脚本的 C 语言实现,并且在程序运行时不需要 modules.*map 文件。整个 dietHotplug 程序的可执行文件大小是原始 modules.*map 文件本身大小的五分之一。尺寸小是因为使用了 dietLibc(可在 www.fefe.de/dietlibc 找到)和其他节省空间的技术。dietHotplug 随着 2.5 内核要求的更加充分了解,将进行更多开发。dietHotplug 可以从 Linux-Hotplug 站点下载。

致谢

我要感谢 David Brownell,他编写了最初的 /sbin/hotplug 内核补丁和大部分 Linux Hotplug 脚本。如果没有他的坚持,Linux 就不会有这个用户友好的功能。我还要感谢整个 Linux USB 开发团队,他们在相对较短的时间内提供了一个可靠的内核子系统。

Keith Owens 在 depmod 实用程序中编写了支持代码,并且忍受了对 MODULE_DEVICE_TABLE() USB 结构格式的不断更改。

linux-hotplug-devel 邮件列表上的其他开发人员也值得表扬,他们为热插拔脚本提供了补丁和反馈,以及 Debian、Red Hat 和 Mandrake 提供的出色的 Linux 发行版特定支持。

本文基于我在 2001 年渥太华 Linux 研讨会上发表的论文和演讲。

Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 LInux 内核相关的工作,可以通过 greg@kroah.com 联系到他。

加载 Disqus 评论