信号
Linux里信号概念和作用
概念
信号 | 值 | 描述 |
---|---|---|
SIGHUP |
1 | 终端挂起或控制进程终止 |
SIGINT |
2 | 中断(Interrupt)信号,通常由Ctrl+C触发 |
SIGQUIT |
3 | 退出(Quit)信号,通常由Ctrl+\触发,导致进程终止并生成核心转储 |
SIGILL |
4 | 非法指令(Illegal Instruction) |
SIGTRAP |
5 | 跟踪陷阱(Trace Trap) |
SIGABRT |
6 | 由abort函数产生的信号 |
SIGBUS |
7 | 总线错误(Bus Error) |
SIGFPE |
8 | 浮点异常(Floating Point Exception),如除以零 |
SIGKILL |
9 | 杀死(Kill)信号,强制终止进程 |
SIGUSR1 |
10 | 用户定义信号1 |
SIGSEGV |
11 | 段错误(Segmentation Fault),非法内存访问 |
SIGUSR2 |
12 | 用户定义信号2 |
SIGPIPE |
13 | 管道破裂(Broken Pipe),写入无读端的管道 |
SIGALRM |
14 | 闹钟(Alarm)信号,由alarm函数触发 |
SIGTERM |
15 | 终止(Termination)信号,软件终止(可捕获) |
SIGCHLD |
17 | 子进程停止或终止 |
SIGCONT |
18 | 继续(Continue)信号,使暂停的进程继续执行 |
SIGSTOP |
19 | 停止(Stop)信号,暂停进程(不可捕获和忽略) |
SIGTSTP |
20 | 终端停止(Terminal Stop)信号,通常由Ctrl+Z触发,暂停前台进程 |
SIGTTIN |
21 | 后台进程尝试从终端读数据 |
SIGTTOU |
22 | 后台进程尝试向终端写数据 |
信号
信号是进程处理紧急情况所用的一种方式,它没有特别复杂的数据结构,就是用一个代号一样的数字。
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。
Linux 提供了几十种信号,分别代表不同的意义。我们可以通过
kill -l
命令查看信号。信号在进程的整个生命周期里都有效。
信号的产生和代码运行的过程是
异步
的。- 因此收到信号之后,可能不会立即处理这个信号,即pengding(例如进程正在进行IO),因此需要先将信号保存起来。
中断
中断又分为软中断和硬中断:
上半部直接处理硬件请求,也就是硬中断。硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序。
信号与进程之间,属于软中断。软中断(下半部)是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,比如
0
号 CPU 对应的软中断内核线程的名字是ksoftirqd/0
。
产生信号
- 信号发送来源广泛,有可能来自于用户态,有可能来自于硬件,也有可能来自于内核。
- 来自键盘组合键 :
Ctrl+C
产生 SIGINT 信号,Ctrl+Z
产生SIGTSTP
信号。 - 来自
kill
命令 :kill -9 pid
可以发送信号给一个进程,杀死它。 - 来自
系统调用
:有的时候,硬件异常也会产生信号。例如:当执行了除以零的指令时,CPU会产生一个异常(状态标志位会进行标记),然后发送SIGFPE(浮点异常)信号给进程。
- 来自键盘组合键 :
会话(session) 前/后台进程
在我们登录Linux服务器时,会产生一个会话
,对于一个会话
,一般只会配上一个bash
(默认前台进程)。
- 当我们执行一个进程时,如果这个进程变为了
前台进程
,bash
则会变为后台进程
,不再提供代码解释。
每一个终端会话只能有一个前台进程组,但可以有多个后台进程,并且我们的键盘信号
只能发送给前台进程。
这也就是为什么我们在终端中运行
./可执行程序
之后,在同一终端中无法使用bash指令。因为此时该程序会占用前台(阻塞当前的bash),此时bash会等待这个前台进程完成。你想在运行可执行程序的同时继续使用bash,可以使用
&
将该程序放到后台运行./可执行程序 &
。这样,该程序会在后台运行,而bash会继续在前台等待你的输入。
其中区分前后台进程,可以使用谁获取键盘输入,谁就是前台进程这样的方法。
操作 | 作用 |
---|---|
jobs | 查看后台进程 |
gf -任务号 | 将某一个任务提为前台进程 |
^Z | 暂停一个前台进程 |
bg | 将暂停(挂起)状态的作业继续在后台运行 |
信号的处理
执行默认操作。
捕捉信号(并非所有信号都能被捕捉):提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数。
- 如果没有收到信号,那么这个方法不会被调用。
忽略信号。
常见的函数
函数 | 描述 | 函数原型 |
---|---|---|
signal |
设置一个函数来处理一个信号。 | void (*signal(int signum, void (*handler)(int)))(int); |
raise |
发送信号给调用进程。 | int raise(int sig); |
kill |
向进程或进程组发送信号。 | int kill(pid_t pid, int sig); |
sigaction |
更改或检查指定信号的处理程序。 | int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
sigemptyset |
初始化一个空的信号集。 | int sigemptyset(sigset_t *set); |
sigfillset |
初始化一个包含所有信号的信号集。 | int sigfillset(sigset_t *set); |
sigaddset |
将指定信号添加到信号集中。 | int sigaddset(sigset_t *set, int signum); |
sigdelset |
从信号集中删除指定信号。 | int sigdelset(sigset_t *set, int signum); |
sigprocmask |
更改进程的信号屏蔽字(阻塞信号)。 | int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); |
sigsuspend |
暂停进程直到接收到一个信号。 | int sigsuspend(const sigset_t *mask); |
sigpending |
检查当前未决的信号(进程正在等待处理的信号)。 | int sigpending(sigset_t *set); |
alarm |
在指定秒数后发送SIGALRM 信号给进程。 |
unsigned int alarm(unsigned int seconds); |
pause |
使进程暂停执行,直到接收到信号。 | int pause(void); |
sigwait |
等待一个或多个信号。 | int sigwait(const sigset_t *set, int *sig); |
sigqueue |
向进程发送一个信号和数据。 | int sigqueue(pid_t pid, int sig, const union sigval value); |
当设置了多个alarm
时,本次调用alarm
的返回值是上次调用的剩余时间。判断alarm
是否超时可以使用堆,到时只用判断堆顶就知道
普通信号[1-31号]如何保存的?
进程的task_struct
(PCB)内,存在一个signal
(32-bit),利用它的比特位来表示一个信号。即位图管理。所谓的发信号,实际就是操作系统去修改对应进程pcb的信号位图。
阻塞-未决信号与信号方法
每个信号都有两个标志位分别表示**阻塞(block) 和未决(pending),还有一个函数指针数组标(handler)**表示处理动作。
block
位图用来记录是否屏蔽(阻塞)某个信号。pending
位图用来记录是否收到某个信号。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
具体流程
信号送达:信号到达进程时,对应的pending位图位被设置为 1。
检查阻塞表:如果该信号未被阻塞,信号处理函数将被调用。
清除pending位图:在信号处理函数执行之前,相应的pending位图位被清除(从 1 变为 0)。
添加到block表:在信号处理函数执行期间,该信号被自动添加到block表中,防止信号处理函数被嵌套调用。
信号处理完毕:信号处理函数执行完毕后,该信号从block表中移除,恢复之前的阻塞状态。
sigset_t数据类型
每个信号只有一个bit的未决标志,非
0
即1
,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型
sigset_t
来存储,sigset_t
称为信号集。
信号集操作函数
函数 | 原型 | 描述 |
---|---|---|
sigemptyset |
int sigemptyset(sigset_t *set); |
初始化一个信号集为空集 |
sigfillset |
int sigfillset(sigset_t *set); |
初始化一个信号集,并将所有的信号加入到此信号集中 |
sigaddset |
int sigaddset(sigset_t *set, int signo); |
将指定的信号加入到信号集中 |
sigdelset |
int sigdelset(sigset_t *set, int signo); |
从信号集中删除指定的信号 |
sigismember |
int sigismember(const sigset_t *set, int signo); |
检查指定的信号是否在信号集中 |
sigprocmask |
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); |
检查或更改当前阻塞的信号集 |
sigsuspend |
int sigsuspend(const sigset_t *mask); |
暂时替换进程的信号掩码,并挂起进程直到接收到一个信号 |
sigpending |
int sigpending(sigset_t *set); |
获取当前未决信号集 |
sigaction |
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
检查或修改指定信号的处理行为 |
sigwait |
int sigwait(const sigset_t *set, int *sig); |
等待信号集中的一个信号,并将其从信号集中删除 |
sigwaitinfo |
int sigwaitinfo(const sigset_t *set, siginfo_t *info); |
等待信号集中的一个信号,并返回附加信息 |
sigtimedwait |
int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout); |
等待信号集中的一个信号,带有超时时间 |
信号处理和捕获
当进程从内核态返回到用户态的时候,进行信号的检测和处理。
操作系统会自动的完成身份切换。
基于信号的等待
条件
- 子进程在退出的时候,会发出
17
号信号。 - 父进程要保持一直在运行。
- 最后还是需要使用
wait()/waitpid()
这样的接口来解决子进程的僵尸状态。
- 子进程在退出的时候,会发出
因此,我们可以完成基于信号捕捉的
等待
。如果是多个子进程,那么在信号捕获的基础上采用循环非阻塞(非阻塞:应对只退出一半子进程的情况)。1
while((rid = waitpid(-1,nullptr,WNOHANG))>0)...
特殊情况
- SIGCHLD信号:
- 在一些Unix系统(包括Linux)中,如果父进程将SIGCHLD信号的处理动作设置为SIG_IGN,则子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。不过,这种方法在所有的Unix系统上并不完全保证可用。