您的位置:首页 > 科技 > IT业 > 个体户45万以下免个税_免费vip网站推广_百度一下你就知道网页_网络营销内容

个体户45万以下免个税_免费vip网站推广_百度一下你就知道网页_网络营销内容

2025/4/16 10:16:51 来源:https://blog.csdn.net/puppy_1mo/article/details/146187553  浏览:    关键词:个体户45万以下免个税_免费vip网站推广_百度一下你就知道网页_网络营销内容
个体户45万以下免个税_免费vip网站推广_百度一下你就知道网页_网络营销内容

目录

一、信号的捕捉

1、用户态和内核态

2、内核如何实现信号的捕捉

3、sigaction

(1)接口介绍

(2)sa_mask

二、可重入函数

三、volatile


一、信号的捕捉

对于如何处理信号,前面也讲了 signal 接口,如 signal(2, handler),所以对 2 号信号执行 handler 捕捉动作,本质是 OS 去 task_struct 通过 2 号信号作索引,找到内核中 handler 函数指针数组中对应的方法,然后把数组内容改成你自己在用户层传入的 handler 函数指针方法。这里我们要讨论的是上面遗留下来的问题 —— 进程收到信号时,不是立即处理的,而是在合适的时候再处理,那合适的时候是什么时候呢 ?

这里先把结论写出来

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

1、用户态和内核态

 

进程如果访问的是用户空间的代码,此时的状态就是用户态;如果访问的是内核空间,此时的状态就是内核态。我们经常需要通过系统调用访问内核,系统调用是 OS 提供的方法,执行 OS 的方法就可能访问 OS 中的代码和数据,普通用户没有这个权限。所以在调用系统接口时,系统会自动进行身份切换 user ➡ kernel

那 OS 是怎么知道现在的状态是用户态还是内核态?

因为 CPU 中有一个状态寄存器或者说权限相关的寄存器,它可以表示所处的状态。每个用户进程都有自己的用户级页表,OS 中也有且只有一份内核级页表。也就是说,多个进程可以通过权限提升来访问同一张内核级页表,每个进程变成内核态的时候访问的就是同一份数据。所以,OS 区分是用户态还是内核态,除了寄存器保存了权限相关的数据之外,还要看进程使用的是哪个种类的页表。

 在什么情况下会触发从用户态到内核态呢?

这里有很多种方式:比如,自己写的一个 cin 程序一运行就卡在那里,你按了 abc,然后程序就会拿到 abc,本质就是键盘在触发的时候被 OS 先识别到,然后放在 OS 的缓冲区中,而你的程序在从 OS 的缓冲区中读取。其中 OS 是通过一种中断技术,这个中断指的是硬件方面的中断,如 8259 中断器,它是一种芯片,用于管理计算机系统中的中断请求,通常和 CPU 一起使用。再举个例子,如果了解过汇编,可能听说过 int 80,它就是传说中系统调用接口的底层原理,系统调用的底层原理就是通过指令 int 80 来中断陷入内核。还有一种比较好理解的,就是在调用系统接口后就陷入内核,然后就可以执行内核代码。然后当从内核态返回用户态时就更简单了,当我们调完系统接口就返到用户态了。总之,这里只需要知道从用户态到内核态是有很多种方式的就行。

用户态和内核态的权限级别不同,那么自然能看到的资源是不一样的。内核态的权限级别一定更高,但它并不代表内核态能直接访问用户态。前面说了信号捕捉的时间点是内核态 ➡ 用户态的时候,信号被处理叫做信号递达,递达有忽略、默认、自定义,自定义动作就叫做捕捉动作,只要理解了捕捉,那么忽略和默认就简单了。上图就是整个信号的捕捉过程:在 CPU 执行我们的代码时,一定会调用系统调用。

系统调用是函数,是 OS 提供的,也有代码,需要被执行,那么应该以 “什么态” 执行呢?
实际上用户态中进程调用系统调用时必须得陷入内核以用户态身份执行,执行完毕后又返回用户态,继续执行用户态中的代码,那么问题就是可以直接以内核态的身份去执行用户态中的代码吗?

从内核态返回到用户态之前,OS 会做一系列的检测捕捉工作,它会检测当前进程是否有信号需要处理,如果没有就会返回系统调用,如果有,那就先处理(具体它会遍历识别位图: 假如信号 pending 了,且没有被 block,那就会执行 handler 方法,比如说终止进程,那就会释放这个进程,如果是暂停,那就不用返回系统调用,然后再把进程 pcb 放在暂停队列中,如果是忽略那就把 pending 中对应的比特位由 1 变为 0,然后返回系统调用)。所以,可以看到比较难处理的是自定义捕捉,当 3 号信号捕捉时且收到了 pending,没有被 block,那么就会执行用户空间中的捕捉方法。换而言之,我们因为系统调用而陷入内核,执行系统方法,执行完方法后做信号检测,检测到信号是自定义捕捉,那么就会执行自定义捕捉的方法。此时,应该以 “什么态” 执行信号捕捉方法?

理论来说,内核态是绝对可以的,因为内核态的权限比用户态的权限高,但实际并不能以内核态的身份去执行用户态的代码,因为 OS 不相信任何人写的任何代码,这样设计就很有可能让恶意用户利用导致系统不安全。所以必须是用户态执行用户空间的代码,内核态执行内核空间的代码,所以你是用户态要执行内核态的代码,你是内核态要执行用户态的代码,必须进行状态或者说权限切换。所以,信号捕捉的完整流程就是在用户区中因为中断、异常或系统调用,接着切换权限陷入内核执行系统方法,然后再返回发现有信号需要被捕捉执行,接着切换权限去执行捕捉方法,然后再执行特殊的系统调用sigretum再次陷入内核,再执行 sys_sigreturn() 系统调用返回用户区。

注意切换到用户态执行捕捉方法后不能直接返回系统调用,因为曾经执行捕捉方法时是由 OS 进入的,所以必须得利用系统接口再次陷入内核,最后由内核调用系统接口返回用户区。

2、内核如何实现信号的捕捉

上面的图和文字都说的太复杂了,这里我们简化一下,宏观来看信号的捕捉过程就是状态权限切换的过程,这里的蓝点表示信号捕捉过程中状态权限切换的次数。其中完整流程就是:

  1. 调用系统调用,陷入内核。
  2. 执行完系统任务。
  3. 进行信号检测。
  4. 执行捕捉代码,调用 sigturm 再次陷入内核。
  5. 调用 sys_sigreturn,返回到用户区中系统调用点。


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

3、sigaction

对于修改 handler 表的操作接口,前面已经了解过 signal 了,下面再讲讲 sigaction,sigaction 相比 signal 有更多的选项,不过只需要知道它怎么用就行了,因为它兼顾了实时信号。 

(1)接口介绍

man sigaction

 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  • signum:指定捕捉信号的编号。
  • act:输入性参数,如何处理信号,它是一个结构体指针,第 2 与第 5 个字段是实时信号相关的,可以不管它。
  • oldact:输出型参数,如果需要可以把老的信号捕捉方式保存,不需要则 NULL。

成功返回0,失败返回-1

这是这个结构体的内容

在这个结构体中,我们只关心这两个字段。其他的字段与实时信号有关

如下样例所示可以简单的先用起来这个函数

#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>void handler(int signo)
{std::cout<<"捕捉到2号信号"<<std::endl;
}
int main()
{struct sigaction act,oct;memset(&act,0,sizeof(act));memset(&oct,0,sizeof(oct));act.sa_handler=handler;sigaction(2,&act,&oct);while(true){std::cout<<"hello linux"<<std::endl;sleep(1);}return 0;
}

运行结果如下

那我们想知道pending位图接收到信号,会在位图里把0->1,那什么时候把1->0呢?

#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>using namespace std;void PrintPending()
{sigset_t pending;sigpending(&pending);for(int signo=31;signo>=1;signo--){if(sigismember(&pending,signo)){cout<<"1";}else{cout<<"0";}}cout<<endl;
}
void handler(int signo)
{PrintPending();std::cout<<"捕捉到2号信号"<<std::endl;
}
int main()
{struct sigaction act,oct;memset(&act,0,sizeof(act));memset(&oct,0,sizeof(oct));act.sa_handler=handler;sigaction(2,&act,&oct);while(true){cout<<"hello linux"<<endl;sleep(1);}return 0;
}

所以pending位图,执行捕捉方法之前,先清0,在调用

我们现在可以验证之前的结论,当某个信号的处理函数被调用时,内核自动将当前信号加入进程信号的屏蔽字中,当信号处理函数返回自动恢复原来的信号屏蔽字

我们写一份代码验证一下 

我们先发送2号信号的时候,被捕捉,执行自定义函数,我们这里写了一个循环,是为了证明当某个信号在处理的时候,内核将该信号先加入到block中不执行该信号的动作。循环5秒后解除屏蔽会自动执行。

我们来看下运行结果

即操作系统不允许对某个信号重复捕捉,最多只能捕捉一层

信号被处理的时候,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用

(2)sa_mask

我们在上面讲了sigaction结构体的函数指针,接下来给大家了解sa_mask

sigaction中的sa_mask字段代表什么呢?

这个字段是一个sigset_t 类型的字段。它代表着屏蔽的信号。也就是会将block表给设置

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

运行结果如下:

二、可重入函数

main 函数调用 insert 函数向一个链表 head 中插入节点 node1,插入操作分为两步,刚做完第一步时,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2,插入操作的两步都做完之后从 sighandler 返回内核态,再次回到用户态就从 main 函数调用的 insert 函数中继续往下执行,之前做第一步后被打断,现在继续做完第二步。结果是 main 函数和 sighandler 先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

这里insert函数被main和handler执行流重复进入,这样会导致节点丢失,内存泄漏。

概念:如果一个函数,被重复进入的情况下,出错了或者可能出错,这样叫做不可重入函数,否则叫做可重入函数。 

目前我们所学的大部分函数都是不可重入的

三、volatile

volatile 是属于 C 语言中的关键字,也叫做易变关键字(被它修饰后的变量就是在告诉编译器这个变量是易变的),它的作用是保持内存的可见性。

这里给一个全局标志位 flag,利用 flag 让程序死循环执行,此时就可以通过信号捕捉,在捕捉方法中改变 flag 的值,然后结束死循环。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;int flag=0;
void handler(int signo)
{cout<<"接收到2号信号"<<signo<<endl;flag=1;
}
int main()
{signal(2,handler);while(!flag){cout<<"haha"<<endl;sleep(1);}cout<<"process quit!"<<endl;return 0;
}

运行结果如下:

上面可以看到 main 函数中没有更改 flag 的任何操作,那么可能会被优化,所以 flag 一变化不会立马被检测到。这里我们可以看到默认 g++(gcc 也一样) 并没有优化这段代码,所以 flag 一变化立马就被检测到。其实,gcc 和 g++ 中有很多优化级别,man gcc 文档筛选后就可以看到 gcc 有 -O0/1/2/3 等优化级别,gcc -O0 表示不会优化代码。经过验证(注意这里不同平台结果可能不一样):

gcc 在 -O0 时不会作优化处理,此时同上默认,进程一收到信号,进程就终止了。

gcc 在 -O1/2/3 时会作优化处理,此时发现 flag 已经置为 1 了,但是进程并没有终止。

这个优化是在是在编译时就处理好了。

因为这里主执行流下并没有对 flag 的修改操作,所以 gcc -O1 在优化的时候可能会将局部变量 flag 优化成寄存器变量,定义 flag 时一定会在内存开辟空间。此时,gcc 在编译时发现以 flag 作为死循环条件,且主执行流中没有对 flag 修改的操作,所以就把 flag 优化成寄存器变量。一般默认情况没有优化级时,gcc -O0 while 循环检测的是内存中的变量,而在优化的情况下 gcc -O1 会将内存中的变量优化到寄存器中,然后 while 循环检测时只检测寄存器中 flag 的值,当执行信号捕捉代码时,flag = 1 又只会对内存进行修改,而此时 wihle 循环只检测寄存器中的 flag = 0。所以,短暂出现了内存数据和寄存器数据不一致的现象,然后就出现了好像把 flag 改了,但 while 循环又不退出的现象。因为要减少代码体积和提高效率,所以在优化时需要优化成寄存器变量。

所以在 gcc -O1(gcc -O3) 优化时还需要加上 volatile,此时要告诉编译器:不要把 flag 优化到寄存器上,每次检测必须把 flag 从内存读到寄存器中,然后再进行检测,不要因为寄存器而干扰 while 循环的判断。这就叫做保持内存的可见性。

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

版权声明:

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

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