linux进程间通信之信号

1:信号

信号是UNIX中所使用的进程间通信比较古老的一种方法,linux直接继承而来。信号的本质其实就是一种软中断。在有些程序中需要用到此种方法:比如最为经典的是在终端中输入kill命令,杀死某个进程,该命令就是通知内核来产生SIGKILL signal,来终止一个进程。这是一种比较典型的异步通信方式。
信号可以直接进行用户空间进程和内核空间进程之间的交互,内核进程也可以利用它来通知用户进程发生了哪些系统事件。可以在任何时候发给某个进程,而无需知道进程的状态。如果该进程当前并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行再传递给它为止;如果一个信号被该进程设置为堵塞,则该信号的传递被延迟,知道其堵塞被取消时才被传递。

 kill -l命令行可以列出,系统支持的所有signal,它是由内核来定义的
 $ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX    

signal的缺点就是只支持有限的几种,用户无法定义,以及无法传递数据。

一个完整的信号生命周期可以分为3个重要阶段,这3个阶段可以由4个重要事件来刻画:信号产生,信号在进程中注册,信号在进程中注销,以及执行信号处理函数。见下图(来自于linux应用程序开发标准教程):

linux进程间通信之信号_第1张图片

信号分为不可靠信号和可靠信号。
不可靠信号:如果发现该信号已经在进程中注册,那么久该忽略该信号。因此若前一个信号还未注销又产生了相同的信号就会产生信号丢失。发生不可靠的原因就是在上图中内核发生进程到信号处理函数之间, 有一个用户进程(上图)的信号注册和注销的时间窗口,造成就由可能在这个时间窗口之间,有信号又重新产生。
可靠信号:发生一个信号给进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。
所有可靠信号都支持排队,而所有不可靠信号都不支持排队。

用户对信号的响应有3种方式:
忽略信号:即对信号不做任何处理。但有两个信号不能忽略,即SIGKILL以及SIGTOP
捕捉信号:定义信号处理函数,当信号发生时,执行响应的自定义处理函数
执行缺陷操作,linux对每种信号都规定了默认操作。

2:信号发送API

linux提供给信号发送函数主要有kill(), raise(), alarm()以及pause()

kill()函数,可以发生信号给进程或进程组
函数原型:
    int kill(pid_t pid, int sig)

 raise()可以运行进程向自身发生信号。
     int raise(int sig)

 pause()函数使调用进程挂起至到捕捉到一个信号
    int pause()

 下面是一个signal的例子
#include 
#include 
#include 
#include 
#include 


int main()
{
    pid_t pid;
    int ret;


    if((pid= fork()) <0) 
    {   
        printf("Fork error\n");
        exit(1);
    }   

    if (0 == pid)
    {   

        printf("Child process is %d\n", getpid());
        raise(SIGSTOP);
        exit(0);
    } else 
    {        sleep(1);
        if ((waitpid(pid, NULL, WNOHANG)) == 0)
        {

            if (kill(pid, SIGKILL) == 0)
            {
                printf("Parent kill %d\n", pid);
            }
         }
         waitpid(pid,NULL, 0);
         exit(0);
     }
}

上述例子来自于《linux应用程序开发标准教程》:首先fork()创建一个子进程,接着为了保证子进程不在父进程调用kill()之前推出,在子进程中使用raise()函数向自身发生SIGSTOP信号,使子进程暂停。同时稍微做了修改,在父进程中增加了1s延迟,父进程中调用kill()进程发生信号到子进程中。

信号发生的另外一个函数alarm()
alarm()函数也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的实际到时,它就向进程发生SIGALARM函数。一个进程只有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所替代。
函数原型
unsigned int alarm(unsigned int seconds)

使用实例:
 #include 
#include 
#include 

int main()
{

    int ret = alarm(5);
    pause();
    printf("Wake up\n");
}

3 signal()和sigaction()函数

上节中,我们将到针对信号的处理,用户可以对信号进行捕捉,并进行自定义函数,对该信号的处理。针对特定信号,挂节用户自定义行为,有两种方法来实现,第一种就是signal函数,另外一种就是信号集函数组。

signal函数原型:
    void (*signal(int signum, void (*handler)(int))) (int)
    其中 参数 signum:为制定信号代码
             void (*handler)(int) 为自定义的信号处理函数指针。

    返回值, 出错 -1
            成功:则是以前的信号处理配置。

在《unix环境高级编程》中为了简化对signal函数的使用,可以使用typedef(),将其简化
typedef void Sigfunc(int);
可以将signal简化为
Sigfunc signal(int, Sigfunc)

下面是使用signal的一个例子

#include 
#include 
#include 

void my_func(int sign_no)
{
    if (sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }
}

int main()
{
    printf("Waiting for signal SIGINT or SIGQUIT...\n");
    signal(SIGINT, my_func);
    signal(SIGQUIT, my_func);
    pause();
    exit(0);
}
运行结果:
$ ./signal
Waiting for signal SIGINT or SIGQUIT...
I have get SIGINT (按 ctrl-c 组合键)
$ ./signal
Waiting for signal SIGINT or SIGQUIT...
I have get SIGQUIT (按 ctrl-\ 组合键)   

注意:当一个进程调用fork()时,其子进程继承父进程的信号处理方式。因此子进程在开始时复制了父进程的存储映像,所以信号捕捉函数的地址在子进程中是有意义的。

sigaction()函数也能注册一个自定义的用户处理函数.《unix环境高级编程》中指出 sigaction函数是检查或修改与指定信号相关联的处理动作(或同时执行这两种操作)。主要是取代UNix早期版本使用的signal函数,linux也完全继承了该接口。
函数原型:
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oldact)
参数: int signo 是检测或修改其具体动作的信号编号。若act指针为非空,则要修改其动作,如果oldact为非空,则系统由oldact 指针返回信号的上一个动作。
其中 sigaction的结构:
struct sigaction{
void (sa_handler)(int); /*add of signal handler 信号制定处理函数/
sigset_t sa_mask; /additional signal to block 可以指定信号处理程序执行过程中哪些信号应对被屏蔽,在调用信号捕捉函数之前,该信号集要加入到信号的屏蔽字中/
int sa_flags /* signal options,包涵了许多标志位*/
void (sa_sigaction)(int, sigino_t , void );/*alternate handler /

sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时在将进程的信号屏蔽字复位原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被堵塞到对前一个信号的处理结束为止。若同一信号多次发生,通常并不将它们排队,所以如果在某种信号被堵塞时它发生了五次,那么对这种信号解除堵塞后,其信号处理函数通常只会被调用一次。
sg_sigaction 字段是一个替代的信号处理程序,当在sigaction结构中sa_flags标志位使用了SA_SIGINFO标志时,使用该信号处理程序。对于sa_sigaction字段和sa_handler字段这两者,其现实可能使用同一个存储区,所以应用只能一次使用这两个字段中的一个。
将上述使用signal的例子,用sigaction()函数来实现:

#include 
#include 
#include 


void my_func(int sign_no)
{
    if(sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }

}


int main()
{
    struct sigaction action;

    printf("Waiting for signal SIGINT or SIGQUIT...\n");

    /*init action */
    action.sa_handler = my_func;
    sigemptyset(&action.sa_mask);
    action.sa_flags =0;

    sigaction(SIGINT, &action, 0);
    sigaction(SIGQUIT, &action, 0);

    pause();
    exit(0);

}
执行结果和signal处理函数一样。

必须要注意的是sigemptyset函数初始化act结构的sa_mask成员。不能保证:act.sa_mask = 0会做同样的事情
与signal()函数相比, sigaction()函数多了许多功能

4: 信号集

上述都是针对一个信号进行操作,有的时候我们需要有一个能表示多个信号-信号集的数据类型,同时对多个信号进行设置。针对信号集,linux提供了一系列API,将这个API按照调用的先后次序可以分为:创建信号集合,注册信号处理函数以及检测信号。
其中与创建信号集合相关的函数:
int sigemptyset(sigset_t *set)
将信号集合初始化为空
int sigfillset(sigset_t *set)
将信号集合初始化为包涵所有已定义的信号集合
int sigaddset(sigset_t *set, int signo)
将指定信号加入到信号集合中去。
int sigdelset(sigset_t *set, int signo)
将指定信号从信号集合中删除
int sigismember(const sigset_t *set, int signo)
查询指定信号是否在信号集合之中。

检测信号函数:
int sigprocmask(int how, const sigset_t restrict * set, sigset_t * oset)
该函数可以检测或更改其信号屏蔽字。
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
若set为非空指针,则参数how指示如何修改当前信号屏蔽字。
how 参数
SIG_BLOCK, 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望堵塞的加信号。
SIG_UNBLOCK, 该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集交集的补集。set包含了我们希望解除堵塞的信号。
SIG_SETMASK 该进程新的信号屏蔽字将被set指向的信号集的值代替
可以通过使用SIG_BLOCK,和SIGUNBLOCK来堵塞所选择的信号,是指在一定的代码区间中不响应该信号。通过使用这种技术可以保护不希望由信号中断的代码临界区。注意它只能堵塞信号,当堵塞解除后被堵塞的信号还会发生。即只能延迟信号发生,不能使该信号消失。

int sigpending(sigset_t *set);
返回信号集,其中的各个信号对于调用进程是堵塞的而不能递送,因而也一定是当前未决的。该信号集通过set参数返回。          

注册信号处理函数: 主要决定了进程如何处理信号,使用sigaction()函数进程注册。
注意信号集里面的信号并不是真正要可以处理的信号,只有当信号的状态处于非堵塞状态时才会真正起到作用,才能调到处理函数。因此使用信号集的一般流程是首先使用sigprocmask()函数检测并更改信号屏蔽字。然后使用sigaction()函数来定于进程接收到特定信号之后的行为。检测信号时信号处理的后续步骤,因为被堵塞的信号不会传递给进程。所以这些信号就处于“未处理”状态。sigpending()函数允许进程检测“未处理”信号,并进一步决定对它们作何处理。通过这种技术也可以保护不希望由信号中断的代码临界区。
如下图所示主要展示了信号操作的处理流程:

![这里写图片描述](https://img-blog.csdn.net/20160824203154799)

将上述例子SIGQUIT, SIGINT信号修改为信号集处理方式:
#include 
#include 
#include 
#include 
#include 

void my_func(int sign_no)
{
    if(sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }

}

int main()
{
    sigset_t set, pendset;
    struct sigaction action1, action2;

    /* init set */
    if (sigemptyset(&set) < 0)
    {
        perror("sigemptyset");
        exit(1);
    }

    /* add SIGQUIT signal into set */
    if(sigaddset(&set, SIGQUIT) < 0)
    {
        perror("sigaddset");
        exit(1);
    }

    /* add SIGINT signal into set*/
    if (sigaddset(&set, SIGINT) < 0)
    {
        perror("sigaddset");
        exit(1);
    }

    /*set SIGINT handler */
    if(sigismember(&set, SIGINT))
        {
        sigemptyset(&action1.sa_mask);
        action1.sa_handler = my_func;
        action1.sa_flags = 0;
        sigaction(SIGINT, &action1, NULL);
    }


    /* set SIGQUIT handler */
    if(sigismember(&set, SIGQUIT))
    {

       sigemptyset(&action2.sa_mask);
       action2.sa_handler = my_func;
       action2.sa_flags = 0;
       sigaction(SIGQUIT, &action2,NULL);
    }


    /* set signal mask */
    if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
    {
        perror("sigprocmask");
        exit(1);
    }
    else
    {
        printf("Signal set was blocked, Press any key!");
        getchar();
    }

    if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
    {
        perror("Sigprocmask");
        exit(1);
    }
    else
    {
        printf("Signal set is in unblock state\n");
    }


    while(1);
    exit(0);
}

运行结果:
root:/repo/training/fork# ./a.out 
Signal set was blocked, Press any key!^C^\
I have get SIGQUIT
I have get SIGINT
Signal set is in unblock state
^\I have get SIGQUIT
^CI have get SIGINT
^\I have get SIGQUIT
^Z
[1]+  Stopped                 ./a.out

下面一个例子是关于sigprocmask()函数 how为0, set为NULL参数的用法:
#include 
#include 
#include 
#include 
#include 

void my_func(int sign_no)
{
    if(sign_no == SIGINT)
    {   
        printf("I have get SIGINT\n");
    }   
    else if (sign_no == SIGQUIT)
    {   
        printf("I have get SIGQUIT\n");
    }   

}

int main()
{
    sigset_t set, pendset;

    /* init set */
    if (sigemptyset(&set) < 0)
    {   
        perror("sigemptyset");
        exit(1);
    }   

    /* add SIGQUIT signal into set */
    if(sigaddset(&set, SIGQUIT) < 0)
    {   
        perror("sigaddset");
        exit(1);
    }   

    /* add SIGINT signal into set*/
    if (sigaddset(&set, SIGINT) < 0)
    {   
        perror("sigaddset");
        exit(1);
    }   

    /* set signal mask */
    if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
    { 
           perror("sigprocmask");
        exit(1);
    }

    if(sigprocmask(0,NULL, &pendset) < 0)
    {
        perror("sigprocmask");
        exit(1);
    }


    if(sigismember(&pendset, SIGINT))
    {
        printf("Block SIGINT\n");
    }


    if (sigismember(&pendset, SIGQUIT))
    {
        printf("Block SIGQUIT\n");
    }

}

运行结果:
root:/repo/training/fork# ./a.out 
Block SIGINT
Block SIGQUIT

下面写一个子进程和父进程之间使用signal进行通信的例子:

#include 
#include 
#include 
#include 
#include 

void my_func(int sign_no)
{

    if(sign_no == SIGINT)
    {
        printf("I have got SIGINT\n");
    } else if ( sign_no == SIGQUIT)
    {
        printf("I have got SIGQUIT\n");
    }

}
int main(void)
{
    pid_t result;

    /* create new process */
    result = fork();
    if (-1 == result)
    {
        printf("Fork error\n");
    } else if ( result ==0 ) /*children process */
    {
        printf("Child process id is %d\n", getpid());
        signal(SIGINT, my_func);
        signal(SIGQUIT, my_func);
        while(1)
        {
            printf("loop \n");
        }
    } else
    {

        sleep(1);
        printf("Father process id is %d\n", getpid());
        printf("Send SIGINT signal to %d\n", result);
        kill(result, SIGINT);
        printf("Send SIGQUIT signal to %d\n", result);
        kill(result, SIGQUIT);
        printf("Send SIGKILL signal to %d\n", result);
        kill(result, SIGKILL);
              kill(result, SIGKILL);
    }

    return result;
运行结果:
roo:/repo/training/fork# ./a.out 
Child process id is 4631
Father process id is 4630
loop 
loop 
loop 
loop 
loop 
loop 
loop 
loop 
Father process id is 4621
Send SIGINT signal to 4622
Send SIGQUIT signal to 4622
Send SIGKILL signal to 4622



以上实验代码github地址:https://github.com/zhikunhuo/training.git

你可能感兴趣的:(linux应用开发)