Search code examples
cbashsignals

How to simulate signals (Ctrl+\ and Ctrl+C) like in bash using C?


I want to simulate the signals (Ctrl+\ and Ctrl+C) in bash but using C.

I want to kill a child process using signal function void (*signal(int sig, void (*func)(int)))(int);.

I reached so far:

#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

pid_t     cpid = 0;

void signal_handler(int sig) {
    // char parent_str[] = "this is ctrl + c in the parent prosess\n";
    char child_str[] = "this is ctrl + c in the child prosess\n";
    // signal(sig, signal_handler);
    if (sig == SIGINT) {
        write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        kill(cpid, SIGINT);
    }
    else if (sig == SIGQUIT)
    {
        write(STDOUT_FILENO, "this is ctrl + \\ in the child prosess\n", sizeof("this is ctrl + \\ in the child prosess\n") - 1);
        kill(cpid, SIGQUIT);
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGQUIT, signal_handler);
    pid = fork();
    cpid = pid;
    if (pid == 0) {
        cpid = 1;
        while(1);
        exit(EXIT_SUCCESS);
    }
    wait (NULL);
    printf("pid = %d\ncpisd = %d", pid, cpid);
    while (1);
}

but when I'm compiling this code, the process won't kill, why?

enter image description here


Solution

  • When you fork a process almost everything that has occurred thus far is duplicated in the child process - that is to say, the child process has a nearly identical environment to its parent at the very moment fork successfully returns. While there are some exceptions to this (as detailed in the man page for fork) signal handlers are retained.

    What happens in your terminal when you type ^C is that a SIGINT is sent to both your parent and child processes. This is handled in the same way, since the signal handling function is the same in parent and child, but with different results because of the value of cpid.

    In the parent, cpid is 0 and thus kill has this effect:

    If pid equals 0, then sig is sent to every process in the process group of the calling process.

    so the parent "forwards" that signal to the child, as it belongs to the parent's process group.

    The child process thus receives two signals; one directly from the terminal, and one from the parent process. Using the same handler, but this time cpid is 1, it calls kill and fulfills this criteria:

    If pid is positive, then signal sig is sent to the process with the ID specified by pid.

    So this calls

    kill(1, SIGINT)
    

    wherein PID 1 is almost certainly init, the root process responsibly for your entire system. It's probably a good thing this had no effect, as an alternative result could be your system shutting down.

    In the end, nothing really happens as each process pushes the signal along this chain, but this is why we see three lines of text: one from the parent, two from the child.


    We'll want to handle our signals differently in the parent and child, so it would be best to use two different handlers.

    Because of the overlap in signal as a result of sending it from the terminal, we will have to determine who sent the signal in the child if we want to only accept signals from the parent process directly. signal handlers can not provide this level of detail, so instead we need sigaction handlers.

    Here is a small example that splits signal handling between the parent and child. In the child, we setup our struct sigaction with the SA_SIGINFO flag, which indicates we want to use the sa_sigaction handler. We use getppid and compare it with info->si_pid to determine if the signal came from the parent directly.

    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    static pid_t child_id;
    
    void sigparent(int sig) {
        const char msg[] = "The parent will kill the child.\n";
    
        switch (sig) {
        case SIGINT:
        case SIGQUIT:
            write(STDOUT_FILENO, msg, (sizeof msg) - 1);
            kill(child_id, SIGQUIT);
            break;
        default:
            break;
        }
    }
    
    void sigchild(int sig, siginfo_t *info, void *ctx) {
        const char msg[] = "The child will now die.\n";
    
        if (getppid() == info->si_pid)
            switch (sig) {
            case SIGINT:
            case SIGQUIT:
                write(STDOUT_FILENO, msg, (sizeof msg) - 1);
                _exit(EXIT_SUCCESS);
                break;
            default:
                break;
            }
    }
    
    void parent(void) {
        signal(SIGINT, sigparent);
        signal(SIGQUIT, sigparent);
    
        puts("Parent will wait...");
        waitpid(child_id, NULL, WUNTRACED);
        puts("Parent is done waiting.");
    }
    
    void child(void) {
        struct sigaction action = { 0 };
    
        sigemptyset(&action.sa_mask);
        action.sa_flags = (SA_SIGINFO);
        action.sa_sigaction = sigchild;
    
        sigaction(SIGINT, &action, NULL);
        sigaction(SIGQUIT, &action, NULL);
    
        while (1) {
            puts("Child is sleeping...");
            sleep(2);
        }
    }
    
    int main(void) {
        if ((child_id = fork()))
            parent();
        else
            child();
    }