相对于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就是一个设备数据库,它管理当前系统中所有的设备,你可以以多种灵活的方式去查询这些设备,可以获取指定设备的特性,可以注册设备变化事件。