Ubuntu 22.04上/etc/init.d/softIOC脚本解析
如何在Linux上设立一个软IOC框架
以下说明是基于我们的Debian Linux机器。其它发行版(或其他Unixes)会有不同命令和对于东西有不同位置。这对我添加到本页的Debian /etc/init.d尤其如此。如果你为一个不同发行版创建一个不同的脚本,请添加它到本页。其它人将能够使用它。但基本步骤在所有发行版上相同。我已经认为了解了基本的系统管理任务(创建用户账户等)。
介绍
我们为什么做这件事?
当在生产中使用软IOC时,它们应该被当作重要的系统服务:
- 软IOCs应该由系统启动和停止。
- 应该有后备系统,遇到硬件故障,你可以简单地切换到这个后备系统。
其它目标是:用如用于VME IOCs相同地方法,应用程序开发这应该能够在无需对主机进行root访问下重置软IOC。
- IOC应用开发者应该能够手动启动和停止IOCs。
当多个软IOC共享相同主机(和相同IP地址)时,通道访问不能区分它们。访问安全将不能区分来自不同软IOCs的CA连接。在调试客户端时,CA不能告诉你一个连接去向哪个软IOCs。
- 即使不同软IOCs驻留在相同机器上,通道访问应该能够区分不同的软IOCs。
我曾考虑使用一个虚拟化层(基于VMware)在一个封装环境中运行软IOCs。我发现工作量太高,此层太厚,并且预计的性能损失太大--仅得到了每个软IOIC一个单独IP地址。
在调试和/或尝试查看在一个IOC上发生了什么时,开发者不必要知道数据库运行在一个基于VME上还是在一个基于主机的软IOC上。
- Console访问(以及记录console输出日志)应该是统一的:对于软IOCs和VME IOCs使用相同方式。
实现这个目标所需的设置在文档。
概念
要使访问安全区分软IOCs,在不同用户名下运行它们。
procServ工具将用做一个环境,它允许在后台启动软IOCs并且之后连接到它们的consoles,非常类型VME IOCs的串行consoles。(见EPICS Related Software — EPICS Documentation documentation (epics-controls.org)上procServ链接。之前,使用screen工具,但报告的问题,例如,在console访问后IOCs挂起,使得我们切花到复杂性更低的东西)。
/etc/init.d/softIOC是用于将EPICS软IOC作为系统服务启动的脚本:参考网页https://wiki-ext.aps.anl.gov/epics/index.php/How_to_Set_Up_a_Soft_IOC_Framework_on_Linux
这个脚本的描述:
DESC="EPICS soft IOCs"
这个脚本的完整路径:
SCRIPTNAME=/etc/init.d/softIOC
获取主机名称:
HOST=`uname -n`
设置procServ程序的路径
PROCSERV=/usr/bin/procServ
设置配置文件的完整路径:
CONFFILE=/usr/local/EPICS/program/softIOC/softiocs.orangepi5plus
设置IOC应用程序所在的家目录:
HOMEDIRS=/usr/local/EPICS/program
检查配置文件 , 不能读取配置文件,显示不能找到配置文件,并且以错误码1退出本脚本。
if [ ! -r $CONFFILE ] then echo "Error: Can't find configuration file $CONFFILE!" exit 1 fi
此函数函数:清理环境变量"CA_AUTO" "CA_ADDR" "CA_PORT" "IOC_USER" "PORT"。
clear_options() { for option in "CA_AUTO" "CA_ADDR" "CA_PORT" "IOC_USER" "PORT" do unset $option; done }
clear_options()的测试脚本如下:
echo "=============test clear_options() =====================" export CA_AUTO="NO" export CA_ADDR="127.0.0.1" export CA_PORT=5065 export IOC_USER="blctrl" export PORT="20000" echo "CA_AUTO=$CA_AUTO" echo "CA_ADDR=$CA_ADDR" echo "CA_PORT=$CA_PORT" echo "IOC_USER=$IOC_USER" echo "PORT=$PORT" echo "call clear_options()" clear_options echo "called clear_options()" echo "CA_AUTO=$CA_AUTO" echo "CA_ADDR=$CA_ADDR" echo "CA_PORT=$CA_PORT" echo "IOC_USER=$IOC_USER" echo "PORT=$PORT" echo "===================end=================================" echo ""
测试结果:
=============test clear_options() ===================== CA_AUTO=NO CA_ADDR=127.0.0.1 CA_PORT=5065 IOC_USER=blctrl PORT=20000 call clear_options() called clear_options() CA_AUTO= CA_ADDR= CA_PORT= IOC_USER= PORT= ===================end=================================
根据读入参数设置环境变量:
evaluate_options() { while [ $# != 0 ] do # 将第一个参数的小写字符全部替换成大写字母 TAG=`echo $1 | tr [:lower:] [:upper:]` case "$TAG" in "#") ;; # 匹配”#”,则进入下次循环 "CA_AUTO" | "CA_ADDR" | "CA_PORT" | "COREDUMPSIZE" | \ "HOMEDIR" | "BOOTDIR" | "IOC_USER" | "PORT" ) # 匹配这些字符 # 测试当前选项值的存在 OPTION=$TAG shift if [ -z $TAG -o $TAG = "#" ] # TAG变量为空或者内容为”#” then echo "$CONFFILE: Value(s) required for $TAG."; exit 1 else VALUE=$1 shift fi # 如果多个值跟随,也分配它们 while [ $1 != '#' -a $# != 0 ] do VALUE="$VALUE $1" shift; done eval ${OPTION}=\$VALUE ;; *) echo "$CONFFILE: Unknown option $1." exit 1 esac shift done }
测试evaluate_options()函数的脚本:
echo "============================Test evaluate_options=============================" ARGS="CA_AUTO NO # CA_ADDR 127.0.0.1 # CA_PORT 5065 5066 # IOC_USER blctrl # PORT 20000" evaluate_options ${ARGS} echo CA_AUTO=$CA_AUTO echo CA_ADDR=$CA_ADDR echo CA_PORT=$CA_PORT echo IOC_USER=$IOC_USER echo PORT=$PORT echo "============================Test evaluate_options==============================="
测试结果如下:
============================Test evaluate_options============================= CA_AUTO=NO CA_ADDR=127.0.0.1 CA_PORT=5065 5066 IOC_USER=blctrl PORT=20000 ============================Test evaluate_options===============================
为选项设置IOC环境变量默认值可能在配置文件中被重写:
- BOOTDIR:IOC启动目录。
- HOMEDIR:IOC应用程序顶层目录
- PIDFILE:生产的pid文件路径
- ENVFILE:环境文件路径
- IOC_USER:IOC应用程序的用户
default_options() { IOC_LC=$1 BOOTDIR=$HOMEDIRS/$LOC_LC/iocBoot/ioc$LOC_LC/ HOMEDIR=$HOMEDIRS/$IOC_LC PIDFILE=$HOMEDIR/$IOC_LC.pid ENVFILE=$HOMEDIR/$IOC_LC.env IOC_USER=blctrl }
default_options()的测试脚本如下:
#echo "===================Test default_options()==============" default_options "aiDriver" echo "BOOTDIR=$BOOTDIR" echo "HOMEDIRS=$HOMEDIRS" echo "HOMEDIR=$HOMEDIR" echo "PIDFILE=$PIDFILE" echo "IOC_USER=blctrl" #echo "===================end=================================" #echo ""
测试结果如下:
BOOTDIR=/usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/ HOMEDIRS=/usr/local/EPICS/program HOMEDIR=/usr/local/EPICS/program/aiDriver PIDFILE=/usr/local/EPICS/program/aiDriver/aiDriver.pid IOC_USER=blctrl
从配置文件中读取配置信息,并且生产一个如以下格式的
assign_options() { TAG=$1 SECTION=`sed -n "/^$TAG:/I,/^[\t ]*$/p" $CONFFILE | \ # 查找$TAG段落 sed -n '/^[^#]/p' | \ # 删除注释 sed -e 's/^[ \t]*//' -e 's/[ \t]*$//' \ # 删除开头和结尾空白 -e "s/$TAG://I" \ # 移除$TAG:标签 -e :a -e '/\\\\$/N; s/\\\\\\n//; ta' \ # 将以一个”\”结尾的行合并 -e 's/$/ \#/' \ # 用一个”#”标记选项结尾 -e 's/[\t ]/ /g'` #移除不必要的空白 evaluate_options $SECTION }
assign_options的测试脚本如下:
echo "=================Test assign_options()=================" assign_options aiDriver echo SECTION=$SECTION echo PORT=$PORT echo CA_AUTO=$CA_AUTO echo CA_ADDR =$CA_ADDR echo CA_PORT =$CA_PORT echo IOC_USER =$IOC_USER echo "===================end=================================" echo ""
配置文件所在完整路径/usr/local/EPICS/program/softIOC/softiocs.orangepi5plus:
AUTO: aiDriver GLOBAL: aiDriver: # 配置端口号 PORT 20000 CA_AUTO NO CA_ADDR 192.168.50.10 CA_PORT 5080 IOC_USER blctrl PORT 20000
测试结果如下:
=================Test assign_options()================= SECTION= # PORT 20000 # CA_AUTO NO # CA_ADDR 192.168.50.10 # CA_PORT 5080 # IOC_USER blctrl # PORT 20000 # PORT=20000 CA_AUTO=NO CA_ADDR =192.168.50.10 CA_PORT =5080 IOC_USER =blctrl
从命令行或配置文件中AUTO:项获取IOCs。测试配置文件中的匹配段落。
get_iocs() { if [ $# = 0 ] then TEST_LIST=`grep -i '^AUTO:' "$CONFFILE" | cut -d: -f2-` else TEST_LIST="$@" fi CHECKED_LIST="" for IOC in $TEST_LIST do grep -qi "^$IOC:" $CONFFILE if [ $? = 0 ] then CHECKED_LIST="$CHECKED_LIST $IOC" fi done echo $CHECKED_LIST }
get_iocs()的测试脚本如下:
echo "===============Test get_iocs()=========================" echo "call get_iocs" get_iocs echo "call get_iocs aiDriver" get_iocs aiDriver echo "===================end=================================" echo ""
测试结果如下:
===============Test get_iocs()========================= call get_iocs aiDriver call get_iocs aiDriver aiDriver ===================end=================================
此脚本第一方面设置环境变量设置字符串,例如CA_AUTO环变量不为空,则将其设置到环境变量,其它CA_XXX环境变量的设置类似,第二方面为procserv程序设置选项。
set_cmdenvopts() { EPICS_CA_AUTO_ADDR_LIST中,并且进行导出 SETENV="LINES=60 "`test ! -z "$CA_AUTO" && echo "export EPICS_CA_AUTO_ADDR_LIST=\"$CA_AUTO\";"` SETENV="$SETENV "`test ! -z "$CA_ADDR" && echo "export EPICS_CA_ADDR_LIST=\"$CA_ADDR\";"` SETENV="$SETENV "`test ! -z "$CA_PORT" && echo "export EPICS_CA_SERVER_PORT=\"$CA_PORT\";"` SETENV="$SETENV "`test ! -z "$BOOTDIR" && echo "export BOOTDIR=\"$BOOTDIR\";"` PROCSERVOPTS=`test ! -z "$IOC_USER" && echo "-n \"$IOC_USER\""` PROCSERVOPTS="$PROCSERVOPTS "`test ! -z "$COREDUMPSIZE" && echo "--coresize \"$COREDUMPSIZE\""` PROCSERVOPTS="$PROCSERVOPTS -q -c $BOOTDIR -p $PIDFILE -i ^D^C^] $PORT" }
set_cmdenvpots()的测试脚本如下:
echo "================Test set_cmdenvopts()==================" default_options aiDriver assign_options aiDriver set_cmdenvopts echo "SETENV=$SETENV" echo "PROCSERVOPTS=$PROCSERVOPTS" echo "===================end=================================" echo ""
测试结果如下:
SETENV变量内容可以设置以下变量:
- LINES=60
- export EPICS_CA_AUTO_ADDR_LIST="NO"
- export EPICS_CA_ADDR_LIST="127.0.0.1"
- export EPICS_CA_SERVER_PORT="5080";
- export BOOTDIR="/usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/"
PROCSERVPORTS内容可以设置procserv的选项:
- -n "blctrl" :在所有服务程序消息中,使用blctrl替代完整命令行来增加可读性。
- -q:不要写信息输出(服务程序)。当作为系统脚本的一部分运行时,避免弄乱屏幕。
- -c /usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/:在启动子进程前,切换目录为 /usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/。
- -p /usr/local/EPICS/program/aiDriver/aiDriver.pid:写服务进程的PID到file以便于集成到常规的系统服务管理机制中。
- -i ^D^C^]:忽略访问连接上再chars中所有字符。
- 20000:本地ftp连接端口。
================Test set_cmdenvopts()================== SETENV=LINES=60 export EPICS_CA_AUTO_ADDR_LIST="NO"; export EPICS_CA_ADDR_LIST="127.0.0.1"; export EPICS_CA_SERVER_PORT="5080"; export BOOTDIR="/usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/"; PROCSERVOPTS=-n "blctrl" -q -c /usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/ -p /usr/local/EPICS/program/aiDriver/aiDriver.pid -i ^D^C^] 20000 ===================end=================================
启动守护/服务进程的函数:
do_start() { # 返回 # 0:启动了守护进程 # 1:守护进程已经在运行 # 2:不能启动守护进程 # 如果需要,在此处添加代码,其等待这个进程准备好处理来自之后启动的服务的请求,这个服务依赖于这个进程。作为最后的方法,休眠一段时间。 echo -n "Starting soft IOCs ... " MYIOCS=`get_iocs $@` [ "$MYIOCS" = "" ] && echo -n " " for IOC in $MYIOCS do echo -n "$IOC " clear_options #清除环境变量 default_options "$IOC" #设置默认环境变量 assign_options "GLOBAL" #设置全局设置的环境变量 assign_options "$IOC" #设置本IOC的环境变量 set_cmdenvopts #用于启动IOC的环境变量字符串,用于procserv的选项字符串 if [ -d $BOOTDIR ] # 测试启动目录是否存在 then if [ -d $HOMEDIR ] # 测试家目录是否存在 then # 测试是否已经有pidfile文件$PIDFILE的进程在运行了,如果是返回1 sudo -H -u $IOC_USER bash -c "$SETENV (env > $ENVFILE; /usr/sbin/start-stop-daemon --start --quiet --chdir $BOOTDIR \ --pidfile $PIDFILE --startas $PROCSERV --name procServ --test > /dev/null)" if [ "$?" = 1 ] then echo -n " " else # 启动服务进程以及IOC sudo -H -u $IOC_USER bash -c "$SETENV (env > $ENVFILE; /usr/sbin/start-stop-daemon --start --quiet --chdir $BOOTDIR \ --pidfile $PIDFILE --startas $PROCSERV --name procServ -- $PROCSERVOPTS ./st.cmd)" if [ "$?" = 1 ] then echo -n " " fi fi else echo -e "\nWarning: Home directory $HOMEDIR does not exist! Ignoring $IOC" fi else echo -e "\nWarning: Boot directory $BOOTDIR does not exist! Ignoring $IOC" fi done echo "... done." }
do_start的测试脚本如下:
echo "========================Test do_start()=============================" default_options aiDriver assign_options aiDriver set_cmdenvopts do_start aiDriver echo "===================end=================================" echo ""
测试结果如下:
========================Test do_start()============================= Starting soft IOCs ... aiDriver ... done. ===================end=================================
IOC顶层目录下,产生两个新的文件aiDriver.env和aiDriver.pid:
:/usr/local/EPICS/program/aiDriver# ls aiDriverApp aiDriver.env aiDriver.pid bin configure db dbd iocBoot lib Makefile
ftp本地连接20000端口,进行测试:
root@orangepi5plus:~# telnet localhost 20000 Trying ::1... Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. @@@ Welcome to procServ (procServ Process Server 2.7.0) @@@ Use ^X to kill the child, auto restart is ON, use ^T to toggle auto restart @@@ procServ server PID: 171785 @@@ Server startup directory: /usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver @@@ Child startup directory: /usr/local/EPICS/program/aiDriver/iocBoot/iocaiDriver/ @@@ Child "orangepi" started as: ./st.cmd @@@ Child "orangepi" PID: 171786 @@@ procServ server started at: Fri Jul 5 15:11:02 2024 @@@ Child "orangepi" started at: Fri Jul 5 15:11:02 2024 @@@ 0 user(s) and 0 logger(s) connected (plus you) epics> dbl Random:AiRandom
通道访问这个IOC:
caget Random:AiRandom Random:AiRandom 527
停止守护/服务进程的函数:
sudo -H -u $IOC_USER bash -c "/usr/sbin/start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name procServ"
--retry:使用--stop,指定start-stop-daemon检查这些进程是否结束了。它将重复检查任何匹配的进行是否正在运行,直到没有匹配的进程运行。如果这些进程不存在,它将采取由调度表决定的进一步操作。如果指定了timeout替代schedule,则调度表signal/timeout/KILL/timeout被使用,此处signal是由--signal指定的信号。
schedule是一个列表,其至少有由斜杆(/)分隔的两项;每项可能是-signal-number或[-]signal-name,它表示发送那个信号,或timeout,它表示为进程退出等待那些秒数,或者forever,它表示如果需要一直重复调度表余下的。
do_stop() { # 返回: # 0:守护进程被停止 # 1:守护进程已经停止 # 2:守护进程不能被停止 # 3:发生故障 echo -n "Stopping soft IOCs ... " MYIOCS=`get_iocs $@` [ "$MYIOCS" = "" ] && echo -n " " for IOC in $MYIOCS do echo -n "$IOC " clear_options default_options "$IOC" assign_options "GLOBAL" assign_options "$IOC" set_cmdenvopts # 测试停止守护进程, 返回1,表示已经停止 sudo -H -u $IOC_USER bash -c "/usr/sbin/start-stop-daemon --stop --quiet --pidfile $PIDFILE --name procServ --test > /dev/null" if [ $? = 1 ] then echo -n " " else sudo -H -u $IOC_USER bash -c "/usr/sbin/start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name procServ" if [ $? = 1 ] then echo -n " " else sudo -H -u $IOC_USER bash -c "rm -f $PIDFILE" fi fi done echo "... done." }
do_stop的测试脚本:
echo "========================Test do_start()=============================" default_options aiDriver assign_options aiDriver set_cmdenvopts do_stop aiDriver echo "===================end=================================" echo ""
测试结果如下,ftp服务程序和epics ioc程序都已经终止。
========================Test do_start()============================= Stopping soft IOCs ... aiDriver ... done. ===================end================================= root@orangepi5plus:~# ss -tlnp State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1383,fd=3)) LISTEN 0 4 127.0.0.1:5037 0.0.0.0:* users:(("adbd",pid=849,fd=5)) LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=900,fd=4),("systemd",pid=1,fd=32)) LISTEN 0 4 0.0.0.0:5555 0.0.0.0:* users:(("adbd",pid=849,fd=10)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=901,fd=14)) LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1383,fd=4)) LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=900,fd=6),("systemd",pid=1,fd=34))
并且IOC顶层目录中pidfile文件也被删除了:
root@orangepi5plus:/usr/local/EPICS/program/aiDriver# ls aiDriverApp aiDriver.env bin configure db dbd iocBoot lib Makefile
发送SIGHUP信号给守护/服务进程的函数如下(此处没有实现):
do_reload() { # 如果守护进程可以在不重启下重载其配置(例如,当向其发送要给SIGHUP), # 则在此实现。 # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME # return 0 echo "Restarting soft IOCs ... " STARTDIR=$PWD IOCS=`get_iocs $@` [ "$IOCS" = "" ] && echo -n " " for IOC in $IOCS do echo -n "$IOC " clear_options default_options "$IOC" assign_options "GLOBAL" assign_options "$IOC" if [ -d $BOOTDIR ] then cd "$BOOTDIR" # restart it! echo -e "\ndebug: Reloading ioc $IOC" cd "$STARTDIR" else echo -e "\nWarning: Boot directory $BOOTDIR does not exist! Entry for $NET ignored!" fi done echo "... done." }
以下脚本用于从命令 行获取参数,并且执行守护进程启动/停止/重启:
COMMAND=$1 shift IOCS=`echo $@` case "$COMMAND" in start) do_start $IOCS ;; stop) do_stop $IOCS ;; restart|force-reload) do_stop $IOCS sleep 1 do_start $IOCS ;; *) echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload} [iocs ...]" >&2 exit 3 ;; esac
以下命令行启动IOC:
root@orangepi5plus:~# ./test.sh start aiDriver Starting soft IOCs ... aiDriver ... done. root@orangepi5plus:~# ss -lntp State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1383,fd=3)) LISTEN 0 20 0.0.0.0:5080 0.0.0.0:* users:(("st.cmd",pid=172122,fd=13)) LISTEN 0 5 127.0.0.1:20000 0.0.0.0:* users:(("st.cmd",pid=172122,fd=3),("procServ",pid=172121,fd=3)) LISTEN 0 4 127.0.0.1:5037 0.0.0.0:* users:(("adbd",pid=849,fd=5)) LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=900,fd=4),("systemd",pid=1,fd=32)) LISTEN 0 4 0.0.0.0:5075 0.0.0.0:* users:(("st.cmd",pid=172122,fd=15)) LISTEN 0 4 0.0.0.0:5555 0.0.0.0:* users:(("adbd",pid=849,fd=10)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=901,fd=14)) LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1383,fd=4)) LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=900,fd=6),("systemd",pid=1,fd=34))
以下命令行停止IOC:jj
root@orangepi5plus:~# ./test.sh stop aiDriver Stopping soft IOCs ... aiDriver ... done. root@orangepi5plus:~# ss -lntp State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1383,fd=3)) LISTEN 0 4 127.0.0.1:5037 0.0.0.0:* users:(("adbd",pid=849,fd=5)) LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=900,fd=4),("systemd",pid=1,fd=32)) LISTEN 0 4 0.0.0.0:5555 0.0.0.0:* users:(("adbd",pid=849,fd=10)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=901,fd=14)) LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1383,fd=4)) LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=900,fd=6),("systemd",pid=1,fd=34))
将以下test.sh更名为softIOC并复制到/etc/init.d/目录下,IOC应用程序就可以当作系统服务程序一样被启动。
root@orangepi5plus:~# /etc/init.d/softIOC start aiDriver Starting soft IOCs ... aiDriver ... done. root@orangepi5plus:~# /etc/init.d/softIOC stop aiDriver Stopping soft IOCs ... aiDriver ... done.
- Console访问(以及记录console输出日志)应该是统一的:对于软IOCs和VME IOCs使用相同方式。
- 即使不同软IOCs驻留在相同机器上,通道访问应该能够区分不同的软IOCs。