Linux应用编程(信号基础)

一、基本概念

信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。

信号的目的是用来通信的
一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号可以由“谁”发出呢?以下列举的很多情况均可以产生信号:
1、硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。
2、用于在终端下输入了能够产生信号的特殊字符。譬如在终端上按下 CTRL + C 组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程。
3、进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组。
4、发生了软件事件,即当检测到某种软件条件已经发生。

信号由谁处理、怎么处理
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:
1、忽略信号。
2、捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。
3、执行系统默认操作。

信号是异步的
程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。

信号本质上是 int 类型数字编号
信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号。
每个信号都是以 SIGxxx 开头,如下所示:

/* 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). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31

二、信号的分类

Linux 系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号。

1、可靠信号与不可靠信号

Linux 下的不可靠信号问题主要指的是信号可能丢失。在 Linux 系统下,信号值小于 SIGRTMIN(34)的信号都是不可靠信号,这就是"不可靠信号"的来源。
在 Linux 系统下使用"kill -l"命令可查看到所有信号,如下所示:
Linux应用编程(信号基础)_第1张图片
2、实时信号与非实时信号

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

三、常见信号与默认行为

Linux应用编程(信号基础)_第2张图片

四、进程对信号的处理

1、signal()函数
本节描述系统调用 signal(),signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作。

#include 
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。

测试

#include 
#include 
#include 

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);
} 

Linux应用编程(信号基础)_第3张图片
我们再执行一次测试程序,这里将测试程序放在后台运行,然后再按下中断符:
Linux应用编程(信号基础)_第4张图片
按下中断符发现进程并没有收到 SIGINT 信号,原因很简单,因为进程并不是前台进程、而是一个后台进程,按下中断符时系统并不会给后台进程发送 SIGINT 信号。可以使用 kill 命令手动发送信号给我们的进程:
Linux应用编程(信号基础)_第5张图片
两种不同状态下信号的处理方式
通过上面的介绍,以及我们的测试实验,不知大家是否出现了一个疑问?如果程序中没有调用 signal()函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到 signal()处,那么这时进程接收到一个信号后是如何处理的呢?带着这个问题来聊一聊。

程序启动
当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。所以如果在我们的程序当中,没有调用 signal()为信号设置处理方式,则默认的处理方式便是系统默认操作。

所以为什么大家平时都可以使用 CTRL + C 中断符来终止一个进程,因为大部分情况下,应用程序中并不会为 SIGINT 信号设置处理方式,所以该信号的处理方式便是系统默认操作,当接收到信号之后便执行系统默认操作,而 SIGINT 信号的系统默认操作便是终止进程。

进程创建
当一个进程调用 fork()创建子进程时,其子进程将会继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中是有意义的。

2、sigaction()函数

除了signal()之外,sigaction()系统调用是设置信号处理方式的另一选择,事实上,推荐大家使用sigaction()函数。虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性。

#include 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

函数参数和返回值含义如下:
signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act:act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
oldact:oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值:成功返回 0;失败将返回-1,并设置 errno。

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_mask:参数 sa_mask 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一
个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现,如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成。
⚫ sa_restorer:该成员已过时,不要再使用了。
⚫ sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志(多个标志使用位或" | "组合):

SA_NOCLDSTOP
如果signum为SIGCHLD,则子进程停止时(即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU中的一种时)或恢复(即它们接收到 SIGCONT)时不会收到 SIGCHLD 信号。

SA_NOCLDWAIT
如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程。

SA_NODEFER
不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞。

SA_RESETHAND
执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。

SA_RESTART
被信号中断的系统调用,在信号处理完成之后将自动重新发起。

SA_SIGINFO
如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler,关于 sa_sigaction信号处理函数的参数信息。

siginfo_t 结构体

siginfo_t {
	 int si_signo; /* Signal number */
	 int si_errno; /* An errno value */
	 int si_code; /* Signal code */
	 int si_trapno; /* Trap number that caused hardware-generated signal(unused on most architectures) */
	 pid_t si_pid; /* Sending process ID */
	 uid_t si_uid; /* Real user ID of sending process */
	 int si_status; /* Exit value or signal */
	 clock_t si_utime; /* User time consumed */
	 clock_t si_stime; /* System time consumed */
	 sigval_t si_value; /* Signal value */
	 int si_int; /* POSIX.1b signal */
	 void *si_ptr; /* POSIX.1b signal */
	 int si_overrun; /* Timer overrun count; POSIX.1b timers */
	 int si_timerid; /* Timer ID; POSIX.1b timers */
	 void *si_addr; /* Memory location which caused fault */
	 long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
	 int si_fd; /* File descriptor */
	 short si_addr_lsb; /* Least significant bit of address(since Linux 2.6.32) */
	 void *si_call_addr; /* Address of system call instruction(since Linux 3.5) */
	 int si_syscall; /* Number of attempted system call(since Linux 3.5) */
	 unsigned int si_arch; /* Architecture of attempted system call(since Linux 3.5) */
}

测试

#include 
#include 
#include 

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);
}

Linux应用编程(信号基础)_第6张图片
关于信号处理函数说明
一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗 CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

五、向进程发送信号

与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;除了 kill()系统调用之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能。

1.1、kill()函数

kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程。

#include 
#include 
int kill(pid_t pid, int sig);

函数参数和返回值含义如下:
pid:参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或-1 以及小于-1 等不同值,稍后给说明。
sig:参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值:成功返回 0;失败将返回-1,并设置 errno。
参数 pid 不同取值含义:
⚫ 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
⚫ 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。
⚫ 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
⚫ 如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。

测试

#include 
#include 
#include 
#include 
#include 

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);
}

Linux应用编程(信号基础)_第7张图片
接收代码是此代码的上一个代码。

2、raise()

有时进程需要向自身发送信号,raise()函数可用于实现这一要求。

#include 
int raise(int sig);

3、alarm()和 pause()函数

使用 alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM信号,其函数原型如下所示:

#include 
unsigned int alarm(unsigned int seconds);

函数参数和返回值:
seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。
返回值:如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0

测试

#include 
#include 
#include 
#include 

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);
}

Linux应用编程(信号基础)_第8张图片
4、pause()函数

pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR。

#include 
int pause(void)

测试

#include 
#include 
#include 
#include 

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);
}

Linux应用编程(信号基础)_第9张图片

六、信号集

通常我们需要有一个能表示多个信号(一组信号)的数据类型—信号集(signalset),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如 sigaction()函数、sigprocmask()函数、sigpending()函数等。

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
	unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;

使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中。

1、初始化信号集

sigemptyset()和 sigfillset()用于初始化信号集。sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),函数原型如下:

#include 
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、向信号集中添加/删除信号

#include 
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);

3、测试信号是否在信号集中

#include 
int sigismember(const sigset_t *set, int signum);

函数参数和返回值含义如下:
set:指定信号集。
signum:需要进行测试的信号。
返回值:如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno。

七、获取信号的描述信息

#include 
#include 
#include 

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);
}

Linux应用编程(信号基础)_第10张图片
1、strsignal()函数

较之于直接引用 sys_siglist数组,更推荐使用 strsignal()函数,其函数原型如下所示:

#include 
char *strsignal(int sig);

使用示例

#include 
#include 
#include 
#include 
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);
}

Linux应用编程(信号基础)_第11张图片

2、psignal()函数

psignal()可以在标准错误(stderr)上输出信号描述信息,其函数原型如下所示:

#include 
void psignal(int sig, const char *s);

Linux应用编程(信号基础)_第12张图片

八、信号掩码(阻塞信号传递)

内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。

向信号掩码中添加一个信号,通常有如下几种方式:
⚫ 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
⚫ 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过 sa_mask 参数进行设置。
⚫ 除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号。

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

函数参数和返回值含义如下:
how:参数 how 指定了调用函数时的一些行为。
set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为NULL,则表示无需对当前信号掩码作出改动。
oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
返回值:成功返回 0;失败将返回-1,并设置 errno。

参数 how 可以设置为以下宏:
⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与 set 的并集。
⚫ SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
⚫ SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。

使用示例

从信号掩码中添加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 
#include 
#include 
#include 

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);
}

上述代码中,我们为 SIGINT 信号注册了一个处理函数 sig_handler,当进程接收到该信号之后就会执行它;然后调用 sigprocmask 函数将 SIGINT 信号添加到信号掩码中,然后再调用 raise(SIGINT)向自己发送一个 SIGINT 信号,如果信号掩码没有生效、也就意味着 SIGINT 信号不会被阻塞,那么调用 raise(SIGINT)之后应该就会立马执行 sig_handler 函数,从而打印出"执行信号处理函数…"字符串信息;如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在 2 秒后才执行,因为程序中使用 sleep(2)休眠了 2 秒钟之后,才将 SIGINT 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。Linux应用编程(信号基础)_第13张图片
1、阻塞等待信号 sigsuspend()

恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作,这正是 sigsuspend()系统调用的目的所在,sigsuspend()函数原型如下所示:

#include 
int sigsuspend(const sigset_t *mask);

函数参数和返回值含义如下:
mask:参数 mask 指向一个信号集。
返回值:sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT。

sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend()会将进程的信号掩码恢复成调用前的值。

调用 sigsuspend()函数相当于以不可中断(原子操作)的方式执行以下操作:

sigprocmask(SIG_SETMASK, &mask, &old_mask);		//将掩码集设为mask
pause();										//挂起信号
sigprocmask(SIG_SETMASK, &old_mask, NULL);		//恢复掩码

测试代码

#include 
#include 
#include 
#include 

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、sigpending()函数

#include 
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 信号未处于等待状态");

2、发送实时信号

等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之,如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一。

实时信号较之于标准信号,其优势如下:
⚫ 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用:SIGUSR1 和 SIGUSR2。
⚫ 内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反,对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。
⚫ 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。
⚫ 不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。

应用程序当中使用实时信号,需要有以下的两点要求:
⚫ 发送进程使用 sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。
⚫ 接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了。

#include 
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。

typedef union sigval
{
	int sival_int;
	void *sival_ptr;
} sigval_t;

携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针。

使用示例
(1)发送进程使用 sigqueue()系统调用向另一个进程发送实时信号

#include 
#include 
#include 

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()函数为信号绑定处理函数

#include 
#include 
#include 
#include 

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);
}

3、 异常退出 abort()函数

使用 exit()、_exit()或_Exit()这些函数来终止进程,然后这些方法使用于正常退出应用程序,而对于异常退出程序,则一般使用 abort()库函数,使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态。

#include 
void abort(void);

使用示例

#include 
#include 
#include 
#include 

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);
}

Linux应用编程(信号基础)_第14张图片
从打印信息可知,即使在我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止,无论阻塞或忽略 SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程。

你可能感兴趣的:(LinuxC编程(文件io,网络编程...),linux,linux应用开发)