Linux应用编程基础03-信号

在很多应用程序中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法

1、信号概念

信号是事件发生时对进程的通知机制,也可以把它称为软件中断,能够打断程序当前执行的正常流程(其实是在软件层次上对中断机制的一种模拟)

1.1 信号的目的是用来通信的

一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式

以下情况可以产生信号:

  • 除数为 0、数组访问越界导致引用 了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况 发生时正在运行的进程发送适当的信号以通知进程
  • 在终端下输入了能够产生信号的特殊字符。譬如在终端上按下 CTRL + C 组合按键可以产生中 断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下 CTRL + Z 组合按键可以产生 暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程
  • 进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组(注意权限)
  • 用户可以通过 kill 命令将信号发送给其它进程。比如通过 kill命令来"杀死"(终止)一个进程,譬如在终端下执行"kill -9 xxx"来杀死 PID 为 xxx 的进程。kill命令其内部的实现原理便是通过 kill()系统调用来完成的
  • 触发了某种软件条件(进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况)

总的来看,信号的目的都是用于通信的,当发生某种情况下,通过信号将情况"告知"相应的进程,从而达到同步、通信的目的。

1.2 信号由谁处理、怎么处理

信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:

  • 忽略信号:当信号到达进程后,该进程并不会去理会它,如果忽略某些由硬件异常产生的信 号,则进程的运行行为是未定义的
  • 捕获信号:当信号到达进程后,执行预先注册好的信号处理函数
  • 系统默认操作:进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其 对应的系统默认的处理方式(大多数信号系统默认是终止进程)

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

信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号

由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用宏

这些信号在头文件中定义,每个信号都是以 SIGxxx 开头

c 复制代码
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
...

2、信号分类

从可靠性方面将信号分为可靠信号与不可靠信号

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

2.1 可靠信号与不可靠信号

不可靠信号

Linux信号机制是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,存在许多问题。

把那些建立在早期机制上的信号叫做"不可靠信号"

信号值小于SIGRTMIN(SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。

信号可能丢失:如果在进程对某个信号进行处理时,这个信号发生多次,对后到来的这类信号不排队,那么仅传送该信号一次,即发生了信号丢失。Linux下的不可靠信号问题主要指的是信号可能丢失

可靠信号

由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些可靠信号克服了信号可能丢失的问题

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号。

编号 1-31 所对应的是不可靠信号,编号 34-64 对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAX-N 的方式来表

2.2 实时信号与非实时信号

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的:

  • 非实时信号(也称标准信号)都不支持排队,都是不可靠信号
  • 实时信号都支持排队,都是可靠信号

实时信号保证了发送的多个信号都能被接收

3、常见信号与默认行为

  • SIGINT:当用户在终端按下中断字符(通常是 CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行
  • SIGILL:如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默 认操作是终止进程的运行
  • SIGFPE:该信号因特定类型的算术错误而产生,譬如除以 0。
  • SIGKILL:该信号的系统默认操作是终止进程此信号为"必杀(sure kill)"信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而"一击必杀",总能终止进程。使用 SIGINT 信号和 SIGQUIT 信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号
  • SIGTERM:这是用于终止进程的标准信号,也是 kill 命令所发送的默认信号(kill xxx,xxx 表示进程 pid),有时 我们会直接使用"kill -9 xxx"显式向进程发送 SIGKILL 信号来终止进程,然而这一做法通常是错误的,精心 设计的应用程序应该会捕获 SIGTERM 信号、并为其绑定一个处理函数,当该进程收到 SIGTERM 信号时, 会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用 SIGKILL 信号终止进程, 从而跳过了 SIGTERM 信号的处理函数,通常 SIGKILL 终止进程是不友好的方式、是暴力的方式,这种方 式应该作为最后手段,应首先尝试使用 SIGTERM,实在不行再使用最后手段 SIGKILL
  • SIGSEGV:当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对 内存无效引用的原因很多,C 语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化 的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程
  • SIGPOLL/SIGIO:用于提示一个异步 IO 事件的发生,譬如应用程序打开的文件描述符发生了 I/O 事件时,内核会向应用程序发送 SIGIO 信号
  • SIGCHLD:当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号
  • ...

4、信号处理

当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作

Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式

4.1 signal

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

c 复制代码
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);

/*
* signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号
* handler:sig_t类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数
*     -sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号
*     -也可以设置为 SIG_IGN 或 SIG_DFL
*           SIG_IGN 表示此进程需要忽略该信号
*           SIG_DFL 则表示设置为系统默认操作
* 返回值:此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno。
*/

测试:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
// 用户自定处理函数
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}

int main()
{
    sig_t ret = NULL;
    // 将 SIGINT(2)信号绑定到了一个用户自定的处理函数上 sig_handler(int sig),当进程收到 SIGINT 信号后会执行该函数然
    ret = signal(SIGINT, (sig_t)sig_handler);
    if (SIG_ERR == ret) {
        perror("signal error");
        exit(-1);
    }
    // 死循环
    while(1);

    exit(0);
}

最后使用kill 9命名杀死进程

再执行一次测试程序,这里将测试程序放在后台运行,按下中断符发现进程并没有收到 SIGINT 信号,原因很简单,因为进程并不是前台进程、而是一个后台进程,按下中断符时系统并不会给后台进程发送 SIGINT 信号。可以使用 kill 命令手动发送信号给我们的进程(普通用户只能杀死该用户自己的进程,无权限杀死其它用户的进程)

4.2 sigaction

相比之下,sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性

sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制

c 复制代码
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

/*
* signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号
* act:一个 struct sigaction 类型指针,该数据结构描述了信号的处理方式;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式
* oldact:也是一个 struct sigaction 类型指针,如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL
* return:成功返回 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:也用于指定信号处理函数(不能和sa_handler同时使用,可以通过sa_flags进行选择),这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取
* sa_mask:参数 sa_mask 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,如果进程接收到出于信号掩码中的信号,那么这个信号就会被阻塞,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除
*        -信号掩码: 通常我们在执行信号处理函数期间不希望被另一个信号所打断,就可以使用信号掩码。如果在某一时刻信号如果包含在进程掩码中,则进程会暂时屏蔽此信号,直到其从进程信号掩码移除为止。
* sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志
*        -SA_NOCLDSTOP:如果 signum 为 SIGCHLD,则子进程停止或恢复时不会收到SIGCHLD信号
*        -SA_NOCLDWAIT:如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程
*        -SA_NODEFER:不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞
*        -SA_RESETHAND:执行完信号处理函数之后,将信号的处理方式设置为系统默认操作
*        -SA_RESTART:被信号中断的系统调用,在信号处理完成之后将自动重新发起
*        -SA_SIGINFO:如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是sa_handler
*/

注意:在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成(此参数是 sigset_t 类型变量,是信号集),信号掩码可以避免一些信号之间的竞争状态(也称为竞态)

测试:

c 复制代码
#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);
    }
    /* 死循环 */
    while (1);
    
    exit(0);
}

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

5、发送信号

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

5.1 kill()

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

c 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

/*
* 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
*/

测试:

c 复制代码
#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信号

5.2 raise()

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

c 复制代码
#include <signal.h>
int raise(int sig);
/*
 * 等价于:
 * kill(getpid(), sig);
 * getpid获取自身的pid
 */

5.3 alarm()

该函数是个系统调用,alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM信号

c 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds); // 以秒为单位

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

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

5.4 pause()

该函数是个系统调用,可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止

只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR

c 复制代码
#include <unistd.h>
int pause(void);

alarm和pause模拟sleep

c 复制代码
#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);
}

6、信号集

需要有一个能表示多个信号(一组信号)的数据类型------信号集(signalset)

信号集其实就是 sigset_t 类型数据结构

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

6.1 初始化信号集

sigemptyset()和 sigfillset()用于初始化信号集

  • sigemptyset()初始化信号集,使其不包含任何信号
  • sigfillset()初始化信号集,使其包含所有信号(包括所有实时信号)
c 复制代码
#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

6.2 添加/删除信号

sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号

c 复制代码
#include <signal.h>

int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

6.3 测试信号是否在集合中

sigismember()函数可以测试某一个信号是否在指定的信号集中

c 复制代码
#include <signal.h>
int sigismember(const sigset_t *set, int signum);

/*
* 返回值:如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno。
*/

7、信号掩码

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

向信号掩码中添加信号的方式:

  • 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中(对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定)
  • 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中
  • 还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号
c 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

/*
* set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为NULL,则表示无需对当前信号掩码作出改动。
* oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
* how:指定了调用函数时的行为
*     -SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中
*     -SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除
*     -SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集
*/

测试:先把SIGINT信号添加到信号掩码中,然后向自己发送SIGINT信号(被阻塞),sleep2秒后,把SIGINT信号从信号掩码中移除,移除后进程才会处理该信号,在移除之前接收到该信号会将其阻塞

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    puts("Processing signal...");
}
int main(int argc, char *argv[])
{
    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("sleep over.\n");

    /* 从信号掩码中移除添加的信号 */
    if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
        exit(-1);
    
    // 因为程序中使用 sleep(2)休眠了 2 秒钟之后,才将 SIGINT 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞
    exit(0);
}

运行结果:执行受保护的关键代码时不希望被 SIGINT 信号打断,所以在执行关键代码之前将 SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码,最后调用了 pause()阻塞等待被信号唤醒。

8、阻塞等待信号 sigsuspend

存在如下代码:执行受保护的关键代码时不希望被 SIGINT 信号打断,所以在执行关键代码之前将 SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码,最后调用了 pause()阻塞等待被信号唤醒。

c 复制代码
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();/* 等待信号唤醒 */

但是会产生一个问题,如果在恢复信号掩码后,进入pause()前,SIGINT信号传递过来,SIGINT信号处理函数返回后又回到主程序继续执行,然后才到pause()被阻塞,直到下一次信号发生时才会被唤醒,这有违代码的本意

要避免这个问题,需要将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作,这正是 sigsuspend()系统调用的目的所在

c 复制代码
#include <signal.h>
int sigsuspend(const sigset_t *mask);

sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回并将进程的信号掩码恢复成调用前的值,相当于以不可中断(原子操作)的方式执行以下操作

c 复制代码
sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);

测试:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    puts("Processing signal...");
}
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)) // 由于wait_mask为空,可被任意信号唤醒
        exit(-1);

    /* 恢复信号掩码 */
    if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
        exit(-1);
    exit(0);
}

9、实时信号

9.1 sigpending

如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()函数获取

c 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

/*
* set:处于等待状态的信号会存放在参数 set 所指向的信号集中
* 返回值:成功返回 0;失败将返回-1,并设置 errno
*/

使用

c 复制代码
sigset_t sig_set;
sigemptyset(&sig_set); // 将信号集初始化为空
sigpending(&sig_set); //获取当前处于等待状态的信号
if (1 == sigismember(&sig_set, SIGINT))
    puts("SIGINT 信号处于等待状态");

9.2 实时信号

等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。如果一个信号在阻塞状态下产生了多次,但是恢复后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一。

实时信号较之于标准信号(非实时信号),其优势如下:

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

Linux 内核定义了31个不同的实时信号,信号编号范围为 34~64,使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数

应用程序当中使用实时信号,需要有以下的两点要求:

  1. 发送进程使用 sigqueue()系统调用向另一个进程发送实时信号以及伴随数据
  2. 使用sigaction函数为信号建立处理函数,并加入SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction 指针指向的处理函数,而不是sa_handler(这样不能获取到实时信号的伴随数据)
c 复制代码
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

/*
* pid:将实时信号发送的进程
* sig:指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在
* value:参数 value 指定了信号的伴随数据,union sigval 数据类型,携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针
*/
typedef union sigval{
    int sival_int;
    void *sival_ptr;
} sigval_t;

9.3 测试实时信号

测试:在被阻塞情况下,向同一个进程发送多次相同的信号

exec1.cpp

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    union sigval sig_val; // 伴随数据
    int pid;              // 进程号
    int sig;              // 发送的信号

    if (argc < 4)
        exit(-1);

    // 获取需要发送的进程号、信号和伴随数据
    pid = atoi(argv[1]);
    sig = atoi(argv[2]);// 注意:发送的信号必须是实时信号(大于SIGMIN)
    sig_val.sival_int = atoi(argv[3]);

    // 发送实时信号
    if (sigqueue(pid, sig, sig_val) == -1){
        perror("sigqueue error");
        exit(-1);
    }
    printf("向进程:%d 发送实时信号:%d\n", pid, sig);
    exit(0);
}

exec2.cpp

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
/*
 * void (*sa_sigaction)(int, siginfo_t *, void *);
 * 可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取
 */
void sig_handler1(int sig, siginfo_t *info, void *context)
{
    printf("被信号: %d,唤醒!\n", sig);
}
void sig_handler2(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 sig_value; // 要接收处理的实时信号
    if (argc < 2)
        exit(-1);
    sig_value = atoi(argv[1]);

    // 信号2处理函数(处理sig_value实时信号)
    sig.sa_flags = SA_SIGINFO; //如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是sa_handler
    sig.sa_sigaction = sig_handler2;
    if (sigaction(sig_value, &sig, NULL) == -1){
        perror("sigaction error");
        exit(-1);
    }   

    // 信号处理函数1,使用SIGINT唤醒程序
    sig.sa_sigaction = sig_handler1;
    if (sigaction(SIGINT, &sig, NULL) == -1){
        perror("sigaction error");
        exit(-1);
    }

    /* 挂起、等待信号唤醒,sig_value除外 */
    sigset_t wait_mask;
    sigemptyset(&wait_mask);
    sigaddset(&wait_mask, sig_value);
    if (-1 != sigsuspend(&wait_mask))
        exit(-1);

    printf("程序结束!\n");
    exit(0);
}
相关推荐
内核程序员kevin2 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ7 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream7 小时前
Linux的桌面
linux
xiaozhiwise7 小时前
Makefile 之 自动化变量
linux
意疏10 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu10 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿10 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K840911 小时前
Flink整合Hudi及使用
linux·服务器·flink
19004311 小时前
linux6:常见命令介绍
linux·运维·服务器