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#

Let's first 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: A signal handling function that 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 manipulate signal sets. Here, it means adding all supported signals of the system to a signal set.
  3. fork3: An API that is familiar to everyone. It creates a new process and returns the process ID (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 the resources of terminated child processes.

OK, after understanding these key syscalls, the above 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 the settings of signal handlers from the parent process.
  2. After execve is executed, the program segment and stack of the current process are reset. So when we execute /bin/date in the above code, the child process will be reset. The settings of signal handlers 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 signal processing of the process is temporarily blocked. At this time, the signal is in a pending state. After unblocking and unmasking the corresponding signal, the signal processing mechanism of the process is triggered again. If the process explicitly declares to ignore the signal, the signal will not be processed. (Tips: Regarding the signal queue, this is a convention in POSIX. 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 reliably delivered. 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 "jumps" 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 with EINTR1.

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

Alright, that's about it. I'm too tired to write more articles. 💊 The next article should be about my recent experience with 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.