C - 捕获 SIGINT 和 SIGTSTP 信号会导致错误

C - Catching SIGINT and SIGTSTP signals causes bug

提问人:ahmed 提问时间:11/10/2023 最后编辑:Jonathan Lefflerahmed 更新时间:11/18/2023 访问量:167

问:

你好,我正在做一个简单的外壳。当用户单击时,我想简单地切换仅前台模式的状态。当用户单击时,我只想终止前台进程(后台子进程应忽略它)。control-Zcontrol-C

如果我运行我的 shell 并发送“ls”或“sleep 10”等命令,它们就会起作用。这样做之后,如果我按 或 ,这也有效。只有在发送或正常命令停止工作后。例如,在之后发送“ls”不会执行。为什么会这样?信号是否与STDIN混淆?control-Ccontrol-Zcontrol-Ccontrol-Zcontrol-C

对所收到评论意见的更新

现在使用 I 可以正确处理信号。然而,尝试会导致叉子炸弹。我怎样才能防止这种情况发生?sigactioncontrol-Ccontrol-Z

#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;
}
C 信号

评论

5赞 Oka 11/10/2023
注意:从信号处理程序调用 是不安全的。请参见:signal-safety(7)printfexit
0赞 ahmed 11/10/2023
@Oka我可以用write代替printf,那么exit呢?如何从信号处理程序中退出子项?
0赞 Oka 11/10/2023
_exit () 是异步信号安全的。另外,请注意,也是不安全的。_Exitfflush
1赞 dimich 11/10/2023
不要用于设置信号处理程序,请使用(和注意标志)。唯一允许从信号处理程序访问的类型是 。在 x86 上,它是,但 excplicit 类型会更具可读性。从信号处理程序访问的变量应为 。失败后打印相关错误消息。将命令拆分为令牌在父进程和子进程中执行两次。其他一些缺陷...对于交互式应用程序(例如 shell),我建议不要弄乱信号处理程序,而是将事件循环与 .signal()sigaction()SA_RESTARTsig_atomic_tintvolatileexecvp()signalfd()
1赞 Andrew Henle 11/11/2023
@ahmed 您的代码忽略了可能返回 的可能性。这很危险,因为它会访问缓冲区之外的内存。请参阅从 fgets() 输入中删除尾随换行符getline()0if (input[read_bytes - 1] == '\n') ...input

答:

1赞 VonC 11/13/2023 #1

关于处理,您可以:SIGINT

  • 在 Main shell 中,忽略 .SIGINT
  • 在子进程中,仅当它们是前台进程时,才处理终止。SIGINT

为了进行处理,请在 .SIGTSTPsigtstp_handler

// rest of your includes and global variables

void sigint_handler(int signum) {
    // Main shell should ignore SIGINT
}

void sigtstp_handler(int signum) {
    ignoreBackground = !ignoreBackground; // Toggle the state
}

// rest of your functions

int main() {
    // rest of your main function

    struct sigaction sa_ignore;
    sa_ignore.sa_handler = SIG_IGN; // Ignore signal
    sigaction(SIGINT, &sa_ignore, NULL);

    struct sigaction sa_tstp;
    sa_tstp.sa_handler = sigtstp_handler; // Handle SIGTSTP
    sigaction(SIGTSTP, &sa_tstp, NULL);

    // rest of your main function

    switch (spwanPID) {
    // rest of your switch case

    case 0:
        // Child process
        if (hasSymbolEnd == 1) {
            // Background process should ignore SIGINT
            sigaction(SIGINT, &sa_ignore, NULL);
        } else {
            // Foreground process should handle SIGINT
            struct sigaction sa_child;
            sa_child.sa_handler = SIG_DFL; // Default signal handling
            sigaction(SIGINT, &sa_child, NULL);
        }

        execvp(argv0, argv);
        // rest of your case 0
    }
    // rest of your main function
}

对于您的分叉过程,您将获得:

  User Input
      |
      v
   Shell Loop
      |
      +---> Fork Process
      |        |
      |        +---> Execute Command (ls, sleep)
      |        |         |
      |        |         +---> SIGINT Handling (Child)
      |        |
      |        +---> SIGINT Ignored (Background Child)
      |
      +---> SIGINT Ignored (Main Shell)
      |
      +---> SIGTSTP Toggles Foreground Mode (Main Shell)

每次单击控件 Z 时,我仍然会得到一个叉形炸弹。但是,控件 C 已正确处理

当被捕获时,它应该切换仅前台模式。但是,它不应创建新流程或干扰现有流程管理。SIGTSTP

确保主循环不会在不应该分叉新进程的时候分叉,尤其是在处理之后。
应该只切换一个标志 ()。它不应执行任何进程创建或其他复杂操作。
处理后,验证主循环是否不分叉新进程。如果在捕获信号后未正确管理程序的状态,则可能会发生这种情况。
SIGTSTPsigtstp_handlerignoreBackgroundSIGTSTP

我添加了一些用于调试:printf

void sigtstp_handler(int signum) {
    ignoreBackground = !ignoreBackground; // Toggle the state
    printf("Toggled foreground-only mode to %d\n", ignoreBackground);
    fflush(stdout);
}

int main() {
    // rest of your main function setup

    while (x == 0) {
        check_background();
        // rest of your command reading and parsing

        if (command != NULL) {
            // tokenizing and processing the command

            if (strcmp(argv[0], "") != 0) { // Check if the command is not empty
                pid_t spwanPID = fork();

                // rest of your fork and exec logic

            } else {
                free(command); // Free the command string if it is empty
            }
        }
    }
    // rest of your main function
}

主要思想是确保空命令或未正确解析的命令不会导致意外的分叉。

评论

0赞 ahmed 11/14/2023
谢谢,每次点击控制 Z 时,我仍然会得到一个叉形炸弹。但是,控件 C 已正确处理。
0赞 VonC 11/14/2023
@ahmed好的,我已经编辑了答案以解决您的评论。