• Linux——浅析信号处理


     信号及其处理


    信号处理是Unix和LInux系统为了响应某些状况而产生的事件,通常内核产生信号,进程收到信号后采取相应的动作

    例如当我们想强制结束一个程序的时候,我们通常会给它发送一个信号,然后该进程会捕捉到信号,紧接着该进程执行一定操作后最终被终止掉。不仅仅如此,通常下面几种情况

      键盘事件(ctrl+c、ctrl+

      ②软件条件(alarm定时器超时)

      ③硬件故障(如算术运算执行除以0操作

      ④终端产生信号  (如进程调用kill(2)函数将信号发送给进程或进程组,或用户执行kill(1)命令向其它进程发送信号

    都会有信号的产生,而对这些产生的信号是需要让进程来处理的,进而信号也被作为进程间通信或修改行为的一种方式,是可明确地由一个进程发送给另一个进程的。一般当一个信号的产生时,我们把它叫作信号生,对一个信号接收到叫信号捕获。关于信号的捕获例子是比较多的,这里列举平时可能经常遇到的几个,其它可自行查询(~v~虽然比较多)

     

    然后来认识一下这些信号,可用  kill -l查看

    平常最常用的信号

    其它信号作用大致如下(前面数字代表信号编号),

     2.ctrl + c 进程终止信号 中断方式终止掉进程
     3. ctrl +   退出信号,发送 SIGQUIT 信号给前台进程组中的所有进程,终止前台进程并生成 core 文件
     6.异常退出信号    像abort退出等
     7.总线与进程虚拟地址空间未成功连接信号
     8.浮点数异常错误信号
     9.终止进程信号,与kill命令一起,可用来强行杀死进程 如kill-SIGKILL pid(注意,它不可被捕获)
     11 段错误信号
     13.管道破裂信号
     14 闹钟信号
     17 子进程返回给父进程的信号
     19 进程暂停信号 但是它不可以被捕获(和9号信号一样,比较特殊)
     20 发送 SIGTSTP 信号给前台进程组中的所有进程,常用于挂起一个进程。相当于ctrl+z
     23 处理紧急数据信号, 某些数据较为紧急,可使其优先传输。
     29 异步IO信号
     32~33号用作多线程使用,不让用户使用; 编号34之后的信号,是没有限制的,可让我们自己开发使用
    

    针对上面这么多信号,它们都是异步事件的经典实例。同时对进程而言,那些产生信号的事件可能随机产生的。进程不能简单地去测试一个变量(如errno)来判定是否发生了一个信号,而是用一下几种处理方式告诉内核此时应当去执行什么操作:

    可通过 man 7 signal命令查看处理的方式,通常进程对收到的信号的处理有以下3种方式

       默认处理方式

    对大多数信号而言,系统默认动作是终止进程

      ② 忽略

    对到来的信号,不做出反应  但SIGKILL  SIGSTOP不能被忽略。因为它们向内核和超级用户提供了使进程终止和停止的可靠方法。另外就是对一些诸如非法内存引用、除以0等由硬件异常产生的信号进行忽略,那结果则是未定义的)   

      ③ 捕获并处理 

    对到来的信号,通知内核,然后去调用执行我们自己写的用户函数 但是注意SIGKILL 和SIGTOP不能捕获

    好,接下来便看看信号处理的一些具体的例子

    对信号的操作


    (1)注册信号

     注册信号实际是对信号进行三种处理操作,用于告诉当前进程对接收到信号后该去执行什么动作

    具体用signal函数来进行操作,它原型如下

    头文件:#include <signal.h>
    
       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);
    
        //void(* signal(int signum, void (*handler)(int)))(int)

    功能:开始获取信号值为signum的信号,如果获取到该信号,则开始执行handler指向的函数(典型回调函数)
    返回值: 调用成功返回原本的信号处理函数指针,失败返回 SIGERR
    SIGERR的宏为 #define SIG_IGN ((sighandler_t)-1)

    参数:

       signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。

       sighandler_t:描述了与信号关联的动作,它可以取以下三种值,如下表:

    注:上面这几个信号可传入作signal函数第二个参数,因为它们虽是整型但进行了强转。

    这里可以来个例子看看

    //比如验证SIG_IGN信号,这样ctrl c就起不了作用了
     
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     #include <signal.h>
     
     int main( void)
     {
         signal( SIGINT, SIG_IGN);
     
         for(int i =0; i< 20; ++i)
         {
             printf( "玩不死我!
    ");
             sleep(1);
         }
         return 0;
     }
    

    结果如下:

    再看另一个

    //验证自定义信号,这样ctrl c 就会去执行 handler 函数
    
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     #include <signal.h>
     
     void handler(int s)
     { 
         printf( "呃,我被搞死了。SIGNAL =%d
    ", s);
         exit(1);
     }   
     int main( void)
     { 
         __sighandler_t ret;
         ret = signal( SIGINT, handler);
         
         for(int i =0; i< 20; ++i)
         { 
             printf( "玩不死我!
    ");
             sleep(1);
         }   
         return 0;
     } 
    

    结果验证:

     

    上面是几个简单的信号处理,其实信号是异步实现的,当信号到达时,系统会保存当前进程的运行环境,转去执行信号处理函数,当信号处理函数执行完毕,然后再恢复现场,继续往后执行(就好像中断处理一样)。

    (2)给进程发送信号

    前面的提到的signal函数对收到的信号进行了处理,同时我们也可主动给进程发送信号。具体可以有两种方式,第一个就是用shell命令的方式

      kill -信号值  pid

    一般可以用jobs  去查看有哪些后台进程;而若要将执行程序以后台方式运行,则可在后面加上 &符号(.如/a.out &)。时,如果当你再用ctrl c想要将该进程终止时 已经无法成功了,因为ctrl + c只能发给前台进程,结束的是前台进程。 这个时候可以用fg +%numid将后台进程调到前台进程(注意:numid是作业号,不是进程pid),这样便可使用ctrl c了。同时,如果想要将前台执行进程转去后台暂停掉可使用 Ctrl + Z命令

    好,然后第二种给进程发送信号还可以通过函数的方式

    原型:int kill(int pid, int signum)

    功能:用该函数给进程id为pid的进程发送一个信号值为signum的信号
    返回值
    成功返回0,失败返回-1
    参数解释
    signum:信号值,即信号编号
    pid:进程id,它可以取以下四种值,如下表: 

    顺道提一下进程组

    进程组中通常有若干个进程。 它们可以是用管道连接的进程,可以是fork创建出来的父子进程;这些都属于同一个进程组

    来例子继续应用一下~

     /*************************************************************************
       > File Name: 3.c
       > Author: tp
       > Mail: 
       > Created Time: Tue 08 May 2018 08:55:26 PM CST
      ************************************************************************/
     
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     #include <signal.h>
     void handler( int s) //自定义函数,验证能否接收信号 
     {   
         printf( "信号收到!recv_SIG=%d
    ", s);
     }
     int main( void)
     {
         signal(SIGUSR1, handler);
         pid_t pid = fork( );
         if( pid == 0)
         {
             sleep(1);
             kill(getppid(), SIGUSR1); //给父进程发送一个自定义信号SIGUSR1,该信号常用于接收发信号
             exit( 0);
         }
         else
         {
             int i =0;
             while( 1)
             {
                 printf( "%d 我执行子进程
    ", i++);
                 sleep(1);  //返回>0 表示还剩多少时间允许其被信号打断
             }
         }
         return 0;
     } 
    

    除此之外还有两个函数可以发送信号,可以了解一下

    1.int rasie (int signum);  //给自己发送信号
    返回值:成功返回0;失败:返回-1
    
    2.int killpig(int gid,int signum); // 给进程组发送信号
    返回值:-1,并把error值设为EINTR

    同时我们还可以暂停进程,直到进程被信号打断

    int pause(void)函数  值得注意的是它暂停时,会让出cpu,不像while(1)循环

    信号的分类

    前面,我们列举了这么多信号,它们大致可分为①可靠信号 ②不可靠信号 ③实时信号 ④非实时信号,这样4种信号

    不可靠信号:编号为1~31 的信号都是不可靠的信号。由于linux的信号继承自早期的UNIX 信号,所以这些不可靠信号也或多或少也继承了UNIX信号的缺陷即,

      * 信号处理函数完毕,信号会恢复成按默认处理方式处理(不过现在liunx已经将其改进)

      * 会出现信号丢失的现象,原因就是此类信号不排队,并且此种情况暂时还没办法解决

    可靠信号34 - 64号信号为可靠的信号。 它不会出现信号丢失,支持排队,信号处理函数执行完毕,不会恢复成缺省的处理方式

    实时信号:就是可靠信号(字面意义上感觉实时信号好像要比非实时信号要快,其实不然)

    非实时信号:其实就是不可靠信号

    (3)SIGALRM信号

    这个SIGALRM信号(编号14)在平常的应用中比较广泛。常常用alarm函数来发出SIGALRM信号,来用作报警处理,这个信号也是一个很有用的信号,用它可以来实现一些比较有意思的东西。当然要使用它,还得先来看看这个alarm函数,它原型是

    功能:
        当规定的seconds时间到了,给当前进程发送一个SIGALRM信号
    返回值:
        成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。失败就返回-1
    参数解释:
     如果second > 0:当seconds秒后,触发SIGALRM信号
     如果seconds = 0: 表示清除SIGALRM信号

    稍稍应用一下

     /*************************************************************************
       > File Name: alarm.c
       > Author: tp
       > Mail: 
       > Created Time: Tue 08 May 2018 09:15:53 PM CST
      ************************************************************************/
     
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     #include <signal.h>
     
     void handler( int s)
     {
         printf( "
    很可惜,时间到了!
    ");
         exit( 1);
     }
     int main( void)
     {
         char buff[ 100]={ };
         printf( "输入字符串:");
     
         signal(SIGALRM, handler); //收到SIGALRM信号时,执行handler函数
         alarm( 3);   //设置3秒的警报时间,时间一到便发出SIGALRM信号
         scanf("%s", buff);
         alarm(0);  //清除闹钟
         printf( "收到:%s
    ", buff);
         while( 1)
         {
             printf( "6 ");
             fflush( stdout);
             sleep( 1);
         }
         return 0;
     }
    

    信号阻塞


    实际执行信号的处理动作称为信号抵达(delivery),信号从产生到抵达之间的状态,称为信号未决(pend)。进程可以选择阻塞(block)某个信号, 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞后才执行后续的抵达动作。 信号阻塞和上面所述的信号忽略是不相同的。信号只要被阻塞时,它就不会抵达;而信号忽略则是在抵达之后可选的一种处理动作。
     
    同时,每个信号都有两个标志比特位来分别表示阻塞(blocking) 和 未决(pending)。还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号抵达才消除该标志.
    如上图的SIGHUP信号未阻塞也未产生过,当它抵达时执行默认处理动作。 而SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前是不能忽略这个信号的,因为进程仍有机会在改变处理动作之后再解除阻塞,然后接着来处理。SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
     
    如果在进程在阻塞某信号时,该信号产生过多次,Liunx这样实现的:常规信号在抵达之前产生多次只计一次,而实时信号在递达之前 产生多个信号可以依次放在一个队列里。 每个信号只有一个bit的未决标志,非0既1,这个地方不记录该信号产生了多少次。同样,未决标志也是这样表示的。因此呢,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t为信号集,这个类型可以表示每个信号的"有效"或"无效“状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中类似。 阻塞信号集也叫做当前进程的信号屏蔽字.
     
    主要的信号集操作函数
    sigset_t类型对于每种信号用一个bit表示 "有效"或者"无效" 
    头文件:#include<signal.h>
    
    ①int sigemptyset(sigset_t *set);
    //初始化set所指向的信号集,使其中所有信号的对应的bit清零,表示该信号集不包含任何有效信号.
    
    ②int sigfillset(sigset_t *set);
    //初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号机的有效信号包括系统支持的所有信号.
    
    ③int sigaddset(sigset_t *set,int signo);
    //在该信号集中添加某种有效信号.
    
    ④int sigdelset(sigset_t *set,int signo);
    //在该信号集中删除某种有效信号
    
    ⑤int sigismemeber(const sigset_t *set,int signo);
    //是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含贼返回1,不包含则返回0,出错返回-1
    
    ⑥int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
    //读取或更改进程的信号屏蔽字(阻塞信号集) 如果成功返回0 失败返回-1
    
    ⑦int sigpending(sigset_t *set);
    //读取当前进程的处于阻塞且未决的信号集,通过set参数传出,调用成功则返回0,出错则返回-1.
    

    一个比较经典的例子,应用一下:

     /*************************************************************************
       > File Name: set.c
       > Author: tp
       > Mail: 
       > Created Time: Thu 10 May 2018 05:38:50 PM CST
      ************************************************************************/
     
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     #include <signal.h>
     
     void printsigset(sigset_t* set)
     {
         int i = 1;
         for(  ; i<= 64; i++) {
             if(sigismember( set,i))//判定指定信号是否在目标集合中  
                 putchar( '1');
             else
                 putchar( '0');
         }
         printf( "
    ");
     }
     
     int main( )
     {
         sigset_t s ,p;  //定义信号集对象s,p, s用作初始化的参数 
         sigemptyset(&s) ; //清空进行初始化 
         sigaddset(&s, SIGINT);
         sigprocmask(SIG_BLOCK, &s, NULL); //设置阻塞信号集  
         while( 1)
         {
             sigpending( &p); //获取未决信号集 
             printsigset(&p);
             sleep(1) ;
         }
         return 0;
     }
    

     这个程序的大概意思就是我们阻塞一个信号集,让它一直处于未决状态,并把它里面的信号编号显示出来,比如中途我们加入了一个ctrl+c, 后面信号集里面就会出现这个信号,然后它们还是一直处于未决状态。

     

    与此同时,ctrl c 同样也就无法去终止该程序,这时候我们就可以再用信号处理函数handler_quit来进行解除信号阻塞,其内部sigemptyset函数来对阻塞信号集作清空处理;具体可SIGQUIT信号来进行发起handler_quit调用。

     /*************************************************************************
       > File Name: set.c
       > Author: tp
       > Mail: 
       > Created Time: Thu 10 May 2018 05:38:50 PM CST
      ************************************************************************/
     
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     #include <signal.h>
     
     void printsigset(sigset_t* set)
     {
         int i = 1;
         for(  ; i<= 64; i++) {
             if(sigismember( set,i))//判定指定信号是否在目标集合中  
                 putchar( '1');
             else
                 putchar( '0');
         }
         printf( "
    ");
     }
     
     void handler_quit( int s)
     {
         sigset_t set;
         sigemptyset( &set);
         sigaddset( &set, SIGINT);
         sigprocmask(SIG_UNBLOCK, &set, NULL);
     }
     int main( )
     {
         signal(SIGQUIT, handler_quit);
         sigset_t s ,p;  //定义信号集对象s,p, s用作初始化的参数 
         sigemptyset(&s) ; //清空进行初始化 
         sigaddset(&s, SIGINT);
         sigprocmask(SIG_BLOCK, &s, NULL); //设置阻塞信号集  
         while( 1)
         {
             sigpending( &p); //获取未决信号集 
             printsigset(&p);
             sleep(1) ;
         }
         return 0;
     }
    

     这样,用ctrl + 便可使SIGINT从未决状态恢复,到达抵达状态

    特别提醒的是如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。

  • 相关阅读:
    使用环信WebIm实现一个客服功能
    html中的下拉框—select和input方式
    [LeetCode] 206. Reverse Linked List
    visual studio 2019安装配置可编写c/c++语言的IDE环境
    JS判断数据类型是不是undefined
    idea微服务架构出现 Run Dashboard 按钮方法
    docker 常用命令
    配置docker镜像加速
    linux安装docker
    linux安装redis
  • 原文地址:https://www.cnblogs.com/tp-16b/p/9011104.html
Copyright © 2020-2023  润新知