OPENWRT 教程第四章 OpenWrt 挂载U盘 扩展篇OpenWRT Hotplug原理分析
作者:互联网
目录表
Hotplug原理
Hotplug即热插拔,在新版本OpenWRT
上,hotplug
,coldplug
与watchdog
等被集成到全新的Procd系统中。
Procd是OpenWRT
下新的预初始化,初始化,热插拔和事件系统。在openwrt
中, procd
作为 init
进程会处理许多事情, 其中就包括 hotplug
。procd
本身并不知道如何处理hotplug
事件,也没有必要知道,因为它只实现机制,而不实现策略。事件的处理是由配置文件决定的,这些配置文件即所谓的rules
.。老版本下独立的hotplug2
在r36987
被移除了。所以下面我们要介绍的就是新版本下Hotplug
的机制。
要了解Hotplug
运行的整个过程,首先得了解procd
系统的工作流程。才能从全局了解hotplug
是如何工作的。在这里我们重点介绍与hotplug
相关的procd
启动过程。
Procd启动过程分析
preinit()函数
void preinit(void) { char *init[] = { "/bin/sh", "/etc/preinit", NULL }; char *plug[] = { "/sbin/procd", "-h", "/etc/hotplug-preinit.json", NULL }; LOG("- preinit -\n"); plugd_proc.cb = plugd_proc_cb; plugd_proc.pid = fork(); if (!plugd_proc.pid) { execvp(plug[0], plug); ERROR("Failed to start plugd\n"); exit(-1); } if (plugd_proc.pid <= 0) { ERROR("Failed to start new plugd instance\n"); return; } uloop_process_add(&plugd_proc); setenv("PREINIT", "1", 1); preinit_proc.cb = spawn_procd; preinit_proc.pid = fork(); if (!preinit_proc.pid) { execvp(init[0], init); ERROR("Failed to start preinit\n"); exit(-1); } if (preinit_proc.pid <= 0) { ERROR("Failed to start new preinit instance\n"); return; } uloop_process_add(&preinit_proc); DEBUG(4, "Launched preinit instance, pid=%d\n", (int) preinit_proc.pid); }
-
创建子进程执行
/etc/preinit
脚本,此时PREINI
T环境变量被设置为1,主进程同时使用uloop_process_add()
把/etc/preinit
子进程加入uloop
进行监控,当/etc/preinit
执行结束时回调plugd_proc_cb()
函数把监控/etc/preinit
进程对应对象中pid
属性设置为0,表示/etc/preinit
已执行完成。 -
创建子进程执行
/sbin/procd -h /etc/hotplug-preinit.json
,主进程同时使用uloop_process_add()
把/sbin/procd
子进程加入uloop
进行监控,当/sbin/procd
进程结束时回调spawn_procd()
函数。 -
spawn_procd()
函数繁衍后继真正使用的/sbin/procd
进程,从/tmp/debuglevel
读出debug
级别并设置到环境变量DBGLVL
中,把watchdog fd
设置到环境变量WDTFD
中,最后调用execvp()
繁衍/sbin/procd
进程。
procd进程
在这里我们主要分析procd
的五个状态,分别为 STATE_EARLY
、STATE_INIT
、STATE_RUNNING
、STATE_SHUTDOWN
、STATE_HALT
,这5个状态将按顺序变化,当前状态保存在全局变量state
中,可通过procd_state_next()
函数使用状态发生变化。
static void state_enter(void) { char ubus_cmd[] = "/sbin/ubusd"; switch (state) { case STATE_EARLY: LOG("- early -\n"); watchdog_init(0); hotplug("/etc/hotplug.json"); procd_coldplug(); break; case STATE_INIT: // try to reopen incase the wdt was not available before coldplug watchdog_init(0); LOG("- ubus -\n"); procd_connect_ubus(); LOG("- init -\n"); service_init(); service_start_early("ubus", ubus_cmd); procd_inittab(); procd_inittab_run("respawn"); procd_inittab_run("askconsole"); procd_inittab_run("askfirst"); procd_inittab_run("sysinit"); break; case STATE_RUNNING: LOG("- init complete -\n"); break; case STATE_SHUTDOWN: LOG("- shutdown -\n"); procd_inittab_run("shutdown"); sync(); break; case STATE_HALT: LOG("- reboot -\n"); reboot(reboot_event); break; default: ERROR("Unhandled state %d\n", state); return; }; }
-
#####STATE_EARLY状态 - init前准备工作
- 初始化
watchdog
- 根据"
/etc/hotplug.json
"规则监听hotplug
procd_coldplug()
函数处理,把/dev
挂载到tmpfs
中,fork udevtrigger
进程产生冷插拔事件,以便让hotplug
监听进行处理udevstrigger
进程处理完成后回调procd_state_next()
函数把状态从STATE_EARLY
转变为STATE_INIT
- 初始化
-
#####STATE_INIT状态 - 初始化工作
- 连接
ubusd
,此时实际上ubusd
并不存在,所以procd_connect_ubus
函数使用了定时器进行重连,而uloop_run()
需在初始化工作完成后才真正运行。当成功连接上ubusd
后,将注册service main_object
对象,system_object
对象、watch_event
对象(procd_connect_ubus()
函数), - 初始化
services
(服务)和validators
(服务验证器)全局AVL tree
- 把
ubusd
服务加入services
管理对象中(service_start_early)
- 根据
/etc/inittab
内容把cmd
、handler
对应关系加入全局链表actions
中 - 顺序加载
respawn、askconsole、askfirst、sysinit
命令 sysinit
命令把/etc/rc.d/
目录下所有启动脚本执行完成后将回调rcdone()
函数把状态从STATE_INIT
转变为STATE_RUNNING
- 连接
-
#####STATE_RUNNING状态
- 进入
STATE_RUNNING
状态后procd
运行uloop_run()
主循环
- 进入
Hotplug原理图
Hotplug
原理的整个流程如下所示:
-----------------------
| procd daemon |
| (hotplug.json) |
-----------------------
netlink| socket user space
-------------------------------------------------
| kernel space
-----------------------
| (uevent [json]) |
| kernel |
-----------------------
主要过程分为以下两个部分:
- 内核发出uevent事件
内核使用uevent
事件通知用户空间,uevent
首先在内核中调用netlink_kernel_create()
函数创建一个socket
套接字,该函数原型在netlink.h
中定义。这是一种特殊类型的socket
,专门用于内核空间与用户空间的异步通信。kobject_uevent()
产生uevent
事件(/lib/kobject_uevent.c)
,事件的部分信息通过环境变量传递,如$ACTION, $DEVPATH, $SUBSYSTEM
等,产生的uevent
先由netlink_broadcast_filtered()
发出,最后调用uevent_helper[]
所指定的程序来处理。
在linux
中,uevent_helper[]
里默认指定”/sbin/hotplug”
,但可以通过/sys/kernel/uevent_helper(kernel/ksysfs.c)
或/proc/kernel/uevent_helper(kernel/sysctl.c)
来修改成指定的程序。 在新OpenWRT
中,并不使用user_helper[]
指定程序来处理uevent(/sbin/hotplug
不存在,在以前版本中存在),而是通过PF_NETLINK
套接字来获取来自内核空间的uevent
。
2. 用户空间监听uevent
在proc/plug/hotplug.c
中,创建一个PF_NETLINK
套接字来监听内核netlink_broadcast_filtered()
发出的uevent
。收到uevent
之后,在根据/etc/hotplug.json
里的描述,定位到对应的执行函数来处理。
通常情况下,/etc/hotplug.json
会调用/sbin/hotplug-call
来处理uevent
,它根据uevent
的$SUBSYSTEM
变量来分别调用/etc/hotplug.d
下不同目录中的脚本。/sbin/hotplug-call
脚本如下所示,这里面的$1
表示hotplug-call
的第一个参数:
root@OpenWrt:/sbin# cat hotplug-call #!/bin/sh # Copyright (C) 2006-2010 OpenWrt.org export HOTPLUG_TYPE="$1" . /lib/functions.sh PATH=/bin:/sbin:/usr/bin:/usr/sbin LOGNAME=root USER=root export PATH LOGNAME USER export DEVICENAME="${DEVPATH##*/}" [ \! -z "$1" -a -d /etc/hotplug.d/$1 ] && { for script in $(ls /etc/hotplug.d/$1/* 2>&-); do ( [ -f $script ] && . $script ); done }
下表是hotplug.json
的具体内容,重点关注蓝色字段。
root@OpenWrt:/etc# cat hotplug.json [ [ "case", "ACTION", { "add": [ [ "if", [ "and", [ "has", "MAJOR" ], [ "has", "MINOR" ], ], [ [ "if", [ "or", [ "eq", "DEVNAME", [ "null", "full", "ptmx", "zero" ], ], [ "regex", "DEVNAME", [ "^gpio", "^hvc" ], ], ], [ [ "makedev", "/dev/%DEVNAME%", "0666" ], [ "return" ], ] ], [ "if", [ "or", [ "eq", "DEVNAME", "mapper/control" ], [ "regex", "DEVPATH", "^ppp" ], ], [ [ "makedev", "/dev/%DEVNAME%", "0600" ], [ "return" ], ], ], [ "if", [ "has", "DEVNAME" ], [ "makedev", "/dev/%DEVNAME%", "0644" ], ], ], ], [ "if", [ "has", "FIRMWARE" ], [ [ "exec", "/sbin/hotplug-call", "%SUBSYSTEM%" ], [ "load-firmware", "/lib/firmware" ], [ "return" ] ] ], ], "remove" : [ [ "if", [ "and", [ "has", "DEVNAME" ], [ "has", "MAJOR" ], [ "has", "MINOR" ], ], [ "rm", "/dev/%DEVNAME%" ] ] ] } ], [ "if", [ "eq", "SUBSYSTEM", "platform" ], [ "exec", "/sbin/hotplug-call", "%SUBSYSTEM%" ] ], [ "if", [ "and", [ "has", "BUTTON" ], [ "eq", "SUBSYSTEM", "button" ], ], [ "exec", "/etc/rc.button/%BUTTON%" ] ], [ "if", [ "eq", "SUBSYSTEM", [ "net", "input", "usb", "ieee1394", "block", "atm", "zaptel", "tty", "button" ] ], [ "exec", "/sbin/hotplug-call", "%SUBSYSTEM%" ] ], [ "if", [ "and", [ "eq", "SUBSYSTEM", "usb-serial" ], [ "regex", "DEVNAME", [ "^ttyUSB", "^ttyACM" ] ], ], [ "exec", "/sbin/hotplug-call", "tty" ] ], ] [⬆]
Hotplug应用
U盘的自动挂载卸载
Hotplug
一个常见的实例应用就是U盘或SD卡等外设的自动挂载和卸载功能。所以这里我们主要介绍如何利用hotplug
实现U盘,移动硬盘等外设自动挂载的方法和原理。本文中的例子还需要根据实际情况作相应适配。
当然,首先得内核有相应的驱动程序支持才行。当U盘插入后,会产生uevent
事件,hotplug
收到这个内核广播事件后,根据uevent
事件json
格式的附带信息内容,在hotplug.json
中进行定位。事件包含的信息一般为如下所示:
ACTION(add), DEVPATH(devpath), SUBSYSTEM(block), MAJOR(8), MINOR(1), DEVNAME(devname), DEVTYPE(devtype), SEQNUM(865)
根据上面的信息,就可以在hotplug.json
中定位到两个条目,如上面hotplug.json
中蓝色显示字段。第一个条目执行的是makedev
,该命令会创建设备节点。第二个条目会根据附带信息中的ACTION, DEVPATH, SUBSYSTEM, DEVNAME, DEVTYPE
等变量,调用命令exec
去执行hotplug-call
脚本。
于是 hotplug-call
会尝试执行 /etc/hotplug.d/block/
目录下的所有可执行脚本。
所以我们可以在这里放置我们的自动挂载/卸载处理脚本。 例如,编写/etc/hotplug.d/block/30-usbmount
,填入以下内容实现U盘自动挂载,卸载:
#!/bin/sh [ "$SUBSYSTEM" = block ] || exit0 [ "$DEVTYPE" = partition -a"$ACTION" = add ] && { echo"$DEVICENAME" | grep 'sd[a-z][1-9]' || exit 0 test-d /mnt/$DEVICENAME || mkdir /mnt/$DEVICENAME mount -o iocharset=utf8,rw /dev/$DEVICENAME/mnt/$DEVICENAME || \ mount-o rw /dev/$i /mnt/$i } [ "$DEVTYPE" = partition -a"$ACTION" = remove ] && { echo"$DEVICENAME" | grep 'sd[a-z][1-9]' || exit 0 umount/mnt/$DEVICENAME && rmdir /mnt/$DEVICENAME }
Button按键的检测
在OpenWRT
中,按键的检测也是通过Hotplug
机制来实现的。
它首先写了一个内核模块:gpio_button_hotplug
, 用于监听按键,有中断和 poll
两种方式。然后在发出事件的同时, 将记录并计算得出的两次按键时间差也作为 uevent
变量发出来。这样在用户空间收到这个 uevent
事件时就知道该次按键按下了多长时间。
hotplug.json
中有描述, 如果 uevent
中含有 BUTTON
字符串, 而且 SUBSYSTEM
为 "button
", 则执行/etc/rc.button/
下的 %BUTTON%
脚本来处理。
细节描述如下: 当按键时,则触发button_hotplug_even
t函数(gpio-button-hotplug.c
)
调用button_hotplug_create_event
产生uevent
事件,调用button_hotplug_fill_event
填充事件(JSON
格式),并最终调用button_hotplug_work
发出uevent
广播。
上述广播,被守护进程procd
中的hotplug_handler (procd/plug/hotplug.c)
收到,并根据etc/hotplug.json
中预先定义的JSON
内容匹配条件,定位到对应的执行函数,具体如下所示,命中了两个条目,所以会依次执行这两个条目队列中的操作函数:
[ "if", [ "and", [ "has", "BUTTON" ], [ "eq", "SUBSYSTEM", "button" ], ], [ "exec", "/etc/rc.button/%BUTTON%" ] ], 和 [ "if", [ "eq", "SUBSYSTEM", [ "net", "input", "usb", "ieee1394", "block", "atm", "zaptel", "tty", "button" ] ], [ "exec", "/sbin/hotplug-call", "%SUBSYSTEM%" ] ],
在rc.button
目录下,我们定义了reset
按钮的执行脚本:
root@OpenWrt:/etc/rc.button# cat reset #!/bin/sh [ "${ACTION}" = "released" ] || exit 0 . /lib/functions.sh logger "$BUTTON pressed for $SEEN seconds" if [ "$SEEN" -lt 1 ] then echo "REBOOT" > /dev/console sync reboot elif [ "$SEEN" -gt 5 ] then echo "FACTORY RESET" > /dev/console jffs2reset -y && reboot & fi
从脚本中我们可以清晰地看出,当按键时间小于1s时,执行reboot
重启命令,当按键时间超过5s时,执行恢复出厂设置并重启命令。
第二个条目,由于默认情况下没有在/etc/hotplug.d
目录下创建button
子目录,因此执行为空。
使用 export DBGLVL=10; procd -h /etc/hotplug.json
截获一些打印信息看看:
root@OpenWrt:/etc/rc.button# export DBGLVL=10; procd -h /etc/hotplug.json procd:hotplug_handler_debug(404): {{"HOME":"\/","PATH":"\/sbin:\/bin:\/usr\/sbin:\/usr\/bin","SUBSYSTEM":"button","ACTION":"pressed","BUTTON":"reset","SEEN":"42949450","SEQNUM":"331"}} procd: rule_handle_command(355): Command: exec procd: rule_handle_command(357): /etc/rc.button/reset procd: rule_handle_command(358): procd: rule_handle_command(360): Message: procd: rule_handle_command(362): HOME=/ procd: rule_handle_command(362): PATH=/sbin:/bin:/usr/sbin:/usr/bin procd: rule_handle_command(362): SUBSYSTEM=button procd: rule_handle_command(362): ACTION=pressed procd: rule_handle_command(362): BUTTON=reset procd: rule_handle_command(362): SEEN=42949450 procd: rule_handle_command(362): SEQNUM=331 procd: rule_handle_command(363): procd: queue_next(281): Launched hotplug exec instance, pid=987 procd: rule_handle_command(355): Command: exec procd: rule_handle_command(357): /sbin/hotplug-call procd: rule_handle_command(357): button procd: rule_handle_command(358): procd: rule_handle_command(360): Message: procd: rule_handle_command(362): HOME=/ procd: rule_handle_command(362): PATH=/sbin:/bin:/usr/sbin:/usr/bin procd: rule_handle_command(362): SUBSYSTEM=button procd: rule_handle_command(362): ACTION=pressed procd: rule_handle_command(362): BUTTON=reset procd: rule_handle_command(362): SEEN=42949450 procd: rule_handle_command(362): SEQNUM=331 procd: rule_handle_command(363): procd: queue_proc_cb(286): Finished hotplug exec instance, pid=987 ...
接口状态检测
当接口状态出现ifup
或者ifdown
时,netifd
守护进程会调用call_hotplug()(/interface-event.c)
来处理这个事件,call_hotplug()
执行run_cmd()
,并且设置系统环境变量$ACTION, $INTERFACE, $DEVICE
, 同时调用hotplug_cmd_path(=DEFAULT_HOTPLUG_PATH=/sbin/hotplug-call
, 在netifd.h
中)并传入参数iface
。下表是上述变量的介绍。
---------------------------------------------------------------
变量名称 | 说明
---------------------------------------------------------------
ACTION 事件,如ifup,ifdown,ifupdate
---------------------------------------------------------------
INTERFACE 发生事件动作的接口名,如(wan, ppp0)
---------------------------------------------------------------
DEVICE 发生事件动作的物理接口名,如(eth0.1或br-lan)
---------------------------------------------------------------
这样用户空间脚本hotplug-call
就会将/etc/hotplug.d/iface
目录下的所有脚本执行一遍。
举例说明:
我们在iface
目录下编写一个脚本名字叫13-my-action
, 内容如下:
root@OpenWrt:/etc/hotplug.d/iface# cat 13-my-action #!/bin/sh [ "$ACTION" = ifup ] && { echo Device:$DEVICE Action:$ACTION "13-my-action" > /dev/console }
让接口down
,从下面的log
中可以看出,iface
下的自定义脚本被执行了一遍。
root@OpenWrt:/etc/hotplug.d/iface# ubus call network.interface.lan up [ 462.370000] IPv6: ADDRCONF(NETDEV_UP): eth1: link is not ready [ 462.370000] device eth1 entered promiscuous mode [ 462.380000] IPv6: ADDRCONF(NETDEV_UP): br-lan: link is not ready Device:br-lan Action:ifup 13-my-action [ 462.980000] eth1: link up (1000Mbps/Full duplex) [ 462.980000] br-lan: port 1(eth1) entered forwarding state [ 462.990000] br-lan: port 1(eth1) entered forwarding state [ 462.990000] IPv6: ADDRCONF(NETDEV_CHANGE): eth1: link becomes ready [ 463.040000] IPv6: ADDRCONF(NETDEV_CHANGE): br-lan: link becomes ready procd: Not starting instance igmpproxy::instance1, an error was indicated [ 464.990000] br-lan: port 1(eth1) entered forwarding state
[备注] 由于守护进程
netifd
在ubus
中注册了服务,因此我们可以通过ubus
调用netifd
提供的服务接口,例如使接口ifdown
命令为:ubus call network.interface.lan up/down
。
早期Hotplug2
早期的Hotplug
机制,单独运行守护进程,内核会指定hotplug2
进程来处理系统内核广播出来的uevent
事件。原理和上面介绍的大同小异,hotplug2
采用了与linux
中的udev
相同的rule
编写规则。
标签:sbin,U盘,procd,rule,hotplug,etc,Hotplug,uevent,OPENWRT 来源: https://www.cnblogs.com/vx-cg248805770/p/11551458.html