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

linux中断

内核态和用户态
用户态:进程执行代码,访问数据,都在访问[0,3GB)进程地址空间,访问用户代码和数据
内核态:进程访问[3GB,4GB)地址空间的时候,就是访问OS的过程
内核态的权限级别更高

如果OS定期主动检测外设是否准备好,而OS有很多工作要做,比如进程调度,比如内存、文件管理等,所以效率低,并且OS属于内存,键盘是外设,轮询的效率就更低了,所以采用中断,当键盘有被按下,键盘通知OS我好了,OS再去读取键盘输入的数据,也就是硬件层面的中断机制

如下图,我们知道数据层面(通信)CPU不会和外设直接打交道,但是CPU可以直接给外设发送控制信号,直接控制外设(通信和控制都是信息传递),并且外设比如输入设备也可以通过导线和中央处理器进行某种连接,也就是不仅CPU可以给外设发控制信号,外设也可以给CPU传递一些必要的信号(中断)
OS为了实现硬件级别CPU对外部数据搬运和管理的,硬件工程师设立硬件中断

1.硬件中断

1.1 原理

常规事件发生期间,发生突然事件,需要停下来当前正在执行的任务,去处理突发事件,处理完毕,回到任务被中断处继续执行,外部设备可以触发中断
计算机组成中,CPU正在执行某行代码(准确的说是某行指令),肯定是以进程为载体,OS调度某个进程,执行某个进程的代码或任务
打破学科之间的壁垒,降低记忆成本
谈硬件中断也与软件分不开
而既然CPU在执行某个进程,那CPU寄存器内部肯定保存当前进程的硬件上下文
假设进程需要从键盘读入数据,之后进程以阻塞/非阻塞的方式运行,键盘数据就绪之后,硬件层面上键盘作为外设会间接地给CPU特定地针脚触发高电平

CPU背面针脚,可以是侧面式的一圈,单片机的ARMCPU跟连接方式有关,英特尔的针脚在CPU背面,排针上是导线,导线连接主板,主板就可以让键盘和CPU走线,键盘被按下,直接或间接向CPU触发高电平,触发电脉冲,CPU识别到某个特定针脚上有电平,就识别到外设给自己发中断,中断的本质是高低电平,实际情况复杂的多,因为计算机体系结构里有很多外设,键盘,内存,显卡,网卡,磁盘,包括外接的各种设备,鼠标,话筒,音响,CPU针脚有限,而且大部分针脚要做通信,跟内存对接,跟数据总线/控制总线勾连,CPU要用相对有限的针脚来处理相对多甚至相对无限的外设,上面是简化情况
外设并没有和CPU直接关联,而是通过中断控制器,当中断控制器收到并受理来自外设的中断,中断控制器通知CPU
中断控制器的作用:一根和CPU相连的导线可以复用来处理多个外部设备发的中断,类似拓展坞(一端在CPU,另一端连多个USB接口),拓展CPU处理中断的能力;间接地获取外设触发中断时,中断的相关信息,中断控制器在x86或老式设备,诸如8259这样的板子,中断控制器可以做各种级联,复用一根导线向CPU触发中断,也就是发高电平
单片机中断用的比较多,OS比较发达,所以我们学习的是带OS的平台,硬件中断是被隐藏起来的,我们一般感受不到
外设就绪给中断控制器发中断(触发高电平),中断控制器在主板上和CPU是连在一起的,中断控制器收到之后通过导线向CPU触发高电平(发送电脉冲),告诉CPU有外设准备好了,CPU内部规定特定针脚如果低电平没有中断,高电平就是有中断,也就是要实现中断效果,CPU首先要支持中断,如果CPU在硬件层面上不支持中断,就不会读取来自中断控制器的电脉冲,更不会处理中断
也就是说中断需要多方面而不是单一设备的支持,包括外部设备,中断控制器和CPU,看到中断,识别中断,分析中断,处理中断
并且CPU是不知道什么时候产生中断(键盘什么时候被按下,磁盘什么时候准备好,网卡什么时候有数据),因此中断是异步产生的
中断以及中断号需要硬件电路来获取,真实情况复杂的多,所以下图有一些不严谨
外部设备可以是网卡(网络里来了数据,CPU怎么知道)磁盘(进程要打开文件,OS把文件代码数据加载到内存,本质是拷贝,OS肯定不可能在旁边干等着,所以会去做其它的事,那OS怎么知道数据拷贝好了)
来自不同设备的中断处理动作是不一样的,CPU在中断来之前肯定要内置中断的处理方法,如果硬件实现,首先根本就不知道到底会有多少外设,涉及到走线、布局、硬件电路的设计,就会非常复杂,所以用软件实现,OS内置中断向量表,可以简单理解为函数指针数组

typedefvoid(*handler_t)(void)handler_t IDT[NUM];

外设接入之后,有唯一的中断号,中断控制器能够分辨外设对应的中断号(中断控制器也有寄存器,把中断后存储在寄存器中),CPU得知有中断就向中断控制器问询中断号(软件思维,真实情况是纯硬件电路完成的),转化为数组下标,通过索引中断向量表,就可以获取处理中断的方法,假设得到的数组下标是0,通过索引之后查到函数指针,接着OS执行处理键盘的操作,处理结束,键盘输入的数据就拷贝到内存了
CPU就可以被动式的相应中断
如果要执行键盘处理的函数,CPU要取指令,读指令,分析指令,形成临时数据,修改PC指针,也就是要利用CPU内部的寄存器,那本来正在执行的“进程”也有自己的上下文存在CPU的寄存器里,如果直接执行键盘处理函数,就会覆盖寄存器里的值,等到执行完处理函数,接着执行进程的时候就会出现混乱,所以CPU在识别到有中断之后,硬件层面上就通过压栈保护现场,恢复现场就是弹栈


暂停CPU正在执行的任务,处理硬件突发事件,结合中断号和中断向量表,称为硬件中断(由外部触发,中断系统处理流程),中断向量表由软件OS提供,其它都是硬件自动完成
和信号很像,当中断来的时候,CPU的工作被打断,要处理中断;当信号来的时候,进程要被打断,转而执行信号处理函数,handler表就是中断的中断向量表,信号编号就是中断向量号
本来是先有的硬件中断,由硬件处理,后来发现,进程也需要这样的机制,发明了信号机制,模拟硬件中断,纯软件方式模仿中断完成特定任务处理,中断和信号,原理类似,但底层实现不同,类似康师傅和庚师傅

中断有中断号,可以被屏蔽(类似block表),有优先级,比如抢占优先级,次优先级
中断向量表是OS一部分,启动的时候加载到内存,因为有中断,所以OS不需要对外设进行周期性的检测或轮询
了解中断原理
中断描述符表里有很多表项,比如中断服务是否开启,中断优先级等设定,标志位,要字节来保留更多的选项

键盘

1.2 OS本质

那么键盘可以固定例程给CPU触发中断
OS没有生命,就是一个软件块,一个躺在中断向量表中的软件集合
入口,软件程序
外部晶振(对电力敏感的设备),硬件电路完成,主板上集成,和CPU在一起

1.3 时间计算原理

在电脑拔掉电源关机一个礼拜开机时间还是正确的,因为主板上有一个纽扣电池,在断电的情况下可以向主板上的计时单元供电
硬件电路会计算当前的时间,一旦开机即可获取,并且给纽扣电池充电

但如果关机时间过长,开机时间不对,因为纽扣电池没电了,但是联网就可以了,因为OS有时间校准功能,通过网络

1.3.1 进程调度原理

下图中sched是关于调度的初始化在main_rename也就是入口中,接着把timer_interrupt方法绑定到20中断向量号,timer_interrupt是由反汇编写的,调用do_timer,do_timer会检测时间片,进行调度等



所以通过do_timer把进程调起来,进程通过系统调用把OS调起来,OS调度进程,进程进行申请内存、打开文件、访问设备、通信、发信号,调OS

时间片耗尽,就是当前进程PCB的counter==0
OS为什么能执行调度算法?固定时间间隔的时钟中断

当代时钟源硬件电路集成在CPU内部,因为如果作为外设,太慢,每次要做很多事情,发中断,还要自动控制器忙前忙后

并且1ms的周期是不是有些短,可以查一下电脑的处理器主频,GHz的频率,ns级别的周期

OS也就是通过时钟源+时钟中断=硬件中断来运行的·

OS内部会启动很多内核级进程/线程,不存在没有进程的情况,定期刷新数据到磁盘

2.软件中断


内核层面每一个系统调用都有一个系统调用号,也就是数组下标,因为系统调用表是函数指针数组
假设用户调用ssize_t read(fd,xxx);


linux的gnu c标准库几乎把所有的系统调用都封装了,我们调用上层函数,ubuntu 24.04的man手册都直接明牌了,就是C标准库的
用户直接通过syscall/int 0x80+系统调用号也能直接进行软中断来系统调用

//源代码路径:linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.h/* at least 8 syscall per cacheline */#define__NR_read0__SYSCALL(__NR_read,sys_read)#define__NR_write1__SYSCALL(__NR_write,sys_write)#define__NR_open2__SYSCALL(__NR_open,sys_open)#define__NR_close3__SYSCALL(__NR_close,sys_close)#define__NR_stat4__SYSCALL(__NR_stat,sys_newstat)#define__NR_fstat5__SYSCALL(__NR_fstat,sys_newfstat)#define__NR_lstat6__SYSCALL(__NR_lstat,sys_newlstat)#define__NR_poll7__SYSCALL(__NR_poll,sys_poll)#define__NR_lseek8__SYSCALL(__NR_lseek,sys_lseek)#define__NR_mmap9__SYSCALL(__NR_mmap,sys_mmap)#define__NR_mprotect10__SYSCALL(__NR_mprotect,sys_mprotect)#define__NR_munmap11__SYSCALL(__NR_munmap,sys_munmap)#define__NR_brk12__SYSCALL(__NR_brk,sys_brk)#define__NR_rt_sigaction13__SYSCALL(__NR_rt_sigaction,sys_rt_sigaction)#define__NR_rt_sigprocmask14__SYSCALL(__NR_rt_sigprocmask,sys_rt_sigprocmask)#define__NR_rt_sigreturn15__SYSCALL(__NR_rt_sigreturn,stub_rt_sigreturn)...

部分版本在glibc中,系统调用实现方式

//glibc-2.4/sysdeps/unix/sysv/linux/x86_64/sysdep.h#defineINTERNAL_SYSCALL_NCS(name,err,nr,args...)\({\unsignedlongresultvar;\LOAD_ARGS_##nr(args)\LOAD_REGS_##nr\asmvolatile(\"syscall\n\t"\:"=a"(resultvar)\:"0"(name)ASM_ARGS_##nr:"memory","cc","r11","cx");\(long)resultvar;})



那是怎么知道有多少参数呢?既然能把系统调用号压到寄存器eax,那也可以通过寄存器传入参数个数,比如约定edx,比如<=3可以使用寄存器,>3可以使用栈空间,函数调用有栈帧,形成临时拷贝,把所有参数可以放到一张表,把表的地址和参数个数传进来,宏定义来处理多参数系统调用

只有syscall/int 0x80能够修改状态位,而访问内核只能通过系统调用,把syscall/int 0x80和中断向量号绑定,只能通过中断访问内核
如果通过syscall/int 0x80来访问内核呢?还有一层屏障,因为只能通过系统调用访问内核,所以syscall之后的例程是固定的,因为一般会把系统调用号传给寄存器比如eax,接着syscall之后直接通过系统调用号访问系统调用表,因此即使syscall可以进入内核,也只能进行系统调用,而不是胡作非为修改内核数据结构
硬件和软件结合保证系统调用

信号就是纯软件模仿中断,信号编号就是中断向量号,信号处理方法就是IDT,而信号的信息保存在PCB,只有在内核态才能访问,所以在系统调用结束,内核态返回用户态时顺手就看一下有没有需要处理的信号并处理

3.异常

硬件中断–时钟中断,外设中断
CPU内部的软(件)中断,比如syscall/int 0x80,叫做陷阱–系统调用的实现
CPU内部的软中断,比如除零/野指针等,叫做异常,比如缺页异常
比如缺页中断–MMU,OS进行调页
内存碎片处理–内存硬件报错,OS检查可以把不活跃进程的代码和数据放到swap分区,把缓冲区内容刷新到磁盘
除零,野指针错误–CPU或者MMU报错,如果是野指针,OS继续检查是没有加载到内存还是真的越界了

系统内提前把常见错误的处理方法绑定中断号

OS是一个软件块,躺在中断处理例程上的代码块,OS是一个基于中断处理的软件集合
比如文件内核缓冲区是由OS刷新到磁盘,比如父子进程发生写时拷贝(w的时候权限是r)由OS来做,这种由OS来做的80%的工作都是通过中断来实现的,处理当前任务的时候,又来了一个需要紧急处理的任务,当前任务/执行流被打断,转而执行突发事件,处理完回到当前执行流,很形象的名字,中断

4.虚拟地址空间

4.1 再谈地址空间

linux中很多地方需要进行权限管理,内核态和用户态需要硬件级别的支持,是CPU两种执行级别,CS code segment
进程的虚拟地址空间以32位为例,共有4GB,[0,3GB)为用户区,[3GB,4GB)为代码区
CPL,也就是Current Privilege Level,当前特权级,一般会用到两类,内核态和用户态,执行内核代码和数据,处于内核态,00表示,处于用户态,CPL为11,两个比特位是CPU的CS寄存器中的低两位,当出现中断/软中断(陷阱)/异常,CPU硬件自动将CS.RPL改为00,允许CPU执行内核态的代码和访问内核态的数据[3BG,4GB),也可以访问用户态的数据,才能进行系统调用
CPU中的寄存器内部保存进程运行大量的临时值,也就是进程的硬件上下文,包括CS,其实现在段式管理在弱化,在虚拟地址空间出来,低两位是RPL
内核空间和用户空间通过页表映射为虚拟地址空间,页表中有每一个地址单元的权限位,其中[0,3GB)的DPL为11,[3GB,4GB)的DPL为11,当CPU要访问虚拟地址空间的某一地址单元,比较CPL<=DPL,如果成立,就可以访问,如果不成立,就不能访问,因此用户是不能直接通过手动设置一个范围属于内核区的地址来直接访问和修改内核区数据的

那不同的进程用户区的内容肯定有不同,定义变量不同则栈空间不同,动态库加载到共享区,也不一定加载到相同的位置,命令行参数环境变量也不尽相同,动态申请的区域大小/分配的起始地址/数据内容不同等等,那么CPU进行进程调度之后,每个进程有不同的页表,用户区的虚拟地址空间肯定变化不小,但是内核区呢?我们知道,内核区映射到物理内存,包括加载OS的程序,系统调用号等,而且用户通过中断等方式陷入到内核,也只能通过中断向量表+中断向量号来访问内核,而不是可以肆意修改,因为每个进程用到的是同一份OS,同一份内核区,所以进程调度之后,内核区的内容不变,因此任意一个进程都能找到OS,而且各个进程的用户页表各不相同,有一套自己的用户级页表,但是共享同一份内核页表
那么用户调用的函数要么是自己实现的,就在代码区跳转,要么是库的(静态库的和自己实现的都是在代码区跳转),动态库就跳转到共享区,如果是内核的,比如系统调用表,各种异常处理方法等,就通过软中断也就是指令0x80或syscall来进入内核,跳转到内核调用
因此所有的函数调用都是在进程的虚拟地址空间完成的

下面是画了两个页表,但其实是一个,只是简单说明情况


我们来重新看一下这张图,基于上面对于内核态和用户态的理解,CPU执行某个任务,准确的说是某个进程代码中某行代码编译汇编生成二进制机器指令中的一行,此时CPU处于用户态,以32位系统为例,执行的是[0,3GB)用户空间的代码和数据,CPL为11,处于用户态,此时因为硬件中断/软中断/异常陷入内核态,CPL为00,执行系统调用或中断,执行完要切换回用户态的时候,首先检查pending表和block表,对于没有在block表但在pending表中为1的比特位代表的信号可以递达,查handler表,如果是SIGIGN忽略,pending表对应得比特位由0置1即可,如果是SIGDFL也就是默认,也是在内核态执行,执行完切换回用户态,也就是主控制流被中断的地方,如果是用户自定义处理方式,也就是函数指针在用户态,需要切换到用户态执行完代码,通过系统调用再回到内核态,再回到主控制流

那为什么不通过用户态直接访问内核态数据,因为具有安全风险,OS只有一份,万一不小心或者恶意破坏,OS崩了

那么为什么在执行完handler表中的方法后还能回到内核中被中断的地方?
举个例子,本来下一条指令是while(1),地址是0x11223344,放在eip中,但是执行func()要跳转,就把0x11223344压栈,接着eip更新为func()地址,等func()执行完,弹栈获取while(1)地址,继续往下执行,也就是说会通过压栈等技术保留被中断位置的地址

4.2 使用信号模拟OS

#include<cstdio>#include<cstdlib>#include<signal.h>#include<time.h>#include<vector>intcurrent=0;classtask_struct{public:task_struct(intp):pid(p),counter(5){}~task_struct(){}voidDesc(){counter--;}intPid(){returnpid;}intExperied(){returncounter==0;}voidRun(){printf("%d号进程正在运行\n",pid);}voidReset(){counter=5;}private:intpid;intstatus;intcounter;};std::vector<task_struct>task_structs;voiddo_timer(intsigno){task_structs[current].Desc();if(task_structs[current].Experied()){printf("%d号进程过期了\n",task_structs[current].Pid());current=rand()%task_structs.size();task_structs[current].Reset();printf("选择%d号进程\n",task_structs[current].Pid());}task_structs[current].Run();alarm(1);}intmain(){srand(time(nullptr));alarm(1);signal(SIGALRM,do_timer);task_structs.emplace_back(1);task_structs.emplace_back(2);task_structs.emplace_back(3);task_structs.emplace_back(4);task_structs.emplace_back(5);for(;;)pause();return0;}

5.其它

5.1 sigaction



可以看到默认情况下,sigaction在执行handler方法时会默认阻塞正在处理的信号,信号处理完毕会恢复原来的屏蔽字

通过sa_mask字段可以阻塞其它信号,信号处理完毕会恢复原来的屏蔽字

5.2 volatile

为什么会出现上面的情况呢?寄存器覆盖了内存,flag的内存不可见了


有没有什么做法能够让编译器不要优化flag呢?加上volatile关键字,保持flag变量的内存可见性

即使是O3也不会优化flag,但其它优化正常进行

还有register关键字,建议优化到寄存器中,但是只能用于局部变量,不能用于全局变量

5.3 可重入函数

下面举了一个例子,当头插node1到一半的时候,被信号中断,在信号处理的时候头插同一个链表,最后导致内存泄漏,最后析构链表,没办法析构node2
因为insert是不可重入函数,如果重入就可能出问题,那什么是可重入呢?就是在执行到一半的时候,再次执行同一函数,不会出问题,重入/不可重入是函数的特点
特别是处理全局变量的时候,比如整个内存中动态申请的空间通过一张链表来维持,那么malloc就是不可重入的,C++中有很多容器,都会使用文件配置器,所以C++中容器的增删一般都是不可重入的

5.4 SIGCHLD

子进程在退出的时候,会给父进程发SIGCHLD信号,只不过对于SIGCHLD的默认处理方式是忽略,所以一般感受不到

如下,既然子进程退出的时候会给父进程发信号,那在SIGCHLD自定义处理方式中回收子进程,这样父进程就不需要阻塞等待子进程退出,更加高效,本来阻塞等待的时间可以去处理其它任务
这样做的问题是什么呢?1.如果多个子进程同时退出,我们知道,在处理某个信号的时候,一般阻塞该信号,而pending表是一个位图,只能表示两种状态,收到和没收到某个信号,那肯定会有进程发送的SIGCHLD丢失,父进程没办法回收子进程
2.如果进程没有退出完呢,如果我们在handler内部搞一个循环,循环十次,每次回收一个进程?比如有10个子进程,只退了9个,那剩下一个,父进程就阻塞在handler的死循环吗?

下面是阻塞等待,可以看到在回收完9个进程后,父进程就在处理函数内部阻塞,因为第10个子进程没有退出,就阻塞在waitpid内部

阻塞等待

非阻塞等待,没有退出的子进程就造成了内存泄漏

进程等待,要么就是在父进程收到SIGCHLD信号之后回收,这样父进程要在子进程之后退出,如果父进程在子进程之前退出,子进程退出时发信号,父进程都不在了,怎么回收呢?这时候可能就需要阻塞或者非阻塞轮询回收,根据实际业务场景抉择

那10个进程,9个进程退出,还有必要进行第10次waitpid吗?到底多少子进程退出是上帝视角,程序员不知道嘞,所以当然有必要

实际上,由于Unix系统历史原因,父进程可以把SIGCHLD的处理方法设置为SIG_IGN,这样OS就会自动回收退出的子进程,不会通知父进程,那SIGCHLD默认处理就是忽略,可以看到用户定义的忽略和系统的忽略不完全一致,但在Liunx下是这样,其它类Unix系统具体情况具体分析

http://www.cnnetsun.cn/news/3523577.html

相关文章:

  • AI 导出鸭实操教程:Grok 的公式怎么复制到 word 高效无乱码
  • 专业3D点云标注工具LabelCloud:高效创建自动驾驶训练数据的终极解决方案
  • 10个CANN启航营使用技巧:从新手到专家的完整教程
  • 仅限首批内测用户知晓的Kimi搜索加速通道:通过自定义User-Agent+Accept-Language组合提升响应速度47.2%(附压测数据截图)
  • 089、锐化与边缘增强:非锐化掩模、自适应锐化与过冲抑制的实战经验
  • SpringBoot+Vue通过ModbusTCP协议实现PLC 设备连接、重连实时控制
  • ECS-Network-Racing-Sample UI系统设计:如何在DOTS架构下构建响应式用户界面
  • TMS320F2838x McBSP中断机制与多通道模式配置详解
  • 【湿法-萃取工艺6】---2#萃取(萃铜锰)---使用P204萃取剂后-全流程解析
  • 终极指南:asdf-python自动化配置与默认Python包一键安装技巧
  • 多模型协同的稳定性设计:主备切换不是加一个 if-else
  • 基于 ThinkPHP 与 Workerman 的高并发聚合支付系统架构设计与实践
  • 【湿法-萃取工艺8】---4# P507全萃钴、P204深萃钴 全流程解析
  • 深度解析Electron+Vue技术栈的磁力搜索应用架构设计
  • 治愈系微文案的数据驱动优化:从直觉写作到埋点验证的界面文案迭代
  • Clarity社区贡献指南:从问题报告到代码提交的完整流程
  • 紧急修复!Kimi搜索突然返回空结果的4种底层原因(DNS劫持/SSL证书链异常/Referer策略变更实测对比)
  • 开源项目的性能回馈机制:用户侧性能数据的采集与问题复现方法
  • 联邦学习 + 区块链:去中心化 AI 训练的隐私保护与激励设计
  • Kimera-Semantics 实战:在Euroc数据集上运行语义重建的完整流程
  • GHelper深度评测:华硕笔记本性能优化的轻量级革命
  • 3步掌握LDDC:让每首歌都有完美歌词的终极指南
  • HarmonyOS7 购物车计数器页实战:Counter/步进器 不只是会用,还要用得顺手
  • VLC for Android:打破格式限制,你的移动娱乐中心
  • 如何使用node-jsonc-parser实现JSON Schema验证与智能提示
  • rtsp-stream性能对比:与传统RTSP代理服务器的优劣分析
  • 鸿蒙原生开发手记:徒步迹 - 扫码功能:Scan Kit 集成
  • 把代码库探索交给 Claude Code Subagent,主会话才不会被文件读取拖垮
  • Spy Extension vs 普通扩展:核心差异与安全风险对比
  • 如何高效处理文本数据:Python字符串匹配实战指南