您好、欢迎来到现金彩票网!
当前位置:彩之网 > 中断机制 >

操作系统课程设计 中断实现机制doc

发布时间:2019-07-25 07:13 来源:未知 编辑:admin

  1.本站不保证该用户上传的文档完整性,不预览、不比对内容而直接下载产生的反悔问题本站不予受理。

  目 录 一、实验题目……………………………………………………4 二、模块整体功能介绍及主要目标……………………………5 三、头文件的分析………………………………………………10 四、数据结构的分析……………………………………………15 五、函数的分析…………………………………………………18 六、时钟中断(一个特别重要的中断) …………………………23 七、分析体会……………………………………………………27 八、参考文献……………………………………………………28 九、附录:Intel保留的中断号含义………………………………29 一、实验题目 1.1 课程设计题目: 中断实现机制源代码分析 1.2题目具体信息 Linux0.11内核源代码中的中断实现机制的源代码分析,主要涉及以下内容: (1) 操作系统中中断服务的处理过程,原理主要在电子书上的中断机制这部分讲到(即第35页的2.4.1—2.4.5); (2) 硬件中断处理类程序主要包括两个代码文件:asm.s 和traps.c 文件。(即电子书中的第150页的5.4—5.5)asm.s 用于实现大部分硬件异常所引起的中断的汇编语言处理过程。而traps.c 程序则实现了asm.s 的中断处理过程中调用相应的C函数程序,显示出错位置和出错号,然后退出中断。; (3)所包含的几个重要的头文件分别为: #include string.h // 字符串头文件。主要定义了一些有关字符串操作的嵌入函数。 #include linux/head.h // head 头文件,定义了段描述符的简单结构,和几个选择符常量。 #include linux/sched.h /* 调度程序头文件,定义了任务结构task_struct、初始任务0 的数据,还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。*/ #include linux/kernel.h // 内核头文件。含有一些内核常用函数的原形定义。 #include asm/system.h //系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。 #include asm/segment.h // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。 #include asm/io.h // 输入/输出头文件。定义硬件端口输入/输出宏汇编语句; (4) 外几个硬件中断处理程序在文件system_call.s 和mm/page.s 中实现。 二、模块整体功能介绍及主要目标 当设备执行某个命令时,如“将读取磁头移动到软盘的第42扇区上”,设备驱动程序可以从查询方式和中断方式中选择一种来判断设备是否已经完成此命令。 查询方式意味着需要经常读取设备的状态,一直到设备状态表表明请求已经完成为止。如果设备驱动程序被连接进内核,这时使用查询方式将会带来灾难性后果:内核将在此过程中无所事事,直到设备完成目前的请求。有一种方法可以有效的改善这一弊端,就是通过使用系统定时器,使内核周期性调用设备驱动程序中的某个例程来检查设备状态。使用定时器是查询方式中最好的一种,但更有效的方法是使用中断。 基于中断的设备驱动程序,指的是在硬件设备需要服务时向CPU发一个中断信号,引发中断服务子程序被执行。这样就大大地提高了系统资源的利用率,使内核不必一直等到设备执行完任务后才开始有事可干,而是在设备工作期间内核就可以转去处理其它的事情,收到中断请求信号时再回头响应设备。其中断示意图如下: 其单个进程执行输出的中断时间线路图如下: 其中断处理过程流程图如下: 2.1 硬件对中断的支持 基于中断的驱动程序是需要一定硬件的支持并有相关软件的协调才能完成,一般的PC结构是通过两片8259A的级联来对各个硬件的中断提供支持的。基结构如下图。 8259A在初始化的时候,可以设定其工作方式和各个中断的优先级,可以屏蔽某些中断,同时也可以设定中断嵌套的方法。也就是说可以非常方便和灵活地控制各个设备的中断。上图给出了两个级连的中断控制器,每个控制器都有一个中断屏蔽寄存器,寄存器的每一位和一个中断对应。如果将此寄存器中某一位置位,就屏蔽掉了对应的中断请求,将对应位置0表示清除屏蔽。但是不幸的是中断屏蔽寄存器是只写的,所以无法读取写入的值勤。这也意味着Linux必须将写入屏蔽寄存器的值保存起来,在开中断和屏蔽中断的时候修改这些保存值,同时将新的屏蔽码写入寄存器中。 当某一设备需要服务时会通过中断控制器向CPU发送一个脉冲信号,CPU接到这个脉冲信号后不通过回送一个ACK信号通知中断控制器将中断号放到数据线上,然后CPU根据中断号去启动中断服务子程序。 2.2 Linux对中断的管理 Linux内核为了将来自硬件设备的中断传递到相应的设备驱动程序,在驱动程序初始化的时候就将其对应的中断程序进行了登记,即通过调用函数request_irq()将其中断信息添加到结构为irqaction的数组中,从而使中断号和中断服务子程序联系起来。 request_irq()函数原形如下: int request_irq(unsigned int irq , /*中断请求号*/ void (*handler)(int ,void * , struct pt_regs * ) , /*指向中断服务子程序*/ unsigned long irqflags , /*中断类型*/ const char * devname , /*设备的字*/ void *dev_id); 另外,irqaction的数据结构如下,如图所示。 struct irqaction { void (* handler)(int , void * , struct pt_regs * ) ; unsigned long flags; unsigned long mask; const char * name; void *dev_id; struct irqaction *next; }; static struct irqaction *irq_action [NR_IRQS+1] 根据设备的中断号可以在数组irq_action检索到设备的中断信息。对中断资源的请求在驱动程序初始化时就已经完成。 在传统的PC体系结构中,有些中断已经被固定下来。软盘设备正是这种情况,它的中断号总为6。有时设备驱动程序可能不知道设备使用的中断号,对PCI设备来说这不是什么大问题,它们总是可以通过设备配置接口知道其中断号。但对于ISA设备则没有取得中断号的方便方式,Linux通过上述设备驱动程序检测它们的中断号来解决这个问题。 让我们来看一下对ISA设备中断号的检测过程。设备驱动程序首先迫使ISA设备引起一个中断,系统中所有未被分配的中断都被打开。此时设备引发的中断可以通过可编程中断控制器发送出去,在它接受到CPU的响应信号以后将中断号放置在数据线上,Linux读取此数据并将其内容返回给设备驱动程序。非0结果则表示在此次检测中有中断发生,设备驱动程序然后将关闭检测并将所有未分配中断屏蔽掉,这样ISA设备驱动程序就成功的找到了设备的IRQ号。 基于PCI系统比基于ISA系统有更多的动态性。ISA设备使用的中断引脚通常是通过硬件设备上的跳线来设置的。而每个PCI设备都对应一个配置头,PCI设备在系统启动与初始化PCI时由PCI BIOSA或PCI子系统来分配中断,将其放入配置头中,故而驱动程序可以方便地获得PCI设备使用的中断号。 系统中可能存在许多PCI中断源,比如在使用PCI-PCI桥接器时。这些中断源的个数可能将超出系统可编程中断控制器的引脚数。此时PCI设备必须共享中断号,中断控制器上的一个引脚可能被多个PCI设备同时使用。Linux让中断的第一个请求者申明此中断是否可以共享,中断的共享将导致irq_action数组中的不念旧恶入口同时指向几个irqaction数据结构,如图所示。当共享中断发生时,Linux将调用对应此中断源的所有中断处理过程。 2.3 Linux对中断的处理 Linux中断处理子系统的一个基本任务是将中断正确联系到中断处理代码中的正确位置。这些代码必须了解系统的中断拓扑结构。例如在中断控制器的引脚6上发生的软盘控制器中断,必须被辨认出的确来自软盘,并同系统的软盘设备驱动的中断处理代码联系起来。 中断发生时,Liunx首先读取系统可编程中断控制器的中断状态寄存器,判断出中断源,将其转换成irq_action数组中偏移值(例如中断控制器引脚6来自软盘控制器的中断将被转换成对应于中断处理过程数组中的第7个指针),然后调用其相应的中断处理程序。 当Linux内核调用设备驱动程序的中断服务子程序时,必须找出中断产生的原因以及相应的解决办法,这是通过读取设备上的状态寄存器的内容来完成的。 下面我们结合输入/输出系统的层次结构来看一下中断在驱动程序工作的过程中的作用: ①用户发出某种输入/输出请求。 ②用驱动程序的read()函数或request()函数,将完成的输入/输出的指令送给设备控制器现在设备驱动程序等待操作的发生。 ③一小段时间以后,硬设备准备好完成指令的操作,并产生中断信号标志事件的发生。 ④中断信号导致调用驱动程序的中断服务子程序,它将所要的数据从硬设备复制到设备驱动程序的缓冲区中,并通知正在等待的read()函数和request()函数,现在数据可供使用。 ⑤在数据可供使用时,read()或request()函数现在可将数据提供给用户进程。 上述过程是经过了简化了的,但却反映了中断的主要过程的主要方面。 三、头文件的分析 (1) string.h 功能: 该头文件中以内嵌函数的形式定义了所有字符串操作函数,为了提高执行速度使用了内嵌汇编程序。另外,在开始处还定义了一个NULL宏和一个SIZE_T类型。 在标准C库函数中也提供同样名称的头文件,但函数实现是在标准C库中,并且其相应的头文件中只包含相关的函数声明。而对于下面列出的string.h文件,Linux虽然给出每个函数的实现,但是每个函数都有extern和inline关键词前缀,即定义的都是一些内联函数。因此对于包含这个头文件的程序,若由于某种原因所使用的内联函数不能被嵌入调用代码中就会使用内核函数库lib/目录下定义的同名函数,参见lib/string.c程序。在那个string.c中,程序首先将extern和inline等给以为空,再包含string.h头文件,因此,string.c程序中实际上包含了string.h头文件中声明函数的另一个实现代码。 源代码中,字符串头文件以内嵌函数的形式定义了所有字符串操作函数。使用gcc 时,同时假定了ds=es=数据空间,这应该是常规的。绝大多数字符串函数都是经手工进行大量优化的,尤其是函数strtok、strstr、str[c]spn。它们应该能正常工作,但却不是那么容易理解。所有的操作基本上都是使用寄存器集来完成的,这使得函数即快有整洁。所有地方都使用了字符串指令,这又使得代码“稍微”难以理解。将一个字符串(src)拷贝到另一个字符串(dest),直到遇到NULL 字符后停止。其中参数:dest - 目的字符串指针,src - 源字符串指针。%0 - esi(src),%1 - edi(dest)。 刚开始清方向位,加载DS,并更新esi。判断刚存储的字节是否是0,不是则向后跳转到标号1 处,否则结束。返回目的字符串指针。拷贝源字符串count 个字节到目的字符串。如果源串长度小于count 个字节,就附加空字符(NULL)到目的字符串。其中参数:dest - 目的字符串指针,src - 源字符串指针,count - 拷贝字节数。 (2) linux/head.h 功能: head 头文件,定义了Intel CPU 中描述符的简单结构,和指定描述符的项号。 1 #ifndef _HEAD_H 2 #define _HEAD_H 4 typedef struct desc_struct { // 定义了段描述符的数据结构。该结构仅说明每个描述 5 unsigned long a,b; // 符是由8 个字节构成,每个描述符表共有256 项。 6 } desc_table[256]; 8 extern unsigned long pg_dir[1024]; // 内存页目录数组。每个目录项为4 字节。从物理地址0 开始。 9 extern desc_table idt,gdt; // 中断描述符表,全局描述符表。 11 #define GDT_NUL 0 // 全局描述符表的第0 项,不用。 12 #define GDT_CODE 1 // 第1 项,是内核代码段描述符项。 13 #define GDT_DATA 2 // 第2 项,是内核数据段描述符项。 14 #define GDT_TMP 3 // 第3 项,系统段描述符,Linux 没有使用。 16 #define LDT_NUL 0 // 每个局部描述符表的第0 项,不用。 17 #define LDT_CODE 1 // 第1 项,是用户程序代码段描述符项。 18 #define LDT_DATA 2 // 第2 项,是用户程序数据段描述符项。 (3) linux/sched.h 功能: 调度程序头文件,定义了任务结构task_struct、初始任务0 的数据,还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。 任务切换宏switch_to(n)(从171行开始)首先声明了一个结构struct{long a,b;}__temp,用于在任务任务内核态堆栈上保留出8字节的空间来存放将切换到新任务的任务状态段TSS的选择符。然后测试我们是否在执行切换到当前任务的操作,如果是则什么也不需要做,直接退出。否则就是把新任务TSS的选择符保存到临时结构__tmp中的偏移位置4处,此时__tmp中的数据设置为: __tmp+0:未定义(long) __tmp+4: 新任务TSS的选择符(word) __tmp+6:未定仪(word) 接下来把%ecx寄存器中的新任务指针与全局变量current中的当前任务指针相交换,让current含有我们将要切换的新任务的指针值,而ecx中则保存着当前任务(本任务)的指针值。接着执行间接长跳转__tmp的指令ljmp。长跳转新任务TSS选择符的指令将忽略__tmp中未定义值的部分,CPU将自动跳转到TSS段指定新任务中去执行,而本任务也就到此暂停执行。这也是我们无需设置结构变量__tmp中其他未定义部分的原因。任务切换操作示意图如下所示: 任务切换操作示意图 当一段时间以后,某个任务的ljmp指令又会跳转到本任务TSS段选择符,从而造成CPU切换回本任务,并从ljmp的下一条指令开始执行。此时ecx中会有本任务即当前任务的指针,因此我们可以使用该指针来检查它是否是最后(最近)一个使用过数学协处理器的任务。若本任务没有使用过协处理器则立刻退出,否则执行clts指令以复位控制寄存器CR0中的任务已切换标志TS。每当任务切换时CPU都可以让内核避免对协处理状态不必要的保存、恢复操作过程,从而提高了协处理器的执行性能。 (4) linux/kernel.h 功能: 定义了一些内核常用的函数原型等。验证给定地址开始的内存块是否超限。若超限则追加内存。( kernel/fork。c, 24 ) 4 void verify_area(void * addr,int count); //显示内核出错信息,然后进入死循环。( kernel/panic.c, 16 )。 5 volatile void panic(const char * str); //标准打印(显示)函数。( init/main.c, 151)。 6 int printf(const char * fmt, ...); //内核专用的打印信息函数,功能与printf()相同。( kernel/printk.c, 21 )。 7 int printk(const char * fmt, ...); //往tty 上写指定长度的字符串。( kernel/chr_drv/tty_io.c, 290 )。 8 int tty_write(unsigned ch,char * buf,int count); //通用内核内存分配函数。( lib/malloc.c, 117)。 9 void * malloc(unsigned int size); //释放指定对象占用的内存。( lib/malloc.c, 182)。 10 void free_s(void * obj, int size); 12 #define free(x) free_s((x), 0) /*下面函数是以宏的形式定义的,但是在某方面来看它可以成为一个真正的子程序如果返回是true 时它将设置标志(如果使用root 用户权限的进程设置了标志,则用于执行BSD 方式的计帐处理)。这意味着你应该首先执行常规权限检查,最后再检测suser()。*/ 21 #define suser() (current-euid == 0) //检测是否是超级用户。 (5) asm/system.h 功能: 该文件中定义了设置或修改描述符/中断门等的嵌入式汇编宏。其中,函数move_to_user_mode()是用于内核在初始化结束时切换到初始进程(任务0)。所使用的方法是模拟中断调用返回过程,也即利用指令iret 运行初始任务0。在切换到任务0 之前,首先设置堆栈,模拟具有特权层切换的刚进入中断调用过程时堆栈的内容布置情况,然后执行iret 指令,从而引起系统切换到任务0 去执行。在执行iret语句时,堆栈内容变化如下图所示: 中断调用层间切换时堆栈内容 任务0 是一个特殊进程,其执行纯粹是人工启动的。它的数据段和代码段直接映射到内核代码和数据空间,即从物理地址0 开始的640K 内存空间,其堆栈地址也即内核代码所使用的堆栈。因此图中堆栈中的原SS 和原ESP 是直接将现有内核的堆栈指针压入堆栈的。 该文件中的另一部分给出了在中断描述符表IDT中设置不同类型描述符项的宏。__set__gate()是一个多参数宏。IDT表中的中断门(Interrupt Gate)和陷阱门(Trap Gate)描述符项的格式见下图所示: 中断门描述符格式 陷阱门描述符格式 其中P是段存在标志;DPL是描述符的优先级。中断门和陷阱门的区别在于对EFLAGS的中断允许标志IF的影响。通过中断门描述符执行的中断复位IF标志,因此这种方式可以避免其他中断干扰当前中断的处理,并且随后的中断结束指令IRET会从堆栈上恢复IF标志的原值;而通过陷阱门执行的中断则不会影响IF标志。 在设置描述符的通用宏__set__gate(gate__addr,type,dpl,addr)中,参数gate__addr指定了描述符所处的物理内存地址。Type指明所需设置的描述符类型,参数dpl即对应描述符格式中的DPL。Addr是描述符对应的中断处理过程的32位偏移地址。因为这段处理过程属于内核段代码,所以他们的段选择符值均为0x0008。 System.h文件的最后一部分是用于设置一般段描述符表GDT中设置任务状态段描述符以及局部表描述符的宏。 (6) asm/segment.h 功能: 该文件中定义了一些访问Intel CPU中段寄存器或与段寄存器有关的内在操作函数。在Linux系统中,当用户程序通过系统调用开始执行内核代码时,内核程序会首先在段寄存器ds和es中加载全局描述符表GDT中的内核数据描述符(段值0x10),即把ds和es用于访问用户数据段。参见system_call.s第89-93行。因此在执行内核代码时,若要存取用户程序(任务)中的数据就需要使用特殊的方式。本文件中的get_fs_byte()和put_fs_byte()等函数就是专门用来访问用户程序中的数据。 1 extern inline unsigned char get_fs_byte(const char * addr) 2 { 3 unsigned register char _v; 5 __asm__ (movb %%fs:%1,%0:=r (_v):m (*addr)); 6 return _v; 7 }//读取fs 段中指定地址处的字节。 /*参数:addr - 指定的内存地址。%0 - (返回的字节_v);%1 - (内存地址addr)。 返回:返回内存fs:[addr]处的字节。*/ extern inline unsigned short get_fs_word(const unsigned short *addr) // 读取fs 段中指定地址处的字。与上类似。 extern inline unsigned long get_fs_long(const unsigned long *addr) // 读取fs 段中指定地址处的长字(4 字节)。与上类似。 25 extern inline void put_fs_byte(char val,char *addr) 26 { 27 __asm__ (movb %0,%%fs:%1::r (val),m (*addr)); 28 }// 将一字节存放在fs 段中指定内存地址处。 /* 参数:val - 字节值;addr - 内存地址。 %0 - 寄存器(字节值val);%1 - (内存地址addr)。*/ extern inline void put_fs_word(short val,short * addr) // 将一字存放在fs 段中指定内存地址处。与上类似。 extern inline void put_fs_long(unsigned long val,unsigned long * addr) // 将一长字存放在fs 段中指定内存地址处。与上类似。 47 extern inline unsigned long get_fs() 48 { 49 unsigned short _v; 50 __asm__(mov %%fs,%%ax:=a (_v):); 51 return _v; 52 }//取fs 段寄存器值(选择符)。返回:fs 段寄存器值。 extern inline unsigned long get_ds()// 取ds 段寄存器值。与上类似。 61 extern inline void set_fs(unsigned long val) 62 { 63 __asm__(mov %0,%%fs::a ((unsigned short) val)); 64 } //设置fs 段寄存器。参数:val - 段值(选择符)。 (7) asm/io.h 功能: 该文件中定义了对硬件IO 端口访问的嵌入式汇编宏函数:outb()、inb()以及outb_p()和inb_p()。前面两个函数与后面两个的主要区别在于后者代码中使用了jmp 指令进行了时间延迟。 // 硬件端口字节输出函数。参数:value - 欲输出字节;port - 端口。 1 #define outb(value,port) \ 2 __asm__ (outb %%al,%%dx::a (value),d (port)) //硬件端口字节输入函数。 //参数:port - 端口。返回读取的字节。 5 #define inb(port) ({ \ 6 unsigned char _v; \ 7 __asm__ volatile (inb %%dx,%%al:=a (_v):d (port)); \ 8 _v; \ 9 }) //带延迟的硬件端口字节输出函数。参数:value - 欲输出字节;port - 端口。 11 #define outb_p(value,port) \ 12 __asm__ (outb %%al,%%dx\n \ 13 \tjmp 1f\n \ 14 1:\tjmp 1f\n \ 15 1:::a (value),d (port)) //带延迟的硬件端口字节输入函数。参数:port - 端口。返回读取的字节。 17 #define inb_p(port) ({ \ 18 unsigned char _v; \ 19 __asm__ volatile (inb %%dx,%%al\n \ 20 \tjmp 1f\n \ 21 1:\tjmp 1f\n \ 22 1::=a (_v):d (port)); \ 23 _v; \ 24 }) 四、数据结构的分析 (1) 段描述符结构(include/linux/head.h,第4 行) CPU 中描述符的简单格式。 struct desc_struct { unsigned long a,b; } desc_table[256]; 定义了段描述符的数据结构。该结构仅说明每个描述符是由8 个字节构成,每个描述符表共有256 项。 (2) i387使用的结构(include/linux/sched.h,第40 行) 这是数学协处理器使用的结构,主要用于保存进程切换时i387 的执行状态信息。 struct i387_struct { long cwd; // 控制字(Control word)。 long swd; // 状态字(Status word)。 long twd; // 标记字(Tag word)。 long fip; // 协处理器代码指针。 long fcs; // 协处理器代码段寄存器。 long foo; long fos; long st_space[20]; /* 8*10 bytes for each FP-reg = 80 bytes */ }; include/linux/sched.h,第51 行)struct tss_struct { long back_link; /* 16 high bits zero */ long esp0; long ss0; /* 16 high bits zero */ long esp1; long ss1; /* 16 high bits zero */ long esp2; long ss2; /* 16 high bits zero */ long cr3; long eip; long eflags; long eax,ecx,edx,ebx; long esp; long ebp; long esi; long edi; long es; /* 16 high bits zero */ long cs; /* 16 high bits zero */ long ss; /* 16 high bits zero */ long ds; /* 16 high bits zero */ long fs; /* 16 high bits zero */ long gs; /* 16 high bits zero */ long ldt; /* 16 high bits zero */ long trace_bitmap; /* bits: trace 0, bitmap 16-31 */ struct i387_struct i387; }; (4) 进程(任务)数据结构task(include/linux/sched.h,第78 行)这是任务(进程)数据结构,或称为进程描述符。 struct task_struct { long state;任务的运行状态(-1 不可运行,0 可运行(就绪),0 已停止)。 long counter ;任务运行时间计数(递减)(滴答数),运行时间片。 long priority ;运行优先数。任务开始运行时counter = priority,越大运行越长。 long signal ;信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1。 struct sigaction sigaction[32] ;信号执行属性结构,对应信号将要执行的操作和标志信息。 long blocked ;进程信号屏蔽码(对应信号位图)。 int exit_code ;任务执行停止的退出码,其父进程会取。 unsigned long start_code ;代码段地址。 unsigned long end_code ;代码长度(字节数)。 unsigned long end_data ;代码长度 + 数据长度(字节数)。 unsigned long brk ;总长度(字节数)。 unsigned long start_stack ;堆栈段地址。 long pid ;进程标识号(进程号)。 long father ;父进程号。 long pgrp ;父进程组号。 long session ;会话号。 long leader ;会话首领。 unsigned short uid ;用户标识号(用户id)。 unsigned short euid ;有效用户id。 unsigned short suid ;保存的用户id。 unsigned short gid ;组标识号(组id)。 unsigned short egid ;有效组id。 unsigned short sgid ;保存的组id。 long alarm ;报警定时值(滴答数)。 long utime ;用户态运行时间(滴答数)。 long stime ;系统态运行时间(滴答数)。 long cutime ;子进程用户态运行时间。 long cstime ;子进程系统态运行时间。 long start_time ;进程开始运行时刻。 unsigned short used_math ;标志:是否使用了协处理器。 int tty ;进程使用tty 的子设备号。-1 表示没有使用。 unsigned short umask ;文件创建属性屏蔽位。 struct m_inode * pwd ;当前工作目录i 节点结构。 struct m_inode * root ;根目录i 节点结构。 struct m_inode * executable ;执行文件i 节点结构。 unsigned long close_on_exec ;执行时关闭文件句柄位图标志。(参见include/fcntl。h struct file * filp[NR_OPEN] ;文件结构指针表,最多32 项。表项号即是文件描述符的值。 struct desc_struct ldt[3] ;任务的局部描述符表。0-空,1-代码段cs,2-数据和堆栈段ds&ss。 struct tss_struct tss ;进程的任务状态段信息结构。 五、函数的分析 (1) asm.s 程序 功能描述 asm.s 汇编程序中包括大部分CPU 探测到的异常故障处理的底层代码,也包括数学协处理器(FPU)的异常处理。该程序与kernel/traps.c 程序有着密切的关系。该程序的主要处理方式是在中断处理程序中调用相应的C 函数程序,显示出错位置和出错号,然后退出中断。 int0 下面是被零除出错(divide_error)处理代码。标号_divide_error实际上是C 语言函数divide_error()编译后所生成模块中对应的名称。_do_divide_error函数在traps.c 中。具体流程图见下: int1 调试中断入口点。处理过程同被零除出错处理。类型:错误/陷阱(Fault/Trap);无错误号。当EFLAGE中TF标志置位时而引发的中断。当发现硬件断点(数据:陷阱,代码:错误);或者开启了指令跟踪陷阱或任务交换陷阱,或者调试寄存器访问无效(错误),CPU就会产生该异常。 int2 非屏蔽中断调用入口点。类型:陷阱;无错误号。这时仅有的被赋予固定中断向量的硬件中断。每当接受到一个NMI信号,CPU内部就会产生中断向量2,并执行标准中断应答周期,因此很节省时间。NMI通常保留为极为重要的硬件使用。当CPU收到一个NMI信号并且开始执行其中断处理过程时,随后所有的硬件诊断将被忽略。 int45 数学协处理器(Coprocessor)发出的中断。当协处理器执行完一个操作时就会发出IRQ13 中断信号,以通知CPU 操作完成。源代码中88行上0XF0是协处理端口,用于清忙琐存器。通过写该端口,本中断将消除CPU的BUSY延续信号,并重新激活80387的处理器扩展请求引脚PEREQ。该操作主要是为了确保在继续执行80387的任务指令之前,CPU响应本中断。具体流程图见下: 注1:内核代码的选择符值为0x10; 注2:无出错代码时就使用0; 注3:调用的C 函数在traps.c 中实现。压入堆栈的出错代码和中断返回地址是用作C 函数的参数。 int8 双出错故障。通常当CPU在调用前一个异常处理程序而检测到一个新的异常时,这两个异常会被串行地进行处理,但也会碰到很少的情况,CPU不能进行这样的串行处理操作,此时就会发生该中断。处理过程如下图: Int3 断点指令引起中断入口点。类型:陷阱;无错误号。该指令通常由调试器插入被调试程序的代码中。 int4 溢出出错处理中断入口点。EFLAGS中OF标志置位时CPU执行INT0指令就会发生该中断。通常用于编辑器跟踪算术计算溢出(overflow)。 int5 边界检查出错中断入口点。当操作数在有效范围以外时引发的中断。当BOUND指令测试失败就会产生该中断。BOUND指令有3个操作书,如果第一个不在另外两个中间,就会产生异常5。 int6 无效操作指令出错中断入口点。CPU执行机构检测到的一个无效的操作码而引起的中断。 int9 协处理器段超出出错中断入口点。该异常基本上等于协处理器出错保护。因为在浮点指令操作数太大时,我们就会有这个机会来加载或保存超出数据段的浮点值。 (2) traps.c 程序 功能描述 traps.c 程序主要包括一些在处理异常故障(硬件中断)的底层代码asm.s 中调用的相应C 函数。用于显示出错位置和出错号等调试信息。其中的die()通用函数用于在中断处理中显示详细的出错信息,而代码最后的初始化函数trap_init()是在前面init/main.c 中被调用,用于硬件异常处理中断向量(陷阱门)的初始化,并设置允许中断请求信号的到来。在阅读本程序时需要参考asm.s 程序。 1) 在程序asm.s 中保存了一些状态后,本程序用来处理硬件陷阱和故障。目前主要用于调试目的,以后将扩展用来杀死遭损坏的进程(主要是通过发送一个信号,但如果必要也会直接杀死)。 13 #include string.h // 字符串头文件。主要定义了一些有关字符串操作的嵌入函数。 14 15 #include linux/head.h // head 头文件,定义了段描述符的简单结构,和几个选择符常量。 16 #include linux/sched.h // 调度程序头文件,定义了任务结构task_struct、初始任务0 的数据, // 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。 17 #include linux/kernel.h // 内核头文件。含有一些内核常用函数的原形定义。 18 #include asm/system.h // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。 19 #include asm/segment.h // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。 20 #include asm/io.h // 输入/输出头文件。定义硬件端口输入/输出宏汇编语句。 2) 定义了一些中断处理程序原型,代码在(kernel/asm.s 或system_call.s)中。 3) static void die(char * str,long esp_ptr,long nr){…} 子程序用来打印出错中断的名称、出错号、调用程序的EIP、EFLAGS、ESP、fs 段寄存器值、 段的基址、段的长度、进程号pid、任务号、10 字节指令码。如果堆栈在用户数据段,则还打印16 字节的堆栈内容。 4) 用以do_开头的函数是对应名称中断处理程序调用的C 函数。 如:87 void do_double_fault(long esp, long error_code) 5) 下面是异常(陷阱)中断程序初始化子程序。设置它们的中断调用门(中断向量)。 set_trap_gate()与set_system_gate()的主要区别在于前者设置的特权级为0,后者是3。因此断点陷阱中断int3、溢出中断overflow 和边界出错中断bounds 可以由任何程序产生。这两个函数均是嵌入式汇编宏程序(include/asm/system.h,第36 行、39 行)。 181 void trap_init(void) 六、时钟中断(一个特别重要的中断) 1.时钟中断的产生 Linux的OS时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲送入CPU,就可以引发一个中断请求信号,我们就把它叫做时钟中断。 时钟中断的物理产生如图所示。 操作系统对可编程定时/计数器进行有关始化,然后定时/计数器就对输入脉冲进行计数(分频),产生的三个输出脉冲Out0、 Out1、 Out2各有用途,其中Out0脉冲信号接以中断控制器8259A_1的0号管脚,触发一个周期性的中断,我们就把这个中断叫做时钟中断,时钟中断的周期,也就是脉冲信号的周期,我们叫做“滴答”或“时标”(tick)。从本质上说,时钟中断只是一个周期性的信号,完全是硬件行为,该信号触发CPU去执行一个中断服务程序,但是为了方便,我们就把这个服务程序叫做时钟中断,延用至今。 2.Linux实现时钟中断的全过程 首先:对可编程中断控制器进行初始化 计算机的所有外部中断都由可编程中断控制器统一管理,IBM PC机都使用8259A做中断控制器。目前绝大多数的PC用两片8259A来管理着15个外部中断,我们用8259A_1和8259_2来表示这两个芯片,上图中Out0上的输出脉冲就接到8259A_1的0号管脚。 8259A初始化的目的是写入有关命令字,8259A内部有相应的寄存器来锁存这些命令字,以控制8259A工作。Linux对8259A的初始化如下(代码在boot/setup.S中): 首先说明:两片8259A的片选信号/CS接I/O端口的译码输出/INTRCS分别占用端口地址20H、21AH和A0H、A1H mov a1 , #0x11 out #0x20 , a1 call delay out #0xA0 , a1 call delay ;call delay:对I/O操作都要做相应的延时。 ;这一步送数据到初始化寄存器ICW1,启动初始化编程。 ;意义:外部中断请求信号为上升沿有效。即外部中断由上升沿触发;系统中有多片8259A级连;要向初始化寄存器ICW4送数据。 mov a1 , #0x20 out #0x21 , a1 call delay mov a1 , #0x28 out #0xA1 , al call delay ;这一步送数据到初始化寄存器ICW2。ICW2是中断矢量寄存器,初始化时写入高五位作为中断矢量的高五位,然后在中断响应时由8259根据中断源(哪个管脚)自动填入形成完整的8位中断类型号。 意义:限定了外部中断的中断类型号为0x20H~0x2FH。所以,时钟中断的中断类型号就是INT20H。 mov a1 , #0x04 out #0x21 , a1 call delay mov al , #0x02 out #0xA1 , a1 call delay ;这一步送数据到初始化寄存器ICW3。ICW3是8259的级连命令字,用来区分主片和从片,并表示了主片和从片物理上的连接关系。 ;意义:8259A_1是主片,8259A_2是从片;8259A_2连接在8259A_1的2号管脚上 mov a1 , #0x01 out #0x21 , a1 call delay out #0xA1 , al cal delay ;这一步送数据到初始化寄存器ICW4,ICW4用于指定中断嵌套方式、数据缓冲选择、中断结束方式和CPU类型。 ;意义有四方面①中断嵌套方式为一般嵌套方式。当某个中断正在服务时,本级中断及更低级的中断都被屏蔽,只有更高级的中断才能响应。注意,这对于多片8259A级连的中断系统来说,当某从片中一个中断正在服务时,主片即将这个从片的所有中断屏蔽,所以此时即使本片有比正在服务的中断级别更高的中断源发出请求,也不能得到响应,即不能中断嵌套。②8259A数据线和总线之间不加态缓冲器。一般来说,只有级连片数很多时才用到三态缓冲器;③中断结束方式为正常方式(非自动结束方式)。即在中断服务结束时(中断服务程序末尾),要向8259A芯片发送结束命令字EOI(送到工作寄存器OCW2中),于是中断服务器寄存器ISR中的当前服务位被清0(外部中断返回指令如下所示:mov a1,20H;out 20H,a1;iret),EOI命令字的格式有多种,在此不详述;④CPU类型为8086/8088系列。 mov a1 , #0xFF out #0xA1 , a1 call delay mov a1 , #0xFB out #0x21 , a1 ;这一步送数据到工作寄存器OCW1。OCW1又称中断屏蔽字,其每一位控制一根中断请求输入线的位,对应的中断请求线被屏蔽;否则,被允许。在8259A初始化完成后,OCW1还会被重复写入以控制中断源的开关。 ;意义:屏蔽所有外部中断(因为此时系统尚未初始化完毕,不能接收任何外部中断请求)。 其初始化流程图如下: 至此,8259A的初始化工作宣告完成。最后要说明的是:IBM PC机的ROM_BIOS中固化有对中断控制器的初始化程序段,在计算机加电时,这段程序自动运行。典型的PC机将外部的中断类型号分配为:08H~0FH,70H~77H。但是Linux对8259A作了重新初始化,修改了外部中断的中断类型号的分配(20H~2FH),使中断类型号的分配更加合理。 然后:对可编程定时/计数器进行化。 计数器0的输出就是图中的Out0,它的频率由操作系统的设计者确定,Linux对8253的初始化程序段如下: //在kernel/irq.c中 /*set the clock to 100HZ*/ outb_p(0x34 , 0x43); /*写计数器0的控制字:工作方式2*/ outb_p(LATCH & 0xff , 0x40); /*写计数初值LSB 计数初值低位字节*/ outb(LATCH 8 , 0x40); /*写计数初值MSB 计数初值高位字节*/ LATCH(英文意思为:锁存器,即其中锁存了计数器0的初值)为计数器0的计数初值,定义如下: //在include/linux/timex.h中 #define CLOCK_TICK_RATE 1193180 /*图中的输入脉冲*/ #define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /*计数器0的计数初值*/ CLOCK_TICK_RATE是整个8253的输入脉冲,如图中所示为1。193180MHz,是近似为1MHz的方波信号,8253内部的三个计数器都对这个时钟进行计数,进而产生不同的输出信号,用于不同的用途。 HZ表示计数器0的频率,也就是时钟中断或系统时钟的频率,定义如下: //在include/asm/param.h中 #define HZ 100 下面我们看时钟中断触发的服务程序,该程序代码比较复杂,颁布在不同的源文件中,主要包括如下函数: 时钟中断程序: timer_interrupt(); 时钟函数: do_timer(); 中断安装程序: setup_x86_irq(); 系统调用函数: ret_from_sys_call(); 这里仅对时钟函数做详细注解 //在kernel/time.c中 /*主要是调用时钟函数do_timer()。另一个主要任务是维持实时时钟(RTC,每隔一定时间段要回写)*/ static inline void timer_interrupt(int irq , void *dev_id , struct pt_regs *regs) { do_timer(regs); /*调用时钟函数,将时钟函数等同于时钟中断未尝不可。每隔11分钟就更新RTC中的时间信息,以使OS时钟和RTC时钟保持同步sec last_rtc_update + 660) /*11分钟即660稍等,xtime.tv_sev的单位是秒。 last_trc_update记录的是上次RTC更新时的值*/ update_RTC(); //更新RTC时间 } 七、分析体会 在这次为期一个礼拜的操作系统课程设计当中,我认真地分析了老师提供的Linux0.11内核源代码分析电子书,在图书馆寻找相关资料,并到Linux相关网站浏览与学习有关中断机制的各个知识点以其具体的知识,实现了课余时间对Linux的拓展学习。在此基础上,我剖析了一个系统中一个最重要的中断——时钟中断。 “时钟中断”是特别重要的一个中断,因为整个操作系统的活动都受到它的激励。系统利用时钟中断维持系统时间、促使环境的切换,以保证所有进程共享CPU;利用时钟中断进行记帐、监督系统工作以及确定未来的高度优先级等工作。可以说,“时钟中断”是整个操作系统的脉搏。从本质上说,时钟中断只是一个周期性的信号,完全是硬件行为,该信号触发CPU去执行一个中断服务程序。 在前面我已经详细地介绍了时钟中断的产生——Linux的OS时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲送入CPU,就可以引发一个中断请求信号,我们就把它叫做时钟中断。 在Linux实现时钟中断的全过程中分为两部分讲解。 首先:对可编程中断控制器进行初始化,8259A初始化的目的是写入有关命令字,8259A内部有相应的寄存器来锁存这些命令字,以控制8259A工作。Linux对8259A的初始化代码在boot/setup.S中。我将系统提供的代码进行了详细分析,其作用以及意义分别为:外部中断由上升沿触发;系统中有多片8259A级连;要向初始化寄存器ICW4送数据;限定了外部中断的中断类型号为0x20H~0x2FH。所以,时钟中断的中断类型号就是INT20H;8259A_1是主片,8259A_2是从片;8259A_2连接在8259A_1的2号管脚上;屏蔽所有外部中断(因为此时系统尚未初始化完毕,不能接收任何外部中断请求)。(其初始化流程图已经在前面画出,可供参考。) 然后:对可编程定时/计数器进行化。 在前面我详细地讲解了时钟中断触发的服务程序,主要包括以下函数: (1) 时钟中断程序timer_interrupt():在kernel/time.c中,主要是调用时钟函数do_timer(),另一个主要任务是维持实时时钟(RTC,每隔一定时间段要回写)。 (2)时钟函数do_timer():在kernel/sched.c中,在该函数中有两个变量lost_ticks和lost_ticks_system,这是用来记录timer_bh()例程执行前时钟中断发生的次数。 (3)中断安装程序setup_x86_irq():在kernel/time.c中Setup_x86_irq(0 , 在kernel/irq.c中,BUILD_TIMER_IRQ(FIRST , 0 , 0x01) (4)系统调用函数ret_from_sys_call():在kernel/entry.S中,这段代码就是从系统调用返回函数ret_from_sys_call,它是从中断和系统调用返回时的通用接口。 最后我们再次从总体上浏览一下时钟中断: 每个时钟滴答,时钟中断得到执行。时钟中断执行的频率很高:100Hz,时钟中断的主要工作是处理和时间有关的所有信息、决定是否执行调度程序以及处理内核例程。和时间有关的所有信息包括系统时间、进程的时间片、延时、使用CPU的时间、各种定时器,进程更新后的时间片为进程调度提供依据,然后在时钟中断返回时决定是否要执行调度程序。处理内核进程是Linux提供的一种机制,叫做内核机制,它使一部分工作推迟执行。时钟中断要绝对保证维持系统时间的准确性,而内核机制的提供不但保证了这种准确性,还大幅提高了系统性能。 Linux最本质的东西体现在其”自由” ,”开放”的思想,”自由”意味着世界范围内的知识共享,而”开放”则意味着Linux对所有的人都打开了大让,在这开放而自由的天地里面,你的创造激情可以得到充分的发挥。 最后感谢熊海泉老师,在平时学习和课程设计中对我们的精心教导。这次内核代码分析的课程设计让我学到了许多书本上没有的知识,也从最深层次上了解到了操作系统。我想这对我以后的学习必定受益非浅。 八、参考文献 [1]Linux内核完全注释(内核版本0.11,修正版V2.0).赵炯著 [2] 《操作系统概念(第六版)》.Peter Baer Gagne.高等教育出版社 [3] 《The Linux Kernel Prime》,Gordon Fischer 等,机械工业出版社,2006 [4] 《Linuxr操作系统内核分析》 陈莉君 人民邮电出版社 2004 [5] 《操作系统概念》[美]Peter Baer Galvin著 郑扣根译 科学出版社 2004年 [6] [7] 九、附录:Intel保留的中断号含义 Intel保留的中断号含义如下表所示: 第2页

http://ando2.com/zhongduanjizhi/267.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有