1. 背景
在Unix/Linux系统中,一个父进程里要创建一个子进程去执行特定的任务(command),可以采用fork和exec函数族来实现,也可以采用popen函数来实现。一般使用fork()来创建一个新的进程,在新进程里面使用exec函数族执行command,成功之后并不会返回值,但是如果调用失败时,会自动设置error值并返回-1,然后在原程序的断点处继续运行。popen()会建立管道连到子进程(command)的标准输出设备或标准输入设备,然后返回一个文件指针,随后父进程便可利用此文件指针来读取子进程(command)的输出设备或是写入到子进程的标准输入设备中。二者的区别之一就是exec函数只会返回command是否执行成功,而不能得到command的输出信息。通过popen返回的文件指针则可以获取到command的输出信息。这里我们要讨论的特定任务,是要在Linux应用程序中通过AT指令来获取4G模块的信号强度。
2. 4G模块和AT指令
4G模块是一种利用TD-LTE或FDD-LTE的4G网络实现无线远距离数据传输,与远程公网服务器进行数据交互的无线模块。4G模块在硬件上将射频、基带集成在一块PCB板上,完成无线接收、发射、基带信号处理功能。软件上通过4G LTE网络连接基站,实现拨号联网、数据传输等功能。应用程序和4G模块之间可以通过UART串口来进行AT指令的信息传递。
AT指令是应用于终端设备与应用程序之间的连接与通信的指令。AT即Attention。每个AT命令行中只能包含一条AT指令;对于AT指令的发送,除AT两个字符外,最多可以接收1056个字符的长度(包括最后的空字符)。AT指令是以AT作首, 字符结束的字符串,AT指令的响应数据包在其中。每个指令执行成功与否都有相应的返回。其他的一些非预期的信息(如有人拨号进来、线路无信号等),模块将有对应的一些信息提示,接收端可做相应的处理。每个AT命令行中只能包含一条AT指令;对于由终端设备主动向应用程序报告的URC指示或者response响应,也要求一行最多有一个,不允许上报的一行中有多条指示或者响应。AT指令以回车作为结尾,响应或上报以回车换行为结尾。
以Linux应用程序获取4G模块的信号强度为例,Linux应用程序和4G模块通过UART接口/dev/ttyUSB1来进行信息的传递。
应用程序通过串口向4G模块发送获取信号强度的AT指令:
echo AT+CSQ > /dev/ttyUSB1;cat /dev/ttyUSB1
4G模块回应信号强度等信息给应用程序:
+CREG: 0,20
OK
3. 通过popen和fgets来获取AT响应信息
函数定义:
\begin{lstlisting}[]
FILE *popen (const char *command, const char *type);
int pclose (FILE *stream);
char *fgets(char *s, int size, FILE *stream);
\end{lstlisting}
popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
如果 type 为 r,那么调用进程读进command的标准输出。
如果 type 为 w,那么调用进程写到command的标准输入。
sprintf(cmd, "echo AT+CSQ > /dev/ttyUSB1;cat /dev/ttyUSB1");
pp = popen(cmd, "r");
if (pp == NULL){ printf("popen failed\r\n");
return -1;
}
int i = 0;
while(!feof(pp) && ferror()){
if (fgets(buf, 1024, pp) != NULL){
if (strstr(buf, "+CSQ")){
sscanf(buf, "+CSQ:%d,%d", &csq, &sig);
break;
} else if (strstr(buf, "OK")){
break;
} else if (strstr(buf, "ERROR")){
break;
}
}
}
pid_t pid = -1;
while((pid = KillProcessPidbyName("cat")) > 0){
printf ("killall cat end\n");
}
if (pp){
pclose(pp);
pp = NULL;
}
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的命令,那么command命令执行的则是父进程的孙子进程。echo指令会执行完成即结束, cat /dev/ttyUSB1会一直等待4G模块的串口输出数据。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。这里pclose会一直等子进程结束,而子进程则一直在等待cat /dev/ttyUSB1结束。所以在子进程结束之前,需要先强制结束孙子进程cat,即KillProcessPidbyName("cat")。
pid_t KillProcessPidbyName(char *name)
{
FILE *fptr;
char buf[1056] = { 0 };
char cmd[256] = { 0 };
pid_t pid = -1;
sprintf(cmd, "pidof %s", name);
if ((fptr = popen(cmd, "r")) != NULL) {
if (fgets(buf, sizeof(buf), fptr) != NULL) {
pid = atoi(buf);
kill(pid, SIGINT);
}
}
if (fptr){
pclose(fptr);
}
return pid;
}
上面的代码能够实现获取4G模块信号强度的功能,经过多次测试会发现,偶尔出现没有应答数据的情况下fgets会阻塞,ferror和feof也不会起作用,进程进入僵死状态,这是接下来我们要讨论的重点问题。
4. 解决fgets获取信息阻塞的问题
......