【在Linux世界中追寻伟大的One Piece】信号捕捉|阻塞信号

news2024/11/23 4:48:12

目录

1 -> 信号捕捉初识

2 -> 阻塞信号

2.1 -> 信号其他相关常见概念

2.2 -> 在内核中的表示

2.3 -> sigset_t

2.4 -> 信号集操作函数

2.5 -> sigprocmask

2.6 -> sigpending

3 -> 捕捉信号

3.1 -> 内核如何实现信号的捕捉

3.2 -> sigaction

3.3 -> 可重入函数

3.4 -> volatile

4 -> SIGCHLD信号


1 -> 信号捕捉初识

#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(2, handler); //信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的。
while(1);
return 0;
}
[hg@localhost code_test]$ ./sig
^Ccatch a sig : 2
^Ccatch a sig : 2
^Ccatch a sig : 2
^Ccatch a sig : 2
^\Quit (core dumped)
[hg@localhost code_test]$

模拟一下野指针异常

//默认行为
[hg@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
[hg@localhost code_test]$ ./sig
Segmentation fault (core dumped)
[hg@localhost code_test]$
//捕捉行为
[hg@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
[hg@localhost code_test]$ ./sig
[hg@localhost code_test]$ ./sig
catch a sig : 11
catch a sig : 11
catch a sig : 11

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

2 -> 阻塞信号

2.1 -> 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2.2 -> 在内核中的表示

信号在内核中的表示示意图

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

2.3 -> sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

2.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 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。

2.5 -> 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_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask = set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

2.6 -> sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用几个函数做个实验。程序如下:

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

3 -> 捕捉信号

3.1 -> 内核如何实现信号的捕捉

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

3.2 -> 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结构体。
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,这里都把sa_flags设为0,sa_sigaction是实时信号的处理函数。

3.3 -> 可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

3.4 -> volatile

[hg@localhost code_test]$ cat sig.c
#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("process quit normal\n");
return 0;
}
[hg@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c #-O2
.PHONY:clean
clean:
rm -f sig
[hg@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal

标准情况下,键入CTRL-C,2号信号被捕捉,执行自定义动作,修改flag=1, while条件不满足,退出循环,进程退出。

[hg@localhost code_test]$ cat sig.c
#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("process quit normal\n");
return 0;
}
[hg@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
[hg@localhost code_test]$ ./sig
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1

优化情况下,键入CTRL-C,2号信号被捕捉,执行自定义动作,修改flag=1 ,但是while条件依旧满足,进程继续运行。但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要volatile。

[hg@localhost code_test]$ cat sig.c
#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("process quit normal\n");
return 0;
}
[hg@localhost code_test]$ cat Makefile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
[hg@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal

volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

4 -> SIGCHLD信号

之前的进程文章用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

事实上,由于UNI 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void handler(int sig)
{
	pid_t id;
	while ((id = waitpid(-1, NULL, WNOHANG)) > 0) 
	{
		printf("wait child success: %d\n", id);
	}

	printf("child is quit! %d\n", getpid());
}

int main()
{
	signal(SIGCHLD, handler);
	pid_t cid;
	if ((cid = fork()) == 0) 
	{//child
		printf("child : %d\n", getpid());

		sleep(3);

		exit(1);
	}

	while (1) 
	{
		printf("father proc is doing some thing!\n");

		sleep(1);
	}

	return 0;
}

感谢各位大佬支持!!!

互三啦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2209530.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

以JavaScript的学习角度看Axios,并以spring boot+vue3为例具体分析实现

什么是Axios Axios 是一个基于 Promise 的 HTTP 客户端&#xff0c;用于在浏览器和 后端 中发送异步的 HTTP 请求。它功能强大、易用&#xff0c;常用于与 API 交互&#xff0c;发送 GET、POST、PUT、DELETE 等请求。 Axios 的主要特点&#xff1a; 支持 Promise Axios 基于 …

【数据结构笔记】搜索树

二叉搜索树 任一节点x的左/右子树中&#xff0c;所有非空节点均不大于&#xff08;不小于&#xff09;x 必须是所有的非空节点&#xff0c;仅左右孩子不够&#xff08;左孩子的右孩子可能很大&#xff09;一棵二叉树是二叉搜索树当且仅当中序遍历序列是单调非降序列 两棵二叉…

在电脑上免费压缩视频的 16 个视频压缩软件

正在寻找适用于 Windows 或 Mac 的最佳视频压缩器&#xff0c;让您轻松压缩 MP4、AVI、MKV、MOV 和更多类型的文件&#xff1f;无论您是通过社交媒体与朋友分享视频录制、释放手机空间&#xff0c;还是通过邮件发送长 MP4 视频&#xff0c;都必须使用付费或免费视频压缩软件来压…

2013年国赛高教杯数学建模D题公共自行车服务系统解题全过程文档及程序

2013年国赛高教杯数学建模 D题 公共自行车服务系统 公共自行车作为一种低碳、环保、节能、健康的出行方式&#xff0c;正在全国许多城市迅速推广与普及。在公共自行车服务系统中&#xff0c;自行车租赁的站点位置及各站点自行车锁桩和自行车数量的配置&#xff0c;对系统的运行…

提升邮件营销设计精准度秘诀,效率与效果实践

邮件营销通过确定目标群体、数据分析、邮件设计、测试优化、保持频率时效性及结合其他渠道实现精准营销&#xff0c;提高市场效益。ZohoCampaigns集成CRM、自动化功能和客户细分提升效果。 1、确定目标群体 精准营销的第一步是了解并确定你的目标群体。标定目标群体包括年龄、…

SpringSecirity(四)——用户退出

因为JWT是无状态的&#xff0c;去中心化的&#xff0c;在服务器端无法清除&#xff0c;服务器一旦进行颁发&#xff0c;就只能等待自动过期 才会失效&#xff0c;所以需要redis配合才能完成登录状态的记录。 实现思路&#xff1a; 登录后在redis中添加一个白名单&#xff0c;把…

信息系统运维管理方案,运维建设文档,运维平台建设方案,软件硬件中间件运维方案,信息安全管理(原件word,PPT,excel)

建设方案目录&#xff1a; 1、智慧运维系统建设背景 2、智慧运维系统建设目标 3、智慧运维系统建设内容 4、智慧运维系统建设技术 5、智慧运维系统建设流程 6、智慧运维系统建设收益 企业对运维管理的需求&#xff1a; 1、提高运维效率&#xff1a;降低运维成本&#xff0c;提高…

威纶通触摸屏与三菱FX5u之间 EtherNet/IP无线以太网通信方案

在实际系统中&#xff0c;同一个车间里分布多台PLC&#xff0c;由触摸屏集中控制。通常所有设备距离在几十米到上百米不等。在有通讯需求的时候&#xff0c;如果布线的话&#xff0c;工程量较大且不美观&#xff0c;这种情况下比较适合采用无线通信方式。本方案以威纶通触摸屏和…

Python数据分析-Scipy科学计算法

1.认识Scipy SciPy&#xff08;发音为 "Sigh Pie"&#xff09;是一个开源的 Python 算法库和数学工具包。 通常与 NumPy、Matplotlib 和 pandas 等库一起使用&#xff0c;这些库共同构成了 Python 的科学计算基础。 2.使用Scipy基本函数 2.1 引用Scipy函数 impor…

vue+echarts实现雷达图及刻度标注

文章目录 前言代码实现实现效果总结 前言 最近项目有做数据可视化 大屏 不免再次使用些echarts应用 记录下其中echarts雷达图的实现 代码实现 先上代码 <template><div class"container"><div ref"chart" style"width: 500px; heig…

【Spring AI】Java实现类似langchain的向量数据库RAG_原理与具体实践

介绍一下RAG&#xff1a; 检索增强生成&#xff08;RAG&#xff09;是一种技术&#xff0c;它结合了检索模型和生成模型来提高文本生成的质量。通过从企业私有或专有的数据源中检索相关信息&#xff0c;并将这些信息与大型语言模型相结合&#xff0c;RAG能够显著减少模型产生幻…

如何建立高质量的谷歌外链?

想做谷歌seo&#xff0c;外链是绝对绕不开的一个门槛&#xff0c;外链对网站的流量和SEO表现有很大帮助&#xff0c;正常途径想建立高质量外链需要策略和技巧&#xff0c;内容平台和博客是获取外链的好地方。在这些平台上发布文章并嵌入外链&#xff0c;不仅可以展示你的专业能…

删除链表的倒数第 N 个结点 | LeetCode-19 | 双指针 | 递归 | 栈 | 四种方法

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 这道题还可以用递归法&#xff0c;你想到了吗&#xff1f;毛毛张介绍四种方法 LeetCode链接&#xff1a;19. 删除链表的倒数第 N 个结点 1.题目描述 给你一个链表&a…

《Linux从小白到高手》综合应用篇:深入理解Linux进程调优

本篇深入介绍Linux进程调优. 1. Linux系统进程类型&#xff1a; Linux的进程可能有成千上万个&#xff1a; ‌新建状态‌&#xff1a;进程刚刚被创建&#xff0c;但尚未运行。 ‌就绪状态‌&#xff1a;进程已经准备好运行&#xff0c;等待CPU分配。 ‌运行状态‌&#xff1…

解读 AI 获客关键要素,开启营销新未来

​在当今数字化浪潮席卷的时代&#xff0c;企业获客的难度与日俱增&#xff0c;传统方式逐渐力不从心。而 AI 获客宛如一颗璀璨的新星&#xff0c;为企业带来全新的机遇。 AI 获客凭借人工智能强大的数据分析能力&#xff0c;能从海量数据中精准挖掘出目标客户。其优势显著&…

C语言题目练习2

前面我们知道了单链表的结构及其一些数据操作&#xff0c;今天我们来看看有关于单链表的题目~ 移除链表元素 移除链表元素&#xff1a; https://leetcode.cn/problems/remove-linked-list-elements/description/ 这个题目要求我们删除链表中是指定数据的结点&#xff0c;最终返…

虹科应用 | 15分钟部署CAN记录仪,节省95%成本的秘诀是什么?

欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 导读 虹科CSS的CANedge数据记录仪专为汽车和工业领域的工程师设计&#xff0c;旨在通过监控现场资产来支持研发、诊断和预测性维护。为了将这些数据转化为直观的可视化信息&#xff0c;工程师们通常依赖于Grafana仪表板…

MuMu模拟器12 KitsumeMask安装教程

这里是引用"> 在MuMu模拟器上安装KitsumeMask的时候遇到安装失败的情况。 一、下载APK安装包 如果你没有apk安装包可以通过下面的百度网盘进行下载 通过网盘分享的文件:KitsumeMask 链接: https://pan.baidu.com/s/1yeq3I6BsUD7J6uI-bnk-Vw?pwd=7n3v 提取码: 7n3v 二…

【LeetCode】动态规划—123. 买卖股票的最佳时机 III(附完整Python/C++代码)

动态规划—123. 买卖股票的最佳时机 III 题目描述前言基本思路1. 问题定义2. 理解问题和递推关系状态定义&#xff1a;状态转移公式&#xff1a;初始条件&#xff1a; 3. 解决方法动态规划方法伪代码&#xff1a; 4. 进一步优化5. 小总结 Python代码Python代码解释 C代码C代码解…

『网络游戏』代码操作数据库增删改查【22】

创建一个新的Vistual Studio案例工程 命名为SqlTest 导入MySql.dll (官网安装即可) 导入到新建工程创建Libs文件夹放里即可 浏览找到位置添加引用即可 1.增加数据 编写脚本&#xff1a;Program 运行工程 - 添加/插入完成 打开navicat查看数据库表信息 在增加数据中可以获取主…