Linux -- 信号【上】

目录

一、信号的引入

1、信号概念

2、signal函数

普通标准信号详解表

3、前台/后台进程

[3.1 概念](#3.1 概念)

[3.2 查看后台进程](#3.2 查看后台进程)

[3.3 后台进程拉回前台](#3.3 后台进程拉回前台)

[3.4 终止后台进程](#3.4 终止后台进程)

[3.5 暂停前台进程](#3.5 暂停前台进程)

[3.6 回复运行后台进程](#3.6 回复运行后台进程)

4、发信号的本质

二、信号的产生

1、终端按键

2、系统调用

[2.1 kill](#2.1 kill)

[2.2 raise](#2.2 raise)

[2.3 abort](#2.3 abort)

总结

3、硬件异常

[3.1 除0错误](#3.1 除0错误)

[3.2 野指针错误](#3.2 野指针错误)

底层原理

4、软件条件

[4.1 SIGPIPE](#4.1 SIGPIPE)

[4.2 SIGALRM](#4.2 SIGALRM)

[4.3 SIGCHLD](#4.3 SIGCHLD)

总结

理解系统闹钟


一、信号的引入

1、信号概念

在Linux系统中,信号(Signal)是一种软件中断机制,用于通知进程发生了特定的事件。信号可以由系统内核、其他进程或者进程自身发送。

我们可以通过指令kill -l查看所有信号:

信号的本质就是一个define定义的宏,其中1 - 31号信号是普通信号 ,34 - 64号信号是实时信号 ,普通信号和实时信号各自都有31个。每一个信号与一个数字相对应,每个信号也都有特定的含义和默认的处理动作。例如,信号SIGINT(通常由用户按下ctrl + c产生)表示中断信号,默认情况下会导致进程终止。

注意:在Linux中,前台进程只能有一个,而后台进程可以为多个。一般而言,我们的bash进程作为我们的前台进程,而一旦我们执行一个可执行程序,这个可执行程序就会成为前台进程,而bash进程就会转为后台进程。但是我们如果在执行一个可执行程序时,在之后加一个&,此时的可执行程序就会由前台进程转换为后台进程。而前台进程与后台进程本质间区别就是前台进程可以从键盘获取数据,后台进程则不能。

比如我们运行一个后台进程,就无法通过ctrl + c终止进程,因为其无法从键盘读取数据。此时就只能通过kill指令直接杀死对应的进程。

2、signal函数

1 - 31号信号是普通信号,可以不用立即处理。普通信号的特点是:不支持排队。如果同一个信号在进程处理它之前多次产生,它只会被记录一次,这可能会导致信号丢失。34 - 64号信号是实时信号,收到就要立即处理!

当一个实时信号被发送给一个进程时,进程可以采取以下几种方式来处理信号:

  1. 忽略信号 :进程可以选择忽略某些信号,即不对信号做出任何反应。但并不是所有信号都可以被忽略,例如 SIGKILL 和 SIGSTOP 信号不能被忽略。
  2. 捕获信号:进程可以注册一个信号处理函数,当接收到特定信号时,就会执行这个函数。通过这种方式,进程可以在接收到信号时执行自定义的处理逻辑。
  3. 执行默认动作:如果进程没有显式地忽略或捕获信号,那么它将执行信号的默认动作。默认动作通常是终止进程、停止进程、继续进程等。

我们可以通过指令 man 7 signal 查看信号的默认处理动作:

普通标准信号详解表

| 编号 | 信号名称 | 默认行为 | 触发场景说明 | 可否捕获或忽略 | 重要说明 |
| 1 | SIGHUP | Term | 挂起。终端连接断开(如网络断开、关闭终端窗口)、控制进程终止。 | Yes | 常被用于通知守护进程重新读取配置文件 (如 nginx -s reload)。 |
| 2 | SIGINT | Term | 中断 。来自键盘的中断,通常是用户按下 Ctrl+C。 | Yes | 请求优雅地终止前台进程。 |
| 3 | SIGQUIT | Core | 退出 。来自键盘的退出,通常是用户按下 Ctrl+\。 | Yes | 不仅终止进程,还会生成 core dump 文件用于调试。表示用户希望进程终止并留下调试信息。 |
| 4 | SIGILL | Core | 非法指令。进程尝试执行一条非法、错误或特权的指令。 | Yes | 通常由程序 bug 引起,例如执行了损坏的二进制文件、栈溢出等。 |
| 5 | SIGTRAP | Core | 跟踪/断点陷阱。由调试器使用,用于在断点处中断进程的执行。 | Yes | 这是调试器(如 gdb)实现断点功能的机制。 |
| 6 | SIGABRT | Core | 中止 。通常由 abort() 函数调用产生。 | Yes | 进程自己调用 abort() 来终止自己,通常表示检测到了严重的内部错误(如 assert 断言失败)。也会生成 core dump。 |
| 7 | SIGBUS | Core | 总线错误。无效的内存访问,即访问的内存地址不存在或违反了内存对齐要求。 | Yes | 硬件级别的错误。例如,在支持对齐要求的架构上访问未对齐的地址。与 SIGSEGV 类似但原因更底层。 |
| 8 | SIGFPE | Core | 浮点异常。错误的算术运算,如除以零、溢出等。 | Yes | 不仅是浮点数,整数除以零也会触发此信号。 |
| 9 | SIGKILL | Term | 杀死。无条件立即终止进程。 | No | 无法被捕获、阻塞或忽略 。是终止进程的最终极、最强制的手段。kill -9 的由来。 |
| 10 | SIGUSR1 | Term | 用户自定义信号 1。 | Yes | 没有预定义的含义,完全留给用户程序自定义其行为。常用于应用程序内部通信(如通知进程切换日志文件、重新加载特定数据等)。 |
| 11 | SIGSEGV | Core | 段错误。无效的内存引用,即访问了未分配或没有权限访问的内存(如向只读内存写入)。 | Yes | C/C++ 程序中最常见的崩溃原因之一(解空指针、访问已释放内存、栈溢出、缓冲区溢出等)。 |
| 12 | SIGUSR2 | Term | 用户自定义信号 2。 | Yes | 同上,另一个用户可自定义用途的信号。 |
| 13 | SIGPIPE | Term | 管道破裂。向一个没有读者的管道(或 socket)进行写入操作。 | Yes | 常见场景:一个管道中,读端进程已关闭或终止,写端进程还在写入。如果忽略此信号,write 操作会返回错误并设置 errnoEPIPE。 |
| 14 | SIGALRM | Term | 定时器信号 。由 alarm()setitimer() 设置的定时器超时后产生。 | Yes | 常用于实现超时机制或周期性任务。 |
| 15 | SIGTERM | Term | 终止 。这是一个友好的终止进程的请求。 | Yes | kill 命令的默认信号。进程收到此信号后,应该执行清理工作(关闭文件、释放资源等)然后退出。是优雅关闭服务的首选方式。 |
| 16 | SIGSTKFLT | Term | 协处理器栈错误。极少使用。 | Yes | 与早期的数学协处理器有关,现代 Linux 系统上基本不会见到。 |
| 17 | SIGCHLD | Ign | 子进程状态改变 。一个子进程停止终止时,内核会向父进程发送此信号。 | Yes | 非常重要!父进程可以通过捕获此信号来调用 wait()waitpid() 回收子进程资源,防止出现僵尸进程。默认行为是 Ignore,但最好显式处理。 |
| 18 | SIGCONT | Cont | 继续 。让一个停止的进程继续运行。 | Yes | 无法被停止的进程忽略。常用于作业控制(fg / bg 命令的底层实现)。 |
| 19 | SIGSTOP | Stop | 停止。暂停进程的执行(进入 Stopped 状态)。 | No | 无法被捕获、阻塞或忽略 。是 Ctrl+Z (SIGTSTP) 的强制版本。 |
| 20 | SIGTSTP | Stop | 终端停止 。来自终端的停止信号,通常是用户按下 Ctrl+Z。 | Yes | 请求优雅地暂停前台进程。进程可以被捕获,在捕获函数中它可以做一些准备工作后再决定是否暂停自己。 |
| 21 | SIGTTIN | Stop | 后台进程读终端。一个后台进程尝试从控制终端读取输入。 | Yes | 为了防止后台进程干扰前台,内核会自动停止该后台进程。 |
| 22 | SIGTTOU | Stop | 后台进程写终端。一个后台进程尝试向控制终端写入输出。 | Yes | 类似于 SIGTTIN,但用于写入操作。是否停止取决于终端配置(stty tostop)。 |

... ... ... ... ... ...

其中,Term是终止进程,Ign是忽略信号,Stop是暂停进程,Cont是继续进程,Core也是终止进程并生成 core dump 文件,但是和Term有区别。

SIGKILL (9) 和 SIGSTOP (19) 是两个特殊的信号,无法被捕获、阻塞或忽略。这是为了给系统管理员一个最终能控制任何进程的手段。

接下来我们介绍一个函数signal,其可以设置进程对某个信号的自定义捕捉 方法:即当进程收到 signum 信号的时候,去执行 handler 方法。

  1. 函数原型:
  • typedef void (*sighandler_t)(int);
  • sighandler_t signal(int signum, sighandler_t handler);

2. 参数:

  • signum:是一个整数,表示要处理的信号编号。
  • handler:是一个函数指针,指向一个信号处理函数。这个信号处理函数接受一个整数参数(即接收到的信号编号),并且没有返回值(void)。可以是以下几种值:
    • SIG_DFL:表示默认的信号处理动作。
    • SIG_IGN:表示忽略该信号。
    • 自定义的信号处理函数指针,用于处理特定信号。

我们知道 ctrl + c 的本质是向前台进程发送 SIGINT(即 2 号信号)。为了验证这一点,我们需要使用系统调用signal函数来进行演示。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
    signal(SIGINT, handlerSig); // 收到2号信号时,调用handlerSig函数,执行函数里的动作,将SIGINT作为参数传过去
    int cnt = 0;
    while(true)
    {
        std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

其中前台进程在运行过程中,用户随时可能按下 ctrl + c 而产生一个信号,也就是说该进程的代码执行到任何地方都可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

我们可以看到,按下 ctrl + c 后打印消息而非终止进程。所以,ctrl + c 触发了 SIGINT 信号,此时会执行我们写的自定义逻辑。这个时候我们 ctrl + c 终止不了这个进程,但是我们还可以使用 ctrl + \ 来结束这个进程。因为 ctrl + \ 会发送另一个不同的信号:SIGQUIT (信号编号3)。

3、前台/后台进程

3.1 概念

3.2 查看后台进程

使用 Shell 内置命令 jobs,可以查看所有的后台任务。

bash 复制代码
$ ./testsig &
[1] 263501 # [job_number] pid
$ jobs
[1]+  Running                 ./testsig &

[1]:任务编号(Job Number),由 Shell 分配管理,在当前 Shell 会话内有效

263501:进程PID,由操作系统分配,在整个系统内有效。

3.3 后台进程拉回前台

使用命令 fg %<job_number>,这样然后就能再用 Ctrl+C 来终止它了。

bash 复制代码
$ fg %1 # 将作业编号为1的后台作业变为前台作业
# 此时终端再次被 testsig 霸占,可以用 Ctrl+C 终止

3.4 终止后台进程

# 既然 Ctrl+C无效,就必须使用 kill 命令通过发送信号来终止。

方法一:通过作业编号(推荐,在当前终端内操作最方便)

bash 复制代码
$ kill %1 # 向作业1发送默认的 TERM 信号,请求终止
$ jobs
[1]+  Terminated              ./testsig # 确认它已终止

方法二:通过进程PID

bash 复制代码
$ kill 263501    # 发送 SIGTERM (15),友好地请求终止
$ kill -9 263501 # 发送 SIGKILL (9),强制杀死,无法被捕获或忽略

3.5 暂停前台进程

我们可以使用 ctrl + z 来暂停前台进程,但是由于前台进程要一直运行着,所以暂停的进程自动变为后台进程。

  • Ctrl+Z 的效果 :向进程发送 SIGTSTP (Terminal Stop) 信号

  • 进程状态变化 :从 Running 变为 Stopped (停止)

3.6 回复运行后台进程

我们可以使用 bg 任务号 来回复运行后台进程。

bg 命令的效果 :向进程发送 SIGCONT (Continue) 信号,但不将其带回前台

关键点 :虽然 bg 让进程继续执行,但 Shell 和内核对其处理方式与直接用 & 启动的进程有细微差别。

# 总结:

4、发信号的本质


二、信号的产生

至此,我们对信号有了一个基本认识,那么接下来我们就先从信号的产生介绍。

在我们操作系统中,信号的产生方式有许多,总体归纳来说有四种。

1、终端按键

其中我们通过键盘快捷键直接向我们的进程发出信号的方式非常常见,其中较为我们常用的有:

组合键 功能
Ctrl+C 向进程发出SIGINT信号,终止进程。
Ctrl+\ 向进程发出SIGQUIT信号,终止进程。
Ctrl+Z 向进程发送SIGTSTP信号,暂停进程的执行。

2、系统调用

我们也可以通过操作系统为我们提供的接口对进程发送对应的信号。

2.1 kill

  1. 头文件:#include <sys/types.h> #include <signal.h>
  2. 函数原型:int kill(pid_t pid, int sig);
  3. 参数:pid对应要发送信号进程的pid,sig表示发送的信号种类。
  4. 返回值:如果成功,返回值为 0。否则,返回值为 -1

这里我们使用kill系统调用,来实现一个我们自己的kill命令:

cpp 复制代码
// kill.cc

#include<iostream>
#include<sys/types.h>
#include<signal.h>

// ./mykill signumber pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cout << "./mykill signumber pid" << std::endl;
        return 1;
    }
    int signumber = std::stoi(argv[1]); // 字符串转成整数
    pid_t target = std::stoi(argv[2]);

    int n = kill(target, signumber);
    if(n == 0)
    {
        std::cout << "send " << signumber << " to " << target << " success.";
    }

    return 0;
}

所以这里我们使用kill系统调用,就可以给另一个进程传递信号了,这里在命令参数传入信号编号和目标进程,我们传入2号信号,进程就收到2号信号。

万一我们的进程是恶意的病毒呢?不就无法杀掉了吗?我们操作系统的设计者也考虑到了这点,所以我们kill发送9号信号时,可以杀掉进程,因为9号信号进禁止自定义捕捉,防止病毒程序屏蔽信号。

SIGKILL (9) 和 SIGSTOP (19) 是两个特殊的信号,无法被捕获、阻塞或忽略。这是为了给系统管理员一个最终能控制任何进程的手段。

2.2 raise

raise的目标**只有一个,就是调用者进程自身,**自己给自己的进程发信号。

  1. 头文件:#include <signal.h>
  2. 函数原型:int raise(int sig);
  3. 返回值:如果成功,返回值为 0。否则,返回值为非0

这里我们把普通信号都捕获了,方便我们使用raise系统调用给当前进程发送信号时,都能捕获到然后打印出来我们查看。

cpp 复制代码
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
    // signal(SIGINT, handlerSig); // 收到2号信号时,调用handlerSig函数,执行函数里的动作,将SIGINT作为参数传过去

    for (int i = 1; i < 32; i++)
        signal(i, handlerSig); // 将1 - 32所有信号都自定义捕捉 

    for (int i = 1; i < 32; i++) // 每隔一秒自己给自己发一个信号
    {
        sleep(1);
        if(i == 9 || i == 19) // 跳过两个无法被捕捉的信号
            continue;
        raise(i);
    }

    int cnt = 0;
    while(true)
    {
        std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}
bash 复制代码
hzy@tata:/home/tata/lesson14$  ./testsig
获得了一个信号: 1
获得了一个信号: 2
获得了一个信号: 3
获得了一个信号: 4
获得了一个信号: 5
获得了一个信号: 6
获得了一个信号: 7
获得了一个信号: 8
Killed

在前文中我们有说过,有两个信号:9号和19号信号不能被捕获,所以当我们进程在准备捕获9号信号时,由于9号信号不能被捕获,所以当前进程被终止。

2.3 abort

abort函数的功能非常明确和强硬:立即异常终止当前进程 ,并生成一个 core dump 文件(如果系统配置允许)。

  1. 头文件:#include <stdlib.h>
  2. 函数原型:void abort(void);

注意:该函数无参数,且无返回值,因为它永远不会返回到调用者。

raise函数用于给当前进程发送sig号信号,而abort函数相当于给当前进程发送SIGABRT信号(6号),使当前进程异常终止。

cpp 复制代码
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
    for (int i = 1; i < 32; i++)
        signal(i, handlerSig); // 将1 - 32所有信号都自定义捕捉 

    int cnt = 0;
    while(true)
    {
        std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;
        abort();
        sleep(1);
    }

    return 0;
}

# abort与exit函数同样是终止进程,它们之间有什么区别吗?

首先明确abort函数和exit函数的不同作用。abort函数的作用是异常终止进程,它本质上是通过向当前进程发送SIGABRT信号来实现这一目的。而exit函数的作用是正常终止进程。

需要注意的是,使用exit函数终止进程可能会失败,因为在某些复杂的程序运行环境中,可能存在一些因素干扰正常的进程终止流程。然而,使用abort函数终止进程通常被认为总是成功的,这是由于其通过发送特定信号强制终止进程,一般情况下进程很难忽略该信号而继续运行。

总结

3、硬件异常

当程序出现除 0、野指针、越界等错误时,程序会崩溃,本质是进程在运行中收到操作系统发来的信号而被终止。 这些发送的信号都是由硬件异常产生的。

比如下面这段代码,进行了对一个数的除0和空指针的解引用,那么其到底是如何被操作系统识别的呢?

cpp 复制代码
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
    exit(13);
}

int main()
{
    int cnt = 0;
    while(true)
    {
        std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;

        int a = 10;
        // a /= 0; // 除0错误

        int *p = nullptr;
        *p = 100; // 野指针

        sleep(1);
    }

    return 0;
}

发现他们分别收到了8号SIGFPE和11号SIGSEGV信号,8号信号就是浮点数异常,11号信号就是段错误,所以我们的程序会崩溃是因为进程收到了信号,此时进程就会执行默认处理动作,进而终止进程。

所以异常也是会产生信号的。那么这个信号是谁发的?我们说过发信号本质就是修改PCB里面的位图,只有OS才能修改,所以是OS发的。那么问题是操作系统如何知道进程出错?

首先我们知道,当我们要访问一个变量时,进程控制块task_struct一定要会经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作。

3.1 除0错误

程序都是运行在硬件CPU之上的,CPU有各种寄存器,如EIP、EBP,还有一个状态寄存器,它里面有运算的标记位,如是否溢出、是否为0等,所以CPU硬件出错后,操作系统作为软硬件的管理者就会第一时间知道,然后CPU寄存器里保存有进程上下文,所以操作系统就知道是哪一个进程出错,然后发现是计算溢出,就给进程发送8号信号进程就终止了。

3.2 野指针错误

野指针拿到一个虚拟地址,同时CPU的CR3寄存器记录了页表的虚拟地址,同时CPU里面集成了一个MMU硬件单元,此时MMU拿着页表地址和虚拟地址就可以做虚拟地址到物理地址的转换了。所以MMU有没有可能转换失败?并且发现指针想要去0号地址写入,所以MMU硬件报错,操作系统立马知道,根据CPU的进程上下文向对应的进程发送11号段错误信号,进程就直接终止了。

底层原理

我们都知道,当我们要访问一个变量时,进程控制块task_struct一定要会经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作。

而页表属于一种软件映射关系,在从虚拟地址到物理地址映射过程中,有一个硬件单元叫做 MMU(内存管理单元),它是负责处理 CPU 的内存访问请求的计算机硬件。如今,MMU 已集成到 CPU 当中。虽然映射工作原本不是由 CPU 做而是由 MMU做,但现在其与 CPU 的紧密结合使得整个内存访问过程更加高效。

当进行虚拟地址到物理地址的映射时,先将页表左侧的虚拟地址提供给 MMU,MMU会计算出对应的物理地址,随后通过这个物理地址进行相应的访问。

由于 MMU 是硬件单元,所以它有相应的状态信息。当要访问不属于我们的虚拟地址时,MMU 在进行虚拟地址到物理地址的转换时会出现错误,并将对应的错误写入到自己的状态信息当中。此时,硬件异常,硬件上的信息会立马被操作系统识别到,进而向对应进程发送 SIGSEGV信号。

现代 CPU 并不是傻傻地执行指令,它的内部有一套复杂的监控电路。当这些电路在执行指令的过程中检测到某些特定条件时,会立即中断当前控制流 ,并强制 CPU 去执行一段预设好的、属于操作系统的代码。这个过程是硬件自动完成的。我们可以用以下流程图来概括这个硬协同的过程:

下面,我们以最常见的 SIGSEGV (段错误)SIGFPE (除零错误) 为例,拆解图中的每一步。

4、软件条件

4.1 SIGPIPE

软件条件也可以产生信号,这类信号的特点是:它们并非由外部进程或用户通过 kill 发送,也非由硬件错误触发,而是由操作系统内核在检测到某种特定的、预先定义的"软件条件"满足时,自动向进程发送的。

在我们前面学习管道通信时,就知道如果进程将读端关闭,而写端进程还一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止。SIGPIPE就是一种典型的因为软件异常而产生的信号。

例如,下面代码,创建匿名管道进行父子进程之间的通信,其中父进程去读取数据,子进程去写入数据,但是一开始将父进程的读端关闭了,那么此时子进程在向管道写入数据时就会收到SIGPIPE信号,进而被终止。

cpp 复制代码
#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 )
    {
        //child -> write
        close(fd[0]);
        char*msg = "hello father, i am child...";
        while(1)
        {
            write(fd[1],msg,strlen(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }
    // father -> read
    close(fd[1]);
    close(fd[0]);
    int status = 0;
    waitpid(id,&status,0);
    printf("child get a signal :%d\n",status&0x7f);
    return 0;
}

4.2 SIGALRM

我们能够通过alarm函数,设定一个闹钟,倒计时完毕向我们的进程发送SLGALRM信号,其具体用法如下:

  1. 头文件:#include<stdio.h>
  2. 函数原型:unsigned int alarm(unsigned int seconds);
  3. 参数:seconds表示倒计时的秒数。如果 seconds 为 0,则表示取消之前设置的所有尚未触发的 alarm 定时器
  4. 返回值:如果调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。如果调用alarm函数前,进程没有设置闹钟,则返回值为0。

例如下面这段代码,我们首先对SLGALRM信号进行捕捉,并给出我们的自定义方法,然后1秒后调用alarm函数。

cpp 复制代码
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>


void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
    exit(13);
}

int main()
{

    signal(SIGALRM, handlerSig);

    alarm(1); // 设定1s闹钟,1s后,当前进程会收到信号SIGALRM

    while (true)
    {
        int cnt = 0;
        std::cout << "count: " << cnt++ << std::endl; 
    }

    return 0;
}

此时cnt的值为才18万多?问题:我们的计算机不是运算次数都是上亿次的吗?

因为我们这里一直在cout打印,cout本质是向显示器文件写入,所以本质是 IO,并且我们用的是云服务器,通过网络把云服务器上跑的代码结果返回给显示器,所以他的效率就比较低。

所以下面直接定义全局的 cnt 循环,不要 IO,直接cnt++,然后收到信号后先打印cnt,再终止进程。

cpp 复制代码
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

int cnt = 0;

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << "count: " << cnt << std::endl;
    exit(13);
}

int main()
{
    signal(SIGALRM, handlerSig);

    alarm(1); // 设定1s闹钟,1s后,当前进程会收到信号SIGALRM

    while (true)
    {
        // cout本质是向显示器文件写入,所以是IO
        // vscode -> ./testSig -> 云服务器 -> 网络 -> 显示器
        // std::cout << "count: " << cnt++ << std::endl; // 打印效率不高!

        cnt++;
    }

    return 0;
}

然后我们就发现 cnt 就是5亿多了,所以 IO 和纯计算相差好几个数量级,因为我们CPU进行 IO 时需要访问外设,外设的速度就是比较慢的。

现在我们想设定一个闹钟,然后进程收到信号后不退出循环,一直打印,所以我们就可以看到一秒后闹钟发送信号打印语句,然后再也收不到信号了,说明我们的闹钟是一次性的。

cpp 复制代码
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

void handlerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
    signal(SIGALRM, handlerSig);
    alarm(1);

    while (true)
    {
        std::cout << "." << std::endl;
        sleep(1);
    }

    return 0;
}

问题:今天我们就是想让闹钟每隔疫苗发送一次信号该怎么办呢?此时可以在自定义捕捉信号方法里面再设置闹钟,然后发送一次信号后就会重新设置闹钟。所以这里我们每个一秒就接收到了一个信号,并且还是同一个进程在接收,因为pid一直都是一样的。

我们可以让进程一直pause暂停,然后每隔一秒发送信号,由信号驱动进程执行我们注册的任务。

cpp 复制代码
#include<iostream>
#include<vector>
#include<functional>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

////////// func //////////

void Sched()
{
    std::cout << "我是进程调度" << std::endl;
}

void MemManger()
{
    std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}

void Fflush()
{
    std::cout << "我是刷新程序,我在定期刷新内存数据到磁盘" << std::endl;
}

//////////////////////////

using func_t = std::function<void()>;

std::vector<func_t> funcs;

// 每隔一秒,完成一些任务
void handlerSig(int sig)
{
    std::cout << "##############################" << std::endl;
    for(auto f : funcs)
        f();
    std::cout << "##############################" << std::endl;
    alarm(1);
}

int main()
{
    funcs.push_back(Sched);
    funcs.push_back(MemManger);
    funcs.push_back(Fflush);

    signal(SIGALRM, handlerSig);
    alarm(1);

    while (true)
    {
        // 让进程什么都不做,就让进程暂停,一旦来一个信号,就唤醒一次执行方法
        pause();
    }

    return 0;
}

而这就是操作系统的原理,操作系统也是一个死循环,在别人发送的信号的驱动下运行,它把闹钟时间设置地很小,此时操作系统就会非常高频地执行任务。所以今天我们可以把进程的PCB链入一个链表中,然后调度时让操作系统根据信号驱动遍历链表,找到时间片消耗最少的进程来调度。

4.3 SIGCHLD

总结

理解系统闹钟

相关推荐
hashiqimiya2 小时前
centos配置环境变量jdk
linux·运维·centos
hashiqimiya2 小时前
权限更改centos中系统文件无法创建文件夹,使用命令让普通用户具备操作文件夹
linux
路由侠内网穿透3 小时前
本地部署 GPS 跟踪系统 Traccar 并实现外部访问
运维·服务器·网络·windows·tcp/ip
傻傻虎虎5 小时前
【Docker】常用帮忙、镜像、容器、其他命令合集(2)
运维·docker·容器
ZERO_pan6 小时前
服务器装机遇到的问题
运维·服务器
逆小舟6 小时前
【Linux】人事档案——用户及组管理
linux·c++
青草地溪水旁6 小时前
pthread_mutex_lock函数深度解析
linux·多线程·pthread
l1t6 小时前
利用DeepSeek实现服务器客户端模式的DuckDB原型
服务器·c语言·数据库·人工智能·postgresql·协议·duckdb
杀气丶6 小时前
Linux下运行芙蕾雅天堂2【俄文简译L2FATER】
运维·服务器·天堂2·l2fater·l2fater.cn