信号
信号
苏丙榅1. 信号概述
Linux中的信号是一种消息处理机制, 它本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很大的信息量,但是信号在系统中的优先级是非常高的。
在Linux中的很多常规操作中都会有相关的信号产生,先从我们最熟悉的场景说起:
通过键盘操作产生了信号
:用户按下Ctrl-C,这个键盘输入产生一个硬件中断,使用这个快捷键会产生信号, 这个信号会杀死对应的某个进程通过shell命令产生了信号
:通过kill命令终止某一个进程,kill -9 进程PID
通过函数调用产生了信号
:如果CPU当前正在执行这个进程的代码调用,比如函数sleep()
,进程收到相关的信号,被迫挂起通过对硬件进行非法访问产生了信号
:正在运行的程序访问了非法内存,发生段错误,进程退出。
信号也可以实现进程间通信,但是信号能传递的数据量很少,不能满足大部分需求,另外信号的优先级很高,并且它对应的处理动作是回调完成的,它会打乱程序原有的处理流程,影响到最终的处理结果。因此非常不建议使用信号进行进程间通信。
1.1 信号编号
通过
kill -l
命令可以察看系统定义的信号列表:
1 | 执行shell命令查看信号 |
下表中详细阐述了信号产生的时机和对应的默认处理动作:
编号 | 信号 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 用户退出shell时,由该shell启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT | 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
3 | SIGQUIT | 用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 | 终止进程 |
4 | SIGILL | CPU检测到某进程执行了非法指令 | 终止进程并产生core文件 |
5 | SIGTRAP | 该信号由断点指令或其他 trap指令产生 | 终止进程并产生core文件 |
6 | SIGABRT | 调用abort函数时产生该信号 | 终止进程并产生core文件 |
7 | SIGBUS | 非法访问内存地址,包括内存对齐出错 | 终止进程并产生core文件 |
8 | SIGFPE | 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 | 终止进程并产生core文件 |
9 | SIGKILL | 无条件终止进程。本信号不能被忽略,处理和阻塞 | 终止进程,可以杀死任何进程 |
10 | SIGUSE1 | 用户定义的信号。即程序员可以在程序中定义并使用该信号 | 终止进程 |
11 | SIGSEGV | 指示进程进行了无效内存访问(段错误) | 终止进程并产生core文件 |
12 | SIGUSR2 | 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 | 终止进程 |
13 | SIGPIPE | Broken pipe向一个没有读端的管道写数据 | 终止进程 |
14 | SIGALRM | 定时器超时,超时的时间 由系统调用alarm设置 | 终止进程 |
15 | SIGTERM | 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 | 终止进程 |
16 | SIGSTKFLT | Linux早期版本出现的信号,现仍保留向后兼容 | 终止进程 |
17 | SIGCHLD | 子进程结束时,父进程会收到这个信号 | 忽略这个信号 |
18 | SIGCONT | 如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP | 停止进程的执行。信号不能被忽略,处理和阻塞 | 为终止进程 |
20 | SIGTSTP | 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 | 暂停进程 |
21 | SIGTTIN | 后台进程读终端控制台 | 暂停进程 |
22 | SIGTTOU | 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 | 暂停进程 |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 | 忽略该信号 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程 | 终止进程 |
25 | SIGXFSZ | 超过文件的最大长度设置 | 终止进程 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 | 终止进程 |
27 | SGIPROF | 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 | 终止进程 |
28 | SIGWINCH | 窗口变化大小时发出 | 忽略该信号 |
29 | SIGIO | 此信号向进程指示发出了一个异步IO事件 | 忽略该信号 |
30 | SIGPWR | 关机 | 终止进程 |
31 | SIGSYS | 无效的系统调用 | 终止进程并产生core文件 |
34~64 | SIGRTMIN ~ SIGRTMAX | LINUX的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
1.2 查看信号信息
通过Linux提供的 man 文档可以查询所有信号的详细信息:
1 | 查看man文档的信号描述 |
在信号描述中介绍了对产生的信号的五种默认处理动作,分别是:
Term
:信号将进程终止Ign
:信号产生之后默认被忽略了Core
:信号将进程终止, 并且生成一个core文件(一般用于gdb调试)Stop
:信号会暂停进程的运行Cont
:信号会让暂停的进程继续运行
关于对信号的介绍有一句非常重要的描述:
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
9号信号和19号信号不能被 捕捉, 阻塞, 和 忽略
9号信号: 无条件杀死进程
19号信号: 无条件暂停进程
有些信号在不同的平台对应的值是不一样的,对应我们使用PC机来说,需要看中间一列的值:
1.3 信号的状态
Linux中的信号有三种状态,分别为:产生,未决,递达。
产生
:键盘输入, 函数调用, 执行shell命令, 对硬件进行非法访问都会产生信号未决
:信号产生了, 但是这个信号还没有被处理掉, 这个期间信号的状态称之为未决状态递达
:信号被处理了(被某个进程处理掉)
2. 信号相关函数
Linux中能够产生信号的函数有很多,下面介绍几个常用函数:
2.1 kill/raise/abort
这三个函数的功能比较类似,可以发送相关的信号给到对应的进程。
kill 发送指定的信号到指定的进程,函数原型如下:
1
2
3
// 给某一个进程发送一个信号
int kill(pid_t pid, int sig);- 参数:
- pid: 进程ID(man 文档里边写的比较详细)
- sig: 要发送的信号
函数使用举例:
1
2
3
4// 自己杀死自己
kill(getpid(), 9);
// 子进程杀死自己的父进程
kill(getppid(), 10);- 参数:
raise:给当前进程发送指定的信号,函数原型如下:
1
2
3// 给自己发送某一个信号
int raise(int sig); // 参数就是要给当前进程发送的信号abort:给当前进程发送一个固定信号 (SIGABRT),函数原型如下:
1
2
3// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
void abort(void);
2.2 定时器
2.2.1 alarm
alarm() 函数只能进行单次定时,定时完成发射出一个信号。
1 |
|
- 参数: 倒计时seconds秒, 倒计时完成发送一个信号 SIGALRM , 当前进程会收到这个信号,这个信号默认的处理动作是中断当前进程
- 返回值: 大于0表示倒计时还剩多少秒,返回值为0表示倒计时完成, 信号被发出
使用这个定时器函数, 检测一下当前计算机1s钟之内能数多少个数
1 |
|
执行上述程序的时候, 计算一下时间
1 | 直接通过终端输出 |
文件IO操作需要进行用户区到内核区的切换,处理方式不同,二者之间切换的频率也不同。也就是说对文件IO操作进行优化是可以提供程序的执行效率的。
2.2.2 setitimer
setitimer () 函数可以进行周期性定时,每触发一次定时器就会发射出一个信号。
1 | // 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号 |
参数:
- which: 定时器使用什么样的计时法则, 不同的计时法则发出的信号不同
ITIMER_REAL
: 自然计时法, 最常用, 发出的信号为SIGALRM
, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间(从进程的用户区到内核区切换使用的总时间)ITIMER_VIRTUAL
: 只计算程序在用户区运行使用的时间,发射的信号为SIGVTALRM
ITIMER_PROF
: 只计算内核运行使用的时间, 发出的信号为SIGPROF
- new_value: 给定时器设置的定时信息, 传入参数
- old_value: 上一次给定时器设置的定时信息, 传出参数,如果不需要这个信息, 指定为NULL
- which: 定时器使用什么样的计时法则, 不同的计时法则发出的信号不同
3. 信号集
3.1 阻塞/未决信号集
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集体现在内核中就是两张表。但是操作系统不允许我们直接对这两个信号集进行任何操作,而是需要自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。
信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了 防止信号打断某些敏感的操作。
阻塞信号集和未决信号集在内核中的结构是相同的,它们都是一个整形数组(被封装过的), 一共 128 字节 (int [32] == 1024 bit),1024个标志位,其中前31个标志位,每一个都对应一个Linux中的标准信号,通过标志位的值来标记当前信号在信号集中的状态。
1 | 上图对信号集在内核中存储的状态的描述 |
在阻塞信号集中,描述这个信号有没有被阻塞
- 默认情况下没有信号是被阻塞的, 因此信号对应的标志位的值为 0
- 如果某个信号被设置为了阻塞状态, 这个信号对应的标志位 被设置为 1
在未决信号集中, 描述信号是否处于未决状态
- 如果这个信号被阻塞了, 不能处理, 这个信号对应的标志位被设置为1
- 如果这个信号的阻塞被解除了, 未决信号集中的这个信号马上就被处理了, 这个信号对应的标志位值变为0
- 如果这个信号没有阻塞, 信号产生之后直接被处理, 因此不会在未决信号集中做任何记录
3.2 信号集函数
因为用户是不能直接操作内核中的阻塞信号集和未决信号集的,必须要调用系统函数,关于阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作。
先来看一下读/写阻塞信号集的函数:
1 |
|
- 参数:
- how:
SIG_BLOCK
: 将参数 set 集合中的数据追加到阻塞信号集中SIG_UNBLOCK
: 将参数 set 集合中的信号在阻塞信号集中解除阻塞SIG_SETMASK
: 使用参 set 结合中的数据覆盖内核的阻塞信号集数据- oldset: 通过这个参数将设置之前的阻塞信号集数据传出,如果不需要可以指定为NULL
- 返回值:函数调用成功返回0,调用失败返回-1
- how:
sigprocmask() 函数有一个 sigset_t 类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数:
1 |
|
未决信号集不需要程序猿修改, 如果设置了某个信号阻塞, 当这个信号产生之后, 内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞, 未决信号集中的信号随之被处理, 内核再次修改未决信号集将该信号的状态修改为递达状态(标志位置0)。因此,写未决信号集的动作都是内核做的,这是一个读未决信号集的操作函数:
1 |
|
下面举一个简单的例子,演示一下信号集操作函数的使用:
1 | 需求: |
1 |
|
通过测试最终得到结论:程序中对 9 号信号的阻塞是无效的,因为它无法被阻塞。
最后通过一张图总结一下这些信号集操作函数之间的关系:
4. 信号捕捉
Linux中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略这个信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。程序中进行信号捕捉可以看做是一个注册的动作,提前告诉应用程序信号产生之后做什么样的处理,当进程中对应的信号产生了,这个处理动作也就被调用了。
4.1 signal
使用 signal() 函数可以捕捉进程中产生的信号,并且修改捕捉到的函数的行为,这个信号的自定义处理动作是一个回调函数,内核通过 signal() 得到这个回调函数的地址,在信号产生之后该函数会被内核调用。
1 |
|
参数:
signum: 需要捕捉的信号
handler: 信号捕捉到之后的处理动作, 这是一个函数指针, 函数原型
1
typedef void (*sighandler_t)(int);
这个回调函数是需要程序猿写的, 但是程序猿不调用, 由内核调用,内核调用回调函数的时候, 会给它传递一个实参,这个实参的值就是捕捉的那个信号值。
下面的测试程序中使用 signal() 函数来捕捉定时器产生的信号 SIGALRM:
1 |
|
4.2 sigaction
sigaction() 函数和 signal() 函数的功能是一样的,用于捕捉进程中产生的信号,并将用户自定义的信号行为函数(回调函数)注册给内核,内核在信号产生之后调用这个处理动作。sigaction() 可以看做是 signal() 函数是加强版,函数参数更多更复杂,函数功能也更强一些。函数原型如下:
1 |
|
- 参数:
signum: 要捕捉的信号
act: 捕捉到信号之后的处理动作
oldact: 上一次调用该函数进行信号捕捉设置的信号处理动作, 该参数一般指定为NULL
- 返回值:函数调用成功返回0,失败返回-1
该函数的参数是一个结构体类型,结构体原型如下:
1 | struct sigaction { |
结构体成员介绍:
sa_handler: 函数指针,指向的函数就是捕捉到的信号的处理动作
sa_sigaction: 函数指针,指向的函数就是捕捉到的信号的处理动作
sa_mask: 在信号处理函数执行期间, 临时屏蔽某些信号, 将要屏蔽的信号设置到集合中即可
- 当前处理函数执行完毕, 临时屏蔽自动解除
- 假设在这个集合中不屏蔽任何信号, 默认也会屏蔽一个(捕捉的信号是谁, 就临时屏蔽谁)
sa_flags:使用哪个函数指针指向的函数处理捕捉到的信号
0
:使用sa_handler
(一般情况下使用这个)SA_SIGINFO
:使用 sa_sigaction (使用信号传递数据==进程间通信)
sa_restorer: 被废弃的成员
示例代码,通过sigaction()捕捉阻塞信号集中解除阻塞的信号,如果捕捉多个信号,可以给不同的信号添加不同的处理动作,代码中的处理动作只有一个:
1 |
|
通过测试最终得到结论:程序中对 9 号信号的捕捉是无效的,因为它无法被捕捉。
5. SIGCHLD 信号
当子进程退出、暂停、从暂停回复运行的时候,在子进程中会产生一个SIGCHLD信号,并将其发送给父进程,但是父进程收到这个信号之后默认就忽略了。我们可以在父进程中对这个信号加以利用,基于这个信号来回收子进程的资源,因此需要在父进程中捕捉子进程发送过来的这个信号。
下面是基于信号回收子进程资源的示例代码:
1 |
|