Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

wywincl/hotplug

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

3 Commits

Repository files navigation

OpenWRT Hotplug原理分析

本次研究基于OpenWRT 14_07 trunk。其他版本有部分差异,请阅读时注意。

  1. Hotplug原理
  2. Hotplug应用
  3. 参考

Hotplug即热插拔,在新版本OpenWRT上,hotplug,coldplugwatchdog等被集成到全新的Procd系统中。

ProcdOpenWRT下新的预初始化,初始化,热插拔和事件系统。在openwrt 中, procd 作为 init 进程会处理许多事情, 其中就包括 hotplugprocd本身并不知道如何处理hotplug事件,也没有必要知道,因为它只实现机制,而不实现策略。事件的处理是由配置文件决定的,这些配置文件即所谓的rules.。老版本下独立的hotplug2r36987被移除了。所以下面我们要介绍的就是新版本下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);
}
  1. 创建子进程执行/etc/preinit脚本,此时PREINIT环境变量被设置为1,主进程同时使用uloop_process_add()/etc/preinit子进程加入uloop进行监控,当/etc/preinit执行结束时回调plugd_proc_cb()函数把监控/etc/preinit进程对应对象中pid属性设置为0,表示/etc/preinit已执行完成。

  2. 创建子进程执行/sbin/procd -h /etc/hotplug-preinit.json,主进程同时使用uloop_process_add()/sbin/procd子进程加入uloop进行监控,当/sbin/procd进程结束时回调spawn_procd()函数。

  3. spawn_procd()函数繁衍后继真正使用的/sbin/procd进程,从/tmp/debuglevel读出debug级别并设置到环境变量DBGLVL中,把watchdog fd设置到环境变量WDTFD中,最后调用execvp()繁衍/sbin/procd进程。

procd进程

在这里我们主要分析procd的五个状态,分别为 STATE_EARLYSTATE_INITSTATE_RUNNINGSTATE_SHUTDOWNSTATE_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前准备工作

    1. 初始化watchdog
    2. 根据"/etc/hotplug.json"规则监听hotplug
    3. procd_coldplug()函数处理,把/dev挂载到tmpfs中,fork udevtrigger进程产生冷插拔事件,以便让hotplug监听进行处理
    4. udevstrigger进程处理完成后回调procd_state_next()函数把状态从STATE_EARLY转变为STATE_INIT
  • #####STATE_INIT状态 - 初始化工作

    1. 连接ubusd,此时实际上ubusd并不存在,所以procd_connect_ubus函数使用了定时器进行重连,而uloop_run()需在初始化工作完成后才真正运行。当成功连接上ubusd后,将注册service main_object对象,system_object对象、watch_event对象(procd_connect_ubus()函数),
    2. 初始化services(服务)和validators(服务验证器)全局AVL tree
    3. ubusd服务加入services管理对象中(service_start_early)
    4. 根据/etc/inittab内容把cmdhandler对应关系加入全局链表actions
    5. 顺序加载respawn、askconsole、askfirst、sysinit命令
    6. sysinit命令把/etc/rc.d/目录下所有启动脚本执行完成后将回调rcdone()函数把状态从STATE_INIT转变为STATE_RUNNING
  • #####STATE_RUNNING状态

    1. 进入STATE_RUNNING状态后procd运行uloop_run()主循环

Hotplug原理图

Hotplug原理的整个流程如下所示:

-----------------------
| procd daemon |
| (hotplug.json) |
-----------------------
		 netlink| socket			user space
-------------------------------------------------
				 |				 kernel space
-----------------------
| (uevent [json]) |
| kernel |
-----------------------

主要过程分为以下两个部分:

  1. 内核发出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" ]
 ],
]

[⬆]

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_event函数(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

[备注] 由于守护进程netifdubus中注册了服务,因此我们可以通过ubus调用netifd提供的服务接口,例如使接口ifdown命令为: ubus call network.interface.lan up/down

早期Hotplug2

早期的Hotplug机制,单独运行守护进程,内核会指定hotplug2进程来处理系统内核广播出来的uevent事件。原理和上面介绍的大同小异,hotplug2采用了与linux中的udev相同的rule编写规则。

[⬆]

https://openwrt.org/

About

the hotplug implements of OpenWRT

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

AltStyle によって変換されたページ (->オリジナル) /