LINUX 信号处理

来源:互联网 发布:喜欢的工作环境知乎 编辑:程序博客网 时间:2024/06/14 20:42

LINUX 信号处理


1、信号的基本概念

     
     信号是进程在运行过程中, 由自身产生或由进程外部发过来的消息(事件)。信号是硬件中断的软件模拟(软中断)。每个信号用一个整型常量宏表示, 以 SIG 开头, 比如 SIGCHLD、 SIGINT 等,它们在系统头文件<signal.h>中定义,也可以通过在 shell 下键入kill –l 查看信号列表,或者键入 man 7 signal 查看详细说明。


信号的生成来自内核, 让内核生成信号的请求来自 3 个地方:
  • 用户:用户能够通过输入 CTRL+c、 Ctrl+\, 或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
  • 内核:当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规)、 浮点数溢出等;
  • 进程:一个进程可以通过系统调用 kill 给另一个进程发送信号, 一个进程可以通过信号和另外一个进程进行通信。

由进程的某个操作产生的信号称为同步信号(synchronous signals),例如除 0;由像用户击键这样的进程外部事件产生的信号叫做异步信(asynchronous signals)。

进程接收到信号以后, 可以有如下 3 种选择进行处理:
  • 接收默认处理: 接收默认处理的进程通常会导致进程本身消亡。 例如连接到终端的进程, 用户按下 CTRL+c, 将导致内核向进程发送一个 SIGINT 的信号, 进程如果不对该信号做特殊的处理, 系统将采用默认的方式处理该信号, 即终止进程的执行; signal(SIGINT,SIG_DFL);
  • 忽略信号:进程可以通过代码,显示地忽略某个信号的处理,例如: signal(SIGINT,SIG_IGN);但是某些信号是不能被忽略的,例如 9 号信号;
  • 捕捉信号并处理: 进程可以事先注册信号处理函数, 当接收到信号时, 由信号处理函数自动捕捉并且处理信号。

有两个信号既不能被忽略也不能被捕捉,它们是SIGKILL和SIGSTOP。即进程接收到这两个信号后,只能接受系统的默认处理,即终止进程。SIGSTOP 是暂停进程。

2、signal 信号处理机制


函数signal注册一个信号捕捉函数。
原型为:
#include <signal.h>
typedef void (*sighandler_t)(int); //函数指针
sighandler_t signal(int signum,sighandler_t handler);

signal的第1个参数signum表示要捕捉的信号,第2个参数是个函数指针,表示要对该信号进行捕捉的函数,该参数也可以是SIG_DFL(表示交由系统缺省处理,相当于白注册了)或SIG_IGN(表示忽略掉该信号而不做任何处理)。
返回值:signal如果调用成功,返回以前该信号的处理函数的地址,否则返回 SIG_ERR。

sighandler_t是信号捕捉函数,由signal函数注册,注册以后,在整个进程运行过程中均有效,并且对不同的信号可以注册同一个信号捕捉函数。该函数只有一个整型参数,表示信号值。

eg:捕捉终端 CTRL+c 产生的 SIGINT 信号
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
void SignHandler(int iSignNo)
{
     printf("Capture sign no:%d\n",iSignNo);
}
int  main()
{
     signal(SIGINT,SignHandler);
     while(1)
          sleep(1);
     return 0;
}

该程序可以通过 Ctrl+\终止, 因为组合键 Ctrl+\能够产生 SIGQUIT 信号,而该信号的捕捉函数尚未在程序中注册。

3、sigaction 信号处理机制

     
在 signal 处理机制下, 还有许多特殊情况需要考虑:
1、注册一个信号处理函数,并且处理完毕一个信号之后,是否需要重新注册,才能够捕捉下一个信号;(不需要)
2、如果信号处理函数正在处理信号, 并且还没有处理完毕时,又发生了一个同类型的信号,这时该怎么处理;(挨着执行),后续相同信号忽略(会多执行一次)。
3、如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个不同类型的信号,这时该怎么处理;(跳转去执行另一个信号,之后再执行剩下的没有处理完的信号)
4、如果程序阻塞在一个系统调用(如 read(...))时,发生了一个信号,这时是让系统调用返回错误再接着进入信号处理函数,还是先跳转到信号处理函数,等信号处理完毕后,系统调用再返回。--->  由下面具体说明: 

总结:信号都是平等的无优先级。但是对于singal接口来说最多执行2次、打断。默认不能打断当前相同信号,可以被其他信号打断。另外一个信号会立即唤醒正在睡眠的进程。

eg:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_iSeq = 0;
void SignHandler(int iSignNo)
{
    int iSeq = g_iSeq++;
    printf("%d Enter SignHandler,signo:%d\n",iSeq,iSignNo);
    sleep(3);
    printf("%d Leave SignHandler,signo:%d\n",iSeq,iSignNo);
}
int main()
{
     char szBuf[8];
     int iRet;
         //不同的信号调用同一个处理函数
     signal(SIGINT,SignHandler); 
     signal(SIGQUIT,SignHandler);
     do{
          iRet = read(STDIN_FILENO,szBuf,sizeof(szBuf)-1);
          if(iRet < 0){
               perror("read fail.");
               break;
          }
          szBuf[iRet] = 0;
          printf("Get: %s",szBuf);
     }while(strcmp(szBuf,"quit\n") != 0);
     return 0;
}
程序运行时,针对于如下几种输入情况,看输出结果:
1、 [CTRL+c] [CTRL+c] (一个一个挨着执行)
2、 [CTRL+c] [CTRL+\] (先执行 c 的进入, 被\打断, 转而执行\,          
     最后执行c的退出)
3、 hello [CTRL+\] [Enter] (先执行中断, 没有任何输出)
4、 [CTRL+\] hello [Enter] (先执行中断, 输出内容)
5、 hel [CTRL+\] lo[Enter] (先执行中断, 只输出 lo)

4、sigaction 信号处理注册

     
函数原型:
     #include <signal.h>
     int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. sigaction 也用于注册一个信号处理函数
  2. 参数 signum 为需要捕捉的信号
  3. 参数 act 是一个结构体, 里面包含信号处理函数地址、 处理方式等信息
  4. 参数 oldact 是一个传出参数, sigaction 函数调用成功后, oldact 里面包含以前对 signum 的处理方式的信息,通常为 NULL,如果函数调用成功,将返回 0,否则返回-1

1、结构体 struct sigaction的原型为:

struct sigaction {
     void (*sa_handler)(int); //老类型的信号处理函数指针
     void (*sa_sigaction)(int, siginfo_t *,void *);//新类型的信号处理函数指针
     sigset_t sa_mask;         //将要被阻塞的信号集合
     int sa_flags;             //信号处理方式掩码(SA_SIGINFO)
     void(*sa_restorer)(void);        //保留, 不要使
};

1、字段sa_handler是一个函数指针,用于指向原型为void handler(int)的信号处理函数地址,即老类型 的信号处理函数(如果用这个再将 sa_flags = 0,就等同于 signal()函数)

2、字段 sa_sigaction 也是一个函数指针,用于指向原型为:void handler(int iSignNum, siginfo_t *pSignInfo, void *pReserved);的信号处理函数,即新类型的信号处理函数
  • iSignNum: 传入的信号
  • pSignInfo: 与该信号相关的一些信息,它是个结构体
  • pReserved: 保留,现没用,通常为NULL

3、字段sa_handler和sa_sigaction只应该有一个生效,如果想采用老的信号处理机制,就应该让sa_handler指向正确的信号处理函数,并且让字段sa_flags为0;否则应该让sa_sigaction指向正确的信号处理函数,并且让字段sa_flags包含SA_SIGINFO选项。

4、字段sa_mask是一个包含信号集合的结构体,该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。
  • 针对sigset_t 结构体,有一组专门的函数对它进行处理,它们是:#include <signal.h>
    • int sigemptyset(sigset_t *set);   //清空信号集合 set
    • int sigfillset(sigset_t *set);                           //将所有信号填充进set中
    • int sigaddset(sigset_t *set, int signum);            //往set中添加信号signum
    • int sigdelset(sigset_t *set, int signum);          //从 set 中移除信号 signum
    • int sigismember(const sigset_t *set, int signum); //判断signum是否包含在set中(是:返回1,否:0)
    • int sigpending(sigset_t *set); //将被阻塞的信号集合由参数set指针返回(挂起信号)
  • 其中,对于函数sigismember而言,如果signum在set集中,则返回 1;不在,则返回0;出错时返回-1.其他的函数都是成功返回0,失败返回-1。

eg: 如果打算在处理信号SIGINT时,只阻塞对SIGQUIT信号的处理,可以用如下方法:
     struct sigaction act;
     act.sa_flags = SA_SIGINFO;
     act.sa_sigaction = newHandler;
     sigemptyset(&act.sa_mask);
     sigaddset(&act.sa_mask, SIGQUIT);
     sigaction(SIGINT,&act,NULL);

5、字段 sa_flags是一组掩码的合成值,指示信号处理时所应该采取的一些行为,各掩码的含义为:


                                    

eg:用sigaction 实现和signal(只能传递一个参数)一样的功能。

#include<signal.h>
#include<stdio.h>
void handle(int signo)
{
     printf("signo: %d\n",signo);
}
main()
{
     struct sigaction st;
     st.sa_handler = handle;
     st.sa_flags = 0;
     sigaction(SIGINT,&st,NULL);
     while(1)
     {
          sleep(1);
     }
}

2、用sigaction 实现调用新的信号处理函数

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_iSeq = 0;
void SignHandlerNew(int iSignNo,siginfo_t *pInfo,void *pReserved)
{
     int iSeq = g_iSeq++;
     printf("%d Enter               
     SignHandlerNew,signo:%d\n",iSeq,iSignNo);
     sleep(3);
 printf("%d Leave SignHandlerNew,signo:%d\n",iSeq,iSignNo);
}
int main()
{
     struct sigaction act;
     act.sa_sigaction = SignHandlerNew;
     act.sa_flags = SA_SIGINFO;
     sigaction(SIGINT,&act,NULL);
     sigaction(SIGQUIT,&act,NULL);
     while(1)
     {
          sleep(1);
     }
     return 0;
}

5、sigprocmask 信号阻塞


函数sigaction中设置的被阻塞信号集合只是针对于要处理的信号,例如
     struct sigaction act;
     sigemptyset(&act.sa_mask);
     sigaddset(&act.sa_mask,SIGQUIT);
     sigaction(SIGINT,&act,NULL);
表示只有在处理信号SIGINT时,才阻塞信号 SIGQUIT;(重点区分)
    
函数sigprocmask 是全程阻塞,在 sigprocmask 中设置了阻塞集合后, 被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号集合。

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

参数 how 的值为如下3者之一:
a: SIG_BLOCK ,将参数 2 的信号集合添加到进程原有的阻塞信号集合中
b: SIG_UNBLOCK ,从进程原有的阻塞信号集合移除参数2中包含的信号
c: SIG_SETMASK, 重新设置进程的阻塞信号集为参数 2 的信号集
参数 set 为阻塞信号集
参数 oldset 是传出参数, 存放进程原有的信号集, 通常为 NULL

eg:添加全程阻塞
     //屏蔽掉 SIGQUIT 信号
     sigset_t sigSet;
     sigemptyset(&sigSet);
     sigaddset(&sigSet,SIGQUIT);
     sigprocmask(SIG_BLOCK,&sigSet,NULL);
    
     struct sigaction act;
     act.sa_sigaction=SignHandlerNew;
     act.sa_flags=SA_SIGINFO;
     sigemptyset(&act.sa_mask);
     sigaction(SIGINT,&act,NULL);

6、用程序发送信号  ---> kill 信号发送函数


原型为:
     #include <sys/types.h>
     #include <signal.h>
     int kill(pid_t pid, int sig);
参数 pid 为将要接受信号的进程的 pid,可以通过 getpid()函数获得来给自身发送信号,还可以发送信号给指定的进程,此时 pid 有如下描述:
    
     pid > 0 将信号发给 ID 为 pid 的进程
     pid == 0 将信号发送给与发送进程属于同一个进程组的所有进程
     pid < 0 将信号发送给进程组 ID 等于 pid 绝对值的所有进程
     pid == -1 将信号发送给该进程有权限发送的系统里的所有进程

参数sig 为要发送的信号
如果成功,返回 0,否则为-1

示例,输入结束后,将通过发送信号 SIGQUIT 把自己杀掉:
     #include <stdio.h>
     #include <signal.h>
     #include <unistd.h>
     #include <sys/types.h>
     int main()
     {
          while(1){
          if(getchar()==EOF) //运行之后输入没有反应,当按下 Ctrl+d(EOF),进程关闭 kill(getpid(),SIGQUIT);
     }
     return 0;
}

7、计时器与信号  --->  睡眠函数


1、Linux 下有两个睡眠函数, 原型为:
    
     #include <unistd.h>
     unsigned int sleep(unsigned int seconds);
     void usleep(unsigned long usec);

函数 sleep让进程睡眠 seconds, 函数 usleep让进程睡眠 usec微秒
sleep 睡眠函数内部是用信号机制进行处理的, 用到的函数有:
    
     #include <unistd.h>
     unsigned int alarm(unsigned int seconds);
     //告知自身进程, 要进程在 seconds 秒后自动产生一个 SIGALRM 的信号
     int pause(void); //将自身进程挂起, 直到有信号发生时才从 pause 返回

2、 Linux 为每个进程维护 3 个计时器,
      分别是 真实计时器、  虚拟计时器和 实用计时器。

1、真实计时器计算的是程序运行的实际时间; ---直接

2、虚拟计时器计算的是程序运行在用户态时所消耗的时间(可认为是实际时间    
     减掉(系统调用和程序睡眠所消耗)的时间); ---需要了解内核

3、实用计时器计算的是程序处于用户态和处于内核态所消耗的时间之和。 ---
     常用

#include <sys/time.h>

1. int getitimer(int which, struct itimerval *value); //获取计时器的设置

参数 which 指定哪个计时器,可选项为 ITIMER_REAL(真实计时器)、 ITIMER_VIRTUAL(虚拟计时器、 ITIMER_PROF(实用计时器))

参数 value 为一结构体的传出参数, 用于传出该计时器的初始间隔时间和重复间隔时间
返回值: 如果成功, 返回 0, 否则-1

2.int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue); //设置计时器

参数 which 指定哪个计时器, 可选项为 ITIMER_REAL(真实计时器)、 ITIMER_VIRTUAL(虚拟计时器、 ITIMER_PROF(实用计时器))

参数 value 为一结构体的传入参数, 指定该计时器的初始间隔时间和重复间隔时间

参数 ovalue 为一结构体传出参数, 用于传出以前的计时器时间设置。
原创粉丝点击