- 信号的概念:用来通知进程发生了异步事件。进程之间可以通过系统调用kill命令发送软中断信号,内核也可以因为内部事件给进程发送中断信号,通知进程发生了某个事件 ,需要注意的是信号只是用来通知进程发生了某个事件,并不给该进程传递任何数据。

- 进程对收到的各种信号有不同的处理方式,分为以下三类:
- 类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来进行处理,也就是自定义捕捉信号。
- 忽略某个信号,对该信号不做任何处理,就像没发生过一样。
- 对该信号的处理保留系统的默认值,对于大部分的信号默认处理方式是终止进程,此方式为缺省操作,进程通过系统调用signal来指定进程对某个信号的处理。
在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应其位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。
- 信号的产生
- 由软件条件产生信号:
例如给一个程序或者特定的函数添加一个闹钟的功能。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送信号SIGALRM,该信号的默认处理动作是终止当前进程,虽然该信号的默认处理动作是终止当前进程,但是我们可以像以下代码一样去捕捉信号打印一些信息可以更好的帮助我们去理解!
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <functional>
#include <vector>
using namespace std;
typedef function<void()> func;
vector<func> callbacks;
uint64_t count = 0;
void showCount()
{
cout << "final count : " << count << endl;
}
void showLog()
{
cout << "日志功能!" << endl;
}
void logUser()
{
if (fork() == 0)
{
execl("/user/bin/who", "who", nullptr);
exit(1);
}
wait(nullptr);
}
void handler(int signum)
{
cout << "进程捕捉到一个信号,正在处理中:" << signum << "pid: " << getpid() << endl;
cout<<" 终止当前进程" << endl;
}
void Getpid()
{
cout << "该进程的pid为: " << getpid() << endl;
}
// 定时器
void catchSig(int signum)
{
cout << endl;
for (auto &f : callbacks)
{
f();
}
alarm(1);
}
int main()
{
//当然我们也可以做一个类似轮回检测的功能更好的模拟信号发送的过程
//signal(SIGALRM, catchSig); // 编号为14号信号
signal(SIGALRM,handler);
alarm(1);
callbacks.push_back(showCount);
callbacks.push_back(showLog);
callbacks.push_back(logUser);
callbacks.push_back(Getpid);
// 测试一秒钟++多少次
while (true)
count++;
}
结果如预测的一样,闹钟时间到了,也如实的给该进程发送信号了,我们也将该信号捕捉了自定义了信号处理方法。
2.由硬件条件产生信号:
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令;CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
#include <iostream>
using namespace std;
int main()
{
int a = 100;
/* while (true)
{
cout << "此进程正在执行……" << endl;
a = a / 0;
} */
int *p=nullptr;
*p=100;
return 0;
}


结果如我们所预测那样,除0或者野指针异常是被当成信号处理的,而此信号的默认处理动作是终止进程!
- 阻塞信号:
- 信号其他相关概念:
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
- 注意:阻塞和忽略是不同的,只要信号被阻塞酒不会被递达,而忽略是在递达后可选择的一种处理动作!
2.信号在内核中的表示:
信号在内核中的表示示意图:

3.sigset_t:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中“有效"和“无效”的含义是该信号是否被阻塞,而在未决信号集中"有效"和“无效"的含义是该信号是否处于未决状态。
4.信号集操作函数
sigset_t类型对于每种信号用一个bit表示“"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
#include<signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signo);
int delsset(sigset_t *set,int signo);
int sigismember(const sigset_t *set,int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
- 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
- sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include<signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
返回值:成功为0否则为-1.
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字;参数how指示如何更改。如果oset和set都是非空指针则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
| SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
| SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除信号阻塞的信号,相当于mask=mask~set |
| SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
- sigpending
#include<signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
思考:
- .如果我们将2号信号进行block,并且不断打印当前进程的pending信号集,然后突然给该进程发送2号信号,那应该可以肉眼观察到有一个比特位有0置1.
- 如果我们将2号进程进行block,那这个进程是不是不会被异常终止或者用户杀掉.
- 如果我们对所有的信号都进行自定义捕捉,那这个进程是不是不会被异常终止或者用户杀掉
基于以上的问题,我们可以写相应的程序进行试验:
1.
#include <iostream>
#include <signal.h>
#include <cassert>
#include <unistd.h>
using namespace std;
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void handler(int signum)
{
cout << "捕捉到: " << signum << "信号" << endl;
}
int main()
{
// 将2号信号进行block,然后不断打印当前进程的pending信号集,接着突然向当前进程发送2号信号
// 那此时应该能够看到pending信号集中有一个比特位由0->1,然后再取消屏蔽应由1又->0
// 捕捉2号信号
signal(2, handler);
// 1、定义信号集对象
sigset_t bset, obset;
sigset_t pending;
// 2、初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3、添加需要屏蔽的信号
sigaddset(&bset, 2);
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(n == 0);
(void)n;
cout << "block 2号信号成功!!!!! "
<< "pid: " << getpid() << endl;
int count = 0;
while (true)
{
// 获取当前进程的pending信号集
sigpending(&pending);
// 显示pending信号集种没有被递达的信号
showPending(pending);
sleep(1);
}
return 0;
}
运行结果如下,当信号阻塞后将会是未决状态,对应的pending信号集也会由0置1
2.如下代码我们阻塞了所有的信号(其实9号信号是不可以被捕捉或者阻塞的)
#include <iostream>
#include <cassert>
#include <signal.h>
#include <unistd.h>
using namespace std;
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
cout << "1";
else
cout << "0";
}
cout << endl;
}
static void blockSig(int sig)
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset,sig);
int n=sigprocmask(SIG_BLOCK,&bset,nullptr);
assert(n==0);
(void)n;
}
int main()
{
//屏蔽所有信号
for(int sig=1;sig<=31;sig++)
{
blockSig(sig);
}
sigset_t pending;
while(true)
{
sigpending(&pending);//获取信号集
showPending(pending);//显示信号集
sleep(1);
}
return 0;
}
如下图运行结果所示,我们block了所有的信号,并且用xshell脚本程序不断给进程发送信号(为了方便测试,9号信号和19号信号就单独发送了),其实9号信号是不可被捕捉或者block的,19号信号的默认处理动作是忽略!


- 信号捕捉
内核如何实现信号的捕捉:
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
1812





