目录
一、信号基础
1、基本概念
信号是事件发生时对进程的通知机制,也可以把它称为软件中断
2、信号的目的是用来通信的
一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。 信号可以由“谁”发出? 以下列举情况均可产生信号:
⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程
⚫ 用于在终端下输入了能够产生信号的特殊字符。 譬如在终端上按下 CTRL + C 组合按键可以 产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程
⚫ 进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组。 当然对此是有限制的,接收信号的进程和发送信号的进程的所有者必须相同,或者发送信号的进程的所有者是 root 超级用户
⚫ 用户可以通过kill命令将信号发送给其它进程。如在终端下执行"kill -9 xxx"来杀死PID为 xxx进程
⚫ 发生了软件事件,即当检测到某种软件条件已经发生。进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等
3、信号由谁处理、怎么处理
信号通常是发送给对应的进程,当信号到达后, 该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:
⚫ 忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。需要注意SIGKILL信号和 SIGSTOP信号不能被忽略
⚫ 捕获信号。 当信号到达进程后,执行预先绑定好用户自定义的信号处理函数
⚫ 执行系统默认操作。 进程不对该信号事件作出处理,而是交由系统进行处理,需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程
4、信号是异步的
产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号
5、信号本质
信号在<signum.h>头文件中定义, 每个信号都是以 SIGxxx 开头。信号本质上是 int 类型的数字编号,内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系
二、信号的分类
1、可靠信号与不可靠信号
在 Linux 系统下使用"kill -l"命令可查看到所有信号。Linux 下的不可靠信号问题主要指的是信号可能丢失。在 Linux 下,信号值小于SIGRTMIN(34) 的信号都是不可靠信号, 这就是"不可靠信号"的来源
可靠信号支持排队,不会丢失, 同时,信号的发送和绑定也出现了新版本, 信号发送函数 sigqueue()及信号绑定函数 sigaction()
有两个信号,名分别为 SIGUSR1 和 SIGUSR2,它们被归类为“用户自定义的信号”。这里的“用户自定义”的意思是,这两个信号并没有被操作系统或者标准库定义为某种特定用途的信号,而是留给应用程序或者用户自己定义并使用的信号。
2、实时信号与非实时信号
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的, 非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。 实时信号保证了发送的多个信号都能被接收, 实时信号是 POSIX 标准的一部分,可用于应用进程。
一般把非实时信号(不可靠信号)称为标准信号
三、进程对信号的处理
Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式
signal()函数
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
signum: 指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名
handler: sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数也可以设置为 SIG_IGN 或 SIG_DFL, SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作
返回值: 此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno。
代码编写
5-8行,定义一个 信号处理函数,就打印
12-13行,用sig_t类型的变量接收signal函数返回的指针,SIGINT信号为终端中断符,通常是 CTRL + C组合键
14-18行,判断函数返回情况
19行,获取这个进程的pid
20行,死循环等待信号
通过 signal()函数将 SIGINT(2) 信号绑定到了一个用户自定的处理函数上sig_handler(int sig), 当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句。当运行程序之后,
程序会卡在 for 死循环处,此时在终端按下中断符 CTRL + C,系统便会给前台进程组中的每一个进程发送SIGINT 信号,我们测试程序便会收到该信号
验证
运行程序,再按下ctrl+c
用新的终端发送 ”kill -9 id号“命令就会杀死这个程序
注意:普通用户只能杀死该用户自己的进程,无权限杀死其它用户的进程
sigaction()函数
推荐大家使用sigaction()函数。虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报, sigaction()也更具灵活性以及移植性。
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,原型如下
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act: act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式,如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如
果参数 act 为 NULL,则表示无需改变信号当前的处理方式
oldact: oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL, 则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL
返回值: 成功返回 0;失败将返回-1,并设置 errno。
struct sigaction 结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler:指定信号处理函数,与 signal()函数的 handler 参数相同
sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取,sa_handler 和sa_sigaction 是互斥的,不能同时设置, 对于标准信号来说, 使用 sa_handler 就可以了,可通过SA_SIGINFO 标志进行选择
sa_mask: 参数 sa_mask 定义了一组信号, 当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除
当进程在执行信号处理函数期间, 可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现, 如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除
在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成,信号掩码可以避免一些信号之间的竞争状态(也称为竞态)
sa_restorer:该成员已过时,不要再使用了。
sa_flags: 参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程
代码编写
和signal()函数差不多,先实例struct sigaction 结构体为sig并将其初始化为 0,以防止变量中包含垃圾数据,sig.sa_flags
是一个标志集合,用于修改信号处理程序的行为。0
表示将所有标志都设置为默认值,即在处理信号时不使用任何附加选项
验证
效果和signal()函数一样
四、信号集
能表示多个信号(一组信号)的数据类型---信号集(signal set,信号集其实就是 sigset_t 类型数据结构
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中, Linux 系统了用于操作sigset_t 信号集的 API
1、sigset_t 信号集的 API
①初始化信号集
sigemptyset()和 sigfillset()用于初始化信号集。 sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),函数原型如下
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
set: 指向需要进行初始化的信号集变量。
返回值: 成功返回 0;失败将返回-1,并设置 errno
②向信号集中添加/删除信号
分别使用 sigaddset()和 sigdelset()函数向信号集中添加或移除一个信号,函数原型如下
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
set: 指向信号集。
signum: 需要添加/删除的信号。
返回值: 成功返回 0;失败将返回-1,并设置 errno
③测试信号是否在信号集中
使用 sigismember()函数可以测试某一个信号是否在指定的信号集中,函数原型如下
int sigismember(const sigset_t *set, int signum);
set: 指定信号集。
signum: 需要进行测试的信号。
返回值: 如果信号 signum 在信号集 set 中,则返回 1;
如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno。
其他用到的函数
④strsignal()函数获取描述信息
char *strsignal(int sig);
调用 strsignal()函数将会获取到参数 sig 指定的信号对应的描述信息,返回该描述信息字符串的指针;函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息
⑤raise()函数
进程需要向自身发送信号, raise()函数可用于实现这一要求
int raise(int sig);
sig: 需要发送的信号。
返回值: 成功返回 0;失败将返回非零值
⑥pause()函数
int pause(void);
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时, pause()才返回,在这种情况下, pause()返回-1,并且将 errno 设置为 EINTR
2、信号掩码(阻塞信号传递)
内核为每一个进程维护了一个信号掩码(其实就是一个信号集) ,即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理, 那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理
向信号掩码中添加一个信号,通常有如下几种方式:
⚫ 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中, 这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction()而言,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除
⚫ 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中, 当信号处理函数结束返回后,再将这组信号从信号掩码中移除; 通过struct sigaction 结构体的 sa_mask 参数进行设置
除了以上两种方式之外,还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号,原型如下
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: 参数 how 指定了调用函数时的一些行为。
⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言 之,将信号掩码设置为当前值与 set 的并集。
⚫ SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
⚫ SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集
set: 将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为NULL,则表示无需对当前信号掩码作出改动。
oldset: 如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信 号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码
返回值: 成功返回 0;失败将返回-1,并设置 errno
代码编写之验证信号掩码的作用
为 SIGINT 信号注册了一个处理函数 sig_handler,当进程接收到该信号之后就会执行
它;然后调用 sigprocmask 函数将 SIGINT 信号添加到信号掩码中,再调用 raise(SIGINT)向自己发送一个 SIGINT 信号,如果信号掩码没有生效、也就意味着 SIGINT 信号不会被阻塞那么调用 raise(SIGINT)之后应该就会立马执行 sig_handler 函数,从而打印出"执行信号处理函数..."字符串信息;
如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在 2 秒后才执行,因为程序中使用 sleep(2)休眠了 2 秒钟之后,才将 SIGINT 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞
验证
休眠之后27行代码就解除阻塞,信号处理函数就能执行
代码编写之sa_mask
在struct sigaction 结构体的 sa_mask
成员表示在执行信号处理函数期间需要阻塞的一组信号集。当处理一个信号时,如果 sa_mask
中设置的信号出现,则将它们加入进程的信号屏蔽字中,以防止中断信号处理程序。
如果 sa_mask
中的信号被阻塞,那么当这个处理程序正在运行时,这些信号的处理程序将不会被执行。因此,阻塞某些信号可以确保在信号处理程序运行时,不会被其他信号干扰。
在下面的示例中,SIGUSR1
信号处理程序在运行时会阻塞 SIGUSR2
信号,以确保处理程序执行时不会被另一个 SIGUSR2
信号中断
20行,定义struct sigaction 结构体变量sig
21行,初始化信号集,使其不包含任何信号
22-23行,使用默认值,绑定信号处理函数
25-29行,设置获取信号为SIGUSR1
6-15行,信号处理函数,添加阻塞掩码的是SIGUSR2信号,在处理信号的时候不允许SIGUSR2打断,所以得到SIGUSR1信号后执行信号处理函数时,SIGUSR2信号会被阻塞,直接SIGUSR1信号处理程序完成后,SIGUSR2将不再被阻塞,就可以发送和接收。
验证
可以看到,左上角用另一个终端,在开始执行的时候,迅速发出两个信号,但是只有信号1被执行,信号2就一直在信号1运行完成之后才能运行,而且运行之后就默认终止了程序
代码编写之sigsuspend()
将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作,这正是 sigsuspend()系
统调用的目的所在, sigsuspend()函数原型如下所示
int sigsuspend(const sigset_t *mask);
mask: 参数 mask 指向一个信号集。
返回值: sigsuspend()始终返回-1,并设置 errno 来指示错误(通常为 EINTR) ,表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT
sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起) 、并从信号处理函数返回,一旦从信号处理函数返回, sigsuspend()会将进程的信号掩码恢复成调用前的值
代码编写
初始化三个信号集:
new_mask
:信号掩码,用来添加需要阻塞的信号(这里SIGINT
); old_mask
:旧的信号掩码,保存原来的信号掩码,以便后面恢复;wait_mask
:等待信号集,一开始为空,后面用于sigsuspend()
函数挂起等待信号;将SIGINT
信号的处理函数设置为sig_handler
设置接收信号为SIGINT,在new信号集里面添加掩码,在添加掩码之前的原来信号掩码保存在old信号集里面,在实际的应用中,可以把需要保护的代码放在30行这个位置。这样,在调用sigprocmask()
之后,这些代码就不会被信号打断,保证了数据的完整性。
通过调用sigsuspend()
来阻塞进程,直到接收到SIGINT
信号(在键入Ctrl-C时发出)。这样做可以保护一些关键代码,以防止它们在不期望的情况下被中断。当SIGINT
信号被接收到后,进程将被唤醒,执行完后续的代码,然后退出。
验证
五、实时信号
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取
sigpending()函数
int sigpending(sigset_t *set);
set: 处于等待状态的信号会存放在参数 set 所指向的信号集中。
返回值: 成功返回 0;失败将返回-1,并设置 errno。
发送实时信号
Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64,实时信号较之于标准信号,其优势如下:
⚫ 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用: SIGUSR1 和 SIGUSR2。
⚫ 内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反, 对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。
⚫ 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。
⚫ 不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。
应用程序当中使用实时信号,需要有以下的两点要求:
⚫ 发送进程使用 sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。
⚫ 接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了
sigqueue()函数
int sigqueue(pid_t pid, int sig, const union sigval value);
pid: 指定接收信号的进程对应的 pid,将信号发送给该进程。
sig: 指定需要发送的信号。也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在。
value: 参数 value 指定了信号的伴随数据, union sigval 数据类型
返回值: 成功将返回 0;失败将返回-1,并设置 errno。
union sigval 数据类型(共用体) 如下所示:
typedef union sigval
{
int sival_int;
void *sival_ptr;
} sigval_t;
携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针
编写代码
接收信号
通过 sigaction
函数注册了一个处理实时信号的处理函数 sig_handler
,并将处理方式设置为 SA_SIGINFO,
也就是使用 sa_sigaction 作为信号处理函数。该程序通过命令行参数获取将要处理的实时信号编号。在进入无限循环后,该程序一直在调用 sleep
函数进入休眠状态。当该程序接收到被绑定的实时信号时,会调用注册的处理函数,并打印出该信号的编号和附带的数据。由于程序处于无限循环状态,因此需要手动终止该程序。
发送信号
通过调用 sigqueue
函数向指定进程发送指定信号,并附带一个整型数据。其中,该程序通过命令行参数获取待发送信号的目标进程 ID 和信号值,将数据存储在 sig_val
变量中,并使用 sigqueue
函数向目标进程发送指定信号以及附带的数据。发送成功后,程序打印出 "信号发送成功!",并退出运行。
验证
先运行接收代码,设置接收的信号为34,在终端中再运行发送代码,发送的对象是37137这个进程,信号是34,看到发送成功之后,接收这边就会打印接收的信号及其数据