信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,大多数情况下,是无法预测信号达到的准确时间。
信号的目的是用来通信的
一个具有合适权限的进程能够向另一个进程发送信号,是进程间通信(IPC)的原始形式。信号可以由“谁”发出?
⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,再由内核发送相应的信号给相关进程。硬件检测到异常的包括执行异常的机器语言指令,诸如,除数为 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()系统调用可用于注册信号的处理函数。
⚫ 执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,对大多数信号来说,系统默认的处理方式就是终止该进程。
信号是异步的
信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。
信号本质上是 int 类型数字编号
信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名(也就是宏定义)。
Linux 系统对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号。
4.2.1 可靠信号与不可靠信号Linux 的不可靠信号问题指的是信号可能丢失。在 Linux 系统下,信号值小于 SIGRTMIN(34)的信号都是不可靠信号,这就是"不可靠信号"的来源。可靠信号支持排队,不会丢失。
4.2.2 实时信号与非实时信号实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证发送的多个信号都能被接收。
一般我们也把非实时信号(不可靠信号)称为标准信号。
当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。Linux 系统提供了系统调用 sigaction()函数用于设置信号的处理方式。
4.3.1 sigaction()函数sigaction()允许单独获取信号的处理函数而不是设置,还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:
#includeint 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_mask:参数 sa_mask 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现,如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成。
⚫ sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程。具体标志请查询某度。
测试 test1
这里使用 sigaction()函数进行测试。
#include#include #include static void sig_handler(int sig) { printf("Received signal: %dn", 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 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。
与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程,其函数原型如下所示:
#include#include int kill(pid_t pid, int sig);
函数参数和返回值含义如下:
pid:参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;
sig:参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号。
进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root 进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID。
测试
使用 kill()函数向一个指定的进程发送信号。
#include#include #include #include #include int main(int argc, char *argv[]) { int pid; if (2 > argc) exit(-1); pid = atoi(argv[1]); printf("pid: %dn", pid); if (-1 == kill(pid, SIGINT)) { perror("kill error"); exit(-1); } exit(0); }
以上代码通过 kill()函数向指定进程发送 SIGINT 信号,可通过外部传参将接收信号的进程 pid 传入到程序中,再执行该测试代码之前,需要运行先一个用于接收此信号的进程,接收信号的进程直接使用示例代码 test1程序。
运行测试:test2
testApp1 是示例代码 test1 对应的程序,testApp 则是示例代码 test2对应的程序,首先执行"./testApp1
&“将接收信号的程序置于后台运行(其进程 pid 为 21825),接着行”./testApp 21825"向接收信号的进程发送 SIGINT 信号。
向大家介绍两个系统调用 alarm()和 pause()。
4.5.1 alarm()函数使用 alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM信号,其函数原型如下所示:
#includeunsigned int alarm(unsigned int seconds);
seconds:设置定时时间,以秒为单位;
参数 seconds 的值是产生 SIGALRM 信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,每个进程只能设置一个 alarm 闹钟;
虽然SIGALRM 信号的系统默认操作是终止进程,但是如果程序当中设置了 alarm 闹钟,但大多数使用闹钟的进程都会捕获此信号。
要注意的是 alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm()函数设置定时器。
测试
使用 alarm()来设计一个闹钟。
#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); 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); }
运行测试:
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回。
#includeint pause(void);
测试
通过 alarm()和 pause()函数模拟 sleep 功能。
#include4.6 信号集#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); 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); }
通常我们需要有一个能表示多个信号(一组信号)的数据类型—信号集(signalset),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如 sigaction()函数、sigprocmask()函数、sigpending()函数等。接下来向大家介绍信号集这个数据类型。
4.6.1 初始化信号集sigemptyset()初始化信号集,使其不包含任何信号;函数原型如下:
#includeint sigemptyset(sigset_t *set);
set:指向需要进行初始化的信号集变量。
使用示例
初始化为空信号集:
sigset_t sig_set; sigemptyset(&sig_set);4.6.2 向信号集中添加/删除信号
分别使用 sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号,函数原型如下:
#includeint sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum);
函数参数和返回值含义如下:
set:指向信号集。
signum:需要添加/删除的信号。
使用示例
向信号集中添加信号:
sigset_t sig_set; sigemptyset(&sig_set); sigaddset(&sig_set, SIGINT);
从信号集中移除信号:
sigset_t sig_set; sigfillset(&sig_set); sigdelset(&sig_set, SIGINT);4.7 信号掩码(阻塞信号传递)
内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
向信号掩码中添加一个信号,通常有如下几种方式:
⚫ 当应用程序调用 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
⚫ 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通 过 sa_mask 参数进行设置。
⚫ 除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号。
sigprocmask()函数原型如下所示:
#includeint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数参数和返回值含义如下:
how:参数 how 指定了调用函数时的一些行为。
set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为NULL,则表示无需对当前信号掩码作出改动。
oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
参数 how 可以设置为以下宏:
⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信
号掩码设置为当前值与 set 的并集。
⚫ SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
⚫ SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。
使用示例
将信号 SIGINT 添加到进程的信号掩码中:
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);
}
从信号掩码中移除 SIGINT 信号:
int ret;
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#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); 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 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。编译测试结果如下:
更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的关键代码段。如果希望对一个信号解除阻塞后,然后调用 pause()以等待之前被阻塞的信号的传递,这将如何?譬如有如下代码段:
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();
执行受保护的关键代码时不希望被 SIGINT 信号打断,所以在执行关键代码之前将 SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。最后调用了 pause()阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从 pause 返回继续执行;
4.9 实时信号等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之,如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一。
使用 sigqueue()函数发送实时信号,其函数原型如下所示:
#includeint sigqueue(pid_t pid, int sig, const union sigval value);
函数参数和返回值含义如下:
pid:指定接收信号的进程对应的 pid,将信号发送给该进程。
sig:指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进
程是否存在。
value:参数 value 指定了信号的伴随数据,union sigval 数据类型。
使用示例
(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: %dnsignal: %dn", pid, sig); sig_val.sival_int = 10; //伴随数据 if (-1 == sigqueue(pid, sig, sig_val)) { perror("sigqueue error"); exit(-1); } puts("信号发送成功!"); exit(0); }
(2)接收进程使用 sigaction()函数为信号绑定处理函数
#include4.10 异常退出 abort()函数#include #include #include static void sig_handler(int sig, siginfo_t *info, void *context) { sigval_t sig_val = info->si_value; printf("接收到实时信号: %dn", sig); printf("伴随数据为: %dn", 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); }
给大家介绍了应用程序中结束进程的几种方法,譬如使用 exit()使用于正常退出应用程序,而对于异常退出程序,则一般使用 abort()库函数,使用 abort()终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()时的程序状态。
abort()函数原型如下所示:
#includevoid abort(void);
函数 abort()通常产生 SIGABRT 信号来终止调用该函数的进程,SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。
#include#include #include #include static void sig_handler(int sig) { printf("接收到信号: %dn", 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); }
运行测试:
从打印信息可知,即使在我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止,无论阻塞或忽略 SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程。



