信号
1.信号的概念
1.1 信号的基本概念
信号是发生事件时对进程的一种通知机制
- 通知机制 | 是用来通信的
- 对进程 | 进程是信号的接收者
- 当发生事件时 | 会触发信号
1.2 信号的目的是用来通信的
那就会有通信双方->信号的发送者和接收者
接收者:进程
发送者:既可以是进程也可以是内核
硬件异常:
1.3 信号由谁处理,怎么处理
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:
忽略信号 (有两种不能被忽略:SIGKILL SIGSTOP)
捕获信号 (signal()系统调用)
执行默认处理操作
1.4 信号是异步的
1.5 信号本质是int类型的数字编号
信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名(也就是宏定义)
2. 信号的分类
2.1 Linux信号机制基本是从UNIX系统继承过来的
早期UNIX信号存在一些问题:
进程每次处理完信号后,就将信号的响应设置为默认处理操作
信号可能会丢失(当阻塞等待的信号集中出现相同的信号,那么就会造成信号丢失)
2.2 不可靠信号和可靠信号
不可靠信号主要指的就是信号可能会丢失
可靠性好支持排队,不会丢失
2.3 实时信号与非实时信号
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程
2.4 常见信号与默认行为
2.5 进程对信号的处理
进程接收到内核或者用户的发来的信号,根据信号情况会采取不同的措施:忽略信号,捕获信号,默认处理操作.Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式.
-
signal函数
signal函数是Linux系统下信号处理最简单的方式,它可以设置信号处理方式为捕获信号,忽略信号,默认处理。
函数原型:
#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_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。
返回值:此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回SIG_ERR,并会设置 errno。
-
sigaction函数
sigaction()也更具灵活性以及移植性。sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制.
函数原型:
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号
act:act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式
oldact:oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构
返回值:成功返回 0;失败将返回-1,并设置 errno。
函数指针的用法
typedef struct {
int si_signo;
int si_code;
union sigval si_value;
int si_errno;
pid_t si_pid;
uid_t si_uid;
void *si_addr;
int si_status;
int si_band;
} siginfo_t;
2.6 向进程发送信号
与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;除了 kill()系统调用之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能.
-
kill函数
函数原型:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
pid:参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或-1 以及小于-1 等不同值.
sig:参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值:成功返回0,失败返回-1,并设置errno
-
raise函数
有时进程需要向自身发送信号,raise()函数可用于实现这一要求
函数原型:#include <signal.h> int raise(int sig);
sig:需要发送的信号。
返回值:成功返回 0;失败将返回非零值
2.7 alarm()和 pause()函数
-
alarm函数
alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM 信号
#include <unistd.h> unsigned int alarm(unsigned int seconds);
seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。
返回值:如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。 -
pause()函数
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR。
#include <unistd.h> int pause(void);
#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); }
2.8 信号集
通常需要有一个能表示多个信号(一组信号)的数据类型—信号集(signalset)
信号集是sigset_t类型数据结构
2.8.1 初始化信号集
sigemptyset()和 sigfillset()用于初始化信号集。sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号)
函数原型:头文件<signal.h>
#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);
2.8.1 向信号集中添加/删除信号
#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);
2.8.3 测试信号是否在信号集中
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
返回值:如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno。
使用示例:
sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts("信号集中包含 SIGINT 信号");
else if (!sigismember(&sig_set, SIGINT))
puts("信号集中不包含 SIGINT 信号");
2.9 获取信号的描述信息
-
sys_siglist 数组
他是一个char *类型的数组,每一个元素存放了指向信号描述信息的指针
示例: -
strsignal()函数
函数原型:
#include <string.h> char *strsignal(int sig);
测试:
#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()函数
函数原型:#include <signal.h> void psignal(int sig, const char *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); }
2.10 信号掩码(阻塞信号传递)
概念:内核为每一个进程都维护了一个信号掩码,就是一组信号,就是信号集。当进程接收到一个属于信号掩码的信号,该信号会被阻塞,无法传递给进程进行信号处理,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
向信号掩码中添加一个信号,通常有如下几种方式:
①使用signal和sigaction函数会为某一信号设置处理方式,进程会自动将该信号加入到信号掩码中,当进程正在处理信号时,相同信号发生那么该信号就会被阻塞,知道信号处理完成才会从信号掩码中移除。
②使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过 sa_mask 参数进行设置
③还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号
how:参数 how 指定了调用函数时的一些行为。(SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK)
set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为NULL,则表示无需对当前信号掩码作出改动。
oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
#include <bits/types/sigset_t.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
static void handler()
{
puts("sigacion run\n");
}
int main(){
struct sigaction sig={0};
sigset_t sig_set;
int ret;
sig.sa_handler=handler;
sig.sa_flags=0;
ret = sigaction(SIGINT, &sig, NULL);
if(-1 == ret){
perror("sigaction errors");
return -1;
}
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);
if(-1 == ret)
{
perror("sigpeomask error");
return -1;
}
raise(SIGINT);
sleep(2);
puts("sleep end\n");
ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);
if(-1 == ret)
{
perror("unsigpeomask error");
return -1;
}
exit(0);
}
2.11 阻塞等待信号 sigsuspend()
意思是 进程在执行部分代码段时不想被某信号干扰,需要先把该信号加到信号掩码中,等待执行部分代码段后放出该信号,然后用pause等待信号唤醒.可以将恢复信号集和pause操作封装为原子操作->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();/* 等待信号唤醒 */
函数原型:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
mask:参数 mask 指向一个信号集。
返回值:sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT。
#include <bits/types/sigset_t.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void handler(int sig){
printf("信号被处理\n");
}
int main(){
struct sigaction sig = {0};
sigset_t new_sigset,old_sigset,wait_sigset;
int ret;
sig.sa_handler=handler;
sig.sa_flags=0;
ret = sigaction(SIGINT, &sig, NULL);
if(-1 == ret){
perror("sigaction error\n");
exit(-1);
}
sigemptyset(&new_sigset);
sigaddset(&new_sigset,SIGINT);
sigemptyset(&wait_sigset);
ret = sigprocmask(SIG_BLOCK,&new_sigset,&old_sigset);
printf(" 休眠3s供测试\n");
printf(" 执行重要代码段\n");
sleep(3);
ret = sigsuspend(&wait_sigset);
if(ret != -1)
{
perror("sigsuspend error\n");
exit(-1);
}
ret = sigprocmask(SIG_BLOCK,&old_sigset,NULL);
printf("ok!\n");
exit(0);
}
2.12 实时信号
查看处于等待中的信号有哪些
-
sigpending函数
#include <signal.h> int sigpending(sigset_t *set);
set:处于等待状态的信号会存放在参数 set 所指向的信号集中。
返回值:成功返回 0;失败将返回-1,并设置 errno。/* 定义信号集 */ 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 信号未处于等待状态");
-
发送实时信号
应用程序当中使用实时信号,需要注意两点:
①:发送进程使用 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);
value:参数 value 指定了信号的伴随数据,union sigval 数据类型。
typedef union sigval { int sival_int; void *sival_ptr; } sigval_t;
2.13 异常退出 abort()函数
正常退出时使用 exit()、_exit()或_Exit()函数,当异常退出时使用abort库函数,使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态。调用abort函数内核会发送 SIGABRT信号
#include <stdlib.h>
void abort(void);
测试:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void handler(int sig){
printf("recive sig %d\n",sig);
}
int main(){
struct sigaction sig= {0};
sig.sa_handler = handler;
sig.sa_flags = 0;
if(-1==sigaction(SIGABRT,&sig,NULL))
{
perror("sigaction error!\n");
exit(-1);
}
sleep(2);
abort();
for(;;){
sleep(1);
}
return 0;
}