[What]Linux udev基础操作

用户空间的udev可以接收内核设备发送的netlink消息,并根据当前状态动作。

运行下面的代码即可体验:

#include <linux/netlink.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>

static void die(char *s)
{

write(2, s, strlen(s));
exit(1);
}

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

struct sockaddr_nl nls;
struct pollfd pfd;
char buf[512];

//Open hotplug event netlink socket
memset(&nls, 0, sizeof(struct sockaddr_nl));
nls.nl_family = AF_NETLINK;
nls.nl_groups = -1;

pfd.events = POLLIN;
pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if(pfd.fd == -1)
{
die("Not root\n");
}

//listen to netlink socket
if(bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
{
die("Bind failed!\n");
}
while(-1 != poll(&pfd, 1, -1))
{
int i, len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
if(len == -1)
{
die("recv\n");
}
//print the data to stdout
i = 0;
while(i < len)
{
printf("%s\n", buf + i);;
i += strlen(buf + i) + 1;
}
}
die("poll\n");

return 0;
}

规则

文件位置

udev接收设备的消息后,其具体动作则是由规则文件所规定的。

  • 规则文件的后缀为 .rules ,以 root 权限用户可以将文件放在 /etc/udev/rules.d 中,或放在运行时文件夹 /run/udev/rules.d
    • 系统的规则文件存在于 /lib/udev/rules.d 中,当用户文件中有与系统文件同名的文件时,则会覆盖系统文件的设置
      • 同名文件覆盖时,优先级由高到低依次是 /etc,/run,/lib
  • 规则文件名称以一个数字开头代表其运行的先后顺序,比如 52-xilinx-pcusb.rules
    • udev 运行文件是以数值和字母排序来运行的

语法

基本思想

需要理解的是,规则文件的语法由两大部分组成:

  1. 设备匹配:此部分使用序列号与内核发送的消息匹配
  2. 后续动作:当设备匹配成功后,便执行当前的动作

基本规则

  • 空格行和注释行(以"#"开头)会被忽略
  • 规则每一行至少包含一对 "key=value" 的键值对,其中的key就包含设备匹配键和赋值键
    • 首先得需要所有的设备匹配键 完全匹配 后,后面的赋值键才会生效
  • 一个规则文件是由多个键值对所组成的,它们之间通过逗号分隔

基本操作

每个键值对可执行如下操作:

  • "==" : 判断是否相等
  • "!=" : 判断是否不等
  • "=" : 直接给key赋值,key之前的值将会被覆盖
  • "+=" : 给key追加赋值,新值会加入key的列表中
  • "-=" : 删除key列表中包含的此值
  • ":=" : 最终赋值,也就是将key的值定死为这个值了,它不会被之后的值覆盖了

key的匹配

有下面这些key匹配可以被使用:

  • 这些key名称在事件返回是也会有对应的名称
key name 与事件中的对应意义
ACTION 当前动作的名称,相当于一个大范围的筛选
DEVPATH 设备的路径
KERNEL 设备的名称
NAME 网络接口的名称
SYMLINK 符号链接的名称
SUBSYSTEM 子系统匹配
DRIVER 与设备对应的驱动名称匹配
ATTR{filename}, SYSCTL{kernel parameter} 分别对应sysfs中的属性值和内核中的参数值
KERNELS 父设备名称
SUBSYSTEMS 父子系统名称
DRIVERS 父设备对应的驱动名称
ATTRS{filename} 父设备对应的sysfs中的属性值
TAGS 父设备标记
ENV{key} 对应设备的属性值
TAG 设备的标记
TEST{octal mode mask} 测试当前文件的属性
PROGRAM 通过执行外部程序来判断是否匹配,如果程序运行成功则返回true
RESULT 跟在 PROGRAM 后面以获取程序运行的返回值

key的字符串,也可以使用shell中的一些通配符:

  • "*" : 匹配0个或多个字符
  • "?" : 匹配1个字符
  • "[]" : 匹配括号中的一个字符
    • 比如 "[0-9]" 表示匹配0-9中一个数字
  • "|" : 匹配左值或右值

key的赋值

下面这些key可以被赋值

  • 所谓赋值的意义就是设置或执行对应的动作
key 意义
NAME 网络接口的名称
SYMLINK 符号链接的名称
OWNER,GROUP,MODE 设备的拥有者、组、模式等
SECLABEL{module} 为设置指定对应的安全模块
ATTR{key} 向设备sysfs中的属性文件写值
SYSCTL{kernel parameter} 向内核参数写值
ENV{key} 设置设备的属性值
TAG 设备的标记,用于过滤和分组
RUN{type} 当规则执行完后,执行程序列表。type的值为 programbuiltin,程序名需要加绝对路径
LABEL 和C中label意义一样,用于GOTO跳转
GOTO 跳转到对应的LABEL处
IMPORT{type} 为设备属性导入一个值
OPTIONS 规则和设备的选项

其中 NAME, SYMLINK , PROGRAM , OWNER , GROUP , MODE , SECLABEL , RUN 的赋值字符串可以使用替换符,

  • RUN 的替换符要在规则执行完后才生效

具有下面这些替换符:

  • $kernel, %k : 设备在内核中的名称
  • $number, %n : 设备在内核中的数值
  • $devpath, %p : 设备的路径
  • %id, %b : 设备的名称
  • $driver : 设备对应的驱动名称
  • $attr{file}, %s{file} : 设备在 sysfs 中的属性值
  • %env{key}, %E{key} : 设备属性值
  • $major, %M : 主设备号
  • $minor, %m : 次设备号
  • $result, %c : 运行外部程序的 PROGRAM 返回值
  • $parent, %p : 符设备节点名
  • $name : 设备名称
  • $links : 符号链接
  • $root, %r : dev_root 的值
  • $sys, %S : sysfs的挂载点
  • $devnode, %N : 设备节点名称
  • %% : 代表 “%”
  • $$ : 代表 “$”

使用

上面一大堆看得人头晕,具体实践一下就明白了,步骤如下:

  1. 找到设备路径
  2. 分析设备及父设备属性
  3. 编写规则文件
  4. 仿真测试
  5. 验证

找到设备路径

首先 运行文章开头的代码 ,然后查看对应设备插入或拔下的输出信息。

比如,我通过将SD卡插入到读卡器后,输出了下面这部分信息:

ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:0c.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
PARTN=1
SEQNUM=4933
USEC_INITIALIZED=6355493922
MAJOR=8
MINOR=17
ID_BUS=usb
ID_INSTANCE=0:0
ID_MODEL=STORAGE_DEVICE
ID_MODEL_ENC=STORAGE\x20DEVICE\x20\x20
ID_MODEL_ID=0749
ID_PART_TABLE_TYPE=dos
ID_PART_TABLE_UUID=61a220e7
ID_PATH=pci-0000:00:0c.0-usb-0:2:1.0-scsi-0:0:0:0
ID_PATH_TAG=pci-0000_00_0c_0-usb-0_2_

由此便可得知设备路径 DEVPATH/sys/ 下的:

/devices/pci0000:00/0000:00:0c.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb/sdb1

分析设备及父设备属性

udevadm 命令可以以层次的方式展示当前设备属性及父设备的属性,一般使用方式为:

udevadm info -ap <devpath>

所以接下来查看其属性:

cec@virtual:~$ udevadm info -ap /devices/pci0000:00/0000:00:0c.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb/sdb1

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/devices/pci0000:00/0000:00:0c.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb/sdb1':
    KERNEL=="sdb1"
    SUBSYSTEM=="block"
    DRIVER==""
    ATTR{alignment_offset}=="0"
    ATTR{discard_alignment}=="0"
    ATTR{inflight}=="       0        0"
    ATTR{partition}=="1"
    ATTR{ro}=="0"
    ATTR{size}=="15515648"
    ATTR{start}=="8192"
    ATTR{stat}=="     396    14920    20016     1332        1        0        1        0        0      728     1332"

  looking at parent device '/devices/pci0000:00/0000:00:0c.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb':
    KERNELS=="sdb"
    SUBSYSTEMS=="block"
    DRIVERS==""
    ATTRS{alignment_offset}=="0"
    ATTRS{capability}=="51"
    ATTRS{discard_alignment}=="0"
    ATTRS{events}=="media_change"
    ATTRS{events_async}==""
    ATTRS{events_poll_msecs}=="-1"
    ATTRS{ext_range}=="256"
    ATTRS{hidden}=="0"
    ATTRS{inflight}=="       0        0"
    ATTRS{range}=="16"
    ATTRS{removable}=="1"
    ATTRS{ro}=="0"
    ATTRS{size}=="15523840"
    ATTRS{stat}=="    1250    44760    64984     4636        3        0        3        0        0     2424     4636"

........

以上只打印了其上一级的父设备,一般来讲这就可以匹配了。

编写规则文件

#以add作为捕捉点
# ATTRS 都是使用父节点的属性
ACTION=="add" \
, ATTRS{capability}=="51" \
, ATTRS{events}=="media_change" \
, RUN+="/bin/bash /home/cec/learn/linux/udev/out.sh"

上面的匹配规则比较简单,需要注意的是 RUN key,因为RUN运行的外部命令都需要绝对路径, 如果我们直接在此处写命令实在麻烦,所以使用 /bin/bash 来运行绝对路径脚本,在脚本中再运行命令就方便多了。

脚本"out.sh" 如下:

#/bin/bash

echo "sdcard inserted!" > /home/cec/learn/linux/udev/test.out

最后,将这个规则文件需要拷贝到规则目录:

sudo cp 99-sdcardcheck.rules /etc/udev/rules.d/

仿真测试

udevadm test 可以模拟规则文件是否被调用, 需要保证设备没有被拔下!

  • 并不会运行 RUN key
....
Reading rules file: /etc/udev/rules.d/99-sdcardcheck.rules
....
run: '/bin/bash /home/cec/learn/linux/udev/out.sh'
....

由上可以看出规则文件已经被正确调用了。

验证

现在重载一下规则文件:

sudo udevadm control --reload

然后重新插入sd卡,可以看到对应路径新建了文件 test.out

mdev

对于嵌入式linux来说,udev相对太重量级了,busybox为此提供了几乎同样功能的mdev。

准备

为了使用mdev,需要以下几个前提:

  • 内核使能hotplug
  • busybox使能mdev
  • 文件系统需要挂载 sysfsproc 文件系统

配置

mdev的配置文件位于 /etc/mdev.conf ,其匹配关系格式为:

<device regex> <uid>:<gid> <octal permissions> [=path] [@|$|*<command>]
  • <device regex> : 以正则表达式的方式匹配 /dev 设备名
  • <uid>:<gid> : 设定user id 和 group id
    • 默认的uid和gid都为root
  • <octal permissions> : 设定操作权限
    • 默认的权限为660
  • <=|>path> : 移动或重命名设备节点
    • 当为 "=" 时,代表单纯的移动或重命名
    • 当为 ">" 时,代表移动或重命名后,还会在 /dev 下创建设备的符号链接
  • [@|$|*<command>] : 匹配成功后,调用对应的命令动作
    • @ : 在创建设备节点后调用此命令
    • $ : 在移除设备节点前调用此命令
    • * : 在创建设备节点后以及移除设备节点前都调用此命令

比如:

mmcblk1p[0-9]  0:0 660 */etc/hotplug/sd/sd_det

sd_det 就是一个脚本,用于当设备匹配后的执行脚本。

  • 当设备匹配后,变量 ${MDEV} 便是设备的名称

使用

  • 使能mdev为hotplug管理者:
echo /sbin/mdev > /proc/sys/kernel/hotplug
  • 启动
mdev -s

实例

mdev.conf:

$MODALIAS=.* 0:0 660 @modprobe "$MODALIAS"

console 0:0 0600 
cpu_dma_latency 0:0 0660 
fb0:0 44 0660 
full 0:0 0666 
initctl 0:0 0600 
ircomm[0-9].* 0:20 0660 
kmem 0:15 0640 
kmsg 0:0 0660 
log 0:0 0666 
loop[0-9].* 0:6 0640 
mem 0:15 0640 
network_latency 0:0 0660 
network_throughput 0:0 0660 
null 0:0 0666 
port 0:15 0640 
ptmx 0:5 0666 
ram[0-9].* 0:6 0640 
random 0:0 0666 
sda 0:6 0640 
tty 0:5 0666 
tty.* 0:0 0620 
urandom 0:0 0666 
usbdev.* 0:0 0660 */etc/mdev/usb.sh
vcs.* 0:5 0660 
zero 0:0 0666 

snd/pcm.* 0:0 0660
snd/control.* 0:0 0660
snd/timer 0:0 0660
snd/seq 0:0 0660
snd/mini.* 0:00 0660

input/event.* 0:0 0660 @/etc/mdev/find-touchscreen.sh
input/mice 0:0 0660
input/mouse.* 0:0 0660

tun[0-9]* 0:0 0660 =net/

[hs]d[a-z][0-9]? 0:0 660 */etc/mdev/mdev-mount.sh
mmcblk[0-9].* 0:0 660 */etc/mdev/mdev-mount.sh

mdev-mount.sh

#!/bin/sh
MDEV_AUTOMOUNT=y
MDEV_AUTOMOUNT_ROOT=/run/media
[ -f /etc/default/mdev ] && . /etc/default/mdev
if [ "${MDEV_AUTOMOUNT}" = "n" ] ; then
exit 0
fi

case "$ACTION" in
add|"")
ACTION="add"
# check if already mounted
if grep -q "^/dev/${MDEV} " /proc/mounts ; then
# Already mounted
exit 0
fi
DEVBASE=`expr substr $MDEV 1 3`
if [ "${DEVBASE}" == "mmc" ] ; then
DEVBASE=`expr substr $MDEV 1 7`
fi
# check for "please don't mount it" file
if [ -f "/dev/nomount.${DEVBASE}" ] ; then
# blocked
exit 0
fi
# check for full-disk partition
if [ "${DEVBASE}" == "${MDEV}" ] ; then
if [ -d /sys/block/${DEVBASE}/${DEVBASE}*1 ] ; then
# Partition detected, just quit
exit 0
fi
if [ ! -f /sys/block/${DEVBASE}/size ] ; then
# No size at all
exit 0
fi
if [ `cat /sys/block/${DEVBASE}/size` == 0 ] ; then
# empty device, bail out
exit 0
fi
fi
# first allow fstab to determine the mountpoint
if ! mount /dev/$MDEV > /dev/null 2>&1
then
MOUNTPOINT="${MDEV_AUTOMOUNT_ROOT}/$MDEV"
mkdir "$MOUNTPOINT"
mount -t auto /dev/$MDEV "$MOUNTPOINT"
fi
;;
remove)
MOUNTPOINT=`grep "^/dev/$MDEV\s" /proc/mounts | cut -d' ' -f 2`
if [ ! -z "$MOUNTPOINT" ]
then
umount "$MOUNTPOINT"
rmdir "$MOUNTPOINT"
else
umount /dev/$MDEV
fi
;;
*)
# Unexpected keyword
exit 1
;;
esac
Last Updated 2018-11-13 Tue 07:29.
Render by hexo-renderer-org with Emacs 26.1 (Org mode 9.1.14)