udev的实现原理


相对于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> 
  
staticintinit_hotplug_sock(void

    structsockaddr_nl snl
    constintbuffersize = 16 * 1024 * 1024; 
    intretval
  
    memset(&snl, 0x00, sizeof(struct sockaddr_nl)); 
    snl .nl_family = AF_NETLINK; 
    snl.nl_pid = getpid (); 
    snl .nl_groups = 1; 
  
    inthotplug_socksocket(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_sockSOL_SOCKET, SO_RCVBUFFORCE, &buffersizesizeof(buffersize )); 
  
    retvalbind(hotplug_sock, (structsockaddr*) &snlsizeof(struct sockaddr_nl)); 
    if(retval < 0) { 
        printf("bind failed: %s"strerror(errno )); 
        close(hotplug_sock ); 
        hotplug_sock = -1; 
        return -1; 
    } 
  
    returnhotplug_sock

  
#define UEVENT_BUFFER_SIZE       2048 
  
intmain(intargccharargv []) 

         inthotplug_sock       = init_hotplug_sock (); 
         
         while (1) 
         { 
                   charbuf[UEVENT_BUFFER_SIZE *2] = {0}; 
                   recv(hotplug_sock, &bufsizeof(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的实现原理。 

另外一篇见:http://hi.baidu.com/littertiger/blog/item/315d12dd8448fedc8c102918.html 
kernel和udev间传递消息 
kernel和udev间传递消息,比如add,remove等,是通过netlink进行。 netlink是个通用的机制,传递udev event只是其中一个应用。 
由于uevent是广播的,所以写个小程序很容易不会这些事件。 
写得比较匆忙,也很丑陋,呵呵。 
#include <stdio.h> 
#include <sys/socket.h> 
#include <linux/netlink.h> 

char buf[2048 + 512]; 
int main() 

    int bufsize = 16 * 1024 * 1024; 
    struct sockaddr_nl anl; 
    int res, i, sk; 

    anl.nl_family = AF_NETLINK; 
    anl.nl_pid = getpid(); 
    anl.nl_groups = 1; 

    sk = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); 
    if (sk == -1){ 
        printf("socket create failed\n"); 
        return sk; 
    } 
    
    setsockopt(sk, SOL_SOCKET, SO_RCVBUFFORCE, &bufsize, sizeof(bufsize)); 
    res = bind(sk, &anl, sizeof(anl)); 
    if (res == -1){ 
        close(sk); 
        printf("socket bind failed\n"); 
        return res; 
    } 

    while(1){ 
    res = recv(sk, buf, sizeof(buf), 0); 
    printf("res:%d\n",res); 
    res = res > sizeof(buf) ? sizeof(buf) : res; 
    printf("begin\n"); 
    for(i=0; i<res; i++) 
        putchar(buf
); 
    printf("end\n"); 
    } 
    return 0; 

}
 

另外: 
Udev的代码树里的版本很多,我下载的最新的版本是udev-117,配合2.6.21版本的内核能够正常使用。网上很多文章介绍的可能都是稍微早期一些的版本,有些步骤包括udev的README文档似乎描述的不是很准确。 
  
基本上这个版本的udev需要注意的是,安装时只需要udevd,udevadm两个文件,其它必需的包括udevtrigger等只是udevadm的一个符号链接。udevstart不是必需的。当然Udev.conf等配置文件还是一样。 


2.2        启动 
你可以在启动脚本中用udevd –d 参数启动udev文件系统的守护进程,然后使用udevtrigger将buildin的设备驱动的节点创建出来,以后模块插入移除时节点的管理会自动处理。 
  
能够正常加载udev的前提,基本包括如下操作: 
  
Ø       设置路径变量 
Ø       加载sysfs文件系统 
Ø       加载一个基于ram的可写的/dev目录(其实,只要提供一个可写的目录即可,目录路径本身也是可以配置的) 
Ø       /dev目录下需要有已经创建好的 console节点和null节点 
脚本类似: 
  
# Set the path 
PATH=/bin:/sbin:/usr/bin:/usr/sbin 
export PATH 
  
# mount proc and devpts filesystem 
/bin/mount -a 
mknod /dev/console c 5 1 
mknod /dev/null c 1 3 
/sbin/udevd -d 
/sbin/udevtrigger 
  
Mount使用的fstab文件类似: 
  
none                    /tmp                    ramfs   defaults        0 0 
udev                    /dev                    ramfs   defaults        0 0 
none                    /proc                   proc    defaults        0 0 
sysfs                   /sys                    sysfs   defaults        0 0 
  
当然,你的系统上可能还会需要预先创建一些其它的设备节点,比如串口的ttySx 才能正常启动shell,完成以上脚本的执行,那就要看具体情况了。 
  
3         使用中的一些问题的思考 
3.1        关于规则的多次匹配 
帮助文档中说一个设备可以被多条规则多次匹配,不过,需要明确的一点是: 
多次匹配只能添加多个Symlink,不能创建多个Name: 
  
例如: 
KERNEL=="mtdblock4", NAME+="mtdbb4" 
KERNEL=="mtdblock4", NAME+="%k" 
就只会创建 /dev/mtdbb4 而不会创建/dev/mtdblock4 
  
而类似: 
KERNEL=="mtdblock4", NAME+="mtdbb4" 
KERNEL=="mtdblock4", SYMLINK+="mtdbb4link" 
是可以正常工作的。 
  
3.2        关于udev.conf的语法 
可能大家会发现,似乎没有什么详细文档描述udev.conf的写法,实际上从udevd的代码里可以看出: 
  
udev.conf文件里面只会解析这三个参数: 
  
udev_root 定义udev的目录路径 
udev_rules 定义udev的规则文件的目录路径 
udev_log 定义log的级别 
  
也许以后会添加一些别的配置参数? 
  
4         基本工作原理方面的问题 
这部分主要是分析了一下udev的source code,对一些自己关心的问题的理解 
  
4.1        Udevd如何获取内核的这些模块动态变化的信息 
  
设备节点的创建,是通过sysfs接口分析dev文件取得设备节点号,这个很显而易见。那么udevd是通过什么机制来得知内核里模块的变化情况,如何得知设备的插入移除情况呢?当然是通过hotplug机制了,那hotplug又是怎么实现的?或者说内核是如何通知用户空间一个事件的发生的呢? 
  
答案是通过netlink socket通讯,在内核和用户空间之间传递信息。 
  
内核调用kobject_uevent函数发送netlink message给用户空间,这部分工作通常不需要驱动去自己处理,在统一设备模型里面,在子系统这一层面,已经将这部分代码处理好了,包括在设备对应的特定的Kobject创建和移除的时候都会发送相应add和remove消息,当然前提是你在内核中配置了hotplug的支持。 
Netlink socket作为一种内核与用户空间的通信方式,不仅仅用在hotplug机制中,同样还应用在其它很多真正和网络相关的内核子系统中。 
  
Udevd通过标准的socket机制,创建socket连接来获取内核广播的uevent事件 并解析这些uevent事件。 
  
4.2        Udevd如何监控规则文件的变更 
如果内核版本足够新的话,在规则文件发生变化的时候,udev也能够自动的重新应用这些规则,这得益于内核的inotify机制, inotify是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知。 
  
在udevd中,对inotify和udev的netlink socket文件描述符都进行了select的等待操作。有事件发生以后再进一步处理。 
  
4.3        Udevtrigger的工作机制? 
运行udevd以后,使用udevtrigger的时候,会把内核中已经存在的设备的节点创建出来,那么他是怎么做到这一点的? 分析udevtrigger的代码可以看出: 
  
udevtrigger通过向/sysfs 文件系统下现有设备的uevent节点写"add"字符串,从而触发uevent事件,使得udevd能够接收到这些事件,并创建buildin的设备驱动的设备节点以及所有已经insmod的模块的设备节点。 
  
所以,我们也可以手工用命令行来模拟这一过程: 
  
/ # echo "add" > /sys/block/mtdblock2/uevent 
/ # 
/ # UEVENT[178.415520] add      /block/mtdblock2 (block) 
  
但是,进一步看代码,你会发现,实际上,不管你往uevent里面写什么,都会触发add事件,这个从kernel内部对uevent属性的实现函数可以看出来,默认的实现是: 
static ssize_t store_uevent(struct device *dev, struct device_attribute *attr, 
                         const char *buf, size_t count) 

       kobject_uevent(&dev->kobj, KOBJ_ADD); 
       return count; 

所以不管写的内容是什么,都是触发add操作,真遗憾,我还想通过这个属性实验remove的操作。 不知道这样限制的原因是什么。 
  
而udevstart的实现方式和udevtrigger就不同了,它基本上是重复实现了udevd里面的机制,通过遍历sysfs,自己完成设备节点的创建,不通过udevd来完成。 
  
4.4        其它 
Ø       udevd创建每一个节点的时候,都会fork出一个新的进程来单独完成这个节点的创建工作。 
  
Ø       Uevent_seqnum 用来标识当前的uevent事件的序号(已经产生了多少uevent事件),你可以通过如下操作来查看: 
  
$ cat /sys/kernel/uevent_seqnum 
2673 

你可能感兴趣的:(udev的实现原理)