You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程环境下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线程是否存在栈溢出或破坏:可在创建线程时设置更大栈空间,或用gdb attach到阻塞线程查看栈帧状态。
  • 确认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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 05:42:47