Chinaunix首页 | 论坛 | 博客

分类: LINUX

2009-11-07 10:10:20

Linux 设备模型
新的Linux设备模型引入了类似C++的抽象从而抽出设备驱动的共同性到总线和核心层中. 先来看下组成设备模型的不同组件,如udev,sysfs,kobjects和device classes和他们对诸如/dev结点管理,热插拔,固件下载和模块自动加载的内核子系统的影响. udev是来查看设备模型好处的最有优势的切入点.

Udev

若干年前当Linux还很年轻时, 管理设备结点不太让人感到高兴. 所有需要的结点(可能会达到上千个)不得不在/dev目录下静态创建. 随着2.4内核的高级特性devfs的到来, 它引入了动态设备结点创建. devfs为内存中文件系统创建设备结点提供了方便服务,但是为结点命名的负担还是留给了设备驱动. 设备命名政策是可管理的并且不能跟内核混在一起,实施的地方是在头文件,内核模块参数或用户空间中.  udev的到来将设备管理推给了用户空间.

udev依赖这些完成它的使命:
1. 内核sysfs的支持,它是linux设备模型中很重要的一部分. sysfs是一个引导时(于/etc/fstab查看详情)挂载到/sys的内存文件系统.
2. 用户空间守护进程集和诸如udevd,udevinfo的工具集.
3. 位于/etc/udev/rules.d目录下用户指定的规则. 你可以设计规则得到你的设备的一致视点.

为理解怎样使用udev,我们来看一个例子. 假设你有一个USB DVD光驱和一个USB CD-RW光驱. 取决于你热插拔它们的顺序,其中一个指派命名为/dev/sr0,另外的名字是/dev/sr1. 在udev出现以前的日子里你得在使用他们之前构想好与之关联的名字,但使用udev你可以不用管它们插入和拔出的顺序来一致查看到DVD(前面的/dev/usbdvd)和CD-RW(前面的/dev/usbcdrw).

首先从sysfs对应的文件中提取出产品属性. 假设(Targus)DVD光驱已经指派给设备结点/dev/sr0, 给(Addonics)CD-RW光驱设备结点的名字是/dev/sr1. 使用udevinfo得到下列它们的结点信息。
接着,我们通过收集到的信息来识别设备并添加udev命名规则. 创建一个文件/etc/udev/rules.d/40-cdvd.rules并添加下列规则到其中:

BUS="usb", SYSFS{idProduct}="0701", SYSFS{idVendor}="05e3",
KERNEL="sr[0-9]*", NAME="%k", SYMLINK="usbdvd"

BUS="usb", SYSFS{idProduct}="0302", SYSFS{idVendor}="0dbf",
KERNEL="sr[0-9]*", NAME="%k", SYMLINK="usbcdrw"

第一条规则告诉udev是否用产品ID为0x0701, 发行商ID为0x05e3和名字以sr起始来查找USB设备, 它应该在/dev下创建一个相同名字的结点并产生一个名字是usbdvd的符号链接到该创建结点. 类似的第二个规则为CD-RW光驱创建一个名字为usbcdrw的符号链接.

要测试规则的语法错误,运行/sys/block/sr*下面的udevtest即可. 要启用/var/log/messages中的开发商信息,设置/etc/udev/udev.conf中的udev_log为yes. 要使用新添加的规则重新产生/dev目录,使用udevstart重启udev即可. 当这完成后你的DVD光驱以/dev/usbdvd一致呈现给系统,你的CD-RW光驱也以/dev/usbcdrw呈现. 可以在shell脚本中使用下面的命令挂载它们:
mount /dev/usbdvd /mnt/dvd

设备结点(和网络接口)的命名一致性并不是udev唯一的能力. 它已经变身到Linux热插拔管理器里面了.  udev也负责根据需要自动加载模块以及需要时下载微码到设备中. 在深入这些功能前让我们再看下其他的设备模型.


sysfs, kobjects和Device classes

sysfs,kobjects和device classes是设备模型的构建块,但是一直隐藏在幕后羞于示人. 它们处于总线和核心实现的使用域内, 并且隐藏内部为设备驱动提供服务的API.

sysfs是内核结构设备模型的用户空间展示. 和procfs相似都是包含内核数据结构信息的内存文件系统. 不过procfs是进入内核内部的通用窗口,而sysfs则是特定于设备模型的. 因此sysfs不是procfs的替代品. 诸如进程描述符和sysctl参数属于procfs而不是sysfs. udev的大多数扩展功能都依赖于sysfs.

kobjects引入了通常对象属性的封装,如引用计数用法.  它们通常嵌入到更大的结构中. 下面是kobjec的主要域,定义在include/linux/kobject.h:
1. kref对象负责引用计数管理. kref_init()接口初始化kref, kref_get()增加关联的kref引用计数, kref_put()减少引用计数并在没有引用时释放对象. 例如URB结构包含一个kref来追踪引用它的数目.
2. 指向kset的指针,它是一个kobject从属的对象集合.
3. 一个kobj_type, 它是描述kobject的对象类型.

kobject和sysfs紧密相连,每个内核中实例化的kobject都有一个对应sysfs展示.

device classes是设备模型的另外一个特性, 它是你在驱动中很有可能用到的一个接口. class接口是在一个更宽广设备类别(或分类)上的设备抽象观点. input分类下的USB鼠标, PS/2键盘和游戏摇杆在/sys/class/input/下都有自己的入口.

下面显示了在笔记本上sysfs的级联结构, 有一个外部USB鼠标连接. 顶层是bus,class和device目录伸展来表示sysfs提供了基于device类型的USB鼠标的一个视角. 鼠标是一个人输入分类的设备但物理上是一个回答两个结束点地址的USB设备:一个控制结束点ep00,一个是中断结束点ep81.  USB端口属于总线2上的USB主控制器, USB主控制器自身通过PCI总线桥接到CPU.
USB鼠标在sysfs中的级联结构:
[/sys]
     +[block]
     -[bus]—[usb]—[devices]—[usb2]—[2-2]—[2-2:1.0]-[usbendpoint:usbdev2.2-ep81]
     -[class]-[input]—[mouse2]—[device]—[bus]—[usbendpoint:usbdev2.2-ep81]
             -[usb_device]—[usbdev2.2]—[device]—[bus]
             -[usb_endpoint]—[usbdev2.2-ep00]—[device]
                            —[usbdev2.2-ep81]—[device]
     -[devices]—[pci0000:00]—[0000:00:1d:1]—[usb2]—[2-2]—[2-2:1.0]
     +[firmware]
     +[fs]
     +[kernel]
     +[module]
     +[power]

class编程接口构建于顶层kobject和sysfs, 所有是个挖掘理解设备模型之间端到端交互的好地方. 拿RTC驱动来说吧, RTC驱动(driver/char/rtc.c)是一个混杂驱动. 插入RTC驱动模块并查看/sys和/dev下面的结点:
bash> modprobe rtc
bash> ls -lR /sys/class/misc
drwr-xr-x 2 root root 0 Jan 15 01:23 rtc
/sys/class/misc/rtc:
total 0
-r--r--r-- 1 root root 4096 Jan 15 01:23 dev
--w------- 1 root root 4096 Jan 15 01:23 uevent
bash> ls -l /dev/rtc
crw-r--r-- 1 root root 10, 135 Jan 15 01:23 /dev/rtc

/sys/class/misc/rtc/dev包含指派给该设备的主次号码, /sys/class/misc/rtc/uevent用来冷插拔, /dev/rtc用于应用程序访问RTC驱动.

我们来顺着设备模型来理解代码, 杂项驱动在初始化过程中定制misc_register()服务, 剥除一些代码就是这样的:

 1 /* ... */
 2 dev = MKDEV(MISC_MAJOR, misc->minor);
 3
 4 misc->class = class_device_create(misc_class, NULL, dev,
 5                                   misc->dev,
 6                                   "%s", misc->name);
 7 if (IS_ERR(misc->class)) {
 8   err = PTR_ERR(misc->class);
 9   goto out;
10 }
11 /* ... */

下图是剥除一些层后得到的设备模型底层. 它解释了在class, kobject, sysfs和udev之间的转变关系,这导致/sys和/dev下面文件的产生.


另外一个设备模型抽象是总线设备驱动编程接口. 内核设备支持很明了的组织进了总线,设备和驱动中. 这让单个驱动实现更加简单和更加通用. 总线实现可以查找到处理一个特定设备的驱动.


想一下内核的I2C子系统. I2C层由总线适配器的一个核心基础设施和设备驱动以及客户端设备驱动组成。 I2C核心层使用bus_register()注册每个检测到的I2C总线适配器。当一个I2C客户端设备(比方说是一个EEPROM芯片)被检测到,就通过device_register()来记录它的存在. 最后I2C EEPROM客户端驱动使用driver_register()来注册它自己. 这些注册都是使用I2C核心提供的服务函数间接做到的.


device_register()在/sys/devices/下添加入口的同时, bus_register()也添加一个对应入口到/sys/bus/下. struct bus_type, struct device和struct device_driver是分别被总线,设备和驱动使用的主要数据结构. include/linux/device.h里面有它们的定义.

热插拔和冷插拔
向一个正在运行的系统连接设备称为热插拔,而在系统引导前连接设备则称为是冷插拔. 早先内核通过调用注册在/proc文件系统中的助手程序来为用户空间通知热插拔事件, 但当前内核检测热插拔后就通过netlink socket分发uevent给用户空间. netlink socket是内核空间与用户空间使用socket API通信的一个高效机制. 在用户空间结尾处,管理设备结点创建和移除的守护进程接收到uevent并管理热插拔.


udev也处理冷插拔,因为udev是用户空间的一部分而且仅仅在内核引导后才启动,所以需要一个特定机制来模仿冷插拔设备的冷插拔事件. 在引导时内核在sysfs中为所有设备都创建一个名为uevent的文件并释放冷插拔事件给这些文件. 当udev启动后就在/sys下读取所有这些uevent文件并为每个冷插拔设备产生冷插拔uevent.

微码下载
你得在某些设备使用前填充微码进去. 微码通过一个卡上微控制器执行. 设备驱动在头文件的静态数组中存储微码.但微码通常由发行商以二进制镜像形式分发变得不可维持, 而且还不能和GPL授权的内核代码混合在一起. 另一个阻止固件和内核代码混合的原因是它们运行在不同的发布时间线(time line). 很明显解决方法是分开在用户空间维护微码并在需要时传给内核. sysfs和udev提供了基础平台实现这个.

以很多笔记本上可以找到的Interl PRO/Wireless 2100 WiFI迷你PCI卡做例子. 该卡基于需要执行外部提供的微码的微码控制器构建. 我们来看下linux驱动遵循那些步骤来下载微码到卡上. 假设你已经从
获取到了微码镜像并保存在系统上/lib/firmware/下面, 接着你加载了驱动模块ipw2100.ko:
1. 在初始化期间,驱动调用下面的:
request_firmware(.., "ipw2100-1.3.fw", ..);
2. 分发一个热插拔uevent给用户空间, 伴以请求的微码镜像的识别号.
3. udevd接受到uevent并调用/sbin/firmware_helper做出回应. 为了这个,它使用了/etc/udev/rules.d/下一个文件中的类似于下面的一个规则:
ACTION=="add", SUBSYSTEM=="firmware", RUN="/sbin/firmware_helper"
4. /sbin/firmware_helper查看/lib/firmware/下面内容并找到请求的微码镜像ipw2100-1.3.fw. 复制(dump)镜像到/sys/class/0000:02:02.0/data. (0000:02:02是这种请求下WiFI卡的PCI bus:device:function识别号.)
5. 驱动接受到微码并下载到设备中. 完成后就调用release_firmware()来释放相应数据结构.
6. 驱动进行剩下的初始化操作和启动WiFI适配器.

自动加载模块
按需自动加载内核模块是Linux支持的一个很方便特性. 为理解内核如何释放一个"模块错误"信号以及udev如何处理它, 现以插入一个Xirom CardBus以太网卡到笔记本的PC卡插槽举例:
1. 在编译时产生支持设备的身份信息作为驱动模块对象的一部分. 查看支持Xircom CardBus以太网卡的驱动代码(drivers/net/tulip/xircom_cb.c)找到下面片段:

1 static struct pci_device_id xircom_pci_table[] = {
2     {0x115D, 0x0003, PCI_ANY_ID, PCI_ANY_ID,},
3     {0,},
4 };
5
6 /* Mark the device table */
7 MODULE_DEVICE_TABLE(pci, xircom_pci_table);
这声明了驱动可以支持任何有PCI发行商ID为0x115D和PCI设备ID为0x0003的卡. 当安装驱动模块时,depmod工具就在内部查找模块镜像并读取出设备表里面的ID, 接着添加下面入口到/lib/modules/kernel-version/modules.alias:
alias pci:v0000115Dd00000003sv*sd*bc*sc*i* xircom_cb
v代表发行商ID, d代表设备ID, sv代表次发行商ID, *代表匹配所有.
2. 当热插拔Xircom卡到CardBus插槽时, 内核产生一个声明新插入设备的身份信息的uevent. 可以使用udevmonitor查看产生的uevent:
bash> udevmonitor --env
   ...
   MODALIAS=pci:v0000115Dd00000003sv0000115Dsd00001181bc02sc00i00
   ...

3.  udevd通过netlink socket接收uevent并调用带有上面的由内核传给它的MODALIAS的modprobe:
modprobe pci:v0000115Dd00000003sv0000115Dsd00001181bc02sc00i00
4. modprobe在第一步中创建的/lib/modules/kernel-version/modules.alias中查到匹配的入口,插入xircom_cb模块:
bash> lsmod
Module      Size   Used by
xircom_cb   10433  0
...
现在卡准备就绪可以工作了.

嵌入式系统中的udev
嵌入式设备中放弃使用udev而垂青静态创建设备结点的原因基于以下几点原因:
1. 相比较于在软件安装时结点只静态创建一次, udev在每次重启时都要创建/dev下面结点. 如果你的嵌入式设备使用flash存储, 持有/dev结点的flash页将因为udev而经受每次启动时的写擦除周期操作, 这会降低flash的生命期限. 但你还能选择挂载/dev到基于RAM的文件系统中.
2. udev会导致额外的引导时间.
3. udev的那些动态创建设备结点和自动加载模块的特性造成了一些方案设计者偏好避开特定嵌入式设备的不确定性, 尤其是不通过热插拔总线跟外部世界交互的那些设备. 根据这些观点, 静态结点创建和插入任何模块的引导时间为系统提供了更多控制, 使之更易于测试.

内存屏障
一些处理器和编译器为了达到优化执行速度而重新排序指令。 排序使得心的指令流和原来的在语义上等同。但是如果你对映射I/O设备寄存器到内存进行写操作,指令重排序将产生不能意料的副作用. 你可以在你的代码中插入屏障来防止处理器重排指令. wmb()函数可以防止越过(move through)写. rmb()为阻止跨越读提供了读屏障, mb()导致读写屏障.

除了前面提到的CPU和硬件之间的交互外, 内存屏障也和SMP系统中的CPU之间的交互相关. 如果CPU数据缓存在写回模式(数据在完全必要时才从缓存拷贝到内存)下操作, 你可能希望推迟指令流直到缓存到内存的队列满了. 当你面对需要锁或释放锁的指令时也是相关的. 在这些场景下使用屏障可以在多CPU之间得到一致感觉.

电源管理
电源管理对靠电池运行的设备如笔记本和手持设备很关键. linux驱动需要关切电池状态并在各种状态之间切换, 如独立工作, 休眠和低电池模式. 驱动在切换到耗电少的模式时可以利用底层硬件支持的省电支持特性, 例如当视频驱动空出显示时存储驱动就运转磁盘.

设备驱动的电源相关代码仅仅是整个电源管理框架中的一小部分. 两个受欢迎的电源管理机制是APM和ACPI(高级配置和电源接口). APM已经荒废了,ACPI已经合并进内核成为Linux系统中事实上的电源管理策略.

查看代码
核心的中断处理代码是通用的,位于kernel/irq/目录下. 架构相关的可以在arch/you-arch/kernel/irq.c中找到. 该文件中定义的do_IRQ()函数是引领你开始刺探内核中断处理机制的好地方.

内核软中断和小任务的实现位于kernel/softirq.c. 该文件也包含了另外的提供更多适中粒度控制的软中断和小任务函数. 查看include/linux/interrupt.h得到软中断向量枚举和用来实现自己的中断处理函数的原型. 书写真实的中断处理函数和后半部的例子是drivers/net/lib8390.c,顺着代码可以追踪到网络栈.

kobject的实现以及相关编程接口在lib/kobject.c和include/linux/kobject.h里面. 查看drivers/base/sys.c得到sysfs的实现. drivers/base/class.c有device class API. 通过netlink socket分发热插拔uevent在lib/kobject_uevent.c中做到的. 你可以从www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html下载udev源码和文档.

为了全面理解x86 Linux上APM如何实现的, 可以查看arch/x86/kernel/apm_32.c, include/linux/apm_bios.h和include/asm-x86/mach-default/apm.h. 如果你对没有BIOS的架构上如何实现APM好奇, 查看include/linux/apm-emulation.h和使用它的地方. 内核的ACPI实现位于drivers/acpi/.

阅读(1197) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~