再谈中断与异常

想知道我们在使用滑鼠操作电脑时作业系统在背後做了什麽事情吗?
又或者为什麽我们在写 C 语言时,老师总是会说要尽量避免多余的 I/O 操作呢?
这些问题的答案我们可以在作业系统中得到解答。本篇将针对这个部分探讨作业系统的异常与处理。

眼尖的读者一定会发现: 我在先前的文章已经探讨过中断与异常的议题了,为何本篇又提到了一次?

答: 我们先前是以 RISC-V 处理器的角度去看待中断以及异常的,而本篇我们针对作业系统端去看待该议题,用不同的角度看同样的事情时往往会产生新的认知!

先备知识

在阅读本篇文章之前,请确保你已经了解什麽是 User mode 以及 Kernel Mode。
若你还不知道,可以参考【文科生都能懂的小黑马作业系统教室】(4) (Ch1)特权指令与系统保謢

认识中断

在电脑科学中,中断是指处理器接收到来自硬体或软件的讯号,提示发生了某个事件,应该被注意,这种情况就称为中断。 通常,在接收到来自外围硬体的非同步讯号,或来自软件的同步讯号之後,处理器将会进行相应的硬体/软件处理。发出这样的讯号称为进行中断请求 (IRQ)。
-- wikipedia

What is PIC?

考虑到效能问题,在实务上都会有额外的控制器先将中断的请求做预处理。等到处理完成後,若处理器没有关闭 Interrupt 的功能,处理器才会真正执行中断。
而这个特别的控制器就称为 PIC。一个 PIC 可以处理 8 个输入中断,在现今的系统上都会有两个 PIC 做中断处理,不过,第二个 PIC 需要接在第一个 PIC 上做分流,所以这样子做能够处理 8 + 8 - 1 = 15 个中断请求,并且中断请求 (IRQ) 是有优先顺序的,IRQ 0 最大,IRQ 15 最小。

笔者已经在 RISC-V::中断与异常处理 -- 异常篇谈过处理器是如何做到拒绝受理中断请求的,详情请点开连结并参考 mstatus register 的部分。
此外,若想看更多有关 PIC 的介绍,可以参考 PIC 中断控制器一文获得更多资讯。

回顾: 同步与异步异常

在先前的 RISC-V::中断与异常处理 -- 异常篇已经谈过异常的种类,本篇我们用作业系统方的角度来看同步异常与异步异常的差别在哪。
我们都知道,异常主要分两大类:

  • 异步异常
  • 同步异常

对作业系统而言,I/O 装置的中断请求都可以被归类在异步异常中,像是使用者在操作滑鼠或是键盘时,经由 PIC 处理後便会产生中断请求。这种由外部产生的中断我们都可以将其归类成异步异常。
反之,若是由执行中的 Program 造成的异常中断,我们可以将其归类在同步异常中。

比起异步异常,同步异常是比较容易除错的。我们可以透过处理器内部的暂存器回去查找有问题的指令位址,再根据 offset 推算错误的程序并回推起因。

Interrupts and exceptions

异步异常会经由 PIC 确认後再查询中断向量表 (Vector Table),并从记忆体载入相关的 Interrupt Handler 做相对应的处理。
此外,我们可以在 stackoverflow 中的文章知道在 RISC-V 处理器中,中断处理程序会由 mtvec register 负责纪录。

根据上图,我们可以知道 Interrupt response time 是由多个时间组成:

  • Interrupt latency
    在现实中,Interrupt latency 可能受处理器设计、中断控制器、中断屏蔽和操作系统的中断处理方法的影响。
  • Processing time
    在这里的 Processing time 是指进入中断後执行相应程序的时间(包含 Context switch ),主要回由设计者的演算法以及处理器效能决定。不过,在这个部分也有可能被其他因素影响,像是: 被其他(权限更高)或是本身 Interrupt 中断。

因此,在一个好的作业系统设计中,开发者会尽量压缩每个 Interrupt 的 Response Time,以确保执行权可以尽快的回到上层的 IRQ 或是 User space 的应用当中。

系统实务面

上面提到: 为了提高系统的效率,中断程序执行的时间应尽可能的缩短。
我们知道在 CPU 没有关闭中断的状况下,当前执行的中断或是程序是能够被优先权更高的中断给抢断的,这样一来,原本的中断就会等待中断处理完成才能继续,进而导致事件的响应速度低落。
为了避免上面的状况发生,各个作业系统都有不同的解决办法。其中,Linux 设计了一些机制,值得我们参考。

再谈 Interrupt Handler

以网路通讯为例,若我们丢失 TCP 封包,我们需要等待正确的封包再次回传到电脑中。这段时间可能长达数秒,若我们一直停留在同一个 Interrupt Handler 会造成作业系统的效能低落。反之,若我们长时间不处理也会有封包遗失的风险 (网卡的 Buffer 满了以後前面的资料被覆盖掉)。为此,许多作业系统都会将 Interrupt Handler 拆分为两大块:

  • Top Half
  • Bottom Half

其设计概念是将基本的硬体操作放在 Top Half,等到前半段结束後再接 CPU Interrupt 做 Enable,进入 Bottom Half 的阶段,这也代表该阶段是能被更高优先权的中断抢断的。

Nested Interrupt

刚刚在说明 Interrupt 中的 Processing time 时就有提到:

不过,在这个部分也有可能被其他因素影响,像是: 被其他(权限更高)或是本身 Interrupt 中断。

所以在计算 Worst case 时也需要考量当前正在处理的所有中断的时间,因为在 Interrupt 没有被屏蔽的情况下,更高优先权的中断将抢占现有中断的执行权限。

为了避免 stack overflow,IRQ 无法被自己或是权限更低的 IRQ 抢断,在 IRQ 数量为 15 的系统上,Worst case 就是每个 IRQ 都被较高一级的 IRQ 抢断,等到最高级的 IRQ 处理完毕後再层层返回并处理较低层级 IRQ 的 bottom half。

补充:
QNX 是搭载在黑莓机上的作业系统,其核心采用 Microkernel 的设计,在其官方提供的规格书中也有提到 Nested Interrupt 的状况。

Bottom Half

在 Linux 作业系统中,使用了三种机制去实现 Bottom Half :

同步问题

若在多个 Interrupt Handler 以及多个 Daemon Task 中有 Shared-memory,我们就有可能会需要处理同步问题,如果不处理可能会导致记忆体的资料被重复写入,造成程序结果不符合预期。
笔者列举了几个 Case 并提出解决办法:

  • IRQ 之间共享记忆体
    假设 IRQ 1 以及 IRQ 2 共享记忆体,为避免同步问题,将另外一个 IRQ 给 Disable 即可。
  • IRQ 与 Daemon Task 共享记忆体
    将 IRQ 给 Disable。
  • Daemon Task 之间共享记忆体
    使用 semaphore :
    • Can do increments and decrements of semaphore value
    • Semaphore can be initialized to any value
    • Thread blocks if semaphore value is less than or equal to zero when a decrement is attempted
    • As soon as semaphore value is greater than zero, one of the blocked threads wakes up and continues
      • no guarantees as to which thread this might be

    -- 并行和多执行绪程序设计讲座

    sem_t semaphore;
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    int sem_wait(sem_t *sem);
    int sem_post(sem_t *sem);
    
    sem_t empty, full;
    void producer(char* buf) {
       int in = 0;
       for(;;) {
        sem_wait(&empty); 
        buf[in] = getChar();
        in = (in + 1) % MAX_SIZE;
        sem_post(&full);
       }
    }	
    
    void consumer(char* buf) {
       int out = 0;
       for(;;) {
        sem_wait(&full);
        useChar(buf[out]);
        out = (out + 1) % MAX_SIZE;
        sem_post(&empty);
       }
    }
    

总结

碍於篇幅问题,无法对并行程序设计做更多的介绍,笔者在这边整理了一些素材供读者们参考:

Reference

  • 维基百科
  • 交通大学 OCW
  • Jserv 线上讲座

<<:  [Day20] Vue 3 单元测试 (Unit Testing) - Form Elements Handling

>>:  【Day 21】 实作 - 启用 AWS CloudFront 日志

[Day 7] Leetcode 621. Task Scheduler (C++)

前言 今天来分享621. Task Scheduler这一题~ 其实会选到这题本来也是要接续昨天pr...

Day 09: 【番外篇】关於写 Code 这件事 (待改进中... )

「42 年里,我什麽都经历过。我被开除过,也被表扬过。我当过小组长、主管、也当过普通员工,甚至当过...

Windows AD使用者OU汇出到新建的AD主机

目前因为关系企业需要单独独立一间公司出来,原本在总公司底下的AD OU群组必须要汇出单独分开,到另外...

Day1-前言:三十天成为D3好手

本篇大纲: 为何选此主题 为何不使用框架 这三十天要讲的大纲 Github 程序码与 Github...

[前端/JavaScript] 实作汇出excel下载按钮的超好用套件:ExcelJS(下)- 用React汇出excel (export excel)

有关於ExcelJS这个套件的教学与说明,请先看我的上一篇文章: [前端/ES6] 实作汇出exce...