欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Liunx之信号捕捉与模拟sleep函数

程序员文章站 2022-07-12 11:17:31
...

信号在未决并且未阻塞状态下被递达的方式有三种:

(1)忽略

(2)执行默认动作

(3)执行用户自定义动作

前两个方式的递达都是由内核完成;而第三种处理的动作是用户自定义函数,在信号递达时就调用这个函数,这就称为捕捉信号


下面我们用看图来分析捕捉信号的过程:

首先我们要知道信号处理的时机是内核态切回到用户态时。

Liunx之信号捕捉与模拟sleep函数

1.首先用户主函数里注册了某信号的自定义函数,处理信号的自定义函数是右上角的sighandler。

2.当前正在执行main()函数时,由于中断、异常或者系统调用切换到内核态。

3.在中断处理完毕后从内核态返回到用户态时检查是否有可以抵达的信号。

4.若有可以抵达的信号,内核则决定不恢复main()函数的上下文继续执行,而是切换到用户态调用sighandler函数抵达信号。sighandler和main使用不同的堆栈空间,不存在调用与被调用关系,是两个不同的控制流。

5.在执行用户自定义函数完成信号递达后,自动执行特殊的系统调用sys_sigreturn()再切换到内核态。

6.如果检查到再没有可以递达的信号,则切换到用户态恢复main()函数的上下文继续执行。


sigaction()函数:用来捕捉信号时修改信号的处理方式

       #include <signal.h>
       int sigaction(int signum,  const struct sigaction *act,  struct sigaction *oldact);

 ◆ signum:要操作的信号。
 ◆ act:要设置的对信号的新处理方式。
 ◆ oldact:原来对信号的处理方式。
 ◆ 返回值:0 表示成功,-1 表示有错误发生。

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

信号处理函数可以采用void (*sa_handler)(int)void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用void (*sa_handler)(int),此时只能向处理函数发送信号的数值。

sa_handler此参数赋值为SIG_IGN代表忽略信号,赋值为SIG_DFL则代表执行默认动作,赋值为一个函数指针表示自定义函数捕捉信号

sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的需要屏蔽的信号集搁置。

sa_restorer 此参数没有使用。

sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。

sa_flags还可以设置其他标志:

SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL

··SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用

SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号


pause()使进程挂起直到有信号递达。

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


现在了解了信号捕捉的原理和一些函数,我们来模拟一个sleep()函数验证一下捕捉信号:

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

void sig_fun()     //用户自定义抵达方法,因为是sleep所以方法什么也不做
{
}

void mysleep(int time)
{
    struct sigaction new, old;
    new.sa_handler = sig_fun;  //自定义抵达方法
    new.sa_flags = 0;
    sigemptyset(&new.sa_mask); //初始化信号集为空
    sigaction(SIGALRM,&new,&old);
    alarm(time);       //设置闹钟
    pause();
    int sig = alarm(0);
    sigaction(SIGALRM,&old,NULL);
}

int main()
{
    while(1)
    {
       mysleep(3);
       printf("my sleep runing\n");
    }
    return 0;
}

运行结果:每sleep 3秒打印一句话。

Liunx之信号捕捉与模拟sleep函数


虽然sleep功能已经实现,但是上面的代码可能存在bug,在哪有bug呢?

 现在重新看sleep程序设想这样的时序 
1.注册 SIGALRM信号的处理函数。
2.调用a1arm( time)设定闹钟。
3.内核调度优先级更高的进程取代当前进程执行并且优先级更高的进程有很多个,每个都要执行很长时间4. time秒钟之后闹钟超时了,内核发送 SIGALRM信号给这个进程处于未决状态。
5.优先级更高的进程执行完了内核要调度回这个进程执行。 SIGALRM信号递达执行处理函数sig_fun之后再次进入内核。
6.返回这个进程的主控制流程alarm( time),调用 pause()挂起等待 。
7.这时候信号SIGALRM已经递达,那么pause就一直在等待了...


出现这种情况的原因是因为竞态条件

竞态条件就是如上面的情况系统出现不恰当的执行时序,而得到不正确的结果。,在执行完alarm(time)后本应该执行pause();由于在alarm(time)后发生了异步时间,所以导致了信号提前递达的错误。从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

在计算机内存或者存储里,如果同时发出读写大量数据的指令的时候竞态条件可能发生,机器试图覆盖相同的或者旧的数据,而此时旧的数据仍然在被读取。结果可能是下面一个或者多个情况:计算机死机,出现非法操作提示并结束程序,错误的读取旧的数据,或者错误的写入新数据。在串行的内存和存储访问能防止这种情况,当读写命令同时发生的时候,默认是先执行读操作的。

网络里,竞态条件(race condition)会在两个用户同时试图访问同一个可用信道的时候发生,在系统同意访问之前没有计算机能得到信道被占用的提示。统计上说这种情况通常是发生在有相当长的延迟时间的网络里,比如使用地球同步卫星。为了防止这种竞态条件发生,需要添加一个优先级列表。比如用户的用户名在字母表里排列靠前可以得到相对较高的优先级。黑客可以利用竞态条件这一弱点来赢得非法访问网络的权利。

当出现如数冲突的时候,逻辑门偶尔发生竞态条件。由于门的输出状态是有限的,相应输入变化的时间是非零值,因此会导致一些不合适的操作。


那么怎么办呢?
     如果在alarm设定之前将信号SIGALRM屏蔽,然后在pause挂起等待之前再将SIGALRM信号解除屏蔽,如果可以把pause和解除屏蔽用某种方法搞成原子性的,那么在pause和alarm之间就不会发生异步事件。这正是sigsuspend的功能。
    这里有一个sigsuspend()函数包含了pause的挂起和等待,同时解决了竞态条件的问题。在时序要求严格场合要用sigsuspend代替pause。

sigsuspend函数 :

     sigsuspend函数接受一个信号集指针,将信号屏蔽字设置为信号集中的值,在进程接受到一个信号之前,进程会挂起,当捕捉一个信号,首先执行信号处理程序,然后从sigsuspend返回,最后将信号屏蔽字恢复为调用sigsuspend之前的值。



下面程序用到的相关函数

sigaddset()函数 

      #include<signal.h>

       int sigaddset(sigset_t *set,int signum);

      sigaddset()用来将参数signum 代表的信号加入至参数set 信号集里。

sigdelset()函数
      #include<signal.h>
  int sigdelset(sigset_t * set,int signum);
      sigdelset()用来将参数signum代表的信号从参数set信号集里删除。函数执行成功则返回0,如果有错误则返回-1。

sigprocmask()函数

       #include <signal.h>
       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

sigprocmask设定对信号屏蔽集内的信号的处理方式(阻塞或不阻塞)。

参数:

      how:用于指定信号修改的方式,可能选择有三种:
      SIG_BLOCK //加入信号到进程屏蔽。
      SIG_UNBLOCK //从进程屏蔽里将信号删除。

      SIG_SETMASK //该进程新的信号屏蔽字将被set指向的信号集的值代替

      set:若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。

      oldset:若oldset是非空指针,那么进程的当前信号屏蔽字通过oldset返回。

返回说明:

成功执行时,返回0。失败返回-1,errno被设为EINVAL。

注意,不能阻塞SIGKILL和SIGSTOP信号

如果set是空指针,则不改变该进程的信号屏蔽字,how的值也无意义


sleep()函数优化版本:

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


void sig_fun()
{
}

void mysleep(int time)
{
    sigset_t newmask, oldmask;
    struct sigaction new, old;
   
    new.sa_handler = sig_fun;  //自定义抵达方法
    new.sa_flags = 0;
    sigemptyset(&new.sa_mask);
    sigaction(SIGALRM,&new,&old); 
   
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGALRM); //将SIGALRM添加到newmask信号集


    /*将newmask中的SIGALRM阻塞掉,并保存当前信号屏蔽字到Oldmask*/
    sigprocmask(SIG_BLOCK,&newmask,&oldmask);
    
    alarm(time);
    sigsuspend(&oldmask);//将SIGALRM解除屏蔽然后挂起等待,信号SIGALRM递达后恢复原来的屏蔽字,也就是继续屏蔽
   
    int sig = alarm(0);
    sigaction(SIGALRM,&old,NULL);
    sigprocmask(SIG_SETMASK,&oldmask,NULL);//恢复被屏蔽的信号SIGALRM
}


int main()
{
    int sig = 0;
    while(1)
    {
       mysleep(3);
        printf("my sleep runing\n");
    }
    return 0;
}