1.信号的初认识
信号是进程之间事件异步通知的一种方式,属于软中断。通俗来说信号就是让用户或进程给其他用户和进程发生异步信息的一种方式。对于信号我们可以根据实际生活,对他有以下几点认识:1.在没有产生信号时我们就知道产生信号要怎么处理(如古代时边境产生烽火,就要集结军队)。2.当信号产生时如果进程在忙着其他工作,信号需要保存,合适时候再处理。3.信号的产生是随机的,我们无法预测,所以信号是异步发生的。我们根据生活中的信号,可以对进程中的信号做出以上认识,下面我们来深入学习Linux信号.
2.信号的产生
1.kill命令产生信号
输入kill -l,我们可以查看操作系统中的各种信号,1-31为非实时信号,32-64为实时信号。
kill 命令后接-数字对应的信号再接进程pid,就可以对指定的进程发生指定的信号。如kill -9 2276702就可以对2276702号进程发送9号信号。
2. 通过终端按键产生信号
如我们在进程运行的过程中,按下ctrl + c:向当前进程发送2号信号。 ctrl + :向当前进程发送3号信号。 ctrl + z:向当前进程发送20号信号等,我们可以通过键盘来发信号。
3.硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除 以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非 法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
4.由软件条件产生信号
如管道在写端打开,读端关闭时会产生13号信号给进程,由软件条件也会产生信号。
2.信号的保存及阻塞
1.相关概念:
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2.实现原理
task_struct 的某个指针指向三张位图block(阻塞),pending(未绝),handler(处理),其中位图中下标数字+1表示几号信号。在block位图中元素为0代表不阻塞信号,为1则要阻塞,如上图block就表示要阻塞2号信号和3号信号。pending也类似,0表示没有信号需处理,1表示有信号需处理,如上图就表示1号信号需处理。hander位图则存入对应的处理方法指针,表示对应信号的处理动作。
从上图来看,pending中每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态
3.对pending,block位图操作
1.信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);//对信号集全置0
int sigfillset(sigset_t *set);//对信号集全置1
int sigaddset (sigset_t *set, int signo);//将信号集指定的signal信号位置1
int sigdelset(sigset_t *set, int signo);//将信号集指定的signal信号位置0
int sigismember(const sigset_t *set, int signo);//判断信号是否在信号集中为1
这四个函数都是成功返回0,出错返回-1。
2.sigprocmask函数
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
oset为输出型参数,读取进程的当前信号屏蔽字通过oset参数传出进行保存。set是输入型参数,按照set更改进程的信号屏蔽字。参数how指示如何更改。下图是how的可选值及作用:
3.sigpending函数
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
#include <signal.h>
int sigpending(sigset_t *set);
4.代码讲解
这里我们模拟一种情况来使用上面的函数,加深理解。我们先屏蔽2号信号,打印pending位图,然后输入2号信号,20秒后再解除2号的屏蔽再输入2号信号,代码如下
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
void PrintSig(sigset_t &pending)
{
std::cout << "Pending bitmap: ";
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
int main()
{
// 1. 屏蔽2号信号
sigset_t block, oblock;
sigemptyset(&block);//全置零
sigemptyset(&oblock);//全置零
sigaddset(&block, 2); //此时只是对block变量进行修改没有设置进当前进程的PCB block位图中
int n = sigprocmask(SIG_SETMASK, &block, &oblock);//将block变量置进当前进程的PCB block位图中
std::cout << "block 2 signal success" << std::endl;
std::cout << "pid: " << getpid() << std::endl;
int cnt = 0;
while (true)
{
// 2. 获取进程的pending位图
sigset_t pending;
sigemptyset(&pending);
n = sigpending(&pending);
// 3. 打印pending位图中的收到的信号
PrintSig(pending);
cnt++;
// 4.20秒后解除对2号信号的屏蔽
if (cnt == 20)
{
std::cout << "解除对2号信号的屏蔽" << std::endl;
n = sigprocmask(SIG_UNBLOCK, &block, &oblock); // 2号信号会被立即递达, 默认处理是终止进程
assert(n == 0);
}
// 我还想看到pending 2号信号 1->0 : 递达二号信号!
sleep(1);
}
return 0;
}
运行结果:
当我们屏蔽2号信号再输入2号信号时, 进程不能处理2号信号,放入pending位图中等候,解除屏蔽时2号信号可以处理,进程处理2号信号退出。
4.捕捉信号
1.什么时候处理信号
进程地址空间中存在用户空间(用户进程使用的内存资源)和内核空间(操作系统使用的内存资源),用户进程默认处于用户态,此时进程无法以任何形式访问操作系统的资源。当进程因系统调用,中断,异常或重新分配到时间片陷入内核态。
2.处理信号动作自定义的信号流程
当处理信号动作自定义的信号时,由于信号处理函数在用户态,为了不影响内核必须返回用户态。处理完后再返回内核执行完流程。简略图如下:
3.信号捕捉函数sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传 出该信号原来的处理动作。act和oact指向sigaction结构体:
其中第一个成员变量是函数指针,用于执行自定义动作,第三个变量表示我们需要添加的屏蔽信号,第四个置为0,其他我们暂时不讲解。
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作。该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
需要注意的是当某个信号处理函数被调用时,系统自动将当前信号加入信号屏蔽字,等信号处理函数调用完毕后再移除,这样是为了防止信号嵌套式进行捕捉处理。