zl程序教程

您现在的位置是:首页 >  系统

当前栏目

linux内核中断体系结构

Linux内核 中断 体系结构
2023-09-11 14:16:29 时间

一、学习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.csystem_call.s代码对应的C语言代码有fork.c、signal.c、exit.c、sys.c,如下:

终端前的处理过程,中断后的恢复中断的执行过程
硬件中断处理asm.strap.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,&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);
	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,&parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}

其中tarp_init()函数中有set_trap_gate()与set_system_gate()这两个函数,他们主要就是被内核调用初始化中断向量表(idt)。这两个函数的区别就是他们的使用权限不同,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)

例:根据以上代码,如果产生除数为零的错误,内核在IDT中感知到中断,给出中断号,执行asm.s中代码,执行到do_divide_error(long esp, long error_code),打印出错误信息,再恢复现场,一次中断就结束了。

        以上所有就是asm.s对应的硬件中断处理的过程。system_call.s对应的可以自己理解。

linux内存抽象图: