中断和异常
中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。
中断通常分为同步中断和异步中断:
2 同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。
◎ 异步中断是由其他硬件设备依照CPU时钟信号随机产生的。
在Intel微处理器手册中:
◎ 把同步中断称为异常(exception)
◎ 把异步中断称为中断(interrupt)
这两类中断的共同特点是什么?如果CPU当前不处于核心态,则发起从用户态到核心态的切换。接下来,在内核中执行一个专门的例程,称为中断服务例程(interrupt service routine)。或中断处理程序(interrupthandler)
另一方面,异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的。第一种情况下,内核通过发送一个每个Unix/Linux程序员都熟悉的信号来处理异常。第二种情况下,内核执行恢复异常需要的所有步骤,例如缺页异常等。
中断信号提供了一种特殊的方式,使处理器转而去运行正常控制流之外的代码。当一个中断信号达到时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。为了这做到这一点,就要在内核态堆栈保存程序计数器的当前值(即EIP和CS寄存器的内容),并把与中断类型相关的一个地址放进程序计数器。
这可能会让我们想起系统调度的进程切换,发生在内核用一个进程替换另一个进程时。但是中断处理与进程切换有一个明显的差异:由中断或异常处理程序执行的代码不是一个进程。更准确的说,它是一个内核控制路径,代表中断发生时正在运行的进程执行。作为一个内核控制路径,中断处理程序比一个进程要“轻”(中断的上下文很少,建立或终止中断处理需要的时间也很少)
中断处理是由内核执行的最敏感的任务之一,因为它必须满足下列约束:
◎ 当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟。因此,内核响应中断后需要进行的操作分为两部分:关键而紧急的部分,内核立即执行;其余推迟的部分,内核随后执行。
◎ 因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个不同类型的中断又发生了。内核应该尽可能地允许这种情况发生,因为这能维持更多的I/O设备得到处理的机会。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核也应能切换到另外的进程。
◎ 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内以开中断的方式运行。
中断这个名词使用得并不是很谨慎,为什么?由于中断是用来表示由CPU和外部硬件发出的信号所产生的。但是中断不能由处理器外部的外设直接产生,而必须借助于一个称为可编程中断控制器(programmable interrupt controller)的标准组件来请求,该组件存在于每个系统中。
外部设备,会有电路连接到用于向中断控制器发送中断请求的组件。控制器在执行了各种电工任务之后,将中断请求转发到CPU的中断输入中。因为外部设备不能直接发出中断,而必须通过中断控制器的标准组件来请求中断,所以这种请求更正确的叫法是IRQ,或中断请求(Interrupt Request)。
每个能够发出中断请求的硬件设备控制器都有这么一条名为IRQ的输出线。所有现有的IRQ线都会与这个中断控制器(PIC)的硬件电路的输入引脚相连。下面来看看这种中断控制器执行下列动作:
1) 监视IRQ线,检查产生的信号 。如果有一条或两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线。
2) 如果一个引发信号出现在IRQ线上:
a) 把接收到的引发信号转换成对应的向量(索引)。
b) 把这个向量存放在中断控器的一个I/O端口,从而允许CPU通过数据总线读取此向量。
c) 把引发信号发送到处理器的INTR引脚,即产生一个中断。
d) 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它;当这种情况发生时,清INTR线。
3) 返回到第一步。
IRQ线是从0开始顺序编号的,因此,第一条IRQ线通常表示成IRQ0。与IRQn关联的Intel缺省向量是n+32。如前所述,通过向中断控制器端口发布合适的指令,就可以修改IRQ和向量之间的映射。
可以有选择地禁止每条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们,禁止的中断是丢失不了的,它们一旦激活,PIC就又把它们发送到CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ。
在CPU得知发生中断后,它将进一步的处理委托给一个软件例程,该例程可能会修复故障、提供专门的处理或将外部事件通知用户进程。由于每个中断和异常都有唯一的编号,内核使用一个数组项是指向处理程序函数的指针。相关的中断号根据数组项在数组中位置判断。
如下图所示:
如下图,中断处理划分为3部分。首先,必须建立一个适当的环境,使得处理程序函数能够在其中执行,接下来调用处理程序自身,最后将系统复原到中断之前的状态。调用中断处理程序前后的两部分,分别称为进入路径和退出路径。
进入和退出任务还负责确保处理器从用户态切换到核心态。进入路径的一个关键任务是,从用户态栈切换到核心态栈。但是这一点还不够。因为内核还要使用CPU资源执行其代码,进入路径必须保存用户应用程序当前的寄存器,以便 在中断活动结束后恢复。这与进程调度间用上下文切换的机制是相同的。在进入核心态时,只保存整个寄存器集合的一部分。内核并不使用全部寄存器。(如内核代码中不使用浮点操作,因而不保存浮点寄存器)。平台相关的数据结构pt_regs列出了核心态可能修改的所有寄存器,它的定义考虑到了不同的CPU之间的差别。
在退出路径中,内核会检查下列事项。
◎ 调度器是否应该选择一个新进程代替旧的进程。
◎ 是否有信号必须投递到原进程
从中断返回之后,只有确认了这两个问题,内核才能完成其常规任务,即还原寄存器集合、切换到用户态栈、切换到适用于用户应用程序的适当的处理器状态,或切换到一个不同的保护环。
术语中断处理程序的使用可能引起岐义。因为它是用于指代CPU对ISR(中断服务程序)的调用,包括了进入/退出路径和ISR本身。当然,如果只指代在进入路径和退出路径之间进行由C语言实现的例程,将更为准确。
中断技术上的实现有两方面:
1) 汇编语言代码:与处理器高度相关,用于处理特定平台上相关的底层细节;
2) 抽象接口:是设备驱动程序及其他内核代码安装和管理IRQ处理程序所需的。
描述汇编语言部分的功能会涉及无数细节,可以参考处理器体系方面的手册。
为响应外部设备的IRQ,内核必须为每个潜在的IRQ提供一个函数。该函数必须能够动态注册和注销。静态表组织方式是不够的,因为可能为设备编写模块,而且设备可能与系统的其他部分通过中断进行交互。
IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号。因为数组位置和中断号是相同的,很容易定位与特定的IRQ相关的数组项:IRQ0在位置0,IRQ15在位置15,等等,IRQ最终映射到哪个处理器中断,在这里不相关的。
尽管各个数组项使用的是一个体系结构无关的数据类型,但IRQ的最大可能数目是通过一个平台相关的常数NR_IRQS指定的,大多数体系结构一,该常数定义在处理器相关的头文件irq.h中。不同处理器间及同一处理器家庭内,该常数的值变化都很大,主要取决于辅助CPU管理IRQ的辅助芯片。
与IRQ的最大数目相比,我们对各数组项的数据类型更感兴趣。在了解细节之前,需要概述内核的IRQ处理子系统。
之前的一些版本包含了大量平台代码来处理IRQ,在许多地方是相同的。因而,在内核版本2.6开发期间,引入了一个新的通用的IRQ子系统。它能够以统一的方式处理不同的中断控制器和不同类型的中断。基本上它由3个抽象层组成。如下图:
◎ 高层ISR(high-level interrupt service routines):针对设备驱动程序端的中断,执行由此引起的所有必要的工作。
◎ 中断电流处理(interrupt flow handling):处理不同的中断电流类型之间的各种差别,如边沿触发和电平触发。
边沿触发:意味着硬件通过感知线路上的电位差来检测中断。
电平触发:根据特定的电势值检测中断,与电势是否改变无关。
◎ 芯片级硬件封装(chip-level hardware encapsulation):需要与电子学层次上产生中断的底层硬件直接通信。该抽象可以视为中断控制器的某种“设备驱动程序”。
用于表示irq的结构如下:(摘自Linux kernel 3.5)
/**
*struct irq_desc - interrupt descriptor
*@irq_data: per irq and chip datapassed down to chip functions
*@timer_rand_state: pointer to timer randstate struct
*@kstat_irqs: irq stats per cpu
*@handle_irq: highlevel irq-eventshandler
*@preflow_handler: handler called beforethe flow handler (currently used by sparc)
*@action: the irq action chain
*@status: status information
*@core_internal_state__do_not_mess_with_it: core internal status information
*@depth: disable-depth, for nestedirq_disable() calls
*@wake_depth: enable depth, for multipleirq_set_irq_wake() callers
*@irq_count: stats field to detectstalled irqs
*@last_unhandled: aging timer forunhandled count
*@irqs_unhandled: stats field forspurious unhandled interrupts
*@lock: locking for SMP
*@affinity_hint: hint to user space forpreferred irq affinity
*@affinity_notify: context fornotification of affinity changes
*@pending_mask: pending rebalancedinterrupts
*@threads_oneshot: bitfield to handleshared oneshot threads
*@threads_active: number of irqactionthreads currently running
*@wait_for_threads: wait queue for sync_irqto wait for threaded handlers
*@dir: /proc/irq/ procfs entry
*@name: flow handler name for/proc/interrupts output
*/
struct irq_desc
{
struct irq_data irq_data;
struct timer_rand_state *timer_rand_state;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list*/
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irqdisables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detectingbroken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
在上面的代码中,注释部分基本上简单的介绍了下各个字段的意义,下面会针对一些特殊的字段进行说明。
在介绍下面的主要字段之前,先说下这个结构中的irq_data字段,该字段保存了一些在中断处理过程各个中断处理阶段都会用到的数据,该结构如下所示:(摘自Linux kernel 3.5)
/**
*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 forbalancing
*@state_use_accessors: status information for irq chip functions.
* Use accessor functions to deal with it
*@chip: low level interrupt hardwareaccess
*@domain: Interrupt translationdomain; responsible for mapping
* between hwirq number and linux irq number.
*@handler_data: per-IRQ data for theirq_chip methods
*@chip_data: platform-specificper-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都可以由该结构完全描述。上面介绍的3个抽象层在该结构中表示如下:
◎ 电流层ISR由handle_irq提供。irq_data结构中的handler_data可以指向任意数据,该数据可以是特定于IRQ或处理程序。每当发生中断时,特定于体系结构的代码都会调用handle_irq。该函数负责使用chip中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作。用于不同中断类型的默认函数由内核提供。
◎ action提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。有一个专门的数据结构用于表示这些操作。
◎ 电流处理和芯片相关操作被封装在ird_data结构中的chip中。为此引入了一个专门的数据结构,irq_chip。该结构相关的东西之后介绍。
◎ name指定了电流层处理程序的名称,将显示在/proc/interrupts中。对边沿触发是“edge”,对电平触发中断,通常是“level”。
一些其它的字段意义如下:
◎ depth有两个任务。它可用于确定IRQ电路是启用的还是禁用的,正值表示禁用的,而0表示启用的。为什么用正值表示禁用的IRQ呢?因为这使得内核能够区分启用和禁用的IRQ电路,以及重复禁用同一中断的情形。这个值相当于一个计数器,内核其余部分的代码每次禁用某个中断,则将对应的计数器加1;每次 断被再次启用,则将计数器减1 。在depth归0时,硬件才能再次使用对应的IRQ。这各方法能够支持对嵌套禁用中断的正确处理。
◎ IRQ不仅可以在处理程序安装期间改变其状态,而且可以在运行时改变:status描述了IRQ的当前状态。
根据status当前的值,内核很容易获知某个IRQ的状态,而无需了解底层实现的硬件相关特性。当然,只设置对应的标志位是不会产生预期效果的。如:通过设置IRQ_DISABLED标志来禁用中断是不可能的,还必须将新状态通知底层硬件。因而,该标准只能通过特定于控制器的函数设置,这些函数同时还负责将设置信息同步到底层硬件。
刚刚提到过,电流处理和芯片相关操作被封装在ird_data结构中的chip中。为此还引入了一个专门的数据结构,下面来详细说下这个结构。这个结构是一个操作的集合,它提供的函数用于改变IRQ的状态,这也是它们还负责设置irq_desc结构中的status字段的原因。该结构如下:(摘自Linux kernel 3.5)
/**
*struct irq_chip - hardware interrupt chip descriptor
*
*@name: name for /proc/interrupts
*@irq_startup: start up the interrupt(defaults to ->enable if NULL)
*@irq_shutdown: shut down the interrupt(defaults to ->disable if NULL)
*@irq_enable: enable the interrupt(defaults to chip->unmask if NULL)
*@irq_disable: disable the interrupt
*@irq_ack: start of a new interrupt
*@irq_mask: mask an interrupt source
*@irq_mask_ack: ack and mask aninterrupt source
*@irq_unmask: unmask an interruptsource
*@irq_eoi: end of interrupt
*@irq_set_affinity: set the CPU affinityon SMP machines
*@irq_retrigger: resend an IRQ to the CPU
*@irq_set_type: set the flow type(IRQ_TYPE_LEVEL/etc.) of an IRQ
*@irq_set_wake: enable/disablepower-management wake-on of an IRQ
*@irq_bus_lock: function to lock accessto slow bus (i2c) chips
*@irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips
*@irq_cpu_online: configure an interrupt source for a secondary CPU
*@irq_cpu_offline: un-configure aninterrupt source for a secondary CPU
*@irq_suspend: function called fromcore code on suspend once per chip
*@irq_resume: function called fromcore code on resume once per chip
*@irq_pm_shutdown: function called fromcore code on shutdown once per chip
*@irq_print_chip: optional to print special chip info in show_interrupts
*@flags: chip specific flags
*
*@release: release function solelyused by UML
*/
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, mightdisappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
};
该结构需要考虑内核中出现的各个IRQ实现的所有特性。因而,一个该结构的特定实例,通常只定义所有可能方法的一个子集。
name包含一个短的字符串,用于标识硬件控制器。
各个函数指针的语义如下:
irq_startup指向一个函数,用于第一次初始化一个IRQ。大多数情况下,初始化工作仅限于启用该IRQ。因而, irq_startup函数实际上就工作转给enable。
irq_enable激活一个IRQ。换句话说,它执行IRQ由禁用状态到启用状态的转换。为此。必须向I/O内存或I/O端口中硬件相关的位置写入特定于硬件的数值。
irq_disable与enable相对应,用于禁用IRQ。而shutdown完全关闭一个中断源。如果不支持该特性,那么这个函数实际上是disable的别名。
irq_ack与中断控制器的硬件密切相关。在某些模型中,IRQ请求的到达必须显式确认,后续的请求才能进行处理。如果芯片组没有这样的要求,该指针可以指向一个空函数,或NULL指针。irq_mask_ack确认一个中断,并在接下来屏蔽该中断。
在现代的中断控制器不需要内核进行太多的电流控制,控制器几乎可以管理所有事务。在处理中断时需要一个到硬件的回调,由irq_eoi提供。eoi表示endof interrupt,即中断结束。
在多处理器系统中,可使用irq_set_affinity指定用哪个CPU来处理特定的IRQ。这使得可以将IRQ分配给某些CPU。该方法在单处理器系统上没用,可以设置为NULL。
set_type设置IRQ的电流类型。该方法主要使用在ARM、PowerPC和SuperH机器上,其他系统不需要该方法,可以将set_type设置为NULL。
还记得在在刚才在“中断处理子系统的各部分交互方式”的图中看到的,刚才介绍了下IRQ控制器的抽象对象,当内核获取到相应的中断请求后,是如何执行对应的中断处理函数。这里又要引出一个新的结构,irqaction结构。每个处理程序函数都对应该结构的一个实例:该结构如下:(摘自LinuxKernel 3.5)
span style="font-size:16px;">typedef irqreturn_t (*irq_handler_t)(int, void *);
/**
*struct irqaction - per interrupt action descriptor
*@handler: interrupt handler function
*@flags: flags (see IRQF_* above)
*@name: name of the device
*@dev_id: cookie to identify the device
*@percpu_dev_id: cookie to identify thedevice
*@next: pointer to the next irqactionfor shared interrupts
*@irq: interrupt number
*@dir: pointer to the proc/irq/NN/nameentry
*@thread_fn: interrupt handler functionfor threaded interrupts
*@thread: thread pointer for threaded interrupts
*@thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @threadactivity
*/
struct irqaction
{
irq_handler_t handler;
unsigned long flags;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
int irq;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
该结构中最重要的成员是处理程序函数本身,即handler成员,这是一个函数指针,位于结构的起始处。在设备请求一个系统中断,而中断控制器通过引用发中断将该请求转发到处理器的时候,内核将调用该处理程序函数。在考虑如何注册处理程序函数时,我们再仔细考察其参数的语义。但请注意,处理程序的类型为irq_handler_t,与电流处理程序的类型irq_flow_handler_t显然是不同的。
name和dev_id唯一地标识一个中断处理程序。name是一个短字符串,用于标识设备,而dev_id是一个指针,指向在所有内核数据结构中唯一标识了该设备的数据结构实例。
如果几个设备共享一个IRQ,那么IRQ编号自身不能标识该设备,此时,在删除处理程序函数时,将需要上述信息。
flag是一个标志变量,通过位图描述了IRQ的一些特性,位图中各个标志位照例可通过预定义的常数。
◎ 对共享的IRQ设置IRQF_SHARED,表示有多于一个设备使用该IRQ电路。
◎ 如果IRQ对内核熵池有贡献,将设置IRQF_SAMPLE_RANDOM。
◎ IRQF_DISABLED表示IRQ的处理程序必须在禁用中断的情况下执行。
◎ IRQF_TIMER表示时钟中断
next用于实现共享的IRQ处理程序。几个irqaction实例聚集到一个链表中。链表的所有元素都必须处理同一个IRQ编号。在一个链表中的中断都属于可共享的IRQ,这在后面会有一些介绍。
下图给出了所描述各数据结构的一个概览,说明其彼此交互的方式。因为通常在一个系统上只有一种类型的中断控制器会占据支配地位,所以在一般情况下所有的irq_desc的handler成员都指向kirq_chip的同一个实例。
在这里将介绍下电流处理是如何实现的。在内核版本2.6重写中断逻辑之前,此领域中的现状令人感到相当痛苦,在电流处理中会涉及大量体系结构相关的代码。幸好,情况现在有了很大的改善,有一个通用框架几乎可用于所有硬件,仅有少量例外。
首先,需要提到内核提供的一些标准函数,用于注册irq_chip和设置电流处理程序:
int irq_set_chip(unsigned int irq, struct irq_chip *chip);
该函数将一个IRQ芯片以irq_chip实例的形式关联到某个特定的中断上。除了从irq_desc选取适当的成员并设置chip之外,如果没有提供特定于芯片的实现,该函数还将设置默认的处理程序。如果chip指针为NULL,将使用通用的“无控制器”irq_chip实例no_irq_chip,该实现只提供了空操作。
void irq_set_handler(unsigned int irq, irq_flow_handler_t handle);
void irq_set_chained_handler(unsigned int irq, irq_flow_handler_thandle);
这两个函数为某个给定的IRQ编号设置电流处理程序。第二种变体表示,处理程序必须处理共享的中断。这会置位irq_desc[irq]->status中的标志位IRQ_NOREQUEST和IRQ_NOPROBE:设置第一标志,是因为共享中断是不能独占使用的,设置第二个标志,是因为在有多个设备的IRQ电路上,使用中断探测显然是个坏主意。
两个函数在内部都使用了__irq_set_handler,该函数执行一些合理性的检查,然后设置irq_desc[irq]->handle_irq。
void irq_set_chip_and_handler(unsigned int irq,struct irq_chip *chip,irq_flow_handler_thandle);
void irq_set_chip_and_handler_name(unsigned int irq,struct irq_chip *chip,irq_flow_handler_thandle,const char *name);
该两个函数是一种快捷方式,它相当于连续调用上述两个函数。_name的函数变体工作方式相同,但可以为电流处理程序指定一个名称,保存在irq_desc[irq]->name中。
在讨论电流处理程序实现方式之前,需要介绍处理程序所用的类型。irq_flow_handler_t指定了IRQ电流处理程序函数的原型:
typedef void (*irq_flow_handler_t)(unsigned int irq,struct irq_desc *desc);
在前面说过,不同的硬件需要不同的电流处理,例如,边沿触发和电平触发就需要不同的处理。内核对各种类型提供了几个默认的电流处理程序。它们有一个共同点:每个电流处理程序在其工作结束后,都要负责调用高层ISR。handle_IRQ_event负责激活高层的处理程序,这将在后面讨论。现在主要讲如何处理电流处理。
◎ 边沿触发
现在的硬件大部分采用的是边沿触发中断,因此首先讲述这一类型。默认处理程序实现在handle_edge_irq中。其代码流程图如下图:
在处理边沿触发的IRQ时无须屏蔽,这与电平触发IRQ是相反的。这对SMP系统有一个重要的含义:当在一个CPU上处理一个IRQ时,另一个同样编号的IRQ可以出现在另一个CPU上,称为第二个CPU。这意味着,当电流处理处理程序在由第一个IRQ触发的CPU上运行时,还可能被再次调用。但为什么应该有两个CPU同时运行同一个IRQ处理程序呢?内核想要避免这种情况:处理程序只应在一个CPU上运行。handle_edge_irq的开始部分必须处理这种情况。如果在irq_desc实例中的irq_data字段内的state_use_accessors字段被设置了IRQD_IRQ_INPROGRESS标志。则IRQ在另一个CPU上已经处于处理过程中。通过设置IRQD_IRQ_INPROGRESS标志,内核能够记录还有另一个IRQ需要在稍后处理。在屏蔽该IRQ并通过mask_ack_irq向控制器发送一个确认后,处理过程可以放。因而第二个CPU可以恢复正常的工作,而第一个CPU将在稍后处理该IRQ。
在IRQ被禁用,或没有可用的ISR处理程序,都会放弃处理。
现在,开始IRQ处理本身所涉及的工作。在用芯片相关的函数chip->irq_ack向中断控制器发送一个确认。然后调用handle_irq_event函数,该函数先将IRQS_PENDING的标志清除,然后设置IRQD_IRQ_INPROGRESS标志。这表示IRQ正在处理过程中,可用于避免同一处理程序在多个CPU上执行。
假定只有一个IRQ需要处理。在这种情况下,这时handle_irq_event函数会激活高层ISR处理程序,然后可以清除IRQD_IRQ_INPROGRESS标志。
void
handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
/*
* If we're currently running this IRQ, or its disabled,
* we shouldn't process the IRQ. Mark it pending, handle
* the necessary masking and go out
*/
if (unlikely(irqd_irq_disabled(&desc->irq_data) ||
irqd_irq_inprogress(&desc->irq_data) ||
!desc->action))
{
if (!irq_check_poll(desc)) {
/* 如果IRQ被禁用了,或者没有IRQ处理程序,
* 或者有别的CPU正在处理同一个IRQ编号的中断
* 这时将把状态设置为IRQS_PENDING状态。并取消处理 */
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
}
kstat_incr_irqs_this_cpu(irq, desc);
/* Start handling the irq */
desc->irq_data.chip->irq_ack(&desc->irq_data);
do {
if (unlikely(!desc->action)) {
mask_irq(desc);
goto out_unlock;
}
/*
* When another irq arrived while we were handling
* one, we could have masked the irq.
* Renable it, if it was not disabled in meantime.
*/
if (unlikely(desc->istate & IRQS_PENDING)) {
if (!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc);
}
/*调用上层ISR函数*/
handle_irq_event(desc);
/*只要IRQS_PENDING仍然置位,而且IRQ没有禁用,就一直迭代下去*/
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data));
out_unlock:
raw_spin_unlock(&desc->lock);
}
IRQ的处理是在一个循环中进行。假定我们刚好处于调用handle_irq_event之后的位置上。在第一个IRQ的ISR处理程序运行时,可能同时有第二个IRQ请求发送过来,前前面已经说过,这通过IRQ_PENDING表示,如果设置了该标志(同时该IRQ没有禁用),那么有另一个IRQ正在等待处理,循环将从头再次开始。
但在这种情况下,IRQ已经被屏蔽(在设置IRQ_PENDING时同时也调用了mask_ack_irq函数将该中断屏蔽掉了)。因而必须用unmask_irq解除IRQ的屏蔽,并清除IRQ_MASKED标志,这确保在handle_irq_event执行期间只能发生一个中断(被屏蔽后将不会在收到同一个编号的中断)。
◎ 电平触发
与边沿触发中断相比,电平触发中断稍微容易处理一些。这也反映在电流处理程序handle_level_irq的代码流程图中。如下图:
电平触发在处理时必须屏蔽,因此需要完成的第一件事就是调用mask_ack_irq,该函数屏蔽并确认IRQ,这是通过调用chip->irq_mask_ack,如果该方法不可用,则连续调用chip->irq_ack和chip->irq_mask。在多处理器系统上,可能发生竞态条件,尽管IRQ已经在另一个CPU上处理,但仍然在当前CPU上调用了handle_level_irq。这可以通过检查IRQD_IRQ_INPROGRESS标志来判断,这种情况下,IRQ已经在另一个CPU上处理,因而在当前CPU上可以放弃处理。
如果没有对该IRQ注册处理程序,也可以立即放弃处理,因为无事可做。另一个导致放弃处理的原因是设置了IRQ_DISABLE。尽管被禁用,有问题的硬件仍然可能发出IRQ,但可以被忽略。
接下来调用handle_irq_event。该函数在边沿触发中已有相关说明。最后需要解除对IRQ的屏蔽。但内核需要考虑到ISR可能禁用中断的情况,在这种情况下,ISR仍然保持屏蔽状态。否则,便调用unmask_irq来解除屏蔽。
由于设备驱动程序动态注册ISR的工作,可以使所述的数据结构非常简单的进行。在内核版本2.6重写中断子系统之前,该函数是由平台相关代码实现的。其原型在所有体系结构上都是相同的,因为对编写平台无关的驱动程序来说,这是一个绝对的先决条件。现在,该函数由通用代码实现:
int __must_check
request_irq( unsigned int irq, irq_handler_t handler,unsigned long flags,const char *name, void *dev);
该函数其实是request_thread_irq的一个包裹函数,该函数首先生成一个新的irqaction的实例,然后用函数参数填充其内容。当然,其中特别重要的是处理程序函数的handler。所有进一步的工作都委托给__setup_irq函数,它将执行下列步骤:
如果设置了IRQF_SAMPLE_RANDOM,则该中断将对内核熵池有所贡献,熵池用于随机数发生器/dev/radom。之后调用rand_initialize_irq将该IRQ添加到对应的数据结构中。
由request_thread_irq生成的irqaction实例被添加到所属IRQ编号对应的例程链表尾部,该链表表头为irq_desc[irq]->action。在处理中断共享中断时,内核就通过这种方式来确保中断发生时调用处理程序的顺序与其注册顺序相同。
如果安装的处理程序是该IRQ编号对应链接中的第一个,则调用handler->tup初始化函数。如果该IRQ此前已经安装了处理程序,则没有必要再调用该函数。
register_irq_proc在proc文件系统中建立目录/proc/irq/NULL。而register_handler_proc生成/proc/irq/NUM/name。接下来,系统中就可以看到对应的IRQ通道在使用了。
释放中断的方案,与前述过程刚好相反。首先,通过硬件相关的函数chip->shutdown通知中断控制器该IRQ已经删除,接下来将相关数据项从内核的一般数据结构中删除。辅助函数free_irq承担这些任务。在重写IRQ子系统之前它是一个体系结构相关的函数。
在IRQ处理程序需要删除一个共享的中断时,IRQ编号本身不足以标识该IRQ。在这种情况下,为提供唯一标识,还必须使用前面讲述的dev_id。内核扫描所有注册的处理程序的链表,直至找到一个匹配的处理程序。这时才能移除该项。
前面讲述的机制喉适用于由系统外设的中断请求所引发的中断。但内核还必须考虑由处理器本身或者用户进程中的软件机制所引发的中断。与IRQ相比,内核无需提供接口,供此类中断动态注册处理程序这是因为,所使用的编号在初始化时就是已知的,此后不会改变。中断和异常的注册在内核初始化时进行,其分配在运行时并不改变。
在注册了IRQ处理程序后,每次发生中断时将执行处理程序例程。仍然会出现如何协调不同平台差异的问题,由于事情的特定性质所致,使得差别不仅涉及平台相关实现中的各个C函数,还深入到用于底层处理、人工优化的汇编语言代码。
我们可以确定各个平台之间的几个结构上的相似性。例如,前文讨论过,各个平台上的中断操作都由3部分组成。进入路径从用户态切换到核心态,接下来执行实际的处理程序例程,最后从核心态切换回用户态。尽管涉及大量的汇编语言代码,至少有一些C代码片段在所有平台上都是相似的。
到核心态的切换,是基于每个中断之后由处理器自动执行汇编代码。该代码的任务如上面所讲,其中通常定义了各个入口点,在中断发生时处理器可以将控制流转到这些入口点。
只有那些最为必要的操作直接在汇编语言代码中执行。内核试图尽快地返回到常规的C语言,因为C语言代码更容易处理。为此,必须创建一个环境,与C编译器预期兼容。
在C语言中调用函数时,需要将所需的灵气按一定的顺序放到栈上。在用户态和核心态之间切换时,还需要将最重要的寄存器保存到栈上,以便以后恢复。这两个操作由平台相关的汇编语言代码执行。在大多数平台上,控制流接下来传递到C函数do_IRQ,其实现也是平台相关的,但情况仍然得到了很大的简化。该函数原型如下:
unsigned int __irq_entry do_IRQ(struct pt_regs *regs);
pt_regs用于保存内核使用的寄存器集合。各个寄存器的值被依次压栈(通过汇编语言代码)。在C函数调用之前,一直保存在栈上。
pt_regs的定义可以确保栈上的各个寄存器项与该结构的各个上对应。这些值并不是仅仅保存用于后续的使用,C代码也可以读取这些值。
此外,寄存器集合也可以被复制到地址空间中栈以外的其它位置。在这咱情况下,do_IRQ的一个参数是指向pt_regs的指针,但这并没有改变以下事实:寄存器的内存已经被保存,可以由C代码读取。
pt_regs的定义是平台相关的,因而不同的处理器提供了不同的寄存器集合。
只有在内核使用内核栈来处理IRQ的情况下,上面描述的情形才是正确的。但不一定总是如此,IA-32体系结构提供了配置选项CONFIG_4KSTACKS。如果启用该配置,内核栈的长度由8KB缩减到4KB。由于IA-32计算机上页面的大小是4KB,实现内核栈所需的页数目由2个减少到一个。由于单个内存页比两个连续的内存页更容易分配,在系统中有大量活动进程时,这使得虚拟内存子系统的工作会稍微容易些。遗憾的,对常规的内核工作以及IRQ处理例程所需的空间来说,4KB并不总是够用,因而引入了另外的两个栈。
◎ 用于硬件IRQ处理的栈
◎ 用于软件IRQ处理的栈
常规的内核栈对每个进程都会分配,而这两个额外的栈是针对各CPU分别分配的,在硬件中断发生时,内核需要切换到适当的栈。
电流处理程序例程的调用方式,因体系结构而不同,我们假定内核栈只使用了一个页帧,即每个进程的内核栈为4KB。如果设置了CONFIG_4KSTACKS,内核栈的配置就是这样。在上面说过,如果在这种情况下,内核需要一个独立的栈处理IRQ。
首先开始先调用set_irq_regs将一个指向寄存器集合的指针保存在一个全局的CPU变量中(中断发生之前,变量中保存的旧指针会保留下来,借后续使用)。需要访问寄存器集合的中断处理程序,可以从该变量中访问。
接下调用irq_enter负责更新一些统计量。对于具备动态时钟周期特性的系统,如果系统已经有很长一段时间没有发生时钟中断,则更新全局计时变量jiffies。接下来内核必须切换到IRQ栈。当前栈可以通过调用辅助函数current_thead_info获得,该函数返回一个指向当前使用的thread_info实例的指针。而指向适当的IRQ栈的指针可以从上下文中的hardirq_ctx获得。有如下两种可能的情况:
进程已经在使用IRQ栈了,因为是在处理嵌套的IRQ,在这种情况下,内核不需要做什么,所有的设置都已经完成。可以调用ird_desc[irq]->handle_irq来激活保存在IRQ数据库中的IRQ。
当前栈不是IRQ栈,(curctx != irqctx),需要在二者之间切换,在这种情况下,内核执行所需的底层汇编语言操作来切换栈,然后调用ird_desc[irq]->handle_irq,最后再将栈切换回去。
接下来调用irq_exit函数,该函数负责记录一些统计量,另外还要调用do_softirq来处理任何待决的软件IRQ。最后再次调用set_irq_regs,将指向struct pt_regs的指针恢复到上一次调用之前的值。这确保嵌套的处理程序能够正确工作。
经过do_IRQ的工作最终将调到不同的电流处理函数,也就是之前所说的handle_edge_irq或handle_level_irq函数,在介绍这两个函数的时候,函数最终都会去调用handle_irq_event的函数,而这个函数最终又会调用到handle_irq_event_percpu函数,都会采用这个函数来激活与特定IRQ相关的高层ISR。现在需要仔细的说下这个函数。该函数原型如下:
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
该函数主要的任务就是逐一调用所注册的IRQ处理程序action。代码大概如下面这样:
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
irqreturn_tretval = IRQ_NONE;
unsigned int random = 0, irq = desc->irq_data.irq;
......
do {
irqreturn_t res;
......
res = action->handler(irq, action->dev_id);
......
retval |= res;
action = action->next;
} while (action);
......
return retval;
}
在共享IRQ时,内核无法找出引发中断请求的设备。该工作完全留自带程序例程,其中将使用设备相关的寄存器或其他硬件特征来查找中断来源。未受影响的例程也需要识别出该中断并非来自于相关设备,应该尽可能快的将控制返回。但处理程序例程也无法向高层代码报告该中断是否是针对它的。内核总是依次执行所有处理程序例程,而不考虑实际上哪个处理程序与该中断相关。
但内核总可以检查是否有负责该IRQ的处理程序。irqreturn_t定义为处理程序函数的返回类型,它只是一个简单的整形变量。可以接收IRQ_NONE和IRQ_HANDLED,这取决于处理程序是否处理了该IRQ。
在执行所有处理程序例程期间,内核将返回结果用逻辑“或”操作合并起来。内核最后可以据此判断IRQ是否被处理。
中断这块的内容大概说完了,不过这里也只是从硬件得到响应后的硬件中断,因为在开始也说过,由于CPU的资源宝贵,而在中断处理期间又不能有任何的抢占操作,所以在硬件中断的过程中,各个中断处理例程只是把一些必须的操作在这里面执行完,然后就把控制权交回,真正做后续处理的是后面将介绍的软中断。下一篇,将要针对软中断进行一些介绍。
本篇文章参考了《深入理解Linux内核架构》。有些代码结合最新3.5的内核进行了分析。