Linux信号-信号集&信号屏蔽字&捕捉信号
阻塞信号&捕捉信号
一. 阻塞信号
**
**
1. 信号的常见其他概念
实际执行信号的处理动作(3种)称为信号递达;
信号从产生到递达之间的状态,叫做信号未决;
进程可以选择阻塞某个信号;
被阻塞的信号产生时,在未决状态下运行;当进程取消对该信号的阻塞时才会进行递达操作。
请注意:阻止与放弃处理是两种不同的机制。一旦信号被阻止(即发生阻塞),就不会进行后续处理;相反,在信号成功传递后,则会采用放弃处理的方式。
2. 在内核中的表示
信号在内核中的表示示意图:

每个信号都包含两个字段分别标识阻塞状态和待办事项状态,并配有指向处理相应事件的函数指针。当一个信号被创建时,在内核管理进程中将该信号标记为待办事项状态,并持续该标记直至事件到达时将其清除。操作系统通过发送特定事件信息给进程来实现对该事件的状态更新。
根据图中所示,在当前状态下既无阻塞状态也未曾触发过 SIGHUP 信号;当其到达目标时会自动执行默认操作。曾触发过 SIGINT 信号但因当前状态处于被阻塞的状态而暂时无法到达目标;尽管其基本操作为忽略但在未解除当前阻塞状态下仍需关注该 SIGHUP 事件以确保能够及时解除禁用状态。 SIGQUIT 事件从未被激活且属于永久性禁用状态;一旦 SIGUIT 事件发生则立即陷入不可逆的状态其对应的基本操作由用户自定义函数完成。
在Linux系统中,在进程解除某特定事件(如一个定时任务)的阻塞状态时(即当该事件在多个周期内触发),其处理逻辑如下:对于常规事件(non-real-time events),它们会在到达目标执行单元前仅被触发一次;而对于实时事件(real-time events),则可以在到达目标执行单元前将所有触发实例收集到同一个队列中进行批量处理。本研究仅涉及常规事件的情形,在后续讨论中,默认所有提及的事件均为常规中断(non-real-time interrupt)。
**
**
3. 信号集
通过查看图表信息, 我们能够确定每个信号对应的标记位仅占用一个bit位, 并且其取值范围限定在0或1之间. 同样地, 在阻塞情况下的标记位也采用类似的方式进行编码. 因此建议将阻塞与未决两种状态的标记位统一使用sigset_t数据类型来进行编码. 其中 sigget_t 被定义为 信号集 的类型.
该类型用于表示每个信号的有效状态与无效状态,在未决信号集中,有效状态与无效状态标识该信号是否处于未决状态;而在阻塞信号集中,有效状态与无效状态标识该信号是否被阻塞。其中,阻塞信号集也被称作当前进程的信号屏蔽字
4. 信号集操作函数
该模块中的sigget_t数据类型用于标识信号的有效性或无效性。具体而言,在此数据类型中采用单比特位的方式来表示每个信号的有效状态。对于该类型内部的具体存储机制,则属于系统实现细节范围之内,在使用过程中无需关注其内部具体存储机制。为了与该数据类型交互并完成相关操作,请调用指定接口函数来进行sigget_t变量的操作,并无需对其中的具体数据结构进行解析。

在其中,在前四个函数中均成功返回值0,在这些函数出现错误时都会返回-1;对于最后一个sigismember函数而言,在其执行过程中若包含目标元素则会返回值1,在未包含目标元素的情况下则不会有任何元素被选中并因此不会有任何结果被输出到标准输出(stdout)。该函数同样会在出现错误时返回-1。
需要立即完成并输出结果的内容如下
5. sigprocmask
调用 sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。
(1)函数原型:

(2)参数:
how:有三个可取值(如下图,假设当前信号屏蔽字为mask)

set:指向一个信号集的指针
oldset:用于备份原来的信号屏蔽字,不想备份时可设置为NULL
1)若set为非空指针,则根据参数how更改进程的信号屏蔽字;
当oldset为非空指针时,则会使得进程当前的信号屏蔽字被oldset参数获取。
当set和oldset均存在时,则会复制原先的信号屏蔽字至oldset中。
接着将信号屏蔽字按照指定的参数进行更新。
(3)返回值:成功返回0,出错返回-1
当执行sigprocmask以释放未被处理的信号阻塞时,在该函数执行期间必须确保至少一个信号被触发处理。
6. sigpending
(1)函数原型:

(2)函数功能:读取当前进程的未决信号集,通过set参数传出
(3)参数:输出型参数,传出数据
(4)返回值:成功返回0,出错返回-1
7.利用以上所介绍函数编写代码使用:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void printsigset(sigset_t* set)//打印pending表
{
int i = 0;
for(i=1; i<=32; ++i)
{
if(sigismember(set,i))//当前信号在信号集中
putchar('1');
else//当前信号不在信号集中
putchar('0');
}
puts("");//printf("\n");
}
int main()
{
sigset_t s,p;
sigemptyset(&s);//初始化信号集
sigaddset(&s,2);//将2号信号设置为有效信号
sigprocmask(SIG_BLOCK, &s, NULL);//屏蔽2号信号
int i = 10;
while(i--)
{
sigpending(&p);//获取当前进程的未决信号集
printsigset(&p);//打印未决信号集
if(i==7)
{
raise(2);//向本进程发送2号信号
}
if(i == 5)
{
sigprocmask(SIG_UNBLOCK, &s, NULL);//解除对2号信号的屏蔽
printf("recober block bitmap\n");
}
sleep(1);
}
return 0;
}
代码解读
运行结果为:

可以看出,在启动过程中未决信号集中不存在有效信号。随后,在3秒钟后发送了一个带有编号2的事件(signal)。这时,在对应的位置上将二进制位更改为1(...),这表明该进程中拥有事件编号为2的事件(signal)。接着,在第5秒钟时取消了对于事件编号2的屏蔽(blocking),这导致在第3秒钟发出的事件编号2被传递到了该进程中(process)。由于其默认处理行为是终止该进程(process),因此导致该进程中立即停止运行。
**
**
二. 捕捉信号
1. 内核如何实现对信号的捕捉
如前所述,在信息 previously 的介绍中可知, 当处理动作由自定义函数执行时, 在触发该函数被调用的情况下, 这一行为被称为捕获机制. 内核的具体捕获流程通过以下图示展示了, 其中包含四次状态转换.

说明:信号处理函数分别占用独立的堆栈空间,并未与其他主函数形成相互调用或被调用的情况,在系统层面形成了两个各自独立的操作流程。
2. signal函数
(1)函数原型

(2)函数功能: 修改signum信号的处理动作为handler指向的函数
(3)参数:
signum:表示要捕捉的信号序号
handler用于表示只有在signum发生且未被阻塞的情况下,在该信号被捕获时会触发该回调函数执行。该回调函数设计中包含一个整型参数以指定需要处理的特定信号类型。
(4)代码实现:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int num)//捕捉信号
{
printf("signo is %d\n",num);
return;
}
int main()
{
signal(2,handler);//捕捉2号信号
signal(3,handler);//捕捉3号信号
while(1)
{
printf("this is youngmay\n");
sleep(1);
}
return 0;
}
代码解读
运行结果:

可以看到,在将2号和3号信号发送至进程中时,这些信号均被捕获。然而,在发送给进程的20号信号未被捕获的情况下,则会触发其默认处理流程,并导致进程暂停。
3. sigaction函数
(1)函数原型

(2)函数功能:获取并更新与其相关的处理指令。 当系统在其执行过程中接收到同一信号时, 会暂时阻止该信号的处理, 直至操作完成后再解除阻止状态
(3)参数:
signum:指定信号的编号
act:若非空,则根据它修改该信号的处理动作
oldact:若非空,则通过它传出该信号原来的处理动作
说明:act、oldact指针指向sigaction结构体,sigaction结构体如下:
struct sigaction {
void (*sa_handler)(int);//信号的处理动作
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;//当正在执行信号处理动作时,希望屏蔽的信号。当处理结束后,自动解除屏蔽
int sa_flags;//一般为0
void (*sa_restorer)(void);
};
代码解读
(4)返回值:成功返回0,出错返回-1
(5)说明
将sahandler的数值设置为常数SIGIGN并传递给sigsction以表明应忽略该信号;设置其数值为常数SIG_DFL以指示系统应按照默认流程处理该事件;同时可将其配置为指向一个自定义的函数指针,这相当于在内核层面注册了针对该事件的专门处理程序。
(6)代码实现:
代码解读
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int num)
{
printf("signo is %d\n",num);
return;
}
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;//将信号处理动作设置为信号捕捉
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);//屏蔽3号信号
sigaction(2,&act,&oact);//自定义2号信号的处理动作
sigaction(3,&act,&oact);//自定义3号信号的处理动作
int i = 10;
while(i--)
{
printf("this is youngmay\n");
if(i == 5)
{
sigaction(2,&oact,NULL);//恢复2号信号的默认处理动作
oact.sa_handler = SIG_IGN;//将3号信号的处理动作设为忽略
sigaction(3,&oact,NULL);
}
sleep(1);
}
return 0;
}
代码解读
运行结果:

可以观察到,在最初的5秒钟内(即前5秒),我们向该进程发送了2#和3#信号,并触发了自定义处理逻辑;在此期间的后续操作中(即在5秒后),由于3#信号的处理逻辑被设置为不响应任何事件而被跳过,并将2#信号的处理逻辑设为主动执行相应操作。因此,在随后向该进程发送3#信号时会没有任何反馈;然而,在随后向该进程发送2#信号时则会触发其预设的默认响应逻辑从而直接终止整个运行过程。
4. pause函数
(1)函数原型:

(2)函数功能:使进程挂起等待直到有信号递达。
(3)返回值: 只有出错的返回值,返回-1
当程序接收一个中断事件并决定对该事件进行捕获时,在执行完相关的操作后 pause 函数会返回 -1 并设置 errno 为 EINTR 表示被中断;如果程序选择将中断事件忽略则当前的操作将不会得到恢复执行;而当程序选择终止当前的操作时 系统将会立即停止当前的操作以供后续的操作使用
(4)利用pause函数实现sleep
我们旨在实现让进程在指定时间内暂停。具体而言,可以通过以下方式实现:首先将进程置于等待状态,在预定的时间后发送相应的信号以触发递达事件。为了确保pause函数返回-1值(其中关键在于确保信号处理机制能够捕获所发送的信号),我们需要调用alarm函数来设置一个定时闹钟。为此需要调用alarm函数来设置一个定时闹钟,并在预定的时间后向进程发送SIGALRM信号以触发递达事件的发生
1)将SIGALRM信号的处理动作设置为自定义动作;
2)调用alarm函数设置闹钟;
3)调用pause函数等待;
alarm(0)清除闹铃,在(3)中pause可能导致其他信号唤醒,因此必须清除闹铃以便返回当前的秒数。
5)恢复SIGALRM信号的原有处理动作;
6)返回剩余的秒数;
实现代码如下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int num)
{
;
}
unsigned int mysleep(unsigned int t)
{
struct sigaction act,oact;//act为要设置闹钟信号的相关信息,oact保存闹钟信号>的原有相关信息
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);//处理闹钟信号时,不屏蔽其他信号
sigaction(SIGALRM,&act,&oact);//捕捉闹钟信号
alarm(t);//t秒之后向进程发送闹钟信号
pause();//使进程挂起等待
int ret = alarm(0);//取消闹钟,返回闹钟剩余秒数
sigaction(SIGALRM,&oact,NULL);//恢复闹钟的默认处理动作
return ret;//返回闹钟剩下的时间
}
int main()
{
while(1)
{
printf("hi,i am youngmay\n");
mysleep(1);
}
return 0;
}
代码解读
运行结果为:

程序运行后每秒输出一条信息,并表明我们自己实习的mysleep函数实现了sleep函数的作用。
为了验证mysleep函数的返回值是否正确,请确认mysleep函数返回的结果是否等于闹钟剩余的时间(以秒为单位)。现将修改后的代码示例如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int num)
{
printf("signo is %d\n",num);
return;
}
unsigned int mysleep(unsigned int t)
{
struct sigaction act,oact;//act为要设置闹钟信号的相关信息,oact保存闹钟信号>的原有相关信息
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);//处理闹钟信号时,不屏蔽其他信号
sigaction(SIGALRM,&act,&oact);//捕捉闹钟信号
alarm(t);//t秒之后向进程发送闹钟信号
pause();//使进程挂起等待
int ret = alarm(0);//取消闹钟,返回闹钟剩余秒数
sigaction(SIGALRM,&oact,NULL);//恢复闹钟的默认处理动作
return ret;//返回闹钟剩下的时间
}
int main()
{
signal(2,handler);//将2号信号的处理动作改为自定义函数
printf("hello\n");
unsigned int ret = mysleep(20);
printf("return ret is %d\n",ret);
return 0;
}
代码解读
运行结果如下:

当程序运行超过两秒时,通过键盘向进程发送Ctrl+C指令,并观察到mysleep函数返回值为18(表示闹钟剩余时间)。
说明:
在mysleep程序中编写针对SIGALRM事件的自定义处理函数旨在实现特定的功能:当 SIGALRM 事件触发时能够控制进程的状态变化。若未编写该中断处理函数,则系统将默认对该信号采取终止进程的操作,在几秒后会导致进程立即终止而非恢复执行后续代码段落。值得注意的是,在睡眠操作中并未采取任何措施维持当前状态:睡眠机制本身并不执行任何操作以保持进程的停滞状态。
(2)mysleep中对SIGALRM信号执行原有处理流程,在mysleep结束后若再次发送该信号本意是为了让进程退出。然而由于SIGALRM信号属于自定义的处理逻辑,在此情况下无法实现预期的效果。我们可以将其理解为:类似于借用他人物品,在使用完后需归还给原主人以避免干扰。
(3)mysleep函数的返回值与sleep函数的返回值作用一致。
