信号
信号的概念
基本概念
- 信号是发生事件时对进程的一种通知机制
信号的目的是用来通信的
-
硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程
-
用于在终端下输入了能够产生信号的特殊字符
-
进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组
-
用户可以通过 kill 命令将信号发送给其它进程
-
发生了软件事件,即当检测到某种软件条件已经发生
信号由谁处理、怎么处理?
-
忽略信号
- SIGKILL 和 SIGSTOP,这两种信号不能被忽略
-
捕获信号
- 当信号传递给进程时,系统会执行预先设定的信号处理函数。为了实现这一功能,需要通Linux提供的signal()系统调用,向内核注册一个自定义的处理函数。当特定信号发生时,内核将调用这个处理函数来响应信号事件。
-
执行系统默认操作
- 交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,大多数信号,系
统默认的处理方式就是终止该进程
- 交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,大多数信号,系
信号是异步的
- 信号是进程无法预测的随机异步事件,类似于硬件中断,程序不能通过常规方法检测信号的发生。只有当信号实际发生时,才会通知程序并中断其正常执行,转而执行特定的中断服务函数
信号本质上是int类型的数字编号
-
信号本质上是整数编号,类似于硬件中断号,每个信号由内核分配一个唯一的整数编号,从1开始。每个信号还有一个对应的名称(宏定义),名称与编号一一对应。由于不同系统中信号的实际编号可能不同,因此在编程中通常使用信号的符号名(宏定义)
-
这些信号在<signum.h>头文件中定义,每个信号都是以 SIGxxx 开头
信号的分类
Linux信号机制基本是从UNIX系统中继承过来的
-
早期UNIX系统的信号机制比较简单和原始,在实践中暴露出了一些问题
-
进程每次处理信号后,就将对信号设置为了系统默认操作
-
信号可能丢失
-
可靠信号与不可靠信号
-
不可靠信号(1-31)
- Linux下的不可靠信号的不可靠问题主要指的是信号可能会丢失
-
可靠信号(34-64)
- 可靠信号支持排队,不会丢失
-
在 Linux 系统下使用"kill -l"命令可查看到所有信号
实时信号与非实时信号
-
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号
-
非实时信号(不可靠信号)称为标准信号
-
实时信号是 POSIX 标准的一部分,可用于应用进程
常见信号(标准信号)介绍
SIGINT中断信号
- 通常是 CTRL + C
SIGQUIT退出信号
- 通常是 CTRL + \
SIGILL
- 检测非法(即格式不正确)的机器语言指令
SIGABRT
- 当进程调用 abort()系统调用时(进程异常止),系统会向该进程发送 SIGABRT 信号。该信号的系统默认操作是终止进程、并生成核心转储文件。
SIGBUS
- 产生该信号(总线错误,bus error)表示发生了某种内存访问错误
SIGFPE
- 因特定类型的算术错误而产生,譬如除以 0
SIGKILL"必杀(sure kill)"信号
-
用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而"一击必杀",总能终止进程
-
使用"kill -9 xxx"命令 来终止一个进程(xxx 表示进程的 pid)
-
通常作为终止进程的最后手段
。。。。。。
- term 表示终止进程;core 表示生成核心转储文件,核心转储文件可用于调试;ignore 表示忽略信号;cont 表示继续运行进程;stop 表示停止进程(注意停止不等于终止,而是暂停)
进程对信号的处理
signal()函数
-
signal()是系统调用,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作
-
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
-
signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名
-
handler:sig_t类型的函数指针,它指向一个信号处理函数,该函数在进程接收到特定信号时自动执行。handler参数可以设置为用户定义的函数,用于处理捕获的信号,或者设置为SIG_IGN(忽略信号)和SIG_DFL(使用系统默认操作)。sig_t函数指针的int类型参数指示触发函数的信号,允许将多个信号绑定到同一处理函数,并通过该参数识别具体触发的是哪个信号。SIG_IGN和SIG_DFL的值分别定义为1(忽略信号)和0(默认操作)
- Tips:SIG_IGN、SIG_DFL 分别取值如下:
/* Fake signal functions. /
#define SIG_ERR ((sig_t) -1) / Error return. /
#define SIG_DFL ((sig_t) 0) / Default action. /
#define SIG_IGN ((sig_t) 1) / Ignore signal. */
- Tips:SIG_IGN、SIG_DFL 分别取值如下:
-
返回值:此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno
sigaction()函数
-
sigaction()是一个系统调用,用于设置信号处理方式,相比于signal()函数,它更受推荐。尽管signal()简单易用,但sigaction()提供了更高的灵活性和移植性。sigaction()不仅允许设置信号处理函数,还能单独获取这些函数,并能精细控制信号处理函数被调用时的行为。
-
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号
-
act:指向struct sigaction的指针,用于描述信号的处理方式。如果act不为NULL,则设置新的信号处理方式;若为NULL,则保持当前处理方式不变
-
oldact:指向struct sigaction,用于返回信号之前的处理方式。如果不需要获取这些信息,可以将oldact设置为NULL
-
返回值:成功返回 0;失败将返回-1,并设置 errno
-
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:指定信号处理函数,与 signal()函数的 handler 参数相同
-
sa_sigaction:一个替代的信号处理函数,提供更多参数,如siginfo_t,用于获取更详细的信息。sa_handler和sa_sigaction互斥,不能同时设置,通常使用sa_handler,但可通过设置SA_SIGINFO标志选择使用sa_sigaction
-
sa_mask:定义一组信号,在执行信号处理函数前,这些信号会被添加到进程的信号掩码中,执行完毕后移除。这防止了在处理信号期间被其他信号中断,避免竞态条件
-
sa_restorer:已过时,不应再使用
-
sa_flags:控制信号处理过程的标志集合
- SA_NOCLDSTOP:对于SIGCHLD信号,子进程停止或恢复时不发送此信号。
SA_NOCLDWAIT:对于SIGCHLD信号,子进程终止时不变为僵尸进程。
SA_NODEFER:在信号处理函数中不阻塞当前信号。
SA_RESETHAND:信号处理函数执行后,信号处理方式重置为默认。
SA_RESTART:被信号中断的系统调用自动重新发起。
SA_SIGINFO:使用sa_sigaction作为信号处理函数,提供更多参数信息
- SA_NOCLDSTOP:对于SIGCHLD信号,子进程停止或恢复时不发送此信号。
-
向进程发送信号
kill()函数
-
将信号发送给指定的进程或进程组中的每一个进程
-
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig)
-
pid
-
⚫ 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
⚫ 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。
⚫ 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
⚫ 如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。
- 只有超级用户(root)可以向任何进程发送信号,而普通用户只能向与其用户ID相同的进程发送信号。这确保了系统中信号传递的安全性和权限控制。
-
-
sig:参数 sig 指定需要发送的信号
- 当信号编号sig设置为0时,kill()函数可以用于检查进程是否存在而不实际发送信号。如果尝试发送信号给不存在的进程,kill()将返回-1,并且errno将被设置为ESRCH,表明进程不存在。
-
返回值:成功返回 0;失败将返回-1,并设置 errno
-
raise()函数
-
进程向自身发送信号
-
#include <signal.h>
int raise(int sig);
-
sig:需要发送的信号
-
返回值:成功返回 0;失败将返回非零值
-
-
raise()其实等价于
-
kill(getpid(), sig);
-
getpid()函数用于获取进程自身的 pid
-
alarm()和 pause()函数
alarm()函数
-
设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM
信号
-
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟
-
返回值:如果调用alarm()时已有未超时的闹钟,则返回其剩余时间,并由新闹钟替代;若无未超时闹钟,则返回0
-
-
alarm()函数用于设置一个定时器,经过参数seconds指定的秒数后,内核会产生SIGALRM信号。每个进程只能有一个闹钟,默认情况下SIGALRM信号会终止进程,但通常进程会捕获此信号进行处理。
-
alarm闹钟是一次性的,若要实现周期性触发,需在SIGALRM信号处理函数中重新调用alarm()设置定时器。
pause()函数
-
系统调用使进程进入休眠状态,暂停执行,直到捕获到一个信号。当信号处理函数执行完毕并返回后,pause()才会返回,此时返回值为-1,并将errno设置为EINTR
-
#include <unistd.h>
int pause(void);
信号集(signal set)
信号集(signal set)是一种数据类型,用于表示一组信号。它通常使用sigset_t结构体来实现。许多系统调用,如sigaction()、sigprocmask()和sigpending(),都使用信号集作为参数。Linux提供了一些API来操作sigset_t信号集,包括sigemptyset()(清空信号集)、sigfillset()(填充所有信号到信号集)、sigaddset()(添加信号到信号集)、sigdelset()(从信号集中删除信号)和sigismember()(检查信号是否在信号集中)
define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
初始化信号集
-
sigemptyset()和 sigfillset()用于 初始化信号 集。 sigemptyset()初始 化信号集 ,使其不 包含任何信 号;而
sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号)
-
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set)
-
set:指向需要进行初始化的信号集变量
-
返回值:成功返回 0;失败将返回-1,并设置 errno
-
向信号集中添加/删除信号
-
sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号
-
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
-
set:指向信号集
-
signum:需要添加/删除的信号
-
返回值:成功返回 0;失败将返回-1,并设置 errno
-
测试信号是否在信号集中
-
sigismember()函数可以测试某一个信号是否在指定的信号集中
-
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
-
set:指定信号集
-
signum:需要进行测试的信号
-
返回值:如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回- 1,并设置 errno
-
获取信号的描述信息
直接使用 sys_siglist 数组获取描述信息
-
在Linux系统中,每个信号都有一个对应的字符串描述信息,用于描述该信号。这些描述信息存储在sys_siglist数组中,该数组是一个char*类型的数组,每个元素是一个指向描述特定信号的字符串的指针。
-
譬如,可以使用 sys_siglist[SIGINT]来获取对 SIGINT 信号的描述
- printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
使用 strsignal()函数(推荐)
-
#include <string.h>
char *strsignal(int sig);
-
获取到参数 sig 指定的信号对应的描述信息,返回该描述信息字符串的指针
-
函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息
-
printf("SIGINT 描述信息: %s\n", strsignal(SIGINT));
在标准错误(stderr)上输出信号描述信息
-
#include <signal.h>
void psignal(int sig, const char *s);
-
将参数 sig 指定的信号对应的描述信息输出到标准错误,并且还允许调用者添加一些输出信息,由参数 s 指定;所以整个输出信息由字符串 s、冒号、空格、描述信号编号 sig 的字符串和尾随的换行符组成
-
psignal(SIGINT, "SIGINT 信号描述信息");
信号掩码(阻塞信号传递)
信号掩码的概念
- 内核为每个进程维护一个信号掩码,这是一个信号集,用于定义一组阻塞的信号。当进程接收到信号掩码中包含的信号时,该信号会被暂时阻塞,不会立即传递给进程处理。只有当该信号从信号掩码中移除后,内核才会解除阻塞,将信号传递给进程进行处理
向信号掩码中添加信号的几种方式
-
使用signal()或sigaction()函数设置信号处理方式时,进程会自动将该信号添加到信号掩码中,以阻塞同一信号的再次发生。对于sigaction(),是否添加取决于是否设置了SA_NODEFER标志。信号处理函数返回后,该信号会自动从信号掩码中移除
-
使用sigaction()函数设置信号处理方式时,可以通过sa_mask参数额外指定一组信号,在调用信号处理函数时自动添加到信号掩码中,并在函数返回后移除
-
使用sigprocmask()系统调用,可以随时显式地向信号掩码中添加或移除信号
sigprocmask()函数
-
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
how:参数 how 指定了调用函数时的一些行为
- ⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与 set 的并集。
⚫ SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
⚫ SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。
- ⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与 set 的并集。
-
set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为
NULL,则表示无需对当前信号掩码作出改动
-
oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,
存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码
-
返回值:成功返回 0;失败将返回-1,并设置 errno
阻塞等待信号 sigsuspend()
#include <signal.h>
int sigsuspend(const sigset_t *mask);
mask:指向一个信号集
返回值:sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT
sigsuspend()函数用于临时替换进程的信号掩码为参数mask指定的信号集,并挂起进程。进程会一直挂起,直到捕获到一个非mask信号集中成员的信号,并从对应的信号处理函数返回。一旦信号处理函数返回,sigsuspend()会自动将信号掩码恢复到调用前的状态。这个过程是不可中断的原子操作
实时信号
sigpending()函数
-
确定进程中处于等待状态的是哪些信号
-
#include <signal.h>
int sigpending(sigset_t *set);
-
set:处于等待状态的信号会存放在参数 set 所指向的信号集中
-
返回值:成功返回 0;失败将返回-1,并设置 errno
实时信号的编号
- Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64,使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数
实时信号的优势
-
实时信号的范围比标准信号更广,允许应用程序自定义使用,而标准信号中只有SIGUSR1和SIGUSR2可供自定义
-
内核对实时信号采用队列化管理,同一实时信号的多次发送会导致多次传递。相比之下,标准信号在同一时间只会传递一次,即使多次发送
-
发送实时信号时,可以为信号指定伴随数据(整数或指针),接收进程可以在信号处理函数中获取这些数据
-
不同实时信号的传递顺序有保障,编号越小的信号优先级越高。对于同一类型的多个信号,传递顺序与发送顺序一致
如何使用实时信号
-
需要满足两点要求
-
发送进程需要使用sigqueue()系统调用向另一个进程发送实时信号及其伴随数据
-
接收实时信号的进程必须为该信号设置一个信号处理函数,通过sigaction()函数并设置SA_SIGINFO标志来实现。这样,信号处理函数需要使用sa_sigaction指针指向的函数,而不是sa_handler,以确保能够接收并处理实时信号及其伴随数据。如果使用sa_handler,则无法获取伴随数据
-
-
使用 sigqueue()函数发送实时信号
-
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
-
pid:指定接收信号的进程对应的 pid,将信号发送给该进程
-
sig:指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在
-
value:参数 value 指定了信号的伴随数据,union sigval 数据类型
- typedef union sigval
{
int sival_int;
void *sival_ptr;
} sigval_t;
- typedef union sigval
-
返回值:成功将返回 0;失败将返回-1,并设置 errno
-
abort异常终止进程
进程的终止分为两类
-
正常终止
-
在main函数中通过return语句退出程序
-
在程序当中调用库函数或者系统调用终止进程,譬如exit()、_Exit()、_exit()
-
-
异常终止
-
被信号终止
-
调用abort系数,SIGABRT
-
abort函数的要点
-
解除对SIGABRT信号的阻塞
-
向本进程发送SIGABRT信号,导致进程异常终止
-
在我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止,无论阻塞或忽略 SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程
-
原因,当SIGABRT信号被捕获或忽然,程序会
-
将SIGABRT信号的处理方式重置为系统默认操作
-
再次向本进程发送SIGABRT信号
-
-
-
#include <stdlib.h>
void abort(void);