⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为 0 、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。⚫ 用于在终端下输入了能够产生信号的特殊字符。譬如在终端上按下 CTRL + C 组合按键可以产生中断信号(SIGINT ),通过这个方法可以终止在前台运行的进程;按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT ),通过这个方法可以暂停当前前台运行的进程。⚫ 进程调用 kill() 系统调用可将任意信号发送给另一个进程或进程组。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是 root 超级用户。⚫ 用户可以通过 kill 命令将信号发送给其它进程。 kill 命令想必大家都会使用,通常我们会通过 kill命令来“杀死”(终止)一个进程,譬如在终端下执行"kill -9 xxx" 来杀死 PID 为 xxx 的进程。 kill命令其内部的实现原理便是通过 kill() 系统调用来完成的。⚫ 发生了软件事件,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为 0 、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况)。
⚫ 忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是 SIGKILL 和 SIGSTOP ,这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。⚫ 捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理, Linux 系统提供了 signal() 系统调用可用于注册信号的处理函数,将会在后面向大家介绍。⚫ 执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,8.3 小节中对此有进行介绍。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。
信号的分类
从可靠性方面将信号分为可靠信号与不可靠信号;

从实时性方面将信号分为实时信号与非实时信号;
常见信号

进程对信号的处理
#include <signal.h>typedef void (*sig_t)(int);sig_t signal(int signum, sig_t handler);使用该函数需要包含头文件 <signal.h> 。signum : 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。handler : sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN 或 SIG_DFL , SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。 sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。
#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);
/*自定义了一个信号处理函数 sig_handler,并将信号SIGINT(ctrl+c)绑定到该信号处理函数上*/
}
sigaction()函数
推荐大家使用sigaction()函数,sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
使用该函数需要包含头文件<signal.h>。
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
⚫ sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取,稍后介绍该数据结构; sa_handler 和sa_sigaction 是互斥的,不能同时设置, 对于标准信号来说, 使用 sa_handler 就可以了,可通过SA_SIGINFO 标志进行选择。
⚫ sa_mask: 参数 sa_mask 定义了一组信号, 当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。 当进程在执行信号处理函数期间, 可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现, 如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。 在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号
时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成,信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。⚫ 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信号处理函数的参数信息。
#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;
//注意此处与signal的不同之处在于这里是 取地址
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret) {
perror("sigaction error");
exit(-1);
}
/* 死循环 */
for ( ; ; ) { }
exit(0);
}
一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗 CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。
向进程发送信号
#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);使用该函数需要包含头文件 <sys/types.h> 和 <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 。
#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);
}
//与signational例程配合
raise()函数
有时进程需要向自身发送信号,raise()函数可用于实现这一要求,raise()函数原型如下所示(此函数为 C 库函数):
#include <signal.h>int raise(int sig);使用该函数需要包含头文件 <signal.h> 。sig : 需要发送的信号。返回值: 成功返回 0 ;失败将返回非零值。
#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()函数
#include <unistd.h>unsigned int alarm(unsigned int seconds);seconds : 设置定时时间,以秒为单位;如果参数 seconds 等于 0 ,则表示取消之前设置的 alarm 闹钟。返回值: 如果在调用 alarm() 时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm() 函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0 。
#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;
/* 检验传参个数
argc 为传入main函数的参数个数 */
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()函数
#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);
}
信号集
#include <signal.h>int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);使用这些函数需要包含头文件 <signal.h> 。set : 指向需要进行初始化的信号集变量。返回值: 成功返回 0 ;失败将返回 -1 ,并设置 errno 。
使用示例初始化为空信号集:sigset_t sig_set;sigemptyset(&sig_set);初始化信号集,使其包含所有信号:sigset_t sig_set;sigfillset(&sig_set);
向信号集中添加/删除信号
#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 信号 ");
#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);
}
#include <string.h>char *strsignal(int sig);使用 strsignal() 函数需要包含头文件 <string.h> ,这是一个库函数。调用 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 );}