一、信号的基本概念
- 信号是事件发生时对进程的通知机制,也可以把它称为软件中断
- 信号提供了一种处理异步事件的方法
- 信号的目的是用来通信
1、信号由谁发出
下列举的很多情况均可以产生信号
- 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程
- 用于在终端下输入了能够产生信号的特殊字符
- 进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组
- 用户可以通过 kill 命令将信号发送给其它进程
- 发生了软件事件,即当检测到某种软件条件已经发生
2、信号由谁接受并处理
通常进程会视具体信号执行以下操作之一:
- 忽略信号
- 捕获信号,当信号到达进程后,执行预先绑定好的信号处理函数
- 执行系统默认操作
3、信号是异步的
产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式
4、信号本质上是int类型数字编号
信号本质上是int类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定
义了一个唯一的整数编号。
二、信号的分类
linux 系统下可对信号从两个不同的角度进行分类:
- 从可靠性方面将信号分为可靠信号与不可靠信号
- 从实时性方面将信号分为实时信号与非实时信号
1、从可靠性上来分类
- Linux下的不可靠信号问题主要指的是信号可能丢失。
- Linux下的可靠信号支持排队,不会丢失。
- 在Linux系统下,信号值小于SIGRTMIN(34)的信号都是不可靠信号,编号 1-31所对应的是不可靠信号,编号 34~64对应的是可靠信号.(可靠信号并没有一个具体对应的名字)
2、从实时性上来分类 - 实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的, 非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收
- 一般我们也把非实时信号(不可靠信号)称为标准信号(编号为1-31)
三、常见信号简介
- term 表示终止进程;
- core 表示生成核心转储文件
- ignore表示忽略信号;
- cont表示继续运行进程;
- stop 表示停止进程(注意停止不等于终止,而是暂停)
四-1、进程对信号的处理==接收
- 当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作
- Linux系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式
1、signal函数
/*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. */
/*
@ 作用: signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作
@ 头文件: #include <signal.h>
@ signum: 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名
@ handler:sig_t类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处
理函数;参数 handler既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设
置为 SIG_IGN或 SIG_DFL, SIG_IGN表示此进程需要忽略该信号, SIG_DFL则表示设置为系统默认操作。
@ sig_t: 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。
@ 返回值:此函数的返回值也是一个 sig_t类型的函数指针,成功情况下的返回值则是指向在此之前的信
号处理函数;如果出错则返回 SIG_ERR,并会设置errno。
*/
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);/*signal()函数可以根据第二个参数 handler的不同设置情况,可对信号进行不同的处理*/
- 例子
#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);//信号产生,SIGINT对应编号2
if (SIG_ERR == ret) {
perror("signal error");
exit(-1);
}
/* 死循环 */
for ( ; ; ) { }
exit(0);
}
- 当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。还记得前面我们呢提到的ctrl+c吗?对于大多数的信号,如果不去专门设置,则处理的方式就是系统默认操作就是term(终止进程)
2、sigaction函数
- 虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性
- sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制
/*struct sigaction结构体*/
struct sigaction {
void (*sa_handler)(int); /*sa_handler:指定信号处理函数,与 signal()函数的handler参数相同*/
void (*sa_sigaction)(int, siginfo_t *, void *); /*sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取*/
sigset_t sa_mask; /*信号掩码可以避免一些信号之间的竞争状态(也称为竞态)*/
int sa_flags; /*参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程
*/
void (*sa_restorer)(void); /*弃用*/
};
/*
@ 头文件:#include <signal.h>
@ signum:需要设置的信号,除了SIGKILL信号和 SIGSTOP 信号之外的任何信号
@ act:act参数是一个struct sigaction 类型指针,指向一个 struct sigaction数据结构,该数据结构描述了信号的处理方式,稍后介绍该数据结构
@ oldact:oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。
@ 返回值:成功返回0;失败将返回-1,并设置errno。
*/
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 例子
#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);
}
- 总结:
将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做
大量消耗CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险
四-2、向进程发送信号==发送
- ipc应用
- 与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号。
- 除了 kill()系统调用之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能。
1、kill()函数
- kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程,
/*
@ 头文件:
#include <sys/types.h>
#include <signal.h>
@ pid:参数pid为正数的情况下,用于指定接收此信号的进程 pid;除此之外,参数 pid也可设置为0或
-1 以及小于-1等不同值
⚫ 如果pid为正,则信号 sig将发送到pid指定的进程。
⚫ 如果pid等于0,则将 sig发送到当前进程的进程组中的每个进程。
⚫ 如果pid等于-1,则将 sig发送到当前进程有权发送信号的每个进程,但进程1(init)除外。
⚫ 如果pid小于-1,则将 sig发送到ID为-pid的进程组中的每个进程。
@ sig:参数sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid指定的进程是否存在
@ 返回值:成功返回0;失败将返回-1,并设置errno
*/
int kill(pid_t pid, int sig);
-
上面要注意:
进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root进程可以将信号发送给任何进程但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程
的实际用户 ID或有效用户ID必须等于接收者进程的实际用户ID或有效用户ID -
例子:
1、首先执行下面的程序,接收信号的程序置于后台运行(其进程pid为 21825)
#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);
}
2、再执行下面的程序。使用kill()函数向一个指定的进程发送SIGINT 信号。
#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);
}
3、分析:
- 第一个程序,设置了信号处理的形式,当运行的后,此时你可以,使用按键触发这个信号,比如CTRlL+C(这是内核给他发送的),也可以使用第二个程序里面的kill函数就行发送。
- 第二个程序的作用,和前面提到早console里面使用控制台的方式一样。
2、raise()函数
- 有时进程需要向自身发送信号,raise()函数可用于实现这一要求。
/*
@ 头文件:#include <signal.h>
@ sig:需要发送的信号。
@ 返回值:成功返回0;失败将返回非零值。
@ 功能等价于;kill(getpid(),sig);
*/
int raise(int sig);
- 例子—向自己发送信号并去接受处理
#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函数
/*
@ 功能:alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM信号,每个进程只能设置一个 alarm闹钟
@ 头文件:#include <unistd.h>
@ seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟
@ 返回值:如果在调用alarm()时,之前已经为该进程设置了alarm闹钟还没有超时,则该闹钟的剩余值作
为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0。
*/
unsigned int alarm(unsigned int seconds);
/*====================================================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()才返回,在这种情况下,pause()返回-1,并且将 errno设置为EINTR
@ 头文件:#include <unistd.h>
*/
int pause(void);
/*======================================================pause函数使用举例*/
#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);
}
六、信号集
- 通常我们需要有一个能表示多个信号(一组信号)的数据类型—信号集(signal set),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如 sigaction()函数。
- 信号集其实就是sigset_t类型数据结构
/*
* 使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中,
*/
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
- Linux 系统了用于操作sigset_t 信号集的 API
譬如 sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()
1、初始化信号集
- sigemptyset()和 sigfillset()用于初始化信号集
- sigemptyset()初始化信号集,使其不包含任何信号
- sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号)
/*
@ 头文件:#include <signal.h>
@ set:指向需要进行初始化的信号集变量
@ 返回值:成功返回0;失败将返回-1,并设置errno
*/
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
/*======================================使用举例*/
/*初始化为空信号集*/
sigset_t sig_set;
sigemptyset(&sig_set);
/*初始化信号集,使其包含所有的信号*/
sigset_t sig_set;
sigfillset(&sig_set);
2、向信号集中添加/删除信号
- 分别使用sigaddset()函数和 sigdelset()函数向信号集中添加或移除一个信号
/*
@ 头文件:#include <signal.h>
@ set:指向信号集
@ signum:需要添加/删除的信号。
@ 返回值:成功返回0;失败将返回-1,并设置errno
*/
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
/*===============================================使用举例*/
/*向信号集中添加信号*/
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
/*从信号集中移除信号*/
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);
3、测试信号是否在信号集中
- 使用sigismember()函数可以测试某一个信号是否在指定的信号集中
/*
@ 头文件:#include <signal.h>
@ set:指向信号集
@ signum:需要进行测试的信号
@ 返回值:如果信号signum在信号集set中,则返回 1;如果不在信号集set中,则返回 0;失败则返回-1,并设置errno
*/
int sigismember(const sigset_t *set, int signum);
/*===============================================使用举例*/
/*判断SIGINT信号是否在 sig_set信号集中*/
sigset_t sig_set;
if (1 == sigismember(&sig_set, SIGINT))
puts("信号集中包含SIGINT 信号");
else if (!sigismember(&sig_set, SIGINT))
puts("信号集中不包含 SIGINT信号");
七、获取信号的描述信息
- 在Linux下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述
- 这些字符串位于 sys_siglist数组中,sys_siglist数组是一个char *类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息
- 举例:可以使用 sys_siglist[SIGINT]来获取对 SIGINT 信号的描述
#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);
}
1、strsignal()函数
- 除了直接使用sys_siglist数组获取描述信息之外,还可以使用strsignal()的库函数===推荐使用
- 调用 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);
}
2、psignal()函数
-** psignal()可以在标准错误(stderr)上输出信号描述信息**
- 调用psignal()函数会将参数 sig 指定的信号对应的描述信息输出到标准错误,并且还允许调用者添加一些输出信息,由参数 s 指定;所以整个输出信息由字符串 s、冒号、空格、描述信号编号 sig 的字符串和尾随的换行符组成。
#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);
}
八、信号掩码(阻塞信号传递)
-
内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
-
向信号掩码中添加一个信号,通常有如下几种方式:
1、当应用程序调用signal()或sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除2、使用sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过sa_mask参数进行设置
3、还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号。
-
sigprocmask()函数
/*
@ 头文件:#include <signal.h>
@ how:参数how指定了调用函数时的一些行为。参数how 可以设置为以下宏:
⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与 set的并集。
⚫ SIG_UNBLOCK:将参数 set指向的信号集内的所有信号从进程信号掩码中移除。
⚫ SIG_SETMASK:进程信号掩码直接设置为参数set指向的信号集。
@ set:将参数set指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数set为NULL,则表示无需对当前信号掩码作出改动。
@ oldset: 如果参数oldset不为NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset所指定的信号集中;如果为 NULL则表示不获取当前的信号掩码
@ 返回值:成功返回0;失败将返回-1,并设置errno。
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*========================================================================使用式例*/
/*=============================将信号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);
}
此时我们综合之前的一些例子知识点,看一个综合的例子
#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); /*这是区别signal的最大区别相当于signal(getpid(),SIGINT)*/
/* 休眠2秒 */
sleep(2);
printf("休眠结束\n");
/* 从信号掩码中移除添加的信号 */
if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
exit(-1);
exit(0);
}
九、阻塞等待信号 sigsuspend()
- 上一节,更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。
sigset_t new_set, old_set;
/* 信号集初始化 */
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set))
exit(-1);
/* 受保护的关键代码段 */
......
/**********************/
/* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL))
exit(-1);
pause();/* 等待信号唤醒 */
- 考虑一种特殊的情况,
如果信号的传递恰好发生在第二次调用 sigprocmask()之后、pause()之前,如果确实发生了这种情况,就会产生一个问题,信号传递过来就会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到 pause()被阻塞,知道下一次信号发生时才会被唤醒,这有违代码的本意 - 综合上述的考虑。我们将需要将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作
/*
@ 头文件:#include <signal.h>
@ mask:参数mask指向一个信号集
@ 返回值:sigsuspend()始终返回-1,并设置errno来指示错误(通常EINTR),表示被信号所中断,如果调用失败,将 errno设置为 EFAULT。
@ sigsuspend()函数会将参数mask所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend()会将进程的信号掩码恢复成调用前的值。
*/
int sigsuspend(const sigset_t *mask);
/*调用sigsuspend函数等价于下面*/
sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);
/*==========================================使用例子*/
#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);
}
- 在上述代码中,我们希望执行受保护代码段时不被SIGINT中断信号打断,所以在执行保护代码段之前将 SIGINT 信号添加到进程的信号掩码中,执行完受保护的代码段之后,调用 sigsuspend()挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态
十、实时信号
- 如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取。
1、获取当前处于等待状态的信号
/*
@ 头文件:#include <signal.h>
@ set:处于等待状态的信号会存放在参数 set所指向的信号集中
@ 返回值:成功返回0;失败将返回-1,并设置errno。
*/
int sigpending(sigset_t *set);
/*==============================================使用实例*/
//判断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 信号未处于等待状态");
**2、 发送实时信号 **
-
等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之,如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一。
-
实时信号较之于标准信号,其优势如下:
1、实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用:SIGUSR1和SIGUSR2。
2、内核对于实时信号所采取的是队列化管理
3、当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取
4、不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。 -
Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64,使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数。
-
应用程序当中使用实时信号,需要有以下的两点要求:
1、发送进程使用sigqueue()系统调用向另一个进程发送实时信号以及伴随数据
2、接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用
sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样
就不能获取到实时信号的伴随数据了. -
sigqueue()函数发送实时信号
/*携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针*/
typedef union sigval
{
int sival_int;
void *sival_ptr;
} sigval_t;
/*
@ 同文件:#include <signal.h>
@ pid:指定接收信号的进程对应的 pid,将信号发送给该进程。
@ sig:指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为0,用于检查参数 pid所指定的进
程是否存在。
@ sig:指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为0,用于检查参数 pid所指定的进
程是否存在。
@ value:参数value指定了信号的伴随数据,union sigval数据类型。
@ 返回值:成功将返回0;失败将返回-1,并设置errno
*/
int sigqueue(pid_t pid, int sig, const union sigval value);
/*===================================================================使用的例子*/
/*)发送进程使用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);
}
/*=========接收进程使用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);
}
十一、异常退出abort函数
- 应用程序中结束进程的几种方法:
譬如使用 exit()、_exit()或_Exit()这些函数来终止进程, - 然后这些方法使用于正常退出应用程序,而对于异常退出程序,则一般使用abort()库函数,使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用abort()时的程序状态
- 函数 abort()通常产生 SIGABRT 信号来终止调用该函数的进程,SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用 abort()函数之后,内核会向进程发送SIGABRT信号。
#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);
}
以上只是大概对信号的简介