赞
踩
相对于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的实现原理。
Udev介绍
Udev的下载网址:http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev/
Udev分为三个子计划:namedev,libsysfs和udev。Namedev为设备命名子系统,libsysfs提供访问sysfs文件系统,从中获取信息的标准接口。Udev是提供/dev设备节点文件的动态创建和删除策略。
Namedev 使用5个步骤来决定指定设备的命名。
(1) 标签/序列号
(2) 设备总线号
(3) 总线上的拓扑
(4) 替换名称
(5) 内核提供的名称
Udev的规则文件
Udev规则文件以行为单位,以“#”开头的代表注释行,其余的一行代表一个规则
规则分为匹配和赋值两部分。两部分皆有自己的关键字。
匹配关键字:
ACTION,用于匹配行为
KERNEL,匹配内核设备名
BUS,匹配总线
SYSFS,匹配从sysfs得到的信息,比如label,vendor,USB序列号。
SUBSYSTEM,匹配子系统名
赋值关键字
NAME,创建文件设备名
SYMLINK,符号链接名
OWNER,设置设备的所有者
GROUP,设置设备的组
IMPORT,调用外部程序
创建和配置udev
本人下载了udev-126,udev-100,udev-172, 使用udev-172无法编译通过,udev-126不能得到试验所说的9个工具程序,只有udevd,test-udev,udevadm三个工具程序。Udev-100可得到全部的9个程序
Udev-126的配置:./configure --prefix=/home/uncompress_software/udev-126/
--target=arm-linux --host=arm-vfp-linux-gnu LD=arm-vfp-linux-gnu-ld
make
make install
udev-100:修改了Makefile,其中包括CROSS_COMPILE,prefix两个地方。
以下内容来自星光灿烂 'S bLog
由于在kernel启动未完成以前我们的设备文件不可用,如果使用mtd设备作为rootfs的挂载点,这个时候/dev/mtdblock
是不存在的,我们无法让kernel找到rootfs,kernel只好停在那里惊慌。 这个问题我们可以通过给kernel传递设备号的方式来解决,在linux系统中,mtdblock的主设备号是31,part号 从0开始,那么以前的/dev/mtdblock/3就等同于31:03,以次类推,所以我们只需要修改bootloader传给kernel 的cmd line参数,使root=31:03,就可以让kernel在udevd未起来之前成功的找到rootfs。
在嵌入式系统中,只需要udevd和udevstart就能使udev正常工作。
将生成的udevd和udevstart复制到/sbin目录,同时将udev源代码目录中etc/udev的文件复制到系统/etc目录下。
最后编写启动,停止,重新启动等工作的udev脚本。
Mdev介绍
以下内容来自mdev入门
mdev有两个主要的应用:初始化对象和动态更新。两个应用都需要内核sysfs的支持。为了实现动态更新,你必须在内核配置时增加热挺拔支持。
以下是系统初始化脚本中一个典型的使用mdev 的代码片段:
[1] mount -t sysfs sysfs /sys
[2] echo /bin/mdev > /proc/sys/kernel/hotplug
[3] mdev -s
简单说明一下上面的代码:
[1]你必须在执行mdev 前挂载 /sys 。
[2] 命令内核在增删设备时执行 /bin/mdev ,使设备节点文件会被创建和删除。
[3] 设置mdev,让它在系统启动时创建所有的设备节点。
当然,一个对mdev 更完整的安装还必须在以上代码片段前执行下面的命令:
[4] mount -t tmpfs mdev /dev
[5] mkdir /dev/pts
[6] mount -t devpts devpts /dev/pts
[4]确保 /dev 是 tmpfs 文件系统(假设文件系统在 flash 外运行)。
[5] 创建/dev/pts 挂载点
[6] 在 /dev/pts 挂载 devpts 文件系统
例如:(来自smartarm3250)
#!/bin/sh
if [ ! -x /sbin/mdev ]
then
exit 0
fi
case "$1" in
start)
echo "/sbin/mdev" > /proc/sys/kernel/hotplug
# put /dev in a tmpfs
mount -n -o mode=0755 -t tmpfs mdev /dev
# Create static device nodes in /dev
mknod /dev/console c 5 1
chmod 600 /dev/console
mknod /dev/null c 1 3
chmod 666 /dev/null
# make and mount devpts
mkdir /dev/pts
mount -n -t devpts devpts /dev/pts
echo "Starting the hotplug events dispatcher mdev"
/sbin/mdev -s
mkdir /dev/shm
;;
stop)
;;
*)
echo "Usage: /etc/rc.d/init.d/mdev {start|stop}"
echo
exit 1
;;
esac
exit 0
说明:以上内容就是mdev的启动脚本,基本体现了上面的6个步骤。
Mount –n,挂载但不写入
--blind,将一个子树重新挂载到其它地方,使有多个地方可以见到些子树
--move ,和blind有可比性,此项是移动子树
Uevent,mdev,udev
如果你对linux设备模型了解的很清楚,那么很自然就会想起驱动模型中的uevent。
以下内容来自张俊岭《对Linux 设备驱动模型的一些理解》
uevent 是“user event”的简称,是一种内核向用户空间发送信息的方式。Linux 内核的热拔插机制(hotplug)就是通过uevent 实现的。
当在总线中注册和删除一个设备或一个设备驱动程序时,会调用kobject_uevent()产生uevent。kobject_uevent()的代码在lib/kobject_uevent.c 中,
1.查找kobject 所属的kset,并获得kset 的uevent_ops如果kobj->uevent_suppress 为1,表示当前kobject 禁止产生uevent,返回0
2. 调用kset->uevent_ops->filter(),如果返回0,表示kset 禁止产生uevent,返回0
3.如果内核支持网络功能,使用netlink Socket 向用户空间广播uevent
4.如果uevent_helper 有效,则调用它。
udev 和mdev 是两个使用uevent 机制处理热插拔问题的用户空间程序,两者的实现机理不同。udev 是基于netlink 机制的,它在系统启动时运行了一个deamon 程序udevd,通过监听内核发送的uevent 来执行相应的热拔插动作,包括创建/删除设备节点,加载/卸载驱动模块等等。mdev 是基于uevent_helper 机制的,它在系统启动时修改了内核中的uevnet_helper 变量(通过写/proc/sys/kernel/hotplug),值为“/sbin/mdev”。这样内核产生uevent 时会调用uevent_helper 所指的用户级程序,也就是mdev,来执行相应的热拔插动作。udev 使用的netlink 机制在有大量uevent 的场合效率高,适合用在PC 机上;而mdev 使用的uevent_helper 机制实现简单,适合用在嵌入式系统中。另外要说明的一点是,uevent_helper 的初始值在内核编译时时可配置的,默认值为/sbin/hotplug。如果想修改它的值,写/proc/sys/kernel/hotplug 文件就可以了,例如:
echo “/sbin/mdev” > /proc/sys/kernel/hotplug
补充一点:如果使用的是udevd,那么uevent_helper变量应为空,即
echo “ ” > /proc/sys/kernel/hotplug
三、内核源码分析
分析一下相关内核源码,看上面提到的功能是如何实现的。
平时我们添加驱动时,如果想自动创建设备节点调用的函数是class_create和device_create。class_create是创建类设备,
就是在/sys/class/创建一个文件夹,这个文件夹代表一类设备,这个文件夹里会包含device_create创建的设备,也是一个
文件夹。
下面就从device_create入手,看是怎么实现自动创建设备节点的。源码基于linux-2.6.30.4内核
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
retval = device_register(dev);
return device_add(dev);
kobject_uevent(&dev->kobj, KOBJ_ADD);
return kobject_uevent_env(kobj, action, NULL); // action = KOBJ_ADD
const char *action_string = kobject_actions[action]; // action_string = "add"
……
//把相关信息存到环境变量里,ACTION代表操作类型,DEVPATH为设备在class下存在的路径,SUBSYSTEM为class_create创建的设备类
//ACTION=add , DEVPATH=/class/test/test_dev , SUBSYSTEM=test
retval = add_uevent_var(env, "ACTION=%s", action_string);
if (retval)
goto exit;
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
if (retval)
goto exit;
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
if (retval)
goto exit;
……
if (uevent_helper[0]){
argv [0] = uevent_helper;
argv [1] = (char *)subsystem;
argv [2] = NULL;
//内核空间调用用户空间程序,调用的程序由argv [0] = uevent_helper指定
retval = call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
下面看看uevent_helper是谁
定义如下:
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
去.config中查看:
CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug"
但是去/sbin目录下查看,并没有hotplug这个文件,所以肯定不是这个文件起作用,于是在上面的if (uevent_helper[0])
里加了一句调试信息,打印uevent_helper,内核启动相关打印信息如下:
uevent_helper is /sbin/hotplug
uevent_helper is /sbin/hotplug
s3c2410-rtc s3c2410-rtc: setting system clock to 2015-04-30 08:12:15 UTC (1430381535)
yaffs: dev is 32505858 name is "mtdblock2"
yaffs: passed flags ""
yaffs: Attempting MTD mount on 31.2, "mtdblock2"
yaffs: auto selecting yaffs2
block 646 is bad
yaffs_read_super: isCheckpointed 0
VFS: Mounted root (yaffs filesystem) on device 31:2.
Freeing init memory: 240K
Start Qtopia-2.2.0
uevent_helper is /sbin/mdev
uevent_helper is /sbin/mdev
看到没,刚开始确实是/sbin/hotplug,但后来就变成了/sbin/mdev。很据上面信息,我们知道是在文件系统启动的过程中
发生改变的。文件系统启动过程中,改变mdev的只有“echo /sbin/mdev > /proc/sys/kernel/hotplug”,也确实是这个
导致了uevent_helper的改变。
涉及到的数据在/kernel/sysctl.c下
#if defined(CONFIG_HOTPLUG) && defined(CONFIG_NET)
{
.ctl_name = KERN_HOTPLUG,
.procname = "hotplug",
.data = &uevent_helper,
.maxlen = UEVENT_HELPER_PATH_LEN,
.mode = 0644,
.proc_handler = &proc_dostring,
.strategy = &sysctl_string,
},
#endif
至于为什么“echo /sbin/mdev > /proc/sys/kernel/hotplug”能改变uevent_helper就是proc虚拟文件系统的内容了,
这里不讨论。
其实设置mdev有三种方法,总结如下:
1、编译内核的时候直接配置CONFIG_UEVENT_HELPER_PATH,并且在之后的启动中不去修改uevent_helper,那么
uevent_helper代表的程序就是CONFIG_UEVENT_HELPER_PATH指定的程序
2、不管CONFIG_UEVENT_HELPER_PATH配置与否或如何设置,通过echo /sbin/mdev > /sys/kernel/uevent_helper
修改uevent_helper的内容,这个指令将会调用内核函数uevent_helper_store。过程涉及sysfs虚拟文件系统的
内容,这里不讨论。改变之后,/proc/sys/kernel/hotplug里的内容也会立即发生改变
3、不管CONFIG_UEVENT_HELPER_PATH配置与否或如何设置,通过echo /sbin/mdev > /proc/sys/kernel/hotplug
修改uevent_helper的内容.它的修改也会导致/sys/kernel/uevent_helper里的内容立即改变
对于上述的2、3两种方法,都是通过用户层的接口直接uevent_helper,所以谁在后面谁起作用
三、busybox源码分析
内核源码的最后是调用uevent_helper指定的用户程序,这个用户程序通常是mdev,那么mdev如何做的呢,来看一下
busybox的源码。源码基于busybox-1.23.1
int mdev_main(int argc UNUSED_PARAM, char **argv)
xchdir("/dev"); // 先把目录改变到/dev下
if (argv[1] && strcmp(argv[1], "-s") == 0) { // 在文件系统启动的时候会调用 mdev -s,创建所有驱动设备节点
putenv((char*)"ACTION=add"); // mdev -s 的动作是创建设备节点,所以为add
if (access("/sys/class/block", F_OK) != 0) { // 当/sys/class/block目录不存在时,才扫描/sys/block
/* Scan obsolete /sys/block only if /sys/class/block
* doesn't exist. Otherwise we'll have dupes.
* Also, do not complain if it doesn't exist.
* Some people configure kernel to have no blockdevs.
*/
recursive_action("/sys/block",
ACTION_RECURSE | ACTION_FOLLOWLINKS | ACTION_QUIET,
fileAction, dirAction, temp, 0);
}
/*
* 这个函数是递归函数,它会扫描/sys/class目录下的所有文件,如果发现dev文件,将按照
* /etc/mdev.conf文件进行相应的配置。如果没有配置文件,那么直接创建设备节点
* 最终调用的创建函数是 make_device
*/
recursive_action("/sys/class",
ACTION_RECURSE | ACTION_FOLLOWLINKS,
fileAction, dirAction, temp, 0);
}
else{
// 获得环境变量,环境变量是内核在调用mdev之前设置的
env_devname = getenv("DEVNAME"); /* can be NULL */
G.subsystem = getenv("SUBSYSTEM");
action = getenv("ACTION");
env_devpath = getenv("DEVPATH");
snprintf(temp, PATH_MAX, "/sys%s", env_devpath);
make_device(env_devname, temp, op);
}
由以上代码分析可知,无论对于何种操作,最后都是调用make_device来创建节点,看一下这个函数
static void make_device(char *device_name, char *path, int operation)
int major, minor, type, len;
char *path_end = path + strlen(path); //path_end指定path结尾处
major = -1;
if (operation == OP_add) {
strcpy(path_end, "/dev"); // 往path结尾处拷贝“/dev”,这时path=/sys/class/test/test_dev/dev
len = open_read_close(path, path_end + 1, SCRATCH_SIZE - 1); // 打开并读取/sys/class/test/test_dev/dev
*path_end = '\0';
if (len < 1) {
if (!ENABLE_FEATURE_MDEV_EXEC)
return;
} else if (sscanf(path_end + 1, "%u:%u", &major, &minor) == 2) { //从/sys/class/test/test_dev/dev获得主次设备号
dbg1("dev %u,%u", major, minor);
} else {
major = -1;
}
}
if (operation == OP_add && major >= 0) // 如果是add,即创建节点
mknod(node_name, rule->mode | type, makedev(major, minor)) // 最终用mknod函数在/dev下创建设备节点
if (operation == OP_remove && major >= -1) // 如果是remove,即删除节点
unlink(node_name);
创建节点最后无非还是调用mknod,当然在class_create和device_create自动创建设备节点时,也会在/sys/class下自动创建
和删除相关设备类和设备,这是sysfs的驱动内容,这里不讲
转载地址:
http://www.cnblogs.com/myitm/archive/2012/05/27/2520614.html
https://blog.csdn.net/huangminilong/article/details/83783971
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。