Linux进程通信——信号

时间:2022-06-22
本文章向大家介绍Linux进程通信——信号,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

版权声明:本文为博主原创文章,转载请注明博客地址: https://blog.csdn.net/zy010101/article/details/83931740

信号是在软件层面对中断机制的一种模拟,信号的出现使得进程直接的通信不在是被动的,不在向之前那样,read()操作往往需要等待write()操作结束。因为信号是对中断的一种模拟。既然是中断,那么它的发生就是不确定。就不会发生一个进程阻塞在这里等待另一个进程执行的结果。这样的异步性通信机制无疑是更加强大的。

在终端输入kill -l可以查看当前系统所支持的所有信号。(我这个是Ubuntu)

可以看到有64个信号,其中有两个较为特殊的信号是SIGRTMIN和SIGRTMAX。Linux下的通信机制是遵从POSIX标准的。34号信号SIGRTMIN信号之前的是早期UNIX操作系统的。它们是不可靠的信号。它的主要问题是:进程每次处理信号后,会设置对该信号的默认处理动作,有时候我们不想让他这么处理了(按照默认处理),这时候就需要调用signal()函数重新安装一次信号。这样会形成新的默认动作。还有更加讨厌的是,信号有可能会丢失。

Linux对不可靠信号做了一些改进,现在的主要问题变成了“信号会丢失”。

后来POSIX仅仅只对可靠信号做了标准化。信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号。可靠信号它不会丢失。

可靠信号都是实时信号,不可靠信号都是非实时信号。可靠信号都支持队列处理,不可靠信号不支持队列处理。在UNIX时代就定义好了前面的不可靠信号的功能,而后来增加的可靠信号是让用户自定义使用的。

信号处理的三种方式:

  1. 忽略信号:对信号不做任何处理,就当做没发生任何事情一样。(SIGKILL和SIGSTOP这两个不能忽略)
  2. 捕捉信号:定义信号处理函数,当信号发出的时候,执行相应的操作。(这个和Qt的信号槽差不多)
  3. 执行默认动作:Linux对每一个信号都规定了默认操作(可靠信号的默认操作是进程终止)。

发送信号

发送信号的函数有kill(),raise(),sigqueue(),alarm(),setittimer(),abort()。常用的是kill()。它们依赖的头文件是#include<signal.h>和#include<sys/types.h>

函数原型:int kill(pid_t pid,int sig);

函数功能:用来将sig所指定的信号发送到pid所指定的进程。

pid有下面几种情形,分别对应于不同情况下应用。

  • pid > 0:把信号传递到进程ID为pid的进程
  • pid == 0:把信号传送给当前进程所在组的所有进程
  • pid == -1:将信号以广播的形式传送给系统内所有进程
  • pid < -1: 讲信号传递给进程组识别码为pid绝对值的所有进程

函数执行成功返回0,否则返回-1.

测试代码如下:

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

int main()
{
    pid_t pid;
    int statu;
    pid = fork();
    if(0 == pid)
    {
        printf("sonn");
        sleep(5);
        printf("I am son!n");
        exit(0);
    }
    if(0 < pid)
    {
        sleep(2);
        printf("fathern");
        kill(pid,SIGABRT);      //SIGABRT是终止子进程
        wait(NULL);
        printf("My son GGn");
        exit(0);
    }
    return 0;
}

让子进程先执行,打印出son。然后让子进程挂起。轮到父进程执行,父进程执行到kill()函数的时候给子进程发了个SIGABRT信号,让子进程终止了。然后wait()回收子进程,打印My son GG.

执行结果如下:

可以看到,子进程收到SIGABRT信号后,终止了。没有向屏幕打印I am son.关于信号的详解,看这里:https://blog.csdn.net/zy010101/article/details/83932113

上面的kill函数发送的信号是不可靠信号,它执行默认操作。即:终止进程。如果我们需要自定义信号处理方式,那么就需要安装信号。Linux安装信号主要由signal()和sigaction()完成。signal是在可靠信号系统调用的基础上实现的,是库函数。

signal()的原型很复杂,我们还是从signal.h这个头文件来看一下吧!

extern __sighandler_t signal (int __sig, __sighandler_t __handler)

__THROW;

可以看到signal有两个参数,一个是信号值,另一个我们再来看看

typedef void (*__sighandler_t) (int);

另一个是这样的一个函数指针变量,那么说明__handler代表了一个函数的入口地址(实际就是函数)。另外,这个函数指针指向的函数需要一个int类型的参数。signal函数的返回值也是一个函数指针。

注意:__handler如果不是函数指针,它只能是SIG_IGN或者是SIG_DFL.

SIG_IGN:忽略参数指定的信号。(忽略该信号)

SIG_DFL:将参数指定的信号重新设置为内核默认的处理方式。

返回值:signal函数本身在成功时返回NULL,它的参数__handler则会返回处理信号的函数的地址(函数指针)。失败返回:SIG_ERR.

所以这就要求自定义的信号处理函数的函数原型是这样的:

void 函数名(int 参数名);即:函数必须有一个int类型的参数。

signal()函数只是定义了将指定信号传送到指定进程。还需要一个用于捕捉信号的函数。在Linux下pause()函数用于捕捉信号,如果没有信号发生,pause函数将会一直等待。直到有信号发生。

函数原型:int pause();

当pause函数捕捉到信号的时候返回-1(注意不是捕捉到的信号的值)。

测试程序如下:

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

//自定义的信号处理函数
void My_Fun(int sig)
{
    if(SIGRTMIN == sig)
    {
        printf("MIN!n");
    }
    if(SIGRTMAX == sig)
    {
        printf("MAX!n");
    }
}

int main()
{
    //注册信号处理函数
    signal(SIGRTMIN,My_Fun);
    signal(SIGRTMAX,My_Fun);
    //挂起10s
    sleep(3);
    //发出信号
    kill(getpid(),SIGRTMIN);    //getpid()函数用于获取当前进程的pid.
    kill(getpid(),SIGRTMAX);
    return 0;
}

输出结果如下:

这样就完成了自定义信号的使用。使用自定义信号有两个关键点。一是必须注册自定义信号的处理函数,二是必须发送自定义信号。怎么样发送自定义信号由你自己来定义,这为程序设计带来了极大的便利。比如上面我们只是直接了当的发送两个信号。你也可以使当满足一定条件的时候才发送信号。比如下面这样。

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

//自定义的信号处理函数
void My_Fun(int sig)
{
    if(SIGRTMIN == sig)
    {
        printf("MIN!n");
    }
    if(SIGRTMAX == sig)
    {
        printf("MAX!n");
    }
}

int main()
{
    //注册信号处理函数
    signal(SIGRTMIN,My_Fun);
    signal(SIGRTMAX,My_Fun);
    //发出信号
    char c;
    while(1)
    {
        scanf("%c",&c);
        getchar();                      //吸收回车
        if('a' == c)
        {
            kill(getpid(),SIGRTMIN);    //getpid()函数用于获取当前进程的pid.
        }
        else
        {
            kill(getpid(),SIGRTMAX);
        }
    }

    return 0;
}

运行结果如下:

这样就实现了发送信号的控制。可以想象,键盘,鼠标等发送的信号很有可能就是被系统采取这样的方式处理的。

另外一个函数是sigaction()函数。

函数原型:int sigaction(int sig,const struct sigaction *newact,const sigaction *oldact);

函数功能:sigaction函数根据参数sig指定的信号来处理信号。参数可以是SIGKILL和SIGSTOP以为的其他信号。newact是新的信号处理方式,oldact是旧的信号处理方式。这个结构体包含如下成员

struct sigaction()
{
    void(*sa_handler)(int);        //指向信号处理函数的函数指针
    sigset_t sa_mask;              //用来设置处理该信号的时候暂时屏蔽sa_mask指定的信号
    int sa_flags;                  //设置信号处理的方式
    int (*sa_restorer)(void);      //输出参数, 指向struct sigaction 结构的指针
}

在C语言里结构体的名字可以和函数名相同。(其实是C语言的结构体名称应该是struct xxx。带上关键字才是真正的结构体名)。

信号集

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

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

信号集主要配合一下的信号阻塞函数来使用。

  1. sigemptyset()函数: 函数原型:int sigemptyset(sigset_t *set); 函数功能:用来将set信号集给初始化并清空。
  2. sigaddset()函数: 函数原型:int sigaddset(sigset_t *set,int sig); 函数功能:将参数sig指定的信号加入信号集set。
  3. sigfillset()函数: 函数原型:int sigfillset(sigset_t *set); 函数功能:把所有信号加入到set中。
  4. sigdelset()函数: 函数原型:int sigdelset(sigset_t *set,ing sig); 函数功能:将参数sig指定的信号从set中删除

使用信号注意的问题:

  1. 注意信号是否会丢失这个问题,尽量使用可靠信号。
  2. 注意信号的可移植性,POSIX标准指定的信号函数和信号
  3. 信号处理函数应当是一个可重入函数。