Linux——浅析信号处理

Posted tp_16b

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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( "玩不死我!\\n");
         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\\n", s);
     exit(1);
 }   
 int main( void)
 { 
     __sighandler_t ret;
     ret = signal( SIGINT, handler);
     
     for(int i =0; i< 20; ++i)
     { 
         printf( "玩不死我!\\n");
         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\\n", 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 我执行子进程\\n", 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( "\\n很可惜,时间到了!\\n");
     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\\n", 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( "\\n");
 }
 
 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( "\\n");
 }
 
 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从未决状态恢复,到达抵达状态

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

 

以上是关于Linux——浅析信号处理的主要内容,如果未能解决你的问题,请参考以下文章

Linux内存屏障浅析

Linux内存屏障浅析

嵌入式Linux—浅析Makefile

浅析 Python 模块

浅析普通函数与构造函数

Linux 2.6内核Makefile浅析