进程间通信之-信号signal--linux内核剖析(九)

信号及信号来源


什么是信号


信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。一个信号的产生叫生成,接收到一个信号叫捕获。

信号本质


信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号来源


信号事件的发生有两个来源

  • 硬件来源(比如我们按下了键盘或者其它硬件故障);

  • 软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了那些系统事件。

如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递个它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞取消时才被传递给进程。

linux产生信号的条件


  1. 当用户按某些终端键时,将产生信号。
    终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT,终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT,终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。

  2. 硬件异常将产生信号。
    比如数据运算时,除数为0;或者无效的存放访问等.这些条件通常由硬件检测到,并通知内核,然后内核为该条件发生时正在运行的进程产生适当的信号.。

  3. 软件异常将产生信号。
    当检测到某种软件条件已发生,并将其通知有关进程时,产生信号。

  4. 调用 kill() 函数将发送信号。
    注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

  5. 运行 kill 命令将发送信号。
    此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。

信号的捕获和处理


若内核(空间)向用户空间(进程)发出某个信号时,用户空间(进程)可按照下列3中方式来面对:

  1. 忽略信号,即对信号不做任何处理
    大多数信号都可以使用这种方式处理,但信号SIGKILL和SIGSTOP绝不能被忽略.因为它们向超级用户提供了一种使进程终止的可靠方法.

  2. 缺省动作,执行信号的默认动作.大多数信号的系统默认动作是终止在进程.

  3. 捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数;

注意,进程对实时信号的缺省反应是进程终止。

Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。

信号是一种软件中断机制,即当信号发生时,必须用中断的方法告诉内核”请执行下列操作”.

在linux终端内输入kill -l可以查看系统所支持的信号.可以看出,每个信号的名字都是以SIG开头.

进程间通信之-信号signal--linux内核剖析(九)_第1张图片

在头文件signal.h(/usr/include/bits/signum.h)中,这些信号都被定义为正整数,即每个信号和一个数字编码相对应.

不同的架构,文件存储路径可能不同可以使用sudo find /usr/include -name signum.h查找

我的位于/usr/include/x86_64-linux-gnu/bits/signum.h

进程间通信之-信号signal--linux内核剖析(九)_第2张图片

其中SIGRTMIN,SIGRTMAX定义如下

#define SIGRTMIN        (__libc_current_sigrtmin ())
#define SIGRTMAX        (__libc_current_sigrtmax ())

/* These are the hard limits of the kernel.  These values should not be
   used directly at user level.  */
#define __SIGRTMIN  32
#define __SIGRTMAX  (_NSIG - 1)

进程间通信之-信号signal--linux内核剖析(九)_第3张图片

linux信号的发展及种类


可以从两个不同的分类角度对信号进行分类:

  1. 可靠性方面:可靠信号与不可靠信号;

  2. 与时间的关系上:实时信号与非实时信号。

可靠信号与不可靠信号


“不可靠信号”


Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做”不可靠信号”,信号值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。这就是”不可靠信号”的来源。

它的主要问题是:
进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;

因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。

信号可能丢失,后面将对此详细阐述。

因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。

可靠信号


随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现”可靠信号”。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。

POSIX.4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。

Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

不要有这样的误解:由sigqueue()发送、sigaction安装的信号就是可靠的。

事实上,可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数(对所有信号这一点都成立),而经过signal安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

实时信号与非实时信号


早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。

前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

信号的发送


发送信号的主要函数有:kill()raise()sigqueue()alarm()setitimer()以及abort()

kill–传送信号给指定进程


使用man 2 kill查看帮助信息

进程间通信之-信号signal--linux内核剖析(九)_第4张图片

函数原型

#include  
#include  
int kill(pid_t pid,int signo)

参数说明

  • 第一个参数pid:指定发送信号的接收线程

  • 第二个参数signo:信号的signum

参数pid

参数pid的值 信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

参数signo

Signo是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

Kill()最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。

对于pid<0时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可

/*************************************************************************
    > File Name: kill.c
    > Author: GatieMe
    > Mail: [email protected]
    > Created Time: 2016年03月27日 星期日 11时07分40秒
 ************************************************************************/

#include 
#include 

#include 
#include 
#include 


int main()
{
    int pid;

    if((pid = fork()) < 0)              //  创建新的进程
    {

        perror("Fail to fork");
        exit(EXIT_FAILURE);

    }
    else if(pid == 0)                   //  子进程中返回0
    {

        while(1);

    }
    else                                //  父进程中返回子进程的pid
    {

        int signum;

        while(scanf("%d",&signum) == 1) //  用户输入带发送的信号
        {
            kill(pid, signum);          //  父进程向子进程发送信号
            system("ps -aux | grep ./test");
        }
    }

    return 0;
}

进程间通信之-信号signal--linux内核剖析(九)_第5张图片

在下面程序中,来父子进程各自每隔一秒打印一句话,3 秒后,父进程通过 kill() 函数给子进程发送一个中断信号 SIGINT( 2 号信号),最终,子进程结束,剩下父进程在打印信息

/*************************************************************************
    > File Name: test_kill2.c
    > Author: GatieMe
    > Mail: [email protected]
    > Created Time: 2016年03月27日 星期日 11时23分06秒
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    pid_t pid;
    int i = 0;

    pid = fork(); // 创建进程
    if( pid < 0 )
    {   // 出错
        perror("fork");
    }

    if(pid == 0)
    {   // 子进程
        while(1)
        {
            printf("I am son\n");
            sleep(1);
        }
    }
    else if(pid > 0)
    {   // 父进程
        while(1)
        {
            printf("I am father\n");
            sleep(1);

            i++;
            if(3 == i)
            {   // 3秒后
                kill(pid, SIGINT);      // 给子进程 pid ,发送中断信号 SIGINT
                // kill(pid, 2);        // 等级于kill(pid, SIGINT);
            }
        }
    }

    return 0;
}

进程间通信之-信号signal--linux内核剖析(九)_第6张图片

raise–向自己发送一信号


向进程本身发送信号,参数为即将发送的信号值。

调用成功返回 0;否则,返回 -1。

#include  
int raise(int signo) 

kill和raise有如下等价关系:
kill(getpid(), xxx)等价于raise(xxx), 意思是, raise函数就是向当前进程发信号的。

进程间通信之-信号signal--linux内核剖析(九)_第7张图片

我们下面的程序,进程通过raise向自身发送了一个SIGINT信号。

在linux的64个信号中,大多数在默认情况下都是终止当前信号.包括SIGINT,当到了定时时间后,内核发出SIGINT信号,该信号会终止当前进程.



#include 
#include 

#include 
#include 


 int main(void)
{
    int i = 0;

    while(1)
    {
        i++;
        if(i == 3)
        {
            printf("I will raise SIGINT to myself...\n");
            raise(SIGINT);
        }
        printf("I am running now...\n");

        sleep(1);

    }

    return 0;

}

进程间通信之-信号signal--linux内核剖析(九)_第8张图片

alarm–设置信号传送闹铃


#include  
unsigned int alarm(unsigned int seconds) 

专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。

进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。

返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

setitimer–设置更精确的定时信号


在linux下如果对定时要求不太精确的话,使用alarm()和signal()就行了,但是如果想要实现精度较高的定时功能的话,就要使用setitimer函数。

进程间通信之-信号signal--linux内核剖析(九)_第9张图片

#include  
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue)); 

setitimer()alarm功能强大,支持3种类型的定时器:

定时器 描述
ITIMER_REAL 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;

* 第一个参数which指定定时器类型(上面三种之一);

  • 第二个参数是结构itimerval的一个实例,结构itimerval形式见附录1。

  • 第三个参数可不做处理。
    Setitimer()调用成功返回0,否则返回-1。

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    struct itimerval value, ovalue, value2; //(1)
    sec = 5;

    printf("process id is %d\n", getpid());

    signal(SIGALRM, sigroutine);

    //signal(SIGVTALRM, sigroutine);

    value.it_value.tv_sec = 1;
    value.it_value.tv_usec = 0;
    value.it_interval.tv_sec = 1;
    value.it_interval.tv_usec = 0;

    ///  设置绝对时间
    setitimer(ITIMER_REAL, &value, &ovalue); //(2)

    value2.it_value.tv_sec = 0;
    value2.it_value.tv_usec = 500000;
    value2.it_interval.tv_sec = 0;
    value2.it_interval.tv_usec = 500000;

    ///  设置相对时间
    setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

    while( 1 )
    {
        /// NOP;
    }
}

这里写图片描述

pause–让进程暂停直到信号出现

进程间通信之-信号signal--linux内核剖析(九)_第10张图片

     #include 
     int pause(void);

通过pause可以十当前进程挂起,直至信号出现。

在我们下面的例子中,系统在延迟3s后打印输出”i am a father process,i will send signal now”,然后结束当前进程.
注意,程序并不会打印输出”hello i am child process”.

#include 
#include 

#include 
#include 
#include 

int main(void)
{

    pid_t   pid;

    pid = fork();

    if(pid < 0)
    {
        perror("fork");
    }
    else if(pid == 0)
    {

        printf("I am child processm, I will PAUSE now\n");

        if(pause( ) < 0)
        {
            perror("pause");
        }

        while(1)
        {
            printf("hello i am child process\n");

            sleep(1);
        }
    }
    else
    {
        sleep(3);

        printf("i am a father process,i will send signal now\n");

        kill(pid, SIGINT);

    }

    return 0;
}

abort–终止进程


进程间通信之-信号signal--linux内核剖析(九)_第11张图片

#include  
void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

#include 
#include 


int main(void)
{
    printf("Calling abort()\n");
    abort();

    /* The next code will never reach... */
    printf("after abort...\n");

    return 0;
}

sigqueue–信号发送函数发送数据


参见 信号发送函数sigqueue和信号安装函数sigaction

在队列中向指定进程发送一个信号和数据。

之前学过kill,raise,alarm,abort等功能稍简单的信号发送函数,现在我们学习一种新的功能比较强大的信号发送函数sigqueue.

进程间通信之-信号signal--linux内核剖析(九)_第12张图片

#include  
#include  
int sigqueue(pid_t pid, int sig, const union sigval val) 

调用成功返回 0;否则,返回 -1。

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

  • 第一个参数是指定接收信号的进程ID,

  • 第二个参数确定即将发送的信号,

  • 第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

    typedef union sigval {
        int  sival_int;
        void *sival_ptr;
    }sigval_t;

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。

如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到参数信号处理函数(参数信号处理函数指的是信号处理函数由sigaction安装,并设定了sa_sigaction指针)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。

由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数;

sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。

信号的安装(设置信号关联动作)


如果进程要处理某一信号,那么就要在进程中安装该信号。

安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,

即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:signal()sigaction()

其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;

而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

signal


#include  
void (*signal(int signum, void (*handler))(int)))(int); 

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int); 
sighandler_t signal(int signum, sighandler_t handler)); 

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

例如之前的setitimer精确定时器信号,操作系统的默认处理是终止进程,那么现在我们就可以自己编写信号处理函数,然后通过signal来安装。

#include 
#include 
#include 
#include 
#include 
#include 

int sec;
void sigroutine(int signo)
{
    switch (signo)
    {
        case SIGALRM :
        {
            printf("Catch a signal -- SIGALRM \n");
            signal(SIGALRM, sigroutine);
            break;
        }
        case SIGVTALRM:
        {
            printf("Catch a signal -- SIGVTALRM \n");
            signal(SIGVTALRM, sigroutine);
            break;
        }
    }
    fflush(stdout);
    return;
}

int main()
{
    struct itimerval value, ovalue, value2; //(1)
    sec = 5;

    printf("process id is %d\n", getpid());

    signal(SIGALRM, sigroutine);

    signal(SIGVTALRM, sigroutine);

    value.it_value.tv_sec = 1;
    value.it_value.tv_usec = 0;
    value.it_interval.tv_sec = 1;
    value.it_interval.tv_usec = 0;

    ///  设置绝对时间
    setitimer(ITIMER_REAL, &value, &ovalue); //(2)

    value2.it_value.tv_sec = 0;
    value2.it_value.tv_usec = 500000;
    value2.it_interval.tv_sec = 0;
    value2.it_interval.tv_usec = 500000;

    ///  设置相对时间
    setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

    while( 1 )
    {
        /// NOP;
    }
}

sigaction–改变进程的行为


#include  
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。

  • 该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。

  • 第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;

  • 第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。

如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。
sigaction结构定义如下:

struct sigaction
{
          union
          {
            __sighandler_t _sa_handler;
            void (*_sa_sigaction)(int,struct siginfo *, void *);
          }_u

          sigset_t sa_mask;
          unsigned long sa_flags; 
          void (*sa_restorer)(void);
} 

其中,sa_restorer,已过时,POSIX不支持它,不应再被使用。

  • 联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

  • 由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

typedef struct siginfo_t{ 
int si_signo;//信号编号 
int si_errno;//如果为非零值则错误代码与之关联 
int si_code;//说明进程如何接收信号以及从何处收到 
pid_t si_pid;//适用于SIGCHLD,代表被终止进程的PID 
pid_t si_uid;//适用于SIGCHLD,代表被终止进程所拥有进程的UID 
int si_status;//适用于SIGCHLD,代表被终止进程的状态 
clock_t si_utime;//适用于SIGCHLD,代表被终止进程所消耗的用户时间 
clock_t si_stime;//适用于SIGCHLD,代表被终止进程所消耗系统的时间 
sigval_t si_value; 
int si_int; 
void * si_ptr; 
void* si_addr; 
int si_band; 
int si_fd; 
};

siginfo_t结构中的联合数据成员确保该结构适应所有的信号,比如对于实时信号来说,则实际采用下面的结构形式:

    typedef struct {
        int si_signo;
        int si_errno;           
        int si_code;            
        union sigval si_value;  
        } siginfo_t;

结构的第四个域同样为一个联合数据结构:

    union sigval {
        int sival_int;      
        void *sival_ptr;    
        }

采用联合数据结构,说明siginfo_t结构中的si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了与信号相关的数据。在信号的处理函数中,包含这样的信号相关数据指针,但没有规定具体如何对这些数据进行操作,操作方法应该由程序开发人员根据具体任务事先约定。

前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。

  • sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。
    注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

  • sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。

实例一:利用sigaction安装SIGINT信号


#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 


void handler(int sig);
/*
struct sigaction
{

    void     (*sa_handler)(int);
    //void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t  sa_mask;
    int       sa_flags;
    void     (*sa_restorer)(void);

};
*/

int main(int argc, char *argv[])
{
    struct sigaction    act;

    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    //因为不关心SIGINT上一次的struct sigaction所以,oact为NULL
    //与signal(handler,SIGINT)相同
    if (sigaction(SIGINT, &act, NULL) < 0)
    {
        perror("sigaction error\n");
    }

    for (;;)
    {
        pause( );
    }

    return 0;
}

void handler(int sig)
{
    printf("recv a sig = %d\n", sig);
}

进程间通信之-信号signal--linux内核剖析(九)_第13张图片

实例二:利用sigaction实现signal


实际上signal底层实现就是利用sigaction

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 



void handler(int sig);
__sighandler_t my_signal(int sig, __sighandler_t handler);

int main(int argc, char *argv[])
{
    my_signal(SIGINT, handler);
    for (;;)
        pause();
    return 0;
}

__sighandler_t my_signal(int sig, __sighandler_t handler)
{
    struct sigaction act;
    struct sigaction oldact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (sigaction(sig, &act, &oldact) < 0)
        return SIG_ERR;

    return oldact.sa_handler;
}

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
}

进程间通信之-信号signal--linux内核剖析(九)_第14张图片

实例三:验证sigaction.sa_mask效果


#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 


void handler(int sig);

int main(int argc, char *argv[])
{
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_flags = 0;

    if (sigaction(SIGINT, &act, NULL) < 0)
    {
        perror("sigaction error");
    }

    struct sigaction act2;
    act2.sa_handler = handler;
    sigemptyset(&act2.sa_mask);
    act2.sa_flags = 0;

    if (sigaction(SIGQUIT, &act2, NULL) < 0)
    {
        perror("sigaction error");
    }

    for (;;)
    {
        pause();
    }

    return 0;
}

void handler(int sig)
{
    if(sig == SIGINT){

        printf("recv a SIGINT signal\n");
        sleep(5);
    }
    if (sig == SIGQUIT)
    {
        printf("recv a SIGQUIT signal\n");
    }
}

可知,安装信号SIGINT时,将SIGQUIT加入到sa_mask阻塞集中,则当SIGINT信号正在执行处理函数时,SIGQUIT信号将被阻塞,只有当SIGINT信号处理函数执行完后才解除对SIGQUIT信号的阻塞,由于SIGQUIT是不可靠信号,不支持排队,所以只递达一次

进程间通信之-信号signal--linux内核剖析(九)_第15张图片

示例四:给自身发送int型数据


#include 
#include 
#include 
#include 

void sighandler(int signo, siginfo_t *info,void *ctx);


//给自身传递信息
int main(void)
{

    struct sigaction act;
    act.sa_sigaction = sighandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;//信息传递开关
    if(sigaction(SIGINT,&act,NULL) == -1)
    {
        perror("sigaction error");
        exit(EXIT_FAILURE);
    }

    sleep(2);
    union sigval mysigval;
    mysigval.sival_int = 100;
    if(sigqueue(getpid(),SIGINT,mysigval) == -1)
    {
        perror("sigqueue error");
        exit(EXIT_FAILURE);
    }
    return 0;
}

void sighandler(int signo, siginfo_t *info,void *ctx)
{
    //以下两种方式都能获得sigqueue发来的数据
    printf("receive the data from siqueue by info->si_int is %d\n",info->si_int);
    printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int);

}

进程间通信之-信号signal--linux内核剖析(九)_第16张图片

示例五:进程间传递数据


发送端

#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
    if(argc != 2)
    {
        fprintf(stderr,"usage:%s pid\n",argv[0]);
        exit(EXIT_FAILURE);
    }

    pid_t pid = atoi(argv[1]);

    sleep(2);

    union sigval mysigval;
    mysigval.sival_int = 100;
    printf("sending SIGINT signal to %d......\n",pid);

    if(sigqueue(pid,SIGINT, mysigval) == -1)
    {
        perror("sigqueue error");
        exit(EXIT_FAILURE);
    }


    return 0;
}

接收端

#include 
#include 
#include 
#include 

void sighandler(int signo, siginfo_t *info,void *ctx);
//给自身传递信息
int main(void)
{

    struct sigaction act;
    act.sa_sigaction = sighandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;//信息传递开关

    if(sigaction(SIGINT, &act, NULL) == -1)
    {
        perror("sigaction error");
        exit(EXIT_FAILURE);
    }

    for(; ;)
    {
        printf("waiting a SIGINT signal....\n");
        pause();
    }

    return 0;
}

void sighandler(int signo, siginfo_t *info,void *ctx)
{
    //以下两种方式都能获得sigqueue发来的数据
    printf("receive the data from siqueue by info->si_int is %d\n",info->si_int);
    printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int);

}

进程间通信之-信号signal--linux内核剖析(九)_第17张图片

信号进阶-信号集


信号生命周期


从信号发送到信号处理函数的执行完毕
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:

  • 信号诞生;

  • 信号在进程中注册完毕;

  • 信号在进程中的注销完毕;

  • 信号处理函数执行完毕。

相邻两个事件的时间间隔构成信号生命周期的一个阶段。

进程间通信之-信号signal--linux内核剖析(九)_第18张图片

下面阐述四个事件的实际意义:

信号”诞生”


信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。
信号在目标进程中”注册”;进程的task_struct结构中有关于本进程中未决信号的数据成员struct sigpending pending

struct sigpending{
    struct sigqueue *head, **tail;
    sigset_t signal;
};

第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为”未决信号信息链”)的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{
    struct sigqueue *next;
    siginfo_t info;
}

信号在进程中注册


信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。 只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做”可靠信号”。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。

因此,非实时信号又叫做”不可靠信号”。

这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构

一个非实时信号诞生后,

  1. 如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;

  2. 如果进程的未决信号中没有相同信号,则在进程中注册自己)。

信号在进程中的注销


在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。

如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。

对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);

而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:

如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。

否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。

进程在执行信号相应处理函数之前,首先要把信号在进程中注销。

信号生命终止


进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。

注:

  1. 信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。

  2. 在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。

信号传递过程


信号源为目标进程产生了一个信号,然后由内核来决定是否要将该信号传递给目标进程。从信号产生到传递给目标进程的流程图如

进程间通信之-信号signal--linux内核剖析(九)_第19张图片

进程可以阻塞信号的传递。当信号源为目标进程产生了一个信号之后,内核会执行依次执行下面操作,

  1. 如果目标进程设置了忽略该信号,则内核直接将该信号丢弃。

  2. 如果目标进程没有阻塞该信号,则内核将该信号传递给目标进程,由目标进程执行相对应操作。

  3. 如果目标进程设置阻塞该信号,则内核将该信号放到目标进程的阻塞信号列表中,等待目标进程对该类型信号的下一步设置。

若目标进程后续设置忽略该信号,则内核将该信号从目标进程的阻塞信号列表中移除并丢弃。若目标进程对该信号解除了阻塞,内核将该信号传递给目标进程进行相对应的操作。

在信号产生到信号传递给目标进程之间的时间间隔内,我们称该信号为未决的(pending)。

每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞传递给该进程的信号集。对于每种可能的信号,信号屏蔽字中都有一位与之对应。

信号集和进程信号屏蔽字


我们已经知道,通过信号实现程序之间的相互通信,我们可以实现如下功能

  • 可以通过信号来终止进程

  • 可以通过信号来在进程间进行通信

  • 程序通过指定信号的关联处理函数来改变信号的默认处理方式

  • 可以通过屏蔽某些信号,使其不能传递给进程。

那么我们应该如何设定我们需要处理的信号,我们不需要处理哪些信号等问题呢?

信号集函数就是帮助我们解决这些问题的。

信号集及信号集操作函数


信号集被定义为一种数据类型

typedef struct
{
    unsigned long sig[_NSIG_WORDS];
}sigset_t;

信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。

POSIX.1 定义了一个数据类型sigset_t,用于表示信号集。

另外,头文件 signal.h 提供了下列五个处理信号集的函数。

函数 功能
sigemptyset(sigset_t *set) 初始化由set指定的信号集,信号集里面的所有信号被清空;
sigfillset(sigset_t *set) 调用该函数后,set指向的信号集中将包含linux支持的64种信号;
sigaddset(sigset_t *set, int signum) 在set指向的信号集中加入signum信号;
sigdelset(sigset_t *set, int signum) 在set指向的信号集中删除signum信号;
sigismember(const sigset_t *set, int signum) 判定信号signum是否在set指向的信号集中。
  • 函数 sigemptyset 初始化由 set 指向的信号集,清除其中所有信号。
int sigemptyset(sigset_t *set);

返回值:若成功则返回0,若出错则返回-1

  • 函数 sigfillset 初始化由 set 指向的信号集,使其包含所有信号。
int sigfillset(sigset_t *set);

返回值:若成功则返回0,若出错则返回-1

  • 函数 sigaddset 将一个信号 signo 添加到现有信号集 set 中。
int sigaddset(sigset_t *set, int signo);

返回值:若成功则返回0,若出错则返回-1

  • 函数 sigdelset 将一个信号 signo 从信号集 set 中删除。
int sigdelset(sigset_t *set, int signo);

返回值:若成功则返回0,若出错则返回-1

  • 函数 sigismember 判断指定信号 signo 是否在信号集 set 中。
int sigismember(const sigset_t *set, int signo);

返回值:若真则返回1,若假则返回0,若出错则返回-1

信号阻塞与信号未决


每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。

下面是与信号阻塞相关的几个函数:

#include 
int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask));

sigprocmask检测或设置进程的信号屏蔽字


  • sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:
参数how 进程当前信号集
SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号
SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
SIG_SETMASK 更新进程阻塞信号集为set指向的信号集

在下面的程序文件中先调用 sigprocmask 设置阻塞信号 SIGALRM,然后调用 alarm(2) 设置一个两秒钟的闹钟(两秒钟之后将向当前进程产生一个 SIGALRM 信号)。在睡眠 4 秒钟之后(此时应该已经产生了 SIGALRM 信号),调用 sigprocmask 函数解除对信号SIGALRM 的阻塞。

#include 
#include 
#include 
#include 
#include 
#include 

static void sig_alrm(int signo)
{
      printf("received SIGALRM\n");
}

int main(void)
{
    sigset_t    sigset;

    //  初始化信号集
    sigemptyset(&sigset);

    //  添加一个闹钟信号
    sigaddset(&sigset, SIGALRM);
    if (sigprocmask(SIG_BLOCK, &sigset, NULL) < 0)
    {
        printf("sigprocmask error: %s\n", strerror(errno));
        exit(-1);
    }
    else
    {
        printf("signal SIGALARM is in in sigset now...\n");
    }

    if (signal(SIGALRM, sig_alrm) < 0)          //  添加信号处理函数
    {
        printf("signal error: %s\n", strerror(errno));
        exit(-1);
    }

    alarm(2);
    sleep(4);

    printf("before unblock sigprocmask\n");
    if (sigprocmask(SIG_UNBLOCK, &sigset, NULL) < 0)
    {
        printf("sigprocmask SIG_UNBLOCK error: %s\n", strerror(errno));
        exit(-1);
    }
    else
    {
        printf("signal SIGALARM isn't in sigset now...\n");
    }

    return 0;
}

进程间通信之-信号signal--linux内核剖析(九)_第20张图片

从上面的执行输出,我们看到信号 SIGALRM 是在调用 sigprocmask函数执行 unblock之后才被传递给当前进程进行处理的。
进程间通信之-信号signal--linux内核剖析(九)_第21张图片

如果我们将代码中的sigprocemask(SIG_BLOCK, &sigset, NULL) 注释掉,编译执行,生成如下结果

进程间通信之-信号signal--linux内核剖析(九)_第22张图片

我们看到由于没有屏蔽信号 SIGALRM ,程序在2秒后捕获了SIGALRM直接调用sig_alrm进行了处理。

sigpending 获取进程未决的信号集


函数 sigpending 获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

#include 
int sigpending(sigset_t *set);

返回值:若成功则返回0,若出错则返回-1

#include 
#include 
#include 
#include 
#include 
#include 

void alrm_is_pending(const char *str)
{
    sigset_t pendingsigset;

    printf("%s: ", str);

    if (sigpending(&pendingsigset) < 0)
    {
        printf("sigpending error: %s\n", strerror(errno));
        exit(-1);
    }

    if (sigismember(&pendingsigset, SIGALRM))
    {
        printf("SIGALRM is pending\n");
    }
    else
    {
        printf("SIGALRM is not pending\n");
    }
}

int main(void)
{
    sigset_t sigset;

    sigemptyset(&sigset);
    sigaddset(&sigset, SIGALRM);

    if (sigprocmask(SIG_BLOCK, &sigset, NULL) < 0)
    {
        printf("sigprocmask error: %s\n", strerror(errno));
        exit(-1);
    }

    alrm_is_pending("before alarm");

    alarm(2);
    sleep(4);

    alrm_is_pending("after alarm");

    return 0;

}

这里写图片描述

从运行结果,我们看到调用 alarm 函数产生信号 SIGALRM 之后,该信号在 sigpending 函数的 set 参数指向的信号集中。

  • sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。

下面以一个例子来说明上述函数的用法,源文件为sigset.c,代码如下:

#include 
#include 
#include 
#include 
#include 

void handler(int sig)
{
    printf("Handle the signal %d\n", sig);
}

int main()
{
    sigset_t            sigset;         //  用于记录屏蔽字
    sigset_t            ign;            //  用于记录被阻塞的信号集
    struct sigaction    act;

    //清空信号集
    sigemptyset(&sigset);
    sigemptyset(&ign);

    //  向信号集中添加信号SIGINT
    sigaddset(&sigset, SIGINT);

    //  设置处理函数和信号集
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);

    printf("Wait the signal SIGINT...\n");
    pause();                            //挂起进程,等待信号

    //  设置进程屏蔽字,在本例中为屏蔽SIGINT
    sigprocmask(SIG_SETMASK, &sigset, 0);

    printf("Please press Ctrl+c in 10 seconds...\n");

    sleep(10);

    //  测试SIGINT是否被屏蔽
    sigpending(&ign);
    if(sigismember(&ign, SIGINT))
    {
        printf("The SIGINT signal has ignored\n");
    }

    //  在信号集中删除信号SIGINT
    sigdelset(&sigset, SIGINT);
    printf("Wait the signal SIGINT...\n");

    //  将进程的屏蔽字重新设置,即取消对SIGINT的屏蔽
    //  并挂起进程
    sigsuspend(&sigset);

    printf("The app will exit in 5 seconds!\n");

    sleep(5);

    return EXIT_SUCCESS;
}

进程间通信之-信号signal--linux内核剖析(九)_第23张图片

你可能感兴趣的:(♥LinuxKernel,♥,Linux内核剖析)