精华内容
下载资源
问答
  • 本词条缺少概述图,补充相关内容使词条更完整,还能快速升级,赶紧来编辑吧...中文名中断外文名Interrupt Request分类中断装置和中断处理程序相关概念程序状态字和向量中断等系统功能实现中断响应中断返回等中断定...

    本词条缺少概述图,补充相关内容使词条更完整,还能快速升级,赶紧来编辑吧!

    中断装置和中断处理程序统称为中断系统。

    中断系统是计算机的重要组成部分。实时控制、故障自动处理、计算机与外围设备间的数据传送往往采用中断系统。中断系统的应用大大提高了计算机效率。

    中文名

    中断

    外文名

    Interrupt Request分    类

    中断装置和中断处理程序

    相关概念

    程序状态字和向量中断等

    系统功能

    实现中断响应和中断返回等

    中断定义

    编辑

    语音

    不同的计算机其硬件结构和软件指令是不完全相同的,因此,中断系统也是不相同的。计算机的中断系统能够加强CPU对多任务事件的处理能力。中断机制是现代计算机系统中的基础设施之一,它在系统中起着通信网络作用,以协调系统对各种外部事件的响应和处理。中断是实现多道程序设计的必要条件。 中断是CPU对系统发生的某个事件作出的一种反应。 引起中断的事件称为中断源。中断源向CPU提出处理的请求称为中断请求。发生中断时被打断程序的暂停点称为断点。CPU暂停现行程序而转为响应中断请求的过程称为中断响应。处理中断源的程序称为中断处理程序。CPU执行有关的中断处理程序称为中断处理。而返回断点的过程称为中断返回。中断的实现实行软件和硬件综合完成,硬件部分叫做硬件装置,软件部分称为软件处理程序。

    中断响应处理

    编辑

    语音

    大多数中断系统都具有如下几方面的操作,这些操作是按照中断的执行先后次序排列的。①接收中断请求。②查看本级中断屏蔽位,若该位为1则本级中断源参加优先权排队。③中断优先权选择。④处理机执行完一条指令后或者这条指令已无法执行完,则立即中止现行程序。接着,中断部件根据中断级去指定相应的主存单元,并把被中断的指令地址和处理机当前的主要状态信息存放在此单元中。⑤中断部件根据中断级又指定另外的主存单元,从这些单元中取出处理机新的状态信息和该级中断控制程序的起始地址。⑥执行中断控制程序和相应的中断服务程序。⑦执行完中断服务程序后,利用专用指令使处理机返回被中断的程序或转向其他程序。

    中断相关概念

    编辑

    语音

    程序状态字和向量中断

    这是两个与中断响应和处理有密切关系的概念。

    ① 程序状态字:每个程序均有自己的程序状态字。现行程序的程序状态字放在处理机的程序状态字寄存器中。程序状态字中最主要的内容有指令地址、条件码、地址保护键,中断屏蔽和中断响应时的中断源记录等。中断响应和处理操作的第④步和第⑤步就是交换程序状态字操作。

    ② 向量中断:对应每一级中断都有一个向量,这些向量顺序存放在主存的指定单元中。向量的内容是:相应的中断服务程序起始地址和处理机状态字(主要是指令地址)。在中断响应时,由中断部件提供中断向量的地址,就可取出该向量。中断响应和处理操作的第⑤步就是取中断向量操作。在采用向量中断的机器中一般不再使用程序状态字。

    中断系统功能

    编辑

    语音

    1)实现中断响应和中断返回

    当CPU收到中断请求后,能根据具体情况决定是否响应中断,如果CPU没有更急、更重要的工作,则在执行完当前指令后响应这一中断请求。CPU中断响应过程如下:首先,将断点处的PC值(即下一条应执行指令的地址)推入堆栈保留下来,这称为保护断点,由硬件自动执行。然后,将有关的寄存器内容和标志位状态推入堆栈保留下来,这称为保护现场,由用户自己编程完成。保护断点和现场后即可执行中断服务程序,执行完毕,CPU由中断服务程序返回主程序,中断返回过程如下:首先恢复原保留寄存器的内容和标志位的状态,这称为恢复现场,由用户编程完成。然后,再加返回指令RETI,RETI指令的功能是恢复PC值,使CPU返回断点,这称为恢复断点。恢复现场和断点后,CPU将继续执行原主程序,中断响应过程到此为止。

    2)实现优先权排队

    通常,系统中有多个中断源,当有多个中断源同时发出中断请求时,要求计算机能确定哪个中断更紧迫,以便首先响应。为此,计算机给每个中断源规定了优先级别,称为优先权。这样,当多个中断源同时发出中断请求时,优先权高的中断能先被响应,只有优先权高的中断处理结束后才能响应优先权低的中断。计算机按中断源优先权高低逐次响应的过程称优先权排队,这个过程可通过硬件电路来实现,亦可通过软件查询来实现。

    3)实现中断嵌套

    当CPU响应某一中断时,若有优先权高的中断源发出中断请求,则CPU能中断正在进行的中断服务程序,并保留这个程序的断点(类似于子程序嵌套),响应高级中断,高级中断处理结束以后,再继续进行被中断的中断服务程序,这个过程称为中断嵌套。如果发出新的中断请求的中断源的优先权级别与正在处理的中断源同级或更低时,CPU不会响应这个中断请求,直至正在处理的中断服务程序执行完以后才能去处理新的中断请求。

    中断源分类

    编辑

    语音

    中断源是指能够引起中断的原因。

    一台处理机可能有很多中断源,但按其性质和处理方法,大致可分为如下五类。

    ① 机器故障中断。

    ② 程序性中断。现行程序本身的异常事件引起的,可分为以下三种:一是程序性错误,例如指令或操作数的地址边界错,非法操作码和除数为零等;二是产生特殊的运算结果,例如定点溢出;三是程序出现某些预先确定要跟踪的事件,跟踪操作主要用于程序调试。有些机器把程序性中断称为“异常”,不称为中断。

    ③ 输入-输出设备中断。

    ④ 外中断。来自控制台中断开关、计时器、时钟或其他设备,这类中断的处理较简单,实时性强。

    ⑤ 调用管理程序。用户程序利用专用指令“调用管理程序”发中断请求,是用户程序和操作系统之间的联系桥梁。

    在51单片机中有5个中断源

    中断号 优先级 中断源 中断入口地址

    0 1(最高) 外部中断0 0003H

    1 2 定时器0 000BH

    2 3 外部中断1 0013H

    3 4 定时器1 001BH

    4 5 串口中断 0023H

    中断优先权

    编辑

    语音

    几个中断请求可能同时出现,但中断系统只能按一定的次序来响应和处理。可最先被响应的中断具有最高优先权,按优先级别顺序进行处理。优先权高低是由中断部件的中断排队线路确定的。

    中断中断级

    当机器设置很多中断源时,为了简化设计,对中断源分组管理。具有相同中断优先权的中断源构成一个中断级。同一级中断使用同一个中断控制程序起点。

    中断中断屏蔽

    对应于各中断级设置相应的屏蔽位。只有屏蔽位为1时,该中断级才能参加中断优先权排队。中断屏蔽位可由专用指令建立,因而可以灵活地调整中断优先权。有些机器针对某些中断源也设置屏蔽位,只有屏蔽位为1时,相应的中断源才起作用。

    展开全文
  • 关注+星标公众号,不错过精彩内容转自 | 痞子衡嵌入式今天给大家分享的是Cortex-M系统中断延迟及其测量方法。在嵌入式领域里,实时性是个经常被我们挂在嘴边的概念,这里的实时性主要强调得...

    关注+星标公众,不错过精彩内容

    b17d5bd021e4fbc3b16d68b6ffdad03e.gif

    转自 | 痞子衡嵌入式

    今天给大家分享的是Cortex-M系统中断延迟及其测量方法

    在嵌入式领域里,实时性是个经常被我们挂在嘴边的概念,这里的实时性主要强调得是当外界事件发生时,系统是否能在规定的时间范围内予以响应处理,这个时间阈值越小,系统的实时性就越高。当然关于这个实时性,也有软硬之分,硬实时要求的是设定的时间阈值内必须完成响应,而软实时则仅需根据任务的优先级尽可能快地完成响应即可。

    无论是 RTOS 环境还是裸机环境下,系统最原始的实时性保障其实来自于 MCU 内核的中断响应能力,关于中断响应能力有一个重要指标叫中断延迟时间,今天我们就来聊一聊 Cortex-M 内核的中断延迟及其测量方法:

    一、什么是系统中断延迟?

    所谓中断延迟,即从中断请求 IRQ 信号置起开始到内核进入执行该中断 ISR 第一条指令时的间隔,如下图所示, 箭头范围内的 11 个周期就是中断延迟时间。关于这个概念,ARM 公司专家 Joseph Yiu 的一篇博客 《Cortex-M内核系统中断延迟入门指南》 介绍得很详细。

    84369eb9bd60ac4cb3c3ddb9e4dfd653.png

    为什么会有中断延迟?其实这是无法避免的,当内核在执行 main thread 代码时,来了中断事件,NVIC 里对应 IRQ 信号被置起,内核接到 NVIC 通知后压栈保存现场(以便中断 ISR 处理完成时回到被打断的 main thread 地方),然后再从中断向量表里取出对应中断 ISR 来执行,这一系列动作是需要时间的。

    Cortex-M 家族发展至今,已有 M0/M0+/M1/M3/M4/M7/M23/M33/M35P/M55 等多款内核,这些内核的中断延迟时间不一,如下表所示。注意表中的数值单位是内核的时钟周期,并且是在零等待内存系统条件下结果(即代码链接在零等待内存里)。

    • Note1: 一般来说 MCU 内部与内核同频的 SRAM 是标准的零等待内存。

    • Note2: 许多运行频率超过 100MHz 的微控制器会搭配非常慢的 Flash 存储器(例如 30 - 50MHz),虽然可以使用 Flash 访问加速硬件来提高性能,但中断延迟仍然受到 Flash 存储系统等待状态的影响。

    0f8ed2ed237ed0c4f5f0e075f66d7c92.png

    二、如何测量系统中断延迟?

    测量 Cortex-M 的中断延迟方法有很多,任何一个带中断的 MCU 外设模块都可以用来测中断延迟,Cortex-M 中断延迟时间跟触发中断的具体外设模块类型基本上是无关的(最多受一点该外设中断信号同步周期的影响)。

    利用 GPIO 模块来测量 Cortex-M 的中断延迟是最简单的方法,选择两个 GPIO,一个配置为输入(GPIO_IN),另一个配置为上拉输出(GPIO_OUT,初态设为高),开启 GPIO_IN 的边沿中断(比如下降沿),在 GPIO_IN 边沿中断 ISR 里翻转两次 GPIO_OUT,然后用示波器测量两个 GPIO 信号边沿间隔得出中断延迟时间。

    uint32_t s_pin_low  = 0x0;
    uint32_t s_pin_high = 0x1;
    
    void GPIO_IN_IRQHandler(void)
    {
        GPIO_OUT->DR = s_pin_low;
        GPIO_OUT->DR = s_pin_high;
    
        ClearInterruptFlag(GPIO_IN);
        __DSB();
    }

    需要注意的是在如上 GPIO_IN_IRQHandler 函数伪代码里,我们对 GPIO_OUT 做了两次翻转,这是有必要的。我们随便选择一个 CM7 芯片去实际编译(IAR环境下,无优化)可以看到如下汇编代码,每一次翻转实际上由四条指令组成,我们作为计时基准的 GPIO_OUT 第一个边沿其实是 ISR 里执行了第一句翻转代码后的时刻,而中断延迟是不包含第一句翻转代码执行时间的。因为指令流水线优化等复杂情况,我们就不从理论上计算四条指令实际消耗时钟周期数,直接再翻转一次 GPIO_OUT,测量 GPIO_OUT 低电平时间即认为是这四条指令执行时间。

    4c295ca5fe6d9e12026808a8731539ae.png

    因此最终中断延迟时间 td = t1 - t2,其中 t1、t2 是我们可以测量出来的时间。此外,td 时间内包含了 tx,这个 tx 是 I/O 跳变信号到芯片内部 IRQ 置起的时间,这个时间是芯片系统所需的同步时间,因芯片设计而异,本应不被计算在系统中断延迟内,但因为这个时间无法测量,故而就放在中断延迟里了,也算是一点小小误差吧。

    下一篇我们将在实际 Cortex-M 芯片上用这种方法去实测中断延迟,敬请期待。

    160f82178d3853f9ecb6a90200c6e5bd.png

    至此,Cortex-M系统中断延迟及其测量方法便介绍完毕了,欢迎大家转发分享。

    ------------ END ------------

    723967deb95690b772612294da3955be.gif

    ●精选 | ST工具、下载编程工具

    ●精选 | 嵌入式软件设计与开发

    ●精选 | 软件工具、 编译器、 编辑器

    迎关注我的公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

    欢迎关注我的视频号:

    d48cd13b9cf8396c948f0c8cff120d01.png

    点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

    展开全文
  • 原标题:Kernel trace tools(一):中断和软中断关闭时间过长问题追踪本文是由字节跳动系统部 STE 团队出品的 “kernel trace tools” 系列文章,以介绍团队自研多类延迟问题追踪工具。在实际工作中,会遇到由于中断...

    原标题:Kernel trace tools(一):中断和软中断关闭时间过长问题追踪

    本文是由字节跳动系统部 STE 团队出品的 “kernel trace tools” 系列文章,以介绍团队自研多类延迟问题追踪工具。

    在实际工作中,会遇到由于中断和软中断关闭时间过长而引发的高网络延迟问题。但是,对于该类问题的定位和追踪缺乏行之有效的方案或客观依据,需要耗费大量时间和精力用于问题排查,Interrupts-off or softirqs-off latency tracer (简称:trace-irqoff)工具便是在该背景下诞生的自研工具。

    目前,trace-irqoff 已开源,如感兴趣详见 Open Source Repo( https://github.com/bytedance/trace-irqoff/tree/master ) 。1. 问题背景

    在工作中,我们经常遇到业务的进程网络延迟高。基于此前分析同类问题的丰富经验,造成上述问题的原因有很多种。我们发现以下两种原因经常出现在我们的视野中。

    hardirq 关闭时间过长。

    softirq 关闭时间过长。

    hardirq 关闭时间过长会导致调度延迟,本地 CPU 的 softirq 也会因得不到执行。我们知道网络收发包就是使用 softirq,因此 hardirq 关闭时间过长必然导致问题。同样,softirq 关闭是一样的,虽不会影响 hardirq,但是它直接影响了 softirq 的执行。

    2. 我们需要什么

    每一次为了确认是否以上原因导致问题,我们经常需要浪费很多的时间。因此有必要开发一款工具专门可以定位这种原因导致的网络延迟问题。我们并不是只求案发现场,我们还要抓住元凶。我们需要知道哪个进程在代码什么位置关闭中断,这很有助于我们高效地解决问题。

    3. 是否有现成的方案

    我们的目的很简单,跟踪 hardirq/softirq 关闭时间。我们有什么办法做到呢?最简单直观的方法应该是在内核开关中断的地方加入 hook 函数,统计开关时间戳即可得到差值,差值即关闭时间。Linux 内核提供打开关闭中断的 API 如下:

    /* hardirq enable/disable api */

    local_irq_disable

    local_irq_enable

    local_irq_save

    local_irq_restore

    /* softirq enable/disable api */

    local_bh_disable

    local_bh_enable

    没错,hardirq 关闭跟踪 Linux 内核中已经有现成的实现方案,我们只需要配置以下 config 选项即可。

    CONFIG_IRQSOFF_TRACER=y

    好了,似乎我们什么都不用做直接使用即可。看起来是这样的,但是这里存在 2 个问题。

    CONFIG_IRQSOFF_TRACER 默认是关闭的,如果需要这么做我们需要重新编译内核,还要重装,最后等待问题复现。这不是我们想要的。

    Linux 内核里面中断开关频繁。即使方案可行,overhead 也会很高。所以再一次不是我们想要的。4. 我们的方法

    我们换一个思路。我们可以利用 hrtimer 判断两次中断之间的时间来判断是否关闭了中断。hrtimer 是 Linux 中的高精度定时器,并且执行上下文是 hardirq。所以可以基于这种方式,虽然不够精确但是足以满足我们的需求。例如,hrtimer 定时周期是 10ms。那么两次中断之间的时间间隔应该是 10ms。如果某次发现两次采样时间间隔是 50ms,可以间接说明关闭中断约 50ms。另外根据采样定理,两次中断之间的时间间隔必须大于 2 倍采样周期(20ms)才能认为中断关闭。所以我们的方法比较明确了,针对每个 CPU 都会启动一个 hrtimer,并且绑定每个 CPU。在 hrtimer handle 里面计算中断时间间隔。这就做到了中断关闭检测了。softirq 该怎么办呢?我们可以如法炮制。在 Linux 内核中,普通定时器 timer 执行上下文就是 softirq。很符合我们需求。所以我们可以按照类似的方法周期性采样,只不过定时器使用的是普通 timer。

    5. 记录元凶的栈

    当我们发现两次中断时间间隔大于阈值时,我们需要知道当前 CPU 究竟在做什么,导致 hardirq/softirq 被关闭。我们可以在中断处理函数中记录当前 CPU 的栈。这么做的前提是,中断处理函数执行时,当前进程不能发生调度,也就是元凶必须在现场。可谓是"抓个现行"。

    5.1 hardirq

    针对 hardirq 关闭情况。当进程在关闭和打开中断之间发生了定时器中断,此时虽不会响应中断,但是硬件会置位 pending。当进程打开中断时,与此同时会响应中断。hardirq handle 会被调用。当前的进程根本没有机会调度,所以这一定是现场,而且当前进程一定是元凶。

    5.2 softirq

    针对 softirq 关闭情况,是否满足上述前提呢?进程调用 local_bh_enable打开 softirq。精简下打开下半部的函数如下:

    void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)

    {

    /*

    * Keep preemption disabled until we are done with

    * softirq processing:

    */

    preempt_count_sub(cnt - 1);

    if (unlikely(!in_interrupt && local_softirq_pending)) {

    do_softirq;

    }

    preempt_count_dec;

    }

    我们可以看到如果有 softirq pending 的话,do_softirq会负责在当前进程上下文处理 softirq。并其他抢占是关闭的状态。所以我们不会调度出去,并且调用 local_bh_enable 会立刻响应普通定时器 timer。所以这也满足以上条件。

    6. softirq 的特殊

    我先回忆下触发 softirq 的执行场景。一共会有 3 个地方。

    irq_exit

    local_bh_enable

    ksoftirqd 进程

    当中断返回时会检查 softirq pending,这是大部分执行 softirq 的场景,由于中断的特殊性,在 timer handle 里面记录当前 CPU 的栈是有效的。local_bh_enable的情况上面已经讨论,可以抓住元凶。但是第 3 种情况下,却不能。这是为什么呢?我们是在 timer handle 里面记录堆栈信息,如果 irq_exit 时 softirq 执行时间过长,会被安排到 ksoftirqd 进程执行。那我们记录的栈信息是什么?仅仅是 ksoftirqd 进程的栈,ksoftirq 并不是元凶。记录是没有用的。所以针对 ksoftirqd 执行的场景,我们需要特殊的处理。我们借助 hrtimer handle。hrtimer 除了测量两次 hardirq 间隔外,还测量 softirq 多长时间没有执行,在合适的时候记录栈。hrtimer 执行的时候,顺便会检测 softirqirq 多长时间没有更新时间戳,如果操作阈值我们也会记录栈信息,这是因为后续如果 softirq 被推迟到 ksoftirqd 进程执行的话,由 softirq 的 timer 记录的栈就没有价值了。

    7. 如何安装

    安装 trace-irqoff 工具很简单,git clone 代码后执行如下命令即可安装。

    make -j8

    make install

    8. 如何使用

    安装 trace-irqoff 工具成功后。会创建如下 /proc/trace_irqoff 目录。

    root@n18-061-206:/proc/trace_irqoff# ls

    distribute enable sampling_period trace_latency

    /proc/trace_irqoff 目录下存在 4 个文件,分别:distribute, enable, sampling_period 和 trace_latency。工具安装后,默认是关闭状态,我们需要手动打开 trace。

    8.1 打开 trace echo 1 > /proc/trace_irqoff/enable

    8.2 关闭 trace echo 0 > /proc/trace_irqoff/enable

    8.3 设置 trace 阈值

    trace-irqoff 工具只会针对关闭中断或者软中断时间超过阈值的情况下记录堆栈信息。因此我们可以通过如下命令查看当前 trace 的阈值:

    root@n18-061-206:/proc/trace_irqoff# cat /proc/trace_irqoff/trace_latency

    trace_irqoff_latency: 50ms

    hardirq:

    softirq:

    默认阈值是 50ms,如第 2 行所示。第 4 行输出 hardirq: 代表下面的栈是可能关闭中断超过阈值的栈。同理,第 6 行是软中断关闭时间超过阈值的栈。如果需要修改阈值至 100ms 可通过如下命令(写入值单位是 ms):

    echo 100 > /proc/trace_irqoff/trace_latency

    8.4 清除栈信息

    当然如果需要清除 /proc/trace_irqoff 记录的栈信息。可以执行如下命令(不会修改阈值为 0):

    echo 0 > /proc/trace_irqoff/trace_latency

    8.5 查看中断关闭次数的统计信息

    如果我们需要知道中断被关闭一定的时间的次数,可以通过如下命令获取统计信息:

    root@n18-061-206:/proc/trace_irqoff# cat distribute

    hardirq-off:

    msecs : count distribution

    20 -> 39 : 1 |********** |

    40 -> 79 : 0 | |

    80 -> 159 : 4 |****************************************|

    160 -> 319 : 2 |******************** |

    320 -> 639 : 1 |********** |

    softirq-off:

    msecs : count distribution

    20 -> 39 : 0 | |

    40 -> 79 : 0 | |

    80 -> 159 : 0 | |

    160 -> 319 : 1 |****************************************|

    在这个例子中,我们看到 hardirq 被关闭时间 x ∈ [80, 159] ms,次数 4 次。softirq 被关闭时间 x ∈ [160, 319] ms,次数 1 次 如果没有任何信息输出,这说明没有任何地方关闭中断时间超过 20ms。8.6 修改采样周期

    从上面一节我们可以看到,中断关闭时间分布图最小粒度是 20ms。这是因为采样周期是 10ms。根据采样定理,大于等于 2 倍采样周期时间才能反映真实情况。如果需要提高统计粒度,可修改采样周期时间。例如修改采样周期为 1ms,可执行如下命令(必须在 tracer 关闭的情况下操作有效):

    # 单位 ms,可设置最小的采样周期是 1ms。

    echo 1 > /proc/trace_irqoff/sampling_period

    9. 案例分析 9.1 hardirq 关闭

    我们使用如下示意测试程序,关闭中断 100ms。查看 trace_irqoff 文件内容。

    static void disable_hardirq(unsigned long latency)

    {

    local_irq_disable;

    mdelay(latency);

    local_irq_enanle;

    }

    通过模块测试以上代码,然后查看栈信息。

    root@n18-061-206:/proc/trace_irqoff# cat trace_latency

    trace_irqoff_latency: 50ms

    hardirq:

    cpu: 17

    COMMAND: bash PID: 22840 LATENCY: 107ms

    trace_irqoff_hrtimer_handler+0x39/0x99 [trace_irqoff]

    __hrtimer_run_queues+0xfa/0x270

    hrtimer_interrupt+0x101/0x240

    smp_apic_timer_interrupt+0x5e/0x120

    apic_timer_interrupt+0xf/0x20

    disable_hardirq+0x5b/0x70

    proc_reg_write+0x36/0x60

    __vfs_write+0x33/0x190

    vfs_write+0xb0/0x190

    ksys_write+0x52/0xc0

    do_syscall_64+0x4f/0xe0

    entry_SYSCALL_64_after_hwframe+0x44/0xa9

    softirq:

    我们可以看到 hardirq 一栏记录 cpu17 执行 bash 命令,关闭中断 107ms(误差 10ms 之内)。其栈信息对应 disable_hardirq 函数中。第 20 行 softirq 一栏没有信息,说明没有记录 softirq 被关闭的栈。

    9.2 softirq 关闭

    我们使用如下示意测试程序,关闭 softirq 100ms。查看 trace_irqoff 文件内容。

    static void disable_softirq(unsigned long latency)

    {

    local_bh_disable;

    mdelay(latency);

    local_bh_enanle;

    }

    通过模块测试以上代码,然后查看栈信息。

    root@n18-061-206:/proc/trace_irqoff# cat trace_latency

    trace_irqoff_latency: 50ms

    hardirq:

    softirq:

    cpu: 17

    COMMAND: bash PID: 22840 LATENCY: 51+ms

    trace_irqoff_hrtimer_handler+0x97/0x99 [trace_irqoff]

    __hrtimer_run_queues+0xfa/0x270

    hrtimer_interrupt+0x101/0x240

    smp_apic_timer_interrupt+0x5e/0x120

    apic_timer_interrupt+0xf/0x20

    delay_tsc+0x3c/0x50

    disable_softirq+0x4b/0x80

    proc_reg_write+0x36/0x60

    __vfs_write+0x33/0x190

    vfs_write+0xb0/0x190

    ksys_write+0x52/0xc0

    do_syscall_64+0x4f/0xe0

    entry_SYSCALL_64_after_hwframe+0x44/0xa9

    COMMAND: bash PID: 22840 LATENCY: 106ms

    trace_irqoff_timer_handler+0x3a/0x60 [trace_irqoff]

    call_timer_fn+0x29/0x120

    run_timer_softirq+0x16c/0x400

    __do_softirq+0x108/0x2b8

    do_softirq_own_stack+0x2a/0x40

    do_softirq.part.21+0x56/0x60

    __local_bh_enable_ip+0x60/0x70

    disable_softirq+0x62/0x80

    proc_reg_write+0x36/0x60

    __vfs_write+0x33/0x190

    vfs_write+0xb0/0x190

    ksys_write+0x52/0xc0

    do_syscall_64+0x4f/0xe0

    entry_SYSCALL_64_after_hwframe+0x44/0xa9

    针对 softirq 关闭情况,有 2 个栈与之对应。我们注意到第 9 行的函数名称和第 24 行的函数名称是不一样的。第 9 行的栈是硬件中断 handler 捕捉到软中断关闭,第 24 行是软中断 handler 捕捉到软中断被关闭。正常情况下,我们以 24 行开始的栈为分析目标即可。当 24 行的栈是无效的时候,可以看第 9 行的栈。这里注意:第 9 行的 lantency 提示信息 51+ms 是阈值信息。并非实际 latency(所以我在后面添加一个'+'字符,表示 latency 大于 51ms)。实际的 latency 是第 24 行显示的 106ms。下面就看下为什么 2 个栈是有必要的。

    9.3 ksoftirqd 延迟

    我们看一个曾经处理的一个实际问题:

    root@n115-081-045:/proc/trace_irqoff# cat trace_latency

    trace_irqoff_latency: 300ms

    hardirq:

    softirq:

    cpu: 4

    COMMAND: lxcfs PID: 4058797 LATENCY: 303+ms

    trace_irqoff_record+0x12b/0x1b0 [trace_irqoff]

    trace_irqoff_hrtimer_handler+0x97/0x99 [trace_irqoff]

    __hrtimer_run_queues+0xdc/0x220

    hrtimer_interrupt+0xa6/0x1f0

    smp_apic_timer_interrupt+0x62/0x120

    apic_timer_interrupt+0x7d/0x90

    memcg_sum_events.isra.26+0x3f/0x60

    memcg_stat_show+0x323/0x460

    seq_read+0x11f/0x3f0

    __vfs_read+0x33/0x160

    vfs_read+0x91/0x130

    SyS_read+0x52/0xc0

    do_syscall_64+0x68/0x100

    entry_SYSCALL_64_after_hwframe+0x3d/0xa2

    COMMAND: ksoftirqd/4 PID: 34 LATENCY: 409ms

    trace_irqoff_record+0x12b/0x1b0 [trace_irqoff]

    trace_irqoff_timer_handler+0x3a/0x60 [trace_irqoff]

    call_timer_fn+0x2e/0x130

    run_timer_softirq+0x1d4/0x420

    __do_softirq+0x108/0x2a9

    run_ksoftirqd+0x1e/0x40

    smpboot_thread_fn+0xfe/0x150

    kthread+0xfc/0x130

    ret_from_fork+0x1f/0x30

    我们看到下面的进程 ksoftirqd/4 的栈,延迟时间是 409ms。ksoftirqd 进程是 kernel 中处理 softirq 的进程。因此这段栈对我们是没有意义的,因为元凶已经错过了。所以此时,我们可以借鉴上面的栈信息,我们看到当 softirq 被延迟 303ms 的时候,当前 CPU 正在执行的进程是 lxcfs。并且栈是 memory cgroup 相关。因此,我们基本可以判断 lxcfs 进程执行时间过长,由于 kernel 态不支持抢占,因此导致 ksoftirqd 进程没有机会得到运行。

    10. 总结

    根据字节内部实践来看,trace-irqoff 安装便捷且使用灵活度高,能将问题定位耗时缩短至分钟级别,使用收益可观,并且已经通过该工具定位很多问题,提高了工作效率。

    更多分享 字节跳动系统部 STE 团队

    字节跳动系统部 STE 团队 一直致力于操作系统内核与虚拟化、系统基础软件与基础库的构建和性能优化、超大规模数据中心的稳定性和可靠性建设、新硬件与软件的协同设计等系统基础技术领域的研发与工程化落地,具备全面的基础软件工程能力,为字节上层业务保驾护航。同时,团队积极关注社区技术动向,拥抱开源和标准。欢迎更多有志之士加入,如有意向可发送简历至:sysrecruitment@bytedance.com。

    欢迎关注字节跳动技术团队

    责任编辑:

    展开全文
  • 本文主要来说明 IOIOIO 外设触发的中断,总的来说一个中断的起末会经历设备,中断控制器,CPU 三个阶段:设备产生中断信号,中断控制器翻译信号,CPU 来实际处理信号。 本文用 xv6xv6xv6 的实例来讲解多处理器下的...

    INTERRUPT

    中断是硬件和软件交互的一种机制,可以说整个操作系统,整个架构都是由中断来驱动的。中断的机制分为两种,中断和异常,中断通常为 I O IO IO 设备触发的异步事件,而异常是 C P U CPU CPU 执行指令时发生的同步事件。本文主要来说明 I O IO IO 外设触发的中断,总的来说一个中断的起末会经历设备,中断控制器,CPU 三个阶段:设备产生中断信号,中断控制器翻译信号,CPU 来实际处理信号

    本文用 x v 6 xv6 xv6 的实例来讲解多处理器下的中断机制,从头至尾的来看一看,中断经历的三个过程。其中第一个阶段设备如何产生信号不讲,超过了操作系统的范围,也超过了我的能力范围。各种硬件外设有着自己的执行逻辑,有各种形式的中断触发机制,比如边沿触发,电平触发等等。总的来说就是向中断控制器发送一个中断信号,中断控制器再作翻译发送给 C P U CPU CPU C P U CPU CPU 再执行中断服务程序对中断进行处理。

    中断控制器

    说到中断控制器,是个什么东西?中断控制器可以看作是中断的代理,外设是很多的,如果没有一个中断代理,外设想要给 C P U CPU CPU 发送中断信号来处理中断,那只能是外设连接在 C P U CPU CPU 的管脚上, C P U CPU CPU 的管脚是很宝贵的,不可能拿出那么多管脚去连接外设。所以就有了中断控制器这个中断代言人,所有的 I O IO IO 外设连接其上,发送中断请求时就向中断控制器发送信号,中断控制器再通知 CPU,如此便解决了上述问题。

    中断控制器有很多,前文讲过 PIC​,PIC 只用于单处理器,对于如今的多核多处理器时代,PIC 无能为力,所以出现了更高级的中断控制器 APICAPIC( A d v a n c e d   P r o g r a m m a b l e   I n t e r r u p t   C o n t r o l l e r Advanced\ Programmable\ Interrupt\ Controller Advanced Programmable Interrupt Controller) 高级可编程中断控制器,APIC 分成两部分 LAPICIOAPIC,前者 LAPIC 位于 C P U CPU CPU 内部,每个 C P U CPU CPU 都有一个 LAPIC,后者 IOAPIC 与外设相连。外设发出的中断信号经过 IOAPIC 处理之后发送给 LAPIC,再由 LAPIC 决定是否交由 C P U CPU CPU 进行实际的中断处理。

    可以看出每个 C P U CPU CPU 上有一个 LAPICIOAPIC 是系统芯片组一部分,各个中断消息通过总线发送接收。关于 APIC 的内容很多也很复杂,详细描述的可以参考 i n t e l intel intel 开发手册卷三,本文不探讨其中的细节,只在上层较为抽象的层面讲述,理清 APIC 模式下中断的过程。

    计算机启动的时候要先对 APIC 进行初始化,后续才能正确使用,下面来看看 APIC 在一种较为简单的工作模式下的初始化过程:

    IOAPIC

    初始化 IOAPIC 就是设置 IOAPIC 的寄存器IOAPIC 寄存器一览:

    所以有了以下定义:

    #define REG_ID     0x00  // Register index: ID
    #define REG_VER    0x01  // Register index: version
    #define REG_TABLE  0x10  // Redirection table base  重定向表
    

    但是这些寄存器是不能直接访问的,需要通过另外两个映射到内存的寄存器来读写上述的寄存器

    内存映射的两个寄存器

    这两个寄存器是内存映射的,IOREGSEL​,地址为 0 x F E C 0   0000 0xFEC0\ 0000 0xFEC0 0000;​IOWIN​,地址为 0 x F E C 0   0010 h 0xFEC0\ 0010h 0xFEC0 0010hIOREGSEL 用来指定要读写的寄存器,然后从 IOWIN 中读写。也就是常说的 index/data 访问方式,或者说 a d r e s s / d a t a adress/data adress/data,用 index 端口指定寄存器,从 data 端口读写寄存器,data 端口就像是所有寄存器的窗口。

    而所谓内存映射,就是把这些寄存器看作内存的一部分,读写内存,就是读写寄存器,可以用访问内存的指令比如 mov 来访问寄存器。还有一种是 IO端口映射,这种映射方式是将外设的 IO端口(外设的一些寄存器) 看成一个独立的地址空间,访问这片空间不能用访问内存的指令,而需要专门的 in/out 指令来访问

    通过 IOREGSELIOWIN 既可以访问到 IOAPIC 所有的寄存器,所以结构体 i o a p i c ioapic ioapic 如下定义:

    struct ioapic {
      uint reg;       //IOREGSEL
      uint pad[3];    //填充12字节
      uint data;      //IOWIN
    };
    

    填充 12 12 12 字节是因为 IOREGSEL 0 x F E C 0   0000 0xFEC0\ 0000 0xFEC0 0000,长度为 4 字节,IOWIN 0 x F E C 0   0010 0xFEC0\ 0010 0xFEC0 0010,两者中间差了 $1$2 字节,所以填充 12 12 12 字节补上空位方便操作。

    通过 IOREGSEL 选定寄存器,然后从IOWIN中读写相应寄存器,因此也能明白下面两个读写函数:

    static uint ioapicread(int reg)
    {
      ioapic->reg = reg;    //选定寄存器reg
      return ioapic->data;  //从窗口寄存器中读出寄存器reg数据
    }
    
    static void ioapicwrite(int reg, uint data)
    {
      ioapic->reg = reg;    //选定寄存器reg
      ioapic->data = data;  //向窗口寄存器写就相当于向寄存器reg写
    }
    

    这两个函数就是根据 i n d e x / d a t a index/data index/data 来读写 IOAPIC 的寄存器。下面来看看 IOAPIC 寄存器分别有些什么意义,了解了之后自然就知道为什么要这样那样的初始化了。下面只说 x v 6 xv6 xv6 中涉及到的寄存器,其他的有兴趣见文末链接。

    IOAPIC 寄存器

    ID Register

    • 索引为 0

    • b i t 24 − b i t 27 bit24 - bit27 bit24bit27:ID

    Version Register

    • 索引为 1

    • b i t 0 − b i t 7 bit0-bit7 bit0bit7 表示版本,

    • b i t 16 − b i t 23 bit16-bit23 bit16bit23 表示重定向表项最多有几个,这里就是 23(从 0 开始计数)

    重定向表项

    IOAPIC 有 24 个管脚,每个管脚都对应着一个 64 位的重定向表项(也相当于 64 位的寄存器),保存在 0 x 10 − 0 x 3 F 0x10-0x3F 0x100x3F,重定向表项的格式如下所示:

    来源于 interrupt in linux

    来源于 interrupt in linux

    来源于 interrupt in linux

    这是 Z X _ W I N G ZX\_WING ZX_WING 大佬在他的 I n t e r r u p t   i n   L i n u x Interrupt\ in\ Linux Interrupt in Linux 中总结出来的,很全面也很复杂,这里有所了解就好,配合着下面的初始化代码对部分字段作相应的解释。

    IOAPIC 初始化

    #define IOAPIC  0xFEC00000   // Default physical address of IO APIC
    
    void ioapicinit(void)
    {
      int i, id, maxintr;
    
      ioapic = (volatile struct ioapic*)IOAPIC;      //IOREGSEL的地址
      maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;  //读取version寄存器16-23位,获取最大的中断数
      id = ioapicread(REG_ID) >> 24;      //读取ID寄存器24-27 获取IOAPIC ID
      if(id != ioapicid)
        cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");
    
      // Mark all interrupts edge-triggered, active high, disabled,
      // and not routed to any CPUs.  将所有的中断重定向表项设置为边沿,高有效,屏蔽状态
      for(i = 0; i <= maxintr; i++){   
        ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));  //设置低32位,每个表项64位,所以2*i,
        ioapicwrite(REG_TABLE+2*i+1, 0);   //设置高32位
      }
    }
    

    宏定义 I O A P I C IOAPIC IOAPIC 是个地址值,这个地址就是 IOREGSEL 寄存器在内存中映射的位置,通过 i n d e x / d a t a index/data index/data 方式读取 ID,支持的中断数等信息。

    I O A P I C   I D IOAPIC\ ID IOAPIC ID M P   C o n f i g u r a t i o n   T a b l e   E n t r y MP\ Configuration\ Table\ Entry MP Configuration Table Entry 中有记录,关于 M P   T a b l e MP\ Table MP Table 我们在 @@@@@@@@@@@ 一文中提到过,简单来说, M P   T a b l e MP\ Table MP Table 有各种表项,记录了多处理器下的一些配置信息,计算机启动的时候可以从中获取有用信息。多处理器下的计算机启动@@@@一文只说明了处理器类型的表项,有多少个处理器类型的表项表示有多少个处理器。IOAPIC 同样的道理,然后每个 I O A P I C IOAPIC IOAPIC 类型的表项中有其 I D ID ID 记录。关于 M P   T a b l e MP\ Table MP Table 咱们就点到为止,有兴趣的可以去公众号后台获取 M P   S p e c MP\ Spec MP Spec 的资料文档,有详细的解释。

    接着就是一个 f o r for for 循环,来初始化 24 个重定向表项。来看看设置了哪些内容:

    • T _ I R Q 0 + i T\_IRQ0+i T_IRQ0+i,这个表示中断向量号,一个中断向量号就表示一个中断。表明此重定向表项处理 T _ I R Q 0 + i T\_IRQ0+i T_IRQ0+i 这个中断。
    • # d e f i n e    I N T _ D I S A B L E D    0 x 00010000 \#define\ \ INT\_DISABLED\ \ 0x00010000 #define  INT_DISABLED  0x00010000,设置此位来屏蔽与该重定向表项相关的中断,也就是说当硬件外设向 IOAPIC 发送中断信号时,IOAPIC 直接屏蔽忽略。
    • 设置 b i t 13 bit13 bit13 b i t 15 bit15 bit15 为 0, 分别表示管脚高电平有效,触发模式为边沿触发,这是数字逻辑中的概念,应该都知道吧,不知的话需要去补补了,基本东西还是需要知道。
    • 设置 b i t 11 bit11 bit11 为 0 表示 P h y s i c a l   M o d e Physical\ Mode Physical Mode,设置高 8 位的 D e s t i n a t i o n   F i e l d Destination\ Field Destination Field 为 0。 P h y s i c a l   M o d e Physical\ Mode Physical Mode 模式下, D e s t i n a t i o n   F i e l d Destination\ Field Destination Field 字段就表示 L A P I C   I D LAPIC\ ID LAPIC ID L A P I C   I D LAPIC\ ID LAPIC ID 又唯一标识一个 C P U CPU CPU,所以 D e s t i n a t i o n   F i e l d Destination\ Field Destination Field 就表示此中断会路由到该 C P U CPU CPU,交由该 C P U CPU CPU 来处理

    因此这里初始化将所有重定向表项设置为边沿触发,高电平有效,所有中断路由到 C P U 0 CPU0 CPU0,但又将所有中断屏蔽的状态。 x v 6 xv6 xv6 的注释描述的是不会将中断路由到任何处理器,这里我认为是有误的,虽然屏蔽了所有中断,但是根据 D e s t i n a t i o n   F i e l d Destination\ Field Destination Field 字段来看应该是路由到 C P U 0 CPU0 CPU0 的,若我理解错还请批评指针。

    另外为什么要加上一个 T _ I R Q 0 T\_IRQ0 T_IRQ0 呢, T _ I R Q 0 T\_IRQ0 T_IRQ0 是个宏,值为 32,前 32 个中断向量号分配给了一些异常或者保留,后面的中断向量号 32~255 才是一些外部中断或者 INT n 指令可以使用的

    上述 IOAPIC 初始化的时候直接将管脚对应的中断全都给屏蔽了,那总得有开启的时候吧,不然无法工作也就无意义了,“开启”函数如下所示:

    void ioapicenable(int irq, int cpunum)
    {
      // Mark interrupt edge-triggered, active high,
      // enabled, and routed to the given cpunum,
      // which happens to be that cpu's APIC ID.     调用此函数使能相应的中断
      ioapicwrite(REG_TABLE+2*irq, T_IRQ0 + irq);
      ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24);  //左移24位是填写 destination field字段
    }
    

    T _ I R Q 0 + i r q T\_IRQ0 + irq T_IRQ0+irq 为中断向量号,填写到低 8 位 vector 字段,表示此重定向表项处理该中断

    c p u n u m cpunum cpunum 为 CPU 的编号, m p . c mp.c mp.c 文件中定义了关于 C P U CPU CPU 的全局数组,存放着所有 C P U CPU CPU 的信息。 x v 6 xv6 xv6 里面,这个数组的索引是就是 c p u n u m cpunum cpunum 也是 L A P I C   I D LAPIC\ ID LAPIC ID,可以来唯一标识一个 C P U CPU CPU。初始化的时候 D e s t i n a t i o n   M o d e Destination\ Mode Destination Mode 为 0,调用此函数没有改变该位,所以还是 0,为物理模式,所以将 c p u n u m cpunum cpunum 写入 D e s t i n a t i o n   F i e l d Destination\ Field Destination Field 字段表示将中断路由到该 C P U CPU CPU

    来做个简单测试,在磁盘相关代码文件 i d e . c ide.c ide.c 中函数 i d e i n i t ( ) ideinit() ideinit() 调用了 i o a p i c e n a b l e ( ) ioapicenable() ioapicenable()

    ioapicenable(IRQ_IDE, ncpu - 1);     //让这个CPU来处理硬盘中断
    

    根据上述讲的,这说明使用最后一个 C P U CPU CPU 来处理磁盘中断,下面我们来验证,验证方式很简单,在中断处理程序当中打印 C P U CPU CPU 编号就行:

    首先在 M a k e f i l e Makefile Makefile 中将 C P U CPU CPU 数量设为多个处理器,我设置的是 4:

    ifndef CPUS
    CPUS := 4
    endif
    

    接着在 t r a p . c trap.c trap.c 文件中添加 p r i n t f printf printf 语句:

    case T_IRQ0 + IRQ_IDE:    //如果是磁盘中断
        ideintr();            //调用磁盘中断程序
        lapiceoi();           //处理完写EOI表中断完成
        cprintf("ide %d\n", cpuid());  //打印CPU编号
        break;
    

    这个函数我们后面会讲到,这里提前看一看,有注释应该还是很好理解的,来看看结果:

    C P U CPU CPU 的数量为 4,处理磁盘中断的 C P U CPU CPU 编号为 3,符合预期, I O A P I C IOAPIC IOAPIC 的初始化就说到这里,下面来看 L A P I C LAPIC LAPIC 的初始化。

    LAPIC

    LAPIC 要比 IOAPIC 复杂的多,放张总图:

    x v 6 xv6 xv6 不会涉及这么复杂,其主要功能是接收 IOAPIC 发来的中断消息然后交由 C P U CPU CPU 处理,再者就是自身也能作为中断源产生中断发送给自身或其他 C P U CPU CPU。同样的初始化 LAPIC 就是设置相关寄存器,但是 LAPIC 的寄存器实在太多了,本文只是说明 xv6 涉及到的寄存器,其他的可以参考前文@@@@@@@@@@@,或者文末的链接。

    LAPIC 的寄存器在内存中都有映射,起始地址一般默认为 0 x F E E 0   0000 0xFEE0\ 0000 0xFEE0 0000,但这个地址不是自己设置使用的,起始地址在 M P   T a b l e   H e a d e r MP\ Table\ Header MP Table Header 中可以获取,详见文末链接@@@@@@@@@@,所以可以如下定义和获取 l a p i c lapic lapic 地址

    /*lapic.c*/
    volatile uint *lapic;  // Initialized in mp.c
    
    /*mp.c*/
    lapic = (uint*)conf->lapicaddr;  //conf就是MP Table Header,其中记录着LAPIC地址信息
    

    l a p i c lapic lapic 也可以看作是 u i n t uint uint 型的数组,一个元素 4 字节,所以计算各个寄存器的索引的时候要在偏移量的基础上除以 4。举个例子,ID 寄存器相对 l a p i c lapic lapic 基地址偏移量为 0 x 20 0x20 0x20,那么 ID 寄存器在 l a p i c lapic lapic 数组里面的索引就该为 0x20/4。各个寄存器的偏移量见文末链接(说了太多次,希望不要觉得太啰嗦,因为内容实在太多,又想说明白那就只能这样放链接)

    因为是 LAPIC 的寄存器是内存映射,所以设置寄存器就是直接读写相应内存,因此读写寄存器的函实现是很简单的:

    static void lapicw(int index, int value)   //向下标为index的寄存器写value
    {
      lapic[index] = value;
      lapic[ID];  // wait for write to finish, by reading  
    }
    

    这里看着是写内存,但是实际上这部分地址已经分配给了 LAPIC,对硬件的写操作一般要停下等一会儿待写操作完成,可以去看看磁盘键盘等硬件初始配置的时候都有类似的等待操作,这里直接采用读数据的方式来等待写操作完成。

    LAPIC 初始化

    有了读写 LAPIC 寄存器的函数,接着就来看看 LAPIC 如何初始化的,初始化函数为 l a p i c i n i t ( ) lapicinit() lapicinit(),我们分开来看:

    lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
    
    #define SVR     (0x00F0/4)   // Spurious Interrupt Vector
      #define ENABLE     0x00000100   // Unit Enable
    

    SVR 伪中断寄存器, C P U CPU CPU 每响应一次 I N T R INTR INTR(可屏蔽中断),就会连续执行两个 I N T A INTA INTA 周期。在 M P   S p e c MP\ Spec MP Spec 中有描述,当一个中断在第一个 I N T A INTA INTA 周期后,第二个 I N T A INTA INTA 周期前变为无效,则为伪中断,也就是说伪中断就是中断引脚没有维持足够的有效电平而产生的。这主要涉及到电气方面的东西,我们了解就好。

    S V R SVR SVR 中的字段还有其他作用, b i t   8 bit\ 8 bit 8 置 1 表示使能 LAPICLAPIC 需要在使能状态下工作。

    lapicw(TDCR, X1);   //设置分频系数
    lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));  //设置Timer的模式和中断向量号
    lapicw(TICR, 10000000);  //设置周期性计数的数字
    
    #define TICR    (0x0380/4)   // Timer Initial Count
    #define TDCR    (0x03E0/4)   // Timer Divide Configuration
    
    #define TIMER   (0x0320/4)   // Local Vector Table 0 (TIMER)
      #define X1         0x0000000B   // divide counts by 1
      #define PERIODIC   0x00020000   // Periodic
    

    LAPIC 自带可编程定时器,可以用这个定时器来作为时钟,触发时钟中断。这需要 T D C R ( T h e   D i v i d e   C o n f i g u r a t i o n   R e g i s t e r ) TDCR(The\ Divide\ Configuration\ Register) TDCR(The Divide Configuration Register) T I C R ( T h e   I n i t i a l − C o u n t   R e g i s t e r ) TICR(The\ Initial-Count\ Register) TICR(The InitialCount Register)、以及 L V T   T i m e r   R e g i s t e r LVT\ Timer\ Register LVT Timer Register 配合使用,其实还有一个 C u r r e n t − c o u n t   R e g i s t e r Current-count\ Register Currentcount Register x v 6 xv6 xv6 没有使用,这些寄存器的具体配置如上代码所示,解释如下:

    这几个寄存器表示 L V T ( L o c a l   V e c t o r   T a b l e ) LVT(Local\ Vector\ Table) LVT(Local Vector Table) 本地中断,LAPIC 除了可以接收 IOAPIC 发来的中断之外,自己也可以产生中断,就是上述列出来的这几种。

    从上图可以看出 T i m e r Timer Timer 寄存器 b i t 17 , b i t 18 bit17,bit18 bit17,bit18 设置 T i m e r   M o d e Timer\ Mode Timer Mode x v 6 xv6 xv6 设置为 01 01 01 P e r i o d i c Periodic Periodic 模式,从名字就可以看出这是周期性模式,周期性的从某个数递减到 0,如此循环往复。

    这个数设置在 T I C R TICR TICR 寄存器, x v 6 xv6 xv6 设置的值是 10000000 10000000 10000000

    递减得有个频率,这个频率是系统的总线频率再分频,分频系数设置在 T D C R TDCR TDCR 寄存器, x v 6 xv6 xv6 设置的是 1 分频,也就相当于没有分频,就是使用的是总线频率。

    另外 T _ I R Q 0 + I R Q _ T I M E R T\_IRQ0 + IRQ\_TIMER T_IRQ0+IRQ_TIMER 是时钟中断的向量号,设置在 T i m e r Timer Timer 寄存器的低 8 位。

    关于时钟中断的设置就是这么多,每个 C P U CPU CPU 都有 L A P I C LAPIC LAPIC,所以每个 C P U CPU CPU 上都会发生时钟中断,不像其他中断,指定了一个 C P U CPU CPU 来处理。

    回到 LAPIC 的初始化上面来:

    // Disable logical interrupt lines.
    lapicw(LINT0, MASKED);
    lapicw(LINT1, MASKED);
    

    L I N T 0 , L I N T 1 LINT0,LINT1 LINT0LINT1连接到了 i 8259 A i8259A i8259A N M I NMI NMI,但实际上只连接到了 B S P BSP BSP(最先启动的 C P U CPU CPU),只有 B S P BSP BSP 能接收这两种中断。一般对于 B S P BSP BSP 如果有 P I C PIC PIC 模式(兼容 i 8259 i8259 i8259) L I N T 0 LINT0 LINT0 设置为 E x t I N T ExtINT ExtINT 模式, L I N T 1 LINT1 LINT1 设置为 N M I NMI NMI 模式。如果是 A P AP AP 直接设置屏蔽位将两种中断屏蔽掉。 x v 6 xv6 xv6 简化了处理,只使用 APIC 模式,所有的 LAPIC 都将两种中断给屏蔽掉了。

    if(((lapic[VER]>>16) & 0xFF) >= 4)
    	lapicw(PCINT, MASKED);
    
    // Map error interrupt to IRQ_ERROR.
    lapicw(ERROR, T_IRQ0 + IRQ_ERROR);
    
    // Clear error status register (requires back-to-back writes).
    lapicw(ESR, 0);
    lapicw(ESR, 0);
    
    #define VER     (0x0030/4)   // Version
    #define ERROR   (0x0370/4)   // Local Vector Table 3 (ERROR)
    #define PCINT   (0x0340/4)   // Performance Counter LVT
    #define ESR     (0x0280/4)   // Error Status
    

    Version Register​ 的 b i t 16 − b i t 23 bit16-bit23 bit16bit23 L V T LVT LVT 本地中断的表项个数,如果超过了 4 项则屏蔽性能计数溢出中断。为什么这么操作,这个中断有什么用不太清楚,这个在 intel 手册卷三有描述,看了之后还是懵懵懂懂,感觉平常不会接触,用到的少,就没深入的去啃了,所以也不能拿出来乱说,在此抱歉,有了解的大佬还请告知。

    ERROR Register​,设置这个寄存器来映射 E R R O R ERROR ERROR 中断,当 $APIC $检测到内部错误的时候就会触发这个中断,中断向量号是 T _ I R Q 0 + I R Q _ E R R O R T\_IRQ0 + IRQ\_ERROR T_IRQ0+IRQ_ERROR

    E S R ( E R R O R   S t a t u s   R e g i s t e r ) ESR(ERROR\ Status\ Register) ESR(ERROR Status Register) 记录错误状态,初始化就是将其清零,而且需要连续写两次。

    lapicw(EOI, 0);
    #define EOI     (0x00B0/4)   // EOI
    

    EOI( E n d   o f   I n t e r r u p t End\ of\ Interrupt End of Interrupt),中断处理完成之后要写 EOI 寄存器来显示表示中断处理已经完成。重置初始化后的值应为 0.

    lapicw(ICRHI, 0);
    lapicw(ICRLO, BCAST | INIT | LEVEL);
    while(lapic[ICRLO] & DELIVS)
    	;
    
    #define ICRHI   (0x0310/4)   // Interrupt Command [63:32]
    #define TIMER   (0x0320/4)   // Local Vector Table 0 (TIMER)
    //ICR寄存器的各字段取值意义
      #define INIT       0x00000500   // INIT/RESET
      #define STARTUP    0x00000600   // Startup IPI
      #define DELIVS     0x00001000   // Delivery status
      #define ASSERT     0x00004000   // Assert interrupt (vs deassert)
      #define DEASSERT   0x00000000
      #define LEVEL      0x00008000   // Level triggered
      #define BCAST      0x00080000   // Send to all APICs, including self.
      #define BUSY       0x00001000
      #define FIXED      0x00000000
    

    ICR( I n t e r r u p t   C o m m a n d   R e g i s t e r Interrupt\ Command\ Register Interrupt Command Register)中断指令寄存器,当一个 C P U CPU CPU 想把中断发送给另一个 C P U CPU CPU 时,就在 ICR 中填写相应的中断向量和目标 LAPIC 标识,然后通过总线向目标 LAPIC 发送消息。因为同样是向另一个 LAPIC 发送中断消息,所以ICR 寄存器的字段和 IOAPIC 重定向表项较为相似,都有 D e s t i n a t i o n   F i e l d , D e l i v e r y   M o d e , D e s t i n a t i o n   M o d e , L e v e l Destination\ Field, Delivery\ Mode, Destination\ Mode, Level Destination Field,Delivery Mode,Destination Mode,Level 等等。

    S e n d   a n   I n i t   L e v e l   D e − A s s e r t   t o   s y n c h r o n i s e   a r b i t r a t i o n   I D ′ s Send\ an\ Init\ Level\ De-Assert\ to\ synchronise\ arbitration\ ID's Send an Init Level DeAssert to synchronise arbitration IDs. 结合 i n t e l intel intel 手册,作用为将所有 C P U CPU CPUAPIC A r b   I D Arb\ ID Arb ID 设置为初始值 A P I C   I D APIC\ ID APIC ID

    关于 Arb,引用 I n t e r r u p t i n L i n u x Interrupt in Linux InterruptinLinux 中的解释:

    Arb,Arbitration Register,仲裁寄存器。该寄存器用 4 个 bit 表示 0~15 共 16 个优先级(15 为最高优先级),用于确定 LAPIC 竞争 APIC BUS 的优先级。系统 RESET 后,各 LAPIC 的 Arb 被初始化为其 LAPIC ID。总线竞争时,Arb 值最大 的 LAPIC 赢得总线,同时将自身的 Arb 清零,并将其它 LAPIC 的 Arb 加一。由 此可见,Arb 仲裁是一个轮询机制。Level 触发的 INIT IPI 可以将各 LAPIC 的 Arb 同步回当前的 LAPIC ID。

    // Enable interrupts on the APIC (but not on the processor).
    lapicw(TPR, 0);
    #define TPR     (0x0080/4)   // Task Priority
    

    任务优先级寄存器,确定当前 CPU 能够处理什么优先级别的中断,CPU 只处理比 TPR 中级别更高的中断。比它低的中断暂时屏蔽掉,也就是在 IRR 中继续等到

    上述就是 x v 6 xv6 xv6 里面对 LAPIC 的一种简单的初始化方式,其实也不简单,涉及了挺多东西。接下来应该是 CPU 来处理中断的部分,在这之前先来看看 l a p i c . c lapic.c lapic.c 里面涉及到的两个用的比较多的函数:

    int lapicid(void)   //返回 CPU/LAPIC ID
    {
      if (!lapic)
        return 0;
      return lapic[ID] >> 24;
    }
    

    这个函数用来返回 L A P I C   I D LAPIC\ ID LAPIC IDID 寄存器 b i t 24 bit24 bit24 位后表示 L A P I C   I D LAPIC\ ID LAPIC ID因为 C P U CPU CPULAPIC 一一对应,所以这也相当于返回 C P U   I D CPU\ ID CPU ID,同样也是 C P U CPU CPU 数组中的索引。而前面在 I O A P I C IOAPIC IOAPIC 一节中出现的 c p u i d ( ) cpuid() cpuid() 函数相当于就是这个函数的封装。

    void lapiceoi(void)
    {
      if(lapic)
        lapicw(EOI, 0);
    }
    

    EOI 表中断完成,这个函数在中断服务程序中会经常用到用到,下面再来看看 LAPIC 中两个比较重要的寄存器:

    • IRR 中断请求寄存器,256 位,每位代表着一个中断。当某个中断消息发来时,如果该中断没有被屏蔽,则将 IRR 对应的 bit 置 1,表示收到了该中断请求但 CPU 还未处理

    • ISR 服务中寄存器 ,256 位,每位代表着一个中断。当 IRR 中某个中断请求发送给 CPU 时,ISR 对应的 bit 上便置 1,表示 CPU 正在处理该中断

    上述就是 APIC 的初始化和一些重要函数的讲解,有了这些了解之后,来总体的看一看 APIC 部分的中断过程:

    1. 外设触发中断,发送中断信号给 IOAPIC
    2. IOAPIC 根据 P R T PRT PRT 表将中断信号翻译成中断消息,然后发送给 D e s t i n a t i o n   F i e l d Destination\ Field Destination Field 字段列出的 L A P I C LAPIC LAPIC
    3. LAPIC 根据消息中的 D e s t i n a t i o n   M o d e Destination\ Mode Destination Mode D e s t i n a t i o n   F i e l d Destination\ Field Destination Field,自身的寄存器 ID 来判断自己是否接收该中断消息,设置 IRR 相应的 b i t bit bit 位,不是则忽略
    4. C P U CPU CPU 在可以处理下一个中断时,从 IRR 中挑选优先级最大的中断,相应位置 0,ISR 相应位置 1,然后送 C P U CPU CPU 执行。
    5. C P U CPU CPU 执行中断服务程序处理中断
    6. 中断处理完成后写 EOI 表示中断处理已经完成,写 EOI 导致 ISR 相应位置 0,对于 l e v e l level level 触发的中断,还会向所有的 IOAPIC 发送 EOI 消息,通知中断处理已经完成。

    上述的过程只是一个很简单的大致过程,没有涉及到不可屏蔽中断,一些特殊的中断,中断嵌套等等,只是来简单认识一下 APIC 在中断时是如何工作的,接下来重点看看 C P U CPU CPU 部分对中断的处理。

    CPU 部分

    上述就是 A P I C APIC APIC 的初始化部分,被 m a i n . c main.c main.c 中的 m a i n ( ) main() main() 调用,是计算机启动时环境初始化的一部分。下面来看 C P U CPU CPU 处理中断的部分。先来复习一下 C P U CPU CPU 部分大致是如何处理中断的:

    • C P U CPU CPU 收到中断控制器发来的中断向量号
    • 根据中断向量号去 I D T IDT IDT 索引门描述符,根据门描述符中的段选择子去 G D T GDT GDT 中索引段描述符
    • 这期间 C P U CPU CPU 会进行特权级检查,如果特权级有变化,如用户态进入内核态,压入原栈 S S SS SS E S P ESP ESP 到内核栈,如果没有变化则不用压入。之后压入 C S CS CS E I P EIP EIP E F L A G S EFLAGS EFLAGS,该中断有错误码的话还需要压入错误码。
    • 根据段描述符中的段基址和中断描述符中的偏移量取得中断服务程序的地址
    • 执行中断服务程序,这期间会压入寄存器等资源,保存上下文
    • 执行完成后恢复上下文,写 EOI​ 表中断完成

    所以在中断正式处理之前就压入一些寄存器,栈中情况如下:

    接下来便就是去 IDTGDT 中索引门描述符和段描述符,寻找中断服务程序,本文主要讲述中断,所以只来看看 IDTGDT 相关内容我在 @@@@@@@@@有所讲述,可以参考参考。

    构建 IDT

    IDT ( I n t e r r u p t   D e s c r i p t o r   T a b l e ) (Interrupt\ Descriptor\ Table) (Interrupt Descriptor Table),中断描述符表,我们得先有这么一个表, C P U CPU CPU 才能使用中断控制器发送来的向量号去 I D T IDT IDT 中索引门描述符。

    所以得构建一个 IDT​,构建 IDT 就是构建一个个中断描述符,一般称作门描述符,IDT​ 里面可以存放几种门描述符,如调用门描述符,陷阱门描述符,任务门描述符,中断门描述符。大多数中断都使用中断门描述符,来看看中断门描述符的格式:

    其实上述也可以作为陷阱门描述符,两者几乎一模一样,只有 T Y P E TYPE TYPE 字段不一样,所以如下定义中断门/陷阱门描述符:

    struct gatedesc {
      uint off_15_0 : 16;   // low 16 bits of offset in segment
      uint cs : 16;         // code segment selector
      uint args : 5;        // # args, 0 for interrupt/trap gates
      uint rsv1 : 3;        // reserved(should be zero I guess)
      uint type : 4;        // type(STS_{IG32,TG32})
      uint s : 1;           // must be 0 (system)
      uint dpl : 2;         // descriptor(meaning new) privilege level
      uint p : 1;           // Present
      uint off_31_16 : 16;  // high bits of offset in segment
    };
    
    • b i t 0 − b i t 15 bit0-bit15 bit0bit15:中断服务程序在目标代码段中的偏移量 0~15 位
    • b i t 16 − b i t 31 bit16-bit31 bit16bit31:中断服务程序所在段的段选择子
    • b i t 40 − b i t 43 bit40-bit43 bit40bit43:中断门的 T Y P E TYPE TYPE 值为 1110,陷阱门为 1111
    • b i t 44 bit44 bit44:S 字段为 0 表示系统段,各种门结构都是系统段,意为这是硬件需要的结构,反之软件需要的则是非系统段,包括平常所说的数据段和代码段,这不是硬件必须的,为非系统段。
    • b i t 45 − b i t 46 bit45-bit46 bit45bit46 D P L ( D e s c r i p t o r   P r i v i l e g e   L e v e l ) DPL(Descriptor\ Privilege\ Level) DPL(Descriptor Privilege Level),描述符特权级,进入中断时会用来特权级检查。
    • b i t 47 bit47 bit47 P ( P r e s e n t ) P(Present) P(Present) 该段在内存中是否存在,存在为 1,否则为 0
    • b i t 48 − b i t 63 bit48-bit63 bit48bit63:中断服务程序在内核代码段中的偏移量 16~31 位

    从上面部分字段代表的意义可以看出,构建中断门描述符还需要中断服务程序的地址信息,所以咱们首先还得准备好各个中断服务程序,取得它们的地址信息。在 x v 6 xv6 xv6 中​,所有的中断都有相同的入口程序,而在中断门描述符中填写的就是这个入口程序的地址。

    IDT 中支持 256 个表项,支持 256 个中断,所以要有 256 个入口程序,入口程序所做的工作是类似的,所以 x v 6 xv6 xv6 使用了 p e r l perl perl 脚本来批量产生代码。脚本文件是 v e c t o r s . p l vectors.pl vectors.pl,生成的代码如下所示:

    .globl alltraps  
    
    .globl vector0   #向量号为0的入口程序
    vector0:
      pushl $0
      pushl $0
      jmp alltraps
    #############################
    .globl vector8
    vector8:
      pushl $8
      jmp alltraps
    ##############################
    .globl vectors  #入口程序数组
    vectors:
      .long vector0
      .long vector1
      .long vector2
    

    这是一段汇编代码,所有的中断入口程序都做了相同的三件事或两件事:

    • 压入 0,其实这个位置是错误码的位置,有些中断会产生错误码压入栈中,所以为了统一,没有错误码的中断也压入一个东西:0
    • 压入自己的中断向量号
    • 跳到 a l l t r a p s alltraps alltraps 去执行中断处理程序

    第一项 压入 0 只有没有错误码产生的中断/异常才会执行,而错误码主要部分就是选择子,一般不使用。但这是 x 86 x86 x86 架构特性,有错误码的时候会自动压入,所以在 p e r l perl perl 脚本中对有错误码的异常做了特殊处理:

    if(!($i == 8 || ($i >= 10 && $i <= 14) || $i == 17)){
            print "  pushl \$0\n";
    

    表示向量号为 8 , 10 − 14 , 17 8,10-14,17 8101417 号会产生错误码,不需要压入 0。

    这 256 个中断入口程序地址写入一个大数组 v e c t o r s vectors vectors,所以中断门描述符要的地址信息不就来了,因此 IDT 的构建如下:

    struct gatedesc idt[256];
    extern uint vectors[];  // in vectors.S: array of 256 entry pointers
    
    void tvinit(void)   //根据外部的vectors数组构建中断门描述符
    {
      int i;
    
      for(i = 0; i < 256; i++)
        SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
      SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
    
      initlock(&tickslock, "time");
    }
    
    #define SETGATE(gate, istrap, sel, off, d)                \  //门描述符,是否是陷阱,选择子,偏移量,DPL
    {                                                         \
      (gate).off_15_0 = (uint)(off) & 0xffff;                \
      (gate).cs = (sel);                                      \
      (gate).args = 0;                                        \
      (gate).rsv1 = 0;                                        \
      (gate).type = (istrap) ? STS_TG32 : STS_IG32;           \
      (gate).s = 0;                                           \
      (gate).dpl = (d);                                       \
      (gate).p = 1;                                           \
      (gate).off_31_16 = (uint)(off) >> 16;                  \
    }
    

    S E G G A T E SEGGATE SEGGATE 宏就是根据信息构建一个中断描述符,应该很容易看懂。

    中断服务程序属于内核程序,段选择子为内核代码段, D P L DPL DPL 设置为 0,但是系统调用需要特殊处理, D P L DPL DPL 字段必须设置为 3。为什么这么设置,原由与特权级检查有关:当前代码段寄存器的 R P L ( R e q u e s t P r i v i l e g e L e v e l , 请 求 特 权 级 ) RPL(Request Privilege Level,请求特权级) RPL(RequestPrivilegeLevel) C P L ( C u r r e n t   P r e v i l e g e   L e v e l , 当 前 特 权 级 ) CPL(Current\ Previlege\ Level,当前特权级) CPL(Current Previlege Level),也就是 C P L = C S . R P L CPL=CS.RPL CPL=CS.RPL。是不是很绕,没办法,事实就是这样。

    作何特权级检查呢? C P L CPL CPL 需要大于等于门描述符中选择子的 D P L DPL DPL,而对于系统调用 C P L CPL CPL 还需要小于等于门描述符的 D P L DPL DPL,不然就会触发一般保护性错异常。系统调用特权级肯定是要转移的,也就是从用户态到内核态,用户态下 C P L = 3 CPL = 3 CPL=3,门描述符 D P L DPL DPL 如果还为 0 的话,那特权级检查不能通过,是要触发异常的,所以对于系统调用 D P L DPL DPL 得设置为 3。

    这说的有点远了,特权级检查是个很复杂的东西,上面还没有加入 R P L RPL RPL 的检查呢。这里只是稍作了解就好,后面有机会写一篇捋一捋特权级检查,下面回到 IDT 本身上来,IDT 构建好了之后需要将其地址加载到 IDTR 寄存器,如此 C P U CPU CPU 才晓得去哪儿找 IDT

    void idtinit(void)
    {
      lidt(idt, sizeof(idt));      //加载IDT地址到IDTR
    }
    
    static inline void lidt(struct gatedesc *p, int size)   //构造idtr需要的48位数据,然后重新加载到idtr寄存器
    {
      volatile ushort pd[3];
    
      pd[0] = size-1;
      pd[1] = (uint)p;
      pd[2] = (uint)p >> 16;
    
      asm volatile("lidt (%0)" : : "r" (pd));
    }
    

    IDTR 寄存器有 48 位

    • b i t 0 − b i t 15 bit0-bit15 bit0bit15 表示 IDT 的界限,也就是这个表有好大,表示的最大范围为 0 x F F F F 0xFFFF 0xFFFF,也就是 64 K B 64KB 64KB,一个门描述符 8 字节,所以描述符最多 64 K B / 8 B = 8192 64KB/8B = 8192 64KB/8B=8192,但是处理器只支持 256 个中断,也就是 256 个门描述符。
    • b i t 16 − b i t 48 bit16-bit48 bit16bit48 表示 IDT 基地址

    上述代码中数组 p d pd pd 就是这 48 位数据,先构造这个数据,然后使用内联汇编,指令 l i d t lidt lidt 将其加载到 IDTR 寄存器,关于内联汇编不多说,可以参考我前面的文章:@@@@@@@@

    中断服务程序

    I D T IDT IDT 准备好之后,这一小节就正式来看中断服务程序的流程,我将其分为三个阶段:中断入口,中断处理,中断退出,咱们一个个来看:

    中断入口程序

    中断入口程序主要是保存中断上下文, v e c t o r s vectors vectors 数组中记录的入口程序只能算是一部分,这一部分做了三件事:压入 0/错误码,压入向量号,跳到 a l l t r a p s alltraps alltraps

    所以现阶段栈中情况如下:

    紧接着程序跳到了 a l l t r a p s alltraps alltraps,来看看这是个什么玩意儿:

    .globl alltraps
    alltraps:
      # Build trap frame.  构建中断栈帧
      pushl %ds
      pushl %es
      pushl %fs
      pushl %gs
      pushal
      
      # Set up data segments.  设置数据段为内核数据段
      movw $(SEG_KDATA<<3), %ax
      movw %ax, %ds
      movw %ax, %es
    
      # Call trap(tf), where tf=%esp  调用trap.c()
      pushl %esp
      call trap
      addl $4, %esp
    

    可以看出 a l l t r a p s alltraps alltraps 也主要干了三件事:

    • 建立栈帧,保存上下文
    • 设置数据段寄存器为内核数据段
    • 传参调用 t r a p . c ( ) trap.c() trap.c() 中断处理程序

    1、建立栈帧,保存上下文

    建立栈帧保存上下文就是将各类寄存器资源压栈保存在栈中, x v 6 xv6 xv6 直接暴力地将所有的寄存器直接压进去。先是压入各段寄存器,再 p u s h a l pushal pushal 压入所有的通用寄存器,顺序为 e a x , e c x , e d x , e b x , e s p , e b p , e s i , e d i eax, ecx, edx, ebx, esp, ebp, esi, edi eax,ecx,edx,ebx,esp,ebp,esi,edi

    所以现下栈中的情况为:

    所以如此定义栈帧:

    struct trapframe {
      // registers as pushed by pusha
      uint edi;
      uint esi;
      uint ebp;
      uint oesp;      // useless & ignored esp值无用忽略
      uint ebx;
      uint edx;
      uint ecx;
      uint eax;
    
      // rest of trap frame
      ushort gs;
      ushort padding1;
      ushort fs;
      ushort padding2;
      ushort es;
      ushort padding3;
      ushort ds;
      ushort padding4;
      uint trapno;       //向量号
    
      // below here defined by x86 hardware
      uint err;
      uint eip;
      ushort cs;
      ushort padding5;
      uint eflags;
    
      // below here only when crossing rings, such as from user to kernel
      uint esp;
      ushort ss;
      ushort padding6;
    };
    

    可以看出定义的中断栈帧结构体与前面的操作是一一对应的,说明两点:

    • 段寄存器只有 16 位 2 字节,压栈段寄存器时用的 p u s h l pushl pushl,压入了一个双字 4 字节,所以需要 s h o r t short short 类型的来填充 2 字节。也可以直接将段寄存器定义为 u i n t uint uint 类型的,省去定义填充变量。
    • p u s h a l pushal pushal 时压入通用寄存器,这些寄存器加上进入中断时 C P U CPU CPU 自动压入的值就是中断发生前一刻进程的上下文。这里 p u s h a l pushal pushal 压入的 E S P ESP ESP 后面注释写着无用忽略,为什呢?买个关子,后面和栈的问题一起说。

    2、设置数据段寄存器为内核数据段

    在根据向量号索引门描述符的时候已经进行了特权级检查,将门描述符中的段选择子——内核代码段选择子加载到了 CS,这里就只需要设置数据段寄存器为内核数据段。附加段,附加的数据段,通常与数据段进行一样的设置,在串操作指令中,将附加段作为目的操作数的存放区域,详见前文内联汇编

    3、调用中断处理程序

    p u s h push push 之后 c a l l call call,标准的函数调用方式,先 p u s h push push 参数,再 c a l l call call 调用函数。 p u s h   % e s p push\ \%esp push %esp,此时的 esp 是中断栈帧的栈顶元素的地址,也就是说传递的参数是中断栈帧的首地址。随后 c a l l   t r a p call\ trap call trap 调用中断处理程序,压入返回地址( c a l l call call 指令后面那条指令的地址,也就是 $addl\ $4, %esp$ 语句的地址),之后跳转到 t r a p ( ) trap() trap() 执行程序。

    此时栈中情况:

    中断处理程序

    上述操作已经将中断处理程序 t r a p ( s t r u c t   ∗ t r a p f r a m e ) trap(struct\ *trapframe) trap(struct trapframe) 需要的参数中断栈帧 t r a p f r a m e trapframe trapframe 的地址压入栈中。其实 t r a p ( ) trap() trap() 也像是中断服务程序的入口,整个程序就是由许多条件语句组成,根据 t r a p f r a m e trapframe trapframe 的向量号去执行不同分支中的中断处理程序,来随便看几个:

    if(tf->trapno == T_SYSCALL){    //系统调用
        if(myproc()->killed)  //如果当前进程已经被杀死
          exit();             //退出
        myproc()->tf = tf;    //当前进程的栈帧
        syscall();            //系统调用入口
        if(myproc()->killed)  //再次确认进程状态
          exit();
        return;        //返回
      }
    

    如果向量号表示这是一个系统调用,则进行系统调用,这部分放在后面文章讲解。

    switch(tf->trapno){
      case T_IRQ0 + IRQ_TIMER:     //时钟中断
        if(cpuid() == 0){
          acquire(&tickslock);
          ticks++;
          wakeup(&ticks);
          release(&tickslock);
        }
        lapiceoi();
        break;
      case T_IRQ0 + IRQ_IDE:  //磁盘中断
        ideintr();
        lapiceoi();
        break;
    /*****************************/
    

    如果是时钟中断,并且是 C P U 0 CPU0 CPU0 发出的时钟中断,就将滴答数 t i c k s ticks ticks 加 1。每个 C P U CPU CPU 都有自己的 LAPIC,也就都有自己的 APIC Timer,都能够触发时钟中断。 t i c k s ticks ticks 记录系统从开始到现在的滴答数,作为系统的时间,发生一次时钟中断其数值就加 1,但是能修改 t i c k s ticks ticks 的应该只能有一个 CPU,不然如果所有的 C P U CPU CPU 都能修改 t i c k s ticks ticks 的值的话,那岂不是乱套了?所以这里就选择 C P U 0 CPU0 CPU0 也是 B S P BSP BSP 来修改 t i c k s ticks ticks 的值。处理完之后写 EOI 表时钟中断完成

    如果是磁盘发出的中断,就调用磁盘中断处理程序,也是磁盘驱动程序的主体,详见前文带你了解磁盘驱动。处理完之后就写 EOI​ 表中断完成

    其他的中断都是这样处理,就不一一举例说明了,其中有一些中断还没有讲到,但所有中断的处理都是如此,根据向量号调用不同的中断处理程序,处理完之后写 EOI 表中断完成

    中断退出程序

    执行完 t r a p ( ) trap() trap() 函数之后,回到汇编程序 t r a p a s m . S trapasm.S trapasm.S

    # Call trap(tf), where tf=%esp
      pushl %esp
      call trap
      addl $4, %esp
    
      # Return falls through to trapret...
    .globl trapret    #中断返回退出
    trapret:
      popal
      popl %gs
      popl %fs
      popl %es
      popl %ds
      addl $0x8, %esp  # trapno and errcode
      iret
    

    中断退出程序基本上就是中断入口程序的逆操作。

    首先从 t r a p ( ) trap() trap() 返回之后清理参数占用的栈空间,将 ESP 上移 4 字节。一般系统的源码就是汇编和 C 程序,所以使用 c d e c l cdecl cdecl 调用约定,该约定规定了参数从右往左入栈,EAX,ECX,EDX 由调用者保存,也是调用者来清理栈空间等等。而清理栈空间呢?其实就是为了栈里面的数据正确,显然要当前栈顶指针需要向上移动 4 字节,后面的操作 p o p a l popal popal 才正确。

    清理了栈空间之后弹出各个寄存器,到错误码向量号的时候直接将 ESP 上移 8 字节跳过。

    栈中变化情况如下:

    这里说明两点:

    • p o p pop pop 出栈操作并不会实际清理栈空间的内容,只是 E S P ESP ESP 指针和弹出目的地寄存器会有相应变化,栈里面的内容不会变化。
    • 返回地址什么时候跳过的?一般情况下 c a l l call call r e t ret ret 是一对儿, c a l l call call 压入返回地址, r e t ret ret 弹出返回地址,可是没看到 r e t ret ret 啊?这里是汇编和 C C C 语言混合编程,将 C C C 代码 t r a p . c trap.c trap.c 编译之后就有 r e t ret ret 了,所以弹出返回地址就发生在 t r a p ( ) trap() trap() 执行完之后。

    现在 E S P ESP ESP 指向的是 E I P _ O L D EIP\_OLD EIP_OLD,该执行 i r e t iret iret 了, i r e t iret iret 时先检查是否进行了特权级转移,如果没有特权级转移,那么就要弹出 EIP,CS 和 EFALGS,如果有特权级转移则还要弹出 ESP,SS

    原任务的所有状态都恢复了原样,则中断结束,继续原先的任务。

    中断的总体过程大致就是这样,不只是 x v 6 xv6 xv6 如此,所有基于 x 86 x86 x86 架构的系统都有类似的过程,只不过复杂的操作对中断的处理有着更微妙的操作,但总体上看大致过程就是如此。

    下面来看一看过程图:

    这主要是定位中断服务程序的图,至于实际处理中断的过程图就不画了,把握上面的栈的变化就行了,而栈的变化情况上述的图应该描述的很清楚了,所以这里就不再赘述,说起栈,关于栈上述我们还遗留了一些问题,在这儿解答:

    栈的问题

    最后再来聊一聊栈的问题,栈一直是一个很困惑的问题,我一直认为,操作系统能把栈捋清楚那基本就没什么问题了。在进入中断的时候,如果特权级发生变化,会先将 SS,ESP 先压入内核栈,再压入 CS,EIP,EFLAGS

    这句话看着没什么问题,但有没有想过这个问题:怎么找到内核栈的?切换到内核栈之后,ESP 已经指向内核栈,但是我们压入的 ESP 应该是切换栈之前的旧栈栈顶值,所以怎么得到旧栈的值再压入再者 i r e t iret iret 时如果按栈中的寄存器顺序只是简单的先 p o p l   % e s p popl\ \%esp popl %esp,再 p o p l   % S S popl\ \%SS popl %SS 那岂不是又乱套了?

    首先怎么切换到内核栈的这个问题,硬件架构提供了一套方法。有个寄存器叫做 TR 寄存器,TR 寄存器存放着 TSS 段选择子,根据 TSS 段选择子去 GDT 中索引 TSS 段描述符,从中获取 TSS

    那说了半天 TSS 是啥?TSS( T a s k   S t a t e   S e g m e n t Task\ State\ Segment Task State Segment),任务状态段,它是硬件支持的系统数据结构,各级(包括内核)栈的 SSESP所以当特权级变化的时候就会从这里获取内核栈的 SSESP。这个 TSS 这里我们只是简介,TSS 什么样子的,怎么初始化,还有些什么用处,它的功能都用到了?这些三言两语说不完,也不是本文重点,后面进程的时候会再次讲述。

    接着第二个问题,切换到新栈怎么压入旧栈信息,其实这个问题很简单,我先把旧栈信息保存到一个地方,换栈之后再压入不就行了。关于 i r e t iret iret 时弹出栈中信息是一个道理,查看 i n t e l intel intel 手册第二卷可以找到答案,的确也是这样处理的,手册中的伪码明显表示了有 t e m p temp temp 来作为中转站。但这个 t e m p temp temp 具体是个啥就不知道了,手册中也没明确说明,可能是另外的寄存器?这个不得而知,也不是重点没必要研究那么深入。

    本文中断关于栈还有一个地方值得聊聊,嗯其实也没多大聊的,就是解释一句。建立栈帧的时候 p u s h a l , p o p a l pushal,popal pushalpopal 的问题,这个是用来压入和弹出那 8 个通用寄存器的,还记得中断栈帧结构体中关于 ESP 的注释吗?写的是 u s e l e s s   i g n o r e useless\ ignore useless ignore,意思是无用忽略,这是为啥?

    这得从 p u s h a l pushal pushal 说起, p u s h a l pushal pushal 中压入 ESP 的时候压入的是 执行到 p u s h l   e s p pushl\ esp pushl esp 的值吗非也,压入的是 执行 p u s h a l pushal pushal 前的栈顶值,在执行 p u s h a l pushal pushal 之前先将 ESP 的值保存到 t e m p temp temp,当压入 ESP 的时候执行的时 p u s h   t e m p push\ temp push temp

    所以 p o p a l popal popal 执行到弹出 t e m p temp temp 的时候,就不能将其中的值弹入 ESP,而是直接将 ESP 的值加 4 跳过 t e m p temp temp。因为将 t e m p temp temp 弹入 ESP 的话等于换了一个栈了,本来只该跳 4 字节的,结果跳过了很多字节,那明显就不对了嘛。

    可以来张图看看,红线叉叉表示出错:

    关于 p u s h a l , p o p a l pushal,popal pushal,popal 的伪码如下:

    中断这一块关于栈方面的问题就是这么多吧,发生中断时有特权级变化就换栈,内核栈地址去 TSS 中找,中断完成后将所有的寄存器信息复原,其中就有 刚进入中断时压入的 SS ESP(有特权级变化的时候),栈也就恢复到了用户态下的栈。当然如果发生中断时就在内核态,那栈就不用变换,当然这只是 x v 6 xv6 xv6 的处理方式,其他系统可能不同,但总的来说中断的处理过程就是这么一个过程。

    当然这只是一个普通外设触发的中断,一些特殊中断,中断嵌套开关中断的内容都没有讲述,中断是个很大的概念,内容也很庞杂,本文利用 x v 6 xv6 xv6 将一个普通外设触发的中断的处理机制说明的应该还是很清楚的,好啦本文就到这里,有什么错误还请批评指正,也欢迎大家来同我交流讨论学习进步。

    https://wiki.osdev.org/APIC#Local_APIC_configuration

    https://wiki.osdev.org/IOAPIC

    http://blog.chinaunix.net/uid-20499746-id-1663122.html

    展开全文
  • MIPS中断总结

    2021-08-19 18:31:47
    1. mips中断总结 本文将总结关于MIPS架构...在介绍具体中断前还是有必要了解一下异常向量表相关信息,这涉及到异常向量表的表项和地址相关信息。同时异常向量表是CPU产生异常时,处理产生异常的入口点。这对中断的理解
  • LINUX系统调用原理-既应用层如何调用内核层函数之软件中断SWI:software interrupt 软件中断ARMLinux系统利用SWI指令来从用户空间进入内核空间,还是先让我们了解下这个SWI指令吧。SWI指令用于产生软件中断,从而...
  • MCS-51单片机的中断系统

    千次阅读 2021-01-05 22:44:03
    在任何一款事件驱动型的CPU里面都应该会有中断系统,因为中断就是为响应某种事件而存在的。中断的灵活应用不仅能够实现想要的功能,而且合理的中断安排可以提高事件执行的效率,因此中断在单片机应用中的地位是非常...
  • 产生死锁的必要条件: 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的...
  • FIQ与IRQ中断

    千次阅读 2021-02-02 10:50:58
    一般的中断控制器里我们可以配置与控制器相连的某个中断输入是FIQ还是IRQ,所以一个中断是可以指定为FIQ或者IRQ的,为了合理,要求系统更快响应,自身处理所耗时间也很短的中断设置为FIQ,否则就设置了IRQ。...
  • 我的理解:比如在执行一条指令的时候,这条指令必须执行完才能再去执行下一条指令或者响应中断,这个时候就要禁止中断了。 中断屏蔽 产生中断请求后,用程序方式有选择的封锁部分中断,而允许其余部分中断仍得到响应...
  • 中断控制器可以管理中断的优先级等,而处理器则由寄存器设置用来响应中断。 二、GIC 作为 ARM 系统中通用中断控制器的是 GIC(Generic Interrupt Controller),目前有四个版本,V1~V4(V2最多支持8个ARM core,V3/...
  • 目录一、引言二、AXI INTC三、按键中断四、测试结果 一、引言 中断是一种当满足要求的突发事件发生时通知处理器进行处理的信号。中断可以由硬件处理单元和外部设备产生,也可以由软件本身产生。 对硬件来说,中断...
  • 本文内容:广义分类狭义分类(x86分类)概念 广义的中断概念硬件中断中断BIOS中断 广义的陷阱概念 优先级 外部中断/中断(Interrupt)非屏蔽中断可屏蔽中断可编程中断控制器8259A高级可编程中断控制器(APIC) ...
  • 上一篇记录了树莓派自带的gpio驱动(外链网址已屏蔽),在bcm2708_gpio.c实现gpio驱动的同时其实也实现了中断控制器的驱动,本文记录bcm2708_gpio.c中驱动的实现。一·bcm2708_gpio_irq_init中断初始化函数建立gpio...
  • 很多情况下,使用信号来终止一个长时间运行的线程是合理的。这种线程的存在,可能是因为工作线程所在的线程池被销毁,或是用户...C11标准没有提供这样的机制(草案上有积极的建议,说不定中断线程会在以后的C标准中添加
  • 中断系统

    2021-07-15 00:37:24
    中文名中断系统性质计算机的重要组成部分来源中断装置和中断处理程序功能响应和返回等中断系统简介编辑语音中断装置和中断处理程序统称为中断系统。中断系统是计算机的重要组成部分。实时控制、故障自动处理、计算.....
  • 在开始之前先明确一个概念,中断会产生中断标志位,而CPU检测到中断标志位后,如果没有其他更高的中断在执行,CPU会响应该中断,并进入中断服务函数。串行通讯属于中断方式的一种,它服从这个概念。此外串行通讯并...
  • 在讲串口中断问题之前,需要明白串口中断内容包括哪些部分,我概括为主要3个部分:串口中断的初始化,串口的中断函数入口函数设置,串口中断的发送函数设置。 一.大意讲解,例程为例 我先以第十届国赛的程序题为例...
  • 【信捷plc标记与中断处理小知识】有关信捷plc的编程知识,在信捷plc中标记P、I用于分支与中断,标记I一般用于中断功能,包括外部中断、定时中断等场合,分支用的标记(P)用于条件跳转或子程序的跳转目标。一、标记P、...
  • 1.ARM cortex_m3 内核支持 256 个中断(16 个内核+240 外部)和可编程 256 级中断优先级的设置,与其相关的中断控制和中断优先级控制寄存器(NVIC、SYSTICK 等)也都属于cortex_m3 内核的部分。STM32 采用了 cortex...
  • 中断装置和中断处理程序统称为中断系统。中断系统是计算机的重要组成部分。实时控制、故障自动处理、计算机与外围设备间的数据传送往往采用中断系统。中断系统的应用大大提高了计算机效率。不同的计算机其硬件结构和...
  • 文件:操作系统中断方式小结.rar大小:15KB下载:X86体系中,CPU在INTR引脚上接到一个中断请求信号,如果此时IF=1,CPU就会在当前指令执行完以后开始响应外部的中断请求,这时,CPU在INTA引脚连续发两个负脉冲,外设在...
  • 硬件条件是:外部晶振使用HIE=6MHz;目标是:定时器每10S进中断一次使LED闪烁。首先要知道这个中断(TIM1上溢事件)的中断向量地址在哪?汇编部分就是编写一个跳转程序,用来保证在产生中断时会跳入自己写的C...
  • 在linux中,中断信号会将CPU转移到其它任务上。其它任务一般被称为中断处理程序或者中断服务程序。 中断来的时候会发生什么 中断往往由硬件设备的电子信号产生,并被引导到中断控制器上。将中断来的时候,内核将完成...
  • 1.1 中断的含义1.2 中断的作用(了解即可)1.3 中断的流程二、中断资源2.1 NVIC中断控制器2.2 NVIC寄存器三、优先级的概念四、中断编程 一、中断是什么? 1.1 中断的含义 说道中断,首先需要对中断这个概念有所了解...
  • 中断和轮询

    2021-09-07 17:39:30
    外部设备与中央处理器交互一般有两种手段:轮询和中断。 轮询(Polling) 很多I/O设备都有一个状态寄存器,用于描述设备当前的工作状态,每当设备状态发生改变时,设备将修改相应状态寄存器位。通过不断查询...
  • 虽然Linux是非常可靠的,明智的系统管理员... Linux基础认证工程师 - 第9部分Linux基础认证计划简介在本文中,我们将介绍大多数上游分发版中可用的几个工具的列表,以检查系统状态,分析中断以及解决正在进行的问...
  • 死锁的原因及必要条件 一、什么是死锁 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 35,375
精华内容 14,150
关键字:

响应中断的必要条件