信号及其处理


信号处理是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。失败就返回-
参数解释:

如果second > :当seconds秒后,触发SIGALRM信号
如果seconds = : 表示清除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——浅析信号处理的更多相关文章

  1. linux 多线程信号处理总结

    linux 多线程信号总结(一) 1. 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知 ...

  2. linux 下信号处理命令trap && linux下各种信号的意义

    1.用途说明 trap是一个shell内建命令,它用来在脚本中指定信号如何处理.比如,按Ctrl+C会使脚本终止执行,实际上系统发送了SIGINT信号给脚本进程,SIGINT信号的默认处理方式就是退出 ...

  3. linux 捕获信号处理中遇到的死锁

    tag: 信号 signal  sigchld  死锁 堆栈 我们的程序需要捕获信号自己处理,所以尝试对1-32的信号处理(后面33-64的信号不处理).但是在调试代码时,发现一个线程死锁的问题.程序 ...

  4. 【linux高级程序设计】(第十章)Linux异步信号处理机制 3

    信号屏蔽 信号忽略:系统仍然传递该信号,只是相应的进程不做任何处理 信号屏蔽:进程不捕获信号,信号处于未决状态,当不再屏蔽信号时可以捕获之前被屏蔽的信号. 信号集数据结构定义: typedef __s ...

  5. linux sigaction信号处理

    sigaction函数相比signal函数更为复杂,但更具灵活性,下面具体介绍她的结构和用法: #include <signal.h> int sigaction(int signum, ...

  6. linux signal 处理

    v/:* {behavior:url(#default#VML);} o/:* {behavior:url(#default#VML);} w/:* {behavior:url(#default#VM ...

  7. Linux 驱动开发

    linux驱动开发总结(一) 基础性总结 1, linux驱动一般分为3大类: * 字符设备 * 块设备 * 网络设备 2, 开发环境构建: * 交叉工具链构建 * NFS和tftp服务器安装 3, ...

  8. linux下 signal信号机制的透彻分析与各种实例讲解

    转自:http://blog.sina.com.cn/s/blog_636a55070101vs2d.html 转自:http://blog.csdn.net/tiany524/article/det ...

  9. 内核信号处理 & CPU8个通用寄存器

    内核信号处理参考: http://www.spongeliu.com/165.html 信号本质上是在软件层次上对中断机制的一种模拟(注意区分中断.异常.信号),其主要有以下几种来源: 程序错误:除零 ...

随机推荐

  1. Unity StrangeIoC HelloWorld

    unity有一个叫StrangeIoC的框架插件,这里写了一个使用StrangeIoC的HelloWorld,比他自带的demo更为简单,方便理解 1.插件下载,在Asset Store直接搜索Str ...

  2. equals()与hashCode()方法协作约定

    翻译人员: 铁锚 翻译时间: 2013年11月15日 原文链接: Java equals() and hashCode() Contract 图1 Java所有对象的超类 java.lang.Obje ...

  3. linux下数学运算器:expr命令(shell中完成数学运算)

    expr用法  expr命令一般用于整数值,但也可用于字符串.一般格式为:  expr argument operator argument  expr也是一个手工命令行计数器.  $expr 10 ...

  4. 如何让你的传输更安全——NIO模式和BIO模式实现SSL协议通信

    对于SSL/TLS协议,如果要每个开发者都自己去实现显然会带来不必要的麻烦,正是为了解决这个问题Java为广大开发者提供了Java安全套接字扩展--JSSE,它包含了实现Internet安全通信的一系 ...

  5. java垃圾回收机制,以及常用的回收算法

    记得之前去平安面试的时候,面试官问到了垃圾回收,我当时也就是说说了垃圾回收的原理,但是具体有哪些实现策略,我当时是懵的. 概念: Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定 ...

  6. C语言设计模式-封装-继承-多态

    快过年了,手头的工作慢慢也就少了,所以,研究技术的时间就多了很多时间,前些天在CSDN一博客看到有大牛在讨论C的设计模式,正好看到了,我也有兴趣转发,修改,研究一下. 记得读大学的时候,老师就告诉我们 ...

  7. iOS监听模式系列之iOS开发证书、秘钥

    补充--iOS开发证书.秘钥 iOS开发过程中如果需要进行真机调试.发布需要注册申请很多证书,对于初学者往往迷惑不解,再加上今天的文章中会牵扯到一些特殊配置,这里就简单的对iOS开发的常用证书和秘钥等 ...

  8. "《算法导论》之‘树’":AVL树

    本文关于AVL树的介绍引自博文AVL树(二)之 C++的实现,与二叉查找树相同的部分则不作介绍直接引用:代码实现是在本文的基础上自己实现且继承自上一篇博文二叉查找树. 1.AVL树的介绍 AVL树是高 ...

  9. 史上最全Android Studio快捷键 -2016-02-28

  10. Erlang cowboy 处理不规范的客户端

    Erlang cowboy 处理不规范的客户端 Cowboy 1.0 参考 本章: Dealing with broken clients 存在许多HTTP协议的实现版本.许多广泛使用的客户端,如浏览 ...