Linux下跨线程安全中断fork()/system()创建进程的方法咨询
问题分析与解决方案
当前示例代码的核心问题
脚本未处理SIGINT信号
bash默认会在收到SIGINT时终止,但脚本中的sleep命令是前台进程,SIGINT会被sleep捕获并终止,而bash会继续执行后续循环。必须显式在脚本中添加信号处理逻辑,才能触发优雅退出与清理。阻塞式
waitpid导致主线程无法及时响应中断
原mysystem中使用阻塞waitpid,主线程会卡在等待子进程结束的状态,即便子线程设置了done标志,也无法立即处理中断请求。潜在PID复用风险
虽然用互斥锁保护了forked_process变量,但子进程退出后到重置变量的窗口内,若线程发送信号,可能命中被系统复用的PID,引发意外问题。
1. 让脚本响应SIGINT并优雅退出
修改bash脚本,添加trap捕获SIGINT,执行清理逻辑后退出:
#!/bin/bash # 定义清理函数 cleanup() { echo "Received SIGINT, starting cleanup..." # 在这里添加你的清理操作:删除临时升级包、恢复备份、重置设备状态等 exit 1 } # 绑定SIGINT信号到清理函数 trap cleanup SIGINT for i in {1..20} do echo "Sleeping in script PID = $$" sleep 1 done echo "Upgrade completed successfully"
2. 安全中断子进程的改进实现
以下是修复竞态条件、支持主线程及时响应中断的C代码:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <stdbool.h> #include <sys/wait.h> #include <stdatomic.h> #include <errno.h> pthread_mutex_t lock; pid_t forked_process = 0; atomic_bool need_exit = ATOMIC_VAR_INIT(false); void *connection_monitor(void *args) { // 模拟客户端断开检测 sleep(5); atomic_store(&need_exit, true); pthread_mutex_lock(&lock); if (forked_process != 0) { // 发送SIGINT给子进程组(确保脚本及其子进程都收到信号) killpg(forked_process, SIGINT); } pthread_mutex_unlock(&lock); return NULL; } int interruptible_system(const char *command) { pid_t pid; char *argp[] = {"sh", "-c", NULL, NULL}; argp[2] = (char *)command; int status = -1; switch (pid = fork()) { case -1: return -1; case 0: // 子进程设置为新进程组组长,确保信号能覆盖所有子进程 setpgid(0, 0); // 恢复SIGINT默认处理逻辑 signal(SIGINT, SIG_DFL); execv("/bin/sh", argp); _exit(127); } pthread_mutex_lock(&lock); forked_process = pid; pthread_mutex_unlock(&lock); // 非阻塞等待子进程,同时轮询中断标志 while (!atomic_load(&need_exit)) { if (waitpid(pid, &status, WNOHANG) == pid) { // 子进程正常退出,跳出循环 break; } usleep(100000); // 100ms轮询一次,平衡响应速度与CPU占用 } // 若触发中断,主动终止子进程并等待其清理退出 if (atomic_load(&need_exit)) { pthread_mutex_lock(&lock); if (forked_process == pid) { killpg(pid, SIGINT); waitpid(pid, &status, 0); } pthread_mutex_unlock(&lock); } pthread_mutex_lock(&lock); forked_process = 0; pthread_mutex_unlock(&lock); return status; } int main(int argc, char const *argv[]) { pthread_t monitor_thread; int ret = 0; if (pthread_mutex_init(&lock, NULL)) { printf("Mutex init failed\n"); return -1; } pthread_create(&monitor_thread, NULL, connection_monitor, NULL); ret = interruptible_system("./script.sh"); printf("Server exiting, upgrade status: %d\n", ret); pthread_join(monitor_thread, NULL); pthread_mutex_destroy(&lock); return 0; }
关键改进点:
- 使用
killpg替代kill:向整个进程组发送信号,确保脚本启动的所有子进程(如sleep)都能收到中断信号 - 非阻塞
waitpid轮询:主线程不再阻塞,能及时响应need_exit标志 - 子进程设置进程组:避免信号只传递给bash进程,而遗漏其启动的子进程
- 原子变量+互斥锁:确保子进程PID的读写安全,避免PID复用风险
3. 其他可行方向
- 使用管道/事件FD替代信号:子线程检测到连接断开时,向管道写入数据,主线程通过
select监听管道与子进程状态,这种方式比信号更稳定,避免信号竞态 - 拆分升级步骤:将长时脚本拆分为多个短步骤,主线程在每个步骤执行前检查
need_exit标志,无需中断正在运行的子进程 - 直接用C实现升级逻辑:完全替代bash脚本,在代码中插入中断检查点,实现更精细的控制(适合对中断响应要求极高的场景)
内容的提问来源于stack exchange,提问作者Douglas B
相关产品推荐
相关产品推荐

