Linux的通用中止子体系的一个规划准则便是把底层的硬件完结尽或许地躲藏起来,使得驱动程序的开发人员不必重视底层的完结,要完结这个方针,内核的开发者们有必要把硬件相关的内容剥离出来,然后界说一些列规范的接口供上层拜访,上层的开发人员只需知道这些接口即可完结对中止的进一步处理和操控。对底层的封装首要包含两部分:
完结不同体系结构中止进口,这部分代码一般用asm完结;
中止操控器进行封装和完结;
本文的内容正是要评论硬件封装层的完结细节。我将以ARM体系进行介绍,大部分的代码坐落内核代码树的arch/arm/目录内。
1. CPU的中止进口
咱们知道,arm的反常和复位向量表有两种挑选,一种是低端向量,向量地址坐落0x00000000,另一种是高端向量,向量地址坐落0xffff0000,Linux挑选运用高端向量形式,也便是说,当反常产生时,CPU会把PC指针自动跳转到始于0xffff0000开端的某一个地址上:
ARM的反常向量表地址反常品种FFFF0000复位FFFF0004未界说指令FFFF0008软中止(swi)FFFF000CPrefetch abortFFFF0010Data abortFFFF0014保存FFFF0018IRQFFFF001CFIQ
中止向量表在arch/arm/kernel/entry_armv.S中界说,为了便利评论,下面只列出部分要害的代码:
[plain] view plain copy
.globl __stubs_start
__stubs_start:
vector_stub irq, IRQ_MODE, 4
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
vector_stub dabt, ABT_MODE, 8
.long __dabt_usr @ 0 (USR_26 / USR_32)
.long __dabt_invalid @ 1 (FIQ_26 / FIQ_32)
.long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)
.long __dabt_svc @ 3 (SVC_26 / SVC_32)
vector_fiq:
disable_fiq
subs pc, lr, #4
……
.globl __stubs_end
__stubs_end:
.equ stubs_offset, __vectors_start + 0x200 – __stubs_start
.globl __vectors_start
__vectors_start:
ARM( swi SYS_ERROR0 )
THUMB( svc #0 )
THUMB( nop )
W(b) vector_und + stubs_offset
W(ldr) pc, .LCvswi + stubs_offset
W(b) vector_pabt + stubs_offset
W(b) vector_dabt + stubs_offset
W(b) vector_addrexcptn + stubs_offset
W(b) vector_irq + stubs_offset
W(b) vector_fiq + stubs_offset
.globl __vectors_end
__vectors_end:
代码被分为两部分:
榜首部分是真实的向量跳转表,坐落__vectors_start和__vectors_end之间;
第二部分是处理跳转的部分,坐落__stubs_start和__stubs_end之间;
[plain] view plain copy
vector_stub irq, IRQ_MODE, 4
以上这一句把宏翻开后实践上便是界说了vector_irq,依据进入中止前的cpu形式,别离跳转到__irq_usr或__irq_svc。
[plain] view plain copy
vector_stub dabt, ABT_MODE, 8
以上这一句把宏翻开后实践上便是界说了vector_dabt,依据进入中止前的cpu形式,别离跳转到__dabt_usr或__dabt_svc。
体系启动阶段,坐落arch/arm/kernel/traps.c中的early_trap_init()被调用:
[cpp] view plain copy
void __init early_trap_init(void)
{
……
/*
* Copy the vectors, stubs and kuser helpers (in entry-armv.S)
* into the vector page, mapped at 0xffff0000, and ensure these
* are visible to the instruction stream.
*/
memcpy((void *)vectors, __vectors_start, __vectors_end – __vectors_start);
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end – __stubs_start);
……
}
以上两个memcpy会把__vectors_start开端的代码复制到0xffff0000处,把__stubs_start开端的代码复制到0xFFFF0000+0x200处,这样,反常中止到来时,CPU就能够正确地跳转到相应中止向量进口并履行他们。
图1.1 Linux中ARM体系的中止向量复制进程
关于体系的外部设备来说,一般都是运用IRQ中止,所以咱们只重视__irq_usr和__irq_svc,两者的差异是进入和退出中止时是否进行用户栈和内核栈之间的切换,还有进程调度和抢占的处理等,这些细节不在这儿评论。两个函数终究都会进入irq_handler这个宏:
[plain] view plain copy
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
adr lr, BSYM(9997f)
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm
假如挑选了MULTI_IRQ_HANDLER装备项,则意味着答应渠道的代码能够动态设置irq处理程序,渠道代码能够修正全局变量:handle_arch_irq,然后能够修正irq的处理程序。这儿咱们评论默许的完结:arch_irq_handler_default,它坐落arch/arm/include/asm/entry_macro_mulTI.S中:
[plain] view plain copy
.macro arch_irq_handler_default
get_irqnr_preamble r6, lr
1: get_irqnr_and_base r0, r2, r6, lr
movne r1, sp
@
@ rouTIne called with r0 = irq number, r1 = struct pt_regs *
@
adrne lr, BSYM(1b)
bne asm_do_IRQ
……
get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码界说,意图便是从中止操控器中取得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开端,中止程序进入C代码中,传入的参数是IRQ编号和寄存器结构指针,这个函数在arch/arm/kernel/irq.c中完结:
[cpp] view plain copy
/*
* asm_do_IRQ is the interface to be used from assembly code.
*/
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
handle_IRQ(irq, regs);
}
到这儿,中止程序完结了从asm代码到C代码的传递,而且取得了引起中止的IRQ编号。
2. 初始化
与通用中止子体系相关的初始化由start_kernel()函数建议,调用流程如下图所视:
图2.1 通用中止子体系的初始化
首要,在setup_arch函数中,early_trap_init被调用,其间完结了第1节所说的中止向量的复制和重定位作业。
然后,start_kernel宣布early_irq_init调用,early_irq_init归于与硬件和渠道无关的通用逻辑层,它完结irq_desc结构的内存恳求,为它们其间某些字段填充默许值,完结后调用体系相关的arch_early_irq_init函数完结进一步的初始化作业,不过ARM体系没有完结arch_early_irq_init。
接着,start_kernel宣布init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc一般在板子的特定代码中,运用MACHINE_START和MACHINE_END宏进行界说。
machine_desc->init_irq()完结对中止操控器的初始化,为每个irq_desc结构装置适宜的流控handler,为每个irq_desc结构装置irq_chip指针,使他指向正确的中止操控器所对应的irq_chip结构的实例,一起,假如该渠道中的中止线有多路复用(多个中止共用一个irq中止线)的状况,还应该初始化irq_desc中相应的字段和标志,以便完结中止操控器的级联。
3. 中止操控器的软件笼统:struct irq_chip
正如上一篇文章Linux中止(interrupt)子体系之一:中止体系基本原理所述,一切的硬件中止在抵达CPU之前,都要先经过中止操控器进行聚集,符合要求的中止恳求才会告诉cpu进行处理,中止操控器首要完结以下这些功用:
对各个irq的优先级进行操控;
向CPU宣布中止恳求后,供给某种机制让CPU取得实践的中止源(irq编号);
操控各个irq的电气触发条件,例如边际触发或许是电平触发;
使能(enable)或许屏蔽(mask)某一个irq;
供给嵌套中止恳求的才能;
供给铲除中止恳求的机制(ack);
有些操控器还需要CPU在处理完irq后对操控器宣布eoi指令(end of interrupt);
在smp体系中,操控各个irq与cpu之间的亲缘联系(affinity);
通用中止子体系把中止操控器笼统为一个数据结构:struct irq_chip,其间界说了一系列的操作函数,大部分多对应于上面所列的某个功用:
[cpp] view plain copy
struct irq_chip {
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
unsigned long flags;
/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
};
各个字段解说如下:
name 中止操控器的姓名,会出现在 /proc/interrupts中。
irq_startup 榜首次敞开一个irq时运用。
irq_shutdown 与irq_starup相对应。
irq_enable 使能该irq,一般是直接调用irq_unmask()。
irq_disable 制止该irq,一般是直接调用irq_mask,严厉含义上,他俩其实代表不同的含义,disable标明中止操控器底子就不呼应该irq,而mask时,中止操控器或许呼应该irq,仅仅不告诉CPU,这时,该irq处于pending状况。相似的差异也适用于enable和unmask。
irq_ack 用于CPU对该irq的回应,一般标明cpu期望要铲除该irq的pending状况,预备承受下一个irq恳求。
irq_mask 屏蔽该irq。
irq_unmask 撤销屏蔽该irq。
irq_mask_ack 相当于irq_mask + irq_ack。
irq_eoi 有些中止操控器需要在cpu处理完该irq后宣布eoi信号,该回调便是用于这个意图。
irq_set_affinity 用于设置该irq和cpu之间的亲缘联系,便是告诉中止操控器,该irq产生时,那些cpu有权呼应该irq。当然,中止操控器会在软件的合作下,终究只会让一个cpu处理本次恳求。
irq_set_type 设置irq的电气触发条件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。
irq_set_wake 告诉电源办理子体系,该irq是否能够用作体系的唤醒源。
以上大部分的函数接口的参数都是irq_data结构指针,irq_data结构的由来在上一篇文章现已说过,这儿仅贴出它的界说,各字段的含义请参阅注释:
[cpp] view plain copy
/**
* struct irq_data – per irq and irq chip data passed down to chip functions
* @irq: interrupt number
* @hwirq: hardware interrupt number, local to the interrupt domain
* @node: node index useful for balancing
* @state_use_accessors: status information for irq chip functions.
* Use accessor functions to deal with it
* @chip: low level interrupt hardware access
* @domain: Interrupt translation domain; responsible for mapping
* between hwirq number and linux irq number.
* @handler_data: per-IRQ data for the irq_chip methods
* @chip_data: platform-specific per-chip private data for the chip
* methods, to allow shared chip implementations
* @msi_desc: MSI descriptor
* @affinity: IRQ affinity on SMP
*
* The fields here need to overlay the ones in irq_desc until we
* cleaned up the direct references and switched everything over to
* irq_data.
*/
struct irq_data {
unsigned int irq;
unsigned long hwirq;
unsigned int node;
unsigned int state_use_accessors;
struct irq_chip *chip;
struct irq_domain *domain;
void *handler_data;
void *chip_data;
struct msi_desc *msi_desc;
#ifdef CONFIG_SMP
cpumask_var_t affinity;
#endif
};
依据设备运用的中止操控器的类型,体系架构的底层的开发只需完结上述接口中的各个回调函数,然后把它们填充到irq_chip结构的实例中,终究把该irq_chip实例注册到irq_desc.irq_data.chip字段中,这样各个irq和中止操控器就进行了相关,只需知道irq编号,即可得到对应到irq_desc结构,从而能够经过chip指针拜访中止操控器。
4. 进入流控处理层
进入C代码的榜首个函数是asm_do_IRQ,在ARM体系中,这个函数仅仅简略地调用handle_IRQ:
[cpp] view plain copy
/*
* asm_do_IRQ is the interface to be used from assembly code.
*/
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
handle_IRQ(irq, regs);
}
handle_IRQ自身也不是很杂乱:
[cpp] view plain copy
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
irq_enter();
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(irq >= nr_irqs)) {
if (printk_ratelimit())
printk(KERN_WARNING “Bad IRQ%u\n”, irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
/* AT91 specific workaround */
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
}
irq_enter首要是更新一些体系的计算信息,一起在__irq_enter宏中制止了进程的抢占:
[cpp] view plain copy
#define __irq_enter() \
do { \
account_system_vtime(current); \
add_preempt_count(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
CPU一旦呼应IRQ中止后,ARM会自动把CPSR中的I方位位,标明制止新的IRQ恳求,直到中止操控转到相应的流控层后才经过local_irq_enable()翻开。你或许会古怪,已然此刻的irq中止都是都是被制止的,为何还要制止抢占?这是由于要考虑中止嵌套的问题,一旦流控层或驱动程序自动经过local_irq_enable翻开了IRQ,而此刻该中止还没处理完结,新的irq恳求抵达,这时代码会再次进入irq_enter,在本次嵌套中止回来时,内核不期望进行抢占调度,而是要比及最外层的中止处理完结后才做出调度动作,所以才有了制止抢占这一处理。
下一步,generic_handle_irq被调用,generic_handle_irq是通用逻辑层供给的API,经过该API,中止的操控被传递到了与体系结构无关的中止流控层:
[cpp] view plain copy
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
generic_handle_irq_desc(irq, desc);
return 0;
}
终究会进入该irq注册的流控处理回调中:
[cpp] view plain copy
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);
}
5. 中止操控器的级联
在实践的设备中,常常存在多个中止操控器,有时多个中止操控器还会进行所谓的级联。为了便利评论,咱们把直接和CPU相连的中止操控器叫做根操控器,别的一些和跟操控器相连的叫子操控器。依据子操控器的方位,咱们把它们分为两品种型:
机器等级的级联 子操控器坐落SOC内部,或许子操控器在SOC的外部,可是是某个板子系列的规范装备,如图5.1的左面所示;
设备等级的级联 子操控器坐落某个外部设备中,用于聚集该设备宣布的多个中止,如图5.1的右边所示;
图5.1 中止操控器的级联类型
关于机器等级的级联,级联的初始化代码天经地义地坐落板子的初始化代码中(arch/xxx/mach-xxx),由于只需是运用这个板子或SOC的设备,必定要运用这个子操控器。而关于设备等级的级联,由于该设备并不一定是体系的标配设备,所以中止操控器的级联操作应该在该设备的驱动程序中完结。机器设备的级联,由于得益于事前现已知道子操控器的硬件衔接信息,内核能够便利地为子操控器保存相应的irq_desc结构和irq编号,处理起来相对简略。设备等级的级联则不相同,驱动程序有必要动态地决议组合设备中各个子设备的irq编号和irq_desc结构。本章我只评论机器等级的级联,设备等级的相关能够运用相同的原理,也能够完结为同享中止,我会在本系列接下来的文章中评论。
要完结中止操控器的级联,要运用以下几个的要害数据结构字段和通用中止逻辑层的API:
irq_desc.handle_irq irq的流控处理回调函数,子操控器在把多个irq聚集起来后,输出端衔接到根操控器的其间一个irq中止线输入脚,这意味着,每个子操控器的中止产生时,CPU一开端只会得到根操控器的irq编号,然后进入该irq编号对应的irq_desc.handle_irq回调,该回调咱们不能运用流控层界说好的几个流控函数,而是要自己完结一个函数,该函数担任从子操控器中取得irq的中止源,并计算出对应新的irq编号,然后调用新irq所对应的irq_desc.handle_irq回调,这个回调运用流控层的规范完结。
irq_set_chained_handler() 该API用于设置根操控器与子操控器相连的irq所对应的irq_desc.handle_irq回调函数,而且设置IRQ_NOPROBE和IRQ_NOTHREAD以及IRQ_NOREQUEST标志,这几个标志确保驱动程序不会过错地恳求该irq,由于该irq现已被作为级联irq运用。
irq_set_chip_and_handler() 该API一起设置irq_desc中的handle_irq回谐和irq_chip指针。
以下比如代码坐落:/arch/arm/plat-s5p/irq-eint.c:
[cpp] view plain copy
int __init s5p_init_irq_eint(void)
{
int irq;
for (irq = IRQ_EINT(0); irq <= IRQ_EINT(15); irq++)
irq_set_chip(irq, &s5p_irq_vic_eint);
for (irq = IRQ_EINT(16); irq <= IRQ_EINT(31); irq++) {
irq_set_chip_and_handler(irq, &s5p_irq_eint, handle_level_irq);
set_irq_flags(irq, IRQF_VALID);
}
irq_set_chained_handler(IRQ_EINT16_31, s5p_irq_demux_eint16_31);
return 0;
}
该SOC芯片的外部中止:IRQ_EINT(0)到IRQ_EINT(15),每个引脚对应一个根操控器的irq中止线,它们是正常的irq,无需级联。IRQ_EINT(16)到IRQ_EINT(31)经过子操控器聚集后,一致衔接到根操控器编号为IRQ_EINT16_31这个中止线上。能够看到,子操控器对应的irq_chip是s5p_irq_eint,子操控器的irq默许设置为电平中止的流控处理函数handle_level_irq,它们经过API:irq_set_chained_handler进行设置。假如根操控器有128个中止线,IRQ_EINT0–IRQ_EINT15一般占有128内的某段接连规模,这取决于实践的物理衔接。IRQ_EINT16_31由于也归于跟操控器,所以它的值也会坐落128以内,可是IRQ_EINT16–IRQ_EINT31一般会在128以外的某段规模,这时,代表irq数量的常量NR_IRQS,有必要考虑这种状况,界说出超越128的某个满足的数值。级联的完结首要依托编号为IRQ_EINT16_31的流控处理程序:s5p_irq_demux_eint16_31,它的终究完结相似于以下代码:
[cpp] view plain copy
static inline void s5p_irq_demux_eint(unsigned int start)
{
u32 status = __raw_readl(S5P_EINT_PEND(EINT_REG_NR(start)));
u32 mask = __raw_readl(S5P_EINT_MASK(EINT_REG_NR(start)));
unsigned int irq;
status &= ~mask;
status &= 0xff;
while (status) {
irq = fls(status) – 1;
generic_handle_irq(irq + start);
status &= ~(1 << irq);
}
}
在取得新的irq编号后,它的最要害的一句是调用了通用中止逻辑层的API:generic_handle_irq,这时它才真实地把中止操控权传递到中止流控层中来。