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

Posted gccbuaa

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信之-信号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.h(/usr/include/bits/signum.h)中,这些信号都被定义为正整数。即每一个信号和一个数字编码相对应.

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

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

技术分享

当中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)

技术分享

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查看帮助信息

技术分享

函数原型

#include <sys/types.h> 
#include <signal.h> 
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时的情况,对于哪些进程将接受信号,各种版本号说法不一。其实非常easy,參阅内核源代码kernal/signal.c即可

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

#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>
#include <signal.h>


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

技术分享

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

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

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

技术分享

raise–向自己发送一信号


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

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

#include <signal.h> 
int raise(int signo) 

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

技术分享

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

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



#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <signal.h>


 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;

}

技术分享

alarm–设置信号传送闹铃


#include <unistd.h> 
unsigned int alarm(unsigned int seconds) 

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

进程调用alarm后,不论什么曾经的alarm()调用都将无效。假设參数seconds为零。那么进程内将不再包括不论什么闹钟时间。

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

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


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

技术分享

#include <sys/time.h> 
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>

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–让进程暂停直到信号出现

技术分享

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

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

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

#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

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–终止进程


技术分享

#include <stdlib.h> 
void abort(void);

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

该函数无返回值。

#include <stdio.h>
#include <stdlib.h>


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.

技术分享

#include <sys/types.h> 
#include <signal.h> 
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 <signal.h> 
void (*signal(int signum, void (*handler))(int)))(int); 

假设该函数原型不easy理解的话,能够參考以下的分解方式来理解:

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>

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 <signal.h> 
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 <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

#include <errno.h>
#include <string.h>
#include <signal.h>


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

技术分享

实例二:利用sigaction实现signal


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

#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

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



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

技术分享

实例三:验证sigaction.sa_mask效果


#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

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


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是不可靠信号,不支持排队,所以仅仅递达一次

技术分享

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


#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

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

}

技术分享

演示样例五:进程间传递数据


发送端

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

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 <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

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

}

技术分享

信号进阶-信号集


信号生命周期


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

  • 信号诞生;

  • 信号在进程中注冊完成;

  • 信号在进程中的注销完成。

  • 信号处理函数运行完成。

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

技术分享

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

信号”诞生”


信号的诞生指的是触发信号的事件发生(如检測到硬件异常、定时器超时以及调用信号发送函数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. 在信号被注销到对应的信号处理函数运行完成这段时间内,假设进程又收到同一信号多次。则对实时信号来说。每一次都会在进程中注冊;而对于非实时信号来说。不管收到多少次信号。都会视为仅仅收到一个信号。仅仅在进程中注冊一次。

信号传递过程


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

技术分享

进程能够堵塞信号的传递。

当信号源为目标进程产生了一个信号之后,内核会运行依次运行以下操作,

  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 <signal.h>
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 <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>

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

技术分享

从上面的运行输出,我们看到信号 SIGALRM 是在调用 sigprocmask函数运行 unblock之后才被传递给当前进程进行处理的。
技术分享

假设我们将代码中的sigprocemask(SIG_BLOCK, &sigset, NULL) 凝视掉,编译运行,生成例如以下结果

技术分享

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

sigpending 获取进程未决的信号集


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

#include <signal.h>
int sigpending(sigset_t *set);

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

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

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 <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>

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内核剖析的主要内容,如果未能解决你的问题,请参考以下文章

进程间通信之信号量

linux进程间通信之Posix 信号量用法详解代码举例

Linux进程间通信之管道(pipe)命名管道(FIFO)与信号(Signal)

进程间通信--信号(进程间通信唯一的异步方式)

linux进程间通信之System V共享内存详解及代码示例

《Linux应用进程间通信 — 信号》