popen调用成功但fgets/fread无法获取数据的原因排查
问题
直接在控制台执行以下命令时,能稳定返回包含<item id="4" value="t9.tar.gz" />的结果:
printf "GET /xml?method=gw.log.download&id=4&pass=voip HTTP/1.1\r\n\r\n" | nc 127.0.0.1 80
控制台输出示例:
/mnt/rwfs/var/run # printf "GET /xml?method=gw.log.download&id=4&pass=voip HTTP/1.1\r\n\r\n" | nc 127.0.0.1 80 tar: removing leading '/' from member names var/log/usb.log var/log/version var/log/versions/ var/log/versions/initsys var/log/warning.log var/tmp/message.log HTTP/1.0 200 OK Date: Wed, 21 Aug 2024 06:52:18 GMT Server: X-Frame-Options:DENY Cache-Control:no-cache Pragma:no-cache Expires:-1 Referrer-Policy: strict-origin-when-cross-origin Strict-Transport-Securit: max-age = 16070400 X-Content-Type-Options: nosniff X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: master-only X-XSS-Protection: 1; mode=block Access-Control-Allow-Headers: accept, Content-Type Access-Control-Allow-Origin: (null) Connection: close Content-Type: text/xml Content-Length: 148 <?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok"> <config method="gw.log.download"> <item id="4" value="t9.tar.gz" /> </config> </rsp>
但通过如下C代码调用该命令时,约50%概率出现fgets/fread直接返回NULL的情况,无法提取到目标文件名:
static void request_download_log(char *tar_file_name) { char sys_cmd[128] = {0}; FILE *fp = NULL; char buff[128] = {0}; char *p = NULL; char *q = NULL; tar_file_name[0] = 0; sprintf(sys_cmd, "printf \"GET /xml?method=gw.log.download&id=4&pass=%s HTTP/1.1\\r\\n\\r\\n\" | nc 127.0.0.1 80 2>&1", "voip"); fp = popen(sys_cmd, "r"); if (NULL != fp) { //while (fgets(buff, sizeof(buff), fp) != NULL) { while(fread(buff, sizeof(char), sizeof(buff) - 1, fp) > 0) { if ((p = strstr(buff, "value=\"")) != NULL) { p += strlen("value=\""); q = p; while (*q != '"') { q++; } *q = 0; strcpy(tar_file_name, p); break; } } pclose(fp); } debug_log(eLOG_COMP_FUNCTION, 4, __FUNCTION__, __LINE__, "%s:%s", __FUNCTION__, tar_file_name); }
原因分析
- 目标字符串跨缓冲区分割:你要找的
value=\"可能被拆分在两个连续的fread缓冲区中,比如前一个缓冲区末尾是value=,后一个开头是"t9.tar.gz",单次strstr无法匹配到完整字符串。代码找到匹配就立刻退出循环,若前几次读取都没命中完整目标,直到读完所有数据也找不到,就会返回空结果,这种情况的概率完全取决于输出数据在管道中的分段情况。 - 子进程与管道同步问题:
nc在服务器关闭连接后可能提前退出,此时子进程的输出缓冲区可能还没完全把数据刷到管道里,父进程的fread就提前遇到EOF返回0,导致后续数据没被读取。 - 信号中断读取过程:子进程退出时会触发
SIGCHLD信号,如果父进程没处理这个信号,可能会打断fread的系统调用,导致读取错误,表现为fread直接返回0或NULL。
解决建议
- 累积数据后再匹配:不要每次读取后立刻匹配,而是把所有读取到的数据累积到一个足够大的缓冲区里,再统一查找目标字符串,避免跨缓冲区的问题。示例修改:
static void request_download_log(char *tar_file_name) { char sys_cmd[128] = {0}; FILE *fp = NULL; char total_buff[1024] = {0}; // 扩大累积缓冲区 char read_buf[128] = {0}; char *p = NULL; char *q = NULL; tar_file_name[0] = 0; sprintf(sys_cmd, "printf \"GET /xml?method=gw.log.download&id=4&pass=%s HTTP/1.1\\r\\n\\r\\n\" | nc 127.0.0.1 80 2>&1", "voip"); fp = popen(sys_cmd, "r"); if (NULL != fp) { size_t total_len = 0; // 先累积所有输出数据 while(fread(read_buf, sizeof(char), sizeof(read_buf)-1, fp) > 0) { if (total_len + sizeof(read_buf)-1 < sizeof(total_buff)) { strncat(total_buff, read_buf, sizeof(read_buf)-1); total_len += sizeof(read_buf)-1; } } // 统一查找目标字符串 if ((p = strstr(total_buff, "value=\"")) != NULL) { p += strlen("value=\""); q = p; while (*q != '"' && q < total_buff + sizeof(total_buff)) { q++; } *q = 0; strncpy(tar_file_name, p, strlen(p)); // 用strncpy避免缓冲区溢出 } pclose(fp); } debug_log(eLOG_COMP_FUNCTION, 4, __FUNCTION__, __LINE__, "%s:%s", __FUNCTION__, tar_file_name); } - 添加错误检查:在
fread返回0后,用ferror(fp)检查是否是读取错误,如果是错误可以尝试重新执行命令或记录错误日志。 - 改用直接Socket通信:彻底避免shell命令和管道的不确定性,直接在C代码里创建TCP连接,发送HTTP请求并读取响应,这是最可靠的方案。核心步骤:
- 创建TCP Socket连接到127.0.0.1:80
- 构造完整的HTTP请求字符串并发送
- 循环读取响应数据并累积
- 在累积的数据中查找目标字符串
- 关闭Socket
内容的提问来源于stack exchange,提问作者dragonkyl
相关产品推荐
相关产品推荐

