Linux信号基础(1)——软件也能实现中断?

基本概念

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

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

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

信号的分类

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

进程对信号的处理

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

(1)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_DFL(系统默认操作)。

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

        测试

        

#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);
    }
    /* 等待SIGINT(ctrl+C)信号 */
    for ( ; ; ) { }

    exit(0);
}

        在上述示例代码中,我们通过 signal()函数将 SIGINT信号绑定到了处理函数sig_handler(int sig),当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句。

        运行测试:

         这里出现一个有趣的问题,平时大家使用 ctrl + C 可以终止一个进程,而这里却不能通过这种方式来终止这个测试程序,原因在于测试程序中捕获了该信号,而对应的处理方式仅仅只是打印一条语句、而并不终止进程。

        那此时该怎么关闭这个测试程序呢?可使用信号 SIGKILL(编号为 9),向该进程发送 SIGKILL 暴力终止该进程, 新打开一个终端,使用 ps 命令找到该进程的 pid 号,再使用 kill 命令,如下所示:

        

        此时测试程序就会强制终止:

(2)sigaction()函数

        除了signal()之外,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。

        struct sigaction 结构体

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信号处理函数的参数信息。

测试

        这里使用 sigaction()函数实现与上文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[])
{
    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 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值