信号的快速认识

生活角度认识

• 网购多件商品时,虽然快递尚未送达,但你清楚收到快递后的处理流程。这种能力可以称为"快递识别能力"
• 快递员到达楼下并通知你时,若你正在打游戏需要5分钟后才能取件,这段时间虽然未立即取件,但你知道快递已到。这说明取件行为可以"在适当时机执行",不必立即完成
• 从收到通知到实际取件之间存在时间窗口,这段时间虽未拿到快递,但你已经记住"有个快递待取"
• 成功取件后,通常有三种处理方式:1. 执行默认操作(开心地拆开快递使用商品)2. 执行自定义操作(如将零食快递转送给女友)3. 忽略快递(将快递扔在床头继续打游戏)
• 整个快递送达过程对你而言是异步的,无法准确预知快递员何时会联系你

基本结论:

你怎么能识别信号呢?

识别信号是内置的,进程识别信号,是内核程序员写的内置特性。

信号产生之后,你知道怎么处理吗?

知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。

处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?

合适的时候。

怎么进行信号处理啊?

a.默认 b.忽略 c.自定义, 后续都叫做信号捕捉。

技术应用角度的信号

假设运行下面的代码

#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {
        printf("I am a process, I am waiting signal!\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
我们知道我们的代码是一个死循环,对于死循环最好的解决方式就是用ctrl+c的方式终止。

所以为什么我们使用ctrl+c的方式就可以终止该进程了呢?

实际上当我们摁下此命令的时候,键盘就会产生硬中断,被操作系统获取并解释成信号(2号信号),然后操作系统将2号信号发送给目标前台进程,当前台进程收到2号信号后就会退出。

下面我们可以来证明一下:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int signum)
{
    printf("get signal!\n");
}

int main()
{
    signal(2,handler); //捕捉2号信号
    while(1)
    {
        printf("111111111\n");
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
我们可以看到上述代码,我们用signal函数对2号信号进行捕捉,证明了我们摁下ctrl+c确实是会收到2号信号的。

signal函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数

  • signum:指定要处理的信号编号,例如 SIGINT(表示中断信号,通常由 Ctrl+C 触发)。
  • handler:指定信号处理函数,当信号发生时,会调用这个函数。如果设置为 SIG_IGN,则忽略该信号;如果设置为 SIG_DFL,则恢复默认的信号处理行为。

返回值

  • 如果成功,返回之前为该信号设置的处理函数指针。
  • 如果失败,返回 SIG_ERR。

注意:

  1. ctrl+c产生的信号只能发给前台进程,在一个命令的后面加上&就可以将其放到后台运行了,这样shell就不必等待进程结束就可以接受到新的命令
  2. shell可以同时运行一个前台进程和任意多个后台进程,但是只有前台进程才能接到类似我们上面所说的信号
  3. 前台进程在运行过程中用户可以随时摁下ctrl+c产生一个信号,也就是说该进程的用户空间代码执行到任何地方都可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说就是异步的
  4. 信号是进程之间事件异步通知的一种方式,输入软中断

查看信号

使用kill -l命令可以查看linux中的信号列表
在这里插入图片描述
我们看上述图片,一共有62个信号,其中1~31是普通信号,34 ~ 64是实时信号,它们都有一个编号和一个宏定义名称:
在这里插入图片描述

信号处理常见方式

  1. 执行该信号的默认处理动作
  2. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行该处理函数,这种方式成为捕捉一个信号
  3. 忽略该信号

我们也可以在man手册中查看各个信号的默认处理动作
在这里插入图片描述

产生信号

在执行上述的死循环代码中,我们可以使用ctrl+c使得代码终止,其实不仅仅时ctrl+c,ctrl+\也可以使得代码终止
在这里插入图片描述

按住ctrl+c和ctrl+\都可以让进程终止,那他们有什么区别呢?

ctrl+c是给进程发送2号信号SIGINT,ctrl+\则是给进程发送3号信号SIGQUIT。
在这里插入图片描述
查看两信号可以发现的是它们的行为是不一样的,2号是Term,3号是Core。它们两者都代表终止进程,但是Core在终止进程的时候会进行一个动作,那就是核心转储

什么是核心转储呢?

是指当一个程序由于某些原因(如违反访问权限、除零错误、非法指令等)异常终止时,操作系统将程序终止时刻的内存映像写入到一个文件中。这个文件通常被称为核心转储文件或核心文件。

在云服务器中,核心转储是默认关闭的,使用ulimit -a命令可以查询当前资源限制的设定
在这里插入图片描述
我们通过ulimit -c size命令可以设置core文件的大小
在这里插入图片描述
在这里插入图片描述
core文件大小设置完毕后,就相当于将核心转储功能打开了,此时我们再使用ctrl+\对进程终止,就会出现上面图片的情况

核心转储有什么作用?

当我们程序运行过程中崩溃了,我们一般会通过调试进行逐步分析原因。但是在某些特殊情况下,我们会用到核心转储,也就是指操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及相关进程的状态和其他信息转而存储到一个磁盘中,这个磁盘文件也叫做核心转储文件。

通过系统函数向进程发信号

kill函数

#include <signal.h>
int kill(pid_t pid, int sig);

参数

  • pid:进程 ID。可以是任何进程的 ID,包括自己的进程 ID(getpid() 返回的值)。如果 pid 是负数,则信号会被发送到与 pid 的绝对值相同的进程组中的所有进程。
  • sig:信号。可以是任何有效的信号值,如 SIGKILL(强制终止进程)、SIGSTOP(暂停进程)、SIGCONT(继续暂停的进程)等。

返回值

  • 如果成功,返回 0。
  • 如果失败,返回 -1,并设置 errno 以指示错误原因。

代码样例:

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t pid = getpid(); // 获取当前进程的 PID
    int sig = SIGTERM;    // 指定要发送的信号,该信号用于请求一个进程终止运行

    int count = 10;
    while(count--)
    {
        printf("111\n");
        if(count == 5 && kill(pid,sig));
    }
    return 0;
}

在这里插入图片描述

raise函数

#include <signal.h>
int raise(int sig);

参数

  • sig:指定要发送的信号编号。这是一个整数值,表示特定的信号,如 SIGINT、SIGTERM 等。

返回值

  • 如果成功,返回 0。
  • 如果失败,返回 -1,并设置 errno 以指示错误原因。

代码样例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signum)
{
    printf("get signal: %d\n",signum);
}

int main()
{
    signal(2,handler);
    while(1)
    {
        sleep(1);
        raise(2);
    }

    return 0;
}

在这里插入图片描述
使用raise函数每隔一秒给自己发送一个2号信号

abort函数

#include <stdlib.h>
void abort(void);

功能

  • 终止程序:abort 函数会立即终止程序的执行。
  • 生成核心转储:默认情况下,abort 会生成一个核心转储文件,该文件包含了程序终止时的内存映像,可以用于调试。
  • 调用 atexit 函数:在程序终止前,abort 会调用所有通过 atexit 注册的函数。

返回值

  • abort 函数没有返回值,因为它会终止程序。

代码样例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void handler(int signum)
{
    printf("get signal:%d\n",signum);
}

int main()
{
    signal(6,handler); //发送6号信号使得进程异常终止
    while(1)
    {
        sleep(1);
        abort();
    }

    return 0;
}

在这里插入图片描述
我们发现虽然我们对6号信号进行了捕捉,并且也收到信号后执行我们的自定义方法,但是进程还是异常终止了。
说明:abort函数的作用是异常终止进程,exit函数的作用是正常终止进程,而abort函数本质是通过向当前进程发送SIGABRT信号(6号信号)而终止的,因此使用exit函数终止可能会失败,使用abort函数终止进程总是成功的。

由软件条件产生信号

SIGPIPE信号

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    //创建管道
    int fd[2] = {0};
    if(pipe(fd) < 0)
    {
        perror("pipe");
        return 1;
    }
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        close(fd[0]); //关闭读端
        const char* msg = "hihihihihi\n";
        int count = 100;
        while(count--)
        {
            write(fd[1],msg,sizeof(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(1);
    }
        //父进程,直接将读端写端都关闭,导致子进程直接被杀掉
        close(fd[1]);
        close(fd[0]);
        int statu = 0;
        waitpid(id,&statu,0);

        printf("子进程捕捉到的信号: %d\n",statu & 0x7F);
    return 0;
}

在这里插入图片描述
SIGPIPE信号实际上是一种由软件条件产生的信号,当进程在使用管道通信的时候,读端进程将读端关闭,而写端进程一直在写,那么这个时候写端进程就会收到此信号进而被系统终止。

SIGALRM信号
alarm 函数用于设置一个定时器,当定时器到期时,它会向进程发送 SIGALRM 信号。这个函数通常用于实现简单的超时机制。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

参数

  • seconds:指定定时器的超时时间,以秒为单位。

返回值

  • 如果成功,返回之前设置的剩余时间(以秒为单位)。
  • 如果失败,返回 0。

代码样例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main()
{
    int count = 0;
    alarm(1);
    while(1)
    {
        count++;
        printf("count : %d\n",count);
    }

    return 0;
}

在这里插入图片描述
测试自己云服务器一秒钟可以将一个变量累加到多大,其实实际比这个值要大很多,原因是计算机和外设进行IO导致速度降低。

由硬件异常产生信号

在我们访问一个变量的时候,一定会先经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作。其中页表属于一种软件映射的关系,实际上在从虚拟地址到物理地址的映射的时候还有一种硬件叫做MMU,它是一种负责处理CPU的内存访问请求的计算机硬件,因此映射工作不是由CPU做的,而是由MMU做的,但是限制MMU已经集成到CPU当中了。当需要进行虚拟地址到物理地址的映射时,我们先将页表的虚拟地址导给MMU然后MMU会计算出对应的物理地址,然后我们再通过这个物理地址进行相应的访问。而MMU既然是硬件单元,那它就会有状态信息,当我们要访问不属于我们的虚拟地址的时候,MMU在进行虚拟地址到物理地址的转换的时候就会报,然后对应的错误写入自己的状态信息当中,这时硬件上的信息也会被操作系统识别,进而将对应的进程发送SIGSEGV信号。
在这里插入图片描述

信号保存

在这里插入图片描述
信号的相关常见概念

信号递达:实际执行信号处理程序的动作
信号未决:信号从产生到被递达前的中间状态
信号阻塞:进程可主动屏蔽特定信号
阻塞效果:被阻塞的信号将保持未决状态,直至解除阻塞才会递达
阻塞与忽略的区别

  • 阻塞:阻止信号递达(根本不会处理)
  • 忽略:信号递达后选择不采取行动(仍会经过递达过程)

在内核中的表示

在这里插入图片描述
在上述图中:

  1. block位图,比特位的位置代表某个信号,比特位的内容代表该信号是否被阻塞
  2. pending位图中,比特位的位置代表某个信号,比特位的内容代表是否收到信号
  3. handler表本质是一个函数指针数组,数组的下标表示某个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。
  4. 以上三张表的每个位置都是一一对应的。

每个信号包含两个标志位:阻塞(block)和未决(pending),以及一个指向处理函数指针。当信号产生时,内核会在进程控制块中设置该信号的未决标志,该标志会一直保持直到信号被递达。
上图分析:

SIGHUP信号:当前既未被阻塞也未产生过,递达时将执行默认处理动作
SIGINT信号:已经产生但被阻塞,暂时无法递达。虽然其处理动作为忽略,但在解除阻塞前仍不能忽略该信号,因为进程可能在解除阻塞前修改处理动作
SIGQUIT信号:尚未产生,一旦产生将被阻塞,其处理动作用户自定义函数sighandler

信号集操作函数

sigset_t
sigset_t 是一个用于表示信号集的数据类型。 sigset_t 用于存储一组信号,可以方便地对信号进行添加、删除和检查等操作。

sigset_t 是一个数据结构,用于表示信号集。 它通常是一个位掩码,每个位对应一个信号。例如,SIGINT(通常是信号 2)对应的位在 sigset_t 中被设置为 1,表示该信号在信号集中。

sigset_t称为信号集,这个类型可以表示每个信号的“有效”或者“无效”状态

  • 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞
  • 在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”理解为阻塞。

信号集操作函数
sigset_t类型通过位掩码形式表示信号的有效或无效状态,其内部存储方式由系统实现决定。使用者无需关心其底层实现,只能通过特定的函数来操作sigset_t变量。直接解释其内部数据(如使用printf打印)是无效的操作。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • sigemptyset函数:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
  • sigfillset函数:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号
  • sigaddset函数:在set所指向的信号集中添加某种有效信号
  • sigdelset函数:在set所指向的信号集中删除某种有效信号
  • sigismember函数:判断在set所指向的信号集中是否包含某种信号,若包含则返回1,不包含则返回0,调用失败返回-1.

代码样例:

#include <stdio.h>
#include <signal.h>

int main()
{
    sigset_t st;

    //清零所有信号对应的bit位
    sigemptyset(&st);
    //所有信号对应的bit置位
    sigfillset(&st);
    //假设添加SIGINT信号
    sigaddset(&st,SIGINT);
    //删除该信号
    sigdelset(&st,SIGINT);
    //判断st所指向的信号集中是否包含SIGINT信号
    sigismember(&st,SIGINT);

    return 0;
}

sigprocmask函数

该函数可以用于读取和更改进程的信号屏蔽字。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数

  • how:指定如何修改信号掩码,可以取以下值:
    SIG_BLOCK:将 set 中的信号添加到当前信号掩码中。
    SIG_UNBLOCK:从当前信号掩码中删除 set 中的信号。
    SIG_SETMASK:将当前信号掩码设置为 set。
  • set:指向一个 sigset_t 类型的指针,表示要修改的信号集。
    如果 set 为 NULL,则不会修改当前信号掩码。
    如果 set 不为 NULL,则根据 how 参数修改当前信号掩码。
  • oldset:指向一个 sigset_t 类型的指针,用于保存修改前的信号掩码。
    如果 oldset 为 NULL,则不会保存旧的信号掩码。
    如果 oldset 不为 NULL,则将修改前的信号掩码保存到 oldset 中。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误原因。

代码样例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
}

int main() {
    // 初始化信号集
    sigset_t set, oldset;
    sigemptyset(&set);

    // 添加 SIGINT 到信号集
    sigaddset(&set, SIGINT);

    // 阻塞 SIGINT
    if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
        perror("sigprocmask");
        return 1;
    }

    // 设置信号处理函数
    signal(SIGINT, signal_handler);

    // 等待 10 秒
    printf("SIGINT is blocked for 10 seconds\n");
    sleep(10);

    // 解除阻塞 SIGINT
    if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    printf("SIGINT is unblocked\n");

    // 等待用户输入
    printf("Press Ctrl+C to trigger SIGINT\n");
    pause();

    return 0;
}

运行结果:
在这里插入图片描述

sigpending函数

sigpending 是一个用于检查当前进程中哪些信号正在等待处理的系统调用。它将当前进程中所有未决的信号存储到一个 sigset_t 类型的信号集中。这些信号可能已经被阻塞,但尚未被处理。

#include <signal.h>
int sigpending(sigset_t *set);

参数

  • set:指向一个 sigset_t 类型的指针,用于存储当前进程中所有未决的信号。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误原因。

代码样例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
}

int main() {
    // 初始化信号集
    sigset_t set, pending_set;
    sigemptyset(&set);

    // 添加 SIGINT 到信号集
    sigaddset(&set, SIGINT);

    // 阻塞 SIGINT
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    // 设置信号处理函数
    signal(SIGINT, signal_handler);

    // 等待 10 秒
    printf("SIGINT is blocked for 10 seconds\n");
    sleep(10);

    // 检查未决的信号
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        return 1;
    }

    // 检查 SIGINT 是否在未决信号集中
    if (sigismember(&pending_set, SIGINT)) {
        printf("SIGINT is pending\n");
    } else {
        printf("SIGINT is not pending\n");
    }

    // 解除阻塞 SIGINT
    if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    printf("SIGINT is unblocked\n");

    // 等待用户输入
    printf("Press Ctrl+C to trigger SIGINT\n");
    pause();

    return 0;
}

在这里插入图片描述

捕捉信号

在这里插入图片描述
内核空间和用户空间
每个进程都有自己的进程地址空间,该进程地址空间是由内核空间和用户空间组成:

  • 用户所写的代码和数据位于用户空间,通过用户页表与物理内存之间建立映射关系
  • 内核空间存储的实际上是操作系统代码和数据,通过内核级页表与物理内存之间建立映射关系

用户态和内核态

用户态和内核态:

  • 内核态通常用来执行操作系统的代码,是一种权限很高的状态
  • 用户态则是一种用来执行普通用户代码的状态,是一种受监管的普通状态

进程收到信号后不会立刻处理信号,而是在合适的时候处理,这个时刻也就指的是从内核态切换到用户态的时候。

从用户态切换到内核态(陷入内核)的几种情况:

  1. 需要进行系统调用
  2. 当前进程的时间片到了,导致进程切换
  3. 产生异常、终端、陷阱等

从内核态切换到用户态的几种情况:

  1. 系统调用返回时
  2. 进程切换完毕时
  3. 异常、中断、陷阱等处理完毕时

内核如何实现信号的捕捉

在这里插入图片描述
下图更方便记忆:
在这里插入图片描述

sigaction函数

sigaction 是一个功能强大的系统调用,用于检查或修改信号的处理方式。它比传统的 signal 函数更加灵活和可靠,支持对信号处理的更多控制,包括信号掩码的设置和信号处理的上下文信息。

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数

  • signum:指定要检查或修改的信号编号。

  • act:指向一个 struct sigaction 类型的指针,用于指定新的信号处理方式。
    如果 act 为 NULL,则不会修改信号的处理方式。
    如果 act 不为 NULL,则根据 act 中的设置修改信号的处理方式。

  • oldact:指向一个 struct sigaction 类型的指针,用于保存修改前的信号处理方式。
    如果 oldact 为 NULL,则不会保存旧的信号处理方式。
    如果 oldact 不为 NULL,则将修改前的信号处理方式保存到 oldact 中。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误原因。

struct sigaction 结构体
struct sigaction 是一个结构体,用于描述信号的处理方式。

struct sigaction {
    void     (*sa_handler)(int);       // 指向信号处理函数的指针
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 另一种信号处理函数
    sigset_t sa_mask;                  // 在信号处理函数执行期间需要屏蔽的信号集
    int      sa_flags;                 // 信号处理的标志
    void     (*sa_restorer)(void);     // 已废弃,不应使用
};
  • sa_handler:指向信号处理函数的指针。当信号发生时,调用此函数。
    可以设置为 SIG_DFL(默认处理)或 SIG_IGN(忽略信号)。

  • sa_sigaction:另一种信号处理函数,可以接收额外的参数,如信号的详细信息。
    如果使用 sa_sigaction,需要在 sa_flags 中设置 SA_SIGINFO 标志。

  • sa_mask:在信号处理函数执行期间需要屏蔽的信号集。
    这些信号在信号处理函数执行期间不会被处理。

  • sa_flags:信号处理的标志,可以设置以下值:
    SA_NOCLDSTOP:如果信号是 SIGCHLD,子进程停止时不会发送信号。
    SA_NOCLDWAIT:如果信号是 SIGCHLD,子进程终止时不会变成僵尸进程。
    SA_NODEFER:在信号处理函数执行期间,不会阻塞当前信号。
    SA_ONSTACK:信号处理函数在备用栈上执行。
    SA_RESETHAND:信号处理函数执行一次后,恢复为默认行为。
    SA_RESTART:信号处理函数执行后,系统调用会自动重启。
    SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler,并传递额外的参数。

代码样例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum, siginfo_t *info, void *context) {
    printf("Received signal %d\n", signum);
    printf("Signal code: %d\n", info->si_code);
    printf("Sender UID: %d\n", info->si_uid);
}

int main() {
    // 初始化信号处理结构体
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = signal_handler;

    // 设置 SIGINT 的处理函数
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // 等待用户输入
    printf("Press Ctrl+C to trigger SIGINT\n");
    pause();

    return 0;
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐