【Linux】信号的理解以及信号集处理函数的使用
信号产生方式
- 通过终端操作指令执行操作。 按下Ctrl+C键终止进程。
- 借助系统提供的信号发送机制向进程发送信号。 使用
kill()函数来终止指定进程。 - 当软件触发特定条件时会发送信号。 可以使用
alarm()函数设置为定时提醒。 - 发生硬件错误会导致程序崩溃或异常处理失败的情况即硬件异常。
利用kill()函数实现自己的kill 命令
// 发送信号给进程
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
代码解读
先写一个死循环程序test并在后台跑起来
//test.c
#include <stdio.h>
int main()
{
while(1);
return 0;
}
代码解读
这是利用kill 函数实现的mykill:
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main( int argc, char** argv)
{
if(argc != 1)
{
printf("parameter error.\n");
exit(1);
}
// 获取pid
pid_t pid = atoi(argv[1]);
kill(pid, SIGKILL);
return 0;
}
代码解读

然后执行jobs ,可以看到后台作业test已经存在。

此时执行mykill 并 输入test程序的pid

在初次运行jobs时会观察到test任务的状态已由Running变为Killed;再次运行则会发现该测试任务已被终止。
认识alarm() 函数
#include <unistd.h>
unsigned int alarm( unsigned int seconds);
代码解读
该alarm函数能够配置定时器,在指定秒数后向内核指定 SIGALRM 信号。此信号采用的标准处理方式为终止当前进程。其返回值可能为零或上一次定时器剩余的时间。
例如,在设置闹钟时指定其时长为10秒;随后调用alarm函数并传递参数0以指示想要取消当前设置;如果函数返回值为零,则表明该闹钟将在指定时间后的某个时刻发出响铃;若返回值大于零,则表示该闹铃会提前响起。
以下是一个基于alarm函数的示例代码,在1秒内计算可执行的++操作次数,并在每次完成操作后打印当前值。
#include <stdio.h>
#include <unistd.h>
int main()
{
int count = 0;
alarm(1);
while(1)
{
count++;
printf("count is %d .\n", count);
}
return 0;
}
代码解读
信号的处理方式
当一个进程接收到一个信号时,它有三种不同的处理方式:
- 忽略此信号
- 执行预设的行为
- 创建一个信号处理函数,在内核切换到用户态后执行特定操作(这一机制也被称为捕获机制)。
一个捕捉信号的小例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigact(int num)
{
printf("\n %d号 信号 被我吃了. \n", num);
}
int main()
{
printf("catch start ... \n");
signal(SIGINT, sigact); // 捕捉 SIGINT 信号,提供自定义动作
while(1)
{
sleep(1);
printf("你杀不掉我 hhh \n");
}
return 0;
}
代码解读

在代码中, 我们捕获了SIGINT中断, 即第2个控制中心(使用kill -l可查看所有中断列表)。当按下Ctrl+C时, 系统将该中断发送给当前正在执行的任务。发现该进程每隔一秒输出一行'你杀不掉我 hhh'字符, 尝试按下Ctrl+C仍无 avail, 因为我们为该进程提供了第2个控制中心的自定义函数('被吃掉'),以捕获此中断。
信号在内存中的表示
我们探讨了影响信号的各种因素;具体地,在执行这一动作时被称为递达(Delivery);在这一过程中所处的状态则被称为未决(Pending);相应的进程可以通过设置其行为来实现对特定信号的阻塞。
当一个进程等待某个特定的事件时,在事件发生并导致进程被阻塞后,在该事件的状态会被标记为未处理状态。只有在释放该事件的 blocked 状态之后才会触发处理事件的动作。
在本节中需要特别注意的是,在本系统中所定义的“信号阻塞”与“信号忽略”具有显著区别。值得注意的是,在接收特定事件之后才会触发的操作属于“信号忽略”,而对于“信号阻塞”而言,在其被解除之前必须满足一定条件才能完成传递。
我们知道在系统中运行的所有进程都具有一个PCB。每个进程对应的信号信息会被操作系统记录在其PCB上。在task_struct结构体中专门设置了几个字段来记录进程当前是否有待处理的信号以及当前需要阻塞的信号,并且还存储了相应的处理函数。下面用一张图来阐述它们之间的关系:

我们可以这样理解:将其视为在PCB中重要的组成部分来看待的话,则有三个关键表格分别为block table, pending table, handler table。
block标识位为1表示该信号已被封锁。当有新的信号产生时无法抵达,并处于未决状态。pending表用于记录尚未处理的信号。handler 根据每个信号的情况进行处理,并支持预设的默认和忽略功能,并通过指针指向用户提供的处理函数。
一大波信号集处理函数袭来
从上表可以看出,在阻塞与未决的状态下, 每个信号只需对应一个bit位即可实现; 其中1代表有效状态而0代表无效状态; 因此系统支持使用sigset_t来存储这些阻塞与未决的状态信息.
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
代码解读
这两个函数负责将信号机设置为全0或全1的状态,在执行任何操作前必须确保执行相应的初始化操作以使信号机达到预定状态。
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
代码解读
上面两个函数用来给,指定信号集,添加或删除signum信号。
int sigismember(sigset_t *set, int );
代码解读
用来判断信号集中是否有该信号,有则返回1,无返回0,执行失败返回-1。
sigprocmask函数
用来读取或者更改进程的信号屏蔽集。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
代码解读
如果 set 为空而 oldset 不为空,则会导致进程原有信号屏蔽集被释放;
如果 set 不为空而 oldset 为空,则会按照 how 参数的要求修改进程的信号屏蔽集;
如果两个指针均不为空,则首先备份原信号屏蔽项到 oldset 接着根据 how参数进行修改。
how参数有如下函数
| 值 | 含义 |
|---|---|
| SIG_BLOCK | 将当前进程屏蔽字mask 增加我们希望通过参数set的屏蔽信号,相当于mask与set执行按位或 |
| SIG_UNBLOCK | 将当前进程屏蔽字mask 删除我们希望通过参数set解除屏蔽的信号 |
| SIG_SETMASK | 设置当前信号屏蔽字为set,相当于 mask = set |
sigpending
用来读取当前进程的未决信号集。
#include <signal.h>
int sigpending(sigset_t *set);
代码解读
下面运用上面介绍的信号集函数写一个小实验。
每当程序运行时,在每一秒内都会输出一个尚未处理的信号集合。这个集合最初全部是零值。当收到ctrl-c事件时会使得该信号暂时未能被处理。
#include <stdio.h>
#include <signal.h>
// 打印信号集
void printsigset(const sigset_t *set)
{
int i = 0;
for(; i<32; ++i)
{
if(sigismember(set, i) == 1)
printf("1");
else
printf("0");
}
printf("\n");
}
int main()
{
sigset_t s;
sigemptyset(&s); // 初始化
sigaddset(&s, SIGINT);
sigprocmask(SIG_BLOCK, &s, NULL );
while(1)
{
sigpending(&s);
printsigset(&s);
sleep(1);
}
return 0;
}
代码解读

