信号1, 信号基础1.1 中断概念: 了解1.2 信号概念1.3 信号递送的流程1.3.1 进程的执行流程1.3.2 一些状态2, 信号和函数2.1 注册信号: signal2.1.1 signal函数2.1.2 注册多个信号2.1.3 多个信号触发: 重要2.1.4 几个问题2.2 注册信号: sigaction2.2.1 sigaction函数2.2.2 sa_mask2.3 信号mask2.3.1 sigpending2.3.2 sigprocmask2.3 其它2.3.1 kill2.3.2 pause2.3.3 sigsuspend: 了解2.3.4 alarm2.3.5 setitimer
中断是计算机硬件和操作系统之间交互的一种机制,用于处理紧急事件或立即需要注意的情况。我们一般把中断分为硬件中断和软件中断,它允许一个硬件设备/软件程序通知CPU它需要处理的事件。(下面是一些常见的例子)
Eg: 一个键盘输入操作触发中断处理的大致流程 (硬件中断)
xxxxxxxxxx
141// 按键动作:
2当你按下键盘上的一个键时,键盘的硬件电路检测到这个按键事件,并生成一个电信号。
3// 硬件中断请求:
4键盘内置的控制器将这个电信号转换为相应的数据发送一个硬件中断请求给CPU。
5// 中断信号接收:
6CPU此时正在运行其它程序, CPU接收到来自键盘的中断信号后暂停当前执行的任务(除非正在处理更高优先级的中断),并保存当前任务的状态/运行上下文,以便之后能够恢复执行。
7// 执行中断服务:
8CPU根据中断信息, 访问存储不同中断对应的处理程序地址表, 找到与键盘中断相对应的中断服务程序,并开始执行它。键盘对应中断处理程序, 读取键盘硬件寄存器中的数据, 并完成初步处理。
9// 操作系统处理:
10键盘对应的中断处理程序将处理解释后的键盘事件传递给操作系统。操作系统进一步处理这个事件。
11// 事件传递给应用程序:
12操作系统将中断处理程序处理后的键盘事件, 以消息的形式发送给当前活动的或具有焦点的应用程序。应用程序通过其事件处理机制接收和响应这些消息,例如,当你在文本编辑器中按下字符键时,编辑器会在光标位置显示相应的字符。
13// 恢复执行:
14应用程序处理完键盘事件后,操作系统将控制权返回给之前被中断的任务。CPU恢复执行之前的操作上下文,直到下一个中断到来。
Eg: 一个网卡接收网络数据包触发中断的大致流程 (硬件中断)
xxxxxxxxxx
221// 数据到达:
2当网卡(网络接口卡,NIC)检测到网络上有数据包到达时,它首先会根据硬件和配置(MAC地址)确定这个数据包是否为该设备所接收。
3// 硬件中断请求:
4如果数据包是给这台机器的,网卡会生成一个硬件中断请求,并发送给CPU,通知CPU有网络数据需要接收。
5// 中断信号接收:
6CPU接收到来自网卡的中断信号后,根据其优先级暂停当前任务,并保存其运行状态/上下文,以便稍后恢复。
7// 执行对应的中断服务例程(ISR):
8CPU根据中断信息找到与网络中断相关联的中断服务程序(即:一个对应ISR/Interrupt Service Routine/中断服务程序),并开始执行它。
9// 这个ISR程序:
10负责与网卡交互,读取网卡缓冲区中的数据包。
11进行初步数据处理,如检查数据包的完整性和目的地。
12将数据包复制到操作系统的内存中。
13// 操作系统处理:
14一旦数据包被复制到系统内存,中断服务程序的工作就完成了,控制权会返回给操作系统。
15操作系统随后会进行更深层次的数据包处理,这包括:
16解析网络协议头(如IP、TCP/UDP头)。
17将数据包传递给适当的网络协议栈层次或服务。
18如果数据包是针对某个特定的应用程序,如一个网页服务器或邮件客户端,操作系统会将数据转发到该应用程序。
19// 应用程序处理:
20接收到数据包的应用程序会根据其功能和数据包内容进行相应处理。(例如: 网页服务器根据HTTP请求返回一个网页, 浏览器得到这个响应, 解析显示网页)
21// 恢复执行:
22应用程序处理完数据包后,操作系统将恢复被中断的任务,直到下一个中断发生。
Eg: 一个读取文件触发中断的大致流程 (软件中断)
ps: 软件中断,也常称为异常或系统调用,是由程序执行的指令引发的中断,通常用于请求操作系统的服务或处理异常情况。
xxxxxxxxxx
131// 应用程序请求:
2一个应用程序需要从文件中读取数据。在代码中,假设调用一个read()的系统函数。
3// 触发软件中断:
4当应用程序执行read()函数时,这个函数实际上会执行一个特殊的指令,该指令触发一个软件中断。这个指令告诉CPU停止执行当前'用户模式'下的指令,转而进入'内核模式'以处理系统调用。
5// 系统调用:
6CPU响应软件中断。这通常涉及到保存当前任务的状态,以便稍后恢复,然后执行与系统调用相关的内核函数。
7// 执行系统调用:
8操作系统内核接管控制权,识别出这是一个read()系统调用。内核会检查调用参数的有效性,然后执行必要的操作来读取文件数据。这可能包括文件系统的查找、磁盘I/O操作等。
9// 返回结果:
10一旦操作系统完成了数据的读取,它会将数据放入应用程序指定的缓冲区,并将控制权和系统调用的结果返回给应用程序。
11// 恢复应用程序执行:
12系统调用返回应用程序/用户模式从后,继续从停止的地方开始执行。
13// 继续执行:应用程继续执行其余的程序代码。
在操作系统中, 信号用于通知进程----发生了某些事情。
信号是一种进程间通信机制。
xxxxxxxxxx
151在Linux中,信号可以由多种不同的情况产生。 (仅了解)
2// 用户交互:
3SIGINT:当用户在终端中按下Ctrl+C时,前台进程组会收到这个信号。这通常用于请求中断一个正在运行的程序。
4SIGQUIT:当用户在终端中按下Ctrl+\时,前台进程组会收到这个信号。这通常会导致进程终止并生成一个核心转储文件,用于调试。
5SIGTSTP:当用户在终端中按下Ctrl+Z时,前台进程组会收到这个信号。这通常用于暂停一个正在运行的程序。
6// 错误或异常条件:
7SIGSEGV:当进程试图访问其内存地址空间之外的内存时,会收到这个信号。
8SIGFPE:当发生异常的算术运算时,如除以零或其他算术错误,进程会收到这个信号。
9SIGBUS:当进程因为某种硬件故障,(Eg:对齐问题OR无法解析的物理地址, 尝试进行非法的内存访问时),会收到这个信号。
10// 系统状态变化:
11SIGCHLD:当子进程终止、停止(由于信号)或继续(由于SIGCONT)时,父进程会收到这个信号。
12SIGHUP:当控制终端关闭或者父进程终止时,与终端关联的进程会收到这个信号。
13// 显式的信号发送:
14SIGKILL:可以通过kill命令显式发送给进程,以请求立即终止。
15...
每个信号用一个整型常量宏表示,以 SIG 开头。
xxxxxxxxxx
31查看信号列表
2// 我们可以通过 "kill -l" 命令, 查看Linux系统中的信号列表。
3// 或者通过"man 7 signal"在man手册中查看信号列表及作用解释。
POSIX.1-1990 标准信号 (了解)
(ps: Term/Terminate表示终止进程; Core表示进程会导出内存映像到文件(一般默认不产生);Ign表示信号被忽略;Stop表示暂停进程)
Signal Value Action Comment SIGHUP 1 Term 链接断开 SIGINT 2 Term 键盘中断(Ctrl+C触发) (默认行为:终止进程) SIGQUIT 3 Core 键盘退出(Ctrl+\触发) (默认行为:终止进程) SIGILL 4 Core CPU指令译码阶段无法识别 SIGABRT 6 Core 异常终止 SIGFPE 8 Core 浮点异常 SIGKILL 9 Term 终止进程 SIGSEGV 11 Core 异常内存访问 SIGPIPE 13 Term 写入无读端的管道 SIGALRM 14 Term 定时器超时 SIGTERM 15 Term 终止 SIGUSR1 30, 10, 16 Term 自定义信号1 SIGUSR2 31, 12, 17 Term 自定义信号2 SIGCHLD 20, 17, 18 Ign 子进程终止或者暂停 SIGCONT 19, 18, 25 Cont 暂停后恢复 SIGSTOP 17, 19, 23 Stop 暂停进程(可通过Ctrl+Z触发)(SIGCONT或者fg恢复) SIGTSTP 18, 20, 24 Stop 终端输入的暂停 SIGTTIN 21, 22, 26 Stop 后台进程控制终端读 SIGTTOU 22, 22, 27 Stop 后台进程控制终端写 xxxxxxxxxx
251常见的信号:
2SIGINT信号
3// 可通过: 键盘 "ctrl+c" 触发
4// 可以通过: "kill -2 pid" 命令触发
5// 信号的默认行为:终止前台进程组中的进程。
6SIGQUIT信号
7// 可通过: 键盘 "ctrl+\" 触发
8// 可以通过: "kill -3 pid" 命令触发
9// 信号的默认行为:终止前台进程组中的进程。
10// 和SIGINT信号的区别是, 会产生核心转储文件/core dumped
11// 如果ulimit设置中core file size为0, 则不会产生转储文件 (ulimit -a)
12SIGKILL信号
13// 可以通过: "kill -9 pid" 命令触发
14// 信号的默认行为:终止前台进程组中的进程
15// 注意: SIGKILL信号的默认行为不可更改(SIGKILL信号不能被进程捕获、忽略或阻塞)
16SIGSTOP信号
17// 可通过: 键盘 "ctrl+Z" 触发
18// 可以通过: "kill -19 pid" 命令触发
19// 信号的默认行为:暂停进程
20// 注意: SIGSTOP信号的默认行为不可更改
21// 可以通过SIGCONT信号把暂停的进程继续运行
22// 也可以通过fg恢复SIGSTOP信号暂停的进程
23SIGCONT信号
24// 可以通过: "kill -18 pid" 命令触发
25// 信号的默认行为:恢复暂停的进程
xxxxxxxxxx
101// kill -l
2// 通过SIGSTOP暂停进程: kill -19 pid (或者ctrl+z暂停)
3// 通过SIGCONT恢复进程: kill -18 pid (或者fg恢复)
4// 可以通过ps -elf查看进程的状态
5int main(int argc,char*argv[])
6{
7while(1){
8}
9return 0;
10}
当进程接收到信号以后,常见有3种选择处理机制(参考信号的Action)。
xxxxxxxxxx
131接收默认处理:
2// 进程接收信号后以默认预设的机制处理。
3// 例如: 连接到终端的进程,用户按下ctrl+c,将导致内核向进程发送一个SIGINT的信号,进程如果不对该信号做特殊的处理,系统将采用默认的方式处理该信号(对应的信号处理函数:signal(SIGINT,SIG_DFL)。
4// 默认处理有5种可能: Term表示终止进程、 Ign表示忽略信号、 Core表示终止进程并产生core文件、 Stop表示暂停进程、 Cont表示继续进程
5
6忽略信号:
7// 进程可以通过代码,显示地忽略某个信号的处理。比如如果将SIGSEGV信号进行忽略(使用信号处理函数signal(SIGSEGV,SIG_IGN)),这是程序运行如果访问到空指针,就不会报错了。
8// 但是某些信号是不能被忽略的
9
10捕捉信号并处理:
11// 进程可以事先注册信号处理函数,当接收到信号时,由信号处理 函数自动捕捉并且处理信号
12
13ps: 有两个信号既不能被忽略也不能被捕捉,它们是SIGKILL和SIGSTOP。即进程接收到这两个信号后,只能接受系统的默认处理,即SIGKILL终止进程。 SIGSTOP是暂停进程。
信号和中断是完全相互独立的: 中断涉及到的是, CPU对紧急事件的处理和响应, 以及进程的调度和上下文切换; 而信号则是被某些行为产生, 产生的信号会修改对应进程的状态, 对应的进程, 又根据被改变的进程状态, 做出对应反应。
xxxxxxxxxx
51// 了解:
2信号处理机制与通过task_struct与进程生命周期紧密关联起来
3// 当信号被发送给一个进程时,Linux内核会更新该进程的task_struct中的信号字段。(Eg: 待处理信号、信号屏蔽和信号处理程序等等。待处理信号字段指示哪些信号已被发送给进程但尚未处理。信号屏蔽指示当前哪些信号被进程阻塞。信号处理程序是指向将在接收信号时执行的函数的指针)
4
5// 当进程被调度运行时,内核检查待处理信号与信号屏蔽。如果有未被屏蔽的待处理信号,内核设置进程的堆栈(涉及进程的PC指针指向要运行的信号处理函数),在进程下次运行时执行适当的信号处理程序。
信号和中断, 很多时候会在操作系统中协作 (并不一定是必须),使得系统和应用程序可以响应各种事件。
xxxxxxxxxx
61以键盘输入(按下Ctrl+C)为例:
2// 硬件中断: 用户按下键盘(Ctrl+C),键盘硬件生成一个中断请求,并发送到CPU。CPU接收到中断请求后,会暂停当前正在执行的任务,保存其状态,并根据中断类型, 找到对应的中断处理程序, 开始执行中断处理程序来响应这个中断。
3
4// 中断处理程序:中断处理程序, 识别中断的来源和类型, 识别出这是一个请求发送中断信号(SIGINT), 然后操作系统确定哪个进程或进程组接收信号(在大多数情况下,SIGINT信号会发送给前台进程组)。这个信号被添加到目标进程的待处理信号队列中(task_struct的pending字段),操作系统还会检查目标进程的信号状态来决定是否立即传递信号。
5
6// 信号处理: 当目标进程下一次被调度执行时,如果它没有阻塞SIGINT信号,操作系统会安排它执行与SIGINT信号相关联的处理程序。(SIGINT的默认情况是终止进程。但是,进程可以自定义信号处理程序来执行不同的操作)
信号的递送流程在 Linux 系统中是一个复杂的过程,涉及到信号的生成、传递、处理等多个步骤。
信号生成: 信号可以有多种事件触发,比如外部中断(如按下 Ctrl+C),系统调用(如
kill
进程函数),或者软件条件(如除零操作)。当这些事件发生时,内核生成一个相应的信号。信号的传递:一旦信号被生成,内核将分辨和处理信号发送给对应的进程。(这一过程涉及到修改目标进程的task_struct结构体中相关的字段) (某种程度上, 信号从产生,到传递给目标进程, 到执行信号的决策过程, 这个步骤的整体也可以称为信号的递送)
信号的排队:内核为信号创建一个信号队列项,并将其加入到目标进程的位图OR信号队列中。(信号也可能会覆盖旧的信号或者被丢弃)
xxxxxxxxxx
251ps: 仅了解
2// 在task_struct中'struct sigpending'类型的pending字段: 用于存储和管理当前进程未决信号中
3struct task_struct{ (source: /include/linux/sched.h)
4struct sigpending pending
5}
6// pending中包含有一个链表list(其实就是上面说的信号队列); 以及一个sigset_t位图(bitmap)。链表list的每个节点都是一个sigqueue结构体(就是上面说的信号队列项)(每个sigqueue都包含了更具体的信号信息),表示一个具体的未决信号。
7// 当信号被递送给进程时,如果这个信号是可排队信号, 内核会创建一个sigqueue实例,并将其插入到进程的未决信号链表中。
8struct sigpending { (source: /include/linux/signal_types.h)
9struct list_head list;// 专门存储, 可排队信号的 eg: 40信号, 再来40号
10// 2号
11sigset_t signal; // 位图-> 存储当前进程有那些信号等待处理
12};
13// 不可排队信号: 同一个信号种类, 多次发送给进程,而进程尚未处理前一个(在未决状态),则新的信号不会排队。(不会在list中记录)
14// 可排队信号: 对某些种类的信号,操作系统允许它具有多个实例状态(即: 信号还是同一种信号, 但是携带信息不同)。这种信号多次发给某个进程(虽然是同一信号, 但是由于携带信息不同), 即使前面存在未处理信号, 也允许它排队。(在list中记录)
15// list参数中存储的是可排队信号的信息。
16// 不可排队的信号信息, 只需要存储在位图对应的标记位里(当然, 可排队信号如果存在未决信号, 也会在位图中标记)。
17// POSIX 标准中的信号,基本都是不可排队的。 (可排队的: SIGRTMIN、SIGRTMAX)
18typedef struct { (source: /include/uapi/asm-generic/signal.h)
19unsigned long sig[_NSIG_WORDS];
20} sigset_t;
21
22信号可能会覆盖:
23// 当一个不可排队的信号被内核通知给进程, 但是在该进程中(位图中), 已经存在了'同一类型'的处于未决状态的信号; 那么同类型的后续信号就可能被视为是重复的, 所谓的"覆盖", 实际上只是简单的忽略。
24信号可能丢弃:
25// 对于那些可以排队的信号,为了防止资源耗尽内核对每个进程可以排队的信号数量有一定的限制。当一个进程的未决信号数量达到这个限制时,如果有新的信号到来,就无法为这个新信号创建新的队列项,因此新信号可能会被丢弃,不会影响到旧的信号。
信号的阻塞与解除阻塞:进程通过修改其信号掩码来阻塞或解除阻塞特定的信号。如果一个信号被阻塞,它仍然可以被递送到目标进程,但不会被立即处理。被阻塞的信号会留在待处理信号集合中,直到它们被解除阻塞。
xxxxxxxxxx
111ps: 仅了解
2// 信号在位图中记录, 进入信号未决不等于信号阻塞。
3// 信号未决的意思是信号暂时未被执行, 而信号阻塞需要对应解除阻塞的操作。
4// 信号阻塞是由进程的信号掩码控制的。
5
6ps: 信号的mask
7// 信号屏蔽字(signal mask),是进程用来暂时阻塞某些信号的一种机制。每个进程都有一个信号屏蔽字,它指定了当前阻塞的信号集合。当一个信号被阻塞时,如果该信号被发送到进程,该信号变为未决状态,但不会立即交付给该进程。换句话说,进程不会处理该信号,直到它被解除阻塞。
8// 当一个进程启动时,大多数信号的mask是不阻塞任何信号的,也就是说,默认的信号mask为零。
9// 信号mask每个信号都对应于位图中的一个位(bit)。如果某个位被设置为1,则表示相应的信号被阻塞。
10// 进程的mask是可以通过函数调用设置和改变的(暂时先不去了解)
11Eg(举例): 如果信号SIGINT(ctrl+c)被触发, SIGINT的值是2, 则对应位图的第二位; 假设此时mask的第二位二进制位为1,则SIGINT信号将被阻塞, 直到mask的第二位二进制位变为为0, 才变为非阻塞态, 并继续等待该信号被处理。
信号的接收与处理:当正在运行的进程某一时刻从内核态执行返回用户态时, 在返回之前会先检查待处理信号集合。如果存在未被阻塞的待处理信号,内核会在进程继续执行前(返回用户态之前), 先安排信号处理程序的执行(默认或者自定义程序)。
信号处理程序的执行:如果进程为信号指定了自定义的处理函数,则该函数会被执行。执行完成后,进程通常会继续其正常执行流程(继续返回用户态)。如果进程使用默认的信号处理行为,或者忽略信号(对于可以被忽略的信号),则相应的默认行动会被采取,比如终止进程或暂停进程。
尽管信号有着多种产生来源,但是对于被动接收信号的进程而言,信号的产生只不过是被修改了task_struct结构体的一些表示信号的参数,就是说,信号产生于内核。
当一个进程处于一个可以接受信号状态的时候(这种状态被称为响应时机),它会取得信号信息,并执行默认行为、或者执行忽略、或者执行自定义信号处理函数。
信号产生表示内核已知信号发生。
信号递送表示内核将生成的信号添加到目标进程的待处理信号集(修改task_struct)(有些时候也表示信号从产生一直到执行的完整过程)。
信号递送侧重表达将信号传递给进程的过程(修改task_struct),而信号处理是侧重表示进程对接收到的信号采取的具体动作。
已经递送但是还没有执行的信号被称为挂起信号(pending signal) 或者是未决信号 (可能是因为: 信号被阻塞, 或者进程暂时不能处理信号, ps: 信号未决 != 信号阻塞, 信号未决的意思是信号暂时未被执行, 而信号阻塞需要对应解除阻塞的操作)。
信号阻塞是由进程的信号掩码控制的。
由进程的某个操作产生的信号称为同步信号(synchronous signals)(例如在代码中除0)。
像用户击键这样的进程外部事件产生的信号叫做异步信号(asynchronous signals)。
再前面我们提到, 进程对信号有三种处理方式。
如果我们未进行任何设置和操作,信号具有预设的处理机制, 将按照预设的处理机制执行。 (操作系统控制/预设的)
我们也可以选择设置忽略一个信号。( signal(信号, SIG_IGN) ) (并不是每一个信号都是可以被忽略的)
设置信号为其默认行为SIG_DFL (signal(信号, SIG_DFL))
xxxxxxxxxx
171Eg: 忽略Ctrl+c (man 2 signal)
2int main(int argc,char*argv[])
3{
4// 给2号信号设置忽略
5//signal(2, SIG_IGN);
6signal(SIGINT, SIG_IGN);
7
8printf("2号信号已经忽略\n");
9sleep(10);
10printf("2号信号已经默认\n");
11
12// 给2号信号恢复默认行为
13signal(2, SIG_DFL);
14sleep(10);
15
16return 0;
17}
我们改变/修改信号的预设机制,给信号设置一个新的处理函数。当信号触发时,让其处理机制,按照我们设置的函数执行。(我们可以通过
signal函数
完成)
signal函数可以用来捕获信号并且指定对应的信号处理行为。 (man 2 signal)
xxxxxxxxxx
912// 定义信号处理函数的类型: int类型的参数(信号编号), void返回值
3typedef void (*sighandler_t)(int);
4
5sighandler_t signal(
6int signum, // 要处理的信号编号(Eg:SIGINT、SIGTERM...)(Eg:2,15...)
7sighandler_t handler // 指向信号处理函数(回调机制): 如上面sighandler_t定义, (另外:SIG_IGN表示忽略信号; SIG_DFL表示恢复信号的默认行为)
8);
9// 返回值: 成功返回关联的指定信号的处理函数的指针; 失败返回SIG_ERR
EgCode:
xxxxxxxxxx
1512
3void func(int sig_value){
4printf("sig_value: %d \n", sig_value);
5//exit(0);
6}
7
8int main(int argc,char*argv[])
9{
10signal(2, func);
11// signal(SIGINT, func);
12
13while(1);
14return 0;
15}
使用signal函数是可以同时注册多个信号。甚至可以把不同的信号的处理函数设置为同一个。
EgCode:
xxxxxxxxxx
131void func(int sig_value){
2printf("sig_value: %d \n", sig_value);
3//exit(0);
4}
5
6int main(int argc,char*argv[])
7{
8signal(SIGINT, func);
9signal(SIGQUIT, func);
10
11while(1);
12return 0;
13}
ps1: 信号的处理函数, 可以在进程执行过程中重新指定.
ps2: sleep()可能会被信号到达而提前终止 (返回剩余睡眠时间)。
xxxxxxxxxx
171void fun1(int sig_value){
2printf("***1*** \n");
3}
4void fun2(int sig_value){
5printf("***2*** \n");
6}
7int main(int argc,char*argv[])
8{
9signal(SIGINT, fun1);
10
11unsigned int retime = sleep(10);
12printf("%u\n", retime);
13
14signal(SIGINT, fun2);
15sleep(10);
16return 0;
17}
在使用函数signal时,如果进程收到一个信号,自然地就会进入信号处理的流程,如果在信号处理的过程中:
接受到了另一个不同类型信号。那么当前的信号处理流程是会被中断的, CPU会先转移执行新到来的信号处理流程,执行完毕以后再恢复原来信号的处理流程。
接受到了另一个相同类型信号。那么当前的信号处理流程是会不会被中断的, CPU会继续原来的信号处理流程,执行完毕以后再响应新来到的信号。
如果接受到了连续重复的相同类型的信号,后面重复的信号会被忽略,从而该信号处理流程只能再多执行一次。
EgCode:
xxxxxxxxxx
141void func(int sig_value){
2printf("***%d***\n", sig_value);
3sleep(10);
4printf("***%d***\n", sig_value);
5printf("----------- \n");
6}
7int main(int argc,char*argv[])
8{
9signal(SIGINT, func);
10signal(SIGQUIT, func);
11
12while(1);
13return 0;
14}
在signal处理机制下,在一些特殊的场景下:
通过signal注册一个信号处理函数,并且处理完毕一个信号之后, 还需要重新注册吗?
xxxxxxxxxx
11不需要重新注册: // 注册一个信号处理函数,并且处理完毕一个信号之后,不需要重新注册,就能够捕捉下一个信号。
如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个同类型的信号,会怎么样?
xxxxxxxxxx
11依次处理或者忽略: // 如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个同类型的信号,那么会依次处理信号,并且忽略多余(超过1次)的信号。
如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个不同类型的信号,会怎么样?
xxxxxxxxxx
11中断当前流程: // 如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个不同类型的信号,那么会中断当前处理流程,跳转新信号的处理流程。
...
虽然signal函数广泛用于设置信号处理函数,但它存在诸多局限性,当需要更精确控制信号处理行为,特别是在编写依赖于特定行为的代码时,更推荐使用sigaction函数 。
使用函数sigaction可以自定义某些场景下的行为。(man 2 sigaction)
xxxxxxxxxx
812// examine and change a signal action
3int sigaction(
4int signum, // 要操作的信号编号(除了不能捕获的SIGKILL和SIGSTOP)
5const struct sigaction *act,// 指定信号的新处理动作(如果非空)
6struct sigaction *oldact// 获取信号的上一个处理动作(如果非空)
7);
8// 返回值: 成功时返回0,错误时返回-1
xxxxxxxxxx
131// 是用于定义信号处理的行为
2struct sigaction {
3void (*sa_handler)(int);// 函数指针:指向一个信号处理函数 (和sa_sigaction选一个即可)
4void (*sa_sigaction)(int, siginfo_t *, void *);// 函数指针:指向一个接受三个参数的信号处理函数
5sigset_t sa_mask;// 信号集: 指定当前信号处理函数执行时需要阻塞的额外信号
6int sa_flags;// 指定信号处理的选项和标志:
7void (*sa_restorer)(void);// 过时,暂无用
8};
9siginfo_t {
10pid_t si_pid; /* Sending process ID */
11sigval_t si_value; /* Signal value */
12// ......
13}
sa_sigaction: 指向一个接受三个参数的信号处理函数
参数一:信号的编号
参数二:siginfo_t 结构体,提供关于信号的更多信息,如发送信号的进程ID等等等(参考man sigaction对siginfo_t类型的说明)(因系统不同结果可能不同, 未必符合预期)
参数三:进程当前上下文的指针(与硬件和操作系统实现相关)
比sa_handler可接受更多的信号的上下文信息
和sa_handler选一个即可
通常仅在参数sa_flags包含SA_SIGINFO标志时使用
sa_flags:信号处理方式掩码, 可以用来设置信号的处理模式。
SA_SIGINFO
:使用sa_sigaction
成员而不是sa_handler
作为信号处理函数。
SA_RESETHAND
: 处理完捕获的信号以后,信号处理回归到默认( 一次注册只生效一次)
SA_NODEFER
: 在信号处理函数执行期间,同一个信号设置可以再次被触发
SA_RESTART
:使被信号打断的系统调用自动重新调用。
EgCode: sa_handler函数作为处理函数
xxxxxxxxxx
121void func(int sig_value){
2printf("sig_value : %d \n", sig_value);
3}
4int main(int argc,char*argv[])
5{
6struct sigaction act, old;
7memset(&act , 0, sizeof(act));
8act.sa_handler = func;
9sigaction(SIGINT, &act, &old);
10while(1);
11return 0;
12}
EgCode: sa_sigaction函数作为处理函数
xxxxxxxxxx
131void func(int sig_vale, siginfo_t *info, void *p){
2printf("sending process id: %d \n",info->si_pid);
3}
4int main(int argc,char*argv[])
5{
6struct sigaction act, old;
7memset(&act , 0, sizeof(act));
8act.sa_sigaction = func;
9act.sa_flags = SA_SIGINFO;
10sigaction(SIGINT, &act, &old);
11while(1);
12return 0;
13}
EgCode: 使用sa_flags演示只注册一次
xxxxxxxxxx
131void func(int sig_value){
2printf("sig_value: %d \n", sig_value);
3}
4int main(int argc,char*argv[])
5{
6struct sigaction act, old;
7memset(&act , 0, sizeof(act));
8act.sa_handler = func;
9act.sa_flags = SA_RESETHAND;
10sigaction(SIGINT, &act, &old);
11while(1);
12return 0;
13}
EgCode: 使用sa_flags演示信号打断的系统调用(eg:read), 信号执行函数结束后自动重新调用
xxxxxxxxxx
211void func(int sig_value){
2printf("sig_value: %d \n", sig_value);
3}
4int main(int argc,char*argv[])
5{
6struct sigaction act, old;
7memset(&act , 0, sizeof(act));
8act.sa_handler = func;
9
10// 不设置SA_RESTART, 则下面的read在信号执行函数结束后不会重启
11// 设置SA_RESTART, 则下面的read在信号执行函数结束后会重启
12// act.sa_flags = SA_RESTART;
13
14sigaction(SIGINT, &act, &old);
15
16char buf[60] = {0};
17read(STDIN_FILENO, buf, sizeof(buf));
18puts(buf);
19
20return 0;
21}
EgCode: 使用sa_flags演示同一信号可以被再次触发
xxxxxxxxxx
171void func(int sig_value){
2printf("before: sig_value %d \n", sig_value);
3sleep(20);
4printf("after: sig_value %d \n", sig_value);
5}
6int main(int argc,char*argv[])
7{
8struct sigaction act, old;
9memset(&act, 0, sizeof(act));
10act.sa_handler = func;
11act.sa_flags = SA_NODEFER;
12
13sigaction(SIGINT, &act, &old);
14
15while(1);
16return 0;
17}
为了避免正在执行的信号处理函数被新的信号中断, 我们可以设置sa_mask参数(指定当前信号处理函数执行时需要阻塞的额外信号)来增加一些信号的阻塞操作。
sa_mask的sigset_t类型是一个位图
xxxxxxxxxx
161int sigaction(
2int signum, // 要操作的信号编号(除了不能捕获的SIGKILL和SIGSTOP)
3const struct sigaction *act,// 指定信号的新处理动作(如果非空)
4struct sigaction *oldact// 获取信号的上一个处理动作(如果非空)
5);
6struct sigaction {
7void (*sa_handler)(int);// 函数指针:指向一个信号处理函数 (和sa_sigaction选一个即可)
8void (*sa_sigaction)(int, siginfo_t *, void *);// 函数指针:指向一个接受三个参数的信号处理函数
9sigset_t sa_mask;// 信号集: 指定当前信号处理函数执行时需要阻塞的额外信号
10int sa_flags;// 指定信号处理的选项和标志:
11void (*sa_restorer)(void);// 过时,暂无用
12};
13
14typedef struct {
15unsigned long sig[_NSIG_WORDS];
16} sigset_t;
使用sa_mask: POSIX 定义了一系列的函数来操作
sigset_t
类型的变量 (man sigemptyset)xxxxxxxxxx
51sigemptyset(sigset_t *set):// 初始化信号集,清除所有信号。
2sigfillset(sigset_t *set):// 添加所有信号到信号集中。
3sigaddset(sigset_t *set, int signo):// 向信号集添加一个信号。
4sigdelset(sigset_t *set, int signo):// 从信号集中删除一个信号。
5sigismember(const sigset_t *set, int signo):// 检查一个特定信号是否在信号集中。
EgCode: 使用sa_mask阻塞信号(Eg: 在SIGINT/Ctrl+c信号处理函数执行的时候, 阻塞SIGQUIT/Ctrl+\ )
xxxxxxxxxx
241void func(int sig_value){
2printf("1: sig_value: %d \n", sig_value);
3sleep(20);
4printf("2: sig_value: %d \n", sig_value);
5}
6int main(int argc,char*argv[])
7{
8sigset_t sa_mask;
9sigemptyset(&sa_mask);
10// 阻塞所有信号
11//sigfillset(&sa_mask);
12// 阻塞SIGQUIT
13sigaddset(&sa_mask, SIGQUIT);
14
15struct sigaction act, old;
16memset(&act, 0, sizeof(act));
17act.sa_handler = func;
18act.sa_mask = sa_mask;
19
20sigaction(SIGINT, &act, &old);
21
22while(1);
23return 0;
24}
ps: 重提一下, 阻塞屏蔽和忽略信号有着截然不同的含义,内核会维护一个所有未决信号的位图,阻塞表示信号因为某种原因限制暂未执行,如果信号已经被阻塞,被阻塞的信号可能会在后续合适的时机被执行,而被忽略的信号则是直接被丢弃了。
sigpending函数用于检查当前进程的未决信号集,即那些已经发送给进程但由于某种原因(通常是因为被阻塞)尚未被处理的信号。这个函数可以用来确定哪些信号已经被产生并等待处理,但尚未被当前进程捕获或忽略。
xxxxxxxxxx
612// examine pending signals
3int sigpending(
4sigset_t *set // 接收当前进程的未决信号集
5);
6// 返回值: 成功时返回0,失败返回-1
EgCode:
xxxxxxxxxx
351void func(int sig_value){
2printf("before: %d \n", sig_value);
3sleep(20);
4
5sigset_t set;
6sigemptyset(&set);
7sigpending(&set);
8
9if(sigismember(&set, SIGQUIT)){
10printf("sigquit is pending \n");
11}
12if(sigismember(&set, SIGINT)){
13printf("sigint is pending \n");
14}
15// ...
16
17printf("after: %d \n", sig_value);
18}
19int main(int argc,char*argv[])
20{
21struct sigaction act, old;
22memset(&act, 0, sizeof(act));
23act.sa_handler = func;
24
25sigset_t set;
26sigemptyset(&set);
27sigfillset(&set);
28
29act.sa_mask = set;
30
31sigaction(SIGINT, &act, &old);
32
33while(1);
34return 0;
35}
sigprocmask函数用于在系统中检查和更改进程的信号屏蔽字(即信号掩码mask)。信号掩码确定了哪些信号可以递送给该进程,哪些信号被阻塞。和前面通过sigaction函数设置的sa_mask阻塞机制不同的是, sigaction函数设置的sa_mask阻塞是临时屏蔽。而sigprocmask函数修改信号掩码mask是全程屏蔽. (man sigprocmask)
xxxxxxxxxx
812// examine and change blocked signals
3int sigprocmask(
4int how, // 如何修改信号掩码. SIG_BLOCK:把set内信号添加阻塞; SIG_UNBLOCK:解除set内信号阻塞; SIG_SETMASK:将信号掩码替换为set指定信号
5const sigset_t *set, // 信号集合
6sigset_t *oldset // 当前信号掩码
7);
8// 返回值: 成功返回0, 失败返回-1
EgCode:
xxxxxxxxxx
151int main(int argc,char*argv[])
2{
3sigset_t set;
4sigemptyset(&set);
5sigfillset(&set);
6
7sigprocmask(SIG_BLOCK, &set, NULL);
8
9sleep(10);
10printf("block over \n");
11sigprocmask(SIG_UNBLOCK, &set, NULL);
12
13while(1);
14return 0;
15}
ps:闲聊:
假设存在这样一种场景,我们需要在进程中写入共享资源,自然就会采用加锁/解锁操作,如果这种写入过程十分重要,那么我们往往需要在加解锁之间屏蔽某些信号的递送。
我们之前的sigaction函数设置的sa_mask阻塞, 只能在某个信号处理过程中去阻塞另一个信号,另一种解决方案的实现思路是,在加锁的时候,将信号注册为忽略,在解锁的时候将信号注册为默认, 也就是使用上面的sigprocmask的方式阻塞信号。
系统调用kill函数可以用来给另一个进程发送信号。 (man 2 kill)
xxxxxxxxxx
8123// send signal to a process
4int kill(
5pid_t pid, //表示进程ID (另外 0:发送信号到与发送进程相同进程组的所有进程; -1:表示所有可以发送信号的进程发送信号; 小于-1:则根据其绝对值去关闭其作为组长的进程组)
6int sig // 信号数值
7);
8// 返回值: 成功0, 失败-1
EgCode:
xxxxxxxxxx
61int main(int argc,char*argv[])
2{
3//kill(getpid(), 3);
4kill(getpid(), SIGQUIT);
5return 0;
6}
pause函数作用是使调用进程挂起(即暂停执行),直到该进程捕获到一个信号。换句话说,pause函数让进程休眠,等待任何类型的信号到来;一旦接收到信号,如果有为该信号定义的处理函数,则执行该函数。如果没有为信号定义处理函数(或者信号的行为是默认的),进程会根据信号的默认行为来响应。
xxxxxxxxxx
312// wait for signal
3int pause(void);
EgCode:
xxxxxxxxxx
131void func(int sig_value){
2printf("sig_value: %d \n", sig_value);
3}
4int main(int argc,char*argv[])
5{
6signal(SIGINT, func);
7
8printf("before pause \n");
9pause();
10printf("after pause \n");
11
12return 0;
13}
ps: 闲聊
如果使用sigprocmask,可以实现所谓的信号保护临界区,在临界区当中执行代码的时候,此时产生的信号将会被阻塞,临界区结束的位置只需要再使用sigprocmask即可。
如果希望在临界区之后再次捕获信号,可以使用系统调用pause进行捕获, 即先使用sigprocmask解开信号阻塞,再调用pause函数, 使进程被阻塞, 等待信号到来, 进而捕捉信号。
但是需要注意的是, 如果是在临界区间产生了信号, 当使用sigprocmask解开信号阻塞时, 却会直接执行信号处理流程,无法使 sigprocmask之后的pause函数就绪。因为在解除阻塞后, 处于sigprocmask函数之后的pause函数,此时进程还未进入到pause的阻塞状态中。
所以为了捕获期间产生的信号, 一种策略就是将解除阻塞和等待信号合并成一个原子操作,这就是sigsuspend。
sigsuspend函数用于原子地更改进程的信号屏蔽字(block mask)并挂起进程执行,直到捕获到一个信号。 (man sigsuspend )
更改信号掩码和挂起进程之间提供原子操作
在sigsuspend返回后,进程的信号掩码会自动恢复到调用sigsuspend之前的状态
xxxxxxxxxx
512// wait for a signal
3int sigsuspend(
4const sigset_t *mask // 指定了在挂起期间要设置的新信号掩码
5);
EgCode:
xxxxxxxxxx
261void func1(int sig_value){
2printf("sigint \n");
3}
4void func2(int sig_value){
5printf("sigquit \n");
6}
7int main(int argc,char*argv[])
8{
9signal(SIGINT, func1);
10signal(SIGQUIT, func2);
11
12sigset_t set1, set2;
13sigemptyset(&set1);
14sigaddset(&set1, SIGINT);
15sigprocmask(SIG_BLOCK,&set1, NULL);
16
17sigemptyset(&set2);
18sigaddset(&set2, SIGQUIT);
19sigsuspend(&set2);
20
21while(1){
22sleep(1);
23printf(" -- \n");
24}
25return 0;
26}
alarm函数用于设置一个计时器(定时器),该计时器在指定的秒数后到期。当计时器到期时,内核会向该进程发送
SIGALRM
信号。如果程序没有捕获或忽略该信号,则其默认行为是终止进程。xxxxxxxxxx
312// set an alarm clock for delivery of a signal
3unsigned int alarm(unsigned int seconds);
EgCode:
xxxxxxxxxx
151void func(int sig_value){
2printf("sig_value: %d \n", sig_value);
3}
4int main(int argc,char*argv[])
5{
6//signal(SIGALRM, func);
7alarm(4);
8
9while(1){
10sleep(1);
11printf("-- \n");
12}
13return 0;
14}
15// ps: 注意的是每个进程只有一个闹钟时间,所以重复使用alarm会更新为新设置的超时时间,并且返回值会是原来闹钟时间的剩余秒数。
使用alarm和sigsuspend实现一个类似sleep的功能
EgCode:
xxxxxxxxxx
201void func(int sig_value){
2}
3void mySleep(int sec){
4signal(SIGALRM, func);
5
6alarm(sec);
7
8sigset_t set;
9sigemptyset(&set);
10sigsuspend(&set);
11}
12int main(int argc,char*argv[])
13{
14printf("-- \n");
15mySleep(10);
16printf("-- \n");
17mySleep(2);
18printf("-- \n");
19return 0;
20}
setitimer函数是一个高级定时器接口,相较于alarm函数,它提供了更多的灵活性和精度。 (man 2 getitimer)
xxxxxxxxxx
1612// get value of an interval timer
3int getitimer(
4int which, // 定时器的类型
5struct itimerval *curr_value //
6);
7
8struct itimerval {
9struct timeval it_interval;// 间隔时间: 字段被设置为非零值,定时器将变为周期性的
10struct timeval it_value; // 定时器的剩余时间
11// 当定时器的it_value达到0并触发信号后,it_value会被重新设置为 it_interval 的值,然后定时器再次开始计时
12};
13struct timeval {
14long tv_sec; /* seconds */
15long tv_usec; /* microseconds */
16};
xxxxxxxxxx
812// set value of an interval timer
3int setitimer(
4int which, // 定时器的类型
5const struct itimerval *new_value, // 指定的新的定时器值
6struct itimerval *old_value // 存储定时器的前一个值
7);
8// 返回值: 成功0, 失败-1
which参数:指定定时器的类型。常用的类型包括:
ITIMER_REAL
:按照真实时间, 当时间到达, 发出一个SIGALRM
信号。
ITIMER_VIRTUAL
:按照用户态代码执行时间计算, 当时间到达, 发出一个SIGVTALRM
信号。
ITIMER_PROF
:按照用户态用户态和内核态代码执行时间计算, 当时间到达, 发出一个SIGPROF
信号。
EgCode:
xxxxxxxxxx
291void func(int sig_value){
2printf("sig_value: %d \n", sig_value);
3}
4int main(int argc,char*argv[])
5{
6signal(SIGALRM, func);
7
8struct itimerval timer;
9memset(&timer, 0, sizeof(timer));
10
11timer.it_value.tv_sec = 10;
12timer.it_value.tv_usec = 0;
13
14timer.it_interval.tv_sec = 2;
15timer.it_interval.tv_usec = 0;
16
17setitimer(ITIMER_REAL, &timer, NULL);
18
19while(1){
20sleep(1);
21struct itimerval get_time;
22memset(&get_time, 0, sizeof(get_time));
23getitimer(ITIMER_REAL, &get_time);
24printf("this time remainder. sec: %ld; usec: %ld \n",
25get_time.it_value.tv_sec,
26get_time.it_value.tv_usec);
27}
28return 0;
29}