捕获SIGINT与SIGTSTP信号导致Shell功能异常及fork炸弹问题
简易Shell开发问题:信号处理异常与fork炸弹排查
问题背景
- 核心需求:
- 按下Ctrl-Z时切换至仅前台模式(禁止后台进程)
- 按下Ctrl-C仅终止当前前台进程,后台子进程需忽略该信号
- 初始异常:程序启动后
ls、sleep 10等命令可正常执行,触发任一信号后功能暂时正常,但后续输入普通命令(如ls)无法执行,怀疑STDIN被信号干扰 - 更新后异常:改用
sigaction处理SIGINT后,该信号可正常响应,但触发SIGTSTP会引发fork炸弹
原始代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <dirent.h> #include <limits.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> int last_exit_status = 0; int last_termination = 0; int backgroundChildrenRunning = 0; int backgroundPid = 0; int ignoreBackground = 0; void child_sigint_handler(int signum) { //nothing here for now } void child_sigint_handlerSec(int signum) { //do nothing here for background process } void sigint_handler(int signum) {// Signal handler for SIGINT (Ctrl-C) //nothing here now } void sigtstp_handler(int signum) {// Signal handler for SIGTSTP //nothing here for now } void check_background() { if (backgroundChildrenRunning > 0) { //this function is to print the background process termination signal int currentStatus; int completedChild = waitpid(-1, ¤tStatus, WNOHANG); //use WHOHANG so it does not block if (completedChild > 0) { if (WIFEXITED(currentStatus)) { printf("Background process with ID %d has been completed. Exit value: %d.\n", completedChild, WEXITSTATUS(currentStatus)); last_termination = WEXITSTATUS(currentStatus); } else { if (completedChild == backgroundPid) { printf("Background process with ID %d has been completed. Terminated by signal: %d.\n", completedChild, WTERMSIG(currentStatus)); last_termination = WTERMSIG(currentStatus); } } backgroundPid = 0; backgroundChildrenRunning -= 1; } } } char* prompt(int pid); //main func int main() { int x = 0; char *token; char *argv[1024]; // Array to store command and arguments int i = 0; struct sigaction sa; sa.sa_flags = SA_RESTART; sa.sa_handler = sigint_handler; sigaction(SIGINT, &sa, NULL); struct sigaction sa_tstp; sa_tstp.sa_handler = sigtstp_handler; sigaction(SIGTSTP, &sa_tstp, NULL); while(x == 0) { check_background(); int pid = getpid(); char* command = prompt(pid); //get user input if (command != NULL) { int childExitMethod; pid_t spwanPID = fork(); token = strtok(command, " "); // Split the command into tokens while (token != NULL) {//add in all tokens into arr argv[i] = token; token = strtok(NULL, " "); i++; } argv[i] = NULL; int hasSymbolEnd = 0; if (strcmp(argv[i - 1], "&") == 0){ //checks to see if background or foreground process if (ignoreBackground == 0) { hasSymbolEnd = 1; } argv[i - 1] = NULL; // Remove the '&' symbol } switch (spwanPID){ case -1: last_exit_status = 1; case 0: if (hasSymbolEnd == 1) { backgroundPid = getpid(); sa.sa_handler = child_sigint_handlerSec; } else { sa.sa_handler = child_sigint_handler; } execvp(argv[0], argv); last_exit_status = 1; exit(1); default: if (hasSymbolEnd == 0){ waitpid(spwanPID, &childExitMethod, 0); // Wait for the child only if background if (WIFEXITED(childExitMethod) && !WEXITSTATUS(childExitMethod)) { last_exit_status = 0; } else if (!WIFSIGNALED(childExitMethod)){ printf("bash: %s: command not found\n", command); last_exit_status = 1; } if (WIFSIGNALED(childExitMethod)) { //Check if the child was terminated by a signal int terminatedBySignal = WTERMSIG(childExitMethod); printf("Terminated by signal %d\n", terminatedBySignal); } } else { printf("background pid is %d\n", spwanPID); backgroundChildrenRunning += 1; } } } } return 0; } char* prompt(int pid) { printf("%d:", pid); fflush(stdout); char* input = NULL; size_t input_size = 0; ssize_t read_bytes = getline(&input, &input_size, stdin);// Use getline to read user input if (read_bytes == -1) { free(input); // Free the memory return NULL; } if (input[read_bytes - 1] == '\n') {// Remove the newline character at the end input[read_bytes - 1] = '\0'; } return input; }
问题分析与修复方案
1. 信号触发后命令无法执行的修复
- 问题根源:SIGTSTP的
sigaction未设置SA_RESTART标志,导致getline被信号打断后无法自动重启,后续输入读取失败。 - 修复代码:
struct sigaction sa_tstp; sa_tstp.sa_flags = SA_RESTART; // 添加该标志 sa_tstp.sa_handler = sigtstp_handler; sigaction(SIGTSTP, &sa_tstp, NULL);
2. SIGTSTP引发fork炸弹的修复
- 问题根源:
main函数中i变量用于记录命令参数个数,但每次循环后未重置为0,导致argv数组被持续累加填充,后续子进程执行execvp时参数混乱,触发异常fork行为。 - 修复代码:在每次读取命令后、解析参数前重置
i:if (command != NULL) { int i = 0; // 移到此处,每次循环重置为0 int childExitMethod; pid_t spwanPID = fork(); token = strtok(command, " "); while (token != NULL) { argv[i] = token; token = strtok(NULL, " "); i++; } // ... 后续代码 }
3. 其他关键问题修复
- 信号处理函数实现:
- SIGTSTP处理函数需切换
ignoreBackground状态,且使用write而非printf(避免信号重入风险):void sigtstp_handler(int signum) { ignoreBackground = !ignoreBackground; const char* msg = ignoreBackground ? "\nEntering foreground-only mode (& is now ignored)\n" : "\nExiting foreground-only mode\n"; write(STDOUT_FILENO, msg, strlen(msg)); } - 后台进程直接忽略SIGINT,无需自定义空函数:
if (hasSymbolEnd == 1) { signal(SIGINT, SIG_IGN); // 直接忽略SIGINT } else { signal(SIGINT, SIG_DFL); // 前台进程恢复默认SIGINT处理 }
- SIGTSTP处理函数需切换
- 修复fork错误分支:
case -1:后添加break,避免执行子进程代码:case -1: last_exit_status = 1; perror("fork failed"); break; // 添加break case 0: // ... 子进程代码 - 完善后台进程状态检查:
check_background中循环处理所有已结束的后台进程:void check_background() { int currentStatus; pid_t completedChild; // 循环处理所有已结束的后台进程 while ((completedChild = waitpid(-1, ¤tStatus, WNOHANG)) > 0) { if (WIFEXITED(currentStatus)) { printf("Background process with ID %d has been completed. Exit value: %d.\n", completedChild, WEXITSTATUS(currentStatus)); last_termination = WEXITSTATUS(currentStatus); } else if (WIFSIGNALED(currentStatus)) { printf("Background process with ID %d has been completed. Terminated by signal: %d.\n", completedChild, WTERMSIG(currentStatus)); last_termination = WTERMSIG(currentStatus); } backgroundChildrenRunning -= 1; } }
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

