OpenWRT Hotplug原理分析

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

目录表

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

Hotplug原理

Hotplug即热插拔,在新版本OpenWRT上,hotplugcoldplugwatchdog等被集成到全新的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

  1. 用户空间监听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_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/