当前位置: 首页 > news >正文

Linux进程信号——初步认识信号、信号的产生

文章目录

  • 初步认识信号、信号的产生
    • 对“信号”的初步认识
      • 信号概念的引入
      • 关于信号的基本结论
    • 信号的产生
      • Linux系统下的信号
      • 键盘信号(引出signal信号捕捉接口)
      • 先行理解发送信号的本质——引出系统调用发送信号
      • 发送信号——系统调用接口
        • kill
        • raise
        • abort
      • 系统命令
      • 硬件异常
        • 除0错误
        • 野指针(段错误)
        • 解释硬件异常原因
      • 软件条件
        • 通过软件条件发送信号——测试IO效率
        • 代码样例——重复设定闹钟
        • 快速理解闹钟

初步认识信号、信号的产生

本篇文章开始,将对Linux下的进程信号做一定的了解。本片文章需要先对信号宏观上有一个认知,能够把信号的产生过程和保存过程理解清楚即可。

对“信号”的初步认识

首先,讲解Linux进程信号前,我们需要对”信号“这一概念有一个初步地认识。

信号概念的引入

生活中存在着许多的信号:
如闹钟、红绿灯、古代打仗的狼烟、上课铃…
这些都可以被称为信号!因为它们带有通知属性。

对于信号的更详细解释:
信号是一种异步通知机制,会中断当前我们正在做的事情。

当然,通知也是有同步的机制的。
异步机制是指:等待信号通知的过程是不会影响当前事件的运行的
同步机制则是:当前事件的发生会因为等待某个通知而阻塞

举个简单的例子:
老师正在上课,突然学校广播说要每个班派人到楼下拿教材。现在老师派出班上几个人去拿:
1.老师认为当前内容重要,要等到拿东西同学回来在讲,那么当前就会等待他们回来——同步
2.老师认为当前内容不重要,就会继续讲课,同学拿东西和老师听课同时发生——异步

在Linux进程下,信号是异步的!也就是进程的运行不会因为信号而阻塞住。就像人睡觉等待闹钟一样。闹钟响了这代表信号让人接收到了。但是等待信号的过程并不会影响我们睡觉。

关于信号的基本结论

这里需要说明关于信号的几个基本结论,以便后续学习进程信号能够更清晰明白原理:

  1. 信号的过程大致可以分为:产生、保存、处理。 后序的讲解也是围绕着这三个方面
  2. 信号的处理并不是马上的(实时信号除外)!信号可以过段时间再处理(等到合适时机处理),但是前提是记得处理!就像我们听到闹钟不一定是马上起床的。所以,信号需要保存!以便后面能够处理。
  3. 不同的信号有不同的处理方式。而这些方式,早就是知道的:
    如:闹钟 -> 起床 | 上课铃 -> 上课…
  4. 人知道对于信号怎么处理,那是被”教育/告知“过的。在Linux系统下也是一样,进程即使不知道会收到什么信号,但是会知道一旦收到了信号,就会根据信号做出指定处理方式!
    编写OS的程序员早把不同信号的处理方式定义好了!这是“教育”进程对于信号的处理!
    5.信号产生是由信号源产生的,而信号源会有很多种!

除了上面的几个结论之外,对于信号的处理,我们需要提及的是:

信号的处理方式分为三种:
1.默认动作
2.自定义动作
3.忽略->不处理

我们可以再举一个例子来理解上面的信号处理方式:
好比我们和朋友玩真心话大冒险,输的人要去十字路口红绿灯变红的时候,当众跳舞。
在这个场景下,红绿灯就是一个信号,红灯->告诉行人停止在斑马线前等待。

大部分人都是默默地等待这个灯,-> 默认动作!
但是总是会有一些比较奇怪的人,在红灯亮的时候跳舞 -> 自定义动作!
还有一些没素质的,不要命的人,即使红灯亮了也不管,仍要过马路 -> 忽略处理!

信号的产生

接下来,我们将来了解信号的产生。首先,我们需要从一个例子出发,把信号在进程中的流动过程理解清楚了,后序再学习信号的保存和处理就不会太难了。

Linux系统下的信号

Linux系统中所有的信号可以通过指令kill -l查询出来:

ynp@hcss-ecs-1643:~$ kill -l1) SIGHUP	 	2) SIGINT	 	3) SIGQUIT		4) SIGILL	 	5) SIGTRAP6) SIGABRT	 	7) SIGBUS	 	8) SIGFPE	 	9) SIGKILL		10) SIGUSR1
11) SIGSEGV		12) SIGUSR2		13) SIGPIPE		14) SIGALRM		15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD		18) SIGCONT		19) SIGSTOP		20) SIGTSTP
21) SIGTTIN		22) SIGTTOU		23) SIGURG		24) SIGXCPU		25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF		28) SIGWINCH	29) SIGIO		30) SIGPWR
31) SIGSYS		34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

如果想要查看到信号的具体信息,可以去man-pages的7号手册查询:
man 7 signal:
在这里插入图片描述

在Linux系统中,信号就是一个个的整数。只不过单纯使用整数的可读性不好。所以不同的信号都被定义成了一个宏!

其中:
1 ~ 31号为普通信号,这是需要我们重点学习和了解的。而34 ~ 64信号则是实时信号。实时信号我们不做了解!其机制和普通信号略有不同。

键盘信号(引出signal信号捕捉接口)

首先,我们就以最常用的一种信号来串联起信号的所有内容——即ctrl + c

曾说过,如果当前某个系统正在跑某个进程,一段时间内停不下来,我们是可以按下ctrl + c把进程给终止掉的。其实,ctrl + c的本质就是通过键盘向进程发送信号。
ctrl + c发送的信号,或者说是Linux系统下的一部分信号,默认动作都是终止进程!


这里提出两个问题:
既然说ctrl + c是向当前进程发送一个信号,发送的就是2号信号。
那应该如何证明呢?

这里我们就需要提前的介绍一个捕捉信号的系统调用接口:signal

NAMEsignal - ANSI C signal handlingSYNOPSIS#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);sighandler_t signal(int signum, sighandler_t handler);
The  use  of  sighandler_t  is  a  GNU  extension,  exposed if _GNU_SOURCE 
is defined; glibc also defines (the BSD-derived) sig_t if _BSD_SOURCE 
(glibc 2.19 and earlier) or _DEFAULT_SOURCE (glibc 2.19 and later) is defined.
Without use of such a type, the declaration of signal() is the somewhat harder to read:void ( *signal(int signum, void (*handler)(int)) ) (int);

这个函数的第一个参数就是信号值(信号对应的整数),而第二个参数,我们会发现是一个函数指针!指向的函数 ->返回值为void,参数为int。

这个接口的作用是:
把signum参数接收的信号值对应的信号处理操作改为sighandler_t类型的操作。这个操作是需要我们自定义的!也就是说,只需要自定义编写一个返回值为void,参数为int的函数传入给signal接口的第二个参数,signum信号的信号处理行为就会转变为handler指向的函数!

为了证明,我们下面用一段代码来验证一下:

#include <iostream>
using namespace std;
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>void SIG2_handle(int sig){cout << getpid() << "进程" << "捕捉到了" << sig << "信号" << endl;
}int main(){signal(2, SIG2_handle);while(1){cout << "我是一个进程, pid是: " << getpid() << endl;sleep(1);}return 0;
}

我们尝试着跑一下这个代码,然后跑的过程中按下ctrl + c来发送信号:
在这里插入图片描述
我们会发现,当我们按ctrl + c的时候,该进程果然是把处理行为变成了我们手动改变的行为——SIG2_handle。而且,我们没办法通过ctrl + c来终止这个进程了!

上述的实验结果验证了ctrl + c就是向当前进程发送2号信号!但现在停不下来了怎么办?
答案很简单,发送别的信号就可以了:
ctrl + \ ,向当前进程发送3号信号。

在这里插入图片描述
3号信号是让进程立马终止的信号,我们查看man手册可以发现,
SIGINT(2)和SIGQUIT(3)对应的Action不同,前者是Term,后者是Core。这里我们先不做深究。我们只需知道二者是让进程终止信号即可。还有一个Action是Ign(ignore),即忽略处理。


前台后台进程相关问题:

现在我们来解决一个问题,即为什么ctrl + c没办法杀掉后台进程?

在学习进程状态的时候,我们曾经讲过进程是可以挂到后台运行的。

1.运行进程的时候在后面加一个&,如./testsig.exe &
2.当父进程比子进程先退出的时候,子进程变为孤儿进程,会被1号进程领养,然后挂后台运行

当进程挂到后台运行的时候,我们会发现:
1.我们发送的信号会失效
2.我们在显示器上输入指令是有效的

在这里插入图片描述
按下ctrl + \ 也是没有效果的。这个时候只能通过kill -9 pid来杀进程。

这里我们就需要来了解前后台进程的相关内容:
首先,键盘的输入只能发给前台进程!而Linux操作系统下,有一个进程始终是在跑的,就是bash进程。所以,当./testsig.exe进程挂到后台运行的时候,前台进程是bash!所以我们发送的信号都是发给了bash,而bash进程是对信号处理修改过的,使用ctrl + c / \ 发送给bash并没有起到作用。所以,我们没办法通过键盘直接给后台进程发送信号。

这背后的原因是:
显示器也是一个文件(stdout)。键盘输入也是一个文件(stdin)。对于stdout而言,是可以把多个进程输出的内容给打印的。所以我们可以发现父子进程同时输出的时候可能会有些混乱!
但是stdin只允许接收一个进程的键盘输入!


这么做的原因是:
从键盘中读取的数据,一般是要经过一定的处理的!所以需要保证读取到的数据的正确性!所以只能让一个进程读取!而且是前台的进程!
但是,显示器文件而言,是把处理好的数据给展示出来,所以不用刻意的限制单一进程输出到显示器上。多个进程可以同时输出。

所以这也就解释了为什么我们没办法通过键盘发送信号来杀掉后台进程!

补充一些前后台移动的命令:
系统中会存在挂到后台运行的进程的任务表,每个后台进程都会有任务号,后序我们如果想要让某个进程在前台后台来回移动,我们需要通过任务号来操作:

jobs 		 ---查看当前后台任务列表
fg 任务编号   ---将任务编号对应的后台进程挂到前台ctrl + z 	---快速把当前前台进程挂到后台(其实就是把进程暂停)
//前台进程是不允许暂停的!一旦暂停,操作系统立马拉到后台。
bg 任务编号 	---让暂停在后台的进程在后台恢复运行

这里需要注意,因为后台进程不断向显示器打印,所以我们如果在显示器上输入指令很可能是被分散到不同行去的:
在这里插入图片描述
但是,这不用担心,因为输入到的是stdin,只不过是系统设计的时候把输入也回显到了stdout上罢了。所以哪怕指令分行了也没有问题的。

上面的指令我就不再具体展示了,直接使用即可。

先行理解发送信号的本质——引出系统调用发送信号

在讲解其他产生信号的方式的时候,我们还需要先理解以下发送信号的本质。

信号,其实就是一个整数,只不过为了可读性,信号被宏定义了。而且,信号并不是一收到就要进行处理的,而是要等一个合适的时机。那么在处理之前,那必须是有地方存放!

问题来了,存放在哪里呢?
直接说答案:在进程的PCB中,存在着一个接收信号表(实际上是一个位图)!
在这里插入图片描述
通过这样一个位图,就可以很轻松的表示当前进程收到了什么信号!
(如果接受到多个相同信号,这个问题先不管,等了解完信号的保存就知道是怎么回事了)。

所以,操作系统发送信号的本质,其实就是向进程PCB内的信号表位图进行填写!
->这个操作本质也就是修改内核数据结构! -> 内核数据结构不可能让用户/进程直接修改 -> 系统需要提供系统调用!

所以,接下来我们将看第二种信号产生的办法——系统调用!

发送信号——系统调用接口

我们来介绍一下发送信号的系统调用接口。因为用户层所有的发送信号,最终都是通过系统调用来完成的!因为用户层没办法直接修改内核数据结构!

kill
NAMEkill - send signal to a processSYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);RETURN VALUEOn success (at least one signal was sent), zero is returned.  On error, -1 is returned, and errno is set appropriately.

这个接口的操作非常简单,就是向特定pid对应的进程发送一个sig信号!
返回值:发送信号成功返回0,反之返回-1,并设置错误码。

基于此,我们其实可以写一个简单的kill指令,即和Linux系统中kill指令一样。我们来试着向某个进程发送信号:

//mykill.cpp
#include <iostream>
using namespace std;#include <sys/types.h>
#include <signal.h>int main(int argc, char* argv[]){//这个进程的用法是 ./mykill 信号值 pidif(argc != 3){cout << "please use the format of <./mykill.exe sig pid>" << endl;}int sig = stoi(argv[1]);//信号值pid_t snd_pid = stoi(argv[2]);//pid_t其实就是整数 所以直接把字符串转整形即可kill(snd_pid, sig);return 0;
}//testsig.cpp
#include <iostream>
using namespace std;
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>void SIG2_handle(int sig){cout << getpid() << "进程" << "捕捉到了" << sig << "信号" << endl;
}int main(){// 把所有的信号捕捉一下 -> 方便观察for(int i = 1; i < 32; ++i)signal(2, SIG2_handle);while(1){cout << "我是一个进程, pid是: " << getpid() << endl;sleep(1);}return 0;
}

在这里插入图片描述
确实是如此,我们在另一个终端运行mykill进程,向pid为780039的进程发送信号。由于在780039对应的进程中,把所有的信号都给捕捉了,并且自定义的修改了信号处理的行为,所以发送信号过去最后处理结构都是打印那句话。

但是,这就会引发一个问题,如果所有信号被捕捉了,那么会导致一个很大的问题!如果所有的信号都被恶意捕捉了,那么不就可以认为的控制进程无法退出吗?如果是病毒那就危险了!
这个不用担心,编写OS的工程师们早就想好了这个解决办法的!31个信号中,是有两个没办法被捕捉的!

在这里插入图片描述
其中,我们只需要记住9号信号!9号信号是没办法被捕捉的!也就是没办法对9号信号的信号处理行为进行修改!

当然,可以手动的测试一下:
在这里插入图片描述
让进程自己给自己发信号(过滤掉9号),我们发现,当i == 19的时候,就当前进程就给终止了!这说明,19号也是不可被捕捉的。
除此之外,其余信号均可手动捕捉,并自定义规定信号处理方式!

raise
NAMEraise - send a signal to the callerSYNOPSIS#include <signal.h>int raise(int sig);

raise其实是语言层封装过的一个接口,但是其底层还是系统调用。所以就将其归类到系统调用这里了。我们来看看它的使用:

raise接口是用来给调用这个接口的进程发信号的——也就是自己给自己发信号!
在这里插入图片描述

abort
NAMEabort - cause abnormal process terminationSYNOPSIS#include <stdlib.h>void abort(void);

abort也是语言层面封装的接口。它的作用是——发送一个信号,让一个进程立马停止下来!
所以这个接口永远都是没有返回值的!

#include <iostream>
using namespace std;
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>void SIG2_handle(int sig){cout << getpid() << "进程" << "捕捉到了" << sig << "信号" << endl;
}int main(){// 把所有的信号捕捉一下 -> 方便观察for(int i = 1; i < 32; ++i)signal(i, SIG2_handle);cout << "hello" << endl;cout << "hello" << endl;cout << "hello" << endl;cout << "hello" << endl;cout << "hello" << endl;abort();
}

最后我们可以发现,该接口向调用它的进程发送了6号信号,并且退出!
在这里插入图片描述
但是,测试捕捉信号的时候,捕捉6号并没有让进程退出,这是为什么?
这是因为,abort函数内做了处理。等到发送了信号后,执行完处理动作,还要把原来6号对应默认处理给替换回来,再执行新的!这是abort的特点!

系统命令

系统命令很简单,就是直接使用kill指令 -> kill -sig_id(id前要带"-”) pid
调用该命令后,系统会向pid对应的进程发送一个信号。

由于这个早就已经使用过,且已经写了mykill.exe进程进行模拟实现,在这里就不多说了。

硬件异常

有时候,我们写的代码会直接崩溃,具体是两种情况:
1.野指针 (如:char* ptr = nullptr; *ptr = 0;)
2.除0操作 (如:a /= 0;)

这些情况都是硬件异常导致的错误!具体为什么时硬件,这个我们先埋个伏笔。


当我们的代码出现上面的两种异常错误的时候,进程调用会直接崩溃,也就是直接退出了。但是,进程为什么会退出呢?
答案:因为当代码发生异常的时候,操作系统会自动地向该进程发送信号!

除0错误
void SIG2_handle(int sig){cout << getpid() << "进程" << "捕捉到了" << sig << "信号" << endl;exit(errno);
}int main(){for(int i = 1; i < 32; ++i)signal(i, SIG2_handle);//1. 除0错误cout << "hello" << endl;cout << "hello" << endl;cout << "hello" << endl;sleep(2);int a = 0;a /= 0;return 0;
}

在这里插入图片描述

8号信号是8) SIGFPE,即Float Point Error,浮点数错误。

野指针(段错误)
int main(){for(int i = 1; i < 32; ++i)signal(i, SIG2_handle);//2. 段错误int a = 10;int* pa = nullptr;*pa = 20;cout << *pa << endl;return 0;
}

在这里插入图片描述
11号信号是11) SIGSEGV,即Segment Violation,段错误。


解释硬件异常原因

最后得出结论:进程崩溃退出的原因 -> 进程收到了信号!

也就是说,操作系统是一直在对进程的运行做检测的!一旦发现错误了(代码错误),就会立马发送信号让进程终止。

问题来了:操作系统怎么知道代码出错?和硬件异常又有什么关系呢?


这就不得不提代码运行的时候硬件的作用了,即CPU。

操作系统是一款管理软硬件资源的软件!所以也能管理得到CPU。CPU内会有很多的寄存器:

寄存器类型x86 架构(32位)x86-64 架构(64位)ARM 架构(32位)ARM 架构(64位)
通用寄存器EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESPRAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8-R15R0-R12X0-X28
指令指针寄存器EIPRIPPC(R15)PC(X30 相关)
状态标志寄存器EFLAGSRFLAGSCPSR(当前程序状态寄存器)CPSR(扩展为64位)
栈指针寄存器ESPRSPSP(R13)SP(X29 相关)
基址指针寄存器EBPRBPFP(R14)FP(X29)
段寄存器CS, DS, ES, FS, GS, SS保留(兼容模式下使用)无(采用内存映射)无(采用内存映射)
控制寄存器CR0, CR1, CR2, CR3, CR4CR0, CR1, CR2, CR3, CR4, CR8CP15 寄存器组SCTLR_ELx 等系统寄存器
调试寄存器DR0-DR7DR0-DR7DBG 寄存器组DBG 寄存器组

不需要关注所有的寄存器,我们只需要关注个别几个:
1.状态标志寄存器(EFLAGS),EFLAGS 寄存器用于记录 CPU 运算后的状态信息(如结果是否为零、是否进位/溢出等),是条件判断(如 if、循环)和指令执行流程控制的基础。

所以,当代码中出现除0操作的时候,这个寄存器就能检测得到。操作系统就会根据其设定的状态来判断当前是否出现了错误!CPU知道了,那么操作系统也就知道了。

2.CR3寄存器,CR3寄存器内保存的是页表中的物理地址。
在这里插入图片描述
上图是正常CPU调度进程时,通过地址查找数据的过程。

但是,如果某个进程中的,使用了野指针操作,即对0地址访问。而0地址是不可能在页表中找到映射关系的!所以就没办法找到真实物理地址。所以就会直接报错。所以野指针的问题就是这样的,就是因为页表中找不到映射的地址!
而直接修改常量这种操作,是因为页表中也带有数据对应的权限。权限错误也是会硬件报错。

因为OS能管理所有的硬件,所以通过硬件的报错,操作系统就知道了是硬件异常。然后会根据硬件的异常来向对应的进程发送指定信号(段错误信号/浮点数错误信号)。

所以,由上我们得知:硬件异常也是会向进程发送信号!

软件条件

当软件触发某些特定条件的时候,系统也是会向进程发送信号,然后让进程中断的。这个就单纯是软件层面的规定,和硬件无关了!

其实我们早就接触过软件条件的中断信号——管道文件读端关闭,写端继续写的情况!
当我们关掉读端的时候,写端进程会收到信号,立马退出(信号为13)SIGPIPE)。

本质的原因是:操作系统并不会做浪费时间、浪费资源的事情!


这里我们来介绍alarm 函数和 SIGALRM 信号。

NAMEalarm - set an alarm clock for delivery of a signalSYNOPSIS#include <unistd.h>unsigned int alarm(unsigned int seconds);RETURN VALUEalarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no previ‐
ously scheduled alarm.
  • 调用 alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程

  • 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0(alarm(0)),表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

也就是说,这个函数返回值是上一次设定的闹钟,在此刻的剩余倒计时时间!(首次调用就是0)


通过软件条件发送信号——测试IO效率

我们来通过这个闹钟来设定程序运行时间,然后看看IO操作的效率:
在这里插入图片描述
我们可以很清楚的发现,IO操作是比较低效率的!但是如果只是让CPU单纯的做一些基础运算,那么CPU一秒钟的运行次数可达数十亿。

所以,我们就通过简单地软件条件设置,让进程跑够一定时间后,就让系统向进程发送信息停止下来,这就是软件条件!


代码样例——重复设定闹钟
#include <functional>
#include <vector>using func_t = function<void()>;
vector<func_t> _vf;void CheckMem(){cout << "检查内存碎片..." << endl;
}void Register(){cout << "注册..." << endl;
}void Init(){cout << "初始化" << endl;
}void SIG_handle(int sig){cout << getpid() << "进程" << "捕捉到了" << sig << "信号" << endl;alarm(1);// 重新设置闹钟
}int main(){for(int i = 1; i < 32; ++i)signal(i, SIG_handle);_vf.push_back(Register);_vf.push_back(Init);_vf.push_back(CheckMem);alarm(1);while(1){pause();//阻塞 收到信号后就不会再阻塞for(auto& func : _vf){func();}}return 0;
}

我们让此进程先阻塞住(pause函数),如果进程收到了信号,就会停止阻塞,从而处理信号。因为这里我们规定了信号alarm函数发的信号不会退出,只会重新设定闹钟。
所以最后的结果应该是,该进程每隔1s做一些系统工作:

在这里插入图片描述
确实是如此。这个时候,就可以通过信号来控制一个进程的工作流程!

快速理解闹钟

我们可以知道的是,操作系统中会存在着很多进程,都使用闹钟函数来进行控制进程。那么,系统中会存在着很多闹钟。那么操作系统必然要以“先描述、再组织”的方式来管理闹钟。

struct timer_list {struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s *base;
};

我们来看看这个描述闹钟的结构体:
expire存储的是当前闹钟到期时间。当然,最重要的就是处理方法function!(接收到闹钟信号的处理方式)。


这里快速理解一下闹钟是如何确定到时的:(如何确定expire?)
我们早在学习基础指令的时候,就了解过时间戳的概念。也就是计算机中存在一个计数器,每隔1s计数一次。不会因为电脑关机而放弃工作!

计算机中就是靠着时间戳计时的,从一个起点开始计时。所以,当一个进程设定闹钟的时候,那么就会根据设定闹钟那一刻的时间戳开始,往后计数n个时间戳。那么,这个进程的脑中的到期时间expire就是那一刻的时间戳 + n

这样子,就可以知道进程闹钟什么时候到期,然后一旦到了那个时间戳,就向进程发送信号!(如果重新设定闹钟就要重新修改过期时间)。


我们可以近似地认为,Linux系统下管理闹钟结构体地数据结构是小堆
小堆地性质就是,堆顶数据一定是最小的,根节点的数据比孩子数据要小!

这样子做,就可以很轻松的组织管理闹钟的同时,还能快速的找出当前进程中最快到期的闹钟!然后到期后,就重新管控堆,找到新的最快到期的闹钟放在堆顶!

通过上面一系列的操作,我们就会发现,信号发送的原因只取决于软件的条件。是不涉及到硬件的异常、状态等。所以,这种产生信号的方式叫做软件条件!

http://www.dtcms.com/a/316560.html

相关文章:

  • 《UE教程》第一章第六回——迁移独立项目(资源)
  • IAR软件中变量监控的几种方法
  • 如何在 FastAPI 中优雅处理后台任务异常并实现智能重试?
  • Wireshark安装过程 Npcap Setup: Failed to create the npcap service: 0x8007007e.
  • 信息系统项目管理中的沟通管理实战精解
  • 智慧能源场景设备缺陷漏检率↓76%:陌讯多模态融合检测方案实战解析
  • SpringCloud学习------Gateway详解
  • Claude Code 完整指南:入门到应用
  • Qt事件系统学习笔记
  • 嵌入式软件架构设计之七:双机通信及通信协议之字符串协议
  • 大语言模型安全攻防:从提示词注入到模型窃取的全面防御浅谈
  • 与功能包相关的指令ros2 pkg
  • 女性成长赛道:现状与发展趋势|创客匠人
  • NumPy 中的取整函数
  • 如何在Android设备上删除多个联系人(3种方法)
  • Java项目:基于SSM框架实现的公益网站管理系统【ssm+B/S架构+源码+数据库+毕业论文+答辩PPT+远程部署】
  • 解锁高效敏捷:2025年Scrum项目管理工具的核心应用解析
  • 智慧社区物业管理平台登录流程全解析:从验证码到JWT认证
  • 关于熵减 - 双线线圈
  • 前端性能测试:从工具到实战全解析
  • 类内部方法调用,自注入避免AOP失效
  • Flutter 国际化
  • OpenSpeedy绿色免费版下载,提升下载速度,网盘下载速度等游戏变速工具
  • spring boot 加载失败 异常没有曝漏出来
  • 基于Java AI(人工智能)生成末日题材的实践
  • 2. JS 有哪些数据类型
  • 【网络运维】Linux:系统启动原理与配置
  • 虚幻GAS底层原理解剖一(开篇)
  • ⭐CVPR2025 用于个性化图像生成的 TFCustom 框架
  • python可视化--Seaborn图形绘制方法和技巧,Bokeh图形绘制方法和技巧