当前位置: 首页 > news >正文

Linux 信号捕捉与软硬中断

一.信号捕捉与处理

1.信号捕捉

经过前两章的学习,对于信号,我们目前的阶段处于:

信号捕捉的流程如下:

1.信号处理要在合适的时候处理,什么叫做合适的时候?当进程从内核态返回到用户态的时候,会进行信号检查——do_signal,判定当前进程是否收到信号,通过检查pending表,block表,根据handler表执行动作。此时就转到用户写的sighandler处理信号,再返回内核态。

2.如果信号是默认,忽略处理方式,会怎么做?忽略时,只需要将pending表由1变0,返回用户态即可。默认时,当前进程会根据特定信号的约定进行特定动作(大部分信号的动作默认为终止)。处理这些要比自定义捕捉和更简单。

3.重谈捕捉过程:我们在执行自定义方法handler(用户写的)时,操作系统也要做身份切换!因为方法是用户写的,如果方法中存在非法操作,如果用内核身份直接执行,会出问题。

函数调用本质:函数栈帧,我们再调用handler方法时也要形成栈帧,我们可以把sigreturn设置在返回地址中,这样调用完handler会穿插执行sigreturn

整个信号调用的过程就像是:无限符号!涉及到四次身份变化!焦点处,就是信号检测的时间点!

进程凭什么进入内核?为什么系统调用就能进入内核?操作系统会给一个进程分配时间片,即使是一个死循环的代码,也需要被调度,他在运行时实际上在消耗自己的时间片,再时间片结束之后会被强制换下运行队列。

二.硬件中断与软件中断

1.硬件中断

1.操作系统是如何运行的

a.操作系统如何知道键盘上有数据?例如这个scanf,键盘数据没有准备好,进程就会阻塞;为什么会阻塞?因为进程PCB的状态被设置为S,将该进程从运行队列放到等待队列中。后来键盘中输入了数据,回车,进程被瞬间唤醒然后运行。进程时如何知道我们用键盘输入数据的呢?键盘这个设备属于硬件,操作系统需要管理,计算机中做了一个设计:

所有的外部设备,在硬件上会跟CPU特定的针脚进行间接链接;其中,当我们的外部设备(键盘)按下回车,就会向CPU发起一个叫做“硬件中断”的东西。

int main(){int a=0;scanf("%d",&a);printf("%d",a);return 0;}

根据冯诺依曼体系结构我们知道:从数据上来说,外设备并不会直接和CPU打交道。但是外设会在线路上回合CPU连在一起,不传输数据而传递信号。

我们要想把外设的数据喂到内存里,就需要发起所谓的硬件中断。

b.中断控制器会把中断信息的入口连上外设,中断信号由外设发起,由中断控制器通知CPU。中断控制器识别针脚,在自己内部生成中断号,每种设备都有叫做控制器的结构,就例如,磁盘控制器中也有对应的寄存器,有的寄存器存储指令,有的寄存器存储地址。

中断控制器也是设备,当外设就绪,发起高电平信号,中断控制器会识别出哪个针脚被点亮(例如0号),然后把这个数据写到自己的寄存器中,然后中断控制器给CPU的指定针脚发高电平信号,CPU知道有外设准备好了,此时CPU访问中断控制器的寄存器,拿到中断号

CPU此时虽然知道哪个设备准备好了,但它并不知道怎么管理处理这些数据,所以需要软件进行处理。因此操作系统在内存中提供了一个叫中断向量表——类似函数指针数组的结构,指向了很多方法。其中数组的下标,我们可以理解为提取到的中断号,然后再中断向量表中进行索引,找到其中一个方法,将方法入口返回CPU,这样CPU就可以根据特定信号进行特定处理了。(让CPU执行中断处理方法,读取键盘数据!)

操作系统不会使用轮询方法查询外设信号,而是由外设主动通知操作系统自己准备好了数据。

上一章讲到的信号虽然是纯软件的概念,但是我们其实在用信号模拟硬件中断

2.没有中断的时候,操作系统在干什么?什么都不做,在暂停等待!

操作的工作方式,我们可以用进程调度为例来讲解:

在中断向量表中,注册一个进程调度的方法,并且在硬件上引入一个时钟源,以固定的频率向CPU发送特定中断,实现了固定频率的进程调度。比如我们给每个进程的task_struct中设置一个时间片,当由时钟中断触发调度,此时会检查当前正在运行的进程时间片是否耗尽,如果进程的时间片耗尽,就执行调度。时间片的本质,就是计数器。

操作系统就在硬件时钟中断的驱动下进行调度了!

这样看来,操作系统其实是一个基于时间中断工作的死循环代码。

操作系统就是基于中断进行工作的软件。后来人们觉得时钟源在外设会跟其他外设竞争中断控制器,因此将时钟源设置在CPU内部,因此CPU有了一个概念叫主频——就是每秒触发中断的次数。

时间->时间戳->历史总频率total

一旦得到时间,除了时间片耗尽,我们也可以让历史总频率一直++,因此计算机可以在离线时知道时间多少,让计算机自带时间计数功能。

进程调度的内核结构如下:

在中断向量表中的下标:0x20,执行的方法:时钟中断

void sched_init(void)
{
...
set_intr_gate(0x20, &timer_interrupt);
// 修改中断控制器屏蔽码,允许时钟中断。
outb(inb_p(0x21) & ~0x01, 0x21);
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
...
}

时钟中断采用汇编编写:

时钟中断中有一个叫do_timer的方法。如果当前进程的时间片没有被耗尽,就不进行调度;如果耗尽了,就进行进程调度(schedule())!

schedule方法中就进行了进程切换。

// system_call.s
_timer_interrupt:
...
;// do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.c,305 ⾏实现。
call _do_timer ;// 'do_timer(long CPL)' does everything from
// 调度⼊⼝
void do_timer(long cpl)
{
...
schedule();
} void schedule(void)
{
...
switch_to(next); // 切换到任务号为next 的任务,并运⾏之。
}

中断向量是操作系统的一部分,在系统启动时也会被载入内存

2.软中断

1.其实在操作系统中,也会通过软件触发上面的逻辑。

首先,我们来讲由软件引发的硬件错误——异常。

由上章讲的信号,我们知道除0错误:引发CPU的EFLAGS寄存器溢出,规定为CPU内部触发的中断!

中断服务:处理异常,向目标进程发送信号!

操作系统怎么知道硬件出异常?所有的异常都会转化为中断

tips:

缺页中断:虚拟地址存在,但物理空间未开辟,自动触发缺页中断。在中断向量表中也会注册这个方法,触发这个中断后就会构建一个新的物理空间和映射。

2.CPU内部可以做到让软件主动触发中断,而不是间接引发硬件错误吗?

指令集的存在让这个行为成为可能,同时还支持了系统调用

常见的指令集:

x86:int

x86_64:syscall

C/C++本质就是编译成为了指令集+数据

syscall自动让CPU触发中断,就需要有对应的编号和方法:0x80号中断号,执行方法

在中断向量表注册的CallSystem方法,需要想办法获取系统调用号用来调用系统调用方法。

问题

• ⽤⼾层怎么把系统调⽤号给操作系统? - 寄存器(⽐如EAX)

• 操作系统怎么把返回值给⽤⼾?- 寄存器或者⽤⼾传⼊的缓冲区地址

• 系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法

• 系统调⽤号的本质:数组下标!

系统调用函数指针表:

这个数组的下标,叫做系统调用号

但这些方法,好像跟我们使用的系统调用不太一样?

用户层面怎么调用系统调用?

用户传入系统调用号,让内核拿到,然后寄存器根据0x80触发软中断,自动索引方法,进行系统调用。

问题:open这种,本身不就是OS提供的方法吗?不是!操作系统不提供任何系统调用接口,OS只提供系统调用号!我们使用的这些,都是glibc封装过的!

直接用表地址+4倍eax值,call对应的系统调用方法,就实现了系统调用。

我们来看Linux的gnu C标准库:

• #define SYS_ify(syscall_name) __NR_##syscall_name :是⼀个宏定义,⽤于将系

统调⽤的名称转换为对应的系统调⽤号。⽐如: SYS_ify(open) 会被展开为 __NR_open

• ⽽系统调⽤号,不是 glibc 提供的,是内核提供的,内核提供系统调⽤⼊⼝函数 man 2

syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头⽂件或者开

发⼊⼝,让上层语⾔的设计者使⽤系统调⽤号,完成系统调⽤过程

3.缺页中断,内存碎片处理,除0野指针错误

void trap_init(void)
{int i;set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3); /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。for (i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。
}

缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,

然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来

处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。

所以:

• 操作系统就是躺在中断处理例程上的代码块!

• CPU内部的软中断,⽐如int 0x80或者syscall,我们叫做 陷阱

• CPU内部的软中断,⽐如除零/野指针等,我们叫做 异常。

3.理解用户态和内核态

系统调用的过程,也是在进程地址空间上进行的!所有的函数调用都是地址空间之间的跳转

操作系统作为软硬件协同的重要软件,它一定会被加载到物理内存中。

看上面的图我们知道,1-3GB属于用户区,3-4GB属于内核区。

这里存在两种页表结构:内核页表和用户页表。其实它们的核心工作和结构没有区别

用户页表:存在多份,因为每个进程都会有自己的进程地址空间,用于描述自己用户区的页表一定会存在多份。

内核页表:用于将当前进程映射到物理内存中操作系统所在的区域。操作系统只有一份,所有进程共享。

结论:

1.这就意味着,不管进程如何被调度,总能找到操作系统的内容。我们调用操作系统的系统调用方法,与进程是否被调度无关,因为只需要跳转进程地址空间到内核地址空间,就可以访问内核的所有代码和数据。

2.也就是说,当中断抵达,CPU要处理中断,不管当前运行的是哪些进程的代码数据,都可以通过地址空间的3-4GB内容查到操作系统中的中断向量表。

3.问题:用户和内核都在同一个0-4GB地址空间上了,用户难道可以随便访问内核中的代码和数据了吗?

操作系统为了保护自己,不相信任何人,必须采用系统调用的方式进行访问内核。

用户态:以用户身份只能访问自己的0-3GB数据

内核态:以内核身份,允许通过系统调用的方式,访问操作系统3-4GB数据

问题是:用户或者操作系统,怎么判断自己是什么状态?

CPU内有一个寄存器,cs段寄存器:用低两位标识(权限位:CPL)当前的状态。0标识内核态,3标识用户态——可以看到判断状态的方式是由硬件提供的

上回说到的指令集,最重要的作用之一就是,让cs段寄存器指向操作系统所对代码区,同时将权限标志位设为0,就叫做陷入内核。

用户态:CPU权限为3,并且只允许用户访问0-3GB

内核态:CPU权限为0,将系统调用号传递到eax寄存器,然后切换地址空间到3-4GB,通过内核页表映射找到操作系统,根据中断号查找系统调用表,执行对应系统调用

三.信号捕捉与其他知识

1.捕捉信号方法sigaction

之前我们学习过signal函数捕捉信号,今天我们学习一个新的捕捉信号的接口。

NAMEsigaction, rt_sigaction - examine and change a signal actionSYNOPSIS#include <signal.h>int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

它的参数我们其实也比较熟悉,分别是信号编号,处理信号的方法(自定义),以及一个输出型参数oldact。

sigaction是一个结构体,它的结构如下:

struct sigaction {void     (*sa_handler)(int);void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;int        sa_flags;void     (*sa_restorer)(void);};

sa_handler即自定义信号处理函数,我们重点看第三个成员,可以看到它的类型是上章讲过的信号集。信号捕捉,怎么会和信号集有关系呢?我们来使用sigaction接口来一探究竟。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>void handler(int signum){std::cout<<"hello signal"<<signum<<std::endl;exit(0);
}int main(){struct sigaction act,oct;act.sa_handler = handler;sigaction(SIGINT,&act,&oct);while(1){std::cout<<"hello world"<<std::endl;sleep(1);}return 0;
}

act中的信号集,是什么?进程在收到2号信号,准备抵达;操作系统此时会自动将block位图的2号对应位置填充为1,在处理完之后清0。这样的操作,就避免了在处理2号时有重复的二号传入,引发递归处理。

wujiahao@VM-12-14-ubuntu:~/TestSig$ ./test
hello world
hello world
^Chello signal2

既然可以令当前捕捉的信号屏蔽,那么自然可以想到:这里的信号集sa_mask可以向block中添加多个信号,在捕捉处理2号信号时这些信号都将被屏蔽。

当某个信号的处理函数被调⽤时,内核⾃动将当前信号加⼊进程的信号屏蔽字,当信号处理函数返回时⾃动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产⽣,那么 它会被阻塞到当前处理结束为⽌。 如果在调⽤信号处理函数时,除了当前信号被⾃动屏蔽之外,还希望⾃动屏蔽另外⼀些信号,则⽤sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时⾃动恢复原来的信号屏蔽字。 sa_flags字段包含⼀些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段

我们可以接着验证上面的说法,通过sa_mask添加更多屏蔽字,并不断获取pending表:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>void handler(int signum){std::cout << "hello signal: " << signum << std::endl;while(true){//不断获取pending表!sigset_t pending;sigpending(&pending);for(int i = 31; i >= 1; i--){if(sigismember(&pending, i))std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;sleep(1);}exit(0);}int main(){struct sigaction act,oct;act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask,3);sigaddset(&act.sa_mask,4);act.sa_flags = 0;sigaction(SIGINT,&act,&oct);while(1){std::cout<<"hello world"<<std::endl;sleep(1);}return 0;
}

现象如下:当我们重复摁下Ctrl+c和Ctrl+\时,pending表就会更新,代表这些信号因阻塞而无法被递达。

wujiahao@VM-12-14-ubuntu:~/TestSig$ ./test
hello world
hello world
hello world
^Chello signal: 2
0000000000000000000000000000000
0000000000000000000000000000000
^C0000000000000000000000000000010
^C0000000000000000000000000000010
0000000000000000000000000000010
^@0000000000000000000000000000010
^\0000000000000000000000000000110

2.可重入函数

先有如下场景:当我们在进行链表的结点插入方法时

假如我们在执行完第一步,因为某些时钟中断或信号捕捉,导致链表node1并未完全插入链表中。不巧的时,信号捕捉方法也要插入一个node2,node2的insert完整执行完,返回原来node1的方法处,修改了head的指向,这就导致了node2结点丢失,内存泄漏。

main执行流,handler执行流,其实是串行的。但是会在某些时刻,insert方法被两个以上的执行流重复进入了。

这种场景叫做函数重入。这种由重入导致异常的函数,叫做不可重入函数——这是一种特点。日常使用的大部分函数,都是不可重入的。

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

• 调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。

• 调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

3.volatile关键字

代码引入

main的执行流中,并不会对flag做修改。比如在编译器优化级别较高的情况下,可能会把flag变量优化到寄存器中。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>int flag = 0;void handler(int signu){std::cout << "更改全局变量, " << flag << "-> 1" << std::endl;flag = 1;}int main(){signal(2, handler);while(!flag);std::cout << "process quit normal!" << std::endl;return 0;}

完成计算,一般需要三个阶段:从物理内存拿数据,在寄存器做计算,有必要时写回内存。在上面的代码中,常规情况下会不断读取物理内存,并在CPU中做逻辑运算判断flag。

但是在某些优化场景下,CPU判断在main函数中flag并没有发生修改,就直接把flag值加载到CPU寄存器,提高运算速度。在gcc编译时,我们可以通过指定-Ox(x为数字)来指定优化程度

wujiahao@VM-12-14-ubuntu:~/TestSig$ g++ test.cc -O0
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./a.out
^C更改全局变量, 0-> 1
process quit normal!
wujiahao@VM-12-14-ubuntu:~/TestSig$ g++ test.cc -O1
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./a.out
^C更改全局变量, 0-> 1
^C更改全局变量, 1-> 1
^C更改全局变量, 1-> 1
^C更改全局变量, 1-> 1
^\Quit (core dumped)

但我们此时发现一个奇怪的问题:为什么main中没有正常执行return?

此时由于优化,寄存器中的值和代码中的值并不相符,flag=1被直接加载到了寄存器中,程序反倒不会退出了。

站在用户角度,就像是:寄存器覆盖了进程看到变量的真实情况,物理内存却不可见

此时我们就用volatile来修饰flag。这样进程就可以正常结束了。由此可见,该关键字的作用是,保证内存空间的可见性。↓下面的代码展示,为flag加上volatile关键字,就算把编译器优化拉满也可以避免物理内存不可见的问题了。

wujiahao@VM-12-14-ubuntu:~/TestSig$ g++ test.cc -O3
wujiahao@VM-12-14-ubuntu:~/TestSig$ ./a.out
^C更改全局变量, 0-> 1
process quit normal!

4.sigchild信号——17号信号

子进程在退出时,会给父进程发送sigchild信号

以前感觉不到,是因为父进程处理该信号的方法是默认IGN忽略。验证这个结论,我们捕捉信号:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>void Say(int num){std::cout << "father get a signal:" << num << std::endl;
}int main(){signal(SIGCHLD,Say);pid_t pid=fork();if (pid==0){std::cout<<"I am child,exit"<<std::endl;sleep(3);exit(3);}//父进程waitpid(pid,NULL,0);std::cout<<"I am father,wait child exit"<<std::endl;}

效果如下:

wujiahao@VM-12-14-ubuntu:~/TestSig$ ./a.out
I am child,exit
father get a signal:17
I am father,wait child exit

既然如此,我们可以通过信号捕捉方式回收子进程。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>void WaitAll(int num){while (true){pid_t n = waitpid(-1, nullptr, WNOHANG); // 阻塞了!if (n == 0){break;}else if (n < 0){std::cout << "waitpid error " << std::endl;break;}}std::cout << "father get a signal: " << num << std::endl;}int main()
{// 父进程signal(SIGCHLD, WaitAll); // 父进程for (int i = 0; i < 10; i++){pid_t id = fork(); // 如果我们有10个子进程呢??6退出了,4个没退if (id == 0){sleep(3);std::cout << "I am child, exit" << std::endl;exit(3);// if(i <= 6) exit(3);// else pause();}}while (true){std::cout << "I am fater, exit" << std::endl;sleep(1);}return 0;
}

如果我们不关注退出码,仅仅想通过这种方法回收子进程避免僵尸进程,还可以将SIGCHILD信号的处理方式设置为SIG_IGN。

问题:一开始就说了父进程的默认方式是SIG_IGN,但这里还要再设置一次,是为啥?

它的处理方式其实是SIG_DFL,缺省方式恰好是IGN。缺省设置的IGN和手动设置的IGN效果就是不同的。

http://www.dtcms.com/a/481630.html

相关文章:

  • Linux NTP配置全攻略:从客户端到服务端
  • 二分查找专题总结:从数组越界到掌握“两段性“
  • aws ec2防ssh爆破, aws服务器加固, 亚马逊服务器ssh安全,防止ip扫描ssh。 aws安装fail2ban, ec2配置fail2ban
  • F024 CNN+vue+flask电影推荐系统vue+python+mysql+CNN实现
  • 谷歌生成在线网站地图买外链网站
  • Redis Key的设计
  • Redis 的原子性操作
  • 竹子建站免费版七牛云cdn加速wordpress
  • python进阶_Day8
  • 在React中如何应用函数式编程?
  • selenium的css定位方式有哪些
  • RabbitMq快速入门程序
  • Qt模型控件:QTreeView应用
  • selenium常用的等待有哪些?
  • 基于51单片机水位监测控制自动抽水—LCD1602
  • 电脑系统做的好的几个网站wordpress主题很卡
  • 数据结构和算法篇-环形缓冲区
  • iOS 26 性能分析深度指南 包含帧率、渲染、资源瓶颈与 KeyMob 协助策略
  • vs网站建设弹出窗口代码c网页视频下载神器哪种最好
  • Chrome性能优化秘籍
  • 【ProtoBuffer】protobuffer的安装与使用
  • Jmeter+badboy环境搭建
  • ARM 总线技术 —— AMBA 入门
  • 【实战演练】基于VTK的散点凹包计算实战:从代码逻辑到实现思路
  • Flink 状态设计理念(附源码)
  • 23种设计模式——备忘录模式(Memento Pattern)
  • 【LeetCode】73. 矩阵置零
  • 网站开发教材男通网站哪个好用
  • 《3D草原场景技术拆解:植被物理碰撞与多系统协同的6个实战方案》
  • 软件测试—BUG篇