You have too many errors in your code (due to the fact that you are not struct sigaction
signal mask for the struct sigaction
) for someone to explain the effects you see.
Instead, consider the following working example code, for example example.c
:
#define _POSIX_C_SOURCE 200809L #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <string.h> #include <stdio.h> #include <errno.h> /* Child process PID, and atomic functions to get and set it. * Do not access the internal_child_pid, except using the set_ and get_ functions. */ static pid_t internal_child_pid = 0; static inline void set_child_pid(pid_t p) { __atomic_store_n(&internal_child_pid, p, __ATOMIC_SEQ_CST); } static inline pid_t get_child_pid(void) { return __atomic_load_n(&internal_child_pid, __ATOMIC_SEQ_CST); } static void forward_handler(int signum, siginfo_t *info, void *context) { const pid_t target = get_child_pid(); if (target != 0 && info->si_pid != target) kill(target, signum); } static int forward_signal(const int signum) { struct sigaction act; memset(&act, 0, sizeof act); sigemptyset(&act.sa_mask); act.sa_sigaction = forward_handler; act.sa_flags = SA_SIGINFO | SA_RESTART; if (sigaction(signum, &act, NULL)) return errno; return 0; } int main(int argc, char *argv[]) { int status; pid_t p, r; if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { fprintf(stderr, "\n"); fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]); fprintf(stderr, " %s COMMAND [ ARGS ... ]\n", argv[0]); fprintf(stderr, "\n"); return EXIT_FAILURE; } /* Install signal forwarders. */ if (forward_signal(SIGINT) || forward_signal(SIGHUP) || forward_signal(SIGTERM) || forward_signal(SIGQUIT) || forward_signal(SIGUSR1) || forward_signal(SIGUSR2)) { fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno)); return EXIT_FAILURE; } p = fork(); if (p == (pid_t)-1) { fprintf(stderr, "Cannot fork(): %s.\n", strerror(errno)); return EXIT_FAILURE; } if (!p) { /* Child process. */ execvp(argv[1], argv + 1); fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno)); return EXIT_FAILURE; } /* Parent process. Ensure signals are reflected. */ set_child_pid(p); /* Wait until the child we created exits. */ while (1) { status = 0; r = waitpid(p, &status, 0); /* Error? */ if (r == -1) { /* EINTR is not an error. Occurs more often if SA_RESTART is not specified in sigaction flags. */ if (errno == EINTR) continue; fprintf(stderr, "Error waiting for child to exit: %s.\n", strerror(errno)); status = EXIT_FAILURE; break; } /* Child p exited? */ if (r == p) { if (WIFEXITED(status)) { if (WEXITSTATUS(status)) fprintf(stderr, "Command failed [%d]\n", WEXITSTATUS(status)); else fprintf(stderr, "Command succeeded [0]\n"); } else if (WIFSIGNALED(status)) fprintf(stderr, "Command exited due to signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status))); else fprintf(stderr, "Command process died from unknown causes!\n"); break; } } /* This is a poor hack, but works in many (but not all) systems. Instead of returning a valid code (EXIT_SUCCESS, EXIT_FAILURE) we return the entire status word from the child process. */ return status; }
Compile it for example
gcc -Wall -O2 example.c -o example
and execute for example
./example sqlite3
You will notice that Ctrl + C does not interrupt sqlite3
- but then again, even if you did not run sqlite3
directly -; instead, you just see ^C
on the screen. This is because sqlite3
sets up the terminal in such a way that Ctrl + C does not cause a signal and is simply interpreted as normal input.
You can exit sqlite3
with the .quit
or press Ctrl + D at the beginning of the line.
You will see that the source program displays the Command ... []
after that, before returning you to the command line. Thus, the parent process is not killed / not damaged / not worried about signals.
You can use ps f
to look at the tree of your terminal processes, and thus find out the PID of the parent and child processes and send signals to one of them to watch what happens.
Please note that since the SIGSTOP
signal cannot be caught, blocked or ignored, it would not be trivial to reflect operation control signals (as when using Ctrl + Z ). For proper job management, the parent process will need to configure a new session and process group and temporarily disconnect from the terminal. This is also entirely possible, but is a bit beyond the scope of this area, since it requires fairly detailed behavior of sessions, process groups, and terminals for proper management.
Let deconstruct the above sample program.
In the most sample program, some signal reflectors are installed first, and then the child process is deployed, and this child process executes the sqlite3
command. (You can specify any executable file and any parameter lines in the program.)
The internal_child_pid
and set_child_pid()
and get_child_pid()
functions are used atomically to control the child process. __atomic_store_n()
and __atomic_load_n()
are built-in built-in compilers; for GCC, see here . They avoid signaling problems when the child pid is only partially assigned. On some common architectures, this cannot happen, but it is intended as a cautious example, therefore atomic calls are used to provide only the full (old or new) value. We could avoid using them completely if we temporarily blocked the associated signals during the transition. Again, I decided that access to atoms is easier, and it can be interesting to see in practice.
The forward_handler()
function receives the physical identifier of the child process and then checks that it is non-zero (we know that we have a child process) and that we are not forwarding the signal sent by the child process (just to make sure we put on " do not cause a signal storm, and both bombard each other with signals "). The various fields in the siginfo_t
structure siginfo_t
listed on the man 2 sigaction
.
The forward_signal()
function sets the above handler for the specified signum
signal. Note that we first use memset()
to clear the entire structure to zeros. Cleaning this method provides future compatibility if some additions in the structure are converted to data fields.
The .sa_mask
field in struct sigaction
is an unordered set of signals. The signals set in the mask are blocked from being delivered in the thread that the signal handler executes. (In the above sample program, we can confidently say that these signals are blocked during the start of the signal handler, namely in multi-threaded programs, signals are blocked only in the specific thread that is used to start the handler.)
It is important to use sigemptyset(&act.sa_mask)
to clear the signal mask. Just setting the structure to zero is not enough, even if it works (probably) in practice on many machines. (I don’t know, I didn’t even check. I prefer rugged and reliable lazy and fragile any day!)
The flags used include SA_SIGINFO
, because the handler uses a form with three arguments (and uses the si_pid
siginfo_t
field). The SA_RESTART
flag exists only because the OP wanted to use it; it just means that if possible, the C library and the kernel try to avoid returning an errno == EINTR
error if the signal is delivered using a stream that is currently blocked in syscall (e.g. wait()
)). You can remove the flag SA_RESTART
and add debugging fprintf(stderr, "Hey!\n");
to a suitable place in the loop in the parent process to find out what happens next.
The sigaction()
function will return 0 if there is no error, or -1
with errno
set differently. The forward_signal()
function returns 0 if the forward_handler
assigned successfully, but nonzero errno otherwise. Some people don't like this type of return value (they prefer just returning -1 for an error, rather than an errno
value), but for some unreasonable reason I loved this idiom. Change it if you want, by all means.
Now we go to main()
.
If you run the program without options or with a single -h
or --help
option, it will print a usage summary. Again, doing it this way is what I like - getopt()
and getopt_long()
are more often used to parse command-line options. For this trivial program, I simply hard-coded the parameter check.
In this case, I intentionally left the usage conclusion very short. It would be much better with an additional paragraph about what the program does. Such texts are very important - and especially comments in the code (an explanation of the intention, the idea of what the code should do, not a description of what the code actually does). More than two decades have passed since the first time I was paid for writing code, and I'm still learning to comment, describe the purpose of my code better, so I think the sooner it starts working on it, it's better.
The fork()
should be familiar. If it returns -1
, the plug failed (possibly due to restrictions or some of them), and then it is a very good idea to print an errno
message. The return value will be 0
in the child, and the child process identifier in the parent process.
The execlp()
function takes two arguments: the name of the binary file (the directories specified in the PATH environment variable will be used to search for such a binary file), as well as an array of pointers to the arguments of this binary file. The first argument will be argv[0]
in the new binary, that is, the command name itself.
Call execlp(argv[1], argv + 1);
actually quite simple to analyze when compared with the above description. argv[1]
denotes an executable binary. argv + 1
is basically equivalent to (char **)(&argv[1])
, i.e. this is an array of pointers starting with argv[1]
instead of argv[0]
. Once again, I just love the idiom execlp(argv[n], argv + n)
, because it allows you to execute another command specified on the command line without worrying about parsing the command line or running it through the shell (which is sometimes completely undesirable) .
The man 7 signal
page explains what happens to the signal handlers in fork()
and exec()
. In short, signal handlers are inherited on fork()
, but reset by default to exec()
. Fortunately, exactly what we want is here.
If we first unlocked and then installed the signal handlers, we would have a window during which the child process already exists, but the parent still has the default settings (basically termination) for the signals.
Instead, we could simply block these signals using, for example, sigprocmask()
in the parent process before forking. Blocking a signal means it is made to "wait"; it will not be delivered until the signal is unlocked. In a child process, signals can remain blocked, since the default location of reset signals is exec()
. In the parent process, we could — or before branching — not matter — set up signal handlers and finally unlock the signals. Thus, we do not need atomic material or even checking if the child pid is zero, since the child pid will be set to its actual value long before any signal is delivered!
The while
is basically a loop around the call to waitpid()
, until the exact child process that we started, or something funny, finishes (the child process somehow disappears). This cycle contains a fairly thorough error checking, as well as the correct EINTR
transmission if signal handlers should have been installed without the SA_RESTART
flags.
If we fork the child process, we check the exit status and / or the reason why he died, and print a diagnostic message for the standard error.
Finally, the program ends with a terrible hack: instead of returning EXIT_SUCCESS
or EXIT_FAILURE
we return the full status word that we received with waitpid when the child process exited. The reason I left this is because it is sometimes used in practice when you want to return the same or similar exit status code as the returned child process. So this is for illustration. If you ever find yourself in a situation where your program must return the same exit status as the child process that it forked and executed, it's still better than creating a mechanism for the process to kill itself with the same signal that killed the child handle. Just post a wonderful comment there if you ever need to use it, as well as a note in the installation instructions so that those who compile the program on architectures where this may not be desirable can fix it.