很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法;
讨论如下主题内容:
⚫ 信号的基本概念;
⚫ 信号的分类、Linux 提供的各种不同的信号及其作用;
⚫ 发出信号以及响应信号,信号由“谁”发送、由“谁”处理以及如何处理;
⚫ 进程在默认情况下对信号的响应方式;
⚫ 使用进程信号掩码来阻塞信号、以及等待信号等相关概念;
⚫ 如何暂停进程的执行,并等待信号的到达;
基本概念
-
信号是事件发生时对进程的通知机制,也可以把它称为软件中断;
-
信号与硬件中断的相似在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的模拟.
信号目的:通信
信号处理
信号是异步事件
信号本质: int 类型数字编号
/*示例代码 8.1.1 信号定义*/
/* Signals. */
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
不存在编号为 0 的信号,从示例代码 8.1.1 中也可以看到,信号编号是从 1 开始的;
事实上 kill()函数对信号编号 0 有着特殊的应用.
信号的分类
-
从可靠性方面将信号分为可靠信号与不可靠信号;
-
从实时性方面将信号分为实时信号与非实时信号;
-
用**"kill -l"命令查看**到所有信号.
可靠信号与不可靠信号
-
信号值小于 SIGRTMIN(34)的信号都是不可靠信号;
-
Linux 下的不可靠信号问题主要指的是信号可能丢失
Linux 支持不可靠信号,对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal();
用"kill -l"命令可查看到所有信号
可靠信号支持排队,不会丢失;
同时,信号的发送和绑定也出现了新版本,信号发送函数 sigqueue()及信号绑定函数 sigaction()
实时信号与非实时信号
-
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应;
-
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号;
-
实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程;
-
一般我们也把非实时信号(不可靠信号)称为标准信号.
常见信号与默认行为
- Linux 下对标准信号(不可靠信号、非实时信号)的编号为 1~31;
下面介绍这些信号以及这些信号所对应的系统默认操作:
Linux 信号总结
Tips:上表中,term 表示终止进程;
core 表示生成核心转储文件,核心转储文件可用于调试,这个便不再给介绍了;
ignore 表示忽略信号;
cont 表示继续运行进程;
stop 表示停止进程(注意停止不等于终止,而是暂停)。
进程对信号的处理
-
进程接收到内核或用户发送过来的信号处理方式:忽略信号、捕获信号或者执行系统默认操作
-
Linux 提供系统调用用于设置信号的处理方式: signal(),signal()函数,信号的处理方式最简单的接口;
-
了解特殊情况如何处理(进程创建、程序启动).
signal()函数
- signal()函数可以根据第二个参数 handler 的不同设置情况,为信号设置相应的处理方式;
原型
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
/*
参数:
signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
handler:sig_t类型函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;
sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,
可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号.
返回值:
sig_t 类型的函数指针;
成功:指向在此之前的信号处理函数;
出错:返回 SIG_ERR,并会设置 errno.
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. */
/*
参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数;
也可以设置为SIG_IGN 或 SIG_DFL,SIG_IGN 表示此进程需要忽略该信号,SIG_DFL表示设置为系统默认操作.
应用典例1
-
通过 signal()函数将 SIGINT(2)信号绑定到了一个用户自定的处理函数上sig_handler(int sig);
-
当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句;
-
当运行程序之后,程序会卡在 for 死循环处,此时在终端按下中断符 CTRL + C,系统便会给前台进程组中的每一个进程发送SIGINT 信号,我们测试程序便会收到该信号,随之结束进程(打印语句).
/*示例代码 8.4.1 signal()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
sig_t ret = NULL;
ret = signal(SIGINT, (sig_t)sig_handler);
if (SIG_ERR == ret)
{
perror("signal error");
exit(-1);
}
/* 死循环 */
for ( ; ; ) { }
exit(0);
}
CTRL + C 可以终止一个进程,而此未能终止测试程序,原因:测试程序捕获该信号,处理方式只是打印语句;
改进1
可向该进程发送 SIGKILL 暴力终止该进程,不推荐这样使用,如果实在没办法才采取这种措施;
Tips:普通用户只能杀死该用户自己的进程,无权限杀死其它用户的进程;
应用典例2
再执行一次测试程序,这里将测试程序放在后台运行,然后再按下中断符:
按下中断符发现进程并没有收到 SIGINT 信号;
原因:进程并不是前台进程,而是一个后台进程,按下中断符时系统并不会给后台进程发送 SIGINT 信号;
改进2
可以使用 kill 命令手动发送信号给我们的进程:
程序启动与进程创建
如果程序没有调用 signal()函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到 signal():
那么这时进程接收到一个信号后是如何处理?
程序启动
进程创建
sigaction()函数
-
设置信号处理方式的另一选择,sigaction()复杂但更具灵活性以及移植性,推荐使用sigaction();
-
sigaction()允许单独获取信号的处理函数而不是设置,并且还可设置各种属性对调用信号处理函数时的行为施以更加精准的控制;
原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
/*
参数:
signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act: 一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构;
如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;
如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
oldact:一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构;
如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;
如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值:
成功返回 0;失败将返回-1,并设置 errno。
*/
/*man手册自行查看 sigset_t结构体、很重要的参数sa_flags还有函数sigaction()帮助信息*/
使用示例
实现效果同signal()典例1.
/*示例代码 8.4.4 sigaction()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int ret;
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret)
{
perror("sigaction error");
exit(-1);
}
/* 死循环 */
for ( ; ; ) { }
exit(0);
}
结构体 struct sigaction
/*示例代码 8.4.2 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);
};
/*
man手册自行查看 sigset_t结构体、很重要的参数sa_flags还有函数sigaction()帮助信息*/
关于信号处理函数说明
一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗 CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。
向进程发送信号
-
kill()、killpg()、raise()
-
与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;
-
之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能;
-
进程中信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号;
kill()函数
- 将信号发送给指定的进程或进程组中的每一个进程;
原型
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
/*
参数:
pid:参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;
sig:参数 sig 指定需要发送的信号;
设置为 0则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在;
返回值:
成功返回 0;
败将返回-1,并设置 errno.
参数 pid 不同取值含义:
⚫ 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
⚫ 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。
⚫ 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
⚫ 如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。
功能
确定特定进程的存在:
当 sig 为 0 时,仍可进行正常执行的错误检查,但不会发送信号,表示进程存在;
如果向一个不存在的进程发送信号,kill()将会返回-1,errno 将被设置为 ESRCH,表示进程不存在.
典例
通过 kill()函数向指定进程发送 SIGINT 信号,可通过外部传参将接收信号的进程 pid 传入到程序中,再执行该测试代码之前,需要运行先一个用于接收此信号的进程;
/*示例代码 8.5.1 kill()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int pid;
/* 判断传参个数 */
if (2 > argc)
exit(-1);
/* 将传入的字符串转为整形数字 */
pid = atoi(argv[1]);
printf("pid: %d\n", pid);
/* 向 pid 指定的进程发送信号 */
if (-1 == kill(pid, SIGINT)) {
perror("kill error");
exit(-1);
}
exit(0);
}
raise()
实现进程向自身发送信号;
原型
#include <signal.h>
int raise(int sig);
/*
参数:
sig:需要发送的信号。
返回值:
成功返回 0;
失败将返回非零值。
等价:
raise() == kill(getpid(), sig)
Tips:getpid()函数用于获取进程自身的 pid。
典例
/*示例代码 8.5.2 raise()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int ret;
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret) {
perror("sigaction error");
exit(-1);
}
for ( ; ; ) {
/* 向自身发送 SIGINT 信号 */
if (0 != raise(SIGINT)) {
printf("raise error\n");
exit(-1);
}
sleep(3); // 每隔 3 秒发送一次
}
exit(0);
}
定时与暂停进程
- 系统调用 alarm()和 pause()
alarm()
-
设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM信号
-
alarm 闹钟并不能循环触发,只触发一次;若要循环触发,可在 SIGALRM 信号处理函数中再调用 alarm();
原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
/*
参数:
seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。
返回值:
如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,
则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;
否则返回 0.
参数 seconds 的值是产生 SIGALRM 信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,
每个进程只能设置一个 alarm 闹钟;
虽然 SIGALRM 信号的系统默认操作是终止进程,但是如果程序当中设置了 alarm 闹钟,但大多数使用闹钟的进程都会捕获此信号;
典例
用 alarm()来设计一个闹钟;
/*示例代码 8.6.1 alarm()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
puts("Alarm timeout");
exit(0);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int second;
/* 检验传参个数 */
if (2 > argc)
exit(-1);
/* 为 SIGALRM 信号绑定处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
/* 启动 alarm 定时器 */
second = atoi(argv[1]);
printf("定时时长: %d 秒\n", second);
alarm(second);
/* 循环 */
for ( ; ; )
sleep(1);
exit(0);
}
pause()
- 使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止;
- 只执行信号处理函数并从其返回时,pause()才返回,此情况下,pause()返回-1,并将 errno 设置为 EINTR;
原型
#include <unistd.h>
int pause(void);
典例
通过 alarm()和 pause()函数模拟 sleep 功能.
示例代码 8.6.2 alarm()和 pause()模拟 sleep
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
puts("Alarm timeout");
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int second;
/* 检验传参个数 */
if (2 > argc)
exit(-1);
/* 为 SIGALRM 信号绑定处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
/* 启动 alarm 定时器 */
second = atoi(argv[1]);
printf("定时时长: %d 秒\n", second);
alarm(second);
/* 进入休眠状态 */
pause();
puts("休眠结束");
exit(0);
}
信号集
-
信号集(signalset):能表示多个信号(一组信号)的数据类型;
-
sigaction()函数、sigprocmask()函数、sigpending()函数等,使用信号集来作为参数传递;
-
信号集本质: sigset_t 类型数据结构,此前struct sigaction 结构体有提及(函数sigaction);
初始化信号集
- sigemptyset()初始化信号集,使其不包含任何信号;
- sigfillset()函数初始化信号集,使其包含所有信号(包括所有s);
原型
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
/*
参数:
set:指向需要进行初始化的信号集变量。
返回值:
成功返回 0;
失败将返回-1,并设置 errno。
使用示例
初始化为空信号集:
sigset_t sig_set;
sigemptyset(&sig_set);
初始化信号集,使其包含所有信号:
sigset_t sig_set;
sigfillset(&sig_set);
信号集中添加/删除信号
- 使用 sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号;
原型
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
/*
参数:
set:指向信号集;
signum:需要添加/删除的信号.
返回值:
成功返回 0;
失败将返回-1,并设置 errno.
使用示例
向信号集中添加信号:
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
从信号集中移除信号:
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);
测试信号是否在信号集
使用 sigismember()函数可以测试某一个信号是否在指定的信号集中;
原型
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
/*
参数:
set:指定信号集。
signum:需要进行测试的信号。
返回值:
如果信号 signum 在信号集 set 中,则返回 1;
如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno。
使用示例
判断 SIGINT 信号是否在 sig_set 信号集中:
sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts("信号集中包含 SIGINT 信号");
else if (!sigismember(&sig_set, SIGINT))
puts("信号集中不包含 SIGINT 信号");
获取信号的描述信息
- 每个信号都有一串与之相对应的字符串描述信息;
- 字符串位于 sys_siglist 数组中,sys_siglist 数组是一个 char *类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息;
- 除了直接使用 sys_siglist 数组获取描述信息之外,还可以使用 strsignal()函数
实例
使用 sys_siglist[SIGINT]来获取对 SIGINT 信号的描述;
Tips:使用 sys_siglist 数组需要包含<signal.h>头文件;
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
exit(0);
}
strsignal()
- 较之于直接引用 sys_siglist数组,更推荐使用 strsignal()库函数获取描述信息;
原型
#include <string.h>
char *strsignal(int sig);
/* 调用 strsignal()函数将会获取到参数 sig 指定的信号对应的描述信息,返回该描述信息字符串的指针;
函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息;
实例
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
printf("SIGINT 描述信息: %s\n", strsignal(SIGINT));
printf("SIGQUIT 描述信息: %s\n", strsignal(SIGQUIT));
printf("SIGBUS 描述信息: %s\n", strsignal(SIGBUS));
printf("编号为 1000 的描述信息: %s\n", strsignal(1000));
exit(0);
}
psignal()
psignal()可以在标准错误(stderr)上输出信号描述信息;
原型
#include <signal.h>
void psignal(int sig, const char *s);
调用 psignal()函数会将参数 sig 指定的信号对应的描述信息输出到标准错误;
并且还允许调用者添加一些输出信息,由参数 s 指定;所以整个输出信息由字符串 s、冒号、空格、描述信号编号 sig 的字符串和尾随的换行符组成.
实例
/*示例代码 8.8.3 psignal()函数使用示例*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
psignal(SIGINT, "SIGINT 信号描述信息");
psignal(SIGQUIT, "SIGQUIT 信号描述信息");
psignal(SIGBUS, "SIGBUS 信号描述信息");
exit(0);
}
信号掩码(阻塞信号传递)
-
内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号;
-
更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞;
-
当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理;
-
向信号掩码中添加一个信号的方式:
函数sigprocmask()
显式地向信号掩码中添加/移除信号
原型
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*
参数:
how: 参数 how 指定了调用函数时的一些行为,见下;
set: 将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;
如果参数 set 为NULL,则表示无需对当前信号掩码作出改动;
oldset:如果为 NULL 则表示不获取当前的信号掩码。
如果参数 oldset 不为 NULL,向信号掩码中添加新的信号之前,获取到进程当前的信号掩码, 存放在 oldset 所指定的信号中;
返回值:
成功返回 0;
失败将返回-1,并设置 errno。
参数 how 可以设置为以下宏:
SIG_BLOCK: 将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中;
换言之,将信号掩码设置为当前值与 set 的并集.
SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除.
SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集.
示例1
将信号 SIGINT 添加到进程的信号掩码中;
int ret;
/* 定义信号集 */
sigset_t sig_set;
/* 将信号集初始化为空 */
sigemptyset(&sig_set);
/* 向信号集中添加 SIGINT 信号 */
sigaddset(&sig_set, SIGINT);
/* 向进程的信号掩码中添加信号 */
ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);
if (-1 == ret) {
perror("sigprocmask error");
exit(-1);
}
从信号掩码中移除 SIGINT 信号:
int ret;
/* 定义信号集 */
sigset_t sig_set;
/* 将信号集初始化为空 */
sigemptyset(&sig_set);
/* 向信号集中添加 SIGINT 信号 */
sigaddset(&sig_set, SIGINT);
/* 从信号掩码中移除信号 */
ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);
if (-1 == ret) {
perror("sigprocmask error");
exit(-1);
}
示例2
/*示例代码 8.9.1 测试信号掩码的作用*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("执行信号处理函数...\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t sig_set;
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 信号集初始化 */
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
exit(-1);
/* 向自己发送信号 */
raise(SIGINT);
/* 休眠 2 秒 */
sleep(2);
printf("休眠结束\n");
/* 从信号掩码中移除添加的信号 */
if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
exit(-1);
exit(0);
}
阻塞等待信号sigsuspend()
- sigsuspend()系统调用的目的:将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作;
情景引入
-
虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能,这必然是一个缺陷;
-
要避免这个问题,需要将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作.
原型
#include <signal.h>
int sigsuspend(const sigset_t *mask);
/*
参数:
mask:参数 mask 指向一个信号集.
返回值:
sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断;
调用失败,将 errno 设置为 EFAULT.
调用 sigsuspend()函数**相当于以不可中断(原子操作)**的方式执行以下操作:
sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);
实例
执行受保护代码段时不被 SIGINT 中断信号打断:
在执行保护代码段之前将 SIGINT 信号添加到进程的信号掩码中,执行完受保护的代码段之后,调用 sigsuspend()挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态;
/*示例代码 8.10.1 sigsuspend()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("执行信号处理函数...\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t new_mask, old_mask, wait_mask;
/* 信号集初始化 */
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigemptyset(&wait_mask);
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
exit(-1);
/* 执行保护代码段 */
puts("执行保护代码段");
/******************/
/* 挂起、等待信号唤醒 */
if (-1 != sigsuspend(&wait_mask))
exit(-1);
/* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
exit(-1);
exit(0);
}
实时信号
-
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号
如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中;
-
为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取;
函数sigpending()
确定进程中处于等待状态的是哪些信号;
原型
#include <signal.h>
int sigpending(sigset_t *set);
/*
参数:
set:处于等待状态的信号会存放在参数 set 所指向的信号集中。
返回值:
成功返回 0;
失败将返回-1,并设置 errno
示例
判断 SIGINT 信号当前是否处于等待状态;
/* 定义信号集 */
sigset_t sig_set;
/* 将信号集初始化为空 */
sigemptyset(&sig_set);
/* 获取当前处于等待状态的信号 */
sigpending(&sig_set);
/* 判断 SIGINT 信号是否处于等待状态 */
if (1 == sigismember(&sig_set, SIGINT))
puts("SIGINT 信号处于等待状态");
else if (!sigismember(&sig_set, SIGINT))
puts("SIGINT 信号未处于等待状态");
发送实时信号
-
Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64;
-
使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数;
-
等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数;
-
标准信号的缺点之一:如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次);
实时信号VS标准信号优势
程序的实时信号要求
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 数据类型.
返回值:
成功将返回 0;
失败将返回-1,并设置 errno.
union sigval 数据类型(共用体)
- 携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针;
typedef union sigval
{
int sival_int;
void *sival_ptr;
} sigval_t;
实例1
发送进程使用 sigqueue()系统调用向另一个进程发送实时信号;
/*示例代码 8.11.1 使用 sigqueue()函数发送信号*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *argv[])
{
union sigval sig_val;
int pid;
int sig;
/* 判断传参个数 */
if (3 > argc)
exit(-1);
/* 获取用户传递的参数 */
pid = atoi(argv[1]);
sig = atoi(argv[2]);
printf("pid: %d\nsignal: %d\n", pid, sig);
/* 发送信号 */
sig_val.sival_int = 10; //伴随数据
if (-1 == sigqueue(pid, sig, sig_val)) {
perror("sigqueue error");
exit(-1);
}
puts("信号发送成功!");
exit(0);
}
实例2
接收进程使用 sigaction()函数为信号绑定处理函数;
/*示例代码 8.11.2 使用 sigaction()函数为实时信号绑定处理函数*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig, siginfo_t *info, void *context)
{
sigval_t sig_val = info->si_value;
printf("接收到实时信号: %d\n", sig);
printf("伴随数据为: %d\n", sig_val.sival_int);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int num;
/* 判断传参个数 */
if (2 > argc)
exit(-1);
/* 获取用户传递的参数 */
num = atoi(argv[1]);
/* 为实时信号绑定处理函数 */
sig.sa_sigaction = sig_handler;
sig.sa_flags = SA_SIGINFO;
if (-1 == sigaction(num, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
/* 死循环 */
for ( ; ; )
sleep(1);
exit(0);
}
异常退出
结束进程方法
-
正常退出应用程序: **exit()、_exit()或_Exit()**这些函数来终止进程;
-
异常退出程序:一般使用 abort()库函数,使用 abort()终止进程运行;
会生成核心转储文件,可用于判断程序调用 abort()时的程序状态
abort()函数
- 终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态;
- 产生 SIGABRT 信号(系统默认操作是终止进程运行、并生成核心转储文件)来终止调用该函数的进程;
原型
#include <stdlib.h>
void abort(void);
- 函数 abort()通常产生 SIGABRT 信号来终止调用该函数的进程;
- SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;
- 当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。
示例
程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止;
无论阻塞或忽略 SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程;
/*示例代码 8.12.1 abort()终止进程*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("接收到信号: %d\n", sig);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGABRT, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
sleep(2);
abort(); // 调用 abort
for ( ; ; )
sleep(1);
exit(0);
}
(1); exit(0); } ```
异常退出
结束进程方法
-
正常退出应用程序: **exit()、_exit()或_Exit()**这些函数来终止进程;
-
异常退出程序:一般使用 abort()库函数,使用 abort()终止进程运行;
会生成核心转储文件,可用于判断程序调用 abort()时的程序状态
abort()函数
- 终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态;
- 产生 SIGABRT 信号(系统默认操作是终止进程运行、并生成核心转储文件)来终止调用该函数的进程;
原型
#include <stdlib.h>
void abort(void);
- 函数 abort()通常产生 SIGABRT 信号来终止调用该函数的进程;
- SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;
- 当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。
示例
程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止;
无论阻塞或忽略 SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程;
/*示例代码 8.12.1 abort()终止进程*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("接收到信号: %d\n", sig);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGABRT, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
sleep(2);
abort(); // 调用 abort
for ( ; ; )
sleep(1);
exit(0);
}