[转的]udev与硬件抽象层HAL的实现原理

udev与硬件抽象层HAL的实现原理 

相对于 linux 来说, udev 还是一个新事物。然而,尽管它 03 年才 出现,尽管它很低调 ( J ) ,但它无疑已经成为 linux 下 不可或缺的组件了。 udev 是什么?它是如何实现的?最近研究 Linux 设备管理时,花了一些时间去研究 udev 的实现。

 

udev 是什么? u 是指 user space dev 是指 device udev 是用户空间的设备驱动程序 吗?最初我也这样认为,调试内核空间的程序要比调试用户空间的程序复杂得多,内核空间的程序的 BUG 所 引起的后果也严重得多, device driver 是内核空间中所占比较最大的代码,如果把这 些 device driver 中硬件无关的代码,从内核空间移动到用户空间,自然是一个不错的想法。

 

但我的想法并不正确, udev 的 文档是这样说的,

1.         dynamic replacement for /dev 。作为 devfs 的替代者,传统的 devfs 不能动态分配 major minor 的值,而 major minor 非常有限,很快就会用完了。 udev 能 够像 DHCP 动态分配 IP 地址 一样去动态分配 major minor

 

2.         device naming 。 提供设备命名持久化的机制。传统设备命名方式不具直观性,像 /dev/hda1 这样的名字肯定 没有 boot_disk 这样的名字直观。 udev 能 够像 DNS 解析域名一样去给设备指定一个有意义的名称。

 

3.         API to access info about current system devices 。提供了一组易用的 API 去 操作 sysfs ,避免重复实现同样的代码,这没有什么好说的。

 

我们知道,用户空间的程序与设备通信的方法,主要有以下几种方式,

1.         通过 ioperm 获取操作 IO 端口的权限,然后用 inb/inw/ inl/ outb/outw/outl 等函数,避开设备驱动程序,直接去操作 IO 端 口。(没有用过)

2.         ioctl 函数去操作 /dev 目录下对应的设备,这是设备驱动程序提供的接口。像键盘、鼠标和触摸屏等输入设备一般都是这样做的。

3.         write/read/mmap 去 操作 /dev 目录下对应的设备,这也是设备驱动程序提供的接口。像 framebuffer 等都是这样做的。

 

上面的方法在大多数情况下,都可以正常工作,但是对于热插拨 (hotplug) 的设备,比如像 U 盘,就 有点困难了,因为你不知道:什么时候设备插上了,什么时候设备拔掉了。这就是所谓的 hotplug 问题了。

 

处理 hotplug 传统的方法是,在内 核中执行一个称为 hotplug 的程序,相关参数通过环境变量传递过来,再由 hotplug 通知其它关注 hotplug 事件的应用程序。这样做不但效率低下,而且感觉也不那么优雅。新的方法是采用 NETLINK 实现的,这是一种特殊类型的 socket ,专门用于内核空间与用户空间的异步通信。下面的这个简单的例子,可以监听来自内核 hotplug 的事件。

#include < stdio .h>

#include <stdlib.h>

#include < string .h>

#include < ctype .h>

#include <sys/un.h>

#include <sys/ioctl.h>

#include <sys/ socket .h>

#include <linux/types.h>

#include <linux/netlink.h>

#include < errno .h>

 

static int init_hotplug_sock ( void )

{

    struct sockaddr_nl snl ;

    const int buffersize = 16 * 1024 * 1024;

    int retval ;

 

    memset (& snl , 0x00, sizeof ( struct sockaddr_nl));

    snl .nl_family = AF_NETLINK;

    snl .nl_pid = getpid ();

    snl .nl_groups = 1;

 

    int hotplug_sock = socket (PF_NETLINK, SOCK_DGRAM , NETLINK_KOBJECT_UEVENT);

    if ( hotplug_sock == -1) {

        printf ( "error getting socket: %s" , strerror ( errno ));

        return -1;

    }

 

    /* set receive buffersize */

    setsockopt ( hotplug_sock , SOL_SOCKET , SO_RCVBUFFORCE, & buffersize , sizeof ( buffersize ));

 

    retval = bind ( hotplug_sock , ( struct sockaddr *) & snl , sizeof ( struct sockaddr_nl));

    if ( retval < 0) {

        printf ( "bind failed: %s" , strerror ( errno ));

        close ( hotplug_sock );

        hotplug_sock = -1;

        return -1;

    }

 

    return hotplug_sock ;

}

 

#define UEVENT_BUFFER_SIZE       2048

 

int main ( int argc , char * argv [])

{

         int hotplug_sock        = init_hotplug_sock ();

        

         while (1)

         {

                   char buf [ UEVENT_BUFFER_SIZE *2] = {0};

                   recv ( hotplug_sock , & buf , sizeof ( buf ), 0); 

                   printf ( "%s/n" , buf );

         }

 

         return 0;

}

 

编译:

gcc -g hotplug.c -o hotplug_monitor

 

运行后插 / U 盘,可以看到:

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/usbdev2.2_ep00

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0

add@/class/scsi_host/host2

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep81

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep02

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep83

add@/class/usb_device/usbdev2.2

add@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/host2/target2:0:0/2:0:0:0

add@/class/scsi_disk/2:0:0:0

add@/block/sda

add@/block/sda/sda1

add@/class/scsi_device/2:0:0:0

add@/class/scsi_generic/sg0

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep81

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep02

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/usbdev2.2_ep83

remove@/class/scsi_generic/sg0

remove@/class/scsi_device/2:0:0:0

remove@/class/scsi_disk/2:0:0:0

remove@/block/sda/sda1

remove@/block/sda

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0/host2/target2:0:0/2:0:0:0

remove@/class/scsi_host/host2

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/2-1:1.0

remove@/class/usb_device/usbdev2.2

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1/usbdev2.2_ep00

remove@/devices/pci0000:00/0000:00:1d.1/usb2/2-1

 

udev 的主体部分在 udevd.c 文件中,它主要监控来自 4 个文件描述符的事件 / 消息, 并做出处理:

1.         来自客户端的控制消息。这通常由 udevcontrol 命令通过地址为 /org/kernel/udev/udevd 的本地 socket ,向 udevd 发送的控制消息。其中消息类型有:

l         UDEVD_CTRL_STOP_EXEC_QUEUE 停止处理消息队列。

l         UDEVD_CTRL_START_EXEC_QUEUE 开始处理消息队列。

l         UDEVD_CTRL_SET_LOG_LEVEL 设置 LOG 的级别。

l         UDEVD_CTRL_SET_MAX_CHILDS 设置最大子进程数限制。好像没有用。

l         UDEVD_CTRL_SET_MAX_CHILDS_RUNNING 设置最大运行子进程数限制 ( 遍历 proc 目录下所有进程,根据 session 的值判断 )

l         UDEVD_CTRL_RELOAD_RULES 重新加载配置文件。

2.         来自内核的 hotplug 事件。如果有事件来源于 hotplug ,它读取该事件,创建一个 udevd_uevent_msg 对象,记录当前的消息序列号,设置消息的状态为 EVENT_QUEUED, 然后并放入 running_list exec_list 两个队列中,稍 后再进行处理。

3.         来自 signal handler 中的 事件。 signal handler 是异步执行的,即使有 signal 产生,主进程的 select 并不会唤醒,为了唤醒主进程的 select ,它建立了一个管道,在 signal handler 中,向该管道写入长度为 1 个子节的数据,这样就可以唤醒主进程的 select 了。

4.         来自配置文件变化的事件。 udev 通 过文件系统 inotify 功能,监控其配置文件目录 /etc/udev/rules.d ,一旦该目录中文件有变化,它就重新加载配置文件。

 

其中最主要的事件,当然是来自内核的 hotplug 事件,如何处理这些事件是 udev 的关键。 udev 本 身并不知道如何处理这些事件,也没有必要知道,因为它只实现机制,而不实现策略。事件的处理是由配置文件决定的,这些配置文件即所谓的 rule

 

关于 rule 的编写方法可以参考《 writing_udev_rules 》, udev_rules.c 实现了对 规则的解析。

 

在规则中,可以让外部应用程序处理某个事件,这有两种方式,一种是直接执行命令,通常是让 modprobe 去加载驱动程序,或者让 mount 去 加载分区。另外一种是通过本地 socket 发送消息给某个应用程序。

 

udevd.c:udev_event_process 函数中,我们可以看到,如果 RUN 参 数以 ”socket:” 开头则认为是发到 socket ,否则认为是执行指定的程序。

 

下面的规则是执行指定程序:

60-pcmcia.rules:                RUN+="/sbin/modprobe pcmcia"

 

下面的规则是通过 socket 发送消息:

90-hal.rules:RUN+="socket:/org/freedesktop/hal/udev_event"

 

hal 正是我们下一步要关心的,接下来我会分析 HAL 的 实现原理。

 

HAL Hardware Abstraction Layer 的首字母缩写。我最早是在 Winnt 3.5 的帮助中知道这个名词的,对帮助文档中的说法我比较认同,所以一直对它抱有好感。不过 Windows 下的 HAL Linux 下的 HAL 两者所指并非相同之物:

 

Windows 下的 HAL 位于操作系统的最底层,直接操作物理硬 件,隔离与硬件相关的信息,为上层的操作系统和设备驱动程序提供一个统一的接口,起到对硬件的抽象作用。有了 HAL ,编写驱动程序就容易多了,因为 HAL 的 接口不但使用简单,而且具有更好的可移植性(没用过)。

 

Linux 下的 HAL :至于对 硬件的抽象, Linux 内核早就有类似机制,只不过没有专门的名称罢了。而 Linux HAL 指的并非这个,它不是位于操 作系统的最底层,直接操作硬件,相反,它位于操作系统和驱动程序之上,是一个运行在用户空间中服务程序。

 

我们知道, Linux 和 所有的 Unix 一样,习惯用文件来抽象设备,任何设备都是一个文件,比如 /dev/mouse 是鼠标的设备文件。这种方法看起来不错,每个设备都有统一的形式,但使用并不那么容易,设备文件名没 有什么规范,从简单的一个文件名,你无法得知它是什么设备,具有有什么特性。

 

结果形成这样的尴尬:有了设备和设备驱动程序,却不知道如何使用它。这些乱七八糟的设备文件,让设备 的管理和应用程序的开发都变得很麻烦,所以有必要提供一个硬件抽象层,来为上层应用程序提供一个统一的接口, Linux HAL 就这样应运而生了。

 

HAL 并不提供诸如拍照和刻录等之 类的功能,相反它只是告诉应用程序,系统中有哪些设备可用,以及这些设备的类型、特性和能力等。主要说来,它提供以下几项功能:

1.         获取指定类型的设备列表。

2.         获取 / 更改设备的属性值。

3.         获取设备具有的能力描述。

4.         设备插入 / 拔除 时,通知相关应用程序。

5.         设备属性或能力变化时,通知相关应用程序。

 

udev 创建 dev 下的文件结点,加载驱动程 序,让设备处于可用状态。而 HAL 则告诉应用程序,现在有哪些设备可用,这些设备的类型、特性和能力,让应用程序知道如何使用它们。

 

设备的属性管理是 HAL 最 重要任务之一,有的设备属性来源于实际的硬件,有的来源于设备信息文件 (/usr/share/hal/fdi/) ,有的来源其它配置信息 ( /usr/share/hwdata/) 。设备属性的都有标准的定义,这些属性定义是 HAL SPEC 的主要内容之一,可以参考 http://people.freedesktop.org/~david/hal-spec/hal-spec.html

 

HAL 作为一个后台服务程序运行,它的主体架构基于 MVC 的 模型,在 DBUS 的帮助下,实现了异步事件通知机制。 HAL 的 分层视图如下:

 

说明:

1.         实线箭头为主动调用,虚线箭头为事件上报。

 

2.         udev 通过 NetLink 注册内核的设备事件,当有设备插入 / 拔除 时, udev 就会收到通知,它会从事件中所带参数和 sysfs 中的信息,加载适当的驱动程序,创建 dev 下的结点,让设备处于可用的状态。

 

3.         udev 只是一个框架,它的行 为完全受它的规则所控制,这些规则存放在目录 /etc/udev/rules.d/ 中, 其中 90-hal.rules 是用来让 udev 把 设备插入 / 拔除的事件通过 socket socket:/org/freedesktop/hal/udev_event 转发给 HAL 的。

 

4.         HAL 挂在 socket:/org/freedesktop/hal/udev_event 上等待事件,有事件发生时就调用函数 hald_udev_data 处理,它先从事件中取出主要参数,创建一个 hotplug_event 对象,把它放入事件队列中,然后调用 hotplug_event_process_queue 处理事件。

 

5.         函数 hotplug_event_begin 负责具体事件的处理,它把全部事件分为四类,并分别处理 hotplug_event_begin_sysfs 处理普通设备事件, hotplug_event_begin_acpi 处理 ACPI 事件, hotplug_event_begin_apm 处理 APM 事件, hotplug_event_begin_pmu 处理 PMU 事件。要注意的是,后三者的 事件源并非源于 udev ,而是在 device_reprobe 时触发的 (osspec_device_reprobe/hotplug_reprobe_tree/hotplug_reprobe_generate_add_events/acpi_generate_add_hotplug_event)

 

6.         函数 hotplug_event_begin_sysfs 中,如果是插入设备,则创建一个设备对象,设置设备的属性,调用相关 callouts ,然后放入设备列表中,并触发 signal dbus 通知相关应用程序。如果是拔除设备,则调用相关 callouts ,然后从设备列表中删除,并触发 signal dbus 通知相关应用程序。

 

7.         应用程序可以主动调用 HAL 提 供的 DBUS 接口函数,这些函数在 libhal.h 中有定义。应用程序也可以注册 HAL signal ,当设备变化时, HAL 通 过 DBUS 上报事件给应用程序。

 

8.         callout HAL 一种扩展方式,它在设备插入 / 拔除时 执行。可以在设备信息文件中 (/usr/share/hal 目录 ) 指定。

 

9.         addon 也是 HAL 一种扩展方式,它与 callout 的不同之处在于 addon 往往是事件的触发者,而不是事件的消费者。 HAL 的 事件源主要源于 udev ,而 udev 源于 kernel hotplug ,然而有的设备如电 源设备、磁盘设备和特殊按键等,它们并不产生 hotplug 事件。 HAL 就得不到通知,怎么办呢, addon 就 是用于支持新事件源的扩展方式。比如 addon-acpi /proc/acpi/event 或 者 /var/run/acpid.socket 收到事件,然后转发成 HAL 事 件。 addon-storage 检测光盘或磁盘的状态,并设置设备的属性。 addon-keyboard 检测一些特殊按键,并触发相应事件。

 

access-check/ci-tracker/ck-tracker 负责权限的检查,里面提到的 PolicyKit/ConsoleKit 不是太熟悉,有时间再看看。

 

简单的说, HAL 就 是一个设备数据库,它管理当前系统中所有的设备,你可以以多种灵活的方式去查询这些设备,可以获取指定设备的特性,可以注册设备变化事件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值