分享
 
 
 

Linux源代码阅读笔记-硬件中断

王朝system·作者佚名  2006-01-31
窄屏简体版  字體: |||超大  

Linux硬中断

Linux 中断和其他操作系统的中断处理一样,要求有硬件和软件的支持。Linux的好处就是可以看到核心处理中断的一举一动,以下对linux的中断机制做详细的分析。

首先对linux中能处理的中断分类:

1.物理硬件设备产生的中断,这些设备与主板上的i8259A中断控制器相连,具体的连接可以找本《计算机组成原理》看看。linux中可以处理的有16个中断号,但这并不意味linux只能处理16个外设中断请求,实际上许多外设是可以共享中断号,这个要求操作系统的软件支持,在后面可以看到linux是如果处理。

2.异常,异常是无法预测的意外。如被0除、缺页、

set_trap_gate(0,&divide_error);

set_trap_gate(1,&debug);

set_intr_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,&spurious_interrupt_bug);

set_trap_gate(16,&coprocessor_error);

set_trap_gate(17,&alignment_check);

set_trap_gate(18,&machine_check);

set_trap_gate(19,&simd_coprocessor_error);

set_system_gate(SYSCALL_VECTOR,&system_call);

/*

* default LDT is a single-entry callgate to lcall7 for iBCS

* and a callgate to lcall27 for Solaris/x86 binaries

*/

set_call_gate(&default_ldt[0],lcall7);

set_call_gate(&default_ldt[4],lcall27);

以上摘自i386\kernel\Traps.c-> trap_init()函数片断,trap_init()函数又被操作系统的初始化工作start_kernel()函数调用,从列表中可以看出linux所处理的异常。从上面的注释中可以看到异常3-5,就是中断指令int3,int4,int5可以被所有的进程调用,这三个实际上是调试程序所用到的,所以当然能被你的程序调用。而其他的异常只能被处于特权级0的进程调用,也就是说这些异常只能被内核所处理,道理非常明显,但是实现起来就不是那么直接了。

3.陷阱(trap)

陷阱又称为主动异常,就是在你的程序中直接出现int n指令,实际上在你的程序中能出现的这样的指令也不多,除非你的程序是已模块的方式联入内核中了。

4.系统调用int80

之所以特地提出就是为了把系统调用说明白,系统调用是操作系统实现的,给用户提供的功能接口。很多人会对之有一种神秘感,它的代码在系统初始化时载入内存的低端,被映射入内核空间(3G)以上,对所有的调用进程来说,它们的地址是一样的。

以上的分类很多人并不一定赞同,但有胜于无。

Linux的中断的处理

在80386以前的cpu中,中断是通过中断向量表IDT处理。这个处理大家可以看看有关的书。就是操作系统把中断处理的人口地址放在内存0开始的地方,每一个中断向量占4个字节,2个为了段基地址,另外2个为了偏移。一共是256个向量,刚好是1k。在80386中,CPU允许在任意地址存放IDT,这样和处理段一样,CPU多了一个寄存器IDTR,存放IDT在内存的起址。80386中为了在保护模式中处理中断,引入了一个新的术语“门”,对于这个术语大家可以参考intel的资料,在这里就理解为进程权限改变时必须通过的一道检查门。Intel为80386设置了4种门:任务门、中断门、陷阱门、调用门。后三者基本上差不多。任务门看不出有什么用处,在linux的注释中说明为了在linux上运行iBCS和Solaris/X86设置了两个任务门。80386的中断向量描述也和8086完全不一样了,中断向量有64位长,给出的是中断处理程序的段选择符(16位)、程序的偏移地址(32位)、类型码。因此在这里可以描述一下80386处理中断的场景了:(1)CPU根据中断矢量在IDT中找到对应的中断项;(2)根据中断向量中的段选择符,在GDT中找到段的起址,结合偏移量,就得到中断服务的线性地址,这个地址肯定在3G以上的,也就是说是映射到核心区域中的。

80386中对中断设置了门(中断向量),它对进程的权限有严格的要求,具体是:

1.进入门之前,进程的权限要高于门的权限(这个权限在中断向量的类型描述中),

2.进入门后,进程的权限要高于中断服务程序所在的段的运行权限(GDT)。

3.硬件中断可以绕过上述过程。

4.其它类型的中断,异常,如果不满足1就会引发一个保护异常。

这种权限设置在初始化的时候就完成了,我们看函数trap_init()中,共有四种设置函数,分别是

set_trap_gate

set_system_gate

set_intr_gate

set_call_gate

在源代码traps.c中找到它们的定义

void set_intr_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,14,0,addr);

}

static void __init set_trap_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,15,0,addr);

}

static void __init set_system_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,15,3,addr);

}

static void __init set_call_gate(void *a, void *addr)

{

_set_gate(a,12,3,addr);

}

实际上它们都是由一个内部函数完成的,_set_gate(),就是参数不同,这里就关注第三个参数,这个参数就是门的权限。如果这个参数等于3,就是说用户态进程也能调用。这样就得出system_gate和call_gate用户态进程能调用,intr_gate和trap_gate只有 核心态进程能调用了。这里要补充一点就是intr_gate和trap_gate系统处理是基本一致的,唯一的不同点就是intr_gate处理关了中断,而trap_gate在处理时不关中断。

对异常的处理具体可以看处理函数的定义,这里就不详细解释了。Linux对硬件的中断进行了统一的处理。这里就要看看具体的代码了。在源文件arch\i386\kernel\i8259.c中,先看一下函数init_IRQ,这个函数是硬件中断的初始化函数,也被start_kernel调用。去掉了SMP宏等条件编译的代码,简化一下就得到如下代码

void __init init_IRQ(void)

{

……….

init_ISA_irqs();

……….

for (i = 0; i < NR_IRQS; i++) {

int vector = FIRST_EXTERNAL_VECTOR + i;

if (vector != SYSCALL_VECTOR)

set_intr_gate(vector, interrupt[i]);

}

……..

}

核心是两个函数init_ISA_irqs,set_intr_gate和一个全局数组变量interrupt。宏FIRST_EXTERNAL_VECTOR=20,也就是硬件中断向量>=20,这里说明一下中断号和中断向量的区别,中断号实际上是硬件连接i8259a是的线号,是物理的;中断向量是逻辑的,代表中断服务在IDT中的下标。宏SYSCALL_VECTOR是0x80,为系统调用保留。

先看init_ISA_irqs的定义

void __init init_ISA_irqs (void)

{

int i;

init_8259A(0);

for (i = 0; i < NR_IRQS; i++) {

irq_desc[i].status = IRQ_DISABLED;

irq_desc[i].action = 0;

irq_desc[i].depth = 1;

if (i < 16) {

/*

* 16 old-style INTA-cycle interrupts:

*/

irq_desc[i].handler = &i8259A_irq_type;

} else {

/*

* 'high' PCI IRQs filled in on demand

*/

irq_desc[i].handler = &no_irq_type;

}

}

}

首先要解释的是全局irq_desc数组。

/*

* This is the "IRQ descriptor", which contains various information

* about the irq, including what kind of hardware handling it has,

* whether it is disabled etc etc.

*

* Pad this out to 32 bytes for cache and indexing reasons.

*/

typedef struct {

unsigned int status; /* IRQ status */

hw_irq_controller *handler;

struct irqaction *action; /* IRQ action list */

unsigned int depth; /* nested irq disables */

spinlock_t lock;

} ____cacheline_aligned irq_desc_t;

extern irq_desc_t irq_desc [NR_IRQS];

从以上的声明中看出,irq_desc是"IRQ descriptor"。不明白的是结构中的类型struct irqaction和hw_irq_controller,再找出它们的声明:

struct hw_interrupt_type {

const char * typename;

unsigned int (*startup)(unsigned int irq);

void (*shutdown)(unsigned int irq);

void (*enable)(unsigned int irq);

void (*disable)(unsigned int irq);

void (*ack)(unsigned int irq);

void (*end)(unsigned int irq);

void (*set_affinity)(unsigned int irq, unsigned long mask);

};

typedef struct hw_interrupt_type hw_irq_controller;

struct irqaction {

void (*handler)(int, void *, struct pt_regs *);

unsigned long flags;

unsigned long mask;

const char *name;

void *dev_id;

struct irqaction *next;

};

hw_irq_controller就是控制中断控制芯片的,它的功能从数据类型上可以看出。所以当I<16时irq_desc[i].handler = &i8259A_irq_type;这里的I对应着中断号。struct irqaction是一个链表,前面说过硬件可以共享中断号,因此一个中断号来了后,就在一个链表中找处理程序,这个处理程序就是irqaction->handler,这个链表是irq_desc[i]-> irqaction。可见init_ISA_irqs就是初始化irq_desc数组用的,irq_desc数组记录了各个中断号的服务程序的列表,在后面的系统统一中断处理程序中还会看到它。

第二个函数set_intr_gate前面已经见过了,这里为了方便再粘贴一下

void set_intr_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,14,0,addr);

}

它是写IDT表格中断向量的,关键是全局数组interrupt。

在同文件中找到

void (*interrupt[NR_IRQS])(void) = {

IRQLIST_16(0x0),

…….

}

是一个函数指针数组,IRQLIST_16(0x0)是一个宏定义

#define IRQ(x,y) IRQ##x##y##_interrupt

#define IRQLIST_16(x) IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)

一个IRQLIST_16(x)定义了16个函数,名称是IRQ0x0n_interrupt,比如3号中断的函数就是void IRQ0x03_interrupt(void)。这个函数的定义在linux的源文件I8259a.c中,就是通过一串的宏定义掩盖了,仔细看

#define BI(x,y) BUILD_IRQ(x##y)

#define BUILD_16_IRQS(x) BI(x,0) BI(x,1) BI(x,2) BI(x,3) BI(x,4) BI(x,5) BI(x,6) BI(x,7) BI(x,8) BI(x,9) BI(x,a) BI(x,b) BI(x,c) BI(x,d) BI(x,e) BI(x,f)

/*

* ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts:

* (these are usually mapped to vectors 0x20-0x2f)

*/

BUILD_16_IRQS(0x0),

宏BUILD_16_IRQS定义了16个BI,每一个BI都通过宏BUILD_IRQ(x##y)展开,

找出BUILD_IRQ(x##y)的宏定义:

#define BUILD_IRQ(nr) asmlinkage void IRQ_NAME(nr); __asm__( "\n"__ALIGN_STR"\n" SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" "pushl $"#nr"-256\n\t" "jmp common_interrupt");

再找出IRQ_NAME的宏定义

#define IRQ_NAME2(nr) nr##_interrupt(void)

#define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)

终于找出IRQ0x03_interrupt的定义,就是

__asm__( "\n"__ALIGN_STR"\n" SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" "pushl $"#nr"-256\n\t" "jmp common_interrupt");

翻译为c语言就是

{

IRQ#nr_interrupt:

Pushl nr-256

Jmp common_interrupt

}

这个一连串的宏定义就是为了简写源代码,否则要写16编相同的代码。如果有人看过《深入浅出MFC》,对这个应该比较熟悉,在MFC中的类定义里面也用到了很多的宏,同样让你看傻了眼。这些函数都要跳到common_interrupt下面的代码执行,那么这个又在哪里?在

一个头文件里面,这个文件是Hw_irq.h,定义如下:

#define BUILD_COMMON_IRQ() asmlinkage void call_do_IRQ(void); __asm__( "\n" __ALIGN_STR"\n" "common_interrupt:\n\t" SAVE_ALL "pushl $ret_from_intr\n\t" SYMBOL_NAME_STR(call_do_IRQ)":\n\t" "jmp "SYMBOL_NAME_STR(do_IRQ));

跳到了一个函数call_do_IRQ里面,这个函数用c复写一遍就是

void call_do_IRQ(void)

{

common_interrupt:

SAVE_ALL

pushl $ret_from_intr

call_do_IRQ:

jmp do_IRQ

}

SAVE_ALL是一个宏,

#define SAVE_ALL "cld\n\t" "pushl %es\n\t" "pushl %ds\n\t" "pushl %eax\n\t" "pushl %ebp\n\t" "pushl %edi\n\t" "pushl %esi\n\t" "pushl %edx\n\t" "pushl %ecx\n\t" "pushl %ebx\n\t" "movl $" STR(__KERNEL_DS) ",%edx\n\t" "movl %edx,%ds\n\t" "movl %edx,%es\n\t"

一看就明白,保存当前进程的寄存器,保存在当前进程的系统堆栈里。ret_from_intr估计可以猜出了,就是处理中断返回的函数,但是为什么要进堆栈呢?这个就和do_IRQ函数的调用方式有关了,do_IRQ函数是通过jmp跳过去执行的,也就是没有把函数后的指令地址入栈,这样函数返回后就找不到下一条指令了,pushl $ret_from_intr就解决了这个问题,函数do_IRQ返回后,指令ret就把ret_from_intr出栈,同时装入到ip中,就到ret_from_intr执行了,这是一个汇编程序设计的技巧,被用到了这里,也难怪会把人看得要抓狂了。

绕了老半天,明白了处理硬件中断服务的是函数do_IRQ。看一下这个函数,在irq.c里面

绕过一些错误处理和SMP的处理,简单地看一下。

/*

* do_IRQ handles all normal device IRQ's (the special

* SMP cross-CPU interrupts have their own specific

* handlers).

*/

asmlinkage unsigned int do_IRQ(struct pt_regs regs)

{

int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */

int cpu = smp_processor_id();

irq_desc_t *desc = irq_desc + irq;

struct irqaction * action;

unsigned int status;

kstat.irqs[cpu][irq]++;

spin_lock(&desc->lock);

desc->handler->ack(irq);

…….

action = NULL;

if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {

action = desc->action;

status &= ~IRQ_PENDING; /* we commit to handling */

status |= IRQ_INPROGRESS; /* we are handling it */

}

desc->status = status;

for (;;) {

spin_unlock(&desc->lock);

handle_IRQ_event(irq, &regs, action);

spin_lock(&desc->lock);

if (!(desc->status & IRQ_PENDING))

break;

desc->status &= ~IRQ_PENDING;

}

…….

if (softirq_active(cpu) & softirq_mask(cpu))

do_softirq();

return 1;

}

关键在函数ndle_IRQ_event(irq, &regs, action)中,

int handle_IRQ_event(unsigned int irq, struct pt_regs * regs, struct irqaction * action)

{

int status;

int cpu = smp_processor_id();

irq_enter(cpu, irq);

status = 1; /* Force the "do bottom halves" bit */

if (!(action->flags & SA_INTERRUPT))

__sti();

do {

status |= action->flags;

action->handler(irq, action->dev_id, regs);

action = action->next;

} while (action);

if (status & SA_SAMPLE_RANDOM)

add_interrupt_randomness(irq);

__cli();

irq_exit(cpu, irq);

return status;

}

在andle_IRQ_event函数的do-while循环中,终于看到了action = action->next,系统在沿着链表处理中断,和原先的设想是一致的。看到这里,再回到前面,明白了interrupt数组的作用。

现在再回顾一下linux处理中断的整个过程,硬件产生一个中断,cpu得到中断号,系统在IDT中找到和这个中断号对应的中断向量,从中断向量中得到IRQ0x0n_interupt的函数地址,

执行这个函数,跳到common_interrupt,到call_do_IRQ,到do_IRQ,最后到handle_IRQ_event

从整个中断的处理过程来说,以上也就是涉及到了中断的初始,处理的方面,还有用户是如何设置中断处理函数,这个在request_IRQ函数中处理。硬件中断就写这么多了,还有软中断和系统调用没有写,等看完后再说。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有