DEV Community

Ariston
Ariston

Posted on • Edited on

unix高级编程之信号

一、信号提出的背景

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

二、信号概念

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

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

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

来自man

1.signal

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Enter fullscreen mode Exit fullscreen mode

对于捕捉到的信号有三个动作,分别是忽略、系统默认和捕捉。
当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信号:

#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;
}

Enter fullscreen mode Exit fullscreen mode

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

二者搭配使用,示例如下

#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;
}
Enter fullscreen mode Exit fullscreen mode

示例代码展示错误的sleepl,简介展示了alarm和pause这两者操作有时需要原子性的必要

#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;
}
Enter fullscreen mode Exit fullscreen mode

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

信号集

表示一个或多个信号的集合。
早期的信号数量少,少于一个整型所包含的位数,但是后来多起来了,就不能再用普通的整型来表示信号了,

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);
Enter fullscreen mode Exit fullscreen mode

函数sigprocmask

是一个信号管理函数,用于检查和更改进程的信号屏蔽字,信号屏蔽字是一个信号集,指示了哪些信号被阻塞不能被递送给进程。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
Enter fullscreen mode Exit fullscreen mode

函数sigpending

用于检查当前进程的挂起信号集,挂起信号集是已被发送给进程但还未被处理的信号,

#include <signal.h>

int sigpending(sigset_t *set);

Enter fullscreen mode Exit fullscreen mode

函数sigaction

用来检查或修改与指定信号相关联的处理动作,允许程序更精确地控制信号的各种行为。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
Enter fullscreen mode Exit fullscreen mode

示例代码如下

#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;
}

Enter fullscreen mode Exit fullscreen mode
signaction的内部如下:

Enter fullscreen mode Exit fullscreen mode

Top comments (0)