信号的概念
在进程运行过程中,经常会产生一些事件,这些事件的产生和进程的执行往往是异步的,信号提供了一种在软件层面上的异步处理事件的机制。在硬件层面上的异步处理机制是中断,信号是中断的软件模拟,是一种软件中断。每个信号用一个整型常量宏表示,以 SIG 开头,比如 SIGCHLD、SIGINT 等,它们在系统头文件<signal.h>中定义,也可以通过在 shell 下键入 kill –l 查看信号列表,或者键入 man 7 signal 查看更详细的说明。
需要注意的是:信号的目标总是进程
信号的产生
产生的信号的方法有很多种:
- 用户按下 ctrl+c 可以产生一个SIGINT中断信号。(这种信号的产生过程:首先硬件上触发一个中断,操作系统需要切换到内核态来执行中断处理程序,在中断处理程序当中会产生一个信号,再由进程处理信号的递送)
- 进程出现除0、访问异常的内存位置时会触发信号,这种信号往往是由硬件检测得到并产生的。
- 用户使用 kill 命令,或者进程调用 kill 系统调用也可以产生信号,这种产生信号方式对用户的身份有所限制。
- 进程也会收到由于软件检测得到并产生信号,比如网络传输异常SIGURG、管道异常SIGPIPE和时钟异常SIGALRM。
信号的处理
进程接收到信号以后,可以有如下 3 种选择进行处理:
- 接收默认处理:进程接收信号后以默认的方式处理。例如连接到终端的进程,用户按下 ctrl+c ,将导致内核向进程发送一个 SIGINT 的信号,进程如果不对该信号做特殊的处理,系统将采用默认的方式处理该信号( 对应的信号处理函数使用是 signal(SIGINT,SIG_DFL) )。默认处理有5种可能:Term表示终止进程、Ign表示忽略信号、Core表示终止进程并产生core文件、Stop表示暂停进程、Cont表示继续进程
- 忽略信号:进程可以通过代码,显示地忽略某个信号的处理。比如如果将SIGSEGV信号进行忽略(使用信号处理函数 signal(SIGSEGV,SIG_IGN) ),这是程序运行如果访问到空指针,就不会报错了。但是某些信号比如SIGKILL是不能被忽略的
- 捕捉信号并处理:进程可以事先注册信号处理函数,当接收到信号时,由信号处理函数自动捕捉并且处理信号有两个信号既不能被忽略也不能被捕捉,它们是 SIGKILL 和 SIGSTOP。即进程接收到这两个信号后,只能接受系统的默认处理,即终止进程。SIGSTOP 是暂停进程。
以下是Linux的所有信号:
信号名 | 信号值 | 产生原因 |
---|---|---|
SIGHUP |
1 | 挂起终端或终端连接中断(如用户注销或终端关闭) |
SIGINT |
2 | 用户通过键盘中断程序(通常是 Ctrl+C) |
SIGQUIT |
3 | 用户通过键盘退出程序(通常是 Ctrl+\) |
SIGILL |
4 | 执行了非法指令(如非法操作码或无效的指令) |
SIGTRAP |
5 | 跟踪陷阱(如调试器设置的断点) |
SIGABRT |
6 | 进程调用 abort() 函数(通常用于程序异常终止) |
SIGBUS |
7 | 总线错误(如非法内存访问) |
SIGFPE |
8 | 浮点异常(如除以零) |
SIGKILL |
9 | 强制终止进程(不能被捕捉或忽略) |
SIGUSR1 |
10 | 用户自定义信号 1(由程序或用户发送) |
SIGSEGV |
11 | 非法内存访问(如访问未分配的内存) |
SIGUSR2 |
12 | 用户自定义信号 2(由程序或用户发送) |
SIGPIPE |
13 | 向已关闭的管道或套接字写数据 |
SIGALRM |
14 | 由 alarm() 函数设置的定时器到期 |
SIGTERM |
15 | 软件终止信号(由程序或用户发送,用于请求程序正常退出) |
SIGSTKFLT |
16 | 栈故障(在某些架构中使用) |
SIGCHLD |
17 | 子进程终止或停止 |
SIGCONT |
18 | 恢复暂停的进程 |
SIGSTOP |
19 | 暂停进程 |
SIGTSTP |
20 | 用户通过键盘暂停程序(通常是 Ctrl+Z) |
SIGTTIN |
21 | 后台进程尝试从终端读取数据 |
SIGTTOU |
22 | 后台进程尝试向终端写入数据 |
SIGURG |
23 | 紧急数据到达(如套接字上的带外数据) |
SIGXCPU |
24 | 超过 CPU 时间限制 |
SIGXFSZ |
25 | 超过文件大小限制 |
SIGVTALRM |
26 | 虚拟时钟信号(由 setitimer() 设置的定时器到期) |
SIGPROF |
27 | 分析时钟信号(由 setitimer() 设置的定时器到期) |
SIGWINCH |
28 | 窗口大小改变 |
SIGIO |
29 | I/O 可用信号(如文件描述符准备就绪) |
SIGPWR |
30 | 电源故障或低电压警告(系统关闭前通知进程) |
SIGSYS |
31 | 非法系统调用 |
SIGRTMIN |
32 | 实时信号范围的起始值(用户自定义实时信号) |
SIGRTMAX |
64 | 实时信号范围的结束值(用户自定义实时信号) |
信号的实现机制
尽管信号有着多种产生来源,但是对于进程而言,信号的产生只不过是修改了内核的 task_struct 结构体的一些表示信号的成员,就是说,信号产生于内核。
当一个进程处于一个可以接受信号状态的时候(这种状态被称为响应时机),它会取得信号,并执行默认行为、忽略或者是自定义信号处理函数。因此信号的实现可以分为两个阶段,信号产生表示内核已知信号发生并修改内核数据结构的内容;信号递送表示内核执行信号处理流程。已经产生但是还没有传递的信号被称为挂起信号(pending signal)或者是未决信号。如果信号一直处于未决状态,那么就称进程阻塞了信号传递。
一个信号的产生对会对进程产生什么样的影响会由两个重要的位图来管理。位图 sigblock 集合(也叫做信号屏蔽字)决定了产生的信号是否会被阻塞;位图 sigpending 集合集中管理了所有的挂起信号。
由进程的某个操作产生的信号称为同步信号(synchronous signals),例如在代码中除 0;由像用户击键这样的进程外部事件产生的信号叫做异步信号(asynchronous signals)。同步和异步是编程当中一个非常重要的概念,同步表示事件之间的执行顺序是确定的,这种事件处理方式就更符合人类的认知习惯;异步表示事件之间的执行顺序是随机的,异步模式在使用良好的情况下更符合真实的物理世界,也能实现跟高的执行效率。目前,很多框架都是采用异步的方式实现底层请求处理,但是框架实现了良好的封装,这样程序员可以比较容易地用同步的方式编写程序代码,间接地使用异常方式提高运行效率。
用户使用自定义信号处理函数的主要目的就是实现进程的有序退出。如果没有实现进程的有序退出,就产生一些严重的运行事故,比如著名的“东京证券交易所系统宕机事件”。
信号注册
signal
signal 函数可以用来捕获信号并且指定对应的信号处理行为。它允许程序指定当接收到特定信号时应该执行的函数。signal
是较早期的信号处理机制,虽然它在现代编程中仍然被广泛使用,但它的行为不如 sigaction
函数精确和可靠。
#include <signal.h>
void (*signal(int sig, void (*handler)(int)))(int);
参数说明
1. sig
类型:
int
含义:指定要处理的信号编号。信号编号是系统定义的,例如:
SIGINT
:表示中断信号(通常是用户按下 Ctrl+C 产生的)。SIGTERM
:表示终止信号。SIGQUIT
:表示退出信号。SIGKILL
:表示杀死信号。SIGUSR1
和SIGUSR2
:用户自定义信号。更多信号可以在
<signal.h>
中找到。
2. handler
类型:
void (*handler)(int)
含义:信号处理函数的指针。当指定的信号发生时,程序将调用这个函数。
选项:
自定义处理函数:用户定义的函数,必须接受一个
int
类型的参数(表示信号编号)。SIG_DFL
:默认行为。系统将对信号采取默认的处理方式(例如,SIGINT
默认会导致程序终止)。SIG_IGN
:忽略信号。程序将忽略指定的信号,不会采取任何动作。
返回值
成功:返回之前设置的信号处理函数的指针。
失败:返回
SIG_ERR
(一个特殊的错误指针),并设置errno
以指示错误原因。
功能说明
signal
函数用于设置当程序接收到指定信号时应该执行的操作。它允许程序自定义信号处理函数,从而在信号发生时执行特定的逻辑。信号处理函数通常用于:
捕获中断信号(如
SIGINT
),以便程序可以优雅地退出。捕获终止信号(如
SIGTERM
),以便程序可以清理资源。捕获用户自定义信号(如
SIGUSR1
和SIGUSR2
),以便程序可以执行特定的逻辑。
示例:
#include <func.h>
void sigfunc(int signum){
printf("signum = %d is coming\n",signum);//signum表示信号的具体数值
}
int main()
{
signal(SIGINT, sigfunc);//将SIGINT信号的处理行为注册成sigfunc
printf("proces begin!\n");
while(1){
sleep(1);
}
return 0;
}
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./signal
proces begin!
^Csignum = 2 is coming
^Csignum = 2 is coming
^Csignum = 2 is coming
^Csignum = 2 is coming
^Csignum = 2 is coming
然后通过查阅进程号关闭该进程
(base) ubuntu@ubuntu:~$ ps -elf | grep signal
4 S root 1074 1 0 80 0 - 30320 - 7月17 ? 00:00:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
0 S ubuntu 55171 1512 0 80 0 - 723 do_wai 21:07 ? 00:00:00 /bin/sh -c dbus-monitor --session "type='signal',interface='org.gnome.ScreenSaver'" | while read x; do case "$x" in *"boolean true"*) echo SCREEN_LOCKED;; *"boolean false"*) echo SCREEN_UNLOCKED;; esac done
0 S ubuntu 55175 55171 0 80 0 - 1627 do_pol 21:07 ? 00:00:00 dbus-monitor --session type='signal',interface='org.gnome.ScreenSaver'
1 S ubuntu 55176 55171 0 80 0 - 723 pipe_r 21:07 ? 00:00:00 /bin/sh -c dbus-monitor --session "type='signal',interface='org.gnome.ScreenSaver'" | while read x; do case "$x" in *"boolean true"*) echo SCREEN_LOCKED;; *"boolean false"*) echo SCREEN_UNLOCKED;; esac done
0 S ubuntu 61504 61171 0 80 0 - 695 hrtime 22:24 pts/0 00:00:00 ./signal
0 S ubuntu 61573 35983 0 80 0 - 3081 pipe_r 22:24 pts/1 00:00:00 grep --color=auto signal
(base) ubuntu@ubuntu:~$ kill 61504
回调函数
回调函数的代码段是由用户自己编写的,它在内存中处于用户态的代码段当中,但是执行这部分代码和普通的函数调用有着显著的区别。
这里简单地描述一下信号递送的内核流程:
- 进程执行过程中调用了 signal 函数(或者随后要讲解的 sigaction 函数)为信号递送注册将要执行的回调函数。
- 进程执行过程由于各种原因从用户态切换到内核态(比如本进程调用系统调用、外界产生的硬件中断)
- 在处理完中断或者异常以后,进程将要恢复到用户态,此时需要检查一下 sigblock 和 sigpending ,检查是否存在可以被递送的信号产生。
- 如果内核发现进程 task_struct 的 sigpending 中存在任意一个可以被递送的信号(这个进程可以是不占用CPU的进程),这个时候内核决定不再返回原来进程,而是转而执行对应的回调函数。此时CPU处于中断上下文的状态,PC寄存器中存储了回调函数代码段的首地址,回调函数也拥有了和原进程执行流独立的用户态栈。
- 当回调函数执行完毕以后,CPU会切换回内核态,并且将上下文恢复为进程上下文,从而继续进程的正常执行。
- stdout对于进程上下文和中断上下文都是全局的,所以无论是进程正常执行还是回调函数执行都可以使用stdout。
我们同样可以注册多个信号:
void sigfunc(int signum){
printf("signum = %d is coming\n",signum);//signum表示信号的具体数值
}
int main(){
signal(SIGINT, sigfunc);//将SIGINT信号的处理行为注册成sigfunc
signal(SIGQUIT, SIG_IGN);
printf("proces begin!\n");
while(1){
sleep(1);
}
return 0;
}
此时无论是输入键盘中断还是键盘终止都无法退出进程了。
内核不可中断状态
处于内核不可中断状态(进程控制块的state成员此时为TASK_UNINTERRUPTIBLE)的进程无法接受并处理信号。处于这种的状态的进程在 ps 中显示为D,通常这种状态出现在进程必须不受干扰地等待或者等待事件很快会发生的时候出现,比如进程正在等待磁盘读写数据的时候。对于非嵌入式程序员而言,这种状态是几乎没办法实现。内核不可中断状态是进程等待态的一种形式。
多个信号处理同时执行
在使用函数 signal 时,如果进程收到一个信号,自然地就会进入信号处理的流程,如果在信号处理的过程中:
- 接受到了另一个不同类型信号。那么当前的信号处理流程是会被中断的,CPU会先转移执行新到来的信号处理流程,执行完毕以后再恢复原来信号的处理流程。
- 接受到了另一个相同类型信号。那么当前的信号处理流程是会不会被中断的,CPU会继续原来的信号处理流程,执行完毕以后再响应新来到的信号。
- 如果接受到了连续重复的相同类型的信号,后面重复的信号会被忽略,从而该信号处理流程只能再多执行一次。
示例:
void sigfunc(int signum){
printf("before signum = %d is coming\n",signum);
sleep(2);
printf("after signum = %d is coming\n",signum);
}
int main(){
signal(SIGINT, sigfunc);//将SIGINT信号的处理行为注册成sigfunc
signal(SIGQUIT, sigfunc);
printf("proces begin!\n");
while (1){
sleep(1);
}
return 0;
}
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./sig2
proces begin!
^Cbefore signum = 2 is coming #第一种情况
^\before signum = 3 is coming
after signum = 3 is coming
after signum = 2 is coming
^Cbefore signum = 2 is coming #第二种情况
^Cafter signum = 2 is coming
before signum = 2 is coming
after signum = 2 is coming
^Cbefore signum = 2 is coming #第三种情况
^C^C^Cafter signum = 2 is coming
before signum = 2 is coming
after signum = 2 is coming
已终止
当进程处于某个信号处理流程的时候,如果再产生一个同类型信号,信号处理流程不会中断(因为回调函数执行过程中,信号sigblock会将本信号对应位设置为1),而sigpending中的对应位会设置为1,表示此时存在一个挂起信号,随后产生的同类型信号将不再被记录。之所以设计同类信号无法中断,是考虑到信号处理流程可能会修改静态数据或者堆数据(比如stdout),这种函数被称为不可重入函数,如果中断处理流程,可能会导致进程破坏这些数据。
重新注册信号处理流程
如果希望在进程执行过程中重新指定信号的处理流程,可以多次使用 signal 函数。
int main(){
signal(SIGINT, SIG_IGN);
printf("proces begin!\n");
sleep(10);
printf("sleep over!\n");
signal(SIGINT, SIG_DFL);
while(1);
return 0;
}//当进程sleep结束以后,进程就可以接受到键盘中断了。
重启低速系统调用
假设目前在进程中注册了某个信号,此时进程正阻塞于某个设备或文件,当进程收到该信号时,这个阻塞的设备会被中断结束(又称低速系统调用),当处理完该信号后,signal会重启该低速系统调用,这是signal函数的一个特点,需要注意一下。
示例:
void sigfunc(int signum){
printf("signum = %d is coming\n",signum);
}
int main(){
signal(SIGINT,sigfunc);
char buf[128] = {0};
read(STDIN_FILENO,buf,sizeof(buf));
puts(buf);
return 0;
}
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./sig4
sdsad^Csignum = 2 is coming
sdsd
sdsd
函数 sigaction 注册信号
在 signal 处理机制下,在一些特殊的场景,它满足这样的行为:
- 注册一个信号处理函数,并且处理完毕一个信号之后,不需要重新注册,就能够捕捉下一个信号。
- 如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个同类型的信号,那么会依次处理信号,并且忽略多余的信号。
- 如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个不同类型的信号,那么会中断当前处理流程,跳转新信号的处理流程。
- 如果程序阻塞在一个系统调用(比如 read )时,产生一个信号,这时会有两种不同类型的行为,一种大多数系统调用的行为,例如读写磁盘文件时的等待,信号递送时会让系统调用返回错误再接着进入信号处理函数;另一种是先跳转到信号处理函数,等信号处理完毕后,再重新启动系统调用,这些系统调用往往是低速系统调用,比如读写管道、终端和网络设备,又比如 wait 和waitpid 等等。
显然如果使用 signal 函数,在这些场景下的执行流程是固定的并且无法调整的,而使用函sigaction 就可以自定义这些场景下进程的行为。
sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明
signum
:指定要设置或查询的信号编号,例如SIGINT
、SIGQUIT
等。act
:指向一个struct sigaction
结构,用于指定新的信号处理方式。如果为NULL
,则只查询当前信号的处理方式。oldact
:指向一个struct sigaction
结构,用于保存当前信号的旧处理方式。如果为NULL
,则不保存旧的处理方式。oldact参数表示之前的回调函数,通常会传入空指针,所以我们主要关心act参数。
返回值
成功时返回
0
。失败时返回
-1
,并设置errno
。
struct sigaction
结构
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
,则信号被忽略。如果设置为其他函数指针,则该函数将被调用以处理信号。
void handler(int signum);
sa_sigaction
:用于支持实时信号的处理函数。
如果需要使用
sa_sigaction
,必须在sa_flags
中设置SA_SIGINFO
标志。
void handler(int signum, siginfo_t *info, void *context);
signum
:信号编号。info
:指向siginfo_t
结构,包含关于信号的详细信息,例如发送信号的进程 ID、用户 ID、信号值等。context
:指向ucontext_t
结构,包含信号发生时的上下文信息(如寄存器状态等)。
sa_mask
:在信号处理函数执行期间需要屏蔽的信号集合。
可以通过
sigaddset
、sigdelset
和sigfillset
等函数来操作该集合。
sa_flags
:用于设置信号处理的标志。
常用标志:
SA_RESTART
:使被信号中断的系统调用自动重新启动。SA_NOCLDSTOP
:当子进程停止时,不发送SIGCHLD
信号给父进程。SA_SIGINFO
:启用sa_sigaction
,并传递额外的信号信息。表示选择sa_sigaction而不是sa_handler作为回调函数SA_ONSTACK
:在信号处理函数执行时使用备用栈(需要先设置备用栈)。SA_RESETHAND:处理完捕获的信号以后,信号处理回归到默认,使用情况较少
SA_NODEFER:解除所有阻塞行为。特别地,执行信号处理流程可以处理同类信号传递,按照栈的方式执行。
注意:
sa_handler
:适用于简单的信号处理,只有一个参数(信号编号)。sa_sigaction
:适用于需要更多信号信息的场景,有三个参数(信号编号、信号信息、上下文)。要使用
sa_sigaction
,必须在sa_flags
中设置SA_SIGINFO
标志。
示例:
void sigfunc(int signum, siginfo_t *p, void *p1){
//printf("%d is coming", signum);
printf("%d is coming\n", signum);//必须添加\n
}
int main()
{
struct sigaction act;
memset(&act,0,sizeof(act));
//act.sa_flags = SA_SIGINFO;
act.sa_flags = SA_SIGINFO|SA_RESETHAND; //这里设置SA_RESETHAND后,按下键盘ctrl+c一次后不再接收该信号进行处理
act.sa_sigaction = sigfunc;
int ret = sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
while(1){
sleep(1);
}
return 0;
}
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./sig3
^C2 is coming
^C
重启系统低速调用
相较于之前的signal函数,sigaction在处理完信号后默认不会重启之前阻塞的系统调用,这会在处理完信号后报错。
示例:
void sigfunc(int signum, siginfo_t *p, void *p1){
printf("%d is coming\n", signum);//必须添加\n
}
int main()
{
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = sigfunc;
int ret = sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
char buf[128] = {0};
ret = read(STDIN_FILENO,buf,sizeof(buf));
ERROR_CHECK(ret,-1,"read1");
return 0;
}//会报错read: Interrupted system call
增加SA_RESTART可以自动重启低速系统调用。
void sigfunc(int signum, siginfo_t *p, void *p1){
printf("%d is coming\n", signum);//必须添加\n
}
int main()
{
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_flags = SA_SIGINFO | SA_RESTART;
act.sa_sigaction = sigfunc;
int ret = sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
char buf[128] = {0};
ret = read(STDIN_FILENO,buf,sizeof(buf));
ERROR_CHECK(ret,-1,"read1");
return 0;
}
sigaddset 设置阻塞集合
在信号处理流程中,如果递送了新的不同类型信号,在没有指定SA_NODEFER参数的情况下,新信号将会中断正在执行的信号处理流程。为了避免这种中断行为,可以使用sa_mask参数来增加一些信号的阻塞操作,该操作类似于计算机组成原理中设置中断屏蔽字。
typedef struct{
unsigned long int __val[(1024 / (8 * sizeof (unsigned long int)))];
} __sigset_t;
typedef __sigset_t sigset_t;
//sigset_t的本质就是一个位图,共有1024位
需要使用的函数:
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化信号集,清除所有信号
int sigfillset(sigset_t *set);
//初始化信号集,包括所有信号
int sigaddset(sigset_t *set, int signum);
//增加信号
int sigdelset(sigset_t *set, int signum);
//删除信号
int sigismember(const sigset_t *set, int signum);
//检查信号处于信号集之中
示例:
void sigfunc(int signum, siginfo_t *p, void *p1){
printf("before %d is coming\n", signum);
sleep(3);
printf("after %d is coming\n", signum);
}
int main(){
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = sigfunc;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT); //SIGQUIT信号被屏蔽
int ret = sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
ret = sigaction(SIGQUIT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
while(1){
sleep(1);
}
return 0;
}
(base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./sig7
^Cbefore 2 is coming
^\after 2 is coming
before 3 is coming
after 3 is coming
已终止
SIGQUIT信号被屏蔽,待SIGINT信号完成处理后再响应
需要特别注意的是阻塞屏蔽和忽略信号有着既然不同的含义,阻塞表示信号产生了但是还未递送,内核会维护一个所有未决信号的位图,如果信号已经被阻塞,再次产生信号就会被忽略了。被阻塞的信号将会后续执行,而被忽略的信号就被丢弃了。
sigpending 查看等待集合
使用系统调用 sigpending 可以获取当前所有未决信号(已经产生没有递送的信号)的集合。通常这个系统调用是在回调函数当中使用的,用于检查当前是否阻塞了某个信号。
#include <signal.h>
int sigpending(sigset_t *set);
参数说明
set
:指向一个sigset_t
类型的变量,用于存储当前进程中正在等待处理的信号集合。调用sigpending
时,该变量会被填充为当前进程的待处理信号集合。
返回值
成功时返回
0
。失败时返回
-1
,并设置errno
。
使用方法
1. 初始化信号集
在使用 sigpending
之前,通常需要初始化一个信号集,并将其传递给 sigpending
函数。信号集可以通过 sigemptyset
、sigfillset
、sigaddset
和 sigdelset
等函数来操作。
2. 调用 sigpending
调用 sigpending
函数,将当前进程中正在等待处理的信号集合存储到指定的信号集中。
3. 检查信号集
通过检查信号集的内容,可以了解哪些信号正在等待处理。
示例:
#include<54func.h>
void sigfunc(int signum, siginfo_t *p, void *p1){
printf("before %d is coming\n", signum);
sleep(3);
sigset_t pendingSet;
sigpending(&pendingSet);
if(sigismember(&pendingSet,SIGQUIT)){
printf("SIGQUIT is pending!\n");
}
else{
printf("SIGQUIT is not pending!\n");
}
printf("after %d is coming\n", signum);
}
int main(){
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = sigfunc;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);
int ret = sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
ret = sigaction(SIGQUIT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
while(1){
sleep(1);
}
return 0;
}
输出结果:
base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./sig8
^Cbefore 2 is coming
^\SIGQUIT is pending!
after 2 is coming
before 3 is coming
SIGQUIT is not pending!
after 3 is coming
sigprocmask 实现全程阻塞
假设存在这样一种场景,我们需要在进程中写入共享资源,自然就会采用加锁/解锁操作,如果这种写入过程十分重要,那么我们往往需要在加解锁之间屏蔽某些信号的递送。我们之前的阻塞操作只能在某个信号处理过程中去阻塞另一个信号,另一种解决方案的实现思路是,在加锁的时候,将信号注册为忽略,在解锁的时候将信号注册为默认。但是这样的实现方法并不是阻塞信号而是忽略信号,在访问共享资源过程中产生的信号就被丢弃掉了。
使用系统调用 sigprocmask 可以实现全程阻塞的效果。它可以检测或者更改信号屏蔽字的内容。参数 how 描述了如何修改;参数set指向了信号集;如果oldset非空时,则会返回当前信号屏蔽字。
#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
类型的变量,表示要修改的信号集。如果为NULL
,则不修改信号掩码,但可以查询当前信号掩码。
oldset
:指向一个
sigset_t
类型的变量,用于保存当前信号掩码的旧值。如果为NULL
,则不保存旧值。
返回值
成功时返回
0
。失败时返回
-1
,并设置errno
。
使用方法
1. 初始化信号集
在使用 sigprocmask
之前,通常需要初始化一个信号集,并将其传递给 sigprocmask
函数。信号集可以通过 sigemptyset
、sigfillset
、sigaddset
和 sigdelset
等函数来操作。
2. 调用 sigprocmask
调用 sigprocmask
函数,根据 how
参数的值来修改或查询信号掩码。
3. 检查或修改信号掩码
通过检查或修改信号掩码,可以控制哪些信号被阻塞或取消阻塞。
int main()
{
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask,SIGINT);
int ret = sigprocmask(SIG_BLOCK,&mask,NULL);
ERROR_CHECK(ret,-1,"sigprocmask");
printf("block success!\n");
sleep(3);
ret = sigprocmask(SIG_UNBLOCK,&mask,NULL);
ERROR_CHECK(ret,-1,"sigprocmask");
while(1);
return 0;
}
sigpending 可以和 sigprocmask 之间配合使用:
int main(){
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask,SIGINT);
int ret = sigprocmask(SIG_BLOCK,&mask,NULL);
ERROR_CHECK(ret,-1,"sigprocmask");
printf("block success!\n");
sleep(3);
sigset_t pend;
sigemptyset(&pend);
sigpending(&pend);
if(sigismember(&pend,SIGINT)){
printf("SIGINT is pending!\n");
}
else {
printf("SIGINT is not pending!\n");
}
ret = sigprocmask(SIG_UNBLOCK,&mask,NULL);
ERROR_CHECK(ret,-1,"sigprocmask");
while(1);
return 0;
}
kill
系统调用 kill 可以用来给另一个进程发送信号。pid参数表示进程ID,sig参数表示信号数值,pid如果是-1,表示给所有可以发送信号的进程发送信号,如果小于-1,则根据其绝对值,去关闭其作为组长的进程组。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数说明
pid
:指定要发送信号的目标进程或进程组的 ID。
可以取以下值:
正数:表示向指定的进程发送信号。
0:表示向当前进程所属的进程组中的所有进程发送信号。
负数:表示向指定的进程组发送信号(绝对值为进程组 ID)。
-1:表示向调用进程的进程组中的所有进程发送信号(不包括调用进程本身)。
sig
:指定要发送的信号编号。可以是标准信号(如
SIGINT
、SIGTERM
等)或实时信号(如SIGRTMIN
、SIGRTMAX
等)。如果
sig
为0
,则不发送信号,但仍然会检查目标进程是否存在以及调用进程是否有权限向目标进程发送信号。
返回值
成功时返回
0
。失败时返回
-1
,并设置errno
。
示例:
int main(int argc, char *argv[]){
// ./kill -9 pid
ARGS_CHECK(argc,3);
int sig = atoi(argv[1]+1);
pid_t pid = atoi(argv[2]);
int ret = kill(pid,sig);
ERROR_CHECK(ret,-1,"kill");
return 0;
}
pause
pause 系统调用可以用来阻塞一个进程,直到某个信号被递送时,进程会解除阻塞,然后终止进程或者执行信号处理函数。
#include <unistd.h>
int pause(void);
功能
pause()
函数的作用是让调用它的进程暂停执行,直到该进程接收到一个信号为止。当进程接收到信号后,pause()
函数会返回,并且进程会继续执行。
参数
pause()
函数没有参数。
返回值
如果进程因为接收到信号而从
pause()
函数返回,那么pause()
的返回值是-1
。pause()
函数不会返回其他值,因为它总是因为信号中断而返回。
使用场景
pause()
函数通常用于以下场景:
等待信号:当进程需要等待某个信号到来时,可以使用
pause()
函数。例如,一个进程可能需要等待用户输入信号(如SIGINT
或SIGQUIT
)来执行某些操作。简化信号处理逻辑:在某些情况下,
pause()
可以简化信号处理逻辑,尤其是当进程的主要任务是等待信号并响应时。
void handle_signal(int signum) {
printf("Received signal: %d\n", signum);
}
int main() {
// 注册信号处理函数
signal(SIGINT, handle_signal); // 捕获 Ctrl+C 信号
signal(SIGQUIT, handle_signal); // 捕获 Ctrl+\ 信号
printf("Process is waiting for a signal...\n");
// 调用 pause() 函数,进程将暂停执行
pause();
printf("Process resumed after receiving a signal.\n");
return 0;
}
sigsuspend
sigsuspend
用于在等待信号的同时临时更改进程的信号掩码的函数。它比 pause()
函数更灵活,因为它允许进程在等待信号时动态调整信号掩码,从而更精细地控制信号的接收和处理。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
参数
mask
:指向sigset_t
类型的指针,表示在调用sigsuspend()
期间要使用的信号掩码。
返回值
成功:如果进程因为接收到信号而从
sigsuspend()
函数返回,返回值是-1
,并且errno
被设置为EINTR
。失败:如果
mask
参数无效,返回值是-1
,并且errno
被设置为EINVAL
。
sigsuspend()
函数的作用是:
设置信号掩码:将进程的信号掩码临时替换为参数
mask
指向的信号集。等待信号:使进程暂停执行,直到接收到一个未被屏蔽的信号为止。
恢复信号掩码:当进程接收到信号后,
sigsuspend()
函数会返回,并且进程的信号掩码会恢复到调用sigsuspend()
之前的值。
示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
// 信号处理函数
void handle_signal(int signum) {
printf("Received signal: %d\n", signum);
}
int main() {
sigset_t mask, oldmask;
// 初始化信号掩码
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // 屏蔽 SIGINT 信号
sigaddset(&mask, SIGQUIT); // 屏蔽 SIGQUIT 信号
// 保存当前信号掩码
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) < 0) {
perror("sigprocmask");
return 1;
}
// 注册信号处理函数
signal(SIGINT, handle_signal); // 捕获 Ctrl+C 信号
signal(SIGQUIT, handle_signal); // 捕获 Ctrl+\ 信号
printf("Process is waiting for a signal...\n");
// 调用 sigsuspend(),传入新的信号掩码
if (sigsuspend(&oldmask) < 0) {
if (errno != EINTR) {
perror("sigsuspend");
return 1;
}
}
printf("Process resumed after receiving a signal.\n");
// 恢复信号掩码
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
perror("sigprocmask");
return 1;
}
return 0;
}
alarm
alarm
允许进程在指定的时间后接收一个 SIGALRM
信号。这个信号可以用于实现超时机制、定时任务等功能。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数
seconds
:指定定时器的超时时间,单位为秒。如果设置为0
,则会取消当前的定时器。
返回值
返回值:
alarm()
函数返回之前设置的定时器的剩余时间(单位为秒)。如果之前没有设置定时器,则返回0
。
功能
alarm()
函数的作用是:
设置定时器:在指定的
seconds
秒后,向进程发送一个SIGALRM
信号。覆盖之前的定时器:如果在调用
alarm()
时已经有一个定时器在运行,新的调用会覆盖之前的定时器设置。
示例:
void handle_alarm(int signum) {
printf("Alarm signal received!\n");
}
int main() {
// 注册 SIGALRM 信号的处理函数
signal(SIGALRM, handle_alarm);
// 设置定时器,5 秒后发送 SIGALRM 信号
printf("Setting alarm for 5 seconds...\n");
alarm(5);
// 主循环
while (1) {
printf("Waiting for alarm...\n");
sleep(1); // 每秒打印一次
}
return 0;
}
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/signal$ ./alarm
Setting alarm for 5 seconds...
Waiting for alarm...
Waiting for alarm...
Waiting for alarm...
Waiting for alarm...
Waiting for alarm...
Alarm signal received!
时钟处理
setitimer 系统调用负责调整间隔定时器。间隔定时器在创建的时候,就会设置一个时间间隔,定时器到达时间间隔时,调用进程会产生一个信号,随后定时器被重置。
操作系统为每个进程维护3种不同的定时器,分别是真实计时器、虚拟计时器和实用计时器。真实计时器会记录真实的时间(也就是时钟时间),当时间到时,会产生一个SIGALRM信号。虚拟计时器会记录用户态模式下的CPU时间,当时间到的时候,会产生一个SIGVTALRM信号。实用计时器会记录用户态以及内核态模式下的CPU时间,当时间到的时候,会产生一个SIGPROF信号。
使用 fork 的时候子进程不会继承父进程的定时器,使用 exec 时候,定时器不会销毁。不要混用
setitimer 、 sleep 、 usleep 和 alarm 。
setitimer
setitimer()
函数是一个更强大的定时器设置函数,它允许设置高精度的定时器,并且可以设置定时器为一次性触发或周期性触发。与 alarm()
函数相比,setitimer()
提供了更高的精度(可以精确到微秒)和更灵活的定时器选项。
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数
which
:指定定时器类型,可以是ITIMER_REAL
、ITIMER_VIRTUAL
或ITIMER_PROF
。new_value
:指向struct itimerval
的指针,表示新的定时器值。old_value
:指向struct itimerval
的指针,用于保存当前定时器的值。如果不需要保存当前值,可以设置为NULL
。
struct itimerval
结构
struct itimerval
的定义如下:
struct itimerval {
struct timeval it_interval; // 间隔时间(周期性定时器)
struct timeval it_value; // 初始时间(一次性定时器)
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
返回值
成功:返回
0
。失败:返回
-1
,并设置errno
。
功能
setitimer()
函数用于设置或查询定时器。它支持三种类型的定时器:
ITIMER_REAL
:递减实际时间(wall-clock time),当定时器到期时,发送SIGALRM
信号。ITIMER_VIRTUAL
:递减进程占用 CPU 的时间,当定时器到期时,发送SIGVTALRM
信号。ITIMER_PROF
:递减进程占用 CPU 的时间和系统调用的时间,当定时器到期时,发送SIGPROF
信号。
示例:
void sigfunc(int signum){
time_t now;
time(&now);
printf("%s\n",ctime(&now));
}
int main(){
struct sigaction act;
act.sa_handler = sigfunc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM,&act,NULL);
struct itimerval timer;
timer.it_value.tv_sec = 1;
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 3;
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
sigfunc(0);
//sleep(2);sleep对结果没有影响
while(1){
sleep(1);
}
}
与 alarm()
的区别
alarm()
:只能设置一次性定时器。
精度为秒级。
只能发送
SIGALRM
信号。
setitimer()
:可以设置一次性或周期性定时器。
精度可以精确到微秒。
支持三种类型的定时器(
ITIMER_REAL
、ITIMER_VIRTUAL
、ITIMER_PROF
)。可以发送不同的信号(
SIGALRM
、SIGVTALRM
、SIGPROF
)。
注意事项
信号处理函数:必须注册信号处理函数来处理定时器到期时发送的信号,否则进程可能会因为未处理的信号而终止。
高精度定时器:
setitimer()
提供的定时精度可以精确到微秒,适用于需要高精度定时的场景。多线程环境:
setitimer()
会作用于整个进程,而不是单个线程。如果需要线程级别的定时器,可以使用timer_create()
。取消定时器:要取消定时器,可以将
it_value
设置为0
,并调用setitimer()
。