Linux -- 信号【上】

目录

一、信号的引入

1、信号概念

2、signal函数

普通标准信号详解表

3、前台/后台进程

3.1 概念

3.2 查看后台进程

3.3 后台进程拉回前台

3.4 终止后台进程

3.5 暂停前台进程

3.6 回复运行后台进程

4、发信号的本质

二、信号的产生

1、终端按键

2、系统调用

2.1 kill

2.2 raise

2.3 abort

总结

3、硬件异常

3.1 除0错误

3.2 野指针错误

底层原理

4、软件条件

4.1 SIGPIPE

4.2 SIGALRM

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

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

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

#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,可以查看所有的后台任务。

$ ./testsig &
[1] 263501 # [job_number] pid
$ jobs
[1]+  Running                 ./testsig &

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

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

3.3 后台进程拉回前台

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

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

3.4 终止后台进程

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

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

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

方法二:通过进程PID

$ 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对应要发送信号进程的pidsig表示发送的信号种类。
  4. 返回值:如果成功,返回值为 0。否则,返回值为 -1

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

// 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系统调用给当前进程发送信号时,都能捕获到然后打印出来我们查看。

#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;
}
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号),使当前进程异常终止。

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

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

# 首先明确abort函数和exit函数的不同作用。abort函数的作用是异常终止进程,它本质上是通过向当前进程发送SIGABRT信号来实现这一目的。而exit函数的作用是正常终止进程。
需要注意的是,使用exit函数终止进程可能会失败,因为在某些复杂的程序运行环境中,可能存在一些因素干扰正常的进程终止流程。然而,使用abort函数终止进程通常被认为总是成功的,这是由于其通过发送特定信号强制终止进程,一般情况下进程很难忽略该信号而继续运行。

总结

3、硬件异常

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

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

#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有各种寄存器,如EIPEBP,还有一个状态寄存器,它里面有运算的标记位,如是否溢出、是否为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 的紧密结合使得整个内存访问过程更加高效。

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

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

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

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

4、软件条件

4.1 SIGPIPE

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

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

# 例如,下面代码,创建匿名管道进行父子进程之间的通信,其中父进程去读取数据,子进程去写入数据,但是一开始将父进程的读端关闭了,那么此时子进程在向管道写入数据时就会收到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 ){//child -> writeclose(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 -> readclose(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函数。

#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后,当前进程会收到信号SIGALRMwhile (true){int cnt = 0;std::cout << "count: " << cnt++ << std::endl; }return 0;
}

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

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

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

#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后,当前进程会收到信号SIGALRMwhile (true){// cout本质是向显示器文件写入,所以是IO// vscode -> ./testSig -> 云服务器 -> 网络 -> 显示器// std::cout << "count: " << cnt++ << std::endl; // 打印效率不高!cnt++;}return 0;
}

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

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

#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暂停,然后每隔一秒发送信号,由信号驱动进程执行我们注册的任务。

#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

总结

理解系统闹钟

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/99229.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/99229.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Altium Designer(AD)自定义PCB外观颜色

目录 1视图设置界面介绍 2PCB阻焊层颜色设置 2.1进入视图设置界面 2.2阻焊层颜色设置 2.3顶层和底层阻焊层颜色设置 2.4顶层阻焊层试图效果 2.5底层阻焊层试图效果 3设置PCB丝印颜色设置 3.1找到丝印设置选项 3.2设置顶层和底层丝印颜色 3.3顶层丝印 3.4底层丝印 4…

5天改造,节能50%!冷能改造如何实现“不停产节能”?

你有没有发现一个现象&#xff1f;很多工厂老板一提到节能改造&#xff0c;第一反应就是摇头。不是不想省电费&#xff0c;而是怕停产。停产一天损失几十万&#xff0c;改造周期动辄几个月&#xff0c;这账怎么算都不划算。但如果我告诉你&#xff0c;有一种改造方式&#xff0…

【Flink】窗口

目录窗口窗口的概念窗口的分类滚动窗口&#xff08;Tumbling Windows&#xff09;滑动窗口&#xff08;Sliding Windows&#xff09;会话窗口&#xff08;Session Windows&#xff09;全局窗口&#xff08;Global Windows&#xff09;窗口API概览窗口函数增量聚合函数ReduceFun…

攻击路径(4):API安全风险导致敏感数据泄漏

本文是《攻防演练 | JS泄露到主机失陷[1]》的学习笔记&#xff0c;欢迎大家阅读原文。攻击路径通过未授权访问攻击获取敏感数据通过SQL注入攻击获取服务器权限通过凭据访问攻击获取数据库权限和敏感数据和应用权限安全风险与加固措施通过未授权访问攻击获取敏感数据、通过SQL注…

机器学习面试题:请介绍一下你理解的集成学习算法

集成学习&#xff08;Ensemble Learning&#xff09;的核心思想是“集思广益”&#xff0c;它通过构建并结合多个基学习器&#xff08;Base Learner&#xff09;来完成学习任务&#xff0c;从而获得比单一学习器更显著优越的泛化性能。俗话说&#xff0c;“三个臭皮匠&#xff…

Invalid bound statement (not found): com.XXX.XXx.service.xxx无法执行service

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.xxx.xxx.service.CitytownService.selectCitytown 出现无法加载sevice层的时候&#xff0c;如下图所示1&#xff0c;处理方法是&#xff0c;先看下注解MapperScan内的包地址&#xff0c…

泛型(Generics)what why when【前端TS】

我总是提醒自己一定要严谨严谨严谨 目录TypeScript 泛型 (Generics)1. 什么是泛型&#xff1f;2. 为什么需要泛型&#xff1f;3. 泛型常见用法3.1 函数泛型3.2 接口泛型3.3 类泛型3.4 泛型约束3.5 泛型默认值3.6 多个泛型参数4. 泛型应用场景TypeScript 泛型 (Generics) 1. 什…

分布式协议与算法实战-协议和算法篇

05丨Paxos算法&#xff08;一&#xff09;&#xff1a;如何在多个节点间确定某变量的值? 提到分布式算法&#xff0c;就不得不提 Paxos 算法&#xff0c;在过去几十年里&#xff0c;它基本上是分布式共识的代名词&#xff0c;因为当前最常用的一批共识算法都是基于它改进的。比…

9.13 9.15 JavaWeb(事务管理、AOP P172-P182)

事务管理事务概念事务是一组操作的集合&#xff0c;是一个不可分割的工作单位&#xff0c;这些操作要么同时成功&#xff0c;要么同时失败操作开启事务&#xff08;一组操作开始前&#xff0c;开启事务&#xff09;&#xff1a;start transaction / begin提交事务&#xff08;这…

检索融合方法- Distribution-Based Score Fusion (DBSF)

在信息检索&#xff08;IR&#xff09;、推荐系统和多模态检索中&#xff0c;我们常常需要融合来自多个检索器或模型的结果。不同检索器可能对同一文档打出的分数差异很大&#xff0c;如果直接简单加权&#xff0c;很容易出现某个检索器“主导融合结果”的情况。 Distribution…

Oracle体系结构-归档日志文件(Archive Log Files)

核心概念&#xff1a;什么是归档日志文件&#xff1f; 定义&#xff1a; 归档日志文件&#xff08;Archive Log Files&#xff09;是在线重做日志文件&#xff08;Online Redo Log Files&#xff09;在被覆盖之前的一个完整副本。它们由 Oracle 的后台进程 ARCn&#xff08;归档…

GoogLeNet实战:用PyTorch实现经典Inception模块

配套笔记&讲解视频&#xff0c;点击文末名片获取研究背景&#xff08;Background&#xff09; 1.1 领域现状&#xff08;大环境与挑战&#xff09; 想象一下&#xff0c;你和朋友们在看一大堆照片——猫、狗、汽车、蛋糕&#xff0c;大家要把每张照片贴上标签。几年前&…

【开题答辩全过程】以 “旧书驿站”微信小程序的设计与开发为例,包含答辩的问题和答案

个人简介一名14年经验的资深毕设内行人&#xff0c;语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。感谢大家的…

【办公类-112-01】20250912家园每周沟通指导(Deepseek扩写完善+Python模拟点击鼠标自动发送给家长微信)

背景需求 孩子刚上小班,家长比较关心孩子情况(情绪、社交、吃饭等) 所以我每周五晚上和家长沟通一下孩子的情况。 操作流程 第一周(9月5日)是“适应周”,我添加了所有孩子的一位家长的微信号 23份全部是手打,足足写了4个小时。第一周案例多,所以写了很多,措辞酝酿后…

Spark专题-第一部分:Spark 核心概述(1)-Spark 是什么?

众所周知&#xff0c;教学文档总该以理论部分作为开篇&#xff0c;于是我们这篇Spark专题同样会以一堆理论和专有名词开始&#xff0c;笔者会尽可能的让专业词汇通俗易懂 第一部分&#xff1a;Spark 核心概述 Spark 是什么&#xff1f; 1. 大数据时代的"超级赛车"…

从零到一上手 Protocol Buffers用 C# 打造可演进的通讯录

一、为什么是 Protobuf&#xff08;而不是 XML/自定义字符串/.NET 二进制序列化&#xff09; 在需要把结构化对象持久化或跨进程/跨语言传输时&#xff0c;常见方案各有痛点&#xff1a; BinaryFormatter 等 .NET 二进制序列化&#xff1a;对类型签名与版本极其脆弱、体积偏大&…

计算机网络(三)网络层

三、网络层网络层是五层模型中的第三层&#xff0c;位于数据链路层和传输层之间。它的核心任务是实现数据包在不同网络之间&#xff08;跨网络&#xff09;的逻辑传输。网络层的数据传输单位是数据报&#xff08;Datagram&#xff09;或数据包&#xff08;Packet&#xff09;。…

互联网大厂Java面试实录:从基础到微服务全栈技术答疑

互联网大厂Java面试实录&#xff1a;从基础到微服务全栈技术答疑 本文以电商场景为背景&#xff0c;展现一场互联网大厂Java开发职位的面试过程。严肃的面试官与搞笑的水货程序员谢飞机展开三轮技术问答&#xff0c;涵盖Java SE、Spring Boot、数据库、微服务、安全以及CI/CD等…

StringBuilder 深度解析:数据结构与扩容机制的底层细节

文章目录 前言 一、数据结构&#xff1a;不止是简单的字符数组 1. 核心成员变量&#xff08;定义在 AbstractStringBuilder 中&#xff09; 2. 构造器与初始容量 二、扩容机制&#xff1a;从 "不够用" 到 "换大容器" 的全过程 步骤 1&#xff1a;计算…

Elasticsearch面试精讲 Day 17:查询性能调优实践

【Elasticsearch面试精讲 Day 17】查询性能调优实践 在“Elasticsearch面试精讲”系列的第17天&#xff0c;我们聚焦于查询性能调优实践。作为全文检索与数据分析的核心引擎&#xff0c;Elasticsearch的查询性能直接影响用户体验和系统吞吐能力。在高并发、大数据量场景下&…