unix高级编程之信号
Published: 2024-01-17 00:46:06

# 一、信号提出的背景
信号在unix早期版本就已经提供，用于进程间通信，他们提供了一种方式，使得操作系统能够**中断**进程的执行，以通知它发生了某个时间。这个机制最初是为了处理不同的异步事件。

# 二、信号概念
每个信号都有一个名字这，些名字都以SIG开头。在头文件<signal.h>或<bits/signum.h>中，都是正整数常量。
信号由内核产生。
当某种信号出现时，我们可以采用以下三种方式：
1.忽略
2.捕捉信号（不能捕捉SIGKILL和SIGSTOP）
3.执行系统默认动作，对于每一种系统默认动作，大多数都是终止该进程。
对于有些信号，默认动作是终止+core，其中core文件复制了该进程的内存映像，core文件可以用来检查进程终止时的状态。
不产生core文件的5个条件，详见书中252页。

一些扫了一眼看上去比较重要的信号：
SIGPIPE：在管道终止时写管道会产生此信号。当类型为SOCK_STREAM的套接字不再连接，进程写该套接字也会产生此信号。
SIGKILL、SIGSTOP：两大不能忽视和捕捉的信号。第一个杀死一个进程，第二个则是挂起一个进程。
SIGTERM：和SIGKILL类似，也是用于中止一个进程的，区别在于该信号能被捕获，让程序有机会在退出之前做好清理工作。
SIGTTIN：后台进程组进程试图读取控制终端会产生此信号。有两种特殊情况，1.写进程忽略或阻塞此信号。2.写进程属于孤儿进程组，直接返回出错。

对于信号处理程序，那自然是越简单越快速越好。

# 来自man
## 1.signal
```c++
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
```
对于捕捉到的信号有三个动作，分别是忽略、系统默认和捕捉。
当handler的值是
SIG_IGN忽略此信号
SIG_DFL系统默认此信号
自定义函数，指定函数地址时，称此函数为signal handler或signal-catching function
对于signal的返回值，是上一个信号处理函数的地址。我们不关心这个，所以我们不接收signal的返回。
书中10-2的代码，其中kill并不是杀死进程，而是发送一个信号给一个进程或进程组。
当我们用exec开启进程，如果原本对于信号的处理是忽略，那么还是忽略，此外其他的全部都是变成默认。
用fork则保留父进程的信号处理方式。

## 2.不可靠的信号
这种老版本会有时间窗口问题，也就是在处理第一个信号时，第二个信号也过来了，那么对第二个信号的处理会采取系统默认的行为，（在 Unix 中，信号处理函数一旦被调用，除非重新设置，否则系统会将信号的处理方式重置为默认。这意味着如果在处理函数执行期间再次收到相同的信号，系统会采取该信号的默认行动，如终止程序。）后面会有更现代的做法。

## 3.中断的系统调用
有些系统调用捕捉到了信号中断后会重新启动，比如ioctl、read、readv、write、writev、wait和waitpid。前5个函数只有在对低速设备进行操作时才会被信号中断，而wait和waitpid捕捉到信号时总是被中断。但是我们有时候不希望这些被中断后重新启动。

注意：在这里我们要区分系统调用和函数，被中断的是内核中执行系统调用而不是函数，

低速系统调用，它们需要等待一些外部事件发生，但是可能他们永远不会发生。

## 4.可重入函数
可以被中断并安全地被另一个线程调用的函数
成为可重入函数要满足如下几个条件：
1.不使用静态或全局数据：可重入函数不能依赖存储在全局或静态变量中的数据，除非这些数据是常量。依赖全局或静态数据会使函数在并发环境中的行为变得不可预测。

2.不修改自己的代码：也就是说，函数不能包含自修改代码。

3.不调用不可重入的函数：可重入函数在执行过程中不能调用其他不可重入的函数，因为那将使整个函数调用链不可重入。

4.使用局部变量：可重入函数应该只操作传入的参数和局部变量。因为局部变量通常存储在堆栈上，每次函数调用都会得到自己的变量副本。


## 5.SIGCLD语义
SIGCLD是进程的子进程状态发生改变时，进程收到的信号，信号SIGCLD或SIGCHLD的默认行为通常是被忽略，这意味着默认情况下父进程不会对它做出任何反应。但是，父进程可以通过编写信号处理函数来捕获这个信号，并进行相应的处理，如调用wait()系列函数来获取子进程的终止状态并清理资源。这样做可以防止子进程成为僵尸进程。
例如，父进程可能会这样设置一个信号处理函数来响应SIGCHLD信号：
```cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

void child_exit_signal_handler(int sig) {
    int status;
    pid_t child_pid;

    // 当捕获到SIGCHLD信号时，调用waitpid()来处理子进程的退出
    while ((child_pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("Child process %d exited with status %d\n", child_pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child process %d was terminated by signal %d\n", child_pid, WTERMSIG(status));
        }
    }
}

int main() {
    struct sigaction sa;
    sa.sa_handler = child_exit_signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;

    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // 这里可以创建子进程和其他逻辑
    // ...

    return 0;
}

```

## 6.可靠信号术语和语义

### 早期的Unix信号处理限制：

#### 1.不排队：早期Unix信号不会排队。如果两个相同类型的信号几乎同时发生，只有一个会被递送；因此，可能丢失信号。
#### 2.不可阻塞：某些信号不能被阻塞，这意味着程序无法暂时忽略这些信号，即使它们在不便的时刻到达。
#### 3.默认行为限制：对于大多数信号，进程只能选择接收默认行为（通常是终止）或忽略信号，而不能指定一个自定义的处理函数。

### 可靠信号的特点：
1.可阻塞：几乎所有的信号都可以被阻塞。如果一个信号被阻塞，那么产生的信号将不会立即递送给进程，直到该信号被解除阻塞。这允许程序控制信号的处理时间，防止在关键代码执行期间被中断。
2.排队支持：虽然标准的Unix信号机制本身不支持排队，POSIX标准引入的实时信号提供了信号排队的功能。这意味着，如果有多个实时信号被发送到进程，它们将会被逐一递送，减少了信号丢失的风险。
3.信号处理函数：程序可以为大多数信号指定自定义的处理函数（信号处理程序），这允许执行比默认行为更复杂的操作。

### 术语和语义
可靠信号是指不会丢失、可以被适当阻塞并处理的信号机制。
语义：指信号处理的行为和信号系统的设计原则，包括如何递送、阻塞和处理信号。
在信号产生和信号送达的这个时间内，信号是未决的pending。


## kill和raise
kill将信号发给进程或进程组。
raise将信号发给自身

## alarm和pause
alarm为当前进程设置一个计时器，计时器到期时，操作系统向进程发送SIGALRM信号，如果进程没有捕获或者忽略这个信号，进程将执行默认的信号处理动作，即停止。
alarm函数常用于限制某操作的执行时间。比如在网络编程中，可能希望限制连接操作的时间，防止程序因等待响应而无限期挂起。
alarm的返回值是之前设置的闹钟的剩余的秒数。
当你在代码中连续两次调用alarm函数时，第二次调用会取消前一次alarm调用所设置的定时器，并替换为新的定时器。

pause函数使调用进程挂起直到捕获一个信号。
pause不关心返回值，因为它一直在等待某个信号到来，总是返回-1

二者搭配使用，示例如下
```c++
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handle_alarm(int sig) {
    printf("Alarm went off!\n");
    // 在这里可以执行定时器到期后需要完成的任务
}

int main(void) {
    // 设置SIGALRM的处理函数
    if (signal(SIGALRM, handle_alarm) == SIG_ERR) {
        perror("Signal handler setting failed");
        return 1;
    }

    printf("Setting a 5-second alarm...\n");
    alarm(5); // 设置定时器为5秒

    printf("Pausing until the alarm goes off...\n");
    pause(); // 等待信号，挂起程序

    printf("Doing some work after the alarm...\n");
    // 定时器到期后需要执行的代码
    // 例如：清理工作、准备下一个定时器、或其他逻辑

    return 0;
}
```

示例代码展示错误的sleepl，简介展示了alarm和pause这两者操作有时需要原子性的必要
```c++
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

static void sig_alrm(int signo) {

}

unsigned int sleep1(unsigned int seconds) {
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        return (seconds);
    }
    alarm(seconds);
    pause();
    return (alarm(0));
}

int main() {
    printf("before sleep\n");
    sleep1(3);
    printf("after sleep\n");
    return 0;
}
```
这里面的问题就在于，如果在pasue没被调用起来的时候，SIGALRM信号到达，alarm返回了，这是就会造成该进程一直处于挂起状态。
还有就是如果在sleepl之前alarm就被调用了，那么后面的alarm就会擦出之前的alarm导致前面的alarm永远不会接收到SIGALRM信号。
还有就是如果其他的函数需要用alarm做一些操作，但是这里用的signal(SIGALRM, sig_alrm)就把这个设置为空了，别的函数就不能在捕捉到SIGALRM信号时做一些操作了。

## 信号集
表示一个或多个信号的集合。
早期的信号数量少，少于一个整型所包含的位数，但是后来多起来了，就不能再用普通的整型来表示信号了，
```c++
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
//成功返回0出错返回-1
int sigismember(const sigset_t *set, int signo);
```

## 函数sigprocmask
是一个信号管理函数，用于检查和更改进程的信号屏蔽字，信号屏蔽字是一个信号集，指示了哪些信号被阻塞不能被递送给进程。
```c++
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
```

## 函数sigpending
用于检查当前进程的挂起信号集，挂起信号集是已被发送给进程但还未被处理的信号，
```c++
#include <signal.h>

int sigpending(sigset_t *set);

```

## 函数sigaction
用来检查或修改与指定信号相关联的处理动作，允许程序更精确地控制信号的各种行为。
```c++
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
```
示例代码如下
```c++
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("Caught signal %d\n", sig);
}

int main() {
    struct sigaction act;

    act.sa_handler = sigint_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (sigaction(SIGINT, &act, NULL) < 0) {
        perror("sigaction");
        return 1;
    }

    // 等待信号
    for (;;) {
        pause(); // 使进程暂停，直到接收到信号
    }

    return 0;
}

```
```c++
signaction的内部如下：

```