内核依据固定的标准与初始化次序命名设备,例如以太网设备,按照初始化次序命名为eth0、eth1等。UDEVD程序的存在,可以动态的添加/删除linux系统的设备节点,可以使用户定制化设备节点名称,创建设备链接等。
使用udevd,需要配置以下内核选项:
udevd要求:
内核支持sysfs, procfs, signalfd, inotify, unix domain sockets, networking and hotplug。
配置选项:
CONFIG_HOTPLUG=y
CONFIG_UEVENT_HELPER_PATH="" //禁用应用层hotplug程序
CONFIG_NET=y //使能uevent通信的netlink接口
CONFIG_UNIX=y //udevd内部进程间通信
CONFIG_SYSFS=y //用于udevd获取内核设备节点
CONFIG_PROC_FS=y //udevd参数调整
CONFIG_INOTIFY_USER=y //监控udevd rules规则文件变化
CONFIG_SIGNALFD=y //udevd信号处理
可选项:
CONFIG_TMPFS=y
CONFIG_TMPFS_POSIX_ACL=y (user ACLs for device nodes)
CONFIG_BLK_DEV_BSG=y (SCSI devices)
内核配置中禁用掉/sbin/hotplug,如果未禁用,将导致系统不稳定,因为hostplug的存在导致kernel并行创建了许多进程,会出现内存溢出。需要将proc文件系统挂到/proc,将sysfs挂载到/sys,udev会需要这些目录。
UDEVD规则文件
udevd的定制规则文件分别位于三个目录下:
用户自定义规则文件目录 /etc/udev/rules.d
UDEV默认规则文件目录 /lib/udev/rules.d
UDEVD运行时动态添加规则目录 /run/udev/rules.d
规则文件以数字开头以rules为后缀,开头的数字表示执行的顺序。/etc/udev/rules.d目录的优先权高于/lib/udev/rules.d目录,如果出现同名的规则文件,/etc/udev/rules.d目录中的文件会覆盖后者。
例如对于存储设备,规则文件为60-persistent-storage.rules,udevd不仅可以按照内核的命名创建/dev目录下的设备节点,还可以创建设备链接文件。如下对于存储设备,根据文件系统的信息UUID,在目录/dev/disk/by-uuid/下创建设备的链接文件,文件名称为设备的文件系统UUID。
# ATA
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{vendor}=="ATA", IMPORT{program}="ata_id --export $devnode"
# by-label/by-uuid links (filesystem metadata)
ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_FS_UUID_ENC}=="?*", SYMLINK+="disk/by-uuid/$env{ID_FS_UUID_ENC}"
ENV{ID_FS_USAGE}=="filesystem|other", ENV{ID_FS_LABEL_ENC}=="?*", SYMLINK+="disk/by-label/$env{ID_FS_LABEL_ENC}"
UDEVD程序使用文件系统的INOTIFY机制监控存放规则的三个文件目录:/lib/udev/rules.d目录、/etc/udev/rules.d目录和/run/udev/rules.d目录。
int main(int argc, char *argv[])
{
monitor = udev_monitor_new_from_netlink(udev, "kernel");
fd_inotify = udev_watch_init(udev);
/* watch rules.d paths for changes */
inotify_add_watch(fd_inotify, UDEV_RULES_DIR, IN_DELETE | IN_MOVE | IN_CLOSE_WRITE);
inotify_add_watch(fd_inotify, UDEV_CONF_DIR "/rules.d", IN_DELETE | IN_MOVE | IN_CLOSE_WRITE);
if (access(UDEV_ROOT_RUN "/udev/rules.d", F_OK) < 0)
udev_mkdir_p(UDEV_ROOT_RUN "/udev/rules.d", 0755);
inotify_add_watch(fd_inotify, UDEV_ROOT_RUN "/udev/rules.d", IN_DELETE | IN_MOVE | IN_CLOSE_WRITE);
}
监听内核UEVENT事件
UDEVD使用netlink套接口监听内核设备的uevent事件,套接口协议指定为NETLINK_KOBJECT_UEVENT(15)。
struct udev_monitor *udev_monitor_new_from_netlink_fd(struct udev *udev, const char *name, int fd)
{
udev_monitor->sock = socket(PF_NETLINK, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, NETLINK_KOBJECT_UEVENT);
}
内核中uevent netlink套接口创建由函数uevent_net_init完成。当内核中设备事件发生时,调用kobject_uevent函数利用netlink接口广播到用户层。
static int uevent_net_init(struct net *net)
{
struct netlink_kernel_cfg cfg = {
.groups = 1,
.flags = NL_CFG_F_NONROOT_RECV,
};
ue_sk->sk = netlink_kernel_create(net, NETLINK_KOBJECT_UEVENT, &cfg);
}
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
内核设备事件
Linux内核使用kobject系统进行设备管理,目前kobject定义了以下的设备事件:
static const char *kobject_actions[] = {
[KOBJ_ADD] = "add",
[KOBJ_REMOVE] = "remove",
[KOBJ_CHANGE] = "change",
[KOBJ_MOVE] = "move",
[KOBJ_ONLINE] = "online",
[KOBJ_OFFLINE] = "offline",
[KOBJ_BIND] = "bind",
[KOBJ_UNBIND] = "unbind",
};
可使用udevadm工具触发内核设备事件,即udevadm trigger命令,其默认的事件为change,使用-c选项指定触发事件类型,目前支持三种事件:add、remove和change。使用-t选项指定触发类型,目前支持subsystem和devices两种,不指定默认使用devices类型。事件触发的实现很简单,原理就是扫描系统的/sys目录,对设备的uevent文件写入相应的action字符。
static void exec_list(struct udev_enumerate *udev_enumerate, const char *action)
{
udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(udev_enumerate)) {
strscpyl(filename, sizeof(filename), udev_list_entry_get_name(entry), "/uevent", NULL);
fd = open(filename, O_WRONLY|O_CLOEXEC);
write(fd, action, strlen(action));
}
}
在系统上电启动之后,udevd守护进程启动之前,系统已经初始化好了内核设备,导致udevd的规则没有生效,此时可在启动脚本中调用udevadm trigger重新出发系统设备的change事件,udevd借此执行定义的规则。
也可收到修改设备的uevent文件(需要root权限),触发设备事件,使用udevadm的另外一个命令monitor去监听我们收到触发的事件(udevadm monitor在后台运行,仅是为了让出shell终端来执行uevent文件修改),如下,如果使用monitor监控trigger命令,将看到所有设备的事件信息:
# udevadm monitor &
[1] 4706
monitor will print the received events for:
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent
# echo change > /sys/block/sr0/uevent
KERNEL[109812.475999] change /devices/pci0000:00/0000:00:11.0/0000:02:05.0/ata4/host4/target4:0:0/4:0:0:0/block/sr0 (block)
UDEV [109812.646193] change /devices/pci0000:00/0000:00:11.0/0000:02:05.0/ata4/host4/target4:0:0/4:0:0:0/block/sr0 (block)
对于subsystem类型,trigger命令扫描/sys目录下的subsystem子目录,如果subsystem不存在,扫描bus子目录下的uevent文件。对于devices类型,如果udevd已经建立了系统设备的tags系统,trigger直接扫描/run/udev/tags目录。否则,如果/sys/subsystem存在,扫描其下的devices目录,不存在,则扫描/sys/bus目录和/sys/class目录,查找系统设备的uevent文件。
内核配置中的CONFIG_UEVENT_HELPER_PATH可指定uevent发生时调用的应用层处理程序,其在kobject_uevent_env中实现,调用call_usermodehelper_exec执行应用程序。目前此配置也不推荐使用。
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[])
{
#ifdef CONFIG_UEVENT_HELPER
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
retval = add_uevent_var(env, "HOME=/");
retval = add_uevent_var(env, "PATH=/sbin:/bin:/usr/sbin:/usr/bin");
retval = init_uevent_argv(env, subsystem);
info = call_usermodehelper_setup(env->argv[0], env->argv, env->envp, GFP_KERNEL, NULL, cleanup_uevent_env, env);
if (info)
retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
}
#endif
}
DEVTMPFS文件系统
内核配置CONFIG_DEVTMPFS后,将启用devtmpfs文件系统:
Device Drivers -> Generic Driver Options -> Maintain devtmpfs to mount at /dev
设置此选项之后,内核将devtmpfs文件系统mount到/dev目录下,此时/dev目录包含系统中的所有已知设备节点,内核使用默认命名创建这些节点,如果没有使用devtmpfs,dev目录将挂载一个tmpfs文件系统。对于不需要处理设备热插拨和自定义设备名称等需求的系统,可不使用udevd设备管理进程。
相关代码
eudev-3.2.5
Linux-4.15