一、信号的基本概念
- 信号是事件发生时对进程的通知机制,也可以把它称为软件中断
- 信号提供了一种处理异步事件的方法
- 信号的目的是用来通信
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函数
#define SIG_ERR ((sig_t) -1)
#define SIG_DFL ((sig_t) 0)
#define SIG_IGN ((sig_t) 1)
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t 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);
if (SIG_ERR == ret) {
perror("signal error");
exit(-1);
}
for ( ; ; ) { }
exit(0);
}
- 当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。还记得前面我们呢提到的ctrl+c吗?对于大多数的信号,如果不去专门设置,则处理的方式就是系统默认操作就是term(终止进程)
2、sigaction函数
- 虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性
- 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);
};
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()系统调用可将信号发送给指定的进程或进程组中的每一个进程,
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);
if (-1 == kill(pid, SIGINT)) {
perror("kill error");
exit(-1);
}
exit(0);
}
3、分析:
- 第一个程序,设置了信号处理的形式,当运行的后,此时你可以,使用按键触发这个信号,比如CTRlL+C(这是内核给他发送的),也可以使用第二个程序里面的kill函数就行发送。
- 第二个程序的作用,和前面提到早console里面使用控制台的方式一样。
2、raise()函数
- 有时进程需要向自身发送信号,raise()函数可用于实现这一要求。
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 ( ; ; ) {
if (0 != raise(SIGINT)) {
printf("raise error\n");
exit(-1);
}
sleep(3);
}
exit(0);
}
五、与信号搭配函数alarm和pause
unsigned int alarm(unsigned int seconds);
#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);
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
second = atoi(argv[1]);
printf("定时时长: %d秒\n", second);
alarm(second);
for ( ; ; )
sleep(1);
exit(0);
}
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);
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
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()函数初始化信号集,使其包含所有信号(包括所有实时信号)
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()函数向信号集中添加或移除一个信号
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()函数可以测试某一个信号是否在指定的信号集中
int sigismember(const sigset_t *set, int signum);
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()函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int ret;
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);
if (-1 == ret) {
perror("sigprocmask error");
exit(-1);
}
sigset_t sig_set;
sigemptyset(&sig_set);
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);
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()挂起进程这两个动作封装成一个原子操作
int sigsuspend(const sigset_t *mask);
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、获取当前处于等待状态的信号
int sigpending(sigset_t *set);
sigset_t sig_set;
sigemptyset(&sig_set);
sigpending(&sig_set);
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;
int sigqueue(pid_t pid, int sig, const union sigval value);
#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);
}
#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();
for ( ; ; )
sleep(1);
exit(0);
}
以上只是大概对信号的简介
|