Manjusaka

Manjusaka

Let's talk about signal handling in processes.

Recently, I helped someone analyze a piece of code related to signal handling in Linux programming in a technical group. I personally think that this code is a good example, so I wrote a simple article to discuss signal handling in Linux using this code.

Main Content#

First, let's take a look at this piece of code:

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

void deletejob(pid_t pid) { printf("delete task %d\n", pid); }

void addjob(pid_t pid) { printf("add task %d\n", pid); }

void handler(int sig) {
  int olderrno = errno;
  sigset_t mask_all, prev_all;
  pid_t pid;
  sigfillset(&mask_all);
  while ((pid = waitpid(-1, NULL, 0)) > 0) {
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    deletejob(pid);
    sigprocmask(SIG_SETMASK, &prev_all, NULL);
  }
  if (errno != ECHILD) {
    printf("waitpid error");
  }
  errno = olderrno;
}

int main(int argc, char **argv) {
  int pid;
  sigset_t mask_all, prev_all;
  sigfillset(&mask_all);
  signal(SIGCHLD, handler);
  while (1) {
    if ((pid = fork()) == 0) {
      execve("/bin/date", argv, NULL);
    }
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    addjob(pid);
    sigprocmask(SIG_SETMASK, &prev_all, NULL);
  }
}

In fact, this code is a typical example of signal handling. To introduce the following content, let's first review several key syscalls in this code:

  1. signal1: The signal handling function allows users to specify the handler for a specific signal in the current process. When a signal is triggered, the system will call the corresponding handler to perform the corresponding logical processing.
  2. sigfillset2: One of the functions used to operate on signal sets. Here, it means adding all supported signals in the system to a signal set.
  3. fork3: A well-known API that creates a new process and returns the PID. If it is in the parent process, the returned PID is the PID of the corresponding child process. If it is in the child process, the PID is 0.
  4. execve4: Executes a specific executable file.
  5. sigprocmask5: Sets the signal mask of a process. When the first parameter is SIG_BLOCK, the function saves the signal mask of the current process in the signal set variable passed in the third parameter, and sets the signal mask of the current process to the signal mask passed in the second parameter. When the first parameter is SIG_SETMASK, the function sets the signal mask of the current process to the value set by the second parameter.
  6. waitpid6: To put it roughly, it reclaims and releases resources of terminated child processes.

OK, after understanding these key syscalls, this code should be relatively easy to understand. But to fully understand this code, we need to review some mechanisms in Linux or POSIX:

  1. Child processes created by fork inherit many things from the parent process. As for the signals discussed in this article, the child process inherits the signal mask and related settings of the signal handler from the parent process.
  2. After execve is executed, the program segment and stack of the current process will be reset. So when we execute /bin/date in the above code, the child process will be reset. The signal handler and other settings will also be reset.
  3. Each process has a signal mask. When a signal in the signal mask is triggered, it enters a queue and the process's signal handling is temporarily not triggered. At this time, the signal is in a pending state. After unblocking and unmasking the corresponding signal, the process's signal handling mechanism is triggered again. If the process explicitly declares to ignore the signal, the signal will not be triggered. (Tips: Regarding the signal queue, this is a POSIX 1 convention. In POSIX, this mechanism is called reliable signals. When multiple signals occur during blocking, they enter a reliable queue to ensure that the signals are delivered reliably. Linux supports reliable signals, but other Unix/Unix-like systems may not.)
  4. After a child process exits, it sends a SIGCHLD1 signal to its parent process. The parent process needs to call the waitpid6 function to handle the child process. Otherwise, unreclaimed child processes will become zombie processes.

OK, by now, with a grasp of these concepts, you should be able to fully understand the above code. However, you may still have a question: why do we need to use sigprocmask5 to block signals in this code? This involves another issue.

As mentioned earlier, when a signal is triggered, the process will "jump" to the corresponding signal handler for processing. But what happens after the signal handler finishes processing? According to the design in Linux, two situations may occur:

  1. For reentrant functions, the signal handler will continue processing after it returns.
  2. For non-reentrant functions, it will return EINTR1.

OK, now you should have a specific understanding of why we use sigprocmask5 here. In fact, it is to ensure that our functions can be executed properly without being interrupted by signal handling. Of course, there are other issues here. If signals are triggered very frequently, this handling will bring additional costs. So it still needs to be a trade-off based on different scenarios.

Alright, that's about it. I'm too tired to write more articles after writing for a long time, 💊. The next article should be about my recent experience in monitoring the kernel protocol stack (flag++ (escape).

Reference#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.