Hotplug解决问题思路
Hotplug 简介
解决问题之前需要大致了解什么是hotplug,有一个比较简单的概念才好下手。
在很多嵌入式设备中,都有着hotplug(热插拔)的身影,热插拔可以帮助设备更快更稳定的处理和维护设备节点。
有 2 个不同角度来看待热插拔:
从内核角度看,热插拔是在硬件、内核和内核驱动之间的交互。
从用户角度看,热插拔是内核和用户空间之间,通过调用用户空间程序(如hotplug、udev 和 mdev)的交互。 当需要通知用户内核发生了某种热插拔事件时,内核才调用这个用户空间程序。
现在的计算机系统,要求 Linux 内核能够在硬件从系统中增删时,可靠稳定地运行。这就对设备驱动作者增加了压力,因为在他们必须处理一个毫无征兆地突然出现或消失的设备。
对于我的Openwrt的路由设备,Openwrt Wiki给的描述为:
Procd (the init system and process management daemon) executes scripts located in /etc/hotplug. d/ when certain events happen, like for example when an interface goes up or down, when a new storage drive is detected, or when a button is pressed. It can be very useful with PPPoE connection or in an unstable network, or to use hardware buttons.
This functionality emulates/extends what was done by the long retired Hotplug2 package.
当某些 events事件发生时,Procd(init系统和进程管理守护进程)会执行位于/etc/hotplug.d/中的脚本,例如当接口启动或关闭时,检测到新的存储驱动器时,或者按下按钮时.
当使用PPPoE连接或者在不稳定的网络中,或使用硬件按钮时非常有用。
该功能模块模拟/扩展了已淘汰的Hotplug2软件包的功能。当在中执行脚本时/etc/hotplug.d,Procd 通常会以环境变量的形式提供大量信息。
如果要查看它提供了什么环境变量,请编写一个包含以下行的脚本:
env > /tmp/envs_log.log并将其放在我们需要调试的文件中,然后触发连接到该文件夹的事件,然后可以通过阅读/tmp/envs_log.log文本文件 来查看传递了哪些环境变量。
问题描述
这次需要分析的问题是当我们网络设备虚拟接口过多的时候,firewall不能通过hotplug正确的被restart起来,我们先贴上firewall的hotplug的脚本代码:
fw3 -q network "$INTERFACE"#!/bin/sh
[ "$ACTION" = ifup -o "$ACTION" = ifupdate ] || exit 0
#IFUPDATE_DATA=1 means some paramters of dhcp are changed, for example, lease time change, it's no need to restart firewall for this change.
#[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" -a -z "$IFUPDATE_DATA" ] && exit 0
#IFUPDATE_ADDRESSES=1 means ip address is changed.
[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" ] && exit 0
/etc/init.d/firewall enabled || exit 0
fw3 -q network "$INTERFACE" >/dev/null || exit 0
fw3 -q restart
我们可以使用上面提到的env > /tmp/envs_log.log 来放入脚本中任意位置,来查看procd触发该hotplug传入的参数都有哪些,我简单贴上一个以供参考:
INTERFACE=vlan1USER=root
ACTION=ifup
SHLVL=2
HOME=/
HOTPLUG_TYPE=iface
LOGNAME=root
DEVICENAME=
TERM=linux
PATH=/usr/sbin:/usr/bin:/sbin:/bin
INTERFACE=vlan1095
PWD=/
DEVICE=br-vlan1095
USER=root
ACTION=ifup
SHLVL=2
HOME=/
HOTPLUG_TYPE=iface
LOGNAME=root
DEVICENAME=
TERM=linux
PATH=/usr/sbin:/usr/bin:/sbin:/bin
INTERFACE=vlan1
PWD=/
DEVICE=br-vlan
回到问题本身,我们通过在hotplug脚本中添加debug log,我们找到在12行:
fw3 -q network "$INTERFACE" >/dev/null || exit 0
当传入的 INTERFACE=vlan1的时候,并不会被正确的执行下去,而是走到了后面的exit 0,也就是退出了当前的脚本,所以并不会调到下面的fw3 restart,然而fw3是编译生成的一个用户空间应用程序,是编译时生成的,所以,我们如果想要知道为什么这条命令执行不成功,就需要去源码中去追,为了省篇幅,我直接贴上我debug的结果:
if (!fw3_ubus_connect())
error("Failed to connect to ubus");
在fw3的源码中,fw3_ubus_connect 这个函数执行失败:
bool
fw3_ubus_connect(void)
{
bool status = false;
uint32_t id;
struct ubus_context *ctx = ubus_connect(NULL);
struct blob_buf b = { };
if (!ctx)
goto out;
if (ubus_lookup_id(ctx, "network.interface", &id))
goto out;
if (ubus_invoke(ctx, id, "dump", NULL, dump_cb, NULL, 500))
goto out;
status = true;
if (ubus_lookup_id(ctx, "service", &id))
goto out;
blob_buf_init(&b, 0);
blobmsg_add_string(&b, "type", "firewall");
ubus_invoke(ctx, id, "get_data", b.head, procd_data_cb, NULL, 2000);
blob_buf_free(&b);
out:
if (ctx)
ubus_free(ctx);
return status;
}
最后定位到该函数的:
struct ubus_context *ctx = ubus_connect(NULL);
...
if (!ctx)
goto out;
可以看到ubus_connect为结构体赋值的时候失败, 但是追到这里的时候我也只是仅仅知道了是哪里出的错,但是并不知道为什么在restart的这个时间点VLAN1传入的时候会出错(restart 完成后在shell上手动输入fw3 -q network vlan1 并不会报错),但是追到这里实际上已经浪费了很多时间,我的leader也并不希望以修改源码的方式解决问题(因为我们项目已经到了量产阶段,所以每一次修改都需要长期验证,需要稳重,像是修改源码这样的事情不到万不得已肯定是不会去做的),所以后续如果我有时间的话我一定继续往下追并更新到博文中,那么问题来了,如何最稳妥的去解决这个问题呢?
Hotplug retry
既然不让改源码,当然要从hotplug的脚本下手了…
既然在传入vlan1的时候:
fw3 -q network "$INTERFACE" >/dev/null || exit 0
一直退出,那么我们在传入vlan1的时候加一个retry机制不就可以了嘛,很简单的思路,为什么非要和fw3源码对着干呢QAQ, 经过修改代码如下:
#!/bin/sh
retry_count=0
fail_flag=0
[ "$ACTION" = ifup -o "$ACTION" = ifupdate ] || exit 0
#IFUPDATE_DATA=1 means some paramters of dhcp are changed, for example, lease time change, it's no need to restart firewall for this change.
#[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" -a -z "$IFUPDATE_DATA" ] && exit 0
#IFUPDATE_ADDRESSES=1 means ip address is changed.
[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" ] && exit 0
/etc/init.d/firewall enabled || exit 0
if [ $INTERFACE = vlan1 ]; then
while true
do
fw3 -q network "$INTERFACE" >/dev/null
if [ $? -ne 0 ]; then
retry_count=$(($retry_count+1))
if [ ${retry_count} -eq 5 ]; then
break;
fi
else
break;
fi
done
fi
fw3 -q network "$INTERFACE" >/dev/null || exit 0
logger -t firewall "Reloading firewall due to $ACTION of $INTERFACE ($DEVICE)"
fw3 -q restart
echo 1 > /tmp/test_flag
这样在vlan1传入的时候,有5次试错的机会,如果在5次内成功,就可以正确执行下去,五次内不成功也可以正确的退出,不至于进入死循环。目前的解决方案就是这样,我后续还会继续追一下fw3的源码和ubus的通信机制更新到文章里。