linux内核中断体系结构
Posted Linux bsping
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核中断体系结构相关的知识,希望对你有一定的参考价值。
一、学习linux的中断目的
1、硬件的中断响应----》了解内核驱动中的中断
2、系统调用的函数响应---》了解系统调用
3、自定义中断---》了解软件的软中断模式
4、信号中断---》了解信号的使用和创建
5、系统的异常与错误---》了解系统的异常的作用,与如何获取异常
二、linux的中断机制
linux的中断分为硬中断和软中断,硬中断包括电脑芯片发出的中断或者ARM中断控制器发出的中断。软中断包括CPU自行保留的异常中断和系统调用。
中断的工作流程:
在ARM体系中,中断的工作流程是:
1、CPU工作模式的转化(七种工作模式,通过更改寄存器来切换)
2、进行寄存器的拷贝与压栈
3、中断异常向量表找中断的入口
4、保存正常运行的函数返回值
5、跳转到对应的中断服务函数上运行
6、中断运行完之后进行模式和寄存器的复原
7、跳转回正常工作函数的地址继续运行
在linux中中断的工作流程:
1、将所有的寄存器值入栈
2、异常码入栈(中断号)
3、将当前函数的返回值入栈(为了复原)
4、调用中断服务子程序
5、从栈中弹出函数返回值
6、弹出所有入栈的寄存器值
在最早起的linux内核代码中,中断的代码结构为汇编+C语言的形式存在,比如asm.s汇编代码对应的C语言代码为trap.c。system_call.s代码对应的C语言代码有fork.c、signal.c、exit.c、sys.c,如下:
终端前的处理过程,中断后的恢复 | 中断的执行过程 | |
硬件中断处理 | asm.s | trap.c |
软件及系统调用的处理 | system_call.s | fork.c exit.c sys.c signal.c |
而在linux内核中asm.s其实也就是硬件中断的处理过程和中断的恢复过程,system_call.s就是软件及系统调用的处理和回复过程。对应的C语言代码就是中断的执行过程。具体体现在一下的代码中(来源于linux 0.11版本)
asm.s中的代码
divide_error:
pushl $do_divide_error # 首先把将要调用的函数地址入栈
no_error_code: # 这里是五出错号处理的入口处。
xchgl %eax,(%esp) # _do_divide_error的地址→eax,eax被交换入栈
pushl %ebx
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds # !!16位的段寄存器入栈后也要占用4个字节。
push %es
push %fs
pushl $0 # "error code" #将数值0作为出错码入栈
lea 44(%esp),%edx # 取对堆栈中原调用返回地址处堆栈指针位置,并压入堆栈。
pushl %edx
movl $0x10,%edx # 初始化段寄存器ds、es和fs,加载内核数据段选择符
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
# 下行上的 * 号表示调用操作数指定地址处的函数,称为间接调用。这句的含义是调用引起本次
# 异常的C处理函数,例如do_divide_error等。
call *%eax #执行前面入栈的C语言函数
addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax # 弹出原来eax中的内容
iret
traps.c中的代码
void do_divide_error(long esp, long error_code)
die("divide error",esp,error_code);
由以上代码可以看出asm.s汇编代码中首先将所需寄存器入栈,这里就是中断的准备过程,之后使用call命令执行C语言代码,进行中断处理,中断处理返回后将前面压栈的寄存器再弹出栈,这样就恢复了之前的状态。
三、linux中断代码实现过程分析
上面引出了die这个C语言函数,一下就是这个函数的源码:
// 取段seg 中地址addr处的一个长字(4 byte).
// 参数:seg - 段选择符;addr - 段内指定地址。
// 输出:%0 - eax(__res);输入:%1 - eax(seg); %2 - 内存地址(*(addr)).
#define get_seg_long(seg,addr) ( \\
register unsigned long __res; \\
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \\
:"=a" (__res):"0" (seg),"m" (*(addr))); \\
__res;)
// 该子程序用来打印出错中断的名称、出错号、调用程序的EIP、EFLAGS、ESP、fs段寄存器值、
// 段的基址、段的长度、进程号PID、任务号、10字节指令码。如果堆栈在用户数据段,则还
// 打印16字节的堆栈内容。
static void die(char * str,long esp_ptr,long nr)
long * esp = (long *) esp_ptr;
int i;
printk("%s: %04x\\n\\r",str,nr&0xffff);
// 下行打印语句显示当前调用进程的CS:EIP、EFLAGS和SS:ESP的值。
// EIP:\\t%04x:%p\\n - esp[1]是段选择符(cs),esp[0]是eip.
// EFLAGS:\\t%p\\n - esp[2]是eflags
// ESP:\\t%04x:%p\\n - esp[4]是源ss,esp[3]是源esp
printk("EIP:\\t%04x:%p\\nEFLAGS:\\t%p\\nESP:\\t%04x:%p\\n",
esp[1],esp[0],esp[2],esp[4],esp[3]);
printk("fs: %04x\\n",_fs());
printk("base: %p, limit: %p\\n",get_base(current->ldt[1]),get_limit(0x17));
if (esp[4] == 0x17)
printk("Stack: ");
for (i=0;i<4;i++)
printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
printk("\\n");
str(i); // 取当前运行任务的任务号
printk("Pid: %d, process nr: %d\\n\\r",current->pid,0xffff & i);
for(i=0;i<10;i++)
printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
printk("\\n\\r");
do_exit(11); /* play segment exception */
该程序是为了异常出现时打印出出错号和栈中的相关信息。
traps.c中最重要的函数还是tarp_init()函数,如下
// 异常(陷阱)中断程序初始化子程序。设置他们的中断调用门(中断向量)。
// set_trap_gate()与set_system_gate()都使用了中断描述符表IDT中的陷阱门(Trap Gate),
// 他们之间的主要区别在于前者设置的特权级为0,后者是3.因此断点陷阱中断int3、溢出中断
// overflow和边界出错中断bounds可以由任何程序产生。
// 这两个函数均是嵌入式汇编宏程序(include/asm/system.h中)
void trap_init(void)
int i;
// 设置除操作出错的中断向量值。
set_trap_gate(0,÷_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);
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);
// 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
// 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21); // 允许8259A主芯片的IRQ2中断请求。
outb(inb_p(0xA1)&0xdf,0xA1); // 允许8259A从芯片的IRQ3中断请求。
set_trap_gate(39,¶llel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
其中tarp_init()函数中有set_trap_gate()与set_system_gate()这两个函数,他们主要就是为了调用中断。这两个函数的区别就是他们的使用权限不同,set_trap_gate()权限高只能由用户程序产生,set_system_gate()权限低可以由用户和系统调用,在下面的代码中可以看到一个权限是0一个是3。
#define _set_gate(gate_addr,type,dpl,addr) \\
__asm__ ("movw %%dx,%%ax\\n\\t" \\
"movw %0,%%dx\\n\\t" \\
"movl %%eax,%1\\n\\t" \\
"movl %%edx,%2" \\
: \\
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \\
"o" (*((char *) (gate_addr))), \\
"o" (*(4+(char *) (gate_addr))), \\
"d" ((char *) (addr)),"a" (0x00080000))
#define set_trap_gate(n,addr) \\
_set_gate(&idt[n],15,0,addr)
#define set_system_gate(n,addr) \\
_set_gate(&idt[n],15,3,addr)
例:根据以上代码,set_trap_gate(0,÷_error);的意思是如果是除于0的错误,则将0传送到中断描述符表中,产生相应的中断。
以上所有就是asm.s对应的硬件中断处理的过程。system_call.s对应的可以自己理解。
linux内存抽象图:
以上是关于linux内核中断体系结构的主要内容,如果未能解决你的问题,请参考以下文章