首页 > 代码库 > 中断机制
中断机制
2.1 分析前得实现说明
操作系统是一个整体机制,事实上不能隔离任何一个机制,也就是说任何一个机
制是不可能脱离其他机制而单独存在,我们将这种机制类比一个图结构,设想一
下,如果有一个数学意义上的图 G,,G={G1,G2 …},这只是数学家给其定义的方
式,但是 G1,G2,….他们之间是不很难发生直接的联系,也就是说他是各自独
立而存在,但是 G1 内的各个节点都会有着直接或者间接的制约关系而存在。如
果我们随便找到一个图算法,他都只会对任何一个子图进行运算而忽视其他的子
图的存在,也就是说要让一个算法在一个图结构上运行,至少一点就是图的各个
点都必须都是相连通的。操作系统同样也遵循这个理念,我们认为操作系统的每
一个机制都会为其他机制进行服务,他们直接存在相互调用的关系,但是如果从
递归论的角度来看,OS 机制岂不是一个无穷递归呢?或者说 OS 有没有那种机
制他不依赖任何一个机制而仅仅是为其他机制而服务?这个问题等到 OS 内核设
计完成之后,应该就比较清楚了!
2.2 中断机制概述
中断是整个 MINIX 机制的心脏,如果没有中断,这个 MINIX 无法完成用户想机 器提供的服务。MINIX 的中断机制是不算很复杂,但是不算非常简单,在讲解 MINIX 中断机制时,必须要借助硬件平台,我们选用的 i386 硬件平台。这是非 常通用的和流行的平台(虽说 Intel 386 架构带来了太多的历史包袱)。关于 INTEL 386 硬件中断机制这里不会详细说明,具体参看本章的参考文献概述,下面会简 单的介绍整个 INTEL 386 中断系统硬件机制
2.3Intel 386 中断机制
首先,明确的一个重要问题是我们假设我们已经开启了 A20 地址线,也就是我 们已经让 Intel386 进入了保护模式,我们这里讨论的所有相关的问题都是基于保 护模式而展开的,实模式在此完全忽略。
首先,386 分为软中断和硬件中断,软中断在 intel 称为 trap。既陷阱。一个用 int
<向量号>进入。 从时间和意外性上来看,trap 类型的 是我们可以预知的,并且
时间是可以粗算出来在哪个时间会发生。硬件中断就往往具有随意性,和用户有
关。比如我正在敲键盘,但是你也许不知道我下敲键盘的是什么时候,事实上我
也不一定知道。当然对于时间中断,往往可以预知,也就是说,什么时候时间中
13
断芯片会产生中断,这个在后面会有所介绍。
其次,我们来看看中断芯片 8259,intel 中断芯片处理器 8259 是一个可编程的中
断芯片,它能对向量地址进行重新设定。在 IBM 兼容机里,IRQ0 是时钟中断,
IRQ1 是键盘,后面等等。也就是说有一部分硬件的中断向量号是固定的。
中断的大体操作:首先中断通过某种状况而触发,386 会进行一些保护措施而进
入内核状态。这里的保护措施 其实主要是做这么几点:保存 PC 值,根据当前
的 TS 值找到 TSS 结构,TSS 描述符中的 SP0 是非常重要的一块。它保存这个进
程内核态的栈帧指针。硬件会自动将 SP0 加载到 ESP 里。 注意上面那些操作都
是硬件进行做的,我们无权过问,剩下的硬件就会交由内核做。内核往往会为了
进一步方便编程而进行一系列操作,之后转向中断服务程序,之后就是 irted.这
里介绍的是 Intel386 的硬件机制。下面我们来看 Minix 是怎么来操控这个 intel386
的硬件机制。
2.4MINIX 操控下的 386 中断硬件机制
为了配合体现硬件机制,MINIX 建立了一个描述符数组,这个数组就是中断门, 由于历史原因,386 的异常处理模式是自动的,所以在中断门描述符中的前 32 项都必须留给硬件,助理理解这里的自动:这里的自动其实就是自动陷入描述符 的所指向的地址,关于每个异常怎么处理,又是该交给操作系统来完成。在后面 的门路中,MINIX 设置了一部分门路。在源程序/kernel/protect.c 的 prot_init 函数 里,截取部分源代码来讲解
这段就是个门路的初始化,数组中每个描述符都是根据 INTEL386 的硬件机制而 设定的,这个表格是由 TR 指向的。
gate_table[] = {
{ divide_error, DIVIDE_VECTOR, INTR_PRIVILEGE },
{ single_step_exception, DEBUG_VECTOR, INTR_PRIVILEGE }, { nmi, NMI_VECTOR, INTR_PRIVILEGE },
{ breakpoint_exception, BREAKPOINT_VECTOR, USER_PRIVILEGE },
{ overflow, OVERFLOW_VECTOR, USER_PRIVILEGE },
{ bounds_check, BOUNDS_VECTOR, INTR_PRIVILEGE },
{ inval_opcode, INVAL_OP_VECTOR, INTR_PRIVILEGE },
{ copr_not_available, COPROC_NOT_VECTOR, INTR_PRIVILEGE },
{ double_fault, DOUBLE_FAULT_VECTOR, INTR_PRIVILEGE },
{ copr_seg_overrun, COPROC_SEG_VECTOR, INTR_PRIVILEGE },
{ inval_tss, INVAL_TSS_VECTOR, INTR_PRIVILEGE },
{ segment_not_present, SEG_NOT_VECTOR, INTR_PRIVILEGE },
{ stack_exception, STACK_FAULT_VECTOR, INTR_PRIVILEGE },
{ general_protection, PROTECTION_VECTOR, INTR_PRIVILEGE },
#if _WORD_SIZE == 4
{ page_fault, PAGE_FAULT_VECTOR, INTR_PRIVILEGE },
{ copr_error, COPROC_ERR_VECTOR, INTR_PRIVILEGE },
#endif
14
{ hwint00, VECTOR( 0), INTR_PRIVILEGE },
{ hwint01, VECTOR( 1), INTR_PRIVILEGE },
{ hwint02, VECTOR( 2), INTR_PRIVILEGE },
{ hwint03, VECTOR( 3), INTR_PRIVILEGE },
{ hwint04, VECTOR( 4), INTR_PRIVILEGE },
{ hwint05, VECTOR( 5), INTR_PRIVILEGE },
{ hwint06, VECTOR( 6), INTR_PRIVILEGE },
{ hwint07, VECTOR( 7), INTR_PRIVILEGE },
{ hwint08, VECTOR( 8), INTR_PRIVILEGE },
{ hwint09, VECTOR( 9), INTR_PRIVILEGE },
{ hwint10, VECTOR(10), INTR_PRIVILEGE },
{ hwint11, VECTOR(11), INTR_PRIVILEGE },
{ hwint12, VECTOR(12), INTR_PRIVILEGE },
{ hwint13, VECTOR(13), INTR_PRIVILEGE },
{ hwint14, VECTOR(14), INTR_PRIVILEGE },
{ hwint15, VECTOR(15), INTR_PRIVILEGE }, #if _WORD_SIZE == 2
{ p_s_call, SYS_VECTOR, USER_PRIVILEGE }, /* 286 system call */
#else
//这里是 386 以及其后的系统调用描述符
{ s_call, SYS386_VECTOR, USER_PRIVILEGE }, /* 386 system call */
#endif
{ level0_call, LEVEL0_VECTOR, TASK_PRIVILEGE },
};
现在来考虑栈问题,MINIX 内核自身会有一个公共栈,进程有个一个栈帧,这
种栈帧是由内核来操控,还用进程有用户状态的栈,是在外核由计算机应用程序
自己操控。这个 3 个栈直接的转换非常的重要。在阅读源码要源码此时是用哪个
ESP
现在来考虑中断处理机制,MINIX 的中断处理函数是比较简单的,在中断处理 函数的数据组织结构中,应该尽量简单,安全。主要是源于下面的原因:
中断机制是直接操控硬件,而且中断发生非常的频繁,所以对于这个机制来讲, 就应该简单,不能复杂。这是高效率的体现。
中断机制直接操控硬件,也就是很容易出现错误,一旦出现错误,可能会让整个 系统直接崩溃。出于此,中断机制一般都是比较简单的,例如 Linux,linux 的中 断机制也是属于简单的。
MINIX3 中断机制源码分析
下面我们来探讨下 MINIX 中断处理函数的具体处理流程:
由前面的章节知道,MINIX 的架构是标准的微内核结构,除了时钟中断处理程
序可以直接在内核态进行,其他的都只能从用户态,用户态像内核态发送消息。
假设我们是一个内核设计者,怎么能够将这种机制实现呢?我们会在系统任务里
设置一个中断注册调用,通过那个调用,我们能够将用户需要的中断处理函数挂
15
到相应的地方并且以后能够被用户识别。
现在继续往下面分析:我们现在又 16 根通用的中断地址,但是这是不够的,对 于一个用户拥有非常多的应用硬件而言,那些中断号很难满足。我们所能够想象 到得就是共享,想办法让多个中断处理程序共享一个中断号。怎么来实现这个共 享中断号呢? MINIX 用了一种比较简单但是非常高效的方法来实现这个中断 共享机制。我们先来看一个数据结构:
typedef struct irq_hook{
struct irq_hook*next; /* next hookin chain */指向下一个链
int (*handler)(struct irq_hook*); /* interrupt handler */中断处理函数 首地址
int irq; /* IRQ vector number */中断向量号 int id; /* id of this hook*/hook的id
int proc_nr; /* NONE if not in use */标志被哪个进程使用
irq_id_t notify_id; /* id to return on interrupt */用于中断返回的id irq_policy_t policy; /* bit maskfor policy */
} irq_hook_t;
这个数据结构就可以实现中断的共享。在 glo.h 中,就有如下 2 个变量,可以看
出,这 2 个变量是 2 个静态数组,数组元素要么是 irq_hook_t,要么是 irq_hook_t
指针。
/* Interrupt related variables. */
EXTERN irq_hook_t irq_hooks[NR_IRQ_HOOKS]; /* hooks for general use
*/
EXTERN irq_hook_t *irq_handlers[NR_IRQ_VECTORS];/* list of IRQ handlers
*/
Irq_hanlders 里面的每一个数据结构就是用来支持这个中断共享功能。在后面可
以详细看出来这个功能。 Irq_hooks 这个数值就是供通用钩子使用的。
看完了中断共享,现在来看看怎么让内核知道或者来初始化一个中断程序!?
MINIX3 是微内核结构,也就是用户的设备驱动程序应该在用户态完成,驱动程
序和中断是紧密相连的,一般一个请求 I/O 操作完成后,设备就会产生一个中断。
但是怎么能够让内核知道呢?MINIX3 关于这点提出了一个观点:就是注册中断
号,通过发送一个消息给内核让内核来完成这个动作。这里又是通过消息机制来
完成
之后想象下,中断返回,中断返回会涉及到 TSS,进程栈,内核栈,中断返回到 用户进程,这里对于绝大部分多进程操作系统而言,可以说是一个绝佳的机会, 可以重新调用另外一个进程。MINIX3 也是抓住这个机会进行重新调度进程。具 体的调度算法会在 MINIX 进程调度有详细的分析
对于上面,我们所讨论的基本上是属于硬件中断,Intel386 还提供一个软中断模 式,其实就是 trap 模式或者是系统调用模式,在 minix3 中,才用微内核结构, trap 模式就是利用消息传递机制来完成,trap 所经过的就是调用门,在返回时, 同样可以重新调用另外一个进程。
涉及到中断相关问题的主要源文件
I8259 是关于注册和中断处理的源文件,exception 是关于异常的处理源文件,事
实上 clock.c 也实际涉及到相关中断。但是会单独在时钟处理机制来实现。最后
16
中断最核心的文件其实是 MPX386.s,这是一个汇编程序构成的源文件。在这里 我会对每一个源文件一一做较为详细的分析。
从执行流程来看,设想一个进程 P,我们准备在键盘敲一段文字,敲击键盘时, 就会产生中断,或者我们进程 P 有一段代码想获取当前的时间是多少,P 就会产 生系统调用,内核会把相关的内容返回给它,这个动作就是系统调用或者嵌入内 核中。以上 2 个过程就是我们需要一一详细进入 MINIX3 内核中详细的分析: 先看敲击键盘,当用户敲击键盘时,(为简单起见,我们不会考虑相关的 TTY 过 程),键盘将产生一个中断,这个中断会被 CPU 发现并嵌入内核中的相关程序, 我们先来分析这个过程和应该调用的相关函数:
敲击键盘,产生中断,由于是进程是用户状态,加之现在是保护模式,所以硬件 会自动涉及以下几个寄存器 TS,TS 会自动将加载 TSS 的 sp0 ss0 放到 sp 和 ss 中,同时暂把 sp 和 ss 存期来,之后将 SP 和 SS 压入新栈中,同时将 CS 和 IP 压 入栈中,通过调用门把 CS 和 IP 放入相应的寄存器中,IP 其实就是中断向量处 理地址,hwint_master(1),之后这里的内容就是这样处理的:
hwint_master(irq)---------àsave----------------à_intr_handle---------à_restart;
先看执行执行过程图:
17
现在深入真正源码一一分析:
先看/kernel/mpx386.s 与中断有关的代码:事实上这个是入口,hwint00-07 和 hwint08-15 其实本质上是一样的,但是它分了主芯片和从芯片,是为了 8259 中 断编程器的 2 级芯片而设立的。先往下面看
!*=========================================================== ================*
!* interrupt handlers *
!* interrupt handlers for 386 32-bit protected mode *
18
!*=========================================================== ================*
!*=========================================================== ================*
!* hwint00 - 07 *
!*=========================================================== ================*
! Note this is a macro, it just looks like a subroutine.
!下面这段代码不是一个例程,(这点要非常的注意),事实上仅仅是一个宏,为 了就是提高
!运行效率
!要理解这段代码需要理解 82539A 的结构,其分为主和从,这里是处理主,下面 是处理从,
!其实所要完成的操作都相同,sp 和 ss 是 TSS 的 sp0 和 ss0 里的值,注意此时 是该进程栈
!的值,(应该说是内核的栈值),可是经过第一个调用,既 call save 之后,栈的 指向就换成
!了内核栈,这个栈是内核公共栈。希望读者跟着我的思路往后走,这段比较复 杂,特别是
!栈的变化,我们现在进入 save 例程中(一定要跟着来) #define hwint_master(irq) \
call save /* save interrupted process state */;\
!我们从 save 中返回出来,我们这里发生了几个变化,eax 存储了进程栈的栈地
址,之后现在的 sp 是内核栈栈地址。同时内核栈压入了_restart 或者_restart1 地
址。
push (_irq_handlers+4*irq) /* irq_handlers[irq] */;\
!这里的语法在前面看过,不在说,把 irq 作为参数值传入 intr_handle 函数,这 个函数
!是处理整个中断机制最为关键的一个函数 我们现在不往里分析,我们现在只 要知道个
!函数就是中断处理函数。完成了相关的中断处理。
call _intr_handle /* intr_handle(irq_handlers[irq]) */;\
!这个 pop ecx 要注意,由于_intr_handle 其实一个 C 语言函数,所以它的参数应 该被 pop 掉,也就是将调用号 irq POP 到 ECX 寄存器中。
pop ecx ;\
!判断 irq_actids[irq]是否为 0,也就是判断 interrupt 是否还是活动的,既判断这个 中断向量号
!是完成。
cmp (_irq_actids+4*irq), 0 /* interrupt still active? */;\
!这里的_irq_actids 事实上是一个记录 irq 号是否完成的例子,为 0 表示完成
jz 0f
!如果完成,则跳到 0,如果没有完成,则执行下面的
!下面的主要是禁止这个中断向量号 irq ;\
19
inb INT_CTLMASK /* get current mask */ ;\
orb al, [1<<irq] /* mask irq */ ;\
outb INT_CTLMASK /* disable the irq */;\
!可以重启这个中断向量号 irq
0: movb al, END_OF_INT ;\
outb INT_CTL /* reenable master 8259 */;\
ret /* restart (another) process */
!!这里的 ret 非常重要,其实他就是一个重新启动另外一个进程的标志,每次进 入来,都
!是由外进入内部,经过 ret 之后,就是从内核跳转到外部应用程序,在后面会详 细讨论。
!现在我们来分析这个 ret,着重分析,ret 在 intel 的默认操作是 pop ip,也就是将栈 顶元素 pop
!到 IP 地址中,这点可以说非常的巧妙,你可以看看,前面我们知道有一个栈顶 元素是_restart
!或者_restart1 函数首地址,也就是在此之后我们又进入 restart 或者 restart1 执行, 真正意义
!上的重新调度进程是在那个函数中。我们现在进入那个函数进行分析,读者请 跟我的步伐
!进入 restart 函数
!下面都是函数的入口,就是针对上面的宏,其实你们可以看到,在门描述符中, 都会有
hwint ***,下面就是定义这些函数的首地址。是一种语法的模式。
! Each of these entry points is an expansion of the hwint_master macro
.align 16
_hwint00: ! Interrupt routine for irq 0 (the clock).
hwint_master(0)
.align 16
_hwint01: ! Interrupt routine for irq 1 (keyboard)
hwint_master(1)
.align 16
_hwint02: ! Interrupt routine for irq 2 (cascade!)
hwint_master(2)
.align 16
_hwint03: ! Interrupt routine for irq 3 (second serial)
hwint_master(3)
.align 16
_hwint04: ! Interrupt routine for irq 4 (first serial)
hwint_master(4)
20
.align 16
_hwint05: ! Interrupt routine for irq 5 (XT winchester)
hwint_master(5)
.align 16
_hwint06: ! Interrupt routine for irq 6 (floppy)
hwint_master(6)
.align 16
_hwint07: ! Interrupt routine for irq 7 (printer)
hwint_master(7)
!*=========================================================== ================*
!* hwint08 - 15 这个就是从芯片中断,在这里基本可以忽略
*
!*=========================================================== ================*
! Note this is a macro, it just looks like a subroutine.
#define hwint_slave(irq) \
call save /* save interrupted process state */;\
push (_irq_handlers+4*irq) /* irq_handlers[irq] */;\
call _intr_handle /* intr_handle(irq_handlers[irq]) */;\
pop ecx ;\
cmp (_irq_actids+4*irq), 0 /* interrupt still active? */;\
jz 0f ;\
inb INT2_CTLMASK ;\
orb al, [1<<[irq-8]] ;\
outb INT2_CTLMASK /* disable the irq */;\
0: movb al, END_OF_INT ;\
outb INT_CTL /* reenable master 8259 */;\
outb INT2_CTL /* reenable slave 8259 */;\
ret /* restart (another) process */
! Each of these entry points is an expansion of the hwint_slave macro
.align 16
_hwint08: ! Interrupt routine for irq 8 (realtime clock)
hwint_slave(8)
.align 16
_hwint09: ! Interrupt routine for irq 9 (irq 2 redirected)
hwint_slave(9)
21
.align 16
_hwint10: ! Interrupt routine for irq 10
hwint_slave(10)
.align 16
_hwint11: ! Interrupt routine for irq 11
hwint_slave(11)
.align 16
_hwint12: ! Interrupt routine for irq 12
hwint_slave(12)
.align 16
_hwint13: ! Interrupt routine for irq 13 (FPU exception)
hwint_slave(13)
.align 16
_hwint14: ! Interrupt routine for irq 14 (AT winchester)
hwint_slave(14)
.align 16
_hwint15: ! Interrupt routine for irq 15
hwint_slave(15)
!*===========================================================
================*
!* save 我们从上面的 hwint_handler(irq)进入这个 save,现在进
入内部分析 *
!*=========================================================== ================*
! Save for protected mode.
! This is much simpler than for 8086 mode, because the stack already points
! into the process table, or has already been switched to the kernel stack.
!这个例程也是非常重要,要好好理解一番,这个例程是用于保护模式,当然在 整个执行过
!程中,都是保护模式,感觉这个话贼其多余!当然在进入内核时,此时堆栈指 针已经是指
!进程表的栈结构,事实上 在 MINIX 中,每个进程都定义一个栈帧(其实就是 一个栈,说
!的太玄乎了!),这里指向的就是每个进程的栈结构,下面都是一系列的保存操
作。
.align 16
save:
cld ! set direction flag to a known value
22
pushad ! save "general" registers
o16 push ds ! save ds
o16 push es ! save es
o16 push fs ! save fs
o16 push gs ! save gs
!!上面一系列动作都是将寄存器的内容保存在进程栈帧中。
mov dx, ss ! ss is kernel data segment
mov ds, dx ! load rest of kernel segments
mov es, dx ! kernel does not use fs, gs
!将 DS 和 ES 设置成内核数据段
mov eax, esp ! prepare to return 这里是把 ESP 保存在 EAX 中,供以后返 回时使用,这
!里 esp 的值其实就是被中断的进程的栈指针。
incb (_k_reenter) ! from -1 if not reentering,这个_k_reenter 值代表的嵌套数, ! 如果是-1,就代表是没有嵌套,这个值主要是在后面的函数_restart 做文章。
jnz set_restart1 ! stack is already kernel stack
!,要非常的注意了,这里准备开始换栈了,准备把内
核栈和进程栈帧换掉,k_stkop 代表的是内核栈,这点要非常的注意,内核栈和 各个进程
!的栈帧是不一样的概念,但是 2 者的操作都是由内核完成。
!经过上面一个语句,栈由进程栈帧变化到内核栈,这里把_restart 压入内核栈 中,在后面
!会看到,这是非常巧妙的做法。暂且在这里留下悬念,到了 hwint_handler()讲 解完,自然
!就会明白。
push _restart ! build return address for int handler
xor ebp, ebp ! for stacktrace
jmp RETADR-P_STACKBASE(eax)
!这里是返回 save 调用原地址里。我继续回到前一个宏中-----hwint_handler(irq)
.align 4
set_restart1:
push restart1!如果是嵌套中断,则将 restart1 压入内核中。之后返回 save 调用地址中,之后我们返回 hwint_handler(irq)分析
jmp RETADR-P_STACKBASE(eax)
!*=========================================================== ================*
!* restart 我们从 hwint_inhandler(irq)进入到这个函数,我们现在
来着重分析这个函数 !在分析之前,我们同样要搞清楚栈和相关寄存器的内
容,首先栈还是内核栈
之后 CS 和 IP 同样是指向这个函数的首地址,现在我们进入函数内部分析
*
!*=========================================================== ================*
_restart:
23
! Restart the current process or the next process if it is set.
!!这里非常的重要,因为这里主要是用于进程的调度。
cmp (_next_ptr), 0 ! see if another process is scheduled,这里的_next_ptr
表示在运行 restart 之后指向的进程地址。其实也就是
jz 0f
mov eax, (_next_ptr) !如果不是为 NULL,则执行流就从这里开始
!将_next_ptr 的值存入 eax,之后将 eax 的值设置存入_proc_ptr 内存地址 中,pro_ptr 是指
!向当前进程指针,这些指针都是已经设定好的,这个指针就就是用于下次 返回所运
!行的指针
mov (_proc_ptr), eax ! schedule new process
mov (_next_ptr), 0! 这里为啥把_next_ptr 设置成为 NULL,主要原因是标
志下次在执行
!这个 restart 例程时是否有重新设置上面那 2 个指针。如果没有就直接进入 0 标志,这个
!就是从最高代码效率来考虑
0: mov esp, (_proc_ptr) ! will assume P_STACKBASE == 0
!这个需要注意,在后面进程和内存管理可以看到,这里的进程首地址就是该 进程的栈帧
!地址,这是经过精心安排的结果 也就是 esp 指向我们即将运行的进程的栈地址。
lldt P_LDT_SEL(esp) ! enable process‘ segment descriptors
!把该进程的段描述符装入到相应的寄存器中
lea eax, P_STACKTOP(esp) ! arrange for next interrupt
!将 esp 的栈头指针存入 eax,之后再_tss+TSS3_S_SP0 里,这需要参考 Intel TSS 结构,这个
!值其实就是状态为 0 的栈指针,这样做是让下次中断时可以直接调用将这个栈 地址放置到
!esp 寄存器中。
mov (_tss+TSS3_S_SP0), eax ! to save state in process table
restart1:
!下面这段代码是说明 如果是在内核中嵌套另一个过程,不需要设置上面的各
个指
!针,只要直接返回即可
decb (_k_reenter)
o16 pop gs
o16 pop fs
o16 pop es
o16 pop ds
popad
add esp, 4 ! skip return adr
iretd ! continue process
24
上面我们分析了整个执行的内核过程,现在我们尝试从另外一个角度来分析中断 机制-----执行中断处理程序。这个中断处理程序基本上在内核中 i8259.c 中。当 然还有一些系统调用。我们先仔细的分析 i8259.c i8259 主要包 put_irq_handler, rm_irq_handler intr_handle,intr_init.既然有执行中断处理程序,那么首先肯定要向 内核注册中断处理程序,同时也应该提供删除一个中断处理程序。I8259.c 就是 基于上面的想法而设定的。
/* This file contains routines for initializing the 8259 interrupt controller: * put_irq_handler: register an interrupt handler
* rm_irq_handler: deregister an interrupt handler
* intr_handle: handle a hardware interrupt
* intr_init: initialize the interrupt controller(s)
*/
//这个文件就是初始 8259A 中断处理器 当然还包括处理中断一些关键功能 #include "kernel.h"
#include "proc.h"
#include <minix/com.h>
//用于设置 8259A 的一些宏定义,便于编程,详细参数含义需要看 INTEL386 的 具体含义。
#define ICW1_AT
#define ICW1_PC
#define ICW1_PS
#define ICW4_AT_SLAVE
*/
0x11 /* edge triggered, cascade, need ICW4 */
0x13 /* edge triggered, no cascade, need ICW4 */ 0x19 /* level triggered, cascade, need ICW4 */
0x01 /* not SFNM, not buffered, normal EOI, 8086
#define ICW4_AT_MASTER
*/
#define ICW4_PC_SLAVE
#define ICW4_PC_MASTER
#if _WORD_SIZE == 2
0x05 /* not SFNM, not buffered, normal EOI, 8086
0x09 /* not SFNM, buffered, normal EOI, 8086 */
0x0D /* not SFNM, buffered, normal EOI, 8086 */
typedef _PROTOTYPE( void (*vecaddr_t), (void) );
FORWARD _PROTOTYPE( void set_vec, (int vec_nr, vecaddr_t addr) );
PRIVATE vecaddr_t int_vec[] = {
int00, int01, int02, int03, int04, int05, int06, int07,
};
//中断向量表入口
PRIVATE vecaddr_t irq_vec[] = {
hwint00, hwint01, hwint02, hwint03, hwint04, hwint05, hwint06, hwint07,
hwint08, hwint09, hwint10, hwint11, hwint12, hwint13, hwint14, hwint15,
};
#else
#define set_vec(nr, addr) ((void)0)
25
#endif
/*=========================================================== ================*
* intr_init
*这个例程就是用于初始 8259 芯片
*============================================================ ===============*/
PUBLIC void intr_init(mine)
int mine;
{
/* Initialize the 8259s, finishing with all interrupts disabled. This is
* only done in protected mode, in real mode we don‘t touch the 8259s, but
* use the BIOS locations instead. The flag "mine" is set if the 8259s are
* to be programmed for MINIX, or to be reset to what the BIOS expects.
*/
int i;
//在设置中断的时候,毫无疑问首先要关闭中断标志,防止干扰 intr_disable();
//如果芯片的保护模式打开,就必须设置 8259 的相关参数
//这里将主从片 0 号中断向量设置成 0X20,这个刚好是 32,为啥要这样设置
呢
//主要是因为 0 到 31 被 intel 保留用于处理相关异常程序 if (machine.protected) {
/* The AT and newer PS/2 have two interrupt controllers, one master, * one slaved at IRQ 2. (We don‘t have to deal with the PC that
* has just one controller, because it must run in real mode.)
*/
outb(INT_CTL, machine.ps_mca ? ICW1_PS : ICW1_AT);
outb(INT_CTLMASK, mine ? IRQ0_VECTOR : BIOS_IRQ0_VEC);
/* ICW2 for master */
outb(INT_CTLMASK, (1 << CASCADE_IRQ)); /* ICW3 tells slaves
*/
outb(INT_CTLMASK, ICW4_AT_MASTER);
outb(INT_CTLMASK, ~(1 << CASCADE_IRQ)); /* IRQ 0-7 mask */
outb(INT2_CTL, machine.ps_mca ? ICW1_PS : ICW1_AT);
outb(INT2_CTLMASK, mine ? IRQ8_VECTOR : BIOS_IRQ8_VEC);
/* ICW2 for slave */
outb(INT2_CTLMASK, CASCADE_IRQ); /* ICW3 is slave nr */
outb(INT2_CTLMASK, ICW4_AT_SLAVE);
outb(INT2_CTLMASK, ~0); /* IRQ 8-15 mask */
/* Copy the BIOS vectors from the BIOS to the Minix location, so we
26
* can still make BIOS calls without reprogramming the i8259s.
*/
#if IRQ0_VECTOR != BIOS_IRQ0_VEC
phys_copy(BIOS_VECTOR(0) * 4L, VECTOR(0) * 4L, 8 * 4L);
#endif
#if IRQ8_VECTOR != BIOS_IRQ8_VEC
phys_copy(BIOS_VECTOR(8) * 4L, VECTOR(8) * 4L, 8 * 4L);
#endif
} else {
/* Use the BIOS interrupt vectors in real mode. We only reprogram the * exceptions here, the interrupt vectors are reprogrammed on demand. * SYS_VECTOR is the Minix system call for message passing.
*/
//这个是用于实模式,不做考虑
for (i = 0; i < 8; i++) set_vec(i, int_vec[i]);
set_vec(SYS_VECTOR, s_call);
}
}
/*=========================================================== ================*
* put_irq_handler 这个函数就是注册一个硬件中
断处理程序,主要是设置 hook 相应的域,并且做出一些合法的检测。
*============================================================ ===============*/
PUBLIC void put_irq_handler(hook, irq, handler) irq_hook_t *hook;
int irq;
irq_handler_t handler;
{
/* Register an interrupt handler. */
//这里主要用于注册一个中断处理程序 int id;
irq_hook_t **line;
//如果向量号不在接受的范围,则返回并且输出相关警告 if (irq < 0 || irq >= NR_IRQ_VECTORS)
panic("invalid call to put_irq_handler", irq); //irq 为首队列
line = &irq_handlers[irq];
id =1;//每注册一个中断处理程序,id 就向左移一位,也就是说最多能够注册
32 个中断处理程序
//在普通机中,根本不需要注册这么多 while (*line != NULL) {
27
if (hook == *line) return; /* extra initialization */
line = &(*line)->next;
id <<= 1;
}
if (id == 0) panic("Too many handlers for irq", irq);
//下面将这些挂钩信息都设置成应有的信息,这就是当做注册中断核心步骤
hook->next = NULL;
hook->handler = handler;
hook->irq = irq;
hook->id = id;
*line = hook;
irq_use |= 1 << irq;
}
/*=========================================================== ================*
* rm_irq_handler *
*============================================================ ===============*/
PUBLIC void rm_irq_handler(hook)
irq_hook_t *hook;
{
/* Unregister an interrupt handler. */
//解除一个已经注册的中断处理程序 int irq = hook->irq;
int id = hook->id;
irq_hook_t **line;
if (irq < 0 || irq >= NR_IRQ_VECTORS)
panic("invalid call to rm_irq_handler", irq);
line = &irq_handlers[irq];
while (*line != NULL) {
if ((*line)->id == id) {
(*line) = (*line)->next;
if (! irq_handlers[irq]) irq_use &= ~(1 << irq); return;
}
line = &(*line)->next;
}
/* When the handler is not found, normally return here. */
}
28
/*=========================================================== ================*
* intr_handle *
*============================================================ ===============*/
PUBLIC void intr_handle(hook)
irq_hook_t *hook;
//注意这里处理的入口是 hook,也就是将 irq_handlers[NR_IRQ_VECTORS]里的 hook 位置上
//所有应该处理的中断函数都应该处理掉
{
/* Call the interrupt handlers for an interrupt with the given hook list.
* The assembly part of the handler has already masked the IRQ, reenabled the * controller(s) and enabled interrupts.
*/
//为一个中断来调用中断处理程序,整个 hook 构成一个链表,依次进行检查,如 果有需要服务的,就进行
//服务
/* Call list of handlers for an IRQ. */
while (hook != NULL) {
/* For each handler in the list, mark it active by setting its ID bit,
* call the function, and unmark it if the function returns true.
*/
irq_actids[hook->irq] |= hook->id;//一个 irq 最多可以对应 32 个中断处理程
序
//每个 id 就是标志一个中断处理程序,irq_actids 每位就标志一位 //将 Hook 里面的每一个中断都进行处理掉
if ((*hook->handler)(hook)) irq_actids[hook->irq] &= ~hook->id; hook = hook->next;
}
/* The assembly code will now disable interrupts, unmask the IRQ if and only * if all active ID bits are cleared, and restart a process.
*/
}
#if _WORD_SIZE == 2
/*=========================================================== ================*
* set_vec
*针对 16 位芯片,在这里忽略
*============================================================
29
===============*/
PRIVATE void set_vec(vec_nr, addr)
int vec_nr; /* which vector */
vecaddr_t addr; /* where to start */
{
/* Set up a real mode interrupt vector. */
u16_t vec[2];
/* Build the vector in the array ‘vec‘. */
vec[0] = (u16_t) addr;
vec[1] = (u16_t) physb_to_hclick(code_base);
/* Copy the vector into place. */
phys_copy(vir2phys(vec), vec_nr * 4L, 4L);
}
#endif /* _WORD_SIZE == 2 */
2.6MINIX3 其部件处理中断举例
关于 i8259 几个非常重要的函数,我准备结合 CLOCK 任务来分析: 首先看 clock.c 文件:先看 init_clock.c 函数:
{
put_irq_handler(&clock_hook, CLOCK_IRQ, clock_handler);/* register handler */
„„„„
}
我们深入里面分析:我们继续把 put_irq_handler()函数拿出来 首先上面的函数的各个参数分析下:
Clock_hook 是时钟钩子,irq 对于的就是时钟向量号,clock_handler 就是时钟处 理函数。我们现在以这几个参数进入 put_irq_handler()函数
PUBLIC void put_irq_handler(hook, irq, handler)
irq_hook_t *hook;
int irq;
irq_handler_t handler;
30
{
/* Register an interrupt handler. */
int id;
irq_hook_t **line;
//irq时钟向量号,这点很显然满足条件也就不用执行panic()
if (irq < 0 || irq >= NR_IRQ_VECTORS)
panic("invalid call to put_irq_handler", irq);
//将line指针指向irq_handlers[irq],准备用line来操作这个钩子信息
line = &irq_handlers[irq];
id =1;
//时钟初始化是第一个做到得,所以这个循环实际上不执行
while (*line != NULL) {
if (hook == *line) return;
line = &(*line)->next;
id <<= 1;
}
//出来之后id=1
/* extra initialization */
if (id == 0) panic("Too many handlers for irq", irq); //对时钟钩子相关信息进行设置
hook->next = NULL;
//将钩子的处理函数是在这里就是clock_handler
hook->handler = handler;
hook->irq = irq;
hook->id = id;
*line = hook;
irq_use |= 1 << irq;
}
irq_use |= 1 << irq;
}
函数示意图如下图表示:
31
现在讨论另外一个过程:就是 intr_handler(hook),前面已经介绍这个函数的内容, 现在我们所要考虑的是整个调用的大体过程,我们先看下执行流程图:
32
在进入 intr_handle 之后,我们在这里就是考虑 clock_handler,所以这里就是做 clock_handler 的调用工作。先看看 intr_handle 是干了啥内容:就是扫描 hook 队 列,完成相关的中断处理函数,这里就是完成 CLOCK 中断处理函数。
2.7 MINIX3 系统调用处理机制
以上就是整个硬件中断的基本处理过程。我们现在把话题换到软中断,也就是陷 阱,看看 MINIX3 是怎么处理陷阱过程:
MINIX3 自动陷入内核之后,会进入一个汇编处理函数_s_call
33
!*=================================================================== ========*
!* _s_call 这是系统调用门陷入进来的 *
!*=================================================================== ========*
.align 16
_s_call:
_p_s_call:
cld ! set direction flag to a known value
sub esp, 6*4 ! skip RETADR, eax, ecx, edx, ebx, est
!这些参数都可以跳过
!压栈,ebp已经由TSS自动放入,则ebp可以压入栈中保存
push ebp ! stack already points into proc table
push esi
push edi
!这里不想硬件中断,调用SAVE来做这些压栈动作,这里不需要,主要是为了提
!高效率
o16 push ds
o16 push es
o16 push fs
o16 push gs
!设置内核段选择器
mov dx, ss
mov ds, dx
mov es, dx
incb (_k_reenter)
mov esi, esp ! assumes P_STACKBASE == 0
mov esp, k_stktop
xor ebp, ebp ! for stacktrace
! end of inline save
34
! now set up parameters for sys_call()
!现在进入真正意义上的内核态,也就是说现在的栈是内核公用栈
!下面一系列操作其实是非常明确的,就是将这些参数全部压入栈中,主要是为 了调用_sys_call()函数,这个函数是处理消息机制的与外界唯一接口的函数, 同时ebx eax ecx都是编译器事先约定好的:ebx:指向用户消息 eax:源或者目的 进程号 ecx:代表的是SEND或者是RECEIVE或者是BOTH
push ebx ! pointer to user message
push eax ! src/dest
push ecx ! SEND/RECEIVE/BOTH
call _sys_call ! sys_call(function, src_dest, m_ptr)
!这个函数我们在进程通信机制可以看到,整个函数是一个与外界的接口,主要 用于发送或者接受目的进程的消息
! caller is now explicitly in proc_ptr
mov AXREG(esi), eax ! sys_call MUST PRESERVE si
现在就是直接执行下面的操作 注意下面的函数没有放上来,下面其实马上跟着 的就是restart函数,也就是重新启动这个函数
! Fall into code to restart proc/task running
35
2.8MINIX3 异常处理
这里的异常简单的介绍下,其实异常的处理大体方法和前面的中断或者陷入是同 样的思路,只不过这里的异常是 CPU 自动检测,自动陷入,自动执行异常处理函 数。整个过程可以看下流程图:
!*=================================================================== ========*
!* exception handlers MINIX3默认处理的异常处理方式,所有
的异常最终会进入一个专门的异常通用处理函数:exception,这个函数在后面
会有详细接受 *
!*=================================================================== ========*
_divide_error:
push DIVIDE_VECTOR
jmp exception
36
_single_step_exception:
push DEBUG_VECTOR
jmp exception
_nmi:
push NMI_VECTOR
jmp exception
_breakpoint_exception:
push BREAKPOINT_VECTOR
jmp exception
_overflow:
push OVERFLOW_VECTOR
jmp exception
_bounds_check:
push BOUNDS_VECTOR
jmp exception
_inval_opcode:
push INVAL_OP_VECTOR
jmp exception
_copr_not_available:
push COPROC_NOT_VECTOR
jmp exception
_double_fault:
push DOUBLE_FAULT_VECTOR
jmp errexception
_copr_seg_overrun:
push COPROC_SEG_VECTOR
jmp exception
_inval_tss:
push INVAL_TSS_VECTOR
jmp errexception
_segment_not_present:
push SEG_NOT_VECTOR
jmp errexception
37
_stack_exception:
push STACK_FAULT_VECTOR
jmp errexception
_general_protection:
push PROTECTION_VECTOR
jmp errexception
_page_fault:
push PAGE_FAULT_VECTOR
jmp errexception
_copr_error:
push COPROC_ERR_VECTOR
jmp exception
!*=================================================================== ========*
!* exception 异常同样处理函数 *
!*=================================================================== ========*
! This is called for all exceptions which do not push an error code.
.align 16
exception:
sseg mov (trap_errno), 0 ! clear trap_errno
sseg pop (ex_number)
jmp exception1!进入这个函数
!*=================================================================== ========*
!* errexception *
!*=================================================================== ========*
! This is called for all exceptions which push an error code.
.align 16
errexception:
sseg pop (ex_number)
sseg pop (trap_errno)
exception1: ! Common for all exceptions.
push eax ! eax is scratch register
!这里将eax压栈主要是后面会用到eax用来操作相关参数
38
!将之前的EIP储存到EAX寄存器中,之后存储到(old_eip)变量中
mov eax, 0+4(esp) ! old eip
sseg mov (old_eip), eax
!下面是一样的道理,存储cs,eflags存储到变量old_cs old_eflags中
movzx eax, 4+4(esp) ! old cs
sseg mov (old_cs), eax
mov eax, 8+4(esp) ! old eflags
sseg mov (old_eflags), eax
pop eax
call save
!调用save 做好寄存器保护工作,save前面看到,SAVE之后就是用户栈变成内
核栈
!下面是将参数压栈,之后调用exception()函数来处理异常,这个就深入往里 面分析,处理完之后,就跳过这些参数,准备做好返回工作,就是从内核态返回 用户态,这个处理过程和中断基本上是一样的
push (old_eflags)
push (old_cs)
push (old_eip)
push (trap_errno)
push (ex_number)
call _exception ! (ex_number, trap_errno, old_eip,
! old_cs, old_eflags)
add esp, 5*4
ret
至此中断,系统调用,异常基本分析完,一中断为例,MINIX3 采用一种能够处 理共享中断的方式来处理中断程序,中断处理程序的操作系统部分 MINIX3 尽量 设计的简单高效,主要就是栈之间的变化,以及一些寄存器的储存到哪个栈中。 以及怎么处理返回动作。
中断机制