1. 信号的基本概念
为了理解信号,先从我们最熟悉的场景说起:
用户输入命令,在Shell下启动一个前台进程。
用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
终端驱动程序将Ctrl-C解释成一个
SIGINT
信号,记在该进程的PCB中(也可以说发送了一个SIGINT
信号给该进程)。当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个
SIGINT
信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。
注意,Ctrl-C产生的信号只能发给前台进程。在第 3.3 节 “wait和waitpid函数”中我们看到一个命令后面加个&
可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT
信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
用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
中找到,例如其中有定义#define SIGINT 2
。编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)
中都有详细说明:
- Signal Value Action Comment
- 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 …
上表中第一列是各信号的宏定义名称,第二列是各信号的编号,第三列是默认处理动作,Term
表示终止当前进程,Core
表示终止当前进程并且Core Dump(下一节详细介绍什么是Core Dump),Ign
表示忽略该信号,Stop
表示停止当前进程,Cont
表示继续执行先前停止的进程,表中最后一列是简要介绍,说明什么条件下产生该信号。
产生信号的条件主要有:
用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生
SIGINT
信号,Ctrl-\产生SIGQUIT
信号,Ctrl-Z产生SIGTSTP
信号(可使前台进程停止,这个信号将在第 34 章 终端、作业控制与守护进程详细解释)。硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为
SIGFPE
信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV
信号发送给进程。一个进程调用
kill(2)
函数可以发送信号给另一个进程。可以用
kill(1)
命令发送信号给某个进程,kill(1)
命令也是调用kill(2)
函数实现的,如果不明确指定信号则发送SIGTERM
信号,该信号的默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生
SIGALRM
信号,向读端已关闭的管道写数据时产生SIGPIPE
信号。
如果不想按默认动作处理信号,用户程序可以调用sigaction(2)
函数告诉内核如何处理某种信号(sigaction
函数稍后详细介绍),可选的处理动作有以下三种:
忽略此信号。
执行该信号的默认处理动作。
提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。