多线程环境下fgets无限阻塞问题的排查与解决咨询
多线程调用popen执行Shell命令时Alerts线程阻塞问题排查
问题背景
维护一套多线程系统,所有线程通过通用函数runCmd执行Shell命令。系统整体运行正常,但偶尔会有Alerts线程在runCmd的fgets调用处无限阻塞,且无论执行什么Shell命令都会卡在该位置。
系统与异常信息
系统信息
- 运行环境:搭载TI芯片的嵌入式Linux系统
- 线程归属:所有线程均属于同一服务进程(已用
ps aux确认)
异常现象
- 用
journalctl查看服务日志时,出现多个不同PID,多数与ps aux显示一致,但部分PID在ps aux中不存在。
阻塞代码
bool runCmd(char const* command, char* response, size_t bufferSize) { FILE* pFile = popen(command, "r"); if (pFile == NULL) { printf("Failed to run command\n" ); return false; } if (NULL != response) { while (fgets(response, bufferSize, pFile) != NULL) { response = response + strlen(response); } } pclose(pFile); return true; }
已尝试方案
- 给
runCmd加互斥锁,确保同一时间仅一个线程执行Shell命令:所有线程均阻塞,因Alerts线程占用锁后卡在其中 - 将
fgets替换为fread:现象完全相同 - 用
select等待管道可读并设置超时:无任何改善
进一步调试方案
1. 追踪子进程状态
- 当Alerts线程阻塞时,执行
pstree -p <父进程PID>查看服务进程的子进程树,确认popen创建的子进程是否存活、是否处于僵尸(Z)或停止(T)状态。 - 执行
lsof -p <父进程PID>查看进程打开的文件描述符,定位popen对应的管道FD,检查其状态是否异常。
2. 内核态追踪阻塞原因
- 执行
strace -p <Alerts线程TID>(注意是线程ID,非进程PID)追踪系统调用,查看fgets对应的read调用是否处于不可中断睡眠(D状态),若为是,则说明内核层面存在资源阻塞。 - 若嵌入式系统支持
ftrace,用其追踪内核中管道相关操作,定位管道读端无法获取数据的根因。
3. 增强日志维度
在runCmd中添加更细粒度的日志:
- 调用
popen时,记录生成的子进程PID(可通过fileno(pFile)获取FD,再通过/proc/self/fd/<FD>路径解析出子进程PID) - 在
fgets循环前后、每次循环时,记录当前线程ID、子进程PID、读取字节数 - 记录
pclose的返回值,检查子进程是否正常退出
4. 验证线程上下文异常
- 检查Alerts线程是否存在栈溢出或破坏:可在创建线程时设置更大栈空间,或用
gdbattach到阻塞线程查看栈帧状态。 - 确认Alerts线程是否屏蔽了
SIGCHLD信号:若父进程未处理SIGCHLD,子进程退出后会变成僵尸进程,可能导致管道读端持续阻塞。
可行规避方案
1. 为子进程设置超时强制回收
放弃无限制的fgets循环,添加超时逻辑,超时后强制杀死子进程并关闭管道:
// 从popen的FILE*中解析子进程PID(需适配嵌入式proc文件系统) pid_t get_child_pid(FILE* fp) { int fd = fileno(fp); char link_buf[64]; snprintf(link_buf, sizeof(link_buf), "/proc/self/fd/%d", fd); char pipe_path[64]; ssize_t len = readlink(link_buf, pipe_path, sizeof(pipe_path)-1); if (len <= 0) return -1; pipe_path[len] = '\0'; // 从pipe:[xxxx]格式中提取inode,再遍历/proc/<pid>/fd找对应inode // 此处为简化示例,实际需完善解析逻辑 int inode = atoi(pipe_path + 5); DIR* proc_dir = opendir("/proc"); if (!proc_dir) return -1; struct dirent* entry; while ((entry = readdir(proc_dir)) != NULL) { pid_t pid = atoi(entry->d_name); if (pid <= 0) continue; char fd_path[64]; snprintf(fd_path, sizeof(fd_path), "/proc/%d/fd", pid); DIR* fd_dir = opendir(fd_path); if (!fd_dir) continue; struct dirent* fd_entry; while ((fd_entry = readdir(fd_dir)) != NULL) { int fd = atoi(fd_entry->d_name); if (fd <= 0) continue; char fd_link[64]; snprintf(fd_link, sizeof(fd_link), "/proc/%d/fd/%d", pid, fd); char target[64]; len = readlink(fd_link, target, sizeof(target)-1); if (len <= 0) continue; target[len] = '\0'; int target_inode = atoi(target + 5); if (target_inode == inode) { closedir(fd_dir); closedir(proc_dir); return pid; } } closedir(fd_dir); } closedir(proc_dir); return -1; } bool runCmdWithTimeout(char const* command, char* response, size_t bufferSize, int timeout_sec) { FILE* pFile = popen(command, "r"); if (!pFile) return false; pid_t child_pid = get_child_pid(pFile); int fd = fileno(pFile); fd_set read_fds; struct timeval tv = {timeout_sec, 0}; bool timeout_triggered = false; if (response) { while (1) { FD_ZERO(&read_fds); FD_SET(fd, &read_fds); int ret = select(fd + 1, &read_fds, NULL, NULL, &tv); if (ret == -1) break; // 处理select错误 if (ret == 0) { // 超时 timeout_triggered = true; break; } if (fgets(response, bufferSize, pFile) == NULL) break; response += strlen(response); tv.tv_sec = timeout_sec; // 重置超时时间 tv.tv_usec = 0; } } // 超时则强制杀死子进程 if (timeout_triggered && child_pid > 0) { kill(child_pid, SIGKILL); } pclose(pFile); return !timeout_triggered; }
2. 替换popen为手动创建子进程
放弃popen,改用fork+pipe+exec手动创建子进程,更精确控制生命周期:
- 在父进程中注册
SIGCHLD信号处理函数,及时回收子进程,避免僵尸进程 - 用
waitpid非阻塞检查子进程状态,配合管道读写逻辑,规避stdio缓冲带来的异常
3. 隔离Alerts线程的命令执行
为Alerts线程单独创建一个独立进程执行Shell命令,即使该进程阻塞,也不会影响主服务的其他线程。
内容的提问来源于stack exchange,提问作者Ash Gupta
相关产品推荐
相关产品推荐

