Linux信号

信号的概念:

信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或者系统异常。

信号的产生:

1>对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。如输入Ctrl+c向进程发送一个中断信号。

2>系统异常。如浮点数异常和非法的内存段的访问

3>系统状态变化。如alarm定时器到期将一起SIGALRM信号。

4>运行kill命令或者调用kill函数。

服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。

常见的信号对应的含义:   查看系统信号---  kill -l   或者   man 7 signal

1>关闭终端产生
2> ctrl +c
3> ctrl + \
4> 非法指令
5> 从用户空间嵌入内核产生
6> abort函数产生
7> 两个设备信号断掉 地址映射出错  pipe(最快)  mmap函数(最高效) socket(最稳定)
8> 浮点数异常
9> 杀死进程
10、12>  由用户定义行为
11> 段错误
13> 管道破裂   <----- 关掉管道读端
14>闹钟、警报
15>进程终止
17> 子进程编程僵尸
18> 继续
19>暂停
23>紧急数据

29> 异步IO信号

信号处理的方式:

1.忽略掉       不能被忽略的信号----1.SIGKILL    2.kill -SIGKILL pid    3.SIGSTOP

2.捕获并处理                     SIGKILL       SIGSTOP   不能被捕获

3.执行系统的默认动作

信号的分类:

1.不可靠信号   1-31 -----可能丢失

1>Linux的信号继承自UNIX,早期的Unix当信号处理函数执行完毕时,该信号恢复成缺省处理动作,(Linux已经改进)
2>信号不排队,会出现信号的丢失现象 

2.可靠信号      34 - 64 ------不可能丢失

3.非实时信号    不可靠信号都是非实时的信号

4.实时信号        可靠信号

信号的注册:

typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

handler:

1.自己定义函数

2.SIG_IGN        //忽略信号

3.SIG_DFL        //默认行为

#define SIG_IGN ((sighandler_t)0)    
#define SIG_DFL ((sighandler_t)1)

#define SIG_ERR ((sighandler_t)-1)

返回值:若成功则返回信号以前的处理配置,若出错则返回SIG_ERR

补充:父进程注册的信号处理方式,子进程会继承

发送信号:

1. kill -信号值 pid 

2..killall -信号值 进程名(name)  给所有叫name的进程发送信号,默认15号信号

3. kill(pid_t pid, int signum);

      pid > 0; 给pid进程发送信号
      pid = 0; 给本进程组的所有进程发送信号
      pid = -1; 给有权发送的所有进程发送信号

      pid < -1; 给|pid|进程组的任何一个进程发送信号  

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

void handler(int s)
{
  printf("recv = %d\n", s);
  exit(0);
}

int main()
{
  signal(SIGUSR1, handler);

  pid_t pid = fork();
  if(pid < 0)
  {
    perror("fork");
    return 1;
  }
  else if(pid == 0)
  {
    sleep(3);
   if(-1 == kill(getppid(), SIGUSR1))             //向父进程发送信号,杀死父进程
   {
    perror("kill");
    return 1;
   }
  }
  else
  {
    for(;;)
    {
      printf(".");
      fflush(stdout);
      sleep(1);
    }
  }

  return 0;
}

int raise(int signum);                        //给进程自己发送信号
int killpg(int pgid, int signum);        //给pgid进程组发送信号

pause()   //将当前进程置为可中断睡眠状态,然后调用schedule(),使Linux 进程调度算法找到另外一个进程来执行。(让出CPU来让其他进程运行)

1.pause 调用者进程一直挂起,直到有信号被捕获。

2.pause 要等到信号处理函数执行完毕再返回,这是的pause返回-1,bingjiangerror设置为EINTR

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>           //pause函数要等到handler函数执行完毕才返回

void handler(int s)
{
  printf("recv = %d\n", s);
  sleep(2);
  printf("handler finish\n");
}

int main()
{
  signal(SIGINT, handler);

  while(1)
  {
    pause();
    printf("pause finish\n");
  }
}

进程组介绍(有的系统也称为作业):管道连接的多个进程,父子进程同属一个进程组

管道连接的进程组:第一个进程的pid就是这个进程组的组pid。fork产生的进程组: 父进程的pid是组pid

sleep 100 &   以后台的方式启动进程
ctrl+c       只能发给前台进程组
jobs 查看有哪些后台作业
fg %作业号(缺省为1) :    将后台的作业拿到前台

经典的信号处理方式:

SIGALRM

1.设置延时器(闹钟)     每个进程只能有一个闹钟

unsigned int alarm(unsigned int seconds);   //时间以秒为单位
一定时间触发SIGALARM信号,alarm函数相当于延时器,一定时间触发一次(可以用来进行超时检测)
              如果参数为0,表示清除该信号

返回值为被信号打断后还余留的秒数。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>                     //程序阻塞三秒后向进程发送SIGALRM信号

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

int main()
{
  signal(SIGALRM, handler);
  alarm(3);
  
  while(1)
  {
    pause();
  }

  return 0;
}

设置定时器:

int setitimer(int which,                          //ITIMER_REAL----按桌面时钟来定时
              const struct itimerval *new_value,  //设置的定时器
              struct itimerval *old_value);       //NULL

时间设置的结构体:

struct itimerval {

        struct timeval it_interval; /* next value */        以后每次间隔多长时间调用定时器
        struct timeval it_value;    /* current value */     第一次启动定时器的时间                              
};
struct timeval {
      time_t      tv_sec;         /* seconds */          秒       //都设置成0表示永不启动
      suseconds_t tv_usec;        /* microseconds */     微秒
};
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>

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

int main()
{

  signal(SIGALRM, handler);

  struct itimerval it;
  it.it_value.tv_sec     = 0; //启动时间
  it.it_value.tv_usec    = 1;
  it.it_interval.tv_sec  = 2; //每隔2秒启动一次
  it.it_interval.tv_usec = 0; 
  setitimer(ITIMER_REAL, &it, NULL);

  while(1)
  {
    pause();
  }
  return 0;
}

可重入函数和不可重入函数: 查看系统的可重入函数  man 7 signal

导致函数成为不可重入函数:

1. 函数内部调用了malloc 或 free
2. 调用了标准IO库函数

3. 函数使用了非常量的全局/静态变量

4.函数调用了其他不可重入函数

信号的内核表示: (数据结构为位图)

信号的递达(delivery):     处理信号的处理动作称为信号递达

信号的未决:从信号产生到信号抵达之间的状态称之为信号未决(pending)
信号阻塞(blocking):

1.SIGHUP信号未阻塞也未发生过,当它抵达时执行默认的处理动作

2.SIGINT信号产生过,但正在被阻塞,所以暂时不能抵达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再接触阻塞。

3.SIGQUIT信号从未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义的函数handler

说明:如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次。从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

进程的信号屏蔽子的设置或检查:(信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前的进程收到)

int sigprocmask(int how,    //SIG_BLOCK         :  mask = mask | set       在原来内核屏蔽字的基础上添加set
                                            //SIG_UNBLOCK   :  mask = mask & ~set                                           解除set
                                            //SIG_SETMASK    :  mask = set                    直接替换
                const sigset_t *set,      
                sigset_t *oldset);      //返回原来的信号屏蔽信息

返回值:   -1 表示失败,并设置errno, 0 表示成功

说明:若oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。

信号集操作函数:信号集---能表示多个信号的数据结构

sigset_t set;

设置信号初值
int sigemptyset(sigset_t* set)
{
  memset(set, 0, sizeof(sigset_t));
}
//添加信号
int sigaddset(sigset_t* set, int signum)
{
   *set |= 1<<(signum - 1);
}
//在原来的基础上删除信号             
int sigdelset(sigset_t* set, int num)
{
  *set &= ~(1<<(num-1));
}
//判断信号是否在信号集中          
int sigismember(sigset_t* set, int num)
{
  return *set & 1<<(num - 1);
}

说明:函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

获得内核的未决信号集:

int sigpending(sigset_t* set);

返回值:成功返回0 ,失败返回-1

说明:设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>                              //循环的获得搜有的信号信息,验证信号未决前后的变化。

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

void handler_quit(int s)
{
  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, SIGINT);  //解除SIGINT的屏蔽
  sigprocmask(SIG_UNBLOCK, &set, NULL);
}

int main()
{

  signal(SIGINT, handler);
  signal(SIGQUIT, handler_quit);

  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, SIGINT);
  sigprocmask(SIG_BLOCK, &set, NULL);
  while(1)
  {
    sigset_t pset;
    int i = 0;
    sigemptyset(&pset);
    sigpending(&pset);
    for(i = 1; i < _NSIG; i++)
    {
      if(sigismember(&pset, i))
      {
        printf("1");
      }
      else{
        printf("0");
      }
    }
    printf("\n");
    sleep(1);
  }
  return 0;
}

竞态条件和sigsuspend函数:

先来看一段代码:Mysleep.c

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

void handler(int s){}

int mysleep(int sec)
{
  signal(SIGALRM, handler);
  
  sigset_t set;
  sigprocmask(SIG_BLOCK, &set, NULL);
  alarm(sec);

  pause();
  int r = alarm(0);
  return r;
}

int main()
{
  mysleep(3);
  printf("3 sec\n");
}

这段代码的执行结果是正常的,也是我们想要的结果:三秒之后返回。

现在我们假设有一新的(极端)状态:

1.调用alarm函数的时候,内核调度优先级更高的进程取代了当前进程执行(或者时间片到,CPU去执行其他任务),并且这个进程执行了很长时间.

2.闹钟时间超时,内核发送SIGALRM信号给这个进程,处于未决状态

3.优先级更高的进程执行完了,内核回到当前进程执行,SIGALRM信号递达,执行处理函数handler。

4.返回进程的主控制流程,alarm(sec)返回,调用pause()挂起等待

5.这时的SIGALRM信号已经处理完了,pause将一直阻塞

解释:出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。虽然alarm(sec)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(sec)之后的sec秒之内被调用。由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题而导致错误,这叫做竞态条件(Race Condition)。

解决方法:

int sigsuspend(const sigset_t *mask);   <===> pause + 信号屏蔽 ----屏蔽mask中规定的信号
函数执行期间  mask的信号屏蔽会覆盖内核的信号屏蔽,函数返回,内核的信号屏蔽恢复。如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。

重写上面的Mysleep.c

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

void handler(int s){}

int mysleep(int sec)
{
  signal(SIGALRM, handler);
  
  sigset_t set, old;
  sigemptyset(&set);
  sigemptyset(&old);
  sigaddset(&set, SIGALRM);
  sigprocmask(SIG_BLOCK, &set, NULL);
  alarm(sec);

 // pause();
  sigset_t pset;
  sigemptyset(&pset);
  sigsuspend(&pset);
  sigprocmask(SIG_UNBLOCK, &pset, NULL);
  int r = alarm(0);
  return r;
}

int main()
{
  mysleep(3);
  printf("3 sec\n");
}
1. 调用sigprocmask(SIG_BLOCK, &set, NULL);时屏蔽SIGALRM。
2. 调用sigsuspend(&pset);时解除对SIGALRM的屏蔽,然后挂起等待待。
3. SIGALRM递达后suspend返回,自动恢复原来的屏蔽字,也就是再次屏蔽SIGALRM。
4. 调用sigprocmask(SIG_UNBLOCK, &pset, NULL);时再次解除对SIGALRM的屏蔽。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值