自2.6 核心开始,就可以使用udev 协助管理系统中各设备名称。例如,磁盘设备排序、网卡设备排序等。udev能动态地在/dev 目录里产生自定义的、标识性强的设备文件或设备链接。本文即以红旗Asianux 3.0 平台,给新加载的U盘设备自定义一个链接为例进行简要说明。
一、关于udev2.4 内核使用devfs(设备文件系统)在设备初始化时创建设备文件,设备驱动程序可以指定设备号、所有者、用户空间等信息,devfs 运行在内核环境中,并有不少缺点:可能出现主/辅设备号不够,命名不灵活,不能指定设备名称等问题。而自2.6 内核开始,引入了sysfs 文件系统。sysfs 把连接在系统上的设备和总线组织成一个分级的文件,并提供给用户空间存取使用。udev 运行在用户模式,而非内核中。udev 的初始化脚本在系统启动时创建设备节点,并且当插入新设备——加入驱动模块——在sysfs上注册新的数据后,udev会创新新的设备节点。udev 是一个工作在用户空间的工具,它能根据系统中硬件设备的状态动态的更新设备文件,包括设备文件的创建,删除,权限等。这些文件通常都定义在/dev 目录下,但也可以在配置文件中指定。udev 必须内核中的sysfs和tmpfs支持,sysfs 为udev 提供设备入口和uevent 通道,tmpfs 为udev 设备文件提供存放空间。注意,udev 是通过对内核产生的设备文件修改,或增加别名的方式来达到自定义设备文件的目的。但是,udev 是用户模式程序,其不会更改内核行为。也就是说,内核仍然会创建sda,sdb等设备文件,而udev可根据设备的唯一信息来区分不同的设备,并产生新的设备文件(或链接)。而在用户的应用中,只要使用新产生的设备文件即可。udev 的工作流程图:
二、参考文档
正如前面提到的,udev 依赖于2.6 核心上的sysfs 文件系统。因此,只有在红旗DC 5.0以上版本中才能使用。随着udev 的不断发展,不同版本的udev 规则也有不少差别,编写规则时必须注意。因为不同版本的udev 规则定义方式不同,在编写时需特别留意。我主要是参考以下资料来了解相关规则的:官网:man udevudev 服务的主要配置文件在/etc/udev/udev.conf,但通常不同修改。规 则文件是存放在/etc/udev/rules.d 目录下面,所有的规则文件都必须以.rules 作为后缀名。系统安装完毕后,该目录中就会有一些默认的规则文件,主要用于产生一些容易标识的设备符号链接。同时,一些应用程序,为了在/dev 下产生方便使用的标识符,也会放入一些规则,例如:40-multipath.rules、99-fuse.rules 等。udev 是按照规则文件的字母顺序来解析各规则文件的,并根据匹配上的规则创建对应的设备文件或链接。所以,解析的顺序很重要,为了使自定义的规则生效,可以把规则写入较前的规则文件中,例如20-names.rules。三、规则说明以下规则说明来自一文,与Asianux 3.0 中man udev 中的说明一致,我就不一一翻译了。1、udev 规则的所有操作符
如果你使用Linux比较长时间了,那你就知道,在对待设备文件这块,Linux改变了几次策略。在Linux早期,设备文件仅仅是是一些带有适当的属性集的普通文件,它由mknod命令创建,文件存放在/dev目录下。后来,采用了devfs, 一个基于内核的动态设备文件系统,他首次出现在2.3.46内核中。Mandrake,Gentoo等Linux分发版本采用了这种方式。devfs创建的设备文件是动态的。但是devfs有一些严重的限制,从2.6.13版本后移走了。目前取代他的便是文本要提到的udev--一个用户空间程序。
目前很多的Linux分发版本采纳了udev的方式,因为它在Linux设备访问,特别是那些对设备有极端需求的站点(比如需要控制上千个硬盘)和热插拔设备(比如USB摄像头和MP3播放器)上解决了几个问题。下面我我们来看看如何管理udev设备。
实际上,对于那些为磁盘,终端设备等准备的标准配置文件而言,你不需要修改什么。但是,你需要了解udev配置来使用新的或者外来设备,如果不修改配置,这些设备可能无法访问,或者说Linux可能会采用不恰当的名字,属组或权限来创建这些设备文件。你可能也想知道如何修改RS-232串口,音频设备等文件的属组或者权限。这点在实际的Linux实施中是会遇到的。为什么使用udev
在此之前的设备文件管理方法(静态文件和devfs)有几个缺点:
*不确定的设备映射。特别是那些动态设备,比如USB设备,设备文件到实际设备的映射并不可靠和确定。举一个例子:如果你有两个USB打印机。一个可能称为/dev/usb/lp0,另外一个便是/dev/usb/lp1。但是到底哪个是哪个并不清楚,lp0,lp1和实际的设备没有一一对应的关系,因为他可能因为发现设备的顺序,打印机本身关闭等原因而导致这种映射并不确定。理想的方式应该是:两个打印机应该采用基于他们的序列号或者其他标识信息的唯一设备文件来映射。但是静态文件和devfs都无法做到这点。
*没有足够的主/辅设备号。我们知道,每一个设备文件是有两个8位的数字:一个是主设备号,另外一个是辅设备号来分配的。这两个8位的数字加上设备类型(块设备或者字符设备)来唯一标识一个设备。不幸的是,关联这些身边的的数字并不足够。
*/dev目录下文件太多。一个系统采用静态设备文件关联的方式,那么这个目录下的文件必然是足够多。而同时你又不知道在你的系统上到底有那些设备文件是激活的。
*命名不够灵活。尽管devfs解决了以前的一些问题,但是它自身又带来了一些问题。其中一个就是命名不够灵活;你别想非常简单的就能修改设备文件的名字。缺省的devfs命令机制本身也很奇怪,他需要修改大量的配置文件和程序。
*内核内存使用,devfs特有的另外一个问题是,作为内核驱动模块,devfs需要消耗大量的内存,特别当系统上有大量的设备时(比如上面我们提到的系统一个上有好几千磁盘时)
udev的目标是想解决上面提到的这些问题,他通采用用户空间(user-space)工具来管理/dev/目录树,他和文件系统分开。知道如何改变缺省配置能让你之大如何定制自己的系统,比如创建设备字符连接,改变设备文件属组,权限等。
udev配置文件
主要的udev配置文件是/etc/udev/udev.conf。这个文件通常很短,他可能只是包含几行#开头的注释,然后有几行选项:
udev_root=“/dev/”
udev_rules=“/etc/udev/rules.d/”udev_log=“err“上面的第二行非常重要,因为他表示udev规则存储的目录,这个目录存储的是以.rules结束的文件。每一个文件处理一系列规则来帮助udev分配名字给设备文件以保证能被内核识别。
你的/etc/udev/rules.d下面可能有好几个udev规则文件,这些文件一部分是udev包安装的,另外一部分则是可能是别的硬件或者软件包生成的。比如在Fedora Core 5系统上,sane-backends包就会安装60-libsane.rules文件,另外initscripts包会安装60-net.rules文件。这些规则文件的文件名通常是两个数字开头,它表示系统应用该规则的顺序。规则文件里的规则有一系列的键/值对组成,键/值对之间用逗号(,)分割。每一个键或者是用户匹配键,或者是一个赋值键。匹配键确定规则是否被应用,而赋值键表示分配某值给该键。这些值将影响udev创建的设备文件。赋值键可以处理一个多值列表。匹配键和赋值键操作符解释见下表:
udev 键/值对操作符
操作符 匹配或赋值 解释
----------------------------------------== 匹配 相等比较!= 匹配 不等比较= 赋值 分配一个特定的值给该键,他可以覆盖之前的赋值。+= 赋值 追加特定的值给已经存在的键:= 赋值 分配一个特定的值给该键,后面的规则不可能覆盖它。这有点类似我们常见的编程语言,比如C语言。只是这里的键一次可以处理多个值。有一些键在udev规则文件里经常出现,这些键的值可以使用通配符(*,?,甚至范围,比如[0-9]),这些常用键列举如下:
常用udev键
键 含义ACTION 一个时间活动的名字,比如add,当设备增加的时候KERNEL 在内核里看到的设备名字,比如sd*表示任意SCSI磁盘设备DEVPATH 内核设备录进,比如/deviceserror = kobject_add(&dev->kobj, dev->kobj.parent, "%s", dev_name(dev));
if (error)
goto Error;
.......
kobject_uevent(&dev->kobj, KOBJ_ADD);
bus_attach_device(dev);
if (parent)
klist_add_tail(&dev->knode_parent, &parent->klist_children);
.......
attrError:
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
kobject_del(&dev->kobj);
Error:
cleanup_device_parent(dev);
if (parent)
put_device(parent);
goto done;
}
上面的代码段为我们常见的添加注册设备时的会调用到的接口,device_add()函数,删除了一些无关代码,可以看出,是先调用了kobject_add()创建添加该内核对象,然后调用kobject_uevent()来通知系统uevent的变化,这里的action是KOBJ_ADD,相对应还有
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
在kobject_uevent()里面采用的就是Linux中比较经典的内核空间和用户空间的一种通信机制netlink socket,这个不是udev的重点,我也不做过多的解释,总之相信它能让内核空间和用户空间进行通信就行了。在udev也会有相应的socket来接受底层的消息。如下为参照udev源码写的一个简单的uevent消息侦听程序:
#define UEVENT_BUFFER_SIZE 2048
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;
}
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;
}
int main(int argc, char* argv[])
{
int hotplug_sock = init_hotplug_sock();
while(1)
{
//printf("sunqidong debug\n");
char buf[UEVENT_BUFFER_SIZE*2] = {0};
recv(hotplug_sock, &buf, sizeof(buf), 0);
printf("%s\n", buf);
}
return 0;
}
这也是一个后台服务程序,循环的执行接受底层的消息过来,当发生U盘的插拔时,会产生如下的log:
[root@localhost test]# ./hotplug
add@/devices/pci0000:00/0000:00:11.0/0000:02:03.0/usb1/1-1
add@/class/usb_endpoint/usbdev1.5_ep00
add@/devices/pci0000:00/0000:00:11.0/0000:02:03.0/usb1/1-1/1-1:1.0
add@/class/scsi_host/host6
add@/class/usb_endpoint/usbdev1.5_ep81
add@/class/usb_endpoint/usbdev1.5_ep02
add@/devices/pci0000:00/0000:00:11.0/0000:02:03.0/usb1/1-1/1-1:1.0/host6/target6:0:0/6:0:0:0
add@/class/scsi_disk/6:0:0:0
add@/block/sdb
add@/block/sdb/sdb1
add@/block/sdb/sdb2
add@/block/sdb/sdb5
add@/block/sdb/sdb6
add@/block/sdb/sdb7
add@/block/sdb/sdb8
add@/class/scsi_device/6:0:0:0
add@/class/scsi_generic/sg2
add@/class/bsg/6:0:0:0
remove@/class/usb_endpoint/usbdev1.5_ep81
remove@/class/usb_endpoint/usbdev1.5_ep02
remove@/class/bsg/6:0:0:0
remove@/class/scsi_generic/sg2
remove@/class/scsi_device/6:0:0:0
remove@/class/scsi_disk/6:0:0:0
remove@/block/sdb/sdb8
remove@/block/sdb/sdb7
remove@/block/sdb/sdb6
remove@/block/sdb/sdb5
remove@/block/sdb/sdb2
remove@/block/sdb/sdb1
remove@/block/sdb
remove@/devices/pci0000:00/0000:00:11.0/0000:02:03.0/usb1/1-1/1-1:1.0/host6/target6:0:0/6:0:0:0
remove@/class/scsi_host/host6
remove@/devices/pci0000:00/0000:00:11.0/0000:02:03.0/usb1/1-1/1-1:1.0
remove@/class/usb_endpoint/usbdev1.5_ep00
remove@/devices/pci0000:00/0000:00:11.0/0000:02:03.0/usb1/1-1
三.udev的规则文件 规则文件是udev中最重要的部分,默认是存放在/etc/udev/rules.d/下。所有的规则文件都必须以".rules"为后缀名。下面是一个简单的规则文件例子说明
KERNEL=="sdb8" , NAME="mydisk",MODE="0660"
KERNEL是匹配键,NAME和MODE是赋值键。"=="这是判断语句,"="是赋值语句,这条规则的意思是,如果有一个设备的内核设备名称为sdb8(我移动硬盘内的一个分区),则该条件生效,执行后面的赋值:在/dev/下产生名为mydisk的设备文件,并把该设备文件的权限设为0660.
通过这条简单的规则,应该就可以对规则文件有了个基本的了解。每个规则文件被分成一个或多个匹配和赋值部分。匹配部分用匹配专用的关键字来表示,相应的赋值部分用赋值专用的字来表示。
1.常见的匹配关键字: ACTION(用于匹配行为add/remove),KERNEL(内核中定义的设备名),BUS(用于匹配总线类型),SYSFS(用于匹配从sysfs得到的信息,比如lable,vendor,USB序列号等),SUBSYSTEM(匹配子系统名)等
2.常见的赋值关键字:
NAME(创建的设备文件名),SYMLINK(符号创建链接名),OWNER(设置设备的所有者),GROUP(设置设备的组),IMPORT(调用外部程序),MODE(权限位)
四.xx项目上自动挂载usb存储设备的应用
在xx项目上,我们需要自动挂载usb存储设备,并且要支持常见的几种文件系统,fat32,ntfs,exfat等,其中fat32等fat系列,Linux下早就有支持,ntfs和exfat目前的内核自身还没支持,我们有关于这两个文件系统的内核模块文件tntfs,ko和texfat.ko,加载进去过后就能让我们的内核识别这两种文件系统,实现手动加载这两种格式的存储设备。但如果要支持自动加载还有问题,需要去修改相应的规则文件。
在加载的时候,不同的格式的文件系统,加载的参数是不一样的,如exfat为
mount -t texfat /dev/sda /mnt/udisk
而ntfs为
mount -t tntfs /dev/sda /mnt/udisk
并且针对不同的格式,还有些其他挂载选项参数不一样,所以对不同的格式需要区别对待。
在规则文件里面是通过blkid -o udev命令来获取文件系统的信息的,判断出该盘是哪种格式,再去执行不同的挂载命令。
如下是blkid -o udev读出来的文件系统格式信息
ID_FS_UUID=2EE054B8E054884B
ID_FS_UUID_ENC=2EE054B8E054884B
ID_FS_LABEL=disk3
ID_FS_LABEL_ENC=disk3
ID_FS_TYPE=ntfs
ID_FS_LABEL=DISK4
ID_FS_LABEL_ENC=DISK4
ID_FS_UUID=B8CF-FF22
ID_FS_UUID_ENC=B8CF-FF22
ID_FS_TYPE=vfat
ID_FS_UUID=3606-1B2C
ID_FS_UUID_ENC=3606-1B2C
ID_FS_TYPE=exfat
ID_FS_LABEL=Disk5
ID_FS_LABEL_ENC=Disk5
可以看到上面的几个赋值项,在规则文件里面就会去读这些值。做相应的判断,实现不同文件系统的区别对待挂载
如下为规则文件的一部分
KERNEL!="sd[a-z][0-9]", GOTO="media_by_label_auto_mount_end"
# Import FS infos
IMPORT{program}="/sbin/blkid -o udev -p %N"
ENV{ID_FS_LABEL}!="", ENV{dir_name}="%E{ID_FS_LABEL}"
ENV{ID_FS_LABEL}=="", ENV{dir_name}="usb-%k"
#vfat,fat
ACTION=="add",ENV{ID_FS_TYPE}=="vfat|fat",RUN+="/bin/mkdir -p /mnt/udisk/%E{dir_name}",ENV{mount_options}="$env{mount_options},gid=100,umask=000",RUN+="/bin/mount -o $env{mount_options},iocharset=utf8 /dev/%k /mnt/udisk/%E{dir_name}"
#ntfs
ACTION=="add",ENV{ID_FS_TYPE}=="ntfs",RUN+="/bin/mkdir -p /mnt/udisk/%E{dir_name}",ENV{mount_options}="$env{mount_options},gid=100,umask=000",RUN+="/bin/mount -t tntfs -o $env{mount_options},iostreaming /dev/%k /mnt/udisk/%E{dir_name}"
#exfat
ACTION=="add",ENV{ID_FS_TYPE}=="exfat",RUN+="/bin/mkdir -p /mnt/udisk/%E{dir_name}",RUN+="/bin/mount -t texfat -o rw /dev/%k /mnt/udisk/%E{dir_name}"
关于blkid,在我们目前的文件系统里面,blkid是不支持exfat格式的,通过命令查看磁盘的信息,根本找不到exfat格式的磁盘。所以之前在做自动挂载的时候没法实现挂exfat,后来在网上找了个util-linux-ng2.18源码包,里面包含了blkid的源码。修改编译,编译出一个新的blkid文件,使其可以在我们的系统上运行,能够识别出exfat文件。