目录
一、信号的基本概念
信号是软件中断的一种形式,它允许程序异步地处理事件。当一个信号被发送到进程时,操作系统会中断该进程的正常执行流程,并转而去执行与该信号相关的处理程序。
在信号处理中,有几个关键概念需要明确:
-
信号递达(Delivery) :实际执行信号处理动作的时刻,就像信件最终被收件人签收。
-
信号未决(Pending) :信号从产生到递达之间的状态,此时信号就像已发出但还未被接收的信件,处于等待处理的状态。
-
信号阻塞(Block) :进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。阻塞和忽略是不同的概念,阻塞是让信号暂时无法递达,而忽略是在信号递达后选择不进行处理。
二、信号在内核中的表示
在内核中,每个信号都有三个关键元素来描述其状态和处理方式:
-
阻塞标志位(Block) :表示该信号是否被阻塞。
-
未决标志位(Pending) :表示该信号是否处于未决状态。
-
处理动作(Handler) :一个函数指针,指向该信号递达时的处理动作,可以是默认动作、忽略或者用户自定义的处理函数。
信号在内核中的表示示意图:
🍇举个栗子:
假设我们有三个信号:SIGHUP、SIGINT 和 SIGQUIT。SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作;SIGINT 信号产生过但正在被阻塞,所以暂时不能递达,虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号;SIGQUIT 信号未产生过,一旦产生将被阻塞,它的处理动作是用户自定义函数 sighandler。
三、信号集(sigset_t)与操作函数
为了方便对信号进行批量操作,Linux 提供了信号集(sigset_t)的概念。信号集是一种数据结构,用于表示一组信号的状态,每个信号在信号集中用一个比特位来表示其 “有效” 或 “无效” 状态。
1. 信号集操作函数
-
sigemptyset :初始化信号集,将所有信号的对应比特位清零,表示该信号集不包含任何有效信号。
-
sigfillset :初始化信号集,将所有信号的对应比特位置位,表示该信号集的有效信号包括系统支持的所有信号。
-
sigaddset :在已初始化的信号集中添加指定信号,将其对应比特位置位。
-
sigdelset :在已初始化的信号集中删除指定信号,将其对应比特位清零。
-
sigismember :判断一个信号是否在指定信号集中,返回值为 1 表示包含,0 表示不包含,-1 表示出错。
2. 示例代码
#include <stdio.h>
#include <signal.h>
int main()
{
sigset_t s; // 定义一个信号集变量
sigemptyset(&s); // 初始化信号集,清空所有信号
sigfillset(&s); // 填充信号集,包含所有信号
sigaddset(&s, SIGINT); // 添加 SIGINT 信号到信号集
sigdelset(&s, SIGINT); // 从信号集删除 SIGINT 信号
int result = sigismember(&s, SIGINT); // 判断 SIGINT 是否在信号集中
if (result == 1)
printf("SIGINT is in the set\n");
else if (result == 0)
printf("SIGINT is not in the set\n");
else
printf("Error occurred\n");
return 0;
}
四、信号屏蔽与未决信号集操作
1. sigprocmask 函数
sigprocmask 函数用于读取或更改进程的信号屏蔽字(阻塞信号集)。通过该函数,我们可以控制哪些信号被阻塞,从而影响信号的递达行为。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
-
how 参数 :指定如何更改信号屏蔽字,可选值为 SIG_BLOCK(将 set 中的信号添加到当前屏蔽字)、SIG_UNBLOCK(将 set 中的信号从当前屏蔽字解除)和 SIG_SETMASK(将当前屏蔽字设置为 set 所指向的值)。
-
set 参数 :指向要设置的新信号集。
-
oset 参数 :用于保存原来的信号屏蔽字。
2. sigpending 函数
sigpending 函数用于读取进程的未决信号集,帮助我们了解哪些信号已经产生但还未被递达。
#include <signal.h>
int sigpending(sigset_t *set);
3. 示例代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 打印未决信号集
void printPending(sigset_t *pending)
{
int i = 1;
for (i = 1; i <= 31; i++)
{
if (sigismember(pending, i))
{
printf("1 ");
}
else
{
printf("0 ");
}
}
printf("\n");
}
// 信号处理函数
void handler(int signo)
{
printf("handler signo:%d\n", signo);
}
int main()
{
// 设置信号处理函数
signal(SIGINT, handler);
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
// 添加 SIGINT 信号到信号集
sigaddset(&set, SIGINT);
// 设置信号屏蔽字,阻塞 SIGINT 信号
sigprocmask(SIG_SETMASK, &set, &oset);
sigset_t pending;
sigemptyset(&pending);
int count = 0;
while (1)
{
// 获取未决信号集
sigpending(&pending);
// 打印未决信号集
printPending(&pending);
sleep(1);
count++;
if (count == 7)
{
// 恢复原来的信号屏蔽字
sigprocmask(SIG_SETMASK, &oset, NULL);
printf("恢复信号屏蔽字\n");
}
}
return 0;
}
运行该程序后,会出现以下情况:
1、初始阶段(前7秒):
- 程序阻塞
SIGINT
(Ctrl+C 触发的信号)。此时按下 Ctrl+C,信号会被标记为未决状态,但不会立即处理。 - 每秒钟打印一次未决信号集,
SIGINT
(对应第2位)会显示为1
(若在此期间按过 Ctrl+C)。例如:
2、第7秒后:
- 恢复原始信号屏蔽字(解除对
SIGINT
的阻塞)。 - 若之前有未决的
SIGINT
,此时会立即触发handler
函数,打印handler signo:2
。 - 后续的 Ctrl+C 会直接触发
handler
,未决信号集中SIGINT
位通常显示0
(除非信号在处理期间再次被发送)。
3、信号处理细节:
- 多次快速按下 Ctrl+C:在阻塞期间,多次信号会被合并,解除阻塞后仅处理一次。
- 解除阻塞后:
handler
执行期间,系统默认会暂时阻塞同一信号,因此快速连续按下可能仅触发一次处理。