生活中有许许多多的信号,比如闹钟,红绿灯,信号枪和鸡叫声等等,当我们接收到这些信号后,我们会立即做出反应,闹钟响了就要起床,红灯停绿灯行,也就是说在接受这些信号之前,我们已经知道收到信号后所对应的措施。同样的,进程具有识别信号并处理信号的能力,远远早于信号的产生,也就是进程在没有收到信号前,就已经知道什么信号对应什么处理动作,这在编写操作系统源代码时工程师已经设置好的。
而且,在生活中我们收到信号,不一定是立即处理的,比如当我们接到外卖员的电话,这是一个信号,但是如果我们现在正在做更重要的事,就不会立即去处理这个信号。同样的,进程的信号随时可能产生(异步产生),但是进程有可能在做更重要的工作,这跟进程的优先级有关。既然信号有可能不能及时被处理,就应该被保存起来,而信号的本质也是数据,收到信号后进程就会往进程的task_struct写入信号数据,信号底层都是通过操作系统发送的。
用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
#include
#include
int main()
{
while (1)
{
printf("process wait signal\n");
sleep(1);
}
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
process wait signal
process wait signal
process wait signal
process wait signal
^C
[cwx@VM-20-16-centos signal]$
信号是进程之间事件异步通知的一种方式,属于软中断。
kill -l查看系统定义的信号列表:
SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业,
这时它们与控制终端不再关联。SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制.
进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用。
SIGABRT 调用abort函数生成的信号。
SIGBUS 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,
但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
SIGUSR1 留给用户使用
SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
SIGUSR2 留给用户使用
SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
SIGTERM 程序结束(terminate)信号,
与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
SIGCHLD 子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程
来接管)。
SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞.
可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束,
只是暂停执行. 本信号不能被阻塞, 处理或忽略.SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
SIGURG 有”紧急”数据或out-of-band数据到达socket时产生.
SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制。
SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
SIGWINCH 窗口大小改变时发出.
SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作.
SIGPWR Power failure
SIGSYS 非法的系统调用。
学习信号需要学习信号产生前、信号产生中和信号产生后三个阶段。
信号捕捉初识:
signal 函数
#include
功能:
捕捉信号
原型:
sighandler_t signal(int signum, sighandler_t handler);
typedef void (*sighandler_t)(int);
参数:
signum:要捕捉的信号号码
handler:函数指针,捕捉指定信号后执行该指针指向的函数
测试代码:
#include
#include
#include
void handler(int signal)
{
printf("get signal: %d\n", signal);
}
int main()
{
signal(2, handler);
while(1)
{
printf("process wait signal\n");
sleep(1);
}
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
process wait signal
process wait signal
process wait signal
process wait signal
^Cget signal: 2
process wait signal
process wait signal
^Cget signal: 2
ctrl+C 通知前台进程组终止进程。SIGINT(2号信号)
ctrl+\ 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。SIGQUIT(3号信号)
ctrl+Z 停止(stopped)进程的执行。SIGSTOP(20号信号)
测试代码:
#include
#include
#include
void handler(int signal)
{
printf("get signal: %d\n", signal);
}
int main()
{
int sig = 1;
for(; sig <= 31; sig++){
signal(sig, handler);
}
while(1)
{
printf("process wait signal\n");
sleep(1);
}
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
process wait signal
^Cget signal: 2
process wait signal
^Zget signal: 20
process wait signal
process wait signal
^\get signal: 3
在学习进程等待时,waitpid方法中的status变量中有大小为一字节的Core Dump标志位。进程异常退出时,core dump位会被设置为1。
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为10240K:
$ ulimit -c 10240
查看详情:
$ ulimit -a
core文件关闭:
$ ulimit -c 0
运行结果:
模拟野指针情况:
#include
#include
#include
int main()
{
int *p = NULL;
*p = 100;
while(1)
{
printf("process wait signal\n");
}
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
Segmentation fault (core dumped)
[cwx@VM-20-16-centos signal]$ ll
total 144
-rw------- 1 cwx cwx 249856 Aug 21 15:18 core.13166
-rw-rw-r-- 1 cwx cwx 69 Aug 21 15:16 makefile
-rwxrwxr-x 1 cwx cwx 9464 Aug 21 15:18 mytest
-rw-rw-r-- 1 cwx cwx 659 Aug 21 15:18 mytest.c
设置允许产生core文件后,运行出错后,会生成core文件,后面的数字是进程的pid,core文件可以通过gdb调试工具调试:
通过core文件和gdb调试工具就可以快速定位到出现异常的行数。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号。
#include
#include
功能:
向进程发送指定信号
原型:
int kill(pid_t pid, int sig);
参数:
pid:要发送信号的进程pid
sig:向进程发送信号的号码
测试代码:
#include
#include
#include
#include
void handler(int signo)
{
printf("get a signal: %d\n", signo);
}
int main()
{
signal(2, handler);
printf("process wait NO.2 signal...\n");
sleep(3); // 三秒后给进程发送2号信号
kill(getpid(), 2);
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
process wait NO.2 signal...
get a signal: 2
#include
功能:
向进程发送指定信号
原型:
int raise(int sig);
参数:
sig:向进程发送信号的号码
测试代码:
#include
#include
#include
void handler(int signo)
{
printf("get a signal: %d\n", signo);
}
int main()
{
signal(2, handler);
printf("process wait NO.2 signal...\n");
sleep(3); // 三秒后给进程发送2号信号
raise(2);
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
process wait NO.2 signal...
get a signal: 2
abort函数使当前进程接收到信号而异常终止。
#include
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
测试代码:
#include
#include
int main()
{
abort();
return 0;
}
运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest
Aborted
软件条件产生信号介绍alarm函数和SIGPIPE信号。SIGPIPE是一种由软件条件产生的信号,在匿名管道中,如果读端关闭,写端会受到操作系统发送到SIGPIPE信号。
alarm函数:
#include
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后
给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
测试代码:
#include
#include
// 1s之内count不断累加,1s被信号SIGALRM信号终止
int main()
{
int count = 0;
alarm(1);
while(1){
printf("count = %d\n", count++);
}
return 0;
}
运行结果:
...
count = 18257
count = 18258
count = 18259
count = 18260
count = 18261
count = 18262
count = 18263
count = 18264Alarm clock
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
模拟野指针异常:
#include
#include
#include
void handler(int signo)
{
printf("get a signal: %d\n", signo);
exit(1);
}
int main()
{
//int sig = 1;
//for(; sig <= 31; sig++)
//{
// signal(sig, handler);
//}
int* p = NULL;
*p = 100;
return 0;
}
运行结果(捕捉信号):
[cwx@VM-20-16-centos signal]$ ./mytest
get a signal: 11
运行结果(未捕捉信号):
[cwx@VM-20-16-centos signal]$ ./mytest
Segmentation fault
SIGSEGV (11号信号)试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
模拟除0错误异常:
#include
#include
#include
void handler(int signo)
{
printf("get a signal: %d\n", signo);
exit(1);
}
int main()
{
int sig = 1;
for(; sig <= 31; sig++)
{
signal(sig, handler);
}
int a = 10;
a /= 0;
return 0;
}
运行结果(捕捉信号):
[cwx@VM-20-16-centos signal]$ ./mytest
get a signal: 8
运行结果(未捕捉信号):
[cwx@VM-20-16-centos signal]$ ./mytest
Floating point exception
SIGFPE (8号信号) 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。