守护进程接收终端输入的一种变通性方法
说明
本文主要介绍某嵌入式产品中DSL用户态驱动模块作为守护进程时,如何接收终端输入的变通性方法。
出于信息安全考虑,文中涉及系统方面的接口函数未给出实现细节,但不影响表述的完整性。
相关性文章参见《GNU Readline库函数的应用示例》。
一 背景知识
init进程(如Busybox init)是嵌入式系统内核自举时启动的第一个也是惟一的用户进程。init进程是后续所有其他进程的父进程(其进程ID为1),在系统运行期间以守护进程的形式一直存在。它主要负责启动各运行层次特定的系统服务(如执行某些脚本、启动shell及运行用户指定的程序等),这些服务通常在其拥有的守护进程的帮助下实现。
Busybox init程序对应的代码在init/init.c文件中,首先启动init_main函数,以设置信号的处理函数、初始化控制台以及解析inittab内容。
守护进程没有控制终端。因此编写守护进程时,通常关闭标准输入、标准输出和标准出错等文件描述符(若不关闭会浪费系统资源,造成进程所在文件系统无法卸载甚至引起无法预料的错误)。这样,守护进程不再持有从其父进程继承而来的文件描述符,任何一个试图读标准输入、写标准输出和标准出错的库例程将不会产生任何效果。因为守护进程并不与终端设备相关联,所以不能在终端设备上显示其输出,也无处从交互式用户那里接收输入。
但某产品中因调试排障需要,未关闭标准输出描述符。事实上,该产品中所有进程均作为pc守护进程的子进程启动,但启动时并未执行关闭文件描述符的步骤,而是在Busybox的init_main函数关闭标准输入和标准出错。虽然守护进程已脱离控制终端,但仍可使用从父进程继承的文件描述符来输出到“原”终端。
此外,嵌入式系统中守护进程通常启动后一直运行直至系统关机,而不受用户输入的影响。若不关闭标准输入,虽然各守护进程可以不处理这些输入,或按需选择性地处理,但当多个进程试图读取标准输入时将引起混乱。
二 需求提出
BCM厂家SDK为该产品DSL驱动模块提供了丰富的配置、查询及调测命令集,入口函数为dispatchingTask。该函数行为类似多级Shell,内部通过BCM_GETLINE宏(循环+getchar实现)等待和捕获用户输入。其模型可简化为:
1 dispatchingTask 2 { 3 while getchar 4 若输入 == "doSomething" 5 do something 6 若输入 == "-4" 7 break 8 }
DSL驱动模块作为守护进程没有控制终端,无法通过getchar捕获终端输入。因此,必须创建一个普通的前台进程,由该进程捕获终端输入并转发给DSL驱动。但这种消息驱动的异步方式无法实现BCM_GETLINE的“同步等待”特性,还需开辟一块读写缓存。
具体实现如下图所示:
前台进程dsltst捕获用户在终端输入的命令行,并转发给DslDriver(DSL驱动)进程下的dsldbg线程。dsldbg线程若接收到BcmShell启动命令行(“StartShell”),则向dsldrv线程发送Shell启动消息;否则将命令行输入进行缓存。dsldrv线程启动BcmShell后将“轮询”该缓存,若缓存非空则读取其中的命令行并做相应处理。
注意,同一进程下线程间的消息收发通过读写一个由指向消息体数据的指针组成的循环缓冲区实现,并非传统意义上的进程间消息队列。
基于命令行交互模式的考虑,无需使用互斥机制保护缓存:
1) 每次执行单条输入并输出,等待下条输入;
2) 本次输入(写)必须等待上次输入被执行(读)并输出。
三 代码实现
为描述清晰,本节将按照代码文件组织各小节内容。
3.1 Readline.c/h
Readline.c/h为基本的命令行输入函数集。考虑到终端设备可能不支持GNU Readline库,本函数集可在需要时用来替代库的行为。
首先定义一组私有函数,如下:
1 /****************************************************************************** 2 * 函数名称: TerminateStr 3 * 功能说明: 对给定字符数组添加结束符 4 ******************************************************************************/ 5 static VOID TerminateStr(CHAR *pszStr, INT32U dwCharsNum) 6 { 7 if(0 == dwCharsNum) 8 return; 9 pszStr[dwCharsNum-1] = '\0'; 10 } 11 12 /****************************************************************************** 13 * 函数名称: FlushStdin 14 * 功能说明: 清空标准输入缓冲区 15 ******************************************************************************/ 16 static VOID FlushStdin(VOID) 17 { 18 INT32S cChar = 0; 19 while((cChar = getchar()) != '\n' && cChar != EOF); 20 } 21 22 /****************************************************************************** 23 * 函数名称: GetChars 24 * 功能说明: 命令行输入函数 25 * 输入参数: CHAR *pszBuf :字符缓冲区 26 * INT32U dwBufSize :字符缓冲区字节大小 27 * 输出参数: NA 28 * 返回值 : 若缓冲区空间不足,则返回-2,以指示需要扩展缓冲区或终止输入; 29 * 若函数出错且尚未输入,则返回-1; 30 * 否则返回当前调用所输入的字符数('\n'被替换为'\0'并计入字符数) 31 * 注意事项: Linux下该函数不支持退格键(BackSpace)消字符,请使用删除键(Delete) 32 ******************************************************************************/ 33 #define LINUX_BS_KEY (CHAR)0x08 //Unix/Linux系统BackSpace键ASCII值 34 static INT32S GetChars(CHAR *pszBuf, INT32U dwBufSize) 35 { 36 INT32U dwIdx = 0; 37 INT32S dwChar = 0; 38 for(; dwIdx<dwBufSize && (dwChar=getchar())!=EOF && dwChar!='\n'; dwIdx++) 39 { 40 if(LINUX_BS_KEY == dwChar) 41 { 42 printf("Try again from scratch, use 'Delete' key to BackSpace!\n"); 43 FlushStdin(); 44 return dwChar; 45 } 46 pszBuf[dwIdx] = dwChar; 47 } 48 49 if(dwIdx==0 && dwChar==EOF) 50 return -1; 51 52 if(dwIdx==dwBufSize && dwChar!=EOF && dwChar!='\n') 53 return -2; 54 55 if(dwChar == '\n') 56 pszBuf[dwIdx] = '\0'; 57 58 return (++dwIdx); 59 }
TerminateStr()函数给字符串添加结束符,其封装性可有可无。FlushStdin()函数清空标准输入缓冲区中残留的字符。此处未采用fflush(stdin)的方式,因为C标准中fflush()函数仅针对输出流或更新流(output or update stream)。
GetChars()函数为核心的命令行输入函数,ReadLine()函数内将分配动态内存并调用该函数对外提供命令行输入功能:
1 /****************************************************************************** 2 * 函数名称: ReadLine 3 * 功能说明: 命令行输入函数 4 * 输入参数: CHAR *pszPrompt: 命令行提示符,若无则置空字符串("") 5 * 输出参数: NA 6 * 返回值 : CHAR *已输入的命令行字符串或NULL(表示函数出错) 7 ******************************************************************************/ 8 CHAR *ReadLine(const CHAR *pszPrompt) 9 { 10 CHECK_SINGLE_POINTER(pszPrompt, NULL); 11 12 printf("%s", pszPrompt); 13 14 CHAR *pszLineRead = NULL; 15 INT32U dwCurAllocSize = ALLOC_BYTES_INIT_TRY; 16 INT32U dwEstReadCharNum = 0; 17 INT32S dwCharSize = 0; 18 while(1) 19 { 20 CHAR *pszReallocBuf = realloc(pszLineRead, dwCurAllocSize); 21 if(NULL == pszReallocBuf) 22 { 23 printf("[%s(%d)] reallocs %uBytes failed, no memory available!\n", 24 FUNC_NAME, __LINE__, dwCurAllocSize); 25 goto ReadLineError; 26 } 27 pszLineRead = pszReallocBuf; //Avoid memory leakage when realloc fails. 28 dwCharSize = GetChars(pszLineRead+dwEstReadCharNum, dwCurAllocSize-dwEstReadCharNum); 29 dwEstReadCharNum = dwCurAllocSize; 30 if(-2 != dwCharSize) 31 return pszLineRead; 32 33 INCREMENT_ALLOC_BYTES(dwCurAllocSize); 34 if(dwCurAllocSize >= ALLOC_BYTES_MAX_TRY) 35 { 36 printf("\nOver %uBytes Memory's Required, There must be something WRONG!\n", dwCurAllocSize); 37 goto ReadLineError; 38 } 39 } 40 41 ReadLineError: 42 FlushStdin(); 43 TerminateStr(pszLineRead, dwEstReadCharNum); 44 return pszLineRead; 45 }
ReadLine()函数内会为命令行缓冲区分配内存,故调用者无需再分配缓存。也正因为如此,ReadLine()函数必须考虑内存裕量问题,即必要时扩展内存以确保足够容纳终端输入的命令行。
因本文所涉产品不支持-lreadline、-lhistory甚至-lncurses库,故AddHistory实现为空函数:
1 /****************************************************************************** 2 * 函数名称: AddHistory 3 * 功能说明: 将命令添加到历史记录,以支持上下键调出历史命令 4 * 输入参数: CHAR *pszCmd :待添加的命令 5 * 输出参数: NA 6 * 返回值 : VOID 7 ******************************************************************************/ 8 VOID AddHistory(CHAR *pszCmd) 9 { 10 /* Remains to be implemented! */ 11 }
终端输入结束后,需调用CloseLine()函数释放ReadLine()函数分配的动态内存:
1 /****************************************************************************** 2 * 函数名称: CloseLine 3 * 功能说明: 关闭命令行输入 4 * 输入参数: CHAR *pszLine :命令行输入字符串指针 5 * 输出参数: NA 6 * 返回值 : VOID 7 ******************************************************************************/ 8 VOID CloseLine(CHAR *pszLine) 9 { 10 if(!pszLine) 11 return; 12 13 free(pszLine); 14 }
为支持用户自定义ReadLine()函数的内存扩展方式,ReadLine.h头文件内定义如下宏:
1 #ifndef ALLOC_BYTES_INIT_TRY 2 #define ALLOC_BYTES_INIT_TRY 32 //初次尝试分配的内存字节数 3 #endif 4 #ifndef ALLOC_BYTES_MAX_TRY 5 #define ALLOC_BYTES_MAX_TRY 1024 //累计尝试分配的最大内存字节数 6 #endif 7 #ifndef INCREMENT_ALLOC_BYTES 8 #define INCREMENT_ALLOC_BYTES(dwCurAllocSize) do{ \ 9 dwCurAllocSize <<= 1; \ 10 }while(0) 11 #endif
用户可在包含ReadLine.h头文件之前,重定义ALLOC_BYTES_INIT_TRY等宏。
3.2 Dsl_Readline.c/h
Dsl_Readline.c/h为DSL模块命令行输入函数集。
首先,根据GNU Readline库的支持情况,做如下条件编译控制:
1 #ifdef READLINE_DEBUG 2 #include <readline/readline.h> 3 #include <readline/history.h> 4 #define READ_LINE readline 5 #define ADD_HISTORY add_history 6 #define CLR_LINE free 7 #else 8 #include "Readline.h" 9 #define READ_LINE ReadLine 10 #define ADD_HISTORY AddHistory 11 #define CLR_LINE CloseLine 12 #endif
然后,定义两个字符串指针:
1 static CHAR *pszLineRead = NULL; //命令行输入字符串 2 static CHAR *pszStripLine = NULL; //剔除首尾空白字符的输入字符串
ReadCmdLine()函数通过READ_LINE宏读取终端输入的命令行后,调用StripWhite()函数剔除命令行字符串首尾的空白字符(主要是空格符)。若剔除后的字符串非空且非空串,则通过ADD_HISTORY宏将该串存入历史记录。
1 /****************************************************************************** 2 * 函数名称: ReadCmdLine 3 * 功能说明: 命令行输入函数 4 * 输入参数: CHAR *pszPrompt: 命令行提示符,若无则置空字符串("") 5 * 输出参数: NA 6 * 返回值 : CHAR *已输入且剔除首尾空白字符的命令行字符串 7 ******************************************************************************/ 8 CHAR *ReadCmdLine(const CHAR *pszPrompt) 9 { 10 CLR_LINE(pszLineRead); 11 pszLineRead = READ_LINE(pszPrompt); 12 13 pszStripLine = StripWhite(pszLineRead); 14 if(pszStripLine && *pszStripLine) 15 ADD_HISTORY(pszStripLine); 16 17 return pszStripLine; 18 }
StripWhite()函数沿用《GNU Readline库函数的应用示例》一文的实现。
结束命令行输入时,必须调用QuitCmdLine()函数退出输入。该函数会做一些必要的清理工作。
1 /****************************************************************************** 2 * 函数名称: QuitCmdLine 3 * 功能说明: 命令行退出函数 4 * 输入参数: NA 5 * 输出参数: NA 6 * 返回值 : VOID 7 * 注意事项: 结束命令行输入时必须调用该函数! 8 ******************************************************************************/ 9 VOID QuitCmdLine(VOID) 10 { 11 CLR_LINE(pszLineRead); 12 pszLineRead = NULL; 13 }
3.3 Dsl_Test.c
Dsl_Test.c为DSL模块的测试函数,也是dsltst进程的入口所在。
该文件内仅有一个main()函数,实现如下:
1 INT32S main(INT32S argc, CHAR *argv[]) 2 { 3 INT32S retCode = 0; 4 retCode = InitAppComm((DSL_DRV_APP>>4), 0); 5 if(retCode != OSS_OK) 6 { 7 printf("Dsl Test[InitAppComm] failed, retCode=%d!\n", retCode); 8 goto ErrorHandle; 9 } 10 11 CHAR *pszCmdLine = "StartShell"; 12 retCode = asend(DSL_DBG_SH_CMD, pszCmdLine, strlen(pszCmdLine)+1, DSL_DBG_PROC_PID); 13 if(retCode != OSS_OK) 14 { 15 printf("Dsl Test[asend](%s) failed, retCode=%d!\n", pszCmdLine, retCode); 16 goto ErrorHandle; 17 } 18 19 while(1) 20 { 21 pszCmdLine = ReadCmdLine(""); 22 retCode = asend(DSL_DBG_SH_CMD, pszCmdLine, strlen(pszCmdLine)+1, DSL_DBG_PROC_PID); 23 if(retCode != OSS_OK) 24 { 25 printf("Dsl Test[asend](%s) failed, retCode=%d!\n", pszCmdLine, retCode); 26 goto ErrorHandle; 27 } 28 if(!strcmp(pszCmdLine, "-4")) 29 { 30 QuitCmdLine(); 31 goto ErrorHandle; 32 } 33 } 34 35 ErrorHandle: 36 FreeAppComm(); 37 return retCode; 38 }
其中,InitAppComm()函数初始化套接字通讯接口。asend()调用表示通过套接字发送DSL_DBG_SH_CMD异步消息到dsldbg线程(隶属DslDriver进程),消息体内容为终端命令行输入(pszCmdLine)。
为便于展示输入效果,将main()函数改为如下实现:
1 INT32S main(INT32S argc, CHAR *argv[]) 2 { 3 CHAR *pszCmdLine = NULL; 4 while(1) 5 { 6 pszCmdLine = ReadCmdLine(">>"); 7 printf("pszCmdLine = %s!\n", pszCmdLine); 8 9 if(!strcmp(pszCmdLine, "-4")) 10 { 11 QuitCmdLine(); 12 break; 13 } 14 } 15 16 printf("Quit!\n"); 17 return 0; 18 }
编译后执行结果如下:
1 [wangxiaoyuan_@localhost ReadLine]$ ./Dsl_Test 2 >>hello 3 pszCmdLine = hello! 4 >>world 5 pszCmdLine = world! 6 >>i'm 7 pszCmdLine = i'm! 8 >>clover! 9 pszCmdLine = clover!! 10 >>-4 11 pszCmdLine = -4! 12 Quit!
3.4 Dsl_GetLine.c/h
Dsl_GetLine.c/h为DslDriver进程内读取终端命令行输入并缓存的实现。
根据前文所述的缓存机制,dsldbg线程接收到BcmShell启动命令行(“StartShell”)后,向dsldrv线程发送Shell启动消息,并缓存之后的命令行输入。dsldrv线程收到启动消息后调用dispatchingTask()函数“轮询”该缓存,若缓存非空则读取其中的命令行并做相应处理。
首先定义一组私有函数,如下:
1 /****************************************************************************** 2 * 函数名称: DslClearShCmd 3 * 功能说明: 清除缓存的命令行输入 4 ******************************************************************************/ 5 static VOID DslClearShCmd(VOID) 6 { 7 gIsShCmdCached = DSL_FALSE; //必须先设置有效标记后清除缓存 8 memset(gCachedShCmd, 0, sizeof(gCachedShCmd)); 9 } 10 11 /****************************************************************************** 12 * 函数名称: DslReadShCmd 13 * 功能说明: 读取缓存的命令行输入 14 * 输入参数: VOID 15 * 输出参数: NA 16 * 返回值 : 成功: 已缓存的命令行输入;失败: NULL 17 ******************************************************************************/ 18 static CHAR *DslReadShCmd(VOID) 19 { 20 if(!gIsShCmdCached) 21 return NULL; 22 23 gIsShCmdCached = DSL_FALSE; 24 return gCachedShCmd; 25 }
注意,清除或读取缓存时必须先设置gIsShCmdCached标记,以保证多线程环境下全局缓存的访问安全性(本线程读取时先向其他线程宣告该缓存已不可读)。
dsldbg线程接收到dsltst进程发来的消息后,调用DslDebugShCmd()函数解析和缓存命令行输入:
1 /****************************************************************************** 2 * 函数名称: DslDebugShCmd 3 * 功能说明: dsldbg线程接收命令行输入并处理 4 * 输入参数: VOID *lpMsg :线程当前消息内容指针 5 * 输出参数: NA 6 * 返回值 : VOID 7 ******************************************************************************/ 8 VOID DslDebugShCmd(VOID *pvMsg) 9 { 10 CHECK_SINGLE_POINTER(pvMsg, RET_VOID); 11 CHAR *pszShCmd = (CHAR *)pvMsg; 12 13 if(!strcmp(pszShCmd, "StartShell")) 14 { //若为BcmShell启动命令(StartShell),则向dsldrv线程发送Shell启动消息 15 DslClearShCmd(); 16 DslAsynSendDrv(DSL_SH_START_MSG, NULL, 0); 17 return; 18 } 19 20 memset(gCachedShCmd, 0, sizeof(gCachedShCmd)); 21 strncpy(gCachedShCmd, pszShCmd, sizeof(gCachedShCmd)-1); 22 gCachedShCmd[sizeof(gCachedShCmd)-1] = '\0'; 23 gIsShCmdCached = DSL_TRUE; //必须先缓存命令后设置有效标记 24 }
当终端输入异常终止时,如在多级提示符下直接输入-4导致dsltst提前退出,缓存内将没有命令可读。为避免dsldrv线程陷入死循环,需要增加超时自动退出轮询的机制。
超时机制相关的几个宏定义如下:
1 #define READ_LINE_TIMEOUT_MS 1000*120 //该超时时长内未等到命令行输入则启动退出机制 2 #define READ_LINE_STEP_MS 1000 //计时步长 3 #define READ_LINE_TRY_TIMES (READ_LINE_TIMEOUT_MS/READ_LINE_STEP_MS) 4 #define READ_EXIT_TRY_TIMES 3 //该宏值不应小于BcmShell的提示符级数(如>、api>计两级)
基于该超时机制的命令行输入读取函数如下:
1 /****************************************************************************** 2 * 函数名称: DslGetLine 3 * 功能说明: dsldrv线程内BcmShell读取命令行输入 4 * 输入参数: CHAR *pszShCmd :命令行缓冲区 5 * INT32U dwCmdSize :命令行缓冲区大小(字节) 6 * 输出参数: NA 7 * 返回值 : INT32U 命令行字符串长度 8 ******************************************************************************/ 9 INT32U DslGetLine(CHAR *pszShCmd, INT32U dwCmdSize) 10 { 11 INT32U dwTryIdx = 0; 12 static INT32U dwExitTryCnt = 0; 13 14 for(; dwTryIdx < READ_LINE_TRY_TIMES; dwTryIdx++) 15 { 16 char *pszCmd = DslReadShCmd(); 17 if(pszCmd != NULL) 18 { 19 dwExitTryCnt = 0; 20 strncpy(pszShCmd, pszCmd, dwCmdSize-1); 21 pszShCmd[dwCmdSize-1] = '\0'; 22 return strlen(pszShCmd); 23 } 24 DslUsleep(READ_LINE_STEP_MS*1000); 25 } 26 27 //当与dsltst通讯异常时,此处可确保dispatchingTask函数安全退出 28 dwExitTryCnt++; 29 if(READ_EXIT_TRY_TIMES != dwExitTryCnt) 30 strcpy(pszShCmd, "exit"); 31 else 32 strcpy(pszShCmd, "-4"); 33 return strlen(pszShCmd); 34 }
3.5 其他
BCM厂家SDK相关头文件内对BCM_GETLINE宏定义如下:
1 #ifndef BCM_GETLINE 2 #define BCM_GETLINE getLine 3 int getLine(char *s, int lim); 4 #endif
可见,在包含该头文件之前,重新定义BCM_GETLINE宏值为DslGetLine即可:
1 #define BCM_GETLINE DslGetLine
四 效果验证
该缓存机制可精确模拟原SDK中BCM_GETLINE的同步等待特征。其操作示例如下:
1 root@FXXX:~ # dsltst 2 3 Enter line number to process (-1: all, -2: shutdown, -3: cli, -4: exit): -3 4 5 calling t&d module 6 Entering interactive trace and debug session 7 > api 8 9 api> configline downSlowMinDataRate 128 10 Configuration modified 11 12 api> exit 13 > ext 14 Unknown command: ext 15 16 > exitt(多输't'后退格) 17 Try again from scratch, use 'Delete' key to BackSpace! 18 Unknown command: exittgline downSlowMinDataRate 1 19 20 > exit 21 22 Enter line number to process (-1: all, -2: shutdown, -3: cli, -4: exit): -4 23 root@FXXX:~ # 24 Now Quit the Dsl Shell!
dsltst命令不支持超时退出机制(需要另起线程),故操作结束后必须输入-4退出。