信号及其处理
信号处理是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; }
信号阻塞
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从未决状态恢复,到达抵达状态
特别提醒的是如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。