Linux系统编程之信号
1.信号的概念
信号在我们的生活中随处可见, 如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…… 他们都有共性:1. 简单
2. 不能携带大量信息
3.
满足某个特设条件才发送
。
信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。
Unix 早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T 都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1 对可靠信号例程进行了标准化。
2.信号的机制
A 给 B 发送信号,B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行, 去处理信号,处理完毕再继续执行。与硬件中断类
似——异步模式
。但信号是软件层面上实现的中断
,早期常被称 为“软中断”
。
信号的特质: 由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
==每个进程收到的所有信号,都是由内核负责发送的,内核处理。==
3.与信号相关的事件和状态
产生信号:
- 按键产生,如:
Ctrl+c
、Ctrl+z
、Ctrl+\
- 系统调用产生,如:
kill
、raise
、abort
- 软件条件产生,如:定时器
alarm
- 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:
kill
命令
递达: 递送并且到达进程。
未决: 产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方式:
执行默认动作
忽略(丢弃)
捕捉(调用户处理函数)
4.信号屏蔽字和未决信号集
Linux 内核的进程控制块 PCB
是一个结构体,task_struct,
除了包含进程 id,状态,工作目录,用户 id,组 id, 文件描述符表,还包含了信号相关的信息,主要指阻塞信号集
和未决信号集
。
阻塞信号集(信号屏蔽字):
本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:
- 本质:位图。
- 信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
阻塞信号集和未决信号集在内核中的结构是相同的,它们都是一个整形数组 (被封装过的), 一共 128 字节 (int [32] == 1024 bit),1024 个标志位,其中前 31 个标志位,每一个都对应一个 Linux 中的标准信号,通过标志位的值来标记当前信号在信号集中的状态。
5.信号四要素和常规信号一览
5.1 信号的编号
可以使用 kill –l
命令查看当前系统可使用的信号有哪些。
不存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号)
,34-64 称之为实时信号
,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同。
5.2 信号 4 要素
与变量三要素类似的,每个信号也有其必备 4 要素,分别是:
- 编号 2. 名称 3. 事件 4. 默认处理动作
注意: 信号使用之前,应先确定其4要素,而后再用!!!
可通过man 7 signal
查看帮助文档获取。
默认动作:
- Term:终止进程
- Ign: 忽略信号 (默认即时对该种信号忽略操作)
- Core:终止进程,生成 Core 文件。(查验进程死亡原因, 用于 gdb 调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
这里特别强调了 ==9) SIGKILL== 和 ==19) SIGSTOP==信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!
5.3 Linux 常规信号一览表
SIGHUP: 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程
SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件
SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止里程 并产生 core 文件。
SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。
SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件。
SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。
SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGPIPE:Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。
SIGALRM: 定时器超时,超时的时间 由系统调用 alarm 设置。默认动作为终止进程。
SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程。
SIGSTKFLT:Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。
SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
SIGTTOU: 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
SIGXCPU:进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程。
SGIPROF:类似于 SIGVTALRM,它不公包括该进程占用 CPU时间还包括执行系统调用时间。默认动作为终止进程。
SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
SIGIO:此信号向进程指示发出了一个异步 IO 事件。默认动作为忽略。
SIGPWR:关机。默认动作为终止进程。
SIGSYS:无效的系统调用。默认动作为终止进程并产生 core 文件。
SIGRTMIN ~ (64) SIGRTMAX:LINUX 的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
6.信号的产生
6.1 终端按键产生信号
Ctrl + c
→ 2) SIGINT
(终止/中断) “INT” —-Interrupt
Ctrl + z
→ 20) SIGTSTP
(暂停/停止) “T” —-Terminal 终端。
Ctrl + \
→ 3) SIGQUIT
(退出)
6.2 硬件异常产生信号
除 0 操作 → 8) SIGFPE
(浮点数例外)
非法访问内存 → 11) SIGSEGV
(段错误)
总线错误 → 7) SIGBUS
6.4 kill 函数/命令产生信号
函数原型:
1 |
|
函数参数:
pid: > 0:发送信号给指定进程
= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
< -1:取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
= -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
函数返回值:
成功: 0
失败: -1 errno
小案例
子进程发送信号kill父进程:
编译运行,结果如下:
kill -9 -groupname 杀一个进程组
7.定时器
7.1 alarm()函数
设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14)SIGALRM 信号
。进程收到该信号,默认动作终止。
==每个进程都有且只有唯一个定时器。==
函数原型
1 |
|
函数参数:
倒计时 seconds 秒,倒计时完成发送一个信号 SIGALRM
, 当前进程会收到这个信号,这个信号默认的处理动作是中断当前进程
函数返回值:
大于 0 表示倒计时还剩多少秒,返回值为 0 表示倒计时完成,信号被发出
小案例:
使用这个定时器函数,检测一下当前计算机 1s 钟之内能数多少个数
1 |
|
使用 time 命令查看程序执行的时间。 程序运行的瓶颈在于 IO,优化程序,首选优化 IO。
1 | 直接通过终端输出 |
==实际执行时间 = 系统时间 + 用户时间 + 等待时间==
7.2 setitimer()函数
设置定时器(闹钟)。 可代替 alarm
函数。精度微秒 us,可以实现周期定时。
函数原型
1 | // 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号 |
函数参数
which
:指定定时方式① 自然定时:
ITIMER_REAL
→ 14)SIGLARM 计算自然时间② 虚拟空间计时(用户空间):
ITIMER_VIRTUAL
→ 26)SIGVTALRM 只计算进程占用 cpu 的时间③ 运行时计时(用户+内核):
ITIMER_PROF
→ 27)SIGPROF 计算占用 cpu 及执行系统调用的时间new_value:
给定时器设置定时秒数,传入参数old_value:
上一次给定时器设置的定时信息,传出参数,如果不需要这个信息,指定为 NULL
函数返回值:
成功: 0
失败: -1 errno
类型:
1 | struct itimerval { |
可以理解为有2个定时器
- 一个用于第一个闹钟什么时候触发打印
- 一个用于之后间隔多少时间再次触发闹钟。
小案例
使用setitimer定时,向屏幕打印信息:
编译运行,结果如下:
第一次信息打印是两秒间隔,之后都是5秒间隔打印一次
8.信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应 用程序中自定义 set 来改变mask。已达到屏蔽指定
信号的目的。因为用户是不能直接操作内核中的阻塞信号集和未决信号集的,必须要调用系统函数,关于阻塞信号集可以通过系统函数进行读写操作,未决信号集
只能对其进行读操作。
8.1 信号集设定
1 |
|
sigset_t
类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
8.2 sigprocmask()函数
用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB 中) 严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。
函数原型
1 |
|
函数参数:how:
SIG_BLOCK:
当 how设置为此值,set 表示需要屏蔽的信号。相当于 mask = mask|setSIG_UNBLOCK:
当 how设置为此,set 表示需要解除屏蔽的信号。相当于 mask = mask & ~setSIG_SETMASK:
使用参数 set 集合中的数据覆盖内核的阻塞信号集数据
set:
传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。
oldset:
传出参数,保存旧的信号屏蔽集,如果不需要可以指定为 NULL
函数返回值:
函数调用成功返回 0,调用失败返回 - 1
8.4 sigpending() 函数
读取当前进程的未决信号集
int sigpending(sigset_t *set);
set 传出参数。 返回值:成功:0;失败:-1,设置 errno
小案例
需求:
在阻塞信号集中设置某些信号阻塞, 通过一些操作产生这些信号, 然后读未决信号集, 最后再解除这些信号的阻塞
假设阻塞这些信号:
- 2号信号: SIGINT: ctrl+c
- 3号信号: SIGQUIT: ctrl+\
- 9号信号: SIGKILL: 通过shell命令给进程发送这个信号 kill -9 PID
1 |
|
编译运行,结果如下:
==通过测试最终得到结论:程序中对 9 号信号的阻塞是无效的,因为它无法被阻塞。==
9.信号捕捉
Linux 中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略这个信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。程序中进行信号捕捉可以看做是一个注册的动作,提前告诉应用程序信号产生之后做什么样的处理,当进程中对应的信号产生了,这个处理动作也就被调用了。
9.1 signal()函数
使用 signal()
函数可以捕捉进程中产生的信号,并且修改捕捉到的函数的行为,这个信号的自定义处理动作是一个回调函数,内核通过 signal()
得到这个回调函数的地址,在信号产生之后该函数会被内核调用。
函数原型
1 |
|
函数参数:
signum:
需要捕捉的信号
handler:
信号捕捉到之后的处理动作,这是一个函数指针,函数原型typedef void (*sighandler_t)(int);
==这个回调函数是需要程序猿写的,但是程序猿不调用,由内核调用,内核调用回调函数的时候,会给它传递一个实参,这个实参的值就是捕捉的那个信号值。==
小案例
下面的测试程序中使用 signal () 函数来捕捉定时器产生的信号 SIGALRM:
1 |
|
编译运行,结果如下:
9.2 sigaction()函数
sigaction ()
函数和 signal ()
函数的功能是一样的,用于捕捉进程中产生的信号,并将用户自定义的信号行为函数(回调函数)注册给内核,内核在信号产生之后调用这个处理动作。sigaction ()
可以看做是 signal ()
函数是加强版,函数参数更多更复杂,函数功能也更强一些。
函数原型:
1 |
|
函数参数:
signum:
要捕捉的信号
act:
捕捉到信号之后的处理动作
oldact:
上一次调用该函数进行信号捕捉设置的信号处理动作,该参数一般指定为 NULL
函数返回值:
函数调用成功返回 0,失败返回 - 1
该函数的参数是一个结构体类型,结构体原型如下:
1 | struct sigaction { |
sa_restorer
:该元素是过时的,不应该使用,POSIX.1 标准将不指定该元素。(弃用)
sa_sigaction
:当 sa_flags
被指定为 SA_SIGINFO
标志时,使用该信号处理程序。(很少使用)
==重点掌握:==
① sa_handler
:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN
表忽略 或 SIG_DFL
表执行默认动作
② sa_mask
: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags
:通常设置为 0,表使用默认属性。
小案例:
通过 sigaction () 捕捉阻塞信号集中解除阻塞的信号
1 |
|
编译运行,如下所示:
9.3 信号捕捉的特性
信号捕捉特性:
捕捉函数执行期间,信号屏蔽字 由 mask –> sa_mask , 捕捉函数执行结束。 恢复回mask
捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).其他信号不屏蔽,如需屏蔽则调用sigsetadd函数修改
捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
10.内核实现信号捕捉简析
11.SIGCHLD 信号
11.1 SIGCHLD 的产生条件
- 子进程终止时
- 子进程接收到SIGSTOP
- 子进程处于停止态,接收到SIGCONT后唤醒时
11.2 借助 SIGCHLD 信号回收子进程
1 |
|
SIGCHLD 信号注意问题
- 子进程继承父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集 spending。
- 注意注册信号捕捉函数的位置。
- 应该在 fork 之前,阻塞 SIGCHLD 信号。注册完捕捉函数后解除阻塞。