Linux-C/C++《C/9、信号:基础》(基本概念、信号分类、信号传递等)

发布于:2025-02-22 ⋅ 阅读:(10) ⋅ 点赞:(0)

       

        本章将讨论信号,虽然信号的基本概念比较简单,但是其所涉及到的细节内容比较多,所以本章篇幅也会相对比较长。事实上,在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法,所以信号机制在 Linux 早期版本中就已经提供了支持,随着 Linux 内核版本的更新迭代,其对信号机制的支持更加完善。

        本章将会讨论如下主题内容。

         信号的基本概念;

         信号的分类、Linux 提供的各种不同的信号及其作用;

         发出信号以及响应信号,信号由“谁”发送、由“谁”处理以及如何处理;

         进程在默认情况下对信号的响应方式;

         使用进程信号掩码来阻塞信号、以及等待信号等相关概念;

         如何暂停进程的执行,并等待信号的到达。

1、信号基础

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

信号的目的是用来通信的

        一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(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()系统调用可用于注册信号的处理函数,将会在后面向大家介绍。

         执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,8.3 小节中对此有进行介绍。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。

信号是异步的

        信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当

前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。

信号本质上是 int 类型数字编号

        信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名(也就是宏定义)。

        这些信号在<signum.h>头文件中定义,每个信号都是以 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

        不存在编号为 0 的信号,从示例代码 8.1.1 中也可以看到,信号编号是从 1 开始的,事实上 kill()函数对信号编号 0 有着特殊的应用,关于这个文件将会在后面的内容向大家介绍。

2、信号的分类

        Linux 系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号,本小节将对这些信号的分类进行简单地介绍。

2.1 可靠信号与不可靠信号

        Linux 信号机制基本上是从 UNIX 系统中继承过来的,早期 UNIX 系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,它的主要问题是:

         进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新为该信号绑定相应的处理函数。

         因此导致,早期 UNIX 下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。

Linux 支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()。因此,Linux 下的不可靠信号问题主要指的是信号可能丢失。在 Linux 系统下,信号值小于 SIGRTMIN(34)的信号都是不可靠信号,这就是"不可靠信号"的来源,所以示例代码 8.1.1 中所列举的信号都是不可靠信号。

        随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种 UNIX版本分别在这方面进行了研究,力图实现"可靠信号"。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号(SIGRTMIN~SIGRTMAX),并在一开始就把它们定义为可靠信号,在 Linux 系统下使用"kill -l"命令可查看到所有信号,如下所示:

2.2 实时信号与非实时信号

        实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。

        一般我们也把非实时信号(不可靠信号)称为标准信号,如果文档中用到了这个词,那么大家要知道,这里指的就是非实时信号(不可靠信号)。关于更多实时信号相关内容将会在 8.10 小节中介绍。

3、进程对信号的处理

        当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。Linux 系统提供了系统调用 signal()sigaction()两个函数用于设置信号的处理方式,本小节将向大家介绍这两个系统调用的使用方法。

3.1 signal()函数

        本节描述系统调用 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_DFLSIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。

sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。

Tips:SIG_IGN、SIG_DFL 分别取值如下:

/* Fake signal functions. */

#define SIG_ERR ((sig_t) -1)

/* Error return. */

#define SIG_DFL ((sig_t) 0)

/* Default action. */

#define SIG_IGN ((sig_t) 1)

/* Ignore signal. */

        返回值:此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno

        由此可知,signal()函数可以根据第二个参数 handler 的不同设置情况,可对信号进行不同的处理。

        测试
        signal()函数的用法其实非常简单,为信号设置相应的处理方式,接下来编写一个简单地示例代码对signal()函数进行测试。
#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()函数将 SIGINT2)信号绑定到了一个用户自定的处理函数上sig_handler(int sig),当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句。当运行程序之后,程序会卡在 for 死循环处,此时在终端按下中断符 CTRL + C,系统便会给前台进程组中的每一个进程发送SIGINT 信号,我们测试程序便会收到该信号。

         程序启动

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

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

         进程创建

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

3.2 sigaction()函数

        除了signal()之外,sigaction()系统调用是设置信号处理方式的另一选择,事实上,推荐大家使用sigaction()函数。虽然 signal()函数简单好用,而 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 数据结构,该数据结构描述了信 号的处理方式,稍后介绍该数据结构;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如 果参数 act NULL,则表示无需改变信号当前的处理方式。

        oldact:oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL

        返回值:成功返回 0;失败将返回-1,并设置 errno

        使用 man 手册查看 sigaction()函数帮助信息时,在下面会有介绍。

        测试

        这里使用 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;
 ret = sigaction(SIGINT, &sig, NULL);
 if (-1 == ret) {
 perror("sigaction error");
 exit(-1);
 }
 /* 死循环 */
 for ( ; ; ) { }
 exit(0);
}

关于信号处理函数说明

        一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗 CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

4、向进程发送信号

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

4.1 kill()函数

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

        参数 pid 不同取值含义:

         如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。

         如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。

         如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1init)除外。

         如果 pid 小于-1,则将 sig 发送到 ID -pid 的进程组中的每个进程。

        进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root 进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID

        从上面介绍可知,当 sig 0 时,仍可进行正常执行的错误检查,但不会发送信号,这通常可用于确定一个特定的进程是否存在,如果向一个不存在的进程发送信号,kill() 将会返回 -1 errno 将被设置为 ESRCH ,表示进程不存在。
        测试
        (1)使用 kill() 函数向一个指定的进程发送信号。
#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);
}

        以上代码通过 kill()函数向指定进程发送 SIGINT 信号,可通过外部传参将接收信号的进程 pid 传入到 程序中,再执行该测试代码之前,需要运行先一个用于接收此信号的进程,接收信号的进程直接使用sigaction()示例代码

4.2 raise()

        有时进程需要向自身发送信号,raise()函数可用于实现这一要求,raise()函数原型如下所示(此函数为 C库函数):

#include <signal.h>
int raise(int sig);

        函数参数和返回值含义如下:

        sig:需要发送的信号。

        返回值:成功返回 0;失败将返回非零值。

        raise()其实等价于:

kill(getpid(), sig);

        Tips:getpid()函数用于获取进程自身的 pid。

        测试

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

5、alarm()pause()函数

5.1 alarm()函数

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

        函数参数和返回值:

        seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。

        返回值:如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0

        参数 seconds 的值是产生 SIGALRM 信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,每个进程只能设置一个 alarm 闹钟;虽然 SIGALRM 信号的系统默认操作是终止进程,但是如果程序当中设置了 alarm 闹钟,但大多数使用闹钟的进程都会捕获此信号。

        需要注意的是 alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm()函数设置定时器。

5.2 pause()函数

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

#include <unistd.h>
int pause(void);

6、异常退出 abort()函数

        abort()函数原型如下所示:
        
#include <stdlib.h>
void abort(void);
        函数 abort() 通常产生 SIGABRT 信号来终止调用该函数的进程, SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用 abort() 函数之后,内核会向进程发送 SIGABRT 信号。

网站公告

今日签到

点亮在社区的每一天
去签到