信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法。举一个不恰当的例子,比如你正兴奋在玩游戏,突然你手机响了,你立马放下手上的游戏,去接听电话。手机随时都会响,随时都会中断你当下的事情。所以称之为异步事件。

虽然信号是一种低级的IPC方式,但同时它保持了很简单的特性。在一些大型服务端程序中,很多时候也要考虑信号造成的影响。这里还是值得一学的。下面对信号进行介绍。

信号概述

每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。

在终端上kill -l来查看信号的名字以及序号。
在这里插入图片描述

Linux 下的信号分为可靠信号不可靠信号,或称为实时信号非实时信号,信号是从1开始编号的,不存在0号信号。对应于 Linux 的信号值为 1-31 和 34-64。0信号用来测试对应进程是否存在或者是否由权限给其发送信号。

可靠信号:也是阻塞信号,当发送了一个阻塞信号,并且该信号的动作时系统默认动作或捕捉该信号,如果信号从发出以后会一直保持未决的状态,直到该进程对此信号解除了阻塞,或将对此信号的动作更改为忽略。

不可靠信号:信号可能会丢失,一旦信号丢失了,进程并不能知道信号丢失。

信号的处理有三种方法

1、忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景
2、捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
3、系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。

具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。

man 7 signal

在这里插入图片描述

一般程序收到信号时进程的现象

停止进程
终止(kill)进程
忽略信号
等等。。。

常用信号类型即默认行为

信号名称 说明 默认动作

SIGINT Ctrl-C终端下产生 终止当前进程
SIGABRT 产生SIGABRT信号 默认终止进程,并产生core(核心转储)文件
SIGALRM 由定时器如alarm(),setitimer()等产生的定时器超时触发的信号 终止当前进程
SIGCHLD 子进程结束后向父进程发送 忽略
SIGBUS 总线错误,即发生了某种内存访问错误 终止当前进程并产生核心转储文件
SIGKILL 必杀信号,收到信号的进程一定结束,不能捕获 终止进程
SIGPIPE 管道断裂,向已关闭的管道写操作 进程终止
SIGIO 使用fcntl注册I/O事件,当管道或者socket上由I/O时产生此信号 终止当前进程
SIGQUIT 在终端下Ctrl-\产生 终止当前进程,并产生core文件
SIGSEGV 对内存无效的访问导致即常见的“段错误” 终止当前进程,并产生core文件
SIGSTOP 必停信号,不能被阻塞,不能被捕捉 停止当前进程
SIGTERM 终止进程的标准信号 终止当前进程

信号处理函数注册与发送

信号处理函数的注册有两种方法分别是:

#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);
#include <signal.h>

int sigaction(int sig, const struct sigaction *restrict act,
       struct sigaction *restrict oact);

信号发送函数也有两个:

#include <signal.h>

int kill(pid_t pid, int sig);
#include <signal.h>

int sigqueue(pid_t pid, int signo, union sigval value);

信号函数

UNIX系统信号机制最简单的接口是signal函数。

#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);

signal函数原型说明此函数要求型数,返回一个函数指针,而该指针指向的函数无返回值,第一个参数sig是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整形参数,无返回值。

设计信号函数需要注意哪些

1、一般而言,信号处理函数设计的越简单越好,因为当前代码的执行逻辑被打断,最好尽快恢复到刚才被打断之前的状态。从而避免竞争条件的产生。
2、在信号处理函数中,建议不要调用printf等与I/O相关的函数。
3、在信号处理函数中,不要使用任何不可重入的函数

实例

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>

#define     MSG         "Catch signal SIGINT processing \n"
#define     MSG_END     "Finished process SIGINT return \n"

void time_fun() ;

static void sig_handler (int signuum ) 
{

    /*
    在信号处理程序中,尽量不要调用与标准IO相关的和不可重入的函数。

    STDIN_FILENO:接收键盘的输入

    STDOUT_FILENO:向屏幕输出
     */
    
    write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
    time_fun();
    write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}


void time_fun() 
{
    long long s = 0 ;
    long long i ;

    for ( i= 0 ; i < 500000000L ; i++ ) 
    {
        s += i ;    
    }
}


int main(int argc, char **argv) 
{

    // 注册信号处理函数
    
    if ( SIG_ERR == signal ( SIGINT , sig_handler ) ) 
    {
        fprintf (stderr , "signal error ") , perror ("") ;
        exit (1) ;
    }

    // 让主程序不退出,挂起,等待信号产生
    while (1) 
    {
        pause () ; //使调用进程在接到一信号前挂起。
    }

    return 0 ;
}

编译输出:
在这里插入图片描述

简单的总结一下,我们通过 signal 函数注册一个信号处理函数,如果系统通过 ctrl+c 产生了一个 SIGINT(中断信号),显然不是所有程序同时结束,那么,信号一定需要有一个接收者。对于处理信号的程序来说,接收者就是自己。

注册信号处理函数

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>


#define     MSG         "Catch signal SIGINT processing \n"
#define     MSG_END     "Finished process SIGINT return \n"

void time_fun() ;

static void sig_handler (int signuum ) 
{

    /*
    在信号处理程序中,尽量不要调用与标准IO相关的和不可重入的函数。

    STDIN_FILENO:接收键盘的输入

    STDOUT_FILENO:向屏幕输出
     */
    
    write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
    time_fun();
    write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}


void time_fun() 
{
    long long s = 0 ;
    long long i ;

    for ( i= 0 ; i < 500000000L ; i++ ) 
    {
        s += i ;    
    }
}


int main(int argc, char **argv) 
{
       /*
     struct sigaction {
     void     (*sa_handler)(int);
     void     (*sa_sigaction)(int, siginfo_t *, void *);
     sigset_t   sa_mask;
     int        sa_flags;
     };
     */

    // 注册信号处理函数
    struct sigaction  newact;

    // 将信号处理函数执行期间掩码设置为空
    sigemptyset (&newact.sa_mask );
    // 将标志设置为0即默认
    newact.sa_flags = 0;
    // 注册信号处理函数
    newact.sa_handler = sig_handler ;

    if ( 0 > sigaction ( SIGINT , &newact , NULL ) ) {
        fprintf (stderr , "sigaction error ") , perror ("") ;
        exit (1) ;
    }
    // 让主程序不退出,挂起,等待信号产生
    while (1) {
        pause () ;
    }

    return 0 ;
}

编译运行

在这里插入图片描述

执行效果和上面的一样。

如何如何发送信号

1、在终端下可以用kill/killall命令发送(缺省为SIGTERM信号),如下:

在这里插入图片描述

kill -SIGKILL 2582

在这里插入图片描述

在这里插入图片描述

2、可以在终端下使用Ctrl+C(SIGINT信号),Ctrl+(SIGQUIT信号).Ctrl+z(SIGSTOP信号)等

3、在程序中可以使用 kill、sigqueue 等。

总结

信号用于大多数复杂的应用程序中,理解进行信号的处理的原因和方式对于Unix编程及其重要。

在信号处理函数中,建议不要调用printf等与I/O相关的函数。不然会使得信号处理函数的执行时间变长。
在信号处理函数中,不要使用任何不可重入的函数。

下一篇讲解信号的各种用法。

参考:《Unix环境高级编程 第三版》

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐