基本概念
信号在Linux中是一个比较常见的概念,例如我们按Ctrl+C中断前台进程,通过Kill命令结束进程都是通过信号实现的。下面就以Ctrl+C为例简单的说明信号的处理流程:
-
用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
-
该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
-
终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。
-
当内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。
用kill -l命令可以察看系统定义的信号列表:
$ kill –l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
...
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,可以通过man signal(7)查看详细说明:
Signal Value Action Comment
-------------------------------------------------------------------------
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
...
产生信号的条件主要有:
-
用户在终端按下某些键时,终端驱动程序会发送信号给前台进程。
例如常见的Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号。 -
硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
-
一个进程调用kill函数可以发送信号给另一个进程。
-
当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。
捕捉信号
如果不想按默认动作处理信号,用户程序可以调用sigaction函数接管该信号的处理流程。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
-
用户程序注册了SIGQUIT信号的处理函数sighandler。
-
当前正在执行main函数,这时发生中断或异常切换到内核态。
-
在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
-
内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
-
sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
-
如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
sigaction函数可以读取和修改与指定信号相关联的处理动作,它的声明如下:
#include
<signal.h>
int sigaction(int signum, const
struct sigaction *act, struct sigaction *oldact);
其中关键就是第二个参数act,他是一个sigaction类型,结构如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
其中关键的是sa_handler这个参数,它通常可以有如下几种赋值:
-
常数SIG_IGN表示忽略信号
-
常数SIG_DFL表示执行系统默认动作,一般用于恢复信号处理
-
赋值为一个函数指针表示用自定义函数捕捉信号
另外,如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
下面就以一个简单的例子演示下如何实现捕捉信号的过程,该函数的功能比较简单,就是在Ctrl+C的时候并不直接退出,而是先输出一条华丽的分割线后才退出。
#include
<stdio.h>
#include
<signal.h>
#include
<unistd.h>
#include
<stdlib.h>
void show_and_exit(int sig)
{
printf("\n----------------------------\n");
exit(0);
}
int main(void)
{
struct sigaction act = {0}, oldact = {0};
act.sa_handler = show_and_exit;
//act.sa_flags = SA_RESETHAND | SA_NODEFER;
//sigaddset(&act.sa_mask, SIGQUIT);
sigaction(SIGINT, &act, &oldact);
int count = 0;
while(1)
{
sleep(1);
printf("sleeping %d\n", count++);
}
}
执行该函数结果如下:
tianfang > run
sleeping 0
sleeping 1
sleeping 2
^C
----------------------------
tianfang >