《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——2.5 异常处理类中断服务程序挂接

简介: 本节书摘来自华章计算机《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》一书中的第2章,第2.5节,作者:新设计团队著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.5 异常处理类中断服务程序挂接

不论是用户进程还是系统内核都要经常使用中断或遇到很多异常情况需要处理,如CPU在参与运算过程中,可能会遇到除零错误、溢出错误、边界检查错误、缺页错误……免不了需要“异常处理”。中断技术也是广泛使用的,系统调用就是利用中断技术实现的。这些中断、异常都需要具体的服务程序来执行。trap_init()函数将中断、异常处理的服务程序与IDT进行挂接,逐步重建中断服务体系,支持内核、进程在主机中的运算。挂接的具体过程及异常处理类中断服务程序在IDT中所占用的位置如图2-6所示。

image

执行代码如下:

//代码路径:init/main.c:
void main(void)
{
    …
    trap_init();
    …
}

//代码路径:kernel/traps.c:
void trap_init(void)
{
    int i;

    set_trap_gate(0,&divide_error);//除零错误
    set_trap_gate(1,&debug);    //单步调试
    set_trap_gate(2,&nmi);    //不可屏蔽中断

    set_system_gate(3,&int3);    /* int3-5 can be called from all */
    set_system_gate(4,&overflow);    //溢出
    set_system_gate(5,&bounds);    //边界检查错误
    set_trap_gate(6,&invalid_op);    //无效指令
    set_trap_gate(7,&device_not_available);    //无效设备
    set_trap_gate(8,&double_fault);    //双故障
    set_trap_gate(9,&coprocessor_segment_overrun);//协处理器段越界
    set_trap_gate(10,&invalid_TSS);    //无效TSS
    set_trap_gate(11,&segment_not_present);    //段不存在
    set_trap_gate(12,&stack_segment);    //栈异常        
    set_trap_gate(13,&general_protection);    //一般性保护异常
    set_trap_gate(14,&page_fault);    //缺页
    set_trap_gate(15,&reserved);    //保留
    set_trap_gate(16,&coprocessor_error);    //协处理器错误
    for (i=17;i<48;i++)    //都先挂接好,中断服务程序函数名初
    //始化为保留
         set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);    //协处理器
    outb_p(inb_p(0x21)&0xfb,0x21);    //允许IRQ2中断请求
    outb(inb_p(0xA1)&0xdf,0xA1);    // 允许IRQ2中断请求
    set_trap_gate(39,&parallel_interrupt);    //并口
}

//代码路径:include\asm\system.h:
    …
#define _set_gate(gate_addr,type,dpl,addr)    \
__asm__("movw %%dx,%%ax\n\t" \    //将edx的低字赋值给eax的低字
         "movw %0,%%dx\n\t" \    //%0对应第二个冒号后的第1行的"i"
         "movl %%eax,%1\n\t" \    //%1对应第二个冒号后的第2行的"o"
         "movl %%edx,%2" \    //%2对应第二个冒号后的第3行的"o"
         : \    //这个冒号后面是输出,下面冒号后面
    //是输入
    : "i" ((short) (0x8000 + (dpl<<13) + (type<<8))), \    //立即数
           "o" (*((char *) (gate_addr))), \        //中断描述符前4个字节的地址
           "o" (*(4 + (char *) (gate_addr))), \        //中断描述符后4个字节的地址
           "d" ((char *) (addr)),"a" (0x00080000))    //"d"对应edx,"a"对应eax
    …
#define set_trap_gate(n,addr) \
    _set_gate(&idt[n],15,0,addr)

这些代码的目的就是要拼出第1章1.3.5节讲述过的中断描述符。为了便于阅读,复制在下面,如图2-7所示。

image

上述代码的执行效果如图2-8所示。

image

对比:
set_trap_gate(0,&divide_error)
set_trap_gate(n,addr)
_set_gate(&idt[n],15,0,addr)
_set_gate(gate_addr,type,dpl,addr)
可以看出,n是0;gate_addr是&idt[0],也就是idt的第一项中断描述符的地址;type是15;dpl(描述符特权级)是0;addr是中断服务程序divide_error(void)的入口地址,如图2-9所示。

image

“movw %%dx,%%axnt”是把edx的低字赋值给eax的低字;edx是(char ) (addr),也就是&divide_error;eax的值是0x00080000,这个数据在head.s中就提到过,8应该看成1000,每一位都有意义,这样eax的值就是0x00080000 + ((char )(addr)的低字),其中的0x0008是段选择符,含义与第1章中讲解过的“jmpi 0,8”中的8一致。
"movw %0,%%dxnt”是把(short) (0x8000 + (dpl<<13) + (type<<8))赋值给dx。别忘了,edx是(char *) (addr),也就是&divide_error。
因为这部分数据是按位拼接的,必须计算精确,我们耐心详细计算一下:
0x8000就是二进制的1000 0000 0000 0000;
dpl是00,dpl<<13就是000 0000 0000 0000;
type是15,type<<8就是1111 0000 0000;
加起来就是1000 1111 0000 0000,这就是dx的值。edx的计算结果就是(char *) (addr) 的高字即&divide_error的高字 + 1000 1111 0000 0000。
"movl %%eax,%1nt”是把eax的值赋给((char ) (gate_addr)),就是赋给idt[0]的前4字节。同理,"movl %%edx,%2”是把edx的值赋给(4 + (char ) (gate_addr)),就是赋给idt[0]的前后4字节。8字节合起来就是完整的idt[0]。拼接的效果如图2-10所示。
IDT中的第一项除零错误中断描述符初始化完毕,其余异常处理服务程序的中断描述符初始化过程大同小异。后续介绍的所有中断服务程序与IDT的初始化基本上都是以这种方式进行的。
set_system_gate(n,addr)与set_trap_gate(n,addr)用的_set_gate(gate_addr,type,dpl,addr)是一样的;差别是set_trap_gate的dpl是0,而set_system_gate的dpl是3。dpl为0的意思是只能由内核处理,dpl为3的意思是系统调用可以由3特权级(也就是用户特权级)
调用。
有关特权级更深入的内容,请参看《Intel IA-32 Architectures Software Developer’s Manual Volume 3.pdf》(可以到Intel官方网站下载)。
接下来将IDT的int 0x11~int 0x2F都初始化,将IDT中对应的指向中断服务程序的指针设置为reserved(保留)。
设置协处理器的IDT项。
允许主8259A中断控制器的IRQ2、IRQ3的中断请求。
设置并口(可以接打印机)的IDT项。
32位中断服务体系是为适应“被动响应”中断信号机制而建立的。其特点、技术路线是这样的:一方面,硬件产生信号传达给8259A,8259A对信号进行初步处理并视CPU执行情况传递中断信号给CPU;另一方面,CPU如果没有接收到信号,就不断地处理正在执行的程序,如果接收到信号,就打断正在执行的程序并通过IDT找到具体的中断服务程序,让其执行,执行完后,返回刚才打断的程序点继续执行。如果又接收到中断信号,就再次处理中断……

image

最原始的设计不是这样,那时候CPU每隔一段时间就要对所有硬件进行轮询,以检测它的工作是否完成,如果没有完成就继续轮询,这样就消耗了CPU处理用户程序的时间,降低了系统的综合效率。可见,CPU以“主动轮询”的方式来处理信号是非常不划算的。以“被动响应”模式替代“主动轮询”模式来处理主机与外设的I/O问题,是计算机历史上的一大进步。

相关文章
|
10天前
|
Linux C语言
Linux内核队列queue.h
Linux内核队列queue.h
|
3天前
|
算法 Linux 调度
深入理解Linux内核的进程调度机制
【4月更文挑战第17天】在多任务操作系统中,进程调度是核心功能之一,它决定了处理机资源的分配。本文旨在剖析Linux操作系统内核的进程调度机制,详细讨论其调度策略、调度算法及实现原理,并探讨了其对系统性能的影响。通过分析CFS(完全公平调度器)和实时调度策略,揭示了Linux如何在保证响应速度与公平性之间取得平衡。文章还将评估最新的调度技术趋势,如容器化和云计算环境下的调度优化。
|
8天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
18 3
|
15天前
|
负载均衡 算法 Linux
深度解析:Linux内核调度器的演变与优化策略
【4月更文挑战第5天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分——内核调度器。文章将首先回顾Linux内核调度器的发展历程,从早期的简单轮转调度(Round Robin)到现代的完全公平调度器(Completely Fair Scheduler, CFS)。接着,分析当前CFS面临的挑战以及社区提出的各种优化方案,最后提出未来可能的发展趋势和研究方向。通过本文,读者将对Linux调度器的原理、实现及其优化有一个全面的认识。
|
15天前
|
Ubuntu Linux
Linux查看内核版本
在Linux系统中查看内核版本有多种方法:1) 使用`uname -r`命令直接显示版本号;2) 通过`cat /proc/version`查看内核详细信息;3) 利用`dmesg | grep Linux`显示内核版本行;4) 如果支持,使用`lsb_release -a`查看发行版及内核版本。
36 6
|
18天前
|
Linux 内存技术
Linux内核读取spi-nor flash sn
Linux内核读取spi-nor flash sn
14 1
|
24天前
|
存储 网络协议 Linux
【Linux 解惑 】谈谈你对linux内核的理解
【Linux 解惑 】谈谈你对linux内核的理解
23 0
|
11天前
|
Web App开发 Linux 网络安全
工作中常用到的Linux命令
工作中常用到的Linux命令
|
12天前
|
Web App开发 Java Linux
Linux之Shell基本命令篇
Linux之Shell基本命令篇
Linux之Shell基本命令篇
|
8天前
|
NoSQL Linux Shell
常用的 Linux 命令
常用的 Linux 命令
30 9