1. 信号的保存
概念与原因
操作系统将信号发送给进程后,进程可能不会立即处理该信号,所以我们要将其保存下来以便让进程之后能执行相应的信号。
实际执行信号的处理动作称为信号递达(即已经处理)。
信号从产生到递达之间的状态称为信号未决(pending)(即还未被处理)。
进程可以选择阻塞(Block)某个信号。
被阻塞的信号产生时将保持在未决状态(不会被处理),直到进程解除对此信号的阻塞,才能执行抵达的动作。
阻塞和忽略是不同的,只要信号阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
在程序中信号的相关信息会记录在三张表中(block、pending、handler):
在block表(记录信号阻塞状态):
1表示 "该信号被阻塞",0表示 "不阻塞"。pending表(记录的未决情况):
1表示 "该信号已产生但未处理(因被阻塞)",0表示 "未产生或已处理"handler(记录的是信号的处理方法):
SIG_DFL:默认处理(如终止进程、忽略等)SIG_IGN:忽略该信号- 自定义函数指针:执行用户指定的逻辑
信号在内核中的表示如下所示

每个信号都有两个标志位分别表示,阻塞(block)和未决(pending),一个函数指针表示处理动作。信号刚产生时,内核在进程控制块设置该信号未决标志,到信号递达后清除该标志。
在上图中SIGHUP未阻塞所以递达时执行默认处理动作。
SIGINT信号产生过正在被阻塞,所以不能递达尽管处理方式是忽略得先接触阻塞才能忽略这个信号(因为进程有机会改变处理动作后接触阻塞)。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号就会被阻塞,处理动作是自定义函数sighandler。
由于一个信号只有一个bit位的未决标志,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞可以用相同的数据类型sigset_t来存储,sigset_t称为信号集。此类型可以表示每个信号的无效或有效状态,阻塞信号集中这两个状态是指该信号是否被阻塞,未决信号集中,有效无效的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。
信号集与信号集操作函数
信号集
sigset_t 信号集,也叫做信号屏蔽字,是操作系统给用户提供的一种数据类型,用来描述和block、pending一样的位图,其结构具体如下:
cpp
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;
信号集操作函数
操作系统给我们提供了信号集操作函数来修改信号集,我们也只能通过这些信号集操作函数来修改信号集
cpp
#include <signal.h>
int sigemptyset(sigset_t *set); // 将位图全部设置为 0
int sigfillset(sigset_t *set); // 将位图全部都设置为 1
int sigaddset (sigset_t *set, int signo); // 将位图中的某一位设置为 1
int sigdelset(sigset_t *set, int signo); // 将位图中的某一位设置为 0
int sigismember(const sigset_t *set, int signo); // 判断一个信号是否在信号集中,不在返回0,在返回1,出错返回-1
对我们自己定义的变量进行修改,并不会对我们的内核数据产生任何影响,要修改内核中的数据可以通过以下函数:
sigprocmask
通过sigprocmask函数读取或修改阻塞信号集(block),其具体用法如下:
函数原型:
cppint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);函数参数:
oldset:若此参数是非空指针,则读取进程当前的信号屏蔽字、然后通过oldset参数传出。set:若此参数是非空指针,更改进程的信号屏蔽字,由how参数来决定如何更改
若 oldset 和 set 都是非空指针,则将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。
若信号屏蔽字为mask,how参数可用以下选项
|-------------|----------------------------------------------------|
| 选项 | 含义 |
| SIG_BLOCK | 添加阻塞。set包含了我们想添加到当前信号屏蔽字里的信号,新的mask-旧的mask∪set |
| SIG_UNBLOCK | 接触阻塞。新mask=旧mask&~set |
| SIG_SETMASK | 完全替换。新mask=set |返回值:
调用成功返回0,出错则返回-1.
sigpending
可以通过sigpending函数修改未决信号集pending()
cpp#include <signal.h> int sigpending(sigset_t *set); //读取当前进程的未决信号集,通过set参数传出。 //调⽤成功则返回0,出错则返回-1
综合使用上面函数来实验一下
cpp
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
//输出pending表
void PrintPending(sigset_t &pending)
{
//sigismember 是 POSIX 信号处理相关的一个函数,用于检查某个特定的信号是否在一个信号集
for(int sigi=31;sigi>0;sigi--)
{
if(sigismember(&pending,sigi))//查看sigi这个信号是否在信号集
std::cout<<1;
else
std::cout<<0;
}
std::cout<<"\n";
}
//
void handler(int signo)
{
std::cout<<signo<<"号新号递达"<<std::endl;
sigset_t pending;
sigpending(&pending);//获取当前进程pending
PrintPending(pending);//输出
}
int main()
{
signal(2,handler);//捕捉2号信号
//屏蔽2号信号,等会再解除屏蔽看pending表变化
sigset_t block_set,old_set;
sigemptyset(&block_set);//初始化信号集
sigemptyset(&old_set);
sigaddset(&block_set,SIGINT);//修改信号集的信息
//真正修改进程的block表
sigprocmask(SIG_BLOCK,&block_set,&old_set);
while(1)
{
int sigi;
std::cin>>sigi;
raise(sigi);
sigset_t pending;
sigpending(&pending);//获取当前进程pending
PrintPending(pending);//输出
sleep(10);
std::cout<<"解除屏蔽:"<<std::endl;
sigprocmask(SIG_SETMASK,&old_set,&block_set);//old_set覆盖&block_set
}
return 0;
}
实验结果为

2. 捕捉信号

如果信号的处理动作是用户自定义函数,信号递达时就调用此信号,这就是捕捉信号。
用户程序注册了SIGQUIT信号的处理函数sighandler
当前正在执行main函数,此时发生中断或异常切换到内核态
中断处理完毕后返回用户态的main函数之前检查到有信号SIGQUIT递达
内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,5. sighandler和main函数使用不同的堆栈空间,它们之间没有调用和被调用的关系,是两个独立的控制流程。
sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态
如果没有新的信号要递达这次返回用户态就是会返回main函数上下文继续执行了
如果没有自定义处理函数时,信号处理完全在内核态完成,不需要额外切换到用户态执行函数。

当用户态程序需要请求内核服务(比如读写文件、申请内存),或发生异常 / 中断时:
- 从 "用户态" 的红色节点,切换到 "内核态"(向下的箭头)。
- 内核处理请求时,会检查 pending 表(记录待处理的中断 / 事件),处理完后再从 "内核态" 切回 "用户态"(向上的箭头)。
用户态与内核态的切换是 "双向且受内核管控" 的 ------ 用户态程序不能主动切换到内核态,必须通过系统调用、中断、异常等 "合法入口" 触发,由内核完成状态切换与权限校验
3. 不可重入函数

main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的 时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换到 sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的 两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续 往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后向 链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
像上面这样insert函数被不同的控制流程调用,可能在第一次调用还没返回时就再次进入该函数,这称之为重入,insert函数访问一个全局链表,可能因为重入而造成混乱,这样的函数称为不可重入函数。
如果一个函数只访问自己的局部变量或参数则称为可重入函数
一个函数符合下面条件之一则是不可重入的
调用malloc或free,因为malloc也是用全局链表来管理堆的
调用了标准I/O库函数,,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
4. volitile
cpp
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag)
printf("test \n");
}
上面代码信号被捕捉后执行handler函数,循环依旧会执行,while检查的flag不是内存中最新的flag,而是被优化放到寄存器中这就需要volatile关键字
cpp
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag)
printf("test \n");
}
volatile作⽤:保持内存的可⻅性,告知编译器,被该关键字修饰的变量,不允许被优化,对该 变量的任何操作,都必须在真实的内存中进⾏操作
5. SIGCHLD信号
子进程在终止时会给父进程发SIGCHLD信号,此信号默认动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作不必关心子进程了(父进程阻塞等待子进程与轮询子进程是否结束,第一种无法处理自己工作,第二种在处理自己工作时还要是不是轮询一下),等待子进程终止父进程在信号处理函数中调用wait来处理子进程即可。
Linux中父进程还可以调用sigaction将SIGCHLD的处理动作都置为SIG_IGN,这样fork出来的子进程在终止后会自动清理不会产生僵尸进程也不会通知父进程。
系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是个特例。
此方法对Linux可用不保证在其他UIX系统上都可用
这篇就到这里啦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤
(づ ̄3 ̄)づ╭❤~