信号的捕捉与可重入函数

  • 信号的捕捉

  • 在信号的相关概念中曾提到如果一个信号没有被Block,但被Pending,但不会立即递达,而是在合适的时候,这里的合适的时候是指:当进程从内核态返回用户态时,会对信息进行检测处理。

  • 正如下图所示,是系统在对信号进行捕捉时经历的过程:
    这里写图片描述

可以简化为:

这里写图片描述

  • 首先先来介绍一下什么是信号捕捉:
    如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为信号捕捉

  • 那么内核是如何实现信号捕捉的呢??
    由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这是发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,他们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

  • sigaction函数

头文件:#include<signal.h>
函数原型:int sigaction(int signo,const struct sigaction                    *act,struct sigaction *oact);
函数功能:可以读取和修改与指定信号相关联的处理动作。
返回值:调用成功返回0,出错返回-1。
参数:signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体
struct sigaction结构体
    struct sigaction {
        void     (*sa_handler)(int);
        //老类型的信号函数处理指针
        void     (*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号函数处理指针
        sigset_t   sa_mask;//将要被阻塞的信号集合
        int       sa_flags;//信号处理方式掩码
        void     (*sa_restorer)(void);//保留,不要使用
    };

sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点掌握:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数),用于指向原型为void handler(int)的信号处理函数地址。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。

  • pause函数
头文件:#include<unsitd.h>
函数原型:int pause(void);
函数功能:使调用进程挂起直到有信号递达

如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作默认是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数后pause返回-1,errno设置为EINTR(被信号中断),所以pause只有出错的返回值
什么叫把进程(PCB)挂起??
把进程状态设置为T状态,把进程放到等待队列里
挂起不能被调度,只有R状态才能被调度

  • 下面我们用alarm和pause实现mysleep函数
    思想:
    1.首先main函数调用mysleep函数,后者调用sigaction注册了SIGALAM信号的处理函数sig_alrm
    2.调用alarm(n)设定闹钟
    3.调用pause等待,内核切换到别的进程运行
    4.n秒后,闹钟超时,内核发送SIGALRM信号给该进程
    5.从内核态返回用户态之前处理未决信号,发现有SIGALRM信号,处理函数是sig_alrm
    6.切换到用户态执行sig_alrm函数,进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程
    7.pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理动作

  • 代码实现如下

#include <stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alarm(int signo)
{
    (void)signo;
}
unsigned int mysleep(unsigned int n)
{
    struct sigaction New,Old;
    unsigned int unslept = 0;
    New.sa_handler = sig_alarm;
    sigemptyset(&New.sa_mask);
    New.sa_flags = 0;
    sigaction(SIGALRM,&New,&Old);//注册信号处理函数
    alarm(n);//设置闹钟
    pause();
    unslept = alarm(0);//清空闹钟
    sigaction(SIGALRM,&Old,NULL);//恢复默认信号处理动作
    return unslept;
}
int main()
{
    while(1)
    {
        mysleep(3);
        printf("hello world!\n");
    }
    return 0;
}
  • 运行结果如图:

这里写图片描述

这里写图片描述

但是系统运行的时序并不像我们写程序时所设想的那样。虽然alarm(n)紧接进行pause(),但是无法保证pause()一定会在调用alarm(n)之后的n秒之内被调用。由于异步事件在任何时候都有可能发生,如果我们写程序时考虑不周全,就可能由于时序问题而导致错误,这叫做竞态条件
解决以上问题的方法就是将“解除信号屏蔽”和“挂起等待信号”这两步合并成一个原子操作,这是就要使用sigsuspend函数了。
sigsuspend函数

头文件:#include<signal.h>
函数原型:int sigsuspend(const sigset_t *sigmask)
函数功能:解除对信号的屏蔽并挂起
  • 可重入函数

  • 首先解释一下什么是重入:

当一个函数被不同的执行流程控制,有可能在第一次调用还没返回时就再次进入此函数,就称为重入

  • 那什么样的函数是可重入的呢??

如果一个函数只访问自己的局部变量或参数,数据和状态不会被破坏,则称为可重入函数。反知,如果一个函数访问全局链表,有可能因为重入而发生错乱,不能保证函数的行为一致和结果相同,像这样的函数就是不可重入的

  • 可重入和线程安全是两个不同的概念:可重入函数一定是线程安全的;线程安全的函数可能是重入的,也可能是不重入的;线程不安全的函数一定是不可重入的

  • 为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

    在线程之中,线程虽然强调资源共享,但是他们的栈却是独有的,所以访问它的同一个局部变量或参数就不会造成错乱。

  • 如果一个函数满足一下条件之一就是不可重入的:
    1.调用了malloc或free,因为malloc也是用全局链表来管理堆的
    2.调用了标准I/O库函数,标准I/O库的实现都以不可重入的方式使用全局数据结构
    3.进行了浮点运算,许多处理器/编译器,浮点运算都是不可重入的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章