Linux Mdev
Table of Contents
基本概念
简介
mdev官方的叫法是mini udev in busybox,mdev实际只是busybox的一个符号链接。执行mdev -s时会扫描/sys/class和/sys/block中的所有目录,如果目录中有名为dev的文件,就从中读取设备号,并利用设备号在/dev下面创建节点。
启动时需要设置热插拔处理程序为mdev,当有热插拔事件产生时,内核调用mdev。 mdev通过环境变量ACTION和DEVPATH确定热插拔事件和影响目录,接着查看目录下是否有dev文件,并利用其信息创建/dev节点。如果ACTION为add,就会创建设备节点,如果为remove则删除设备节点。
mdev通过判断路径字符串第6个字符串是否为c来判断是字符设备还是块设备,如path = "sys/class…"第6个字符为c,就被判断为字符设备, path = "sys/devices…"会被判断为块设备。所以写驱动只应当在/sys/class和/sys/block中创建设备属性文件。
编译配置
在busybox配置中加上mdev支持,此外如果要使用自动挂载功能,需要利用额外的脚本,也就是可能还需要其他常用工具,如grep、mount、sh等。
另外在内核配置上面也要添加文件系统支持,包括语言支持,既然要用到mdev也需要热插拔支持。
初始化脚本
mdev有两个用途,一个是初始化扫描,一个是动态更新。 mdev关键需要sysfs的支持,也就是必须要有/sys目录。在扫描的时候mdev会扫描/sys/class/…/dev文件,dev文件是包含设备号的文件,其所在的目录一般就对应为相关设备的属性目录。至于动态更新,需要在内核支持热插拔,当发现有插拔事件的时候,内核会发出通知,并调用hotplug程序,一般PC上是udev,嵌入式平台是mdev,通过写入如下文件来指定热插拔程序为mdev。
echo /bin/mdev > /proc/sys/kernel/hotplug
内核在调用热插拔处理程序的时候会传递一系列环境变量,在写mdev的配置文件时,也可以利用这里环境变量。
在嵌入式平台,要支持mdev,首先要做的是配置系统启动设置,包括挂载必要的文件系统,以及设置hotplug程序。如下是一段典型的初始化脚本。
mount -t proc proc /proc # 用于设定hotplug的文件系统 mount -t sysfs sysfs /sys # 用于mdev扫描的文件系统 mount -t tmpfs -o size=64k,mode=0755 tmpfs /media # 用于挂载磁盘设备 mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev # 用于创建设备节点 echo /bin/mdev > /proc/sys/kernel/hotplug # 设定hotplug程序 mdev -s # 启动扫描
如果文件系统在flash外运行,还需要先创建/dev/pts节点,再执行mdev的初始化脚本。
mkdir /dev/pts mount -t devpts devpts /dev/pts
也可以不通过procfs来设置hotplug程序,通过如下命令实现。
sysctl -w kernel.hotplug=/sbin/mdev
配置文件
配置文件位于/etc/mdev.conf,语法分为四个部分。第1部分是匹配规则,也就是匹配到对应的设备,第2部分是用户ID和组ID,第3部分是权限,最后一部分是命令。
文件格式如下:
[-][envmatch]<device_regex> <uid>:<gid> <permissions> [cmd]
[-][envmatch]@<maj[,min1[-min2]]> <uid>:<gid> <permissions> [cmd]
[-][envmatch]$envvar=<regex> <uid>:<gid> <permissions> [cmd]
[-][envmatch]subsystem/regex <uid>:<gid> <permissions> [cmd]
- -
- 不要在匹配到该行就停止搜索
- uid/gid
- 可以是数字也可以是名字
其中"$envvar=<regex>"在热插拔设备需要模块载入时非常有用,因为此时没有驱动,也就不会存在/sys/class/…/dev文件,但是$MODALIAS会设置,表示需要的模块。可以使用如下规则,在需要模块的时候自动导入模块。
$MODALIAS=.* 0:0 660 @modprobe "$MODALIAS"
当/sys/class/…/dev出现的时候又会产生另一个热插拔事件给mdev处理。
命令分两种,一种是控制设备节点生成路径,一种是执行外部shell命令,两者可以同时使用。
路径控制命令
路径控制的命令格式为
[=>!]path
- =
- 仅仅移动路径,如果要移动到一个目录下面,在path后面一定要有一个斜杠"/"。
- >
- 移动路径,并在dev下面创建一个符号链接
- !
- 禁止创建设备节点
- %1..%9
- 如果用了正则表达式,那么用该符号引用匹配到的表达式,一个小括号作为一组,依次用%1..%9表示。
简单示例如下。
hda 0:0 660 =drives/ # 将hda移动到drives目录 hdb 0:0 660 =cdrom # 将hdb命名为cdrom tty[a-z] 0:0 660 ! # 利用!阻止创建设备节点 ([hs]d[a-z]) root:disk 660 >disk/%1/0 ([hs]d[a-z])([0-9]+) root:disk 660 >disk/%1/%2 mmcblk([0-9]+) root:disk 660 >disk/mmc/%1/0 mmcblk([0-9]+)p([0-9]+) root:disk 660 >disk/mmc/%1/%2 (tun|tap) root:network 660 >net/%1
外部命令
要执行外部命令,需要在特殊符号后面加上要执行的命令,特殊符号用于指示执行的时间。命令是通过system系统调用执行的,因此要确保有安装sh。命令会获取到两个环境变量,$SUBSYSTEM和$MDEV,$SUBSYSTEM表示设备所在的子系统, $MDEV为设备名字,如hda。
[@|$|*]<command>
- @
- 在创建设备节点之后运行
- $
- 在移除设备节点之前运行
- *
- 在创建设备节点之后,移除设备节点之前运行
hotplug将stdout、stderr和stdin连接到/dev/null,因此在执行mdev的时候是看不到输出的。
配置举例:
- eg0-mdev.conf @github
- eg1-mdev.conf @snafu
- clfs mdev.conf
固件
所有的firmware需要放到/lib/firmware,运行的时候,内核调用mdev,并传递文件名,文件名是在源代码中直接指定的。
顺序插拔
内核并不会将热插拔事件顺序化,仅仅增加SEQNUM环境变量的值, mdev可能会按照不同的顺序处理热插拔事件。
如果发现了/dev/mdev.seq那么就会和SEQNUM比较,有两秒钟的比较时间,如果不相同,会按照通常方式运行,并写入SEQNUM + 1。想要激活这个特性非常简单,执行如下命令。
echo > /dev/mdev.seq
就会会插入一个换行符,但是mdev足够聪明,它不会在这种情况下去等待两秒钟。
热插拔配置
插拔自动挂载需要先写一个脚本,假定脚本为/lib/mdev/automounter.sh,并且要确保文件有可执行权限,内容如下。
#!/bin/sh destdir=/media do_umount() { if grep -qs "^/dev/$1 " /proc/mounts ; then umount "${destdir}/$1"; fi [ -d "${destdir}/$1" ] && rmdir "${destdir}/$1" } do_mount() { mkdir -p "${destdir}/$1" || exit 1 if ! mount -t auto "/dev/$1" "${destdir}/$1"; then rmdir "${destdir}/$1" exit 1 fi } case "${ACTION}" in add|"") do_umount ${MDEV} do_mount ${MDEV} ;; remove) do_umount ${MDEV} ;; esac
接着根据automounter.sh配置mdev.conf文件。
sd[a-z] 0:0 660 mmcblk[0-9] 0:0 660 sd[a-z][0-9] 0:0 660 */lib/mdev/automounter.sh mmcblk[0-9]p[0-9] 0:0 660 */lib/mdev/automounter.sh
此外还要修改一下/media目录文件系统类型,因为打算用它创建动态挂在点,所以最好将它用tmpfs从Flash移动到RAM中。可以通过上面提到的方法,写入到启动脚本,可以编辑/etc/fstab。
tmpfs /media tmpfs defaults 0 0
如果存在/dev/mdev.log文件,调试信息将自动添加到该文件。
源代码分析
消息打印
分级打印
#if DEBUG_LVL >= 1 # define dbg1(...) do { if (G.verbose) bb_error_msg(__VA_ARGS__); } while(0) #else # define dbg1(...) ((void)0) #endif #if DEBUG_LVL >= 2 # define dbg2(...) do { if (G.verbose >= 2) bb_error_msg(__VA_ARGS__); } while(0) #else # define dbg2(...) ((void)0) #endif #if DEBUG_LVL >= 3 # define dbg3(...) do { if (G.verbose >= 3) bb_error_msg(__VA_ARGS__); } while(0) #else # define dbg3(...) ((void)0) #endif
这段代码无非是定义了不同级别的打印。但是还是有些细节需要说明的。
- …
- 省略号用于传递可变参数,__VA_ARGS__用于解析可变参数。
- G
- 在busybox的代码中,对G的定义随处可见,大多数源代码中都有一个叫G的宏,在mdev.c中定义如下。
#define G (*(struct globals*)&bb_common_bufsiz1)
很明显,这里的G就是一块全局的内存,只不过把它当作globals来使用。至于globals,和G的思路差不多,就是每个程序将其最重要的数据结构定义为globals,所以globals也是随处可见的。
// <platform.h> #define ALIGNED(m) __attribute__ ((__aligned__(m))) // libbb/message.c char bb_common_bufsiz1[COMMON_BUFSIZE] ALIGNED(sizeof(long long));
属性操作符让让bb_common_bufsize1按照long long类型对齐,如果再纠结一下COMMON_BUFSIZE,可以看到它的定义是这样的。
// <libbb.h> extern char bb_common_bufsiz1[COMMON_BUFSIZE]; #ifndef BUFSIZ # define BUFSIZ 4096 enum { COMMON_BUFSIZE = (BUFSIZ >= 256 * sizeof(void*) ? BUFSIZ + 1 : 256 * sizeof(void*)) };
消息打印函数
最终要打印数据是要落实到bb_error_msg的,多数情况下打印代码都是写成宏的,这里的代码则是用函数来实现。
void FAST_FUNC bb_error_msg(const char *s, ...) { va_list p; va_start(p, s); bb_verror_msg(s, p, NULL); va_end(p); }
解析可变参数的典型写法,如果这里看不明白,没有关系,接下来在下面一个函数详细分析。不过这里还有一个细节,FAST_FUNC,其实就是指定一些属性,不妨看看定义。
/* FAST_FUNC is a qualifier which (possibly) makes function call faster * and/or smaller by using modified ABI. It is usually only needed * on non-static, busybox internal functions. Recent versions of gcc * optimize statics automatically. FAST_FUNC on static is required * only if you need to match a function pointer's type */ #if __GNUC_PREREQ(3,0) && defined(i386) /* || defined(__x86_64__)? */ /* stdcall makes callee to pop arguments from stack, not caller */ # define FAST_FUNC __attribute__((regparm(3),stdcall)) /* #elif ... - add your favorite arch today! */ #else # define FAST_FUNC #endif
regparam用于告诉编译器要用几个寄存器来传递参数,在0x86上最大为3(EAX/EDX/ECX),多个attribute用逗号分割,注意不要在这里加上多余的空格,而顺序是无所谓的,可以将stdcall放在前面。
1: void FAST_FUNC bb_verror_msg(const char *s, va_list p, const char* strerr) 2: { 3: char *msg, *msg1; 4: int applet_len, strerr_len, msgeol_len, used; 5: 6: if (!logmode) 7: return; 8: 9: if (!s) /* nomsg[_and_die] uses NULL fmt */ 10: s = ""; /* some libc don't like printf(NULL) */ 11: 12: used = vasprintf(&msg, s, p); 13: if (used < 0) 14: return; 15: 16: /* This is ugly and costs +60 bytes compared to multiple 17: * fprintf's, but is guaranteed to do a single write. 18: * This is needed for e.g. httpd logging, when multiple 19: * children can produce log messages simultaneously. */ 20: 21: applet_len = strlen(applet_name) + 2; /* "applet: " */ 22: strerr_len = strerr ? strlen(strerr) : 0; 23: msgeol_len = strlen(msg_eol); 24: /* can't use xrealloc: it calls error_msg on failure, 25: * that may result in a recursion */ 26: /* +3 is for ": " before strerr and for terminating NUL */ 27: msg1 = realloc(msg, applet_len + used + strerr_len + msgeol_len + 3); 28: if (!msg1) { 29: msg[used++] = '\n'; /* overwrites NUL */ 30: applet_len = 0; 31: } else { 32: msg = msg1; 33: /* TODO: maybe use writev instead of memmoving? Need full_writev? */ 34: memmove(msg + applet_len, msg, used); 35: used += applet_len; 36: strcpy(msg, applet_name); 37: msg[applet_len - 2] = ':'; 38: msg[applet_len - 1] = ' '; 39: if (strerr) { 40: if (s[0]) { /* not perror_nomsg? */ 41: msg[used++] = ':'; 42: msg[used++] = ' '; 43: } 44: strcpy(&msg[used], strerr); 45: used += strerr_len; 46: } 47: strcpy(&msg[used], msg_eol); 48: used += msgeol_len; 49: } 50: 51: if (logmode & LOGMODE_STDIO) { 52: fflush_all(); 53: full_write(STDERR_FILENO, msg, used); 54: } 55: #if ENABLE_FEATURE_SYSLOG 56: if (logmode & LOGMODE_SYSLOG) { 57: syslog(LOG_ERR, "%s", msg + applet_len); 58: } 59: #endif 60: free(msg); 61: }
- 6
- logmode是一个全局变量,用于指示日志级别,或者说要将日志打印到哪里去。包括LOGMODE_NONE、LOGMODE_STDIO、LOGMODE_SYSLOG、LOGMODE_BOTH
- 9
- 特殊处理NULL指针
- 12
- vasprintf和vsprintf唯一不同之处在于该函数会分配空间,从传递&msg二级指针就应该想到该函数会修改指针msg。另一方面vsprintf和sprintf的区别是v开头表示接收的参数是va_list类型。
- 21
- applet_name是全局变量
- 23
- 换行符定义,全局变量,默认为"\n"
- 27
- 增加额外格式信息到msg1中,故需要重新分配空间。
- 28-30
- 重新分配失败,不添加额外信息。
- 31-48
- 无非是将信息重组
- 51-53
- 刷新STDIO,full_write起始就是将msg写入到标准错误文件描述符,由busybox自己定义,full_write保证足量写入,在遇到错误或EINTR时能够及时退出。
- 56-57
- 借助syslog()将信息打印到系统日志中去
数据结构
struct rule { bool keep_matching; bool regex_compiled; mode_t mode; int maj, min0, min1; struct bb_uidgid_t ugid; char *envvar; char *ren_mov; IF_FEATURE_MDEV_EXEC(char *r_cmd;) regex_t match; struct envmatch *envmatch; };
- IF_FEATURE_MDEV_EXEC
- 用于支持命令行,这个功能非常实用,建议开启,要执行额外的脚本也需要该选项支持。后面认为是开启的,不再额外说明。
- keep_matching
- 如果配置文件中以"-"作为开头,表示匹配完之后继续匹配,此时会置起该成员。
struct globals { int root_major, root_minor; smallint verbose; char *subsystem; char *subsys_env; /* for putenv("SUBSYSTEM=subsystem") */ #if ENABLE_FEATURE_MDEV_CONF const char *filename; parser_t *parser; struct rule **rule_vec; unsigned rule_idx; #endif struct rule cur_rule; char timestr[sizeof("60.123456")]; } FIX_ALIASING; #define G (*(struct globals*)&bb_common_bufsiz1) #define INIT_G() do { \ IF_NOT_FEATURE_MDEV_CONF(G.cur_rule.maj = -1;) \ IF_NOT_FEATURE_MDEV_CONF(G.cur_rule.mode = 0660;) \ } while (0)
- root_major/root_minor
- 根目录的主设备号/次设备号
- ENABLE_FEATURE_MDEV_CONF
- 表示支持mdev.conf文件,对mdev的行为进行配置,如果没有这个宏,很显然就无法按照用户的要求来工作,也就是说一般都是需要开启的。后面遇到这个宏就直接默认为是打开的,不再特别提示。
- filename
- 这个文件就是配置文件mdev.conf,这个是写死的,只能是/etc/mdev.conf。
#define MAX_SYSFS_DEPTH 3 #define SCRATCH_SIZE 128
- MAX_SYSFS_DEPTH
- 最大支持扫描深度为3层,比方
/sys/block/sda/sda1
就已经到极限了,再往下就不支持了。
基本操作
static void make_default_cur_rule(void) { memset(&G.cur_rule, 0, sizeof(G.cur_rule)); G.cur_rule.maj = -1; /* "not a @major,minor rule" */ G.cur_rule.mode = 0660; } static void clean_up_cur_rule(void)
- clean_up_cur_rule
- 清除所有数据,并设置为默认值, rule里面动态分配的存储也会释放。
static char *parse_envmatch_pfx(char *val) { struct envmatch **nextp = &G.cur_rule.envmatch; for (;;) { struct envmatch *e; char *semicolon; char *eq = strchr(val, '='); if (!eq /* || eq == val? */) return val; if (endofname(val) != eq) return val; semicolon = strchr(eq, ';'); if (!semicolon) return val; /* ENVVAR=regex;... */ *nextp = e = xzalloc(sizeof(*e)); nextp = &e->next; e->envname = xstrndup(val, eq - val); *semicolon = '\0'; xregcomp(&e->match, eq + 1, REG_EXTENDED); *semicolon = ';'; val = semicolon + 1; } }
- 8
- 查找val中的'='
- 11
- 确定val名字后面的第一个字符为eq,其实就是看'='前面的部分是不是合法的名字,至于名字的定义和C/C++一样,可以是下划线和字母开头,其它部分可以是下划线数字和字符。辅助宏是如下这样的。
#define is_name(c) ((c) == '_' || isalpha((unsigned char)(c))) #define is_in_name(c) ((c) == '_' || isalnum((unsigned char)(c)))
- 13
- 查找val中的';',因为环境变量是以分号分割的
- 18
- 很显然这里是构造一个单向链表,链表头由G.cur_rule.envmatch保存
- 19
- 将名字保存到e->envname中,由xstrndup负责分配空间
- 20 & 22
- 这样的写法比较好,免去了重新分配空间
- 21
- xregcomp实际是调用regcomp()函数,这个函数的作用是编译正则表达式,以便regexec()执行搜索动作,REG_EXTENDED表示使用POSIX扩展正则表达式语法。
复杂操作
recursive_action
typedef int FAST_FUNC (*fileAction)(const char *fileName, struct stat *statbuf, void* userData, int depth); typedef int FAST_FUNC (*dirAction)(const char *fileName, struct stat *statbuf, void* userData, int depth); int FAST_FUNC recursive_action(const char *fileName, unsigned flags, fileAction, dirAction, void* userData, unsigned depth);
make_device
static void make_device(char *device_name, char *path, int operation);
主函数
#define UNUSED_PARAM __attribute__ ((__unused__)) int mdev_main(int argc UNUSED_PARAM, char **argv)
- UNUSED_PARAM
- 提示编译器这个参数不使用,不要报警。
分配/初始化变量
#if ENABLE_FEATURE_BUFFERS_GO_ON_STACK #define RESERVE_CONFIG_BUFFER(buffer,len) char buffer[len] #define RESERVE_CONFIG_UBUFFER(buffer,len) unsigned char buffer[len] #define RELEASE_CONFIG_BUFFER(buffer) ((void)0) #else #if ENABLE_FEATURE_BUFFERS_GO_IN_BSS #define RESERVE_CONFIG_BUFFER(buffer,len) static char buffer[len] #define RESERVE_CONFIG_UBUFFER(buffer,len) static unsigned char buffer[len] #define RELEASE_CONFIG_BUFFER(buffer) ((void)0) #else #define RESERVE_CONFIG_BUFFER(buffer,len) char *buffer = xmalloc(len) #define RESERVE_CONFIG_UBUFFER(buffer,len) unsigned char *buffer = xmalloc(len) #define RELEASE_CONFIG_BUFFER(buffer) free(buffer) #endif #endif RESERVE_CONFIG_BUFFER(temp, PATH_MAX + SCRATCH_SIZE); INIT_G(); #if ENABLE_FEATURE_MDEV_CONF G.filename = "/etc/mdev.conf"; #endif
上面的宏无非是选择从哪个地方分配temp而已,PATH_MAX一般表示名字长度,256,而SCRATCH_SIZE则是mdev.c中定义的,128。
umask(0); // 设置umask xchdir("/dev"); // 切换到目录/dev
扫描sysfs
mdev程序只支持一个参数,如果这个参数为"-s",那么就会执行扫描工作。
- 初始化G(剩余参数,如rulue_vec,root_major/root_minor等)
if (access("/sys/class/block", F_OK) != 0) { /* 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); } recursive_action("/sys/class", ACTION_RECURSE | ACTION_FOLLOWLINKS, fileAction, dirAction, temp, 0);
现在系统大多都有/sys/class/block
目录,因此很少需要扫描/sys/block
目录了。
处理热插拔事件
当mdev不是以"-s"参数调用时,说明内核发生了热插拔事件。 mdev实际上只对两种事件作出反应,即add/remove,都是调用函数make_device()来创建或者删除设备节点。