LV05-05-进程通信-03-信号
本文主要是进程通信——信号的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
--- | --- |
一、信号简介
1. 信号的概念
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。
信号的目的都是用于通信的,当发生某种情况下,通过信号将情况告知相应的进程,从而达到同步、通信的目的。
另外信号是异步事件,而且是进程通信中唯一的异步通信机制,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。
2. 信号的产生
- 硬件发生异常
就是硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为0
、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
- 在终端下输入了能够产生信号的特殊字符
之前我们结束一个进程都是使用Ctrl+C
,其实这样一个组合按键是产生了一个中断信号(SIGINT
),通过这个信号可以终止在前台运行的进程;还有其他的组合键,例如按下Ctrl + Z
组合按键可以产生暂停信号(SIGCONT
),通过这个信号可以暂停当前前台运行的进程。
- 进程调用
kill()
系统调用可将任意信号发送给另一个进程或进程组
接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root
超级用户。
- 通过
kill
命令将信号发送给其它进程。
kill
命令其实我们前边有使用过,通常我们会通过kill
命令来杀死(终止)一个进程,例如在终端下执行kill -9 xxx
来杀死PID
为xxx
的进程。kill
命令其内部的实现原理便是通过kill()
系统调用来完成的。
- 发生软件事件
也就是检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0
、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的CPU
时间超限、进程的某个子进程退出等等情况)。
其实进程同样也可以向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核。
3. 信号的处理方式
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会根据信号进行如下操作。
- 忽略信号
当信号到达进程后,该进程直接忽略,就好像是没有出现该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是SIGKILL
和SIGSTOP
,这是因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
- 捕获信号
当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux
系统提供了signal()
系统调用可用于注册信号的处理函数。
- 执行系统默认操作
当信号到达进程后,进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。
进程对信号的处理是可以通过函数来修改的,后边会详细学习。
4. 都有哪些信号
上边说到了几个信号,那在我们的Linux
系统中,有多少种信号呢?信号在本质上其实是int
类型的数字编号,这些数字从1
开始,定义在.h
,文件中,我们可以使用如下命令查找该文件的位置:
1 | locate signum.h |
当然我们也可以通过终端直接打印出支持的信号,命令如下:
1 | kill -l |
然后终端便会有如下信息显示:
1 | 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP |
当我们需要t通过终端发送某个信号给进程时,可以使用如下命令,大但是我们需要先知道接收信号的进程的PID
,
1 | kill [-signal] <pid> |
signal
即为信号对应的int
类型数字,例如,我们要杀死一个进程,使用的命令如下:
1 | kill -9 <pid> |
5. 常用信号及含义
上边那么多的信号,我们并不一定都用得上,常用的几个如下所示:
点击查看详细说明
【说明】
term
表示终止进程core
表示生成可用于调试的核心转储文件ignore
表示忽略信号continue
表示继续运行进程pause
表示暂停进程
编号 | 信号名 | 默认操作 | 含义 |
1 | SIGHUP | term | 在用户终端关闭时产生,通常是发给和该终端关联的会话内的所有进程 |
2 | SIGINT | term | 该信号在用户键入INTR字符(Ctrl-C)时产生,内核发送此信号到当前终端的所有前台进程 |
3 | SIGQUIT | term+core | 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-\)来产生,进程如果陷入无限循环、或不再响应时,使用SIGQUIT信号就很合适 |
4 | SIGILL | term+core | 该信号在一个进程企图执行一条非法指令时产生 |
6 | SIGABRT | term+core | 当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT信号 |
7 | SIGBUS | term+core | 总线错误(bus error)信号,表示发生了某种内存访问错误 |
8 | SIGFPE | term+core | 该信号因特定类型的算术错误而产生,例如除以0 |
9 | SIGKILL | term | 该信号用来结束进程,并且不能被捕捉和忽略 |
10 | SIGUSR1 | term | 该信号保留给用户程序使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作 |
11 | SIGSEGV | term | 该信号在非法访问内存时产生,如野指针、缓冲区溢出 |
12 | SIGUSR2 | term | 该信号保留给用户程序使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作 |
13 | SIGPIPE | term | 当进程向已经关闭的管道、FIFO或套接字写入信息时,那么系统将发送该信号给进程 |
14 | SIGALRM | term | 该信号用于通知进程定时器时间已到,与系统调用alarm()或setitimer()有关 |
15 | SIGTERM | term | 终止进程的标准信号,也是kill命令所发送的默认信号。有时我们会直接使用"kill -9 xxx"显式向进程发送SIGKILL信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获SIGTERM信号、并为其绑定一个处理函数,当该进程收到SIGTERM信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用SIGKILL信号终止进程,从而跳过了SIGTERM信号的处理函数,通常SIGKILL终止进程是不友好且暴力的方式,这种方式应该作为最后手段,应首先尝试使用SIGTERM,而将SIGKILL作为最后手段 |
17 | SIGCHLD/SIGCLD | ignore | 当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,我们可以理解为暂停 |
18 | SIGCONT | continue | 该信号让进程进入运行态 |
19 | SIGSTOP | pause | 该信号用于暂停进程,并且不能被捕捉和忽略 |
20 | SIGTSTP | pause | 该信号用于暂停进程,用户可键入SUSP字符(通常是Ctrl-Z)发出这个信号,按下组合键后系统会将SIGTSTP信号发送给前台进程组中的每一个进程,使其暂停运行 |
24 | SIGXCPU | term+core | 当进程的CPU时间超出对应的资源限制时,内核将发送此信号给该进程 |
26 | SIGVTALRM | term | 应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程 |
28 | SIGWINCH | ignore | 在窗口环境中,当终端窗口尺寸发生变化时(例如用户手动调整了大小,应用程序调用ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号 |
29 | SIGPOLL/SIGIO | term/ignore | 用于提示一个异步IO事件的发生,例如应用程序打开的文件描述符发生了I/O事件时,内核会向应用程序发送SIGIO信号 |
31 | SIGSYS | term+core | 如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程 |
6. 信号分类
Linux
系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号。
6.1 可靠信号与不可靠信号
Linux
信号机制基本上是从UNIX
系统中继承过来的,早期UNIX
系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal()
,重新为该信号绑定相应的处理函数。
早期UNIX
下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。Linux
支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()
。因此,Linux
下的不可靠信号问题主要指的是信号可能丢失。在Linux
系统下,信号值小于SIGRTMIN(34)
(编号为1~31
)的信号都是不可靠信号,这就是不可靠信号的来源。
随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种UNIX
版本分别在这方面进行了研究,力图实现可靠信号。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号SIGRTMIN~SIGRTMAX
,编号为34~64
,并在一开始就把它们定义为可靠信号。可靠信号并没有一个具体对应的名字,而是使用了SIGRTMIN+N
或SIGRTMAX-N
的方式来表示。
可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数sigqueue()
及信号绑定函数sigaction()
。
6.2 实时信号与非实时信号
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号,一般我们也把非实时信号(不可靠信号)称为标准信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX
标准的一部分,可用于应用进程。
7. 信号描述信息
在Linux
下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于sys_siglist
数组中,sys_siglist
数组是一个char *
类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。
7.1 strsignal()
7.1.1 函数说明
在linux
下可以使用man 3 strsignal
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以用于获取信号的描述信息。
【函数参数】
sig
:int
类型,需要显示详细信息的信号的宏(需要加上<signal.h>
头文件)或者编号。
【返回值】char *
类型,返回执行信号描述信息字符串的指针;函数会对参数sig
进行检查,若传入的sig
无效,则会返回Unknown signal
信息。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
7.1.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | SIGINT Description: Interrupt |
7.2 psignal()
7.2.1 函数说明
在linux
下可以使用man 3 psignal
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以在标准错误(stderr
)上输出信号描述信息,常用来输出信号的出错消息。
【函数参数】
sig
:int
类型,需要显示详细信息的信号的宏(需要加上<signal.h>
头文件)或者编号。s
:char *
类型,调用该函数时添加的一些提示信息,由s
指定,所以整个输出信息由字符串s
、冒号、空格、描述信号编号sig
的字符串和尾随的换行符组成。
【返回值】none
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
7.2.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | SIGINT: Interrupt |
二、进程对信号的处理
1. 信号捕捉
如果信号的处理是自定义的,当信号递达时就调用某个用户自定义函数,这就是信号的捕捉。信号的捕捉流程大概如下图所示:
对于我们编程来说,可以看做是有两步:
(1)定义新的信号的执行函数
handle
。(2)使用
signal/sigaction
函数,把自定义的handle
和指定的信号相关联。
【注意事项】一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗CPU
时间的事情,设计的越简单也将降低引发信号竞争条件的风险。
2. signal() 函数
2.1 函数说明
在linux
下可以使用man signal
命令查看该函数的帮助手册,大概会有两种函数声明形式,但是其实是一样的,习惯上会使用形式一。
【说明】使用的命令为man signal
或者man 2 signal
查询到的系统调用的函数声明形式。
1 | /* 需包含的头文件 */ |
【说明】使用man 3 signal
查询到的库函数中的函数声明形式。
1 | /* 需包含的头文件 */ |
点击查看两种声明的关系
我们先来看声明形式二:
1 | void (*signal(int sig, void (*func)(int)))(int); |
我们先拆分一下:
1 | /* 注意一下函数指针的形式:<数据类型> (*<函数指针名称>) (<参数说明列表>); */ |
- 由于
()
的存在,*func
是一个指针变量,后边的括号和int
说明这个指针变量可以指向一个带有int
形参的函数,前边的void
说明这个函数指针指向的函数返回值为void
类型,总的来说就是void (*func)(int)
定义了一个函数指针变量func
,它可以指向一个带有一个int
参数的返回值为void
类型的函数。可以指向的函数形式如下:
1 | void functionName(int arg); |
- 再往上一层看,这就到了
signal
了,*
的优先级是低于()
的,所以signal
先与后边的(int sig, void (*func)(int))
相结合,说说明signal
是一个函数,并且带有两个参数,一个是int
类型的变量,一个是void (*func)(int)
类型的函数指针变量。 - 再看
signal
前边的*
表示这是一个指针变量,也就是说这个signal
函数的返回值是一个指针变量。 - 接着就是最后的
(int)
,这表示,signal
函数返回的指针变量可以指向一个带有int
类型参数的函数。 - 最前边的
void
表示,signal
函数返回的指针变量可以指向一个带有int
类型参数且没有返回值的函数。
总的来说,定义了一个指针函数signal
,指针函数的返回值是一个函数指针,可以指向一个带有int
类型参数且无返回值的函数;而signal
含有两个参数,一个是普通的整型变量,另一个是函数指针类型,可以指向带有一个int
类型且无返回值的函数。
经过分析,会发现,signal
的返回值和signal
函数第二个参数的类型是一样的,他们都是函数指针,都可以指向一个带有一个int
类型参数的没有返回值的函数。前边我们使用typedef
简化过这样的定义的,所以这里,我们可以这样也做一个简化:
1 | typedef void (*pfunc)(int) |
这样,上边的函数就可以写为:
1 | pfunc signal(int sig, pfunc);/* 需要注意的是,声明只需要类型即可,不一定需要写出形参*/ |
这样是不是就跟前边形式一很像了呢?我们把pfunc
换成sighandler_t
其实就得到了形式一的声明,这完全是可以的,毕竟指针变量名只要符合标识规则即可
【函数说明】该函数可以修改进程对信号的处理方式,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作。
【函数参数】
signum
:int
类型,此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,不过一般建议使用信号名。handler
:sighandler_t
类型,是一个函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;也可以指向几个预定义函数。
点击查看 handler 可指向的预定义函数
SIG_DFL | 表示设置为系统默认操作 |
SIG_IGN | 表示设置为忽视信号 |
【返回值】sighandler_t
类型,是一个函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回SIG_ERR
,并会设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】信号处理函数声明一般如下:
1 | void handler(int sig); |
2.2 使用实例1
【说明】此例子我们将会捕捉SIGINT
信号,捕捉完成后,会执行我们自定的函数,而不会再使进程终止。这样的话我们想要结束进程,可以重开一个终端,然后输入以下命令:
1 | ps -ef|grep <filename> # 查看PID |
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | Please Enter: Ctrl+c |
2.3 使用实例2
上边的例子,我们要是想再恢复Ctrl+C
的按键功能,让它按下后进程执行默认的操作,我们该怎么来写呢?还记得signal
函数成功情况下返回什么吗?哈哈,返回值就是指向在此之前的信号处理函数,我们在自定义处理函数中将信号的处理方式还原不就好了吗。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | Please Enter: Ctrl+c |
我们按下第一次的时候,成功捕捉到信号,当我们按下第二次的时候,进程退出了。
3. sigaction() 函数
3.1 函数说明
在linux
下可以使用man sigaction
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于检查或修改与指定信号相关联的处理动作(可同时进行两种操作)。系统建议使用sigaction
函数,因为signal
在不同类UNIX
系统的行为不完全一样。sigaction()
也更具灵活性以及移植性,并且它允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制。
【函数参数】
signum
:int
类型,表示需要设置的信号,可以是除了SIGKILL
信号和SIGSTOP
信号之外的任何信号。act
:struct sigaction
类型的结构体指针变量,指向一个struct sigaction
数据结构,该数据结构描述了信号的处理方式。如果参数act
不为NULL
,则表示需要为信号设置新的处理方式;如果参数act
为NULL
,则表示无需改变信号当前的处理方式。oldact
:struct sigaction
类型的结构体指针变量,指向一个struct sigaction
数据结构。如果参数oldact
不为NULL
,则会将信号之前的处理方式等信息通过参数oldact
返回出来;如果我们不需要获取此类信息,那么可将该参数设置为NULL
。
点击查看 struct sigaction 结构体详情
使用man sigaction
的时候,显示的帮助手册中会有这个结构体成员详情。
1 | struct sigaction |
sa_handler
:指定信号处理函数,与signal()
函数的handler
参数相同。sa_sigaction
:也用于指定信号处理函数,这是一个替代的信号处理函数,这个函数指针提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过siginfo_t
参数获取;sa_handler
和sa_sigaction
是互斥的,不能同时设置,对于标准信号来说,使用sa_handler
就可以了,可通过SA_SIGINFO
标志进行选择。sa_mask
:sigset_t
类型,该参数sa_mask
定义了一组信号。
当进程在执行由sa_handler
所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套。
通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?这时候就可以通过信号掩码来实现,如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数sa_mask
来完成,信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。
sa_flags
:该参数指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志(多个标志使用位或|
组合)。
点击查看 sa_flags 常用可取的值
SA_NOCLDSTOP | 如果signum为SIGCHLD,则子进程停止时(即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU中的一种时)或恢复(即它们接收到SIGCONT)时不会收到SIGCHLD信号。 |
SA_NOCLDWAIT | 如果signum是SIGCHLD,则在子进程终止时也不会转变为僵尸进程。 |
SA_NODEFER | 不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了SA_NODEFER标志,则表示不对它进行阻塞。 |
SA_RESETHAND | 执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。 |
SA_RESTART | 被信号中断的系统调用,在信号处理完成之后将自动重新发起。 |
SA_SIGINFO | 如果设置了该标志,则表示使用sa_sigaction作为信号处理函数、而不是sa_handler。 |
sa_restorer
:该成员已过时,一般是已经不再使用了。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】关于信号处理函数的两种形式:
1 | /* 使用 sig.sa_sigaction 时*/ |
3.2 获取信号携带的信息
sigaction()
函数在act
参数也就是第二个参数的成员sa_flags
取SA_SIGINFO
的时候,可以获取信号所携带的数据(如何携带数据?后边的使用实时信号一节会有介绍),此时的自定义信号处理函数形式可以如下所示:
1 | /* act.sa_flags = SA_SIGINFO */ |
sig
:int
类型,表示接收到的信号编号。info
:siginfo_t
类型的指针变量,siginfo_t
是一个包含信号进一步信息的结构,该信息的结构体成员中有一个成员是si_value
,该成员类型为union sigval
,我们获取信号携带的数据时使用的就是这个成员。
点击查看 siginfo_t 结构体成员
使用man sigaction
的时候,显示的帮助手册中会有这个结构体成员详情。
1 | siginfo_t |
ucontext
:void *
类型,该参数指向的结构包含内核保存在用户空间堆栈上的信号上下文信息, 通常,处理程序函数不使用第三个参数。 有关详细信息,请参阅sigreturn(2)
。 有关ucontext_t
结构的更多信息可以在getcontext(3)
和signal(7)
中找到。
【注意事项】有关实例可以参考第七节的使用实时信号例子。
3.3 使用实例1
【说明】此例子我们将会捕捉SIGINT
信号,捕捉完成后,会执行我们自定的函数,而不会再使进程终止。这样的话我们想要结束进程,可以重开一个终端,然后输入以下命令:
1 | ps -ef|grep <filename> # 查看PID |
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | Please Enter: Ctrl+c |
3.4 使用实例2
上边的例子,我们要是想再恢复Ctrl+C
的按键功能,让它按下后进程执行默认的操作,我们该怎么来写呢?这个函数与signal
不同,我们可以直接通过参数设定执行完一次信号处理函数后恢复原来的默认操作,这个参数就是act
参数的sa_flags
成员。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | Please Enter: Ctrl+c |
我们按下第一次的时候,成功捕捉到信号,当我们按下第二次的时候,进程退出了。说明信号恢复了系统的默认操作。
4. 子进程回收
前边我们知道,子进程在结束的时候会向父进程发送一个SIGCHLD
信号,并且父进程若是未对已结束的子进程进行回收的话,子进程就会变成僵尸进程,上边我们学习了两个信号函数,那是不是可以通过信号来实现进程的回收呢?当然也是可以的啦。SIGCHLD
的产生会在下边三种情况下产生:
(1)子进程终止时。
(2)子进程接收到SIGSTOP
信号停止时。
(3)子进程处在停止态,接受到SIGCONT
后唤醒时。
4.1 signal() 实现进程回收
我们可以通过signal
函数来捕获SIGCHLD
信号,在信号处理函数中进行进程的回收。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | The father process is runing! |
运行后,我们可以使用下边的命令查看进程以及子进程的情况:
1 | ps -ef | grep a.out |
会发现,使用信号捕获子进程发出的信号后,完成了对子进程的回收,回收成功是这样的:
1 | 0 S hk 24361 19822 0 80 0 - 628 hrtime 05:48 pts/0 00:00:00 ./a.out |
若未回收,则子进程结束后成为僵尸进程,将会是如下状态:
1 | 0 S hk 24350 19822 0 80 0 - 628 hrtime 05:48 pts/0 00:00:00 ./a.out |
4.2 sigaction() 实现进程回收
我们还可以通过sigaction
函数来捕获SIGCHLD
信号,但是sigaction
功能很强大,我们就可以直接在参数中设置子进程结束后不成为僵尸进程。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | The father process is runing! |
运行后,我们可以使用下边的命令查看进程以及子进程的情况:
1 | ps -ef|grep a.out |
会发现,使用信号捕获子进程发出的信号后,完成了对子进程的回收,回收成功是这样的:
1 | 0 S hk 24491 19822 0 80 0 - 628 hrtime 05:59 pts/0 00:00:00 ./a.out |
若未回收,则子进程结束后成为僵尸进程,将会是如下状态:
1 | 0 S hk 24507 19822 0 80 0 - 628 hrtime 05:59 pts/0 00:00:00 ./a.out |
5. SIGABRT 信号
SIGABRT
信号通常是由abort()
函数产生的,该信号来终止调用该函数的进程,SIGABRT
信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用abort()
函数之后,内核会向进程发送SIGABRT
信号。那这个信号能被捕获嘛?我们来试一下。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | Please send signal.[i=1] |
事实证明,即便SIGABRT
信号可以被捕获,但是,它依然会使程序结束。
三、向进程发送信号
Linux
系统提供了kill()
系统调用,一个进程可通过kill()
向另一个进程发送信号;Linux
系统还提供了库函数raise()
,也可用于实现发送信号的功能。
1. kill()
1.1 函数说明
在linux
下可以使用man 2 kill
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数是一个系统调用,可将信号发送给指定的进程或进程组中的每一个进程。
【函数参数】
pid
:pid_t
类型,表示要发送信号给指定进程的PID
号。参数pid
为正数的情况下,用于指定接收此信号的进程pid
;除此之外,参数pid
也可设置为0
或-1
以及小于-1
等值。
点击查看 pid 不同取值的含义
pid > 0 | 则信号sig将发送到pid指定的进程。 |
pid = 0 | 则将sig发送到当前进程的进程组中的每个进程。 |
pid = -1 | 则将sig发送到当前进程有权发送信号的每个进程,但进程1(init)除外。 |
pid < -1 | 则将sig发送到ID为-pid的进程组中的每个进程。 |
sig
:int
类型,表示要发送的信号。也可设置为0
,如果参数sig
设置为0
则表示不发送信号,但任执行错误检查,这通常可用于检查参数pid
指定的进程是否存在。
【返回值】int
类型,成功返回0
,失败返回-1
,并会设置errno
。如果向一个不存在的进程发送信号,kill()
将会返回-1
,errno
将被设置为ESRCH
,表示进程不存在。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root
进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户ID
或有效用户ID
必须等于接收者进程的实际用户ID
或有效用户ID
。
1.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会打印提示信息,然后我们输入目标进程的PID
和要发送的信号即可:
1 | Please enter pid(this process pid is 24683) and signal: 24683 11 |
2. raise()
2.1 函数说明
在linux
下可以使用man 3 raise
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可用于发送信号给自身,其实就等价于kill(getpid(), int sig);
。
【函数参数】
sig
:int
类型,表示要发送给自身的信号。
【返回值】int
类型,成功返回非0
值,失败返回0
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会打印提示信息,然后我们输入要发送的信号即可:
1 | Please enter signal: 7 |
四、定时器产生信号
1. alarm() 函数
1.1 函数说明
在linux
下可以使用man 3 alarm
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于设置一个定时器, 在定时器超时的时候, 内核会向进程发送SIGALRM
信号,此函数也称为闹钟函数,一个进程只能有一个闹钟时间。如果不忽略或捕捉此信号, 它的默认操作是终止调用该函数的进程。
【函数参数】
seconds
:unsigned int
类型,设置定时时间,以秒为单位;如果参数seconds
等于0
,则表示取消之前设置的alarm
闹钟。
【返回值】unsigned int
类型,如果在调用alarm()
之前已经为该进程设置了alarm
闹钟,但是还没有超时,则已经设置的闹钟的剩余值作为本次alarm()
函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)参数seconds
的值是产生SIGALRM
信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,每个进程只能设置一个alarm
闹钟;虽然SIGALRM
信号的系统默认操作是终止进程。
(2)alarm
闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在SIGALRM
信号处理函数中再次调用alarm()
函数设置定时器。
1.2 使用实例1
点击查看实例1
该实例为单次触发alarm
闹钟。
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会显示如下信息:
1 | i = 1 |
1.3 使用实例2
点击查看实例2
该实例为循环触发alarm
闹钟。
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会显示以下信息:
1 | i = 1 |
2. ualarm() 函数
2.1 函数说明
在linux
下可以使用man 3 ualarm
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于设置一个循环发送SIGALRM
信号的定时器,函数会在usecs
微秒后,将SIGALRM
信号发送给进程,并且之后每隔interval
微秒再发送一次 SIGALRM
信号。
【函数参数】
usecs
:useconds_t
类型,设置定时时间,以微秒为单位,不能大于1000 000us
。interval
:useconds_t
类型,设置循环发送信号的间隔时间,以微妙为单位,不能大于1000 000us
。
【返回值】unsigned int
类型,如果在调用ualarm()
之前已经为该进程设置了ualarm
闹钟,但是还没有超时,则已经设置的闹钟的剩余值作为本次ualarm()
函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0
(第一次调用该函数也返回0
)。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】在ualarm
函数的手册中,返回值的错误信息中有这么一条:
1 | ERRORS |
这就意味着,我们的两个参数最好都不要大于1s
,否则闹钟是不会生效的。
2.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会显示如下信息:
1 | i = 1 |
3. setitimer() 函数
3.1 函数说明
在linux
下可以使用man 2 setitimer
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于设置一个定时器,可以定时发送信号。可替代alarm
函数,比alarm
函数精确度更高,可以实现周期定时。
【函数参数】
which
:int
类型,设置定时的方式,一共有三种定时方式,每种都工作在不同的时钟上,并且在定时器定时结束时产生不同的信号。
点击查看 which 取值
ITIMER_REAL | 按实际时间计时,到达时间的时候会产生SIGALRM信号 |
ITIMER_VIRTUAL | 这种方式根据进程消耗的用户模式CPU时间进行计时。(测量包括进程中所有线程消耗的CPU时间)在每次到期时,都会生成一个SIGVTALRM信号。 |
ITIMER_PROF | 根据进程消耗的总CPU时间(即用户和系统)进行倒计时,每次到期时,发送SIGPROF信号 |
new_value
:struct itimerval
类型的结构体指针变量,是传入参数,表示设定定时的时长,也就是超时时间。它有两个成员it_interval
和it_value
,分别用于设置间隔时间和定时时间,如果it_value
为0,那么定时器将不不会启动;如果计时器it_value
过期之后,it_interval
为0,那么定时器也将停止工作。
点击查看 struct itimerval 结构体说明
使用man 2 setitimer
的时候,显示的帮助手册中会有这个结构体成员详情。
1 | struct itimerval |
it_interval
:struct timeval
类型,表示定时器循环的时间间隔,即第一次计时it_value
时长发送信号,再往后的信号每隔一个it_interval
发送一次。它有两个成员,为tv_sec
和tv_usec
,用于设置时间,分别表示秒和微秒。it_value
:struct timeval
类型,表示定时器定时时长,算起来应该是第一次启动定时器的定时时间,它有两个成员,为tv_sec
和tv_usec
,分别表示秒和微秒。
old_value
:struct itimerval
类型的结构体指针变量,是传出参数,表示上一次定时剩余的时间,如果不关心上一次定时剩余时间,可以设置为NULL
。例如第一次定时10
s,但是过了6s
后,再次用setitimer
函数定时,此时第二次的计时会将第一次计时覆盖,而上一次定时的剩余时间则为4s
。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会显示如下信息:
1 | i = 1 |
4. getitimer() 函数
4.1 getitimer()
在linux
下可以使用man 2 getitimer
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于将定时器的当前值填写在curr_value
指向的结构体中,也就是获取定时器的当前值,该函数不会发送信号。
【函数参数】
which
:int
类型,定时的方式,一共有三种定时方式,每种都工作在不同的时钟上,并且在定时器定时结束时产生不同的信号。
点击查看 which 取值
ITIMER_REAL | 按实际时间计时,到达时间的时候会产生SIGALRM信号 |
ITIMER_VIRTUAL | 这种方式根据进程消耗的用户模式CPU时间进行计时。(测量包括进程中所有线程消耗的CPU时间)在每次到期时,都会生成一个SIGVTALRM信号。 |
ITIMER_PROF | 根据进程消耗的总CPU时间(即用户和系统)进行倒计时,每次到期时,发送SIGPROF信号 |
curr_value
:struct itimerval
类型的结构体指针变量,用于保存当前定时器的值。它有两个成员it_interval
和it_value
,分别用于存放已开启的定时器的间隔时间和定时时间。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 |
|
【注意事项】none
4.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会显示如下信息:
1 | [i = 1]: it_interval=2s 0us;it_value=4s 999993us. |
五、信号集与信号阻塞
在上边,当我们的信号来临的时候,马上就开始执行信号处理函数了,这个时候,我们的主进程就会被打断,但是,有的时候,主进程正在执行一些很重要的事情,不希望被打断,就像我们正在跟女朋友视频电话,但是好基友发来消息说“王者?”,啊,,,这,,,当然是女朋友更重要啦,我们可以等跟女朋友视频完毕再跟好友打游戏,好友发来的消息就像信号,我们不希望马上去执行,但是又希望这个信号不会消失,暂时忽略掉,当我们做完一些事后再去处理这个信号,这一部分就是关于这样如何实现的一些笔记啦。
1. 相关的概念
1.1 信号的状态
信号产生后有三种状态,分别是信号递达状态、信号未决和信号阻塞状态:
- 信号递达(
delivery
)
实际信号执行的处理过程(3
种状态:忽略,执行默认动作或者捕获)。
- 信号未决(
pending
)
从产生到递达之间的状态。
- 信号阻塞(
block
)
被阻塞的信号产生时将一直保持在未决状态,直到进程解除对此信号的阻塞, 才执⾏递达的动作。我们有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。这种情况就可以通过阻塞信号实现。信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。所以信号被阻塞,实际上是当该信号产生后,进程将此信号的状态保持为未决(pending
)状态,直到对该信号解除了阻塞或将该信号的动作改为忽略。
【注意事项】信号阻塞和忽略的区别:
(1)忽略是进程对信号的一种处理方式,它属于信号递达状态。而阻塞是跟信号递达同级的概念。
(2)只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
1.2 信号的存储
信号的不同状态在进程的PCB
中对应不同的表,三种状态就有三个表对应
前两张表都是位图(BitSet
)来存储的。信号被阻塞就将相应位置1
,否则置0
。而pending
表中,若置1
则表示信号存在,0
则相反。也就是说,pending
表中的数据是判断信号是否存在的唯一依据。上图中的三个信号状态说明如下:
SIGHUP
信号未阻塞也未产生过,但当它递达的时候就会执行默认处理动作。SIGINT
信号产生过,但已被阻塞。所以暂时不能递达,虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,这是因为进程仍有机会改变处理动作之后再解除阻塞。SIGQUIT
信号未产生过,一旦产⽣将被阻塞,它的处理动作是用户自定义的捕捉函数sigHandler
。
如果在进程解除对信号的阻塞之前,该信号产生过多次,将会如何处理呢?
POSIX.1
允许系统递送该信号一次或多次。Linux
是这样规定的:常规信号(1-31
)在递达之前产生多次只记一次,而实时信号(34-64
)在递达之前产生多次,并可以依次放在一个队列中。
1.3 信号集
信号未决和信号阻塞标志都可以用相同的数据结构(位图)存储,所以它们可以用同一数据类型来表示,在linux
中这个数据类型就是sigset_t
。这个结构体在哪里定义的呢?我们还是要通过locate
命令来查找一些文件的位置,
首先是signal.h
文件中有如下定义:
1 | typedef __sigset_t sigset_t; |
额,按理说应该再去找__sigset
的定义,但是回到开头一看,有这么一条语句:
1 |
于是我们立刻可以定位到bits/sigset.h
文件中,打开该文件并查找__sigset_t
的定义如下:
1 | /* A `sigset_t' has a bit for each signal. */ |
而sigset_t
就被称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。在阻塞信号集其含义是该信号是否被阻塞;在未决信号集中就代表该信号是否处于未决状态。
另外,阻塞信号集也可以叫做当前进程的**信号屏蔽字(Signal Mask)**,而这里的屏蔽我们应该理解为阻塞而不是忽略。
【注意事项】虽然信号的未决和阻塞状态都是用位图来表示的,但是不可以通过移位操作来改变信号状态,统对于信号集有特定的信号集操作函数,我们只能调用这些操作函数来改变信号状态。
2. 信号集相关操作
2.1 初始化信号集
2.1.1 sigemptyset()
2.1.1.1 函数说明
在linux
下可以使用man 3 sigemptyset
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数初始化一个信号集,使其不包含任何信号,也就是将一个信号集初始化为空。
【函数参数】
set
:sigset_t
类型指针变量,表示需要进行初始化的信号集变量。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.1.2 使用实例
暂无。
2.1.2 sigfillset()
2.1.2.1 函数说明
在linux
下可以使用man 3 sigfillset
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数初始化一个信号集为满,就是将所有信号加入该信号集。
【函数参数】
set
:sigset_t
类型指针变量,表示需要进行初始化的信号集变量。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2.2 使用实例
暂无。
2.2 向信号集添加信号
2.2.1 sigaddset()
2.2.1.1 函数说明
在linux
下可以使用man 3 sigaddset
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数向信号集中添加一个信号。
【函数参数】
set
:sigset_t
类型指针变量,表示已经初始化过的信号集变量。signum
:int
类型,表示要添加到信号集中的信号。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.2.1.2 使用实例
暂无。
2.3 从信号集删除信号
2.3.1 sigdelset()
2.3.1.1 函数说明
在linux
下可以使用man 3 sigdelset
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数从信号集中删除一个信号。
【函数参数】
set
:sigset_t
类型指针变量,表示已经初始化过的信号集变量。signum
:int
类型,表示要从信号集中删除的信号。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.3.1.2 使用实例
暂无。
2.4 测试信号是否在信号集
2.4.1 sigismember()
2.4.1.1 函数说明
在linux
下可以使用man 3 sigismember
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数检测一个信号是否在指定的信号集中。
【函数参数】
set
:sigset_t
类型指针变量,表示已经初始化过的信号集变量。signum
:int
类型,表示要需要测试的信号。
【返回值】int
类型,如果信号在信号集中,则返回1
;如果不在信号集中,则返回0
,失败则返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.4.1.2 使用实例
暂无。
3. 阻塞信号
3.1 信号掩码
Linux
内核为每一个进程维护了一个信号掩码(其实就是一个信号集,严格来说就是前边提到的信号屏蔽字或者叫阻塞信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
那么,如何将一个信号添加到信号掩码中去呢?大概有三种途径:
(1)当应用程序调用signal()
或sigaction()
函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;对于sigaction()
而言,是否会如此,需要根据sigaction()
函数是否设置了SA_NODEFER
标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
(2)使用sigaction()
函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过sa_mask
参数进行设置。
(3)使用sigprocmask()
系统调用,可以显式地向信号掩码中添加或者移除信号。
3.2 sigprocmask() 函数
3.2.1 函数说明
在linux
下可以使用man
命令查看该函数的帮助手册。大概会有两种函数声明形式,但是其实是一样的,形式二已经弃用,但是有些地方可能还是会看到,因此还是使用形式一比较好,形式二仅作了解吧。
【说明】使用的命令为man sigprocmask
或者man 2 sigprocmask
查询到的系统调用的函数声明形式。
1 | /* 需包含的头文件 */ |
【说明】使用man 3 sigprocmask
查询到的库函数中的函数声明形式。
1 | /* 需包含的头文件 */ |
不过在man
手册中有说明,该函数声明已经弃用,使用形式一即可,形式二仅作了解。
【函数说明】该函数向信号掩码中添加或者删除信号,并设定对信号掩码内的信号的处理方式(阻塞或不阻塞)。
【函数参数】
how
:int
类型,用于指定信号修改的方式,可能选择有三种。
点击查看 how 可能的取值
SIG_BLOCK | 将参数set所指向的信号集内的所有信号添加到进程的信号掩码中。 |
SIG_UNBLOCK | 将参数set指向的信号集内的所有信号从进程信号掩码中移除。 |
SIG_SETMASK | 进程信号掩码直接设置为参数set指向的信号集。 |
set
:sigset_t
类型指针变量,将参数set
指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数set
为NULL
,则表示无需对当前信号掩码作出改动。oldset
:sigset_t
类型指针变量,如果参数oldset
不为NULL
,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在oldset
所指定的信号集中;如果为NULL
则表示不获取当前的信号掩码。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.2.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | i = 0 |
我们会发现,在i
不大于4
的时候,按下Ctrl+c
按键并没有效果,这是因为我们在这段时间阻塞了信号,当i>4
的时候,循环结束了,这个时候之前收到的信号立刻执行一次信号处理函数,即便我们按下多次,也只捕获了一次,之后信号被从信号掩码中移除,后边就可以正常接收信号了。
3.3 获取处于阻塞状态的信号
3.3.1 sigpending()
3.3.1.1 函数说明
在linux
下可以使用man sigpending
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以获取进程中处于等待的信号也就是处于信号未决状态的信号,也可以说是可以获取进程的信号掩码(信号屏蔽字)。
【函数参数】
set
:sigset_t
类型,处于等待状态的信号会存放在参数set
所指向的信号集中。
【返回值】int
类型,成功返回0
,失败将返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.3.1.2 使用实例
暂无。
六、阻塞进程来等待信号
1. pause() 函数
1.1 函数说明
在linux
下可以使用man 2 pause
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止。
【函数参数】none
【返回值】int
类型,只有执行了信号处理函数并从其返回时,pause()
才返回,在这种情况,pause()
返回-1
,并且将errno
设置为EINTR
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)如果信号的默认处理动作是终止进程,则进程终止,pause函数就没有机会返回了。
(2)如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause
函数不返回。
(3)如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause
返回-1
。
(4)pause
收到的信号如果被屏蔽,那么pause
就不能被唤醒 。
1.2 使用实例
这里是一个pause
的基本使用实例。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,就会发现程序一直阻塞,直到我们按下Ctrl+c
,捕获到信号后,程序才开始运行,之后终端会有以下信息显示:
1 | ^CI catch the SIGINT![1 times] |
程序开始运行后吗,再按一次Ctrl+c
,程序便会正常终止了,而这个时候,pause()
后边的语句不再执行,pause
根本就没有进行返回,程序就结束了,这是因为Ctrl+c
后来被改回了系统默认操作,也就是直接终止进程,根本不会给pause
返回的机会。
1.3 信号驱动任务
这个例子其实是为了给下边的另一个阻塞进程等待信号的函数做铺垫。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,就会发现程序一直阻塞,直到我们按下Ctrl+c
,捕获到信号后,程序才开始运行,之后我们在程序执行期间多按几次Ctrl+c
按键,最后按ctrl+\
退出进程,然后我们就会得到以下信息:
1 | ^CI catch the signal [2] 1 times! |
观察输出信息,我们发现,我们一共按下Ctrl+c
组合键四次,并且自定义的信号处理函数也成功捕获并执行了四次,但是我们的任务函数仅仅是执行了两次,这是为什么呢?我们来分析一下,当我们在执行任务函数的过程中,时间长达3s
,这段时间内我们按下Ctrl+c
组合键,这个时候由于信号是被屏蔽的,所以任务函数并不会被打断,任务函数结束后,解除了信号的屏蔽,这个时候会直接进入我们自定义的信号处理函数,执行信号处理函数,处理完毕后,这个信号就没有了,所以后边的pause
函数并没有接收到该信号,所以会一直停留在pause
处继续等待信号的到来,才会继续回到循环开始,再执行一次任务函数。
2. sigsuspend() 函数
上边的pause()
函数的信号驱动任务例子中,信号会被提前处理,导致pause()
函数接收不到信号,想要解决这个问题,就需要将恢复信号掩码和pause()
挂起进程这两个动作封装成一个原子操作,这样信号处理函数便无法在恢复信号掩码后直接执行了,这样信号就会被pause()
接收到了。
2.1 函数说明
在linux
下可以使用man 2 sigsuspend
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数会将参数mask
所指向的信号集替换进程的信号掩码,也就是将进程的信号掩码设置为参数mask
所指向的信号集,然后挂起进程,如果捕捉到一个信号并从信号处理函数返回,sigsuspend()
返回,并将进程的信号掩码恢复成调用前的值;如果捕获的信号是mask
信号集中的成员,将不会唤醒、会继续挂起,直到有非mask
信号集中成员信号的到来。
【函数参数】
mask
:sigset_t
类型指针变量,指向一个已经初始化过的信号集。
【返回值】int
类型,始终返回-1
,并设置errno
来指示错误(通常为EINTR
),表示被信号所中断。如果调用失败,将errno
设置为EFAULT
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】调用sigsuspend()
就等价于以不可中断的方式执行以下操作:
1 | sigprocmask(SIG_SETMASK, &mask, &old_mask); |
2.2 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,就会发现程序一直阻塞,直到我们按下Ctrl+c
,捕获到信号后,程序才开始运行,之后我们在程序执行期间多按几次Ctrl+c
按键,最后按ctrl+\
退出进程,然后我们就会得到以下信息:
1 | ^CI catch the signal [2] 1 times! |
可以看到,在任务函数执行期间,发出的信号在任务函数执行完毕后被捕获,并且返回,使任务函数再次执行。这样,在任务函数执行期间到来的信号就可以使任务函数再运行一次了,但是任务函数运行期间,收到多个相同信号,信号就相当于只被捕获了一次。
七、实时信号
1. 实时信号的优势
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,且该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()
函数获取。
等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。意思就是,如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这个情况上边其实我们有遇到过就是在sigsuspend
中遇到过,只是发生在任务处理函数中,不过这与发生在信号处理函数中是一样的,在执行不可被打断的操作时期间来的信号都只会被记录一次,后期也只会被处理一次,就是这是标准信号,也就是非实时信号的缺点之一。
相对于标准信号,实时信号有如下优势:
(1)实时信号的信号可应用于用户自定义的信号数量较多,标准信号仅提供了两个信号SIGUSR1
和SIGUSR2
用于应用程序自定义使用。
(2)内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。
(3)当发送一个实时信号时,我们可以为信号指定伴随数据(一个整形数据或者指针值),然后接收信号的进程就可以在它的信号处理函数中获取这些数据。
(4)不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。也就是说,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。
2. 使用实时信号
Linux
内核定义了31
个不同的实时信号,信号编号范围为34~64
,使用SIGRTMIN
表示编号最小的实时信号,使用SIGRTMAX
表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数。
使用实时信号的过程中,需要注意的有以下两点:
(1)发送进程使用sigqueue()
系统调用向另一个进程发送实时信号以及伴随数据。
(2)接收实时信号的进程要为该信号建立一个信号处理函数,为了更便于我们的操作,我们应该选择使用sigaction
函数为信号建立处理函数,并加入SA_SIGINFO
,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction
指针指向的处理函数,而不是sa_handler
,当然也允许使用sa_handler
,但这样就无法获取到实时信号的伴随数据了。
2.1 sigqueue() 函数
2.1.1 函数说明
在linux
下可以使用man 3 sigqueue
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数是一个发送信号的系统调用,主要是针对实时信号(当然也支持标准信号),支持信号带有数据,与函数sigaction()
配合使用。
【函数参数】
pid
:pid_t
类型,指定接收信号的进程对应的pid
,后续会将信号发送给该进程。sig
:int
类型,表示需要发送的信号。与kill()
函数一样,也可将参数sig
设置为0,用于检查参数pid
所指定的进程是否存在。value
:union sigval
类型的共用体,它指定了信号的伴随数据。
点击查看 union sigval 成员
在使用man 3 sigqueue
查看使用手册的时候,下边有这个共用体的介绍。
1 | union sigval |
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
点击查看实例
1 | /* 头文件 */ |
1 | /* 头文件 */ |
1 | CC = gcc |
在终端执行以下命令编译程序:
1 | make # 生成可执行文件 |
然后,终端会有以下信息显示:
1 | 执行 ./signalSend 的终端 |
会发现,发送的三次信号并未打断正在执行的信号处理函数,并且所有的信号都保留了下来。