信号
这里的信号:OS提供的一种软件层面的通信机制
什么是信号
信号是一种通知进程某件事情发生了的一种通信机制
通过向进程发送某个信号,可以告诉进程发生了什么事情,进程收到这个信号后,就知道某事情发生了,进程可以做出相应的响应(处理)
信号是一种向进程发送通知,告诉其某件事情发生了的一种简单通信机制
信号也是一种
通信机制,目的就是用于告诉进程发生了什么事情
Linux所实现的信号这种通信机制,与IPC(进程间通信)所不同的是:
信号属于不精确通信,信号只能告诉进程大概发生了什么事情,但是不能准确的告诉进程详细的细节信息
信号有区别于精确IPC之外,自己的使用场合
本章所涉及到各个API就是专门与信号相关的API
有关信号的内容
1)比如ctrl+c为什么可以结束进程
2)产生指针错误时为什么进程能够自动结束,并提示产生了“segment fault”(段错误)。
3)kill命令到底是怎么结束进程的
所有的信号机制的实现,原理都是相通
- c、c++、java的线程库有类似的信号概念
- QT界面开发时有类似信号和槽的概念
信号的相关概念
信号的命名
- Linux定义了很多的信号,所有的信号都是一个整数编号
- 为了好辨识,Linux系统给这些整数编号都定义了对应的宏名
- 宏名都是以SIG开头,比如
SIGABRT - SIG:signal的缩写;ABRT:abort的缩写
- 宏名都是以SIG开头,比如
宏名SIG____,____是对信号的描述
调用abort函数时,该函数就会向进程发送一个SIGABRT信号
谁会向进程发送信号

其他进程发送信号
在命令行终端窗口通过kill命令向某个进程发送一个信号将其终止
内核发送信号
Linux内核可能会发送该事件对应的信号给某个进程
ex:
进程从管道文件读取数据,但是管道文件的读权限被关闭了,进程会被内核发送一个
SIGPIPE信号,提示读管道出错了- 指针错误,发送SIGSEGV
底层硬件发送信号
底层硬件发生了某个事件,会向进程发送对应的某个信号
但是硬件先是发送到内核,然后内核转发该硬件信号
比如:
按下ctrl+c按键终止进程时,内核收到ctrl+c按键后,会向正在运行的进程发送SIGINT信号,将其异常终止
进程收到信号的处理
分别是忽略、捕获、默认
忽略的意思就是说,进程就当信号从来没有发生过
捕获的意思就是说,进程会调用相应的
处理函数,进行相应的处理
如果不忽略也不捕获的话,此时进程会使用系统设置的
默认处理方式来处理信号
信号的种类

在命令行执行kill -l,可以显示所有的信号列表(没有32,33)
为什么信号多
//每个信号代表着某种事件,一般情况下,当进程收到某个信号时,就表示该信号所代表的事件发生了。
//可能发生的事件会有很多种,所以弄出了这么多的信号,每个信号对应着自己特有的事件。
- 35~64:这些信号是Linux后期增设的信号,这些个信号不需要关心,不用了解
- 1~34:也不是所有的信号都要掌握,我们只关心其中常用的信号
对于这些个信号,理解才是关键
常用信号

kill + PID如果杀不死进程,中间加入:-SIGKILL 或 -9
ctrl+\ 和 ctrl+c 都可以终止进程,但是\会产生core文件
如果发送的这些信号的处理方式是终止,那么进程会被终止掉
kill()
kill -l:查询信号列表
进程是否会被信号终止,是由信号的处理方式来决定的,而不是kill命令
- kill所起到的作用只是发送信号
发送信号的完整格式:kill -信号编号 PID
kill PID,默认发送的是15(SIGTERM)信号,等价于
kill -SIGTERM PID或者kill -15 PID
- 只有发送15这个信号时才能省略信号编号,发送其它信号时必须写明信号编号
注意:
命令+&是在后台,让出命令行窗口,交给shell;在终端输入fg即可切换
pkill()
用法与kill差不多,只不过kill是按照PID来识别进程的,pkill是按照名字来识别进程的
Pkill -信号编号 名字:pkill -SIGINT a.out
如果不写明信号编号的话,默认发送的是15(SIGTERM)这个信号
信号的发送与接收
一般来说,大多数发送信号的原因,都是因为内核、硬件发生了某些事件
向某个进程发送该事件专用的信号,告诉该进程这个事件发生了
不过对于我们自己写的进程来说,其实更多是接收信号,而不是发送信号
自己发送信号(非内核和硬件)的情况
- ctrl+C、ctrl+\发送信号,终止正在运行进程
- 命令行执行kill命令发送信号,终止进程
- 因为某些特殊需求,在我们自己的程序里面,需要调用函数向另一个进程发送某个信号
比如A进程和B进程需要协同工作,A进程将相应事件准备好以后,可能需要发送一个信号给B进程,通知B进程,B进程收到信号后,就知道该事件已经准备好,可以配合A进程做事了。
--------
自己进程调用kill函数发送信号时,最好发送的是SIGUSR1、SIGUSR2这两个自定义信号
发送专用事件的信号的话,很可能造成误会
接收信号
在一般情况下,进程并不会去重新设置信号的处理方式,而是使用信号的默认处理方式来处理信号
如果需要重新设置信号的处理方式,可以调用相应API来重新设置信号的处理方式。
core文件
同样终止进程-----ctrl+c不产生core文件,ctrl+\产生core文件
- core dumped
什么是core文件
用于保存程序(进程)在当前结束的这一刻,进程在内存中的代码和数据
**作用:**core文件可以用于分析进程在结束时的状况
由于进程代码和数据都是二进制的,需要特殊软件翻译后才能看懂
1.只有某个些信号在终止进程时才会产生core文件
2.一般情况下并不会创建这个文件,因为系统默认将产生core的设置给关闭了,只有打开后这个设置后才会保存core文件。
所以当你看到提示core dumped,这就表示这个信号终止进程时,会产生core文件,只不过由于关闭了设置,因此core文件被丢弃了,dumped就是丢弃的意思。
signal()
#include <signal.h>
//功能:设置某个信号的处理方式
//处理方式可以被设置为忽略,捕获,默认
typedef void (*sighandler_t)(int);
/*
成功:返回上一次的处理方式
失败:返回SIG_ERR宏值,并且设置errno
*/
//signum:信号编号
//handler:信号处理方式
//sighandler_t handler也有直接写成void (*handler)(int)
sighandler_t signal(int signum, sighandler_t handler);
/*
(a)忽略:SIG_IGN
(b)默认:SIG_DFL
(c)捕获:填写类型为void (*)(int)的捕获函数的地址
*/
进程的进程表(task_struct)中会有一个“信号处理方式登记表”
专门用于记录信号的处理方式
- 调用signal函数设置某个信号的处理方式时,会将信号的处理方式登记到该表中
每个进程拥有
独立的task_struct结构体变量,因而每个进程的“信号处理方式登记表”都是独立的,所以每个进程对信号的处理方式自然也是独立的,互不干扰
捕获函数也被称为信号处理函数
void signal_fun1(int signo)
{
...
}
void signal_fun2(int signo)
{
...
}
int main(void)
{
signal(SIGINT, signal_fun1);
signal(SIGSEGV, signal_fun2);
return 0;
}

#include<signal.h>
int main(){
//SIGINT 2 ctrl+C终止进程
//采用:SIG_INT后,该方式就不起作用了
signal(SIGINT,SIG_IGN);
//再次还原成默认就起作用了
signal(SIGINT,SIG_DFL);
while(1);
return 0;
}
把ctrl+\的信号也设置成忽略
此时:只能在终端通过ps -al查看pid,通过kill杀死,或pkill+文件名
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void signal_fun1(int signo){
printf("SIGINT singo=%d\n",signo);
}
void signal_fun2(int signo){
printf("SIGQUIT singo=%d\n",signo);
}
int main(){
signal(SIGINT,signal_fun1);
signal(SIGQUIT,SIG_IGN);
signal(SIGQUIT,signal_fun2);
while(1);
return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ ./main
^CSIGINT singo=2
^\SIGQUIT singo=3
^CSIGINT singo=2
^\SIGQUIT singo=3
已终止----------pkill main
signal()返回值
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
typedef void (*sighandler_t)(int);
void signal_fun1(int signo){
printf("SIGQUIT singo=%d\n",signo);
}
int main(){
//sighandler_t handler也有直接写成void (*handler)(int)
//sighandler_t signal(int signum, sighandler_t handler);
sighandler_t ret=NULL;
ret=signal(SIGINT,SIG_IGN);
ret=signal(SIGINT,SIG_DFL);
ret=signal(SIGINT,signal_fun1);
ret=signal(SIGINT,SIG_DFL);
if(ret==SIG_ERR){
perror("signal fails!");
}
else if(ret==SIG_IGN){
printf("ignore!\n");
}
else if(ret==SIG_DFL){
printf("default!\n");
}
else{
printf("capture=%p\n",ret);
}
while(1);
return 0;
}
/*每一次返回值是上一次的结果*/
调用捕获函数的过程
- 当信号没有发生时,进程正常运行
- 当信号没有发生时,进程正常运行
- 一看信号的处理方式是捕获,就会从“信号处理方式登记表”中将捕获函数的地址取出并执行捕获函数
- 捕获函数执行完毕后,恢复进程的正常运行
当信号来时,如果当前有一条指令正在运行,会先等这条指令运行执行完毕后再去调用信号处理函数
如果信号处理函数有提前执行return的话,会提前返回到主线
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
typedef void (*sighandler_t)(int);
void signal_fun1(int signo){
printf("SIGQUIT singo=%d\n",signo);
exit(1);//_exit()
}
void process_exit_deal(void){
printf("save data in list!\n");
}
int main(){
atexit(process_exit_deal);
signal(SIGINT,signal_fun1);
while(1);
return 0;
}
/*
^CSIGQUIT singo=2
save data in list!
*/
//exit很重要,会在exit前执行进程处理函数:atexit
信号被设置为SIG_IGN(忽略)
进程将不会再接收到这个信号,这信号对进城没有任何影响设置为捕获时,需要将handler设置为捕获函数的地址,类型为void (*)(int)
为了确保和捕获函数的类型统一:
SIG_DFL、SIG_IGN和SIG_ERR宏的类型也必须是void (*)(int)
这几个宏定义在了<signal.h>头文件中
#define SIG_DFL ((void (*)(int))0) #define SIG_IGN ((void (*)(int))1) #define SIG_ERR ((void (*)(int))-1)#include<stdio.h> #include<stdlib.h> #include<signal.h> int main(){ printf("SIG_ERR=%d\n",SIG_ERR); printf("SIG_IGN=%d\n",SIG_IGN); printf("SIG_DFL=%d\n",SIG_DFL); while(1); return 0; } /*有警告 guojiawei@ubantu-gjw:~/Desktop/signal$ ./a.out SIG_ERR=-1 SIG_IGN=1 SIG_DFL=0 */除了
SIGKILL这个信号外,其它所有的信号都可被忽略和捕获
Linux规定SIGKILL这个信号一定不能被忽略和捕获

