Linux - 信号

本文围绕操作系统中的中断和信号展开。先介绍中断概念、分类及处理流程,包括中断源、屏蔽、现场保护等。接着阐述信号相关内容,如信号分类(可靠与不可靠)、发送函数、进程响应方式,还提及可重入函数、信号在内核表示及相关操作函数等知识。

中断【纯概念,但是很重要】

在学习信号之前,首先需要理解一下什么是中断,因为信号与中断有很多的相似之处,中断,顾名思义就是中途打断:

那什么是异步事件呢?它是没有一定时序关系,随机发生的事件,在中断技术出现之前,计算机对异步事件处理能力是有限的,通过是通过查询的方式来处理的,举一个现实生活中的例子: 比如张三正在看书,这时厨房里又正在烧开水,这时,张三看书时并不知道水是否烧开了,他就需要跑到厨房当中"查询"一下水是否烧开了,然后再回来看书,这时又不放心厨房水是否烧开了,于是又跑进厨房查询,如此循环,直到水烧开之后,他才能够静下心来看自己的书,这就是典型的查询技术。 有了中断技术后,又是如何的呢?还是以上面这个例子: 张三在看书的同时,设置一个闹钟,比如说是10分钟的闹钟,当水烧开的时候,会响铃,其中响铃可以看作是一个中断信号,闹钟可以看成是一个中断源,当闹钟响之后,张三就会去处理这一次的中断事件,也就是跑到厨房将煤气给关闭,将开水倒进热水瓶当中,这叫中断执行程序,实际上张三在做这件事之前,需要保护现场,记住当前看到了第几页,当执行完中断执行程序之后,则恢复现场,继续从之前看到的页数开始看书,这就是中断执行的整个流程,总结一下:   中断源 -> 中断屏蔽 -> 保护现场 -> 中断处理程序 -> 恢复现场 那中断处理程序是保存在哪的呢?实际上,它的入口地址是保存在中断向量表当中的,由于计算机中的中断个数是固定的,一般在操作系统启动的时候,会初始化一个中断向量表,它会保存固定个数的中断处理程序入口的地址,这样的话,CPU就可以根据中断号,从中断向量表当中找到对应中断的中断处理程序的入口地址,从而调用处理程序。 对于闹钟这个中断源,产生了一个中断信号,不同的中断源会产生不同的中断信号,再回到这个例子,张三在看书的时候,可能闹钟响的同时,会听到外面有人敲门的中断信号到来,还有可能是电话响起来产生另外一个中断信号,但是对于同时到来的中断,张三可以决定哪个中断先处理,这也就是中断的优先级,他觉得开水烧开的中断优化级最高。另外张三也有可能屏蔽一些不必要的中断,比如说电话响铃了,在看书之时,他觉得这个中断是可以屏蔽的,也就是中断屏蔽.

中断分类

比如键盘产生的中断、鼠标产生的中断、打印机产生的中断等,这些都是属于硬件中断。

比如说除0中断、单步执行、对于X86平台来说执行了一个INT指令,从用户空间到内核空间

信号【核心】

信号与中断总结

信号名称

这些信号都有它们不同的涵义:

对于这些信号的默认处理行为(也就是我们可以自定义自己的行为),可以在man手册上查询到:

进程对信号的三种响应

当对于到来的信号,可以有三种不同的响应

为什么呢?SIGKILL是杀死进程的信号,它是9号进程:

在shell命令中,我们可以用kill -9 pid来对一个进程进行杀死,如果进程能够忽略9号信号的话,意味着当管理员向进程发送9号信号时,如果进程可以屏蔽不处理的话,那就无法杀死一个非法的进程了,而同理,SIGSTOP是停止一个进程信号,

也就是我们看到的信号对应的action,上面有介绍过:

signal

下面来学一下安装信号函数:

下面以代码来进行说明:

编译运行:

按下ctrl+c:

可以信号是一种异步事件的响应,当响应完之后,会还原现场,又回到了for死循环代码上了:

这时按下ctrl+\退出:

实际上,ctrl+\会产生一个退出信号:

由于我们程序没有注册该信号,所以由系统默认处理,既程序结束。

对于signal函数,它的返回值为它注册的上一个信号的处理程序,这样说有点空洞,下面以程序来说明一下:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
//typedef void (*__sighandler_t) (int); //Mac 上需要自定义__sighandler_t函数指针
int main(int argc, char *argv[])
{
    __sighandler_t oldhandler;
    oldhandler = signal(SIGINT, handler);//这时返回的处理程序是注册handler之前的,也就是系统默认的处理程序
    if (oldhandler == SIG_ERR)
        ERR_EXIT("signal error");

    while (getchar() != '\n')//死循环是为了测试,当按了ctrl+c之后,会不断死循环,直到按了回车键
        ;
    if (signal(SIGINT, oldhandler) == SIG_ERR)//这时,再次注册信号,但是这次是注册成了默认处理程序,而ctrl+c的默认处理就是终止程序
        ERR_EXIT("signal error");
    for (;;) ;
    return 0;
}

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

编译运行:

按回车键:

这时,再按ctrl+c:

实际上,恢复默认的处理行为,还可以用它来代替:

编译运行,输出效果一样:

信号分类

可靠信号与不可靠信号

不可靠信号

我们用上节课的例子来解释上面的这断话:

运行来看下:

而第二次再按ctrl+c,还是会输出我们的处理程序:

所以与之对比,上面文字中提到的不可靠信号的默认动作就能理解了,就是早期的unix当我们注册完了信号,并处理信号时,则会恢复到默认动作,表现形式也就是这样(模拟):

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

从上面这段程序可以看出,当再次注册信号时,如果新的SIGINT信号过来了,但是信号还没注册完,那么还是会响应ctrl+c的默认动作,也就是程序退出了,这就说明是做出了一个错误的反应,另外,关于信号可能会丢失说的是哪方面呢?是指当来了多个SIGINT信号时,不可靠信号是不会排队的,只会保留一个,其它的都被丢弃掉。

也就是说,如今的linux的不可靠信号在处理完之后,是不会被恢复成默认动作的,而不可靠信号同样还有这样的特征:"不可靠信号是不会排队的,只会保留一个,其它的都被丢弃掉",也就是存在信号的丢失。

总结一下:早期unix的不可靠信号在执行完之后会被恢复成默认动作,也就是会做出错误的反应,并且信号不会排队,存在信号丢失问题;而如今的linux的不可靠信号在执行完之后是不会被恢复的,也就是不会做出错误的反应,但是还是存在信号丢失的问题,所以基于这个原因,就出现了下面要介绍的可靠信号了。

可靠信号

那新增的可靠信号是哪些呢?

在之前介绍不可靠信号时,其中说到“linux信号安装函数(signal)是在可靠机制上的实现”,也就是说signal和sigaction是一样的,都是可靠信号的安装,实际上它们都调用了更加底层的dosigaction内核函数,只能内核才能调用到,做一个了解。

实时信号

其中后32个信号是没有具体含义的,可供应用程序进行使用, 另外SIGRTMIN不一定都是从34开始,先查看一下目前它是从哪开始的:

可以从signal的帮助文档中可以阅读到:

信号发送

关于kill函数,我们经常会用到:

实际上更准确的说法是向pid进程发送9号信号,由于9号信号不能被忽略,也不能被捕获的,而它默认动作就是将进程给杀掉,所以我们经常用这个命令来杀死进程。

实际上kill命令的实现是靠kill系统函数,可以man查看一下:

下面对该函数的描述进行认识,之后会用到:

当pid=-1时,信号sig将发送给调用者进程有权限发送的每一个进程,除了1号进程和自身之外。

举一个子进行程父进程发送信号的例子:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
int main(int argc, char *argv[])
{
    if (signal(SIGUSR1, handler) == SIG_ERR)//注册了一个可靠信号
        ERR_EXIT("signal error");
    pid_t pid = fork();
    if (pid == -1)
        ERR_EXIT("fork error");

    if (pid == 0)
    {
        kill(getppid(), SIGUSR1);//向父进程发送一个信号
        exit(EXIT_SUCCESS);
    }

    sleep(5);//父进程睡眠五秒
    return 0;
}

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

编译运行:

原因是由于sleep函数会被信号所打断而返回,在打断返回之前会先处理信号,所以就没有出现了我们的预期,那如果要实现真正睡眠5秒怎么做呢?可以查看sleep函数的man帮助:

这时再看效果:

可见通过这种取巧的办法就解决了我们所遇到的问题。 下面来看一下这个情况,也就是给进程组发送信号:

首先查看一下怎么得到进程组ID:

下面看下具体代码:

运行效果:

解释一下程序,之所以打印出两条语句,是由于注册信号是在fork()之前,所以子进程会继承父进程的信号所安装的程序,也就是子进程中也安装了这个信号,而子进程向进程组发送了一个信号,则每个进程都会收到信号,当子进程收到时,会打印一条语句,然后立马退出了,而父进程同样也会收到,但是它会sleep五秒后才退出,所以才出现了如上效果。

另外向父进程发送信号还有另外一种等价的使用方法:

查看一下帮助:

pause

而比较好的方式是采用我们要学的这个pause函数来让进程挂起,直到一个信号被捕获了,代码调整如下:

看下效果:

可以很清楚的看到,当收到信号时,则pause函数就被返回了,这样的做法就会比较好,在信号没发送之前让进程挂起,信号处理完,则就返回了。

更多信号发送函数

alarm:只能发送SIGALRM信号

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
int main(int argc, char *argv[])
{
    if (signal(SIGALRM, handler) == SIG_ERR)//注册了一个alrm信号
        ERR_EXIT("signal error");

    alarm(1);//发送一个alarm信号
    for (;;)
        pause();
    return 0;
}

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

关于alarm函数的说明可以查看man:

运行效果:

可见,是隔了一秒才发送出alarm信号的,实际上,我们可以找到该进程,用shell命令中人为的发送该信号:

通过kill命令来发送信号,为了看到效果,新开一个命令终端,效果如下:

可以看到手动发送信号也是可以正常收到的,另外,我们在发送信号时,既可以用数字,也可以用它对应的名称,如下:

实际上,对于进程的ID,可以通过动态方式来获取,按如下步骤(了解一下):

1、先过滤掉其它行

2、然后再只得到进程ID列,过滤掉其它列

所以,手动发送信号时,就可以用动态的方式来发送了,如下:

我们发现,alarm函数不能每隔一秒发送一次信号,那如果要做到这点该怎么办呢?

setitimer:发送SIGALRM、SIGVTALRM、SIGPROF信号

abort:只能发送SIGABRT信号

可重入函数与不可重入函数

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)


typedef struct
{
    int a;
    int b;
} TEST;

TEST g_data;//定义了一个全局变量,是为了说明不可重入函数的问题

void handler(int sig);
int main(int argc, char *argv[])
{
    TEST zeros = {0, 0};
    TEST ones = {1, 1};
    if (signal(SIGALRM, handler) == SIG_ERR)
        ERR_EXIT("signal error");

    g_data = zeros;//默认赋值
    alarm(1);
    for (;;)
    {
        g_data = zeros;
        g_data = ones;//不断对其进行赋值,正常情况应该要不是zeros,要不就是ones
    }
    return 0;
}

void handler(int sig)
{
    printf("%d %d\n", g_data.a, g_data.b);//打印出值,观察其输出可以看到不可重录函数的缺点
    alarm(1);
}
复制代码

编译运行:

这是为什么呢?

这是由于有一个全局变量g_data,而且在for循环中不断进行赋值,由于赋值不是一个原子操作,拿g_data=zeros这个赋值操作来说,由两部组成:

①g_data.a = zeros.a;

②g_data.b = zeros.b

如果g_data之前的值为ones,当执行到第①步赋值操作时,信号来了终止了第②步赋值操作,那处理handler打印时,则会打印出0,1,因为第二个赋值操作停止了,造成了只赋值了一部分,所以上面程序的打印结果就可以解释了,将handler中的打印语句提取到一个新的函数中:

导致不可重录函数的原因,是由于: 中断之前的处理程序跟中断程序访问了一些共享数据g_data,

【说明】:如果此处不访问g_data的话,也就不会产生不可重录的问题。

所以导致不可重录只要有以下几个方面:

那一些函数才算是安全可以在信号处理函数中使用呢?查看man帮助:

除了这些函数是安全的之外,其它的都是不安全的,所以说使用信号是很容易出错的,现在的内核也正在考虑有没有一个机制来替换信号,实际上正在考虑能否用文件描述符来替换信号,这正是下个内核要实现的功能,将信号融入到文件描述上进行处理。

信号在内核中的表示

下面用图来进一步描述这种信号从产生到递达之间的状态(信号阻塞与未诀):

那是怎么来决定的呢?下面慢慢来举例分解:

信号集操作函数

其中解释一下sigset_t,百度百科解释为:

而这个函数的意义就是将这64位清0

这个函数的意义是将这屏蔽字的64位都变为1

将这个信号所对应的位置为1

将这个信号所对应的位置为0

检测这一个信号所对应的位当前是0还是1

以上是操作信号集的五个相关的函数,但是注意:这五个函数仅仅是改变这个信号集变量,如set,并非真正改变进程信号当中的屏蔽字,所以接下来介绍的函数就是改变信号当中的屏蔽字的

sigprocmask

一个信号从产生到递达的一个状态转换过程:

首先,我们打印出系统的未诀信号,目的是为了观察之后的状态:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
void printsigset(sigset_t *set)//打印出信号集的状态,其中参数set为未诀状态的信号集
{
    int i;
    for (i=1; i<NSIG; ++i)//NSIG表示信号的最大值,也就是等于64
    {
        if (sigismember(set, i))//说明是未诀状态的信号
            putchar('1');
        else
            putchar('0');//说明不是未诀状态的信号
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    sigset_t pset;

    for (;;)
    {
        sigpending(&pset);//该函数是获取进程当中未诀状态的信号集 ,保存在pset当中
        printsigset(&pset);//打印信号集的状态,看有没有未诀状态的信号产生
        sleep(1);
    }
    return 0;
}
复制代码

【说明】:sigpending是用来获取进程中所有的未诀信号集:

这时看一下运行效果:

可以发现,当前状态没有未诀的信号,因为还没有被阻塞的信号过,信号也没有产生过,所以不可能有未诀的状态。

这时,我们来安装一个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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
void printsigset(sigset_t *set)
{
    int i;
    for (i=1; i<NSIG; ++i)
    {
        if (sigismember(set, i))
            putchar('1');
        else
            putchar('0');
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    sigset_t pset;
    if (signal(SIGINT, handler) == SIG_ERR)//安装一个SIGINT信号
        ERR_EXIT("signal error");

    for (;;)
    {
        sigpending(&pset);
        printsigset(&pset);
        sleep(1);
    }
    return 0;
}

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

这时再看下效果:

从结果来看,信号被直接递达了,所以这次也没有看到有1的未诀状态的信号,因为信号必须被阻塞才会出现未诀状态,所以接下来将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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
void printsigset(sigset_t *set)
{
    int i;
    for (i=1; i<NSIG; ++i)
    {
        if (sigismember(set, i))
            putchar('1');
        else
            putchar('0');
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    sigset_t pset;
    
    sigset_t bset;
    sigemptyset(&bset);//将信号集清0
    sigaddset(&bset, SIGINT);//将SIGINT所对应的位置1

    if (signal(SIGINT, handler) == SIG_ERR)
        ERR_EXIT("signal error");

    sigprocmask(SIG_BLOCK, &bset, NULL);//更改进程中的信号屏蔽字,其中第三个参数传NULL,因为不关心它原来的信号屏蔽字
    for (;;)
    {
        sigpending(&pset);
        printsigset(&pset);
        sleep(1);
    }
    return 0;
}

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

编译运行:

从结果来看,将SIGINT信号来了,由于添加到了信号屏蔽字为1,所以会被阻塞掉,并且可以看到SIGINT对应的位也打印为1了。

【说明】:SIGINT对应的位是指:

下面,我们做一件事情,就是当我们按下ctrl+\解除阻塞,这样处于未诀状态的信号就会被递达,则对应的未诀状态位也会还原成0,具体代码如下:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
void printsigset(sigset_t *set)
{
    int i;
    for (i=1; i<NSIG; ++i)
    {
        if (sigismember(set, i))
            putchar('1');
        else
            putchar('0');
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    sigset_t pset;
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, SIGINT);
    if (signal(SIGINT, handler) == SIG_ERR)
        ERR_EXIT("signal error");
    if (signal(SIGQUIT, handler) == SIG_ERR)//注册一个ctrl+c信号
        ERR_EXIT("signal error");

    sigprocmask(SIG_BLOCK, &bset, NULL);
    for (;;)
    {
        sigpending(&pset);
        printsigset(&pset);
        sleep(1);
    }
    return 0;
}

void handler(int sig)
{
    if (sig == SIGINT)
        printf("recv a sig=%d\n", sig);
    else if (sig == SIGQUIT)
    {
        sigset_t uset;//当按下ctrl+\时,则对SIGINT信号解除阻塞
        sigemptyset(&uset);
        sigaddset(&uset, SIGINT);
        sigprocmask(SIG_UNBLOCK, &uset, NULL);
    }
}
复制代码

编译运行:

从中可以看到,当我们按下ctrl+\时,并没有退出,而是解除了阻塞,所以对应的SIGINT位也变为0了。

另外,看下这种情况:

多次按了ctrl+c,可在按ctrl+\解除阻塞时,只响应了一次信号处理函数,这也由于SIGINT是不可靠信号,不支持排队。

另外,由于我们捕获了ctrl+\信号,所以没办法退出这个进程了,那怎么办呢,可以利用shell命令将其强制杀掉如下:

sigaction函数

安装信号之前我们已经学过一个函数:signal,它最早是在unix上出现的,它是对不可靠信号进行安装的,之后出现了可靠信号和实时信号,所以新的安装函数sigaction函数就出现了,它的原形如下:

sigaction结构体

通过man手册来查看一下它的说明:

sigaction

实现signal相同的功能来安装一个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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);

int main(int argc, char *argv[])
{
    struct sigaction act;//安装信号时需要传参
    //act.__sigaction_u.__sa_handler = handler;//for mac
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);//先将sa_mask清空,关于这个属性的用法之后实验再说明
    act.sa_flags = 0;//同样将sa_flags设为0,这个实现不需要关心,之后会说明

    if (sigaction(SIGINT, &act, NULL) < 0)//安装信号
        ERR_EXIT("sigaction error\n");

    for (;;)
        pause();
    return 0;
}

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

编译运行:

实际上,对于signal这个安装函数是在可靠的机制之上进行的,也就是说可以认为它是通过sigaction来实现的,所以接下来,用sigaction来模拟signal函数的行为:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);
__sighandler_t my_signal(int sig, __sighandler_t handler);//这跟signal的信号安装函数声明一样,实现自己的signal

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

    if (sigaction(SIGINT, &act, NULL) < 0)
        ERR_EXIT("sigaction error\n");
*/
    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功能比signal要强大说了,其中有两个参数需要说明一下:

其中sa_handler只适合不可信号的安装,也就是说不可信号的安装不能用sa_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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);

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

    if (sigaction(SIGINT, &act, NULL) < 0)
        ERR_EXIT("sigaction error");

    for (;;)
        pause();
    return 0;
}

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    sleep(5);//故意睡眠5秒是为了看在执行期间按了ctrl+\就能立马响应退出信号
}
复制代码

编译运行:

可以看到,在执行信号处理函数期间,按ctrl+c时,并没有等待sleep5秒完之后,再执行退出动作,而是立马执行了,那能不能改变这种默认,也就是必须得等sleep5秒后再执行退出动作,答案是当然可以的,sa_mask属性就派上用场了:

结果如下:

从实验结果来看,在执行信号处理时,多次按了ctrl+c退出信号,并未立马执行退出动作,而是等执行完了才退出的,这也就是sa_mask的作用,实际上也就是上节学习的信号屏蔽字。

思考一个问题: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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

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);//将其sa_mask注册去掉,也就是清零了
    act.sa_flags = 0;

    sigset_t s;
    sigemptyset(&s);
    sigaddset(&s, SIGINT);
    sigprocmask(SIG_BLOCK, &s, NULL);//这就是之前学的,将SIGINT加入到进程的屏蔽字中

    if (sigaction(SIGINT, &act, NULL) < 0)
        ERR_EXIT("sigaction error");

    for (;;)
        pause();
    return 0;
}

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    sleep(5);
}
复制代码

编译运行:

这时可以看到,信号被阻塞而压缩就不会执行处理函数了,这也是之前学过时的现象,从实验可以总结出:

sa_mask中指定的掩码也可以阻塞信号,它阻塞的信号是指函数在执行的过程当中,如果发生了在掩码级中指定的信号,信号将被阻塞,直到handler返回,这些信号才能被递达;

sigprocmask它所阻塞的信号表示将这些集合中的信号添加到进程信号屏蔽字当中,这些信号就不能被递达了,既使它发生了。

sigqueue函数

sigval联合体

实际上sigval参数是用来进程间通信用的,实际上信号是一个很古老的进程间通信的一种手段,通过这个参数,可以从一个进程发送到另外一个进程,并且可以附带参数。下面以实际代码来说明sigqueue是如何传递数据的:

这里需要编写一个进程发送信号和一个进程接收信号来说明sigqueue:

接收信号:

要想接收信号数据,则需要在sigaction上加入flag,也就是上节当中遗漏的一个知识点,查看man帮助:

另外,还得注意,如果要想接收信号,需要用到sigaction的另外一个处理函数:

具体接收代码如下sigaction_recv.c:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int, siginfo_t *, void *);//需要用带多个参数的handler

int main(int argc, char *argv[])
{
    struct sigaction act;
    act.sa_sigaction = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;//设置该flags可以接收其它进程传过来的信号

    if (sigaction(SIGINT, &act, NULL) < 0)
        ERR_EXIT("sigaction error");

    for (;;)
        pause();
    return 0;
}

void handler(int sig, siginfo_t *info, void *ctx)
{
    printf("recv a sig=%d data=%d\n", sig, info->si_value.sival_int);//它实际上就是sigqueue的sigval联合体,并打印出传过来的整型值
}
复制代码

发送信号: 编写一个通过sigqueue向某进程发送信号并携带数据来达到进程间通讯的目的: sigaction_send.c:

编译运行: 先运行接收信号的程序:

再运行进程发送程序,向该接收进程发送信号,先找到该接收进程号:

然后进行我们写的信号发送程序:

另外,数据的接收还可以用这种方式来打印:

输出如下:

为啥可以这样写呢?查看sigaction的帮助:

所以通过这种方式就能达到进程间通讯的目的。

接下来再演示一个不可靠与可靠信号的一个问题:可靠信号支持排队,不可靠信号是不支持排队的。

验证方法:

① 写一个接收程序,里面注册一个可靠信号,一个不可靠信号,并将其加入屏蔽字中进行阻塞,然后再注册一个解除这两个信号阻塞的信号,程序如下:

sigaction_recv.c:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int);

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

    sigset_t s;
    sigemptyset(&s);
    sigaddset(&s, SIGINT);
    sigaddset(&s, SIGRTMIN);
    sigprocmask(SIG_BLOCK, &s, NULL);//将这两个信号都加入屏蔽字当中进行阻塞
    if (sigaction(SIGINT, &act, NULL) < 0)//注册一个不可靠信号
        ERR_EXIT("sigaction error");

    if (sigaction(SIGRTMIN, &act, NULL) < 0)//注册一个可靠信号
        ERR_EXIT("sigaction error");

    if (sigaction(SIGUSR1, &act, NULL) < 0)//注册一个解除阻塞信号的信号
        ERR_EXIT("sigaction error");
    for (;;)
        pause();
    return 0;
}

void handler(int sig)
{
    if (sig == SIGINT || sig == SIGRTMIN)
        printf("recv a sig=%d\n", sig);
    else if (sig == SIGUSR1)
    {//当接收到SIGUSR1信号时,则解除阻塞,这时被阻塞的SIGINT,SIGRTMIN信号就变为递达状态了,这时就可以观察两者的区别了
        sigset_t s;
        sigemptyset(&s);
        sigaddset(&s, SIGINT);
        sigaddset(&s, SIGRTMIN);
        sigprocmask(SIG_UNBLOCK, &s, NULL);
    }
}
复制代码

② 编写一个发送信号的程序,里面发送多个SIGINT可靠信号,多个SIGTMIN不可靠信号,并且延时几秒之后,再将发送SIGUSR1信号解除绑定,最终观察看SIGINT收到了几个,SIGTMIN收到了几个,具体代码如下:

sigaction_send.c:

#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>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

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]);
    union sigval v;
    v.sival_int = 100;
    sigqueue(pid, SIGINT, v);
    sigqueue(pid, SIGINT, v);
    sigqueue(pid, SIGINT, v);//发送三个SIGINT不可靠信号

    sigqueue(pid, SIGRTMIN, v);
    sigqueue(pid, SIGRTMIN, v);
    sigqueue(pid, SIGRTMIN, v);//发送三个SIGTMIN可靠信号
    
    sleep(3);//休眠3秒,目的是让其多个信号进行阻塞
    
    kill(pid, SIGUSR1);//发送一个SIGUSR1信号进行解除阻塞,改用kill来发送,当然也可用sigqueue啦
    return 0;
}
复制代码

编译运行:

从运行结果来看,经过3秒之后,34号信号,也就是SIGRTMIN信号接收了三次,而2号信号SIGINT信号只接收了一次,所以也论证了可靠信号是支持排队的,而不可靠信号是不支持排队的。

三种不同精度的睡眠

查看man帮助:

以微秒为单位,那微秒跟秒是什么关系呢? 1秒=10的3次方毫秒=10的6次方微妙

以纳秒为单位的休眠

三种时间结构

setitimer

查看一下man帮助,其中ittimerval结构体的内容如下:

#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>
#include <sys/time.h>//另外需要包含头文件


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

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

int main(int argc, char *argv[])
{
    if (signal(SIGALRM, handler) == SIG_ERR)//注册一个SIGALRM信号
        ERR_EXIT("signal error");

    struct timeval tv_interval = {1, 0};
    struct timeval tv_value = {5, 0};//那这两个值有什么意义呢?
    struct itimerval it;
    it.it_interval = tv_interval;
    it.it_value = tv_value;
    setitimer(ITIMER_REAL, &it, NULL);//这个函数会间接性地产生SIGALRM信号,需传递ITIMER_REAL参数

    for (;;)
        pause();
    return 0;
}
复制代码

编译运行:

从运行结果可以知道,等待五秒钟之后才打印出来,以后每过一秒打印,那再回过头来理解下代码:

struct timeval tv_interval = {1, 0};//之后产生信号的间隔时间

struct timeval tv_value = {5, 0};//第一次产生信号的时间

另外,setitimer的第三个参数ovalue是干嘛用的呢?下面用实验也来说明下:

#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>
#include <sys/time.h>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)


int main(int argc, char *argv[])
{
    struct timeval tv_interval = {1, 0};
    struct timeval tv_value = {1, 0};//第一次产生信号需要等待1秒
    struct itimerval it;
    it.it_interval = tv_interval;
    it.it_value = tv_value;
    setitimer(ITIMER_REAL, &it, NULL);

    int i;
    for (i=0; i<10000; i++);//故意产生10000次的循环,运行时间肯定不足一秒,所以目的也就是为了看到第三个参数的效果
        ;

    struct itimerval oit;
    setitimer(ITIMER_REAL, &it, &oit);//在第一次信号还没有产生之时,又重新设置一个定时,这时oit会存放上一次设置的时钟它还剩余的时间
    printf("%d %d %d %d\n", (int)oit.it_interval.tv_sec, (int)oit.it_interval.tv_usec, (int)oit.it_value.tv_sec, (int)oit.it_value.tv_usec);

    return 0;
}
复制代码

编译运行:

【注意】:这个效果跟下面要说明的getitimer效果一样,但是有一些区别,getitimer得到剩余时间是不重新设置时钟。

另外还有一个相关函数:

#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>
#include <sys/time.h>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)


int main(int argc, char *argv[])
{
    struct timeval tv_interval = {1, 0};
    struct timeval tv_value = {1, 0};
    struct itimerval it;
    it.it_interval = tv_interval;
    it.it_value = tv_value;
    setitimer(ITIMER_REAL, &it, NULL);

    int i;
    for (i=0; i<10000; i++);
        ;

    getitimer(ITIMER_REAL, &it);//获得产生首次信号的时间,并没有从
    printf("%d %d %d %d\n", (int)it.it_interval.tv_sec, (int)it.it_interval.tv_usec, (int)it.it_value.tv_sec, (int)it.it_value.tv_usec);

    return 0;
}
复制代码

转载于:https://juejin.im/post/5cea1804e51d4510926a7ac7

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值