[linux仓库]信号保存[进程信号·肆]
🌟 各位看官好,我是!
🌍 Linux == Linux is not Unix !
🚀 今天来学习Linux的信号保存,明白进程是如何识别信号及相关函数。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!
信号保存
对信号产生有了一定的理解后,就可以从时间维度上讲解信号保存的话题

信号相关概念
- 实际执⾏信号的处理动作称为信号递达(Delivery)
- 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。(屏蔽某个信号 --> 既然能屏蔽就能解除)
- 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
- 注意 : 阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。

信号是否被阻塞在内核中是用位图进行表示:pending位图表示是否收到信号,block位图表示信号是否被屏蔽.

进程如何识别信号
信号在内核中的表示示意图:
- 每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
- SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是用户⾃定义函数sighandler。

这表明什么呢?信号是否收到信号,是否阻塞,用什么方法进行处理.都是围绕这张表进行的啊!
结论:围绕这信号,进程能识别信号,本质是三张表:block表pending表handler表 --> 由理论转实操: 都是对这三张表的操作.
struct task_struct { ... /* signal handlers */ sigset_t blocked struct sigpending pending; ... } struct sigpending { struct list_head list; sigset_t signal; } typedef struct { unsigned long sig[_NSIG_WORDS]; } sigset_t; struct sighand_struct { atomic_t count; struct k_sigaction action[_NSIG]; // #define _NSIG 64 spinlock_t siglock; };sigset_t
从上图来看,每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, , 这个类型可以表⽰每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞, ⽽在未决信号集中“有 效”和“⽆效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的“屏蔽”。
// 用户层面设置位图 sigset_t block, oblock;sigprocmask
int sigprocmask(int how, const sigset_t *_Nullable restrict set, sigset_t *_Nullable restrict oldset);
该函数主要用来检查和修改block位图how:指示如何修改set : 输入型参数 --> 若为非空指针,更该进程的信号屏蔽字oldset : 输出型参数 --> 若为非空指针,返回老图,方便后悔操作
如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后 根据set和how参数更改信号屏蔽字。返回值:若成功则为0,若出错则为-1
假设当前信号屏蔽字为mask,下面为how参数可选值
如果调用sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。
因此,对这三张表的操作,可以通过sigprocmask对block位图进行修改,对pending可以通过进程对pending位图进行修改,sigpending函数可以修改pending位图(如下介绍),signal函数可以设置三种处理行为.
sigpending

信号集操作函数
sigset_t类型对于每种信号⽤⼀个bit表⽰“有效”或“⽆效”状态, ⾄于这个类型内部如何存储这些bit则依赖于系统实现, 从使⽤者的⻆度是不必关⼼的, 使⽤者只能调⽤以下函数来操作sigset_ t变量.
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(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.
这里提出几层理解:
- 问题1:屏蔽所有信号 ? 9号信号不可被捕捉,不可被屏蔽 -- done
- 问题2:如果解除对2号的屏蔽,我也要看到pending位图 1 -> 0 -- done
- 问题3:2号信号被递达,pending 位图由1置为0,是在递达前还是递达后置为0
为了验证问题2,这里写了一段程序:先对2号信号进行屏蔽,接着我通过kill指令向该进程发送2号信号,应该观察到pending位图由0置1,等待一段时间让该进程对2号信号解除屏蔽,此时信号会被递达,应该观察到pending位图的2号信号由1重新置为0,必要的时候可以加上自定义捕捉查看是否捕捉到2号信号.
为了验证问题3,可以这样处理,如果是递达后处理2号信号时,在自定义捕捉方法中应一直为1,如果为0则证明是递达前.
void PrintPending(sigset_t &pending) { std::cout << "[pid: " << getpid() << "] " << "sigpending list: "; // 右->左, 低->高 , 0000 0000 for (int signo = 31; signo > 0; signo--) { if (sigismember(&pending, signo)) { std::cout << "1"; } else { std::cout << "0"; } } std::cout << "\r\n"; } void handler(int signo) { std::cout << "我获取到了: " << signo << " 信号" << std::endl; // 不要让进程终止 // 在对2号信号捕捉的代码中,获取pending && 打印?? sigset_t pending; sigemptyset(&pending); // 2.1 获取当前进程的pending信号集 sigpending(&pending); // 2.2 不断打印所有的pending信号集中的信号 std::cout << "###########################" << std::endl; PrintPending(pending); std::cout << "###########################" << std::endl; } int main() { // 0.设置2号信号的处理动作,不要让他终止 signal(2, handler); // 1.屏蔽2号信号 // 1.1 用户层面设置位图 sigset_t block, oblock; sigemptyset(&block); // 进行初始化 sigemptyset(&oblock); // 是输出型,也可以不初始化 sigaddset(&block, SIGINT); // 这里的时候,我们有没有设置当前进程的信号屏蔽字?没有! // 1.2将栈上位图设置到内核的信号屏蔽字-->block图 sigprocmask(SIG_SETMASK, &block, &oblock); int cnt = 10; while (true) { sigset_t pending; sigemptyset(&pending); // 2.1获取当前进程的pending信号集 sigpending(&pending); // 2.2不断打印所有pending信号集中的信号 PrintPending(pending); // 验证取消block,看信号是否递达 cnt--; if (cnt == 0) { // 解除对2号的屏蔽 std::cout << "解除对2号的屏蔽啦!" << std::endl; // 取消block sigprocmask(SIG_SETMASK, &oblock, nullptr); } sleep(1); } return 0; } 
总结
本文介绍了Linux系统中信号保存的相关概念和操作。信号从产生到递达之间存在未决状态,进程可通过阻塞位图屏蔽信号。内核使用三张表(block、pending、handler)管理信号状态。文章详细讲解了如何通过sigprocmask函数修改block位图,以及使用sigpending、signal等函数操作信号集。通过实验程序验证了信号屏蔽、解除和递达过程中pending位图的变化,说明信号处理的核心是对这三张表的操作。关键点包括:信号递达前后pending位图的变化、9号信号的特殊性,以及如何通过编程验证信号处理流程。
