您的位置:首页 > 新闻 > 热点要闻 > 丹东东港_域名一定要备案吗_为什么中国禁止谷歌浏览器_厦门网站seo

丹东东港_域名一定要备案吗_为什么中国禁止谷歌浏览器_厦门网站seo

2024/12/21 23:10:54 来源:https://blog.csdn.net/Yikefore/article/details/141061870  浏览:    关键词:丹东东港_域名一定要备案吗_为什么中国禁止谷歌浏览器_厦门网站seo
丹东东港_域名一定要备案吗_为什么中国禁止谷歌浏览器_厦门网站seo

朋友们、伙计们,我们又见面了,本期来给大家带来信号的保存和信号处理相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

​ 

目录

1. 信号的保存

1.1 信号相关概念

1.2 信号的保存 

1.3 处理位图的接口 

2. 信号的处理 

2.1 状态的切换

2.2 信号的处理

2.3 sigaction函数 

3. 信号的其他补充 

3.1 可重入函数

3.2 SIGCHLD信号 


1. 信号的保存

1.1 信号相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。

信号递达的方式有三种:

  • ① 信号的默认处理
  • ② 信号的忽略
  • ③ 信号的自定义捕捉

当我们自定义捕捉信号的时候使用的signal接口就是对指定信号进行捕捉,然后去执行我们自定义的方法,下面再来介绍一下两种用法:

  • ① signal(signo, SIG_DFL):对指定信号恢复默认操作;
  • ② signal(signo, SIG_IGN):对指定信号进行忽略(忽略也算做对信号进行处理)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

1.2 信号的保存 

当信号产生时,我们不一定要立即对信号进行递达,而是在合适的时候进行递达,那么在信号未决时期,我们要有能力将信号保存,所以在进程PCB中会存在三张位图表,用于保存信号:

信号屏蔽字(block表):比特位的位置表示信号的编号、比特位的内容表示是否对特定信号进行屏蔽(阻塞)。

未决位图表(pending表):比特位的位置表示信号编号、比特位的内容表示特定的信号时候被递达。

handler表(函数指针数组):比特位的位置表示信号编号、比特位的内容是一个函数指针,指向该信号的处理方法。

注意:常规信号在递达之前产生多次只记一次!

1.3 处理位图的接口 

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作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在该信号集中添加或删除某种有效信号。
  • 函数sigaddset用于向指定的信号集添加某种信号。
  • 函数sigdelset用于删除指定信号集中的某种信号。
  • 函数sigismember用于判断指定信号在指定信号集是否存在。

对block表进行操作:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

① set:将要设置的新的信号屏蔽字

② oldset:获取旧的信号屏蔽字

③ how:修改block表的选项

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask l set
SIG_UNBLOCK

set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set

SIG_SETMASK

设置当前信号屏蔽字为set所指向的值,相当于 

mask = set

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
 
对pengding表操作:
#include <signal.h>
int sigpending(sigset_t *set);

参数:

① set:获取当前进程的信号未决表

返回值:

成功返回0,出错返回-1

接下来通过这些接口我们可以实现一个动态的打印pending表的一个代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;// 打印pending表
void PrintPending(const sigset_t &pending)
{for (int signo = 31; signo > 0; signo--){if (sigismember(&pending, signo)){std::cout << "1";}else{std::cout << "0";}}std::cout << "\n";
}int main()
{// 1. 屏蔽2号信号sigset_t set, oset;// 1.1 初始化信号集sigemptyset(&set);sigemptyset(&oset);// 1.2 添加信号sigaddset(&set, 2);// 1.3 修改信号集sigprocmask(SIG_BLOCK, &set, &oset);// 2. 让进程不断获取当前进程的pendingint cnt = 0;sigset_t pending;while (true){// 2.1 获取pending表sigpending(&pending);// 2.2 打印PrintPending(pending);sleep(1);cnt++;if (cnt == 10){std::cout << "解除对2号信号的屏蔽, 2号信号准备递达" << std::endl;// 2.3 恢复原pending表sigprocmask(SIG_SETMASK, &oset, nullptr);}}return 0;
}

2. 信号的处理 

2.1 状态的切换

进程会在合适的时候处理信号,那么这个合适的时候是指什么时候呢?

进程从内核态返回到用户态时,进行信号的检测和处理。

  • 用户态:一种受控的状态,能够访问的资源是有限的。
  • 内核态:操作系统的工作状态,能访问大部分的系统资源,并且可以让用户以操作系统的身份访问内核空间。
  • ① 用户是无法直接访问OS底层资源,只能通过系统调用间接访问,所以用户调用系统调用,必然包含了身份的变化;
  • ② 进程要被调度首先得加载到内存然后通过页表映射到物理内存,那么操作系统也是需要被映射到物理内存的;
  • ③ 用户空间由用户级页表映射到物理内存;
  • ④ 内核空间由内核级页表映射到物理内存;
  • 所以在调用系统调用时访问OS直接在进程地址空间内进行跳转,就如同函数调用一样,调用系统调用接口也是在进程地址空间内进行的。

① 操作系统的代码、系统调用、数据结构、数据在整个系统中只有一份,所以内核级页表只需要有一张即可;

② 如果有多个进程,只需将内核空间通过内核级页表映射到物理内存,尽管有多个进程,使用的也是同一份系统调用接口;

③ 无论进程如何调度,CPU都可以直接找到操作系统!

④ 我们进程所有代码的执行,都可以在自己的进程地址空间内通过跳转的方式,进行调用和返回。

那么如何区分内核态和用户态呢?

CPU内存在的寄存器CS寄存器,CS寄存器用来保存代码段的,其中有两个比特位01表示内核态(1)、11表示用户态(3);切换用户的状态其实就是修改CS寄存器中对应的比特位。

CPU内部还存在一些CR寄存器:

CR3寄存器用于保存当前运行进程的用户级页表的物理地址;

CR1寄存器用于保存上一次引发缺页中断的虚拟地址。

2.2 信号的处理

用户在调用系统调用之后,在要完成调用任务时,会从用户态切换至内核态完成对应的任务,此时并不是直接切换回用户态,而是先要检测信号,如果有需要处理的信号,根据对信号的处理方法,如果是默认动作、忽略就直接处理,如果是用户自定义方法,那么此时不能在内核态处理,而是要返回用户态去执行用户自定义方法,在执行完之后,不能直接跳转到用户代码处,而是要再次返回内核态,再从内核态返回进入内核态的用户代码处。

简化的图就是一个♾️

在信号捕捉中,一共会涉及到4次状态的切换!

上述情况是只有一个信号需要被处理,那么如果存在多个需要处理的信号,那么在处理完一个信号之后会轮训式的检测需要处理的信号,在所有信号处理完之后再切换为用户态。

2.3 sigaction函数 

该函数是一个检测信号并改变处理动作的函数:

参数:

signum:要改变的信号的编号;

sigaction是一个结构体:

其中我们只需要关注sa_handler和sa_mask

sa_handler是要指定的处理动作;

sa_mask是要额外屏蔽的信号集。

act:表示要改变的新的处理方法;

oldact:表示被改变之前的处理方法。

Linux是不允许同一个信号已经在被处理的过程中,再次进行嵌套处理的,所以当某一个信号在被处理的过程中,内核会自动将该信号加入到信号屏蔽字中,当处理的函数返回之后,会对该信号进行恢复,除了当前处理的信号被屏蔽外,我们也可以通过sa_mask(信号集)添加一些额外的信号进行屏蔽。 

代码演示:

#include <iostream>
#include <unistd.h>
#include <signal.h>void Print(const sigset_t &pending);void handler(int signo)
{std::cout << "get a sig: " << signo << std::endl;sleep(1);while (true){sigset_t pending;sigpending(&pending);Print(pending);sleep(1);}
}// 打印pending表
void Print(const sigset_t &pending)
{for (int signo = 31; signo > 0; signo--){if (sigismember(&pending, signo)){std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}
int main()
{std::cout << "pid: " << getpid() << std::endl;struct sigaction act, oact;// 自定义处理方法act.sa_handler = handler;// 初始化信号集sigemptyset(&act.sa_mask);// 添加3号信号sigaddset(&act.sa_mask, 3);// 自定义捕捉2号信号sigaction(2, &act, &oact);while (1)sleep(1);return 0;
}

3. 信号的其他补充 

3.1 可重入函数

将函数和信号结合起来研究:

在链表阶段我们实现了一个头插的函数接口,头插的阶段分为两步,做完第一步的时候由于某些硬件中断使进程回到了内核态,再次返回用户态时需要进行信号的检测与处理,如果此时的信号自定义方法中也调用了头插的函数,在做完头插的两步之后又重新返回用户态的代码处继续向下执行,那么在自定义方法中插入的头节点就会被丢失掉,此时这个头插的这个函数就是一个不可重入函数。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的 :
  • 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

3.2 SIGCHLD信号 

在进程等待的章节说到过,子进程退出时父进程必须进行等待(waitpid()),否则会造成僵尸问题,并且我们有时还需要知道子进程的退出信息,另外在子进程退出的时候回向父进程发送SIGCHLD信号。

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signo)
{std::cout << "get a sig: " << signo << std::endl;
}int main()
{std::cout << "pid: " << getpid() << std::endl;// 自定义捕捉信号signal(SIGCHLD, handler);pid_t id = fork();if (id == 0){std::cout << "child is running" << std::endl;sleep(5);exit(10);}while (true)sleep(1);return 0;
}

父进程自定义捕捉SIGCHLD信号,子进程在运行5秒后退出,可以看到果然子进程给父进程发送了SIGCHLD信号。

所以我们就可以基于信号来对子进程进行回收等待了:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>void handler(int signo)
{std::cout << "get a sig: " << signo << std::endl;// 等待任意进程waitpid(-1, nullptr, 0);
}int main()
{std::cout << "pid: " << getpid() << std::endl;// 自定义捕捉信号signal(SIGCHLD, handler);pid_t id = fork();if (id == 0){std::cout << "child is running" << std::endl;sleep(5);exit(10);}while (true)sleep(1);return 0;
}

Linux支持手动忽略SIGCHLD,如果对其进行忽略,那么所有的子进程都不要父进程进行等待了,子进程会在终止时自动的清理。

#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{std::cout << "pid: " << getpid() << std::endl;// 手动忽略SIGCHLDsignal(SIGCHLD, SIG_IGN);pid_t id = fork();if (id == 0){std::cout << "child is running" << std::endl;sleep(5);exit(10);}return 0;
}

 

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!   

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com