Calling kill in a child process using SIGTERM terminates the parent process, but calling it using SIGKILL supports the parent

This Continuation How to prevent SIGINT in a child process from spreading and destroying the parent process?

In the above question, I found out that SIGINT does not bubble from the child to the parent, but rather it is issued to the whole group of foreground processes, that is, I need to write a signal handler to prevent the parent when I hit CTRL + C

I tried to implement this, but here is the problem. In particular, the kill script that I call to terminate the child process, if I passed in SIGKILL , everything works as expected, but if I go through SIGTERM , it will also end the parent process, showing Terminated: 15 in the shell later.

Despite the fact that SIGKILL works, I want to use SIGTERM, because it seems like the best idea in general from what I read about it, giving the process, signaling the cessation of the opportunity to clean itself.

The following code is a stripped-down example of what I came up with

 #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> pid_t CHILD = 0; void handle_sigint(int s) { (void)s; if (CHILD != 0) { kill(CHILD, SIGTERM); // <-- SIGKILL works, but SIGTERM kills parent CHILD = 0; } } int main() { // Set up signal handling char str[2]; struct sigaction sa = { .sa_flags = SA_RESTART, .sa_handler = handle_sigint }; sigaction(SIGINT, &sa, NULL); for (;;) { printf("1) Open SQLite\n" "2) Quit\n" "-> " ); scanf("%1s", str); if (str[0] == '1') { CHILD = fork(); if (CHILD == 0) { execlp("sqlite3", "sqlite3", NULL); printf("exec failed\n"); } else { wait(NULL); printf("Hi\n"); } } else if (str[0] == '2') { break; } else { printf("Invalid!\n"); } } } 

My educated guess about why this happens will be to intercept SIGTERM with something and kill the entire group of processes. Whereas when I use SIGKILL, it cannot intercept the signal, so my kill call works as expected. It's just a hit in the dark though.

Can anyone explain why this is happening?

As I note, I am not happy with my handle_sigint function. Is there a more standard way to kill an interactive child process?

+6
source share
1 answer

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.

+9
source

Source: https://habr.com/ru/post/1012128/


All Articles