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

捕获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, &currentStatus, 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处理
      }
      
  • 修复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, &currentStatus, 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 19:17:09