Linux内核活动之中断1
系统调用不是在用户态和系统状态之间切换的唯一途径。所有支持Linux的平台都采用了中断(interrupt)的概念,以便(因种种原因)引入周期性的中断。需要区分两种类型的中断。
- 硬件中断(hardware interrupt):由系统自身和与之连接的外设自动产生。它们用于支持更高效地实现设备驱动程序,也用于引起处理器自身对异常或错误的关注,这些是需要与内核代码进行交互的。
- 软中断(SoftIRQ):用于有效实现内核中的延期操作。
与内核的其他部分相比,用于处理中断和系统调用相关部分的代码中,汇编和C代码交织在一起,以解决C语言无法独立处理的一些微妙问题。这不是一个特定于Linux的问题。无论各个操作系统采用的方法如何,大多数操作系统的开发者都试图将此类问题的底层处理尽可能深地隐藏到内核源代码中,使之对其余的代码不可见。因为技术上的现实情况,这并不是总能够做到的,但中断处理部分随着时间的演化,已经达到了这样一种状态:高层代码和底层的硬件交互代码,已经尽可能有效而干净地分隔开了。
内核经常需要一些机制,将某些活动延迟到未来的某个时间执行,或将活动置于某个队列上,在时间充裕时进行后续处理。
1. 中断介绍
直至内核版本2.4,在Linux所支持的各个平台上,中断实现的唯一共同点就是,这些代码都是真实存在的,但所有的相似性也就到此为止了。大量代码(和许多复制功能)散布到各个特定于体系结构的组件中。在内核版本2.6的开发期间,这种情况有了很大改善,因为其中引入了一个用于中断和IRQ的通用框架。各个平台现在只负责在最低层次上与硬件交互。所有其他功能都由通用代码提供。
1.1 中断类型
通常,各种类型的中断可分为如下两个类别。
-
同步中断
这些由CPU自身产生,针对当前执行的程序。
这种情况下,内核必须通知应用程序出现了异常。举例来说,内核可以使用信号机制。这使得应用程序有机会改正错误、输出适当的错误消息或直接结束。 -
异常
异常可能因种种原因触发:由于运行时发生的程序设计错误(典型的例子是除0),或由于出现了异常的情况或条件,致使处理器需要“外部”的帮助才能处理。
异常情况不见得是由进程直接导致的,但必须借助于内核才能修复。一个可能的例子是缺页异常,在进程试图访问虚拟地址空间的一页,而该页不在物理内存中时,才会发生此类异常。内核必须与CPU交互,确保将预期的数据取入物理内存。接下来,进程可以在发生异常的位置恢复执行。由于内核自动恢复了这种情况,进程甚至不会注意到缺页异常的存在。 -
异步中断
这是经典的中断类型,由外部设备产生,可能发生在任意时间。不同于同步中断,异步中断并不与特定进程关联。它们可能发生在任何时间,而不牵涉系统当前执行的活动。
网卡通过发出一个相关的中断来报告新分组的到达。因为数据可能在任意时刻到达系统,所以当前执行的很可能是与数据无关的某个进程或其他东西。为避免损害该进程,内核必须确保中断能够尽快处理完毕(通过缓冲数据),使得CPU时间能够返还给当前进程。这也是内核需要延期操作机制的原因,
两类中断的共同特性是:如果CPU当前不处于核心态,则发起从用户态到核心态的切换。接下来,在内核中执行一个专门的例程,称为中断服务例程(interrupt service routine,简称ISR)或中断处理程序(interrupt handler)。
该例程的作用是处理异常条件或情况,毕竟,中断的作用就在于引起内核对此类改变的关注。
同步和异步中断之间的简单区别,并不足以描述这两类类型中断的特性。还需要考虑另一方面。许多中断可以禁用,但有些不行。举例来说,后一类就包括了因硬件故障或其他系统关键事件而发出的中断。
在可能的情况下,内核试图避免禁用中断,因为这显然会损害系统性能。但有些场合禁用中断是必要的,这是为防止内核遇到一些严重的麻烦。在仔细考察中断处理程序时,读者会看到,在处理第一个中断时,如果发生第二个中断,内核中可能发生严重的问题。如果内核在处理关键代码(即获得了锁)时发生了中断,那么可能会发生同步问题。在最坏情况下,这可能引起内核死锁,致使整个系统变得不可用。
如果内核容许在禁用中断的情况下,花费过多时间处理一个ISR,那么可能(也必将)会丢失一些对系统正确运作必不可少的中断。内核为解决该问题,将中断处理程序划分为两个部分,性能关键的前一部分在禁用中断时执行,而不那么重要的后一部分延期执行,进行所有次要的操作。早期的内核版本也包含了一种同名机制,用于将操作延期一段时间执行。
1.2 中断共享
每个中断都有一个编号。如果中断号n分配给一个网卡而m≠ n分配给SCSI控制器,那么内核即可区分两个设备,并在中断发生时调用对应的ISR来执行特定于设备的操作。当然,同样的原则也适应于异常,不同的异常指派了不同的编号。遗憾的是,由于特别设计(通常是历史上的)的“特性”(IA-32体系结构就是一个恰当的特例),情况并不总是像描述的那样简单。因为只有很少的编号可用于硬件中断,所以必须由几个设备共享一个编号。在IA-32处理器上,硬件中断的最大数目通常是15,这个值可不怎么大,还有考虑到有些中断编号已经永久性地分配给了标准的系统组件(键盘、定时器,等),因而限制了可用于其他外部设备的中断编号数目。这个过程称为中断共享(interrupt sharing)。 但必须硬件和内核同时支持才能使用该技术,因为必须要识别出中断来源于哪个设备。
1.3 硬件IRQ
以前,中断这个名词使用得很不谨慎,用来表示由CPU和外部硬件发出的中断。明白的读者当然会注意到这陈述得不大准确。中断不能由处理器外部的外设直接产生,而必须借助于一个称为中断控制器(interrupt controller)的标准组件来请求,该组件存在于每个系统中。
外部设备(或其槽位),会有电路连接到用于向中断控制器发送中断请求的组件。控制器在执行了各种电工任务之后,将中断请求转发到CPU的中断输入。因为外部设备不能直接发出中断,而必须通过上述组件请求中断,所以这种请求更正确的叫法是IRQ,或中断请求(interrupt request)。
但这里的一个要点涉及IRQ和中断的数目,我们绝不能忽视这一点,因为它会影响到软件。对大多数CPU来说,都只是从可用于处理硬件中断的整个中断号范围抽取一小部分使用。抽取出的范围通常位于所有中断号序列的中部,例如,IA-32 CPU总共提供了16个中断号,从32到47。
1.4 处理中断
在CPU得知发生中断后,它将进一步的处理委托给一个软件例程,该例程可能会修复故障、提供专门的处理或将外部事件通知用户进程。由于每个中断和异常都有唯一的编号,内核使用一个数组,数组项是指向处理程序函数的指针。相关的中断号根据数组项在数组中的位置判断
1.4.1 进入和退出任务
如图所示,中断处理划分为3部分。首先,必须建立一个适当的环境,使得处理程序函数能够在其中执行,接下来调用处理程序自身,最后将系统复原(在当前程序看来)到中断之前的状态。调用中断处理程序前后的两部分,分别称为进入路径(entry path)和退出路径(exit path)。
进入和退出任务还负责确保处理器从用户态切换到核心态。进入路径的一个关键任务是,从用户态栈切换到核心态栈。但是,只有这一点还不够。因为内核还要使用CPU资源执行其代码,进入路径必须保存用户应用程序当前的寄存器状态,以便在中断活动结束后恢复。这与调度期间用于上下文切换的机制是相同的。在进入核心态时,只保存整个寄存器集合的一部分。内核并不使用全部寄存器。举例来说,内核代码中不使用浮点操作(只有整数计算),因而并不保存浮点寄存器。浮点寄存器的值在执行内核代码时不会改变。平台相关的数据结构pt_regs列出了核心态可能修改的所有寄存器,它的定义考虑到了不同的CPU之间的差别。在汇编语言编写的底层例程负责填充该结构。
在退出路径中,内核会检查下列事项。
- 调度器是否应该选择一个新进程代替旧的进程。
- 是否有信号必须投递到原进程。
从中断返回之后,只有确认了这两个问题,内核才能完成其常规任务,即还原寄存器集合、切换到用户态栈、切换到适用于用户应用程序的适当的处理器状态,或切换到一个不同的保护环。
因为需要C语言代码和汇编语言代码之间的交互,所以必须特别小心,才能正确设计在汇编语言层次和C语言层次上的数据交换。对应的代码位于arch/arch/kernel/entry.S
中,彻底利用了各个处理器的具体特性。为此,该文件的内容应该尽可能少修改,即使修改也必须极其小心。
术语中断处理程序(interrupt handler)的使用是可能引起岐义的。它用于指代CPU对ISR(中断服务程序)的调用,包括了进入/退出路径和ISR本身。当然,如果只指代在进入路径和退出路径之间执行、由C语言实现的例程,将更为准确。
1.4.2 中断处理程序
中断处理程序可能会遇到困难,特别是,在处理程序执行期间,发生了其他中断。尽管可以通过在处理程序执行期间禁用中断来防止,但这会引起其他问题,如遗漏重要的中断。屏蔽(Masking,这个术语用于表示选择性地禁用一个或多个中断)因而只能短时间使用。
因此ISR必须满足如下两个要求
- 实现(特别是在禁用其他中断时)必须包含尽可能少的代码,以支持快速处理。
- 可以在其他ISR执行期间调用的中断处理程序例程,不能彼此干扰。
尽管后一个要求可以通过高超的编程和精巧的ISR设计来满足,然而前一个要求更难满足。根据具体的中断,必须运行某个程序,来满足中断处理的最低要求。因而代码长度无法任意缩减。
内核如何解决这种两难问题呢?并非ISR的每个部分都同等重要。通常,每个处理程序例程都可以划分为3个部分,具有不同的意义。
- 关键操作必须在中断发生后立即执行。否则,无法维持系统的稳定性,或计算机的正确运作。在执行此类操作期间,必须禁用其他中断。
- 非关键操作也应该尽快执行,但允许启用中断(因而可能被其他系统事件中断)。
- 可延期操作不是特别重要,不必在中断处理程序中实现。内核可以延迟这些操作,在时间充裕时进行。
内核提供了tasklet,用于在稍后执行可延期操作。
1.5 数据结构
中断技术上的实现有两方面:汇编语言代码,与处理器高度相关,用于处理特定平台上相关的底层细节;抽象接口,是设备驱动程序及其他内核代码安装和管理IRQ处理程序所需的。
为响应外部设备的IRQ,内核必须为每个潜在的IRQ提供一个函数。该函数必须能够动态注册和注销。静态表组织方式是不够的,因为可能为设备编写模块,而且设备可能与系统的其他部分通过中断进行交互。
IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号。因为数组位置和中断号是相同的,很容易定位与特定的IRQ相关的数组项:IRQ 0在位置0,IRQ 15在位置15,等等。IRQ最终映射到哪个处理器中断,在这里是不相关的。
该数组定义如下:
kernel/irq/irqdesc.c
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
尽管各个数组项使用的是一个体系结构无关的数据类型,但IRQ的最大可能数目是通过一个平台相关的常数NR_IRQS指定的。大多数体系结构下,该常数定义在处理器相关的头文件arch/arch/include/asm/mach-la64/irq.h
中。
同样串口log中也有关于NR_IRQS
的具体介绍
NR_IRQS: 4160, nr_irqs: 4160, preallocated irqs: 16
1.5.1 内核的IRQ处理子系统
内核在2.6之前的版本包含了大量平台相关代码来处理IRQ,在许多地方是相同的。因而,在内核版本2.6开发期间,引入了一个新的通用的IRQ子系统。它能够以统一的方式处理不同的中断控制器和不同类型的中断。基本上,它由3个抽象层组成,如图所示。
- 高层ISR(high-level interrupt service routines,高层中断服服务例程)针对设备驱动程序端(或其他内核组件)的中断,执行由此引起的所有必要的工作。例如,如果设备使用中断通知一些数据已经到达,那么高层ISR的工作应该是将数据复制到适当的位置。
- 中断电流处理(interrupt flow handling):处理不同的中断电流类型之间的各种差别,如边沿触发(edge-triggering)和电平触发(level-triggering)。
边沿触发意味着硬件通过感知线路上的电位差来检测中断。在电平触发系统中,根据特定的电势值检测中断,与电势是否改变无关。
从内核的角度来看,电平触发更为复杂,因为在每个中断后,都需要将线路明确设置为一个特定的电势,表示“没有中断”。 - 芯片级硬件封装(chip-level hardware encapsulation):需要与在电子学层次上产生中断的底层硬件直接通信。该抽象层可以视为中断控制器的某种“设备驱动程序”。
1.5.2 IRQ描述符
用于表示IRQ描述符的结构定义如下:
/**
* struct irq_common_data - per irq data shared by all irqchips
* @state_use_accessors: status information for irq chip functions.
* Use accessor functions to deal with it
* @node: node index useful for balancing
* @handler_data: per-IRQ data for the irq_chip methods
* @affinity: IRQ affinity on SMP. If this is an IPI
* related irq, then this is the mask of the
* CPUs to which an IPI can be sent.
* @effective_affinity: The effective IRQ affinity on SMP as some irq
* chips do not allow multi CPU destinations.
* A subset of @affinity.
* @msi_desc: MSI descriptor
* @ipi_offset: Offset of first IPI target cpu in @affinity. Optional.
*/
struct irq_common_data {
unsigned int __private state_use_accessors;
#ifdef CONFIG_NUMA
unsigned int node;
#endif
void *handler_data;
struct msi_desc *msi_desc;
cpumask_var_t affinity;
#ifdef CONFIG_GENERIC_IRQ_EFFECTIVE_AFF_MASK
cpumask_var_t effective_affinity;
#endif
#ifdef CONFIG_GENERIC_IRQ_IPI
unsigned int ipi_offset;
#endif
};
/**
* struct irq_data - per irq chip data passed down to chip functions
* @mask: precomputed bitmask for accessing the chip registers
* @irq: interrupt number
* @hwirq: hardware interrupt number, local to the interrupt domain
* @common: point to data shared by all irqchips
* @chip: low level interrupt hardware access
* @domain: Interrupt translation domain; responsible for mapping
* between hwirq number and linux irq number.
* @parent_data: pointer to parent struct irq_data to support hierarchy
* irq_domain
* @chip_data: platform-specific per-chip private data for the chip
* methods, to allow shared chip implementations
*/
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
/**
* struct irq_desc - interrupt descriptor
* @irq_common_data: per irq and chip data passed down to chip functions
* @kstat_irqs: irq stats per cpu
* @handle_irq: highlevel irq-events handler
* @preflow_handler: handler called before the 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 nested irq_disable() calls
* @wake_depth: enable depth, for multiple irq_set_irq_wake() callers
* @irq_count: stats field to detect stalled irqs
* @last_unhandled: aging timer for unhandled count
* @irqs_unhandled: stats field for spurious unhandled interrupts
* @threads_handled: stats field for deferred spurious detection of threaded handlers
* @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
* @lock: locking for SMP
* @affinity_hint: hint to user space for preferred irq affinity
* @affinity_notify: context for notification of affinity changes
* @pending_mask: pending rebalanced interrupts
* @threads_oneshot: bitfield to handle shared oneshot threads
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
* @nr_actions: number of installed actions on this descriptor
* @no_suspend_depth: number of irqactions on a irq descriptor with
* IRQF_NO_SUSPEND set
* @force_resume_depth: number of irqactions on a irq descriptor with
* IRQF_FORCE_RESUME set
* @rcu: rcu head for delayed free
* @kobj: kobject used to represent this struct in sysfs
* @request_mutex: mutex to protect request/free before locking desc->lock
* @dir: /proc/irq/ procfs entry
* @debugfs_file: dentry for the debugfs file
* @name: flow handler name for /proc/interrupts output
*/
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
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 *//* IRQ操作列表 */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int tot_count;
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#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_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
const char *dev_name;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
struct mutex request_mutex;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
从内核中高层代码的角度来看,每个IRQ都可以由该结构完全描述。上面介绍的3个抽象层在该结构中表示如下。
-
电流层ISR由
handle_irq
提供。handler_data
可以指向任意数据,该数据可以是特定于IRQ或处理程序的。每当发生中断时,特定于体系结构的代码都会调用handle_irq
。该函数负责使用chip中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作。用于不同中断类型的默认函数由内核提供。 -
action
提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。有一个专门的数据结构用于表示这些操作。 -
电流处理和芯片相关操作被封装在
chip
中。为此引入了一个专门的数据结构。chip_data
指向可能与chip相关的任意数据。 -
name
指定了电流层处理程序的名称,将显示在/proc/interrupts
中。对边沿触发中断,通常是“edge”,对电平触发中断,通常是“level”。 -
depth
有两个任务。它可用于确定IRQ电路是启用的还是禁用的。正值表示禁用,而0表示启用。为什么用正值表示禁用的IRQ呢?因为这使得内核能够区分启用和禁用的IRQ电路,以及重复禁用同一中断的情形。这个值相当于一个计数器,内核其余部分的代码每次禁用某个中断,则将对应的计数器加1;每次中断被再次启用,则将计数器减1。在depth归0时,硬件才能再次使用对应的IRQ。这种方法能够支持对嵌套禁用中断的正确处理。 -
status_use_accessors
描述了IRQ的当前状态。IRQ不仅可以在处理程序安装期间改变其状态,而且可以在运行时改变。./include/linux/irq.h
文件定义了各种常数,可用于描述IRQ电路当前的状态。每个常数表示位串中一个置位的标志位,只要不相互冲突,几个标志可以同时设置。/* * Bit masks for irq_common_data.state_use_accessors * * IRQD_TRIGGER_MASK - Mask for the trigger type bits * IRQD_SETAFFINITY_PENDING - Affinity setting is pending * IRQD_ACTIVATED - Interrupt has already been activated * IRQD_NO_BALANCING - Balancing disabled for this IRQ * IRQD_PER_CPU - Interrupt is per cpu * IRQD_AFFINITY_SET - Interrupt affinity was set * IRQD_LEVEL - Interrupt is level triggered * IRQD_WAKEUP_STATE - Interrupt is configured for wakeup * from suspend * IRDQ_MOVE_PCNTXT - Interrupt can be moved in process * context * IRQD_IRQ_DISABLED - Disabled state of the interrupt * IRQD_IRQ_MASKED - Masked state of the interrupt * IRQD_IRQ_INPROGRESS - In progress state of the interrupt * IRQD_WAKEUP_ARMED - Wakeup mode armed * IRQD_FORWARDED_TO_VCPU - The interrupt is forwarded to a VCPU * IRQD_AFFINITY_MANAGED - Affinity is auto-managed by the kernel * IRQD_IRQ_STARTED - Startup state of the interrupt * IRQD_MANAGED_SHUTDOWN - Interrupt was shutdown due to empty affinity * mask. Applies only to affinity managed irqs. * IRQD_SINGLE_TARGET - IRQ allows only a single affinity target * IRQD_DEFAULT_TRIGGER_SET - Expected trigger already been set * IRQD_CAN_RESERVE - Can use reservation mode */ enum { IRQD_TRIGGER_MASK = 0xf, IRQD_SETAFFINITY_PENDING = (1 << 8), IRQD_ACTIVATED = (1 << 9), IRQD_NO_BALANCING = (1 << 10), IRQD_PER_CPU = (1 << 11), IRQD_AFFINITY_SET = (1 << 12), IRQD_LEVEL = (1 << 13), IRQD_WAKEUP_STATE = (1 << 14), IRQD_MOVE_PCNTXT = (1 << 15), IRQD_IRQ_DISABLED = (1 << 16), IRQD_IRQ_MASKED = (1 << 17), IRQD_IRQ_INPROGRESS = (1 << 18), IRQD_WAKEUP_ARMED = (1 << 19), IRQD_FORWARDED_TO_VCPU = (1 << 20), IRQD_AFFINITY_MANAGED = (1 << 21), IRQD_IRQ_STARTED = (1 << 22), IRQD_MANAGED_SHUTDOWN = (1 << 23), IRQD_SINGLE_TARGET = (1 << 24), IRQD_DEFAULT_TRIGGER_SET = (1 << 25), IRQD_CAN_RESERVE = (1 << 26), };
IRQ_DISABLE_UNLAZY
用于表示被设备驱动程序禁用的IRQ电路。该标志通知内核不要进入处理程序。IRQD_IRQ_INPROGRESS
:在IRQ处理程序执行期间,状态设置为IRQD_IRQ_INPROGRESS
。与IRQD_IRQ_DISABLED
类似,这会阻止其余的内核代码执行该处理程序。- 在CPU注意到一个中断但尚未执行对应的处理程序时,
IRQD_SETAFFINITY_PENDING
标志位置位。 - 为正确处理发生在中断处理期间的中断,需要
IRQD_IRQ_MASKED
标志。 - 在某个IRQ只能发生在一个CPU上时,将设置
IRQD_PER_CPU
标志位。(在SMP系统中,该标志使几个用于防止并发访问的保护机制变得多余。) IRQD_LEVEL
用于Alpha和PowerPC系统,用于区分电平触发和边沿触发的IRQ。IRQD_AFFINITY_MANAGED
用于IRQ的自动检测和配置。相应的代码位于kernel/irq/autoprobe.c
中。
根据status
当前的值,内核很容易获知某个IRQ的状态,而无需了解底层实现的硬件相关特性。
当然,只设置对应的标志位是不会产生预期效果的。通过设置IRQ_DISABLED
标志来禁用中断是不可能的,还必须将新状态通知底层硬件。因而,该标志只能通过特定于控制器的函数设置,这些函数同时还负责将设置信息同步到底层硬件。在很多情况下,这必须使用汇编语言代码,或通过out命令向特定地址写入特定数值。
最后,irq_desc
的irq_count
和irq_unhandled
字段提供了一些统计量,可用于检测停顿和未处理,但持续发生的中断。后者通常称作假中断(spurious interrupt)。可参见kernel/irq/spurious.c
中的note_interrupt
函数。
1.5.2.1 IRQ控制器抽象
handler
是一个hw_irq_controller
数据类型的实例,该类型抽象出了一个IRQ控制器的具体特
征,可用于内核的体系结构无关部分。它提供的函数用于改变IRQ的状态,这也是它们还负责设置flag
的原因:
/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @parent_device: pointer to parent device for irqchip
* @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 an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
* @irq_set_affinity: Set the CPU affinity on SMP machines. If the force
* argument is true, it tells the driver to
* unconditionally apply the affinity setting. Sanity
* checks against the supplied affinity mask are not
* required. This is used for CPU hotplug where the
* target CPU is not yet set in the cpu_online_mask.
* @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/disable power-management wake-on of an IRQ
* @irq_bus_lock: function to lock access to 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 an interrupt source for a secondary CPU
* @irq_suspend: function called from core code on suspend once per
* chip, when one or more interrupts are installed
* @irq_resume: function called from core code on resume once per chip,
* when one ore more interrupts are installed
* @irq_pm_shutdown: function called from core code on shutdown once per chip
* @irq_calc_mask: Optional function to set irq_data.mask for special cases
* @irq_print_chip: optional to print special chip info in show_interrupts
* @irq_request_resources: optional to request resources before calling
* any other callback related to this irq
* @irq_release_resources: optional to release resources acquired with
* irq_request_resources
* @irq_compose_msi_msg: optional to compose message content for MSI
* @irq_write_msi_msg: optional to write message content for MSI
* @irq_get_irqchip_state: return the internal state of an interrupt
* @irq_set_irqchip_state: set the internal state of a interrupt
* @irq_set_vcpu_affinity: optional to target a vCPU in a virtual machine
* @ipi_send_single: send a single IPI to destination cpus
* @ipi_send_mask: send an IPI to destination cpus in cpumask
* @flags: chip specific flags
*/
struct irq_chip {
struct device *parent_device;
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_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
unsigned long flags;
};
该结构需要考虑内核中出现的各个IRQ实现的所有特性。因而,一个该结构的特定实例,通常只
定义所有可能方法的一个子集。
name
包含一个短的字符串,用于标识硬件控制器。在IA-32系统上可能的值是“XTPIC”和“IO-APIC”,在AMD64系统上大多数情况下也会使用后者。在其他系统上有各种各样的值,因为有许多不同的控制器类型,其中很多类型都得到了广泛应用。
各个函数指针的语义如下。
irq_startup
指向一个函数,用于第一次初始化一个IRQ。大多数情况下,初始化工作仅限于启用该IRQ。因而,irq_startup
函数实际上就是将工作转给enable
。irq_enable
激活一个IRQ。换句话说,它执行IRQ由禁用状态到启用状态的转换。为此,必须向I/O内存或I/O端口中硬件相关的位置写入特定于硬件的数值。disable
与enable
的相对应,用于禁用IRQ。而shutdown
完全关闭一个中断源。如果不支持该特性,那么这个函数实际上是disable的别名。- ack与中断控制器的硬件密切相关。在某些模型中,IRQ请求的到达(以及在处理器的对应中断)必须显式确认,后续的请求才能进行处理。如果芯片组没有这样的要求,该指针可以指向一个空函数,或NULL指针。ack_and_mask确认一个中断,并在接下来屏蔽该中断。
- 调用end标记中断处理在电流层次的结束。如果一个中断在中断处理期间被禁用,那么该函数负责重新启用此类中断。
- 现代的中断控制器不需要内核进行太多的电流控制,控制器几乎可以管理所有事务。在处理中断时需要一个到硬件的回调,由eoi提供,eoi表示end of interrupt,即中断结束。
- 在多处理器系统中,可使用set_affinity指定CPU来处理特定的IRQ。这使得可以将IRQ分配给某些CPU(通常,SMP系统上的IRQ是平均发布到所有处理器的)。该方法在单处理器系统上没用,可以设置为NULL指针。
- set_type设置IRQ的电流类型。该方法主要使用在ARM、PowerPC和SuperH机器上,其他系统不需要该方法,可以将set_type设置为NULL。
辅助函数set_irq_type(irq, type)是一个便捷函数,用于设置irq的IRQ类型。类型IRQ_TYPE_RISING
和IRQ_TYPE_FALLING分别指定了边沿触发中断使用上升沿和下降沿,而IRQ_TYPE_EDGE_BOTH
则指定两种边沿触发均适用。电平触发中断分为IRQ_TYPE_LEVEL_HIGH
用于高电平触发和用于低电平触发的IRQ_TYPE_LEVEL_LOW
。最后,IRQ_TYPE_NONE
设置了一种未指定的类型。
1.5.2.2 处理程序函数的表示
irqaction
结构定义如下,每个处理程序函数都对应该结构的一个实例:
./include/linux/interrupt.h
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @name: name of the device
* @dev_id: cookie to identify the device
* @percpu_dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @flags: flags (see IRQF_* above)
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @secondary: pointer to secondary irqaction (force threading)
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
* @dir: pointer to the proc/irq/NN/name entry
*/
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
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是一个短字符串,用于标识设备(例如,“e100”、“ncr53c8xx”,等等),而dev_id
是一个指针,指向在所有内核数据结构中唯一标识了该设备的数据结构实例,例如网卡的net_device
实例。如果几个设备共享一个IRQ,那么IRQ编号自身不能标识该设备,此时,在删除处理程序函数时,将需要上述信息。
flags
是一个标志变量,通过位图描述了IRQ(和相关的中断)的一些特性,位图中的各个标志位照例可通过预定义的常数访问。interrupt.h
中定义了下列常数。
/*
* These flags used only by the kernel as part of the
* irq handling routines.
*
* IRQF_SHARED - allow sharing the irq among several devices
* IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
* IRQF_TIMER - Flag to mark this interrupt as timer interrupt
* IRQF_PERCPU - Interrupt is per cpu
* IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
* IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
* registered first in an shared interrupt is considered for
* performance reasons)
* IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
* Used by threaded interrupts which need to keep the
* irq line disabled until the threaded handler has been run.
* IRQF_NO_SUSPEND - Do not disable this IRQ during suspend. Does not guarantee
* that this interrupt will wake the system from a suspended
* state. See Documentation/power/suspend-and-interrupts.txt
* IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
* IRQF_NO_THREAD - Interrupt cannot be threaded
* IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
* resume time.
* IRQF_COND_SUSPEND - If the IRQ is shared with a NO_SUSPEND user, execute this
* interrupt handler after suspending interrupts. For system
* wakeup devices users need to implement wakeup detection in
* their interrupt handlers.
*/
#define IRQF_SHARED 0x00000080//对共享的IRQ设置`IRQF_SHARED`,表示有多于一个设备使用该IRQ电路。
#define IRQF_PROBE_SHARED 0x00000100
#define __IRQF_TIMER 0x00000200
#define IRQF_PERCPU 0x00000400
#define IRQF_NOBALANCING 0x00000800
#define IRQF_IRQPOLL 0x00001000
#define IRQF_ONESHOT 0x00002000
#define IRQF_NO_SUSPEND 0x00004000
#define IRQF_FORCE_RESUME 0x00008000
#define IRQF_NO_THREAD 0x00010000
#define IRQF_EARLY_RESUME 0x00020000
#define IRQF_COND_SUSPEND 0x00040000
#define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)//表示时钟中断。
- next用于实现共享的IRQ处理程序。几个
irqaction
实例聚集到一个链表中。链表的所有元素都必须处理同一IRQ编号(处理不同编号的实例,位于irq_desc
数组中不同的位置)。在发生一个共享中断时,内核扫描该链表找出中断实际上的来源设备。特别是在单芯片(只有一个中断)上集成了许多不同的设备(网络、USB、FireWire、声卡等)的笔记本电脑中,此类处理程序链表可能包含大约5个元素。但我们预期的情况是,每个IRQ下都注册一个设备。
下图给出了所描述各数据结构的一个概览,说明其彼此交互的方式。因为通常在一个系统上只有一种类型的中断控制器会占据支配地位(当然,并没有什么约束条件阻止多个控制器并存),所有irq_desc的handler成员都指向irq_chip的同一实例。
1.6 中断电流处理
1.6.1 设置控制器硬件
首先,需要提到内核提供的一些标准函数,用于注册irq_chip和设置电流处理程序:
include/linux/irq.h
int set_irq_chip(unsigned int irq, struct irq_chip *chip);
void set_irq_handler(unsigned int irq, irq_flow_handler_t handle);
void set_irq_chained_handler(unsigned int irq, irq_flow_handler_t handle)
void set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip,
irq_flow_handler_t handle);
void set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
irq_flow_handler_t handle, const char
*name);
set_irq_chip
将一个IRQ芯片以irq_chip实例的形式关联到某个特定的中断。除了从irq_desc
选取适当的成员并设置chip指针之外,如果没有提供特定于芯片的实现,该函数还将设置默认的处理程序。如果chip指针为NULL,将使用通用的“无控制器”irq_chip
实例no_irq_chip
,该实现只提供了空操作。set_irq_handler
和set_irq_chained_handler
为某个给定的IRQ编号设置电流处理程序。第二种变体表示,处理程序必须处理共享的中断。这会置位irq_desc[irq]->status
中的标志位IRQ_NOREQUEST
和IRQ_NOPROBE
:设置第一个标志,是因为共享中断是不能独占使用的,设置第二个标志,是因为在有多个设备的IRQ电路上,使用中断探测显然是个坏主意。两个函数在内部都使用了__set_irq_handler
,该函数执行一些合理性检查,然后设置irq_desc[irq]->handle_irq
。set_chip_and_handler
是一个快捷方式,它相当于连续调用上述的各函数。_name变体的工作方式相同,但可以为电流处理程序指定一个名称,保存在irq_desc[irq]->name
中。
1.6.2 电流处理
irq_flow_handler_t
指定了IRQ电流处理程序函数的原型:
typedef void fastcall (*irq_flow_handler_t)(unsigned int irq,
struct irq_desc *desc);
电流处理程序的参数包括IRQ编号和一个指向负责该中断的irq_handler
实例的指针。该信息接下来可用于实现正确的电流处理。
回想前文,可知不同的硬件需要不同的电流处理方式,例如,边沿触发和电平触发就需要不同的处理。内核对各种类型提供了几个默认的电流处理程序。它们有一个共同点:每个电流处理程序在其工作结束后,都要负责调用高层ISR。handle_IRQ_event
负责激活高层的处理程序。 现在,主要讲述如何进行电流处理。
1.6.2.1 边沿触发中断
现在的硬件大部分采用的是边沿触发中断,因此首先讲述这一类型。默认处理程序实现在handle_edge_irq
中。其代码流程图如图所示。
在处理边沿触发的IRQ时无须屏蔽,这与电平触发IRQ是相反的。这对SMP系统有一个重要的含义:当在一个CPU上处理一个IRQ时,另一个同样编号的IRQ可以出现在另一个CPU上,称为第二个CPU。这意味着,当电流处理程序在由第一个IRQ触发的CPU上运行时,还可能被再次调用。但为什么应该有两个CPU同时运行同一个IRQ处理程序呢?内核想要避免这种情况:处理程序只应在一个CPU上运行。handle_edge_irq
的开始部分必须处理这种情况。如果设置了IRQ_INPROGRESS
标志,则该IRQ在另一个CPU上已经处于处理过程中。通过设置IRQ_PENDING
标志,内核能够记录还有另一个IRQ需要在稍后处理。在屏蔽该IRQ并通过mask_ack_irq
向控制器发送一个确认后,处理过程可以放弃。因而第二个CPU可以恢复正常的工作,而第一个CPU将在稍后处理该IRQ。
请注意,如果IRQ被禁用,或没有可用的ISR处理程序,都会放弃处理。(有缺陷的硬件可能在IRQ禁用的情况下仍然生成IRQ,内核需要考虑到这种情况。)
现在,开始IRQ处理本身所涉及的工作。在用芯片相关的函数chip->ack向中断控制器发送一个确认之后,内核设置IRQ_INPROGRESS
标志。这表示IRQ正在处理过程中,可用于避免同一处理程序在多个CPU上执行。
我们假定只有一个IRQ需要处理。在这种情况下,可以通过调用handle_IRQ_event
激活高层ISR处理程序,然后可以清除IRQ_INPROGRESS
标志。但实际上的情况更为复杂,如源代码所示:
kernel/irq/chip.c
/**
* handle_edge_irq - edge type IRQ handler
* @desc: the interrupt description structure for this irq
*
* Interrupt occures on the falling and/or rising edge of a hardware
* signal. The occurrence is latched into the irq controller hardware
* and must be acked in order to be reenabled. After the ack another
* interrupt can happen on the same source even before the first one
* is handled by the associated event handler. If this happens it
* might be necessary to disable (mask) the interrupt depending on the
* controller hardware. This requires to reenable the interrupt inside
* of the loop which handles the interrupts which have arrived while
* the handler was running. If all pending interrupts are handled, the
* loop is left.
*/
void handle_edge_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (!irq_may_run(desc)) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
/*
* If its disabled or no action available then mask it and get
* out of here.
*/
if (irqd_irq_disabled(&desc->irq_data) || !desc->action) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
kstat_incr_irqs_this_cpu(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.
* 如果在处理irq时有另一个irq到达,
* 那么当时可能屏蔽了该irq。
* 解除对irq的屏蔽,如果它在此期间没有被禁用的话。
*/
if (unlikely(desc->istate & IRQS_PENDING)) {
if (!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc);
}
handle_irq_event(desc);
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data));
out_unlock:
raw_spin_unlock(&desc->lock);
}
EXPORT_SYMBOL(handle_edge_irq);
IRQ的处理是在一个循环中进行。假定我们刚好处于调用handle_IRQ_event
之后的位置上。在第一个IRQ的ISR处理程序运行时,可能同时有第二个IRQ请求发送过来,前文已经说明。这通过
IRQ_PENDING
表示。如果设置了该标志(同时该IRQ没有禁用),那么有另一个IRQ正在等待处理,循环将从头再次开始。
但在这种情况下,IRQ已经被屏蔽。因而必须用chip->unmask
解除IRQ的屏蔽,并清除IRQ_MASKED
标志。这确保在handle_IRQ_event
执行期间只能发生一个中断。在清除IRQ_PENDING
标志之后,在技术上仍然有一个待决的IRQ,但它将被立即处理,因为handle_IRQ_event
还可以处理第二个IRQ。
1.6.2.2 电平触发中断
与边沿触发中断相比,电平触发中断稍微容易处理一些。这也反映在电流处理程序handle_ level_irq
的代码流程图中,如图所示。
kernel/irq/chip.c
/**
* handle_level_irq - Level type irq handler
* @desc: the interrupt description structure for this irq
*
* Level type interrupts are active as long as the hardware line has
* the active level. This may require to mask the interrupt and unmask
* it after the associated handler has acknowledged the device, so the
* interrupt line is back to inactive.
*/
void handle_level_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc);
if (!irq_may_run(desc))
goto out_unlock;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
/*
* If its disabled or no action available
* keep it masked and get out of here
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out_unlock;
}
kstat_incr_irqs_this_cpu(desc);
handle_irq_event(desc);
cond_unmask_irq(desc);
out_unlock:
raw_spin_unlock(&desc->lock);
}
EXPORT_SYMBOL_GPL(handle_level_irq);
请注意,电平触发中断在处理时必须屏蔽,因此需要完成的第一件事就是调用mask_ack_irq
。该辅助函数屏蔽并确认IRQ,这是通过调用chip->mask_ack
,如果该方法不可用,则连续调用chip->mask
和chip->ack
。在多处理器系统上,可能发生竞态条件,尽管IRQ已经在另一个CPU上处理,但仍然在当前CPU上调用了handle_level_irq。这可以通过检查IRQ_INPROGRESS
标志来判断,这种情况下,IRQ已经在另一个CPU上处理,因而在当前CPU上可以立即放弃处理。
如果没有对该IRQ注册处理程序,也可以立即放弃处理,因为无事可做。另一个导致放弃处理的原因是设置了IRQ_DISABLED
。尽管被禁用,有问题的硬件仍然可能发出IRQ,但可以被忽略。
接下来开始对IRQ的处理。设置IRQ_INPROGRESS
,表示该IRQ正在处理中,实际工作委托给handle_IRQ_event
。这触发了高层ISR,在下文讨论。在ISR结束之后,清除IRQ_INPROGRESS
。
最后,需要解除对IRQ的屏蔽。但内核需要考虑到ISR可能禁用中断的情况,在这种情况下,ISR仍然保持屏蔽状态。否则,使用特定于芯片的函数chip->unmask
解除屏蔽。
1.6.2.3 其他中断类型
除了边沿触发和电平触发IRQ,还可能有一些不那么常见的电流类型。内核也对它们提供了默认处理程序。
-
现代IRQ硬件只需要极少的电流处理工作。只需在IRQ处理结束之后调用一个芯片相关的函数
chip->eoi
。此类型的默认处理程序是handle_fasteoi_irq
。它基本上等同于handle_level_irq
,除了只需在最后与控制器芯片交互。/** * handle_fasteoi_irq - irq handler for transparent controllers * @desc: the interrupt description structure for this irq * * Only a single callback will be issued to the chip: an ->eoi() * call when the interrupt has been serviced. This enables support * for modern forms of interrupt handlers, which handle the flow * details in hardware, transparently. */ void handle_fasteoi_irq(struct irq_desc *desc) { struct irq_chip *chip = desc->irq_data.chip; raw_spin_lock(&desc->lock); if (!irq_may_run(desc)) goto out; desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING); /* * If its disabled or no action available * then mask it and get out of here: */ if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) { desc->istate |= IRQS_PENDING; mask_irq(desc); goto out; } kstat_incr_irqs_this_cpu(desc); if (desc->istate & IRQS_ONESHOT) mask_irq(desc); preflow_handler(desc); handle_irq_event(desc); cond_unmask_eoi_irq(desc, chip); raw_spin_unlock(&desc->lock); return; out: if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED)) chip->irq_eoi(&desc->irq_data); raw_spin_unlock(&desc->lock); } EXPORT_SYMBOL_GPL(handle_fasteoi_irq);
-
非常简单,根本不需要电流控制的中断由
handle_simple_irq
管理。如果调用者想要自行处理电流,也可以使用该函数。/** * handle_simple_irq - Simple and software-decoded IRQs. * @desc: the interrupt description structure for this irq * * Simple interrupts are either sent from a demultiplexing interrupt * handler or come from hardware, where no interrupt hardware control * is necessary. * * Note: The caller is expected to handle the ack, clear, mask and * unmask issues if necessary. */ void handle_simple_irq(struct irq_desc *desc) { raw_spin_lock(&desc->lock); if (!irq_may_run(desc)) goto out_unlock; desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING); if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) { desc->istate |= IRQS_PENDING; goto out_unlock; } kstat_incr_irqs_this_cpu(desc); handle_irq_event(desc); out_unlock: raw_spin_unlock(&desc->lock); } EXPORT_SYMBOL_GPL(handle_simple_irq);
-
各CPU IRQ,即IRQ只能发送到多处理器系统的一个特定的CPU,由
handle_percpu_irq
处理。该函数在接收之后确认IRQ,在处理之后调用eoi
例程。其实现非常简单,因为不需要锁,根据定义代码只能在一个CPU上运行。/** * handle_percpu_irq - Per CPU local irq handler * @desc: the interrupt description structure for this irq * * Per CPU interrupts on SMP machines without locking requirements */ void handle_percpu_irq(struct irq_desc *desc) { struct irq_chip *chip = irq_desc_get_chip(desc); /* * PER CPU interrupts are not serialized. Do not touch * desc->tot_count. */ __kstat_incr_irqs_this_cpu(desc); if (chip->irq_ack) chip->irq_ack(&desc->irq_data); handle_irq_event_percpu(desc); if (chip->irq_eoi) chip->irq_eoi(&desc->irq_data); }
1.7 初始化和分配IRQ
本节将主要讲述如何注册和初始化IRQ。
1.7.1 注册IRQ
由设备驱动程序动态注册ISR的工作,可以使所述的数据结构非常简单地进行。
include/linux/interrupt.h
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
/**
* request_threaded_irq - allocate an interrupt line
* @irq: Interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL and thread_fn != NULL the default
* primary handler is installed
* @thread_fn: Function called from the irq handler thread
* If NULL, no irq thread is created
* @irqflags: Interrupt type flags
* @devname: An ascii name for the claiming device
* @dev_id: A cookie passed back to the handler function
*
* This call allocates interrupt resources and enables the
* interrupt line and IRQ handling. From the point this
* call is made your handler function may be invoked. Since
* your handler function must clear any interrupt the board
* raises, you must take care both to initialise your hardware
* and to set up the interrupt handler in the right order.
*
* If you want to set up a threaded irq handler for your device
* then you need to supply @handler and @thread_fn. @handler is
* still called in hard interrupt context and has to check
* whether the interrupt originates from the device. If yes it
* needs to disable the interrupt on the device and return
* IRQ_WAKE_THREAD which will wake up the handler thread and run
* @thread_fn. This split handler design is necessary to support
* shared interrupts.
*
* Dev_id must be globally unique. Normally the address of the
* device data structure is used as the cookie. Since the handler
* receives this value it makes sense to use it.
*
* If your interrupt is shared you must pass a non NULL dev_id
* as this is required when freeing the interrupt.
*
* Flags:
*
* IRQF_SHARED Interrupt is shared
* IRQF_TRIGGER_* Specify active edge(s) or level
*
*/
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
retval = __setup_irq(irq, desc, action);
if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
EXPORT_SYMBOL(request_threaded_irq);
内核首先生成一个新的irqaction
实例,然后用函数参数填充其内容。当然,其中特别重要的是处理程序函数handler
。所有进一步的工作都委托给setup_irq
函数,它将执行下列步骤。
- 如果设置了
IRQF_SAMPLE_RANDOM
,则该中断将对内核熵池有所贡献,熵池用于随机数发生器/dev/random
。rand_initialize_irq
将该IRQ添加到对应的数据结构。 - 由
request_irq
生成的irqaction
实例被添加到所属IRQ编号对应的例程链表尾部,该链表表头为irq_desc[NUM]->action
。在处理共享中断时,内核就通过这种方式来确保中断发生时调用处理程序的顺序与其注册顺序相同。 - 如果安装的处理程序是该IRQ编号对应链表中的第一个,则调用
handler->startup
初始化函数(如果没有提供显式的startup
函数,则只调用chip->enable
来启用该IRQ)。如果该IRQ此前已经安装了处理程序,则没有必要再调用该函数。 register_irq_proc
在proc文件系统中建立目录/proc/irq/NUM
。而register_handler_proc
生成proc/irq/NUM/name
。接下来,系统中就可以看到对应的IRQ通道在使用中。
1.7.2 释放IRQ
释放中断的方案,与前述过程刚好相反。首先,通过硬件相关的函数chip->shutdown
(如果没有提供显式的shutdown函数,则只调用chip->disable
禁用该中断)通知中断控制器该IRQ已经删除,接下来将相关数据项从内核的一般数据结构中删除。辅助函数free_irq
承担这些任务。在重写IRQ子系统之前它是一个体系结构相关的函数,但现在并非如此,可以在kernel/irq/manage.c
中找到该函数。
在IRQ处理程序需要删除一个共享的中断时,IRQ编号本身不足以标识该IRQ。在这种情况下,为提供唯一标识,还必须使用前面讲述的dev_id
。内核扫描所有注册的处理程序的链表,直至找到一个匹配的处理程序(dev_id匹配)。这时才能移除该项。
1.7.3 注册中断
前面讲述的机制只适用于由系统外设的中断请求所引发的中断。但内核还必须考虑由处理器本身或者用户进程中的软件机制所引发的中断。与IRQ相比,内核无需提供接口,供此类中断动态注册处理程序。这是因为,所使用的编号在初始化时就是已知的,此后不会改变。中断和异常的注册在内核初始化时进行,其分配在运行时并不改变。
平台相关的内核源代码基本上没有共同点,这并不出人意料,技术上的差别有时候还是很大的。尽管一些变体背后的概念可能是相似的,但不同平台间的具体实现差别很大。这是因为具体的实现必然要在C代码和汇编代码之间进行精细的划分,才能公平对待具体系统的相关特性。
各个平台之间最大的相似性就是文件名。arch/arch/kernel/traps.c
包含了用于中断处理程序注册的系统相关的实现。
所有实现的结果都是这样:在中断发生时自动调用对应的处理程序函数。因为系统中断不支持中断共享,只需要建立中断号和函数指针之间的关联。
通常,内核以下述两种方式之一来响应中断。
- 向当前用户进程发送一个信号,通知有错误发生。举例来说,在IA-32和AMD64系统上,除0操作通过中断0通知。自动调用的汇编语言例程
divide_error
,会向用户进程发送SIGPFE
信号。 - 内核自动修复错误,这对用户进程不可见。例如,在IA-32系统上,中断14用于表示缺页异常
1.8 处理IRQ
在注册了IRQ处理程序后,每次发生中断时将执行处理程序例程。仍然会出现如何协调不同平台差异的问题。由于事情的特定性质所致,使得差别不仅涉及平台相关实现中的各个C函数,还深入到用于底层处理、人工优化的汇编语言代码。
我们可以确定各个平台之间的几个结构上的相似性。例如,前文讨论过,各个平台上的中断操作都由3部分组成。进入路径从用户态切换到核心态,接下来执行实际的处理程序例程,最后从核心态切换回用户态。尽管涉及大量的汇编语言代码,至少有一些C代码片段在所有平台上都是相似的。
1.8.1 切换到核心态
到核心态的切换,是基于每个中断之后由处理器自动执行的汇编语言代码的。该代码的任务如上文所述。其实现可以在arch/arch/kernel/entry.S
中找到(统一的x86体系结构下,entry_32用于IA-32,而entry_64用于AMD64系统),其中通常定义了各个入口点,在中断发生时处理器可以将控制流转到这些入口点。
只有那些最为必要的操作直接在汇编语言代码中执行。内核试图尽快地返回到常规的C代码,因为C代码更容易处理。为此,必须创建一个环境,与C编译器的预期兼容。
在C语言中调用函数时,需要将所需的数据(返回地址和参数)按一定的顺序放到栈上。在用户态和核心态之间切换时,还需要将最重要的寄存器保存到栈上,以便以后恢复。这两个操作由平台相关的汇编语言代码执行。在大多数平台上,控制流接下来传递到C函数do_IRQ,其实现也是平台相关的,但情况仍然得到了很大的简化。 根据平台不同,该函数的参数或者是处理器寄存器集合:
arch/arch/kernel/irq.c
fastcall unsigned int do_IRQ(struct pt_regs regs)
或者是中断号和指向处理器寄存器集合的指针:
arch/arch/kernel/irq.c
unsigned int do_IRQ(int irq, struct pt_regs *regs)
pt_regs
用于保存内核使用的寄存器集合。各个寄存器的值被依次压栈(通过汇编语言代码),在C函数调用之前,一直保存在栈上。
pt_regs
的定义可以确保栈上的各个寄存器项与该结构的各个成员相对应。这些值并不是仅仅保存用于后续的使用,C代码也可以读取这些值。下图说明了这一点。
此外,寄存器集合也可以被复制到地址空间中栈以外的其他位置。在这种情况下,do_IRQ
的一个参数是指向pt_regs
的指针,但这并没有改变以下事实:寄存器的内存已经被保存,可以由C代码读取。
struct pt_regs
的定义是平台相关的,因为不同的处理器提供了不同的寄存器集合。pt_res中
包含了内核使用的寄存器。其中不包括的寄存器,可能只能由用户态应用程序使用。
1.8.2 IRQ栈
只有在内核使用内核栈来处理IRQ的情况下,上面描述的情形才是正确的。但不一定总是如此。IA-32体系结构提供了配置选项CONFIG_4KSTACKS
。如果启用该配置,内核栈的长度由8 KiB缩减到4 KiB。由于IA-32计算机上页面大小是4 KiB,实现内核栈所需的页数目由2个减少到1个。由于单个内存页比两个连续的内存页更容易分配(前文讨论过),在系统中有大量活动进程(或线程)时,这使得虚拟内存子系统的工作会稍微容易些。遗憾的是,对常规的内核工作以及IRQ处理例程所需的空间来说,4 KiB并不总是够用,因而引入了另外两个栈:
- 用于硬件IRQ处理的栈。
- 用于软件IRQ处理的栈。
常规的内核栈对每个进程都会分配,而这两个额外的栈是针对各CPU分别分配的。在硬件中断发生时(或处理软中断时),内核需要切换到适当的栈。
下列数组提供了指向额外的栈的指针:
arch/x86/kernel/irq_32.c
static union irq_ctx *hardirq_ctx[NR_CPUS] __read_mostly;
static union irq_ctx *softirq_ctx[NR_CPUS] __read_mostly;
请注意,属性__read_mostly
不是指栈本身,而是指这里的指针,也就是栈在内存中的地址。只有在最初分配这些栈时才会操作这些指针,而后在系统运行期间都只是读取。
用作栈的数据结构并不复杂:
arch/x86/kernel/irq_32.c
union irq_ctx {
struct thread_info tinfo;
u32 stack[THREAD_SIZE/sizeof(u32)];
};
tinfo
用于存储中断发生之前所运行线程的有关信息(更多细节请参见第2章)。stack
提供了栈空间。如果启用了4 KiB栈,则THREAD_SIZE
定义为4 096,这确保了所要求的栈长度。请注意,由于使用了一个union来合并tinfo
和stack[]
,该数据结构刚好能够放在一个页帧中。这也意味着,tinfo
中包含的线程信息,在栈上总是可用的。
1.8.3 调用电流处理程序例程
1.8.3.1 AMD64系统上的处理
这里首先讲述AMD64系统上do_IRQ的实现。与IA-32相比,这个函数变体要稍微简单些,而许多其他现代的体系结构也采用了类似的方法。
该函数的原型如下:
/*
* do_IRQ handles all normal device IRQ's (the special
* SMP cross-CPU interrupts have their own specific
* handlers).
*/
void __irq_entry do_IRQ(unsigned int irq)
{
irq_enter();
check_stack_overflow();
generic_handle_irq(irq);
irq_exit();
}
底层汇编程序代码负责将寄存器集合的当前状态传递到该函数,do_IRQ
的第一项任务是使用set_irq_regs
将一个指向寄存器集合的指针保存在一个全局的各CPU变量中(中断发生之前,变量中保存的旧指针会保留下来,供后续使用)。需要访问寄存器集合的中断处理程序,可以从该变量中访问。
接下来irq_enter
负责更新一些统计量。对于具备动态时钟周期特性的系统,如果系统已经有很长一段时间没有发生时钟中断,则更新全局计时变量jiffies
。接下来,调用对所述IRQ注册的ISR的任务委托给体系结构无关的函数generic_handle_irq
,它调用irq_desc[irq]->handle_irq
来激活电流控制处理程序。
接下来irq_exit
负责记录一些统计量,另外还要调用(假定内核此时已经不再处于中断状态,即此前处理的不是嵌套中断)do_softirq
来处理任何待决的软件IRQ。
最后,再次调用set_irq_regs
,将指向struct pt_regs
的指针恢复到上一次调用之前的值。这确保嵌套的处理程序能够正确工作。
1.8.4 调用高层ISR
回想上文可知,不同的电流处理程序例程都有一个共同点:采用handle_IRQ_event
来激活与特定IRQ相关的高层ISR。现在需要更仔细地考察这个函数。该函数需要IRQ编号和操作链作为参数:
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;
record_irq_time(desc);
for_each_action_of_desc(desc, action) {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
#if defined(CONFIG_CPU_LOONGSON3)
__asm__ __volatile__(
" .set push \n"
" .set noreorder \n"
" move $a0, %[irq] \n"
" move $a1, %[dev_id] \n"
" jalr.hb %[func] \n"
" nop \n"
" move %[res], $v0 \n"
" .set pop \n"
:[res] "=r"(res)
:[irq]"r"(irq), [dev_id]"r"(action->dev_id), [func]"r"(action->handler)
: "v0", "a0", "a1", "ra");
#else
res = action->handler(irq, action->dev_id);
#endif
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action);
/* Fall through to add to randomness */
case IRQ_HANDLED:
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
- 如果第一个处理程序函数中没有设置
IRQF_DISABLED
,则用local_irq_enable_in_hardirq
启用(当前CPU的)中断。换句话说,该处理程序可以被其他IRQ中断。但根据电流类型,也可能一直屏蔽刚处理的IRQ。 - 逐一调用所注册的IRQ处理程序的
action
函数。 - 如果对该IRQ设置了
IRQF_SAMPLE_RANDOM
,则调用add_interrupt_randomness
,将事件的时间作为熵池的一个源(如果中断的发生是随机的,那么它们是理想的源)。 local_irq_disable
禁用中断。因为中断的启用和禁用是不嵌套的,与中断在处理开始时是否启用是不相关的。handle_IRQ_event
在调用时禁用中断,在退出时仍然预期禁用中断。
在共享IRQ时,内核无法找出引发中断请求的设备。该工作完全留给处理程序例程,其中将使用设备相关的寄存器或其他硬件特征来查找中断来源。未受影响的例程也需要识别出该中断并非来自于相关设备,应该尽快将控制返回。但处理程序例程也无法向高层代码报告该中断是否是针对它的。内核总是依次执行所有处理程序例程,而不考虑实际上哪个处理程序与该中断相关。
但内核总可以检查是否有负责该IRQ的处理程序。irqreturn_t
定义为处理程序函数的返回类型,它只是一个简单的整型变量。可以接收IRQ_NONE或IRQ_HANDLED两个值,这取决于处理程序是否处理了该IRQ。
在执行所有处理程序例程期间,内核将返回结果用逻辑“或”操作合并起来。内核最后可以据此判断IRQ是否被处理。
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
1.8.5 实现处理程序例程
在实现处理程序例程时,必须要注意一些要点。这些会极大地影响系统的性能和稳定性。
1.8.5.1 限制
在实现ISR时,主要的问题是它们在所谓的中断上下文(interrupt context)中执行。内核代码有时在常规上下文运行,有时在中断上下文运行。为区分这两种不同情况并据此设计代码,内核提供了in_interrupt
函数,用于指明当前是否在处理中断。
中断上下文与普通上下文的不同之处主要有如下3点:
- 中断是异步执行的。换句话说,它们可以在任何时间发生。因而从用户空间来看,处理程序例程并不是在一个明确定义的环境中执行。这种环境下,禁止访问用户空间,特别是与用户空间地址之间来回复制内存数据的行为。
例如,对网络驱动程序来说,不能将接收的数据直接转发到等待的应用程序。毕竟,内核无法确定等待数据的应用程序此时是否在运行(事实上,这种可能性很低)。 - 中断上下文中不能调用调度器。因而不能自愿地放弃控制权。
- 处理程序例程不能进入睡眠状态。只有在外部事件导致状态改变并唤醒进程时,才能解除睡眠状态。但中断上下文中不允许中断,进程睡眠后,内核只能永远等待下去(这里没有进程,是内核和当前处理器永远等待)。因为也不能调用调度器,不能选择进程来执行。
当然,只确保处理程序例程的直接代码不进入睡眠状态,这是不够的。其中调用的所有过程和函数(以及被这些函数/过程调用的函数/过程,依此类推)都不能进入睡眠状态。对此进行的检查并不简单,必须非常谨慎,特别是在控制路径存在大量分支时。
1.8.5.2 实现处理程序
回想前文,ISR函数的原型是由irq_handler_t
指定的。由于前文没有给出这个typedef
的实际定义,这里先给出其定义:
<interrupt.h>
typedef irqreturn_t (*irq_handler_t)(int, void *);
irq指定了IRQ编号,dev_id
是注册处理程序时传递的设备ID。irqreturn_t
是另一个typedef
,实际上只是整数。
中断处理程序显然是所谓的“热”代码路径,耗费的处理时间是非常关键的。尽管大多数处理程序都不需要寄存器状态,但仍然需要花费时间和栈空间来向每个ISR传递一个指针。因而从原型删除该指针是个好主意。
需要访问寄存器集合的处理程序也仍然可以访问。内核定义了一个全局的各CPU数组来保存寄存器集合,而include/asm-generic/irq_regs.h
中的get_irq_regs
可用于获取一个指向pt_regs
实例的指针。该实例包含了切换到核心态时活动的寄存器集合的状态。普通的设备驱动程序不使用该信息,但有时候对调试内核问题有用。
再次强调,中断处理程序只能使用两种返回值:如果正确地处理了IRQ则返回IRQ_HANDLED
,如果ISR不负责该IRQ则返回IRQ_NONE
。
处理程序例程的任务是什么?为处理共享中断,例程首先必须检查IRQ是否是针对该例程的。如果相关的外部设备设计得比较现代,那么硬件会提供一个简单的方法来执行该检查,通常是通过一个专门的设备寄存器。如果是该设备引起中断,则寄存器值设置为1。在这种情况下,处理程序例程必须将设备寄存器恢复默认值(通常是0),接下来开始正常的中断处理。如果例程发现设备寄存器值为0,它可以确信所管理的设备不是中断源,因而可以将控制返回到高层代码。
如果设备没有此类状态寄存器,还在使用手工轮询的方案。每次发生一个中断时,处理程序都检查相关设备是否有数据可用。倘若如此,则处理数据。否则,例程结束。
当然,可能有一个处理程序例程同时负责多个设备的情况,例如同一类型的两块网卡。如果收到一个IRQ,则对两块卡执行同样的代码,因为两个处理程序函数指向内核代码中同一位置。如果两个设备使用不同的IRQ编号,那么处理程序例程可以区分二者。如果二者共享同一个IRQ,仍然可以根据设备相关的dev_id
字段来唯一地标识各个卡。
2. 软中断
软中断使得内核可以延期执行任务。因为它们的运作方式与上文描述的中断类似,但完全是用软件实现的,所以称为软中断(software interrupt)或softIRQ是完全符合逻辑的。
内核借助于软中断来获知异常情况的发生,而该情况将在稍后由专门的处理程序例程解决。如上所述,内核在do_IRQ末尾处理所有待决软中断,因而可以确保软中断能够定期得到处理。
从一个更抽象的角度来看,可以将软中断描述为一种延迟到稍后时刻执行的内核活动。但尽管硬件和软件中断之间有明显的相似性,它们并不总是可比较的。
软中断机制的核心部分是一个表,包含32个softirq_action
类型的数据项。该数据类型结构非常简单,只包含两个成员:
<interrupt.h>
struct softirq_action
{
void (*action)(struct softirq_action *);
};
其中action
是一个指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程。
该数据结构的定义是体系结构无关的,而软中断机制的整个实现也是如此。除了处理的激活之外,没有利用处理器相关的功能或特性,这与普通的中断是完全相反的。
软中断必须先注册,然后内核才能执行软中断。open_softirq
函数即用于该目的。它在softirq_ vec表中指定的位置写入新的软中断:
kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
各个软中断都有一个唯一的编号,这表明软中断是相对稀缺的资源,使用其必须谨慎,不能由各种设备驱动程序和内核组件随意使用。默认情况下,系统上只能使用32个软中断。但这个限制不会有太大的局限性,因为软中断充当实现其他延期执行机制的基础,而且也很适合设备驱动程序的需要。
只有中枢的内核代码才使用软中断。软中断只用于少数场合,这些都是相对重要的情况:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
其中两个用来实现tasklet
(HI_SOFTIRQ
、TASKLET_SOFTIRQ
),两个用于网络的发送和接收操作(NET_TX_SOFTIRQ
和NET_RX_SOFTIRQ
,这是软中断机制的来源和其最重要的应用),一个用于块层,实现异步请求完成(BLOCK_SOFTIRQ
),一个用于调度器(SCHED_SOFTIRQ
),以实现SMP系统上周期性的负载均衡。在启用高分辨率定时器时,还需要一个软中断(HRTIMER_SOFTIRQ
)。
软中断的编号形成了一个优先顺序,这并不影响各个处理程序例程执行的频率或它们相当于其他系统活动的优先级,但定义了多个软中断同时活动或待决时处理例程执行的次序。
raise_softirq(int nr)
用于引发一个软中断(类似普通中断)。
软中断的编号通过参数指定。该函数设置各CPU变量irq_stat[smp_processor_id].__softirq_pending
中的对应比特位。该函数将相应的软中断标记为执行,但这个执行是延期执行。通过使用特定于处理器的位图,内核确保几个软中断(甚至是相同的)可以同时在不同的CPU上执行。
如果不在中断上下文调用raise_softirq
,则调用wakeup_softirqd
来唤醒软中断守护进程,这是开启软中断处理的两个可选方法之一。
2.1 开启软中断处理
有几种方法可开启软中断处理,但这些都归结为调用do_softirq
函数。
./kernel/softirq.c
asmlinkage __visible void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending && !ksoftirqd_running(pending))
do_softirq_own_stack();
local_irq_restore(flags);
}
该函数首先确认当前不处于中断上下文中(当然,即不涉及硬件中断)。如果处于中断上下文,则立即结束。因为软中断用于执行ISR中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用。
通过local_softirq_pending
,确定当前CPU软中断位图中所有置位的比特位。如果有软中断等待处理,则调用__do_softirq
。
该函数将原来的位图重置为0。换句话说,清除所有软中断。这两个操作都是在(当前处理器上)禁用中断的情况下执行,以防其他进程对位图的修改造成干扰。而后续代码是在允许中断的情况下执行。这使得在软中断处理程序执行期间的任何时刻,都可以修改原来的位图。
softirq_vec
中的action
函数在一个while
循环中针对各个待决的软中断被调用。
在处理了所有标记出的软中断之后,内核检查在此期间是否有新的软中断标记到位图中。要求在前一轮循环中至少有一个没有处理的软中断,而重启的次数没有超过MAX_SOFTIRQ_RESTART
(通常设置为10)。如果是这样,则再次按序处理标记的软中断。这操作会一直重复下去,直至在执行所有处理程序之后没有新的未处理软中断为止。
如果在MAX_SOFTIRQ_RESTART
次重启处理过程之后,仍然有未处理的软中断,那么应该如何?内核将调用wakeup_softirqd
唤醒软中断守护进程。
2.2 软中断守护进程
软中断守护进程的任务是,与其余内核代码异步执行软中断。为此,系统中的每个处理器都分配了自身的守护进程,名为ksoftirqd
。
/*
* If ksoftirqd is scheduled, we do not want to process pending softirqs
* right now. Let ksoftirqd handle this at its own rate, to get fairness,
* unless we're doing some of the synchronous softirqs.
*/
#define SOFTIRQ_NOW_MASK ((1 << HI_SOFTIRQ) | (1 << TASKLET_SOFTIRQ))
static bool ksoftirqd_running(unsigned long pending)
{
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (pending & SOFTIRQ_NOW_MASK)
return false;
return tsk && (tsk->state == TASK_RUNNING);
}
内核中有两处调用wakeup_softirqd
唤醒了该守护进程。
- 在
do_softirq
中,如前所述。 - 在
raise_softirq_irqoff
末尾。该函数由raise_softirq
在内部调用,如果内核当前停用了中断,也可以直接使用。
唤醒函数本身只需要几行代码。首先,借助于一些宏,从一个各CPU变量读取指向当前CPU软中断守护进程的task_struct
的指针。如果该进程当前的状态不是TASK_RUNNING
,则通过wake_up_ process
将其放置到就绪进程的列表末尾。尽管这并不会立即开始处理所有待决软中断,但只要调度器没有更好的选择,就会选择该守护进程(优先级为19)来执行。
在系统启动时用initcall
机制调用init不久,即创建了系统中的软中断守护进程。在初始化之后,各个守护进程都执行以下无限循环(如果软中断守护进程显式停止,则kthread_should_stop()
返回true
。但是这只发生在一个CPU从系统移除的情
况下)。
每次被唤醒时,守护进程首先检查是否有标记出的待决软中断,否则明确地调用调度器,将控制转交到其他进程。
如果有标记出的软中断,那么守护进程接下来将处理软中断。进程在一个while
循环中重复调用两个函数do_softirq
和cond_resched
,直至没有标记出的软中断为止。cond_resched
确保在对当前进程设置了TIF_NEED_RESCHED
标志的情况下调用调度器。这是可能的,因为所有这些函数执行时都启用了硬件中断。
3. tasklet
软中断是将操作推迟到未来时刻执行的最有效的方法。但该延期机制处理起来非常复杂。因为多个处理器可以同时且独立地处理软中断,同一个软中断的处理程序例程可以在几个CPU上同时运行。对软中断的效率来说,这是一个关键,多处理器系统上的网络实现显然受惠于此。但处理程序例程的设计必须是完全可重入且线程安全的。另外,临界区必须用自旋锁保护(或其他IPC机制),而这需要大量审慎的考虑。
tasklet和工作队列是延期执行工作的机制,其实现基于软中断,但它们更易于使用,因而更适合于设备驱动程序(以及其他一般性的内核代码)。
在深入技术细节之前,请注意所使用的术语:由于历史原因,术语下半部(bottom half)通常指代两个不同的东西;首先,它是指ISR代码的下半部,负责执行非时间关键操作。遗憾的是,早期内核版本中使用的操作延期执行机制,也称为下半部因而使用的术语经常是含糊不清的。在此期间,下半部不再作为内核机制存在。它们在内核版本2.5开发期间被废弃,被tasklet代替,这是一个好得多的替代品。
tasklet
是“小进程”,执行一些迷你任务,对这些任务使用全功能进程可能比较浪费。
3.1 创建tasklet
不出所料,各个tasklet的中枢数据结构称作tasklet_struct,定义如下:
<interrupt.h>
/* Tasklets --- multithreaded analogue of BHs.
Main feature differing them of generic softirqs: tasklet
is running only on one CPU simultaneously.
Main feature differing them of BHs: different tasklets
may be run simultaneously on different CPUs.
Properties:
* If tasklet_schedule() is called, then tasklet is guaranteed
to be executed on some cpu at least once after this.
* If the tasklet is already scheduled, but its execution is still not
started, it will be executed only once.
* If this tasklet is already running on another CPU (or schedule is called
from tasklet itself), it is rescheduled for later.
* Tasklet is strictly serialized wrt itself, but not
wrt another tasklets. If client needs some intertask synchronization,
he makes it with spinlocks.
*/
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
从设备驱动程序的角度来看,最重要的成员是func
。它指向一个函数的地址,该函数的执行将被延期。data
用作该函数执行时的参数。
next
是一个指针,用于建立tasklet_struct
实例的链表。这容许几个任务排队执行。
state
表示任务的当前状态,类似于真正的进程。但只有两个选项,分别由state
中的一个比特位表示,这也是二者可以独立设置/清除的原因。
- 在
tasklet
注册到内核,等待调度执行时,将设置TASKLET_STATE_SCHED
。 TASKLET_STATE_RUN
表示tasklet
当前正在执行。
第二个状态只在SMP系统上有用。用于保护tasklet
在多个处理器上并行执行。
原子计数器count
用于禁用已经调度的tasklet
。如果其值不等于0,在接下来执行所有待决的tasklet
时,将忽略对应的tasklet
。
3.2 注册tasklet
tasklet_schedule
将一个tasklet
注册到系统中:
<interrupt.h>
static inline void tasklet_schedule(struct tasklet_struct *t);
如果设置了TASKLET_STATE_SCHED
标志位,则结束注册过程,因为该tasklet
此前已经注册了。否则,将该tasklet
置于一个链表的起始,其表头是特定于CPU的变量tasklet_vec
。该链表包含了所有注册的tasklet
,使用next
成员作为链表元素。
在注册了一个tasklet
之后,tasklet
链表即标记为即将进行处理。
3.3 执行tasklet
tasklet
的生命周期中最重要的部分就是其执行。因为tasklet
基于软中断实现,它们总是在处理软中断时执行。
tasklet
关联到TASKLET_SOFTIRQ
软中断。因而,调用raise_softirq(TASKLET_SOFTIRQ)
,即可在下一个适当的时机执行当前处理器的tasklet
。内核使用tasklet_action
作为该软中断的action
函数。
该函数首先确定特定于CPU的链表,其中保存了标记为将要执行的各个tasklet
。它接下来将表头重定向到函数局部的一个数据项,相当于从外部公开的链表删除了所有表项。接下来,函数在以下循环中逐一处理各个tasklet
:
kernel/softirq.c
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr)
{
struct tasklet_struct *list;
local_irq_disable();
list = tl_head->head;
tl_head->head = NULL;
tl_head->tail = &tl_head->head;
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr);
local_irq_enable();
}
}
在while循环中执行tasklet
,类似于处理软中断使用的机制。
因为一个tasklet
只能在一个处理器上执行一次,但其他的tasklet
可以并行运行,所以需要特定于tasklet
的锁。 state
状态用作锁变量。在执行一个 tasklet
的处理程序函数之前,内核使用tasklet_trylock
检查tasklet
的状态是否TASKLET_STATE_RUN
。换句话说,它是否已经在系统的另一个处理器上运行:
#ifdef CONFIG_SMP
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
#else
#define tasklet_trylock(t) 1
#endif
如果对应比特位尚未设置,则设置该比特位。
如果count
成员不等于0,则该tasklet
已经停用。在这种情况下,不执行相关的代码。
在两项检查都成功通过之后,内核用对应的参数执行tasklet的处理程序函数,即调用t->func(t->data)
。最后,使用tasklet_unlock
清除tasklet
的TASKLET_SCHED_RUN
标志位。
如果在 tasklet
执行期间,有新的 tasklet
进入当前处理器的 tasklet
队列,则会尽快引发TASKLET_SOFTIRQ
软中断来执行新的tasklet
。(因为我们对完成该工作的代码不是特别感兴趣,所以在上文中没有给出该代码。)
除了普通的tasklet
之外,内核还使用了另一种tasklet
,它具有“较高”的优先级。除以下修改之外,其实现与普通的tasklet
完全相同。
- 使用
HI_SOFTIRQ
作为软中断,而不是TASKLET_SOFTIRQ
,相关的action
函数是tasklet_hi_action
。 - 注册的
tasklet
在CPU相关的变量tasklet_hi_vec
中排队。这是使用tasklet_hi_schedule
完成的。
在这里,“较高优先级”是指该软中断的处理程序HI_SOFTIRQ
在所有其他处理程序之前执行,尤其是在构成了软中断活动主体的网络处理程序之前执行。
当前,大部分声卡驱动程序都利用了这一选项,因为操作延迟时间太长可能损害音频输出的音质。而用于高速传输的网卡也可以得益于该机制。
4. 等待队列和完成量
等待队列(wait queue)用于使进程等待某一特定事件发生,而无须频繁轮询。进程在等待期间睡眠,在事件发生时由内核自动唤醒。
完成量(completion)机制基于等待队列,内核利用该机制等待某一操作结束。这两种机制使用得都比较频繁,主要用于设备驱动程序
4.1 等待队列
4.1.1 数据结构
每个等待队列都有一个队列头,由以下数据结构表示:
./include/linux/wait.h
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
因为等待队列也可以在中断时修改,在操作队列之前必须获得一个自旋锁lock
。task_list
是一个双链表,用于实现双链表最擅长表示的结构,即队列。
队列中的成员是以下数据结构的实例:
/*
* A single wait-queue entry structure:
*/
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
typedef struct wait_queue_entry wait_queue_entry_t;
flags
的值或者为WQ_FLAG_EXCLUSIVE
,或者为0
,当前没有定义其他标志。WQ_FLAG_EXCLUSIVE
表示等待进程想要被独占地唤醒private
是一个指针,指向等待进程的task_struct
实例。该变量本质上可以指向任意的私有数据,但内核中只有很少情况下才这么用,因此这里不会详细讲述这种情形。- 调用
func
,唤醒等待进程。 task_list
用作一个链表元素,用于将wait_queue_t
实例放置到等待队列中。
等待队列的使用分为如下两部分。
- 为使当前进程在一个等待队列中睡眠,需要调用
wait_event
函数(或某个等价函数,在下文讨论)。进程进入睡眠,将控制权释放给调度器。
内核通常会在向块设备发出传输数据的请求后,调用该函数。因为传输不会立即发生,而在此期间又没有其他事情可做,所以进程可以睡眠,将CPU时间让给系统中的其他进程。 - 在内核中另一处,就我们的例子而言,是来自块设备的数据到达后,必须调用
wake_up
函数(或某个等价函数,将在下文讨论)来唤醒等待队列中的睡眠进程。
在使用
wait_event
使进程睡眠之后,必须确保在内核中另一处有一个对应的wake_up
调用。
4.1.2 使进程睡眠
add_wait_queue
函数用于将一个进程增加到等待队列,该函数在获得必要的自旋锁后,将工作委托给__add_wait_queue
:
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
list_add(&wq_entry->entry, &wq_head->head);
}
在将新进程统计到等待队列时,除了使用标准的list_add
链表函数,没有其他工作需要做。
内核还提供了add_wait_queue_exclusive
函数。它的工作方式与add_wait_queue
相同,但将进程插入在队列尾部,并将其标志设置为WQ_EXCLUSIVE
(该标志的语义在下文讨论)。
使进程在等待队列上睡眠的另一种方法是prepare_to_wait
。除了add_wait_queue
需要的参数之外,还需要进程的状态:
/*
* Note: we use "set_current_state()" _after_ the wait-queue add,
* because we need a memory barrier there on SMP, so that any
* wake-function that tests for the wait-queue being active
* will be guaranteed to see waitqueue addition _or_ subsequent
* tests in this thread will see the wakeup having taken place.
*
* The spin_unlock() itself is semi-permeable and only protects
* one way (it only protects stuff inside the critical region and
* stops them from bleeding out - it would still allow subsequent
* loads to move into the critical region).
*/
void
prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
if (list_empty(&wq_entry->entry))
__add_wait_queue(wq_head, wq_entry);
set_current_state(state);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(prepare_to_wait);
像在上文讨论的那样,调用__add_wait_queue
之后,内核将进程当前的状态设置为传递到prepare_to_wait
的状态。
prepare_to_wait_exclusive
是一个变体,它会设置WQ_FLAG_EXCLUSIVE
标志并将等待队列的成员添加到队列尾部。
下面两个标准方法可用于初始化一个等待队列项。
init_waitqueue_entry
初始化一个动态分配的wait_queue_t
实例:static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p) { wq_entry->flags = 0; wq_entry->private = p; wq_entry->func = default_wake_function; }
default_wake_function
只是一个进行参数转换的前端,试图用进程调度和管理中描述的try_to_wake_up
函数来唤醒进程。DEFINE_WAIT
创建wait_queue_t
的静态实例,它可以自动初始化:
这里用#define DEFINE_WAIT_FUNC(name, function) \ struct wait_queue_entry name = { \ .private = current, \ .func = function, \ .entry = LIST_HEAD_INIT((name).entry), \ } #define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
autoremove_wake_function
来唤醒进程。该函数不仅调用default_wake_function
,还将所属等待队列成员从等待队列删除。
add_wait_queue
通常不直接使用。更常用的是wait_event。这是一个宏,需要如下两个参数。- 在其上进行等待的等待队列。
- 一个条件,以所等待事件有关的一个C表达式形式给出。
这个宏只确认条件尚未满足。如果条件已经满足,可以立即停止处理,因为没什么可等待的了。
主要的工作委托给__wait_event
:
/*
* The below macro ___wait_event() has an explicit shadow of the __ret
* variable when used from the wait_event_*() macros.
*
* This is so that both can use the ___wait_cond_timeout() construct
* to wrap the condition.
*
* The type inconsistency of the wait_event_*() __ret variable is also
* on purpose; we use long where we can return timeout values and int
* otherwise.
*/
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
for (;;) { \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
if (condition) \
break; \
\
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
cmd; \
} \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
在用DEFINE_WAIT
建立等待队列成员之后,这个宏产生了一个无限循环。使用prepare_to_wait
使进程在等待队列上睡眠。每次进程被唤醒时,内核都会检查指定的条件是否满足,如果条件满足则退出无限循环。否则,将控制转交给调度器,进程再次睡眠。
很重要的一点是,wait_event
和__wait_event
都实现为宏,这可以用标准C表达式来指定条件。由于C语言不支持任何像高阶函数之类的时髦特性,如果使用常规的函数,这种行为是不可能的(至少会非常笨拙)。
在条件满足时,finish_wait
将进程状态设置回TASK_RUNNING
,并从等待队列的链表移除对应的项(但由于finished_wait
可能从许多处调用,此处需谨慎,以防进程已经被唤醒函数从队列移除。内核设法谨慎地操作链表的元素,保证一切都正确运作)。
除了wait_event
之外,内核还定义了其他几个函数,可以将当前进程置于等待队列中。其实现实际上等同于sleep_on
:
#define wait_event_interruptible(wq, condition)
#define wait_event_timeout(wq, condition, timeout) { ... }
#define wait_event_interruptible_timeout(wq, condition, timeout)
wait_event_interruptible
使用的进程状态为TASK_INTERRUPTIBLE
。因而睡眠进程可以通过接收信号而唤醒。wait_event_timeout
等待满足指定的条件,但如果等待时间超过了指定的超时限制(按jiffies
指定)则停止。这防止了进程永远睡眠。wait_event_interruptible_timeout
使进程睡眠,但可以通过接收信号唤醒。它也注册了一个超时限制。从内核采用的命名方式来看,一般不会有出人意料之处!
此外,内核还定义了若干废弃的函数(sleep_on
、sleep_on_timeout
、interruptible_sleep_on
和interruptible_sleep_on_timeout
),这些不应该在新的代码中继续使用。保留这些函数,主要是出于兼容性的目的。
4.1.3 唤醒进程
内核定义了一系列宏,可用于唤醒等待队列中的进程。它们基于同一个函数:
#define wake_up(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
在获得了用于保护等待队列首部的锁之后,_wake_up
将工作委托给_wake_up_common
。
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
{
wait_queue_entry_t *curr, *next;
int cnt = 0;
lockdep_assert_held(&wq_head->lock);
if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
curr = list_next_entry(bookmark, entry);
list_del(&bookmark->entry);
bookmark->flags = 0;
} else
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
if (&curr->entry == &wq_head->head)
return nr_exclusive;
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
int ret;
if (flags & WQ_FLAG_BOOKMARK)
continue;
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0)
break;
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
(&next->entry != &wq_head->head)) {
bookmark->flags = WQ_FLAG_BOOKMARK;
list_add_tail(&bookmark->entry, &next->entry);
break;
}
}
return nr_exclusive;
}
wq_head
用于选定等待队列mode
指定进程的状态,用于控制唤醒进程的条件nr_exclusive
表示将要唤醒的设置了WQ_FLAG_EXCLUSIVE
标志的进程的数目。
内核接下来遍历睡眠进程,并调用其唤醒函数func
:
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
...
ret = curr->func(curr, mode, wake_flags, key);
这里会反复扫描链表,直至没有更多进程需要唤醒,或已经唤醒的独占进程的数目达到了nr_exclusive
。该限制用于避免所谓的惊群(thundering herd)问题。如果几个进程在等待独占访问某一资源,那么同时唤醒所有等待进程是没有意义的,因为除了其中一个之外,其他进程都会再次睡眠。 nr_exclusive
推广了这一限制。
最常使用的wake_up
函数将nr_exclusive
设置为1
,确保只唤醒一个独占访问的进程。
回想上文,WQ_FLAG_EXCLUSIVE
进程被添加在等待队列的尾部。这种实现确保在混合访问类型的队列中,首先唤醒所有的普通进程,然后才考虑到对独占进程的限制。
如果进程在等待数据传输的结束,那么唤醒等待队列中所有的进程是有用的。这是因为几个进程的数据可以同时读取,而互不干扰。
4.2 完成量
完成量与信号量有些相似,但是基于等待队列实现的。我们感兴趣的是完成量的接口。场景中有两个参与者:一个在等待某操作完成,而另一个在操作完成时发出声明。实际上,这已经被简化过了:可以有任意数目的进程等待操作完成。为表示进程等待的即将完成的“某操作”,内核使用了下述数据结构:
/*
* struct completion - structure used to maintain state for a "completion"
*
* This is the opaque structure used to maintain the state for a "completion".
* Completions currently use a FIFO to queue threads that have to wait for
* the "completion" event.
*
* See also: complete(), wait_for_completion() (and friends _timeout,
* _interruptible, _interruptible_timeout, and _killable), init_completion(),
* reinit_completion(), and macros DECLARE_COMPLETION(),
* DECLARE_COMPLETION_ONSTACK().
*/
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
可能在某些进程开始等待之前,事件就已经完成,done
用来处理这种情形。这将在下文讨论。wait
是一个标准的等待队列,等待进程在队列上睡眠。
init_completion
初始化一个动态分配的completion
实例,而DECLARE_COMPLETION
宏用来建立该数据结构的静态实例。
/**
* init_completion - Initialize a dynamically allocated completion
* @x: pointer to completion structure that is to be initialized
*
* This inline function will initialize a dynamically created completion
* structure.
*/
static inline void __init_completion(struct completion *x)
{
x->done = 0;
init_waitqueue_head(&x->wait);
}
进程可以用wait_for_completion
添加到等待队列,进程在其中等待(以独占睡眠状态),直至请求被内核的某些部分处理。这函数需要一个completion
实例作为参数:
/**
* wait_for_completion: - waits for completion of a task
* @x: holds the state of this particular completion
*
* This waits to be signaled for completion of a specific task. It is NOT
* interruptible and there is no timeout.
*
* See also similar routines (i.e. wait_for_completion_timeout()) with timeout
* and interrupt capability. Also see complete().
*/
void __sched wait_for_completion(struct completion *x)
{
wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}
EXPORT_SYMBOL(wait_for_completion);
void wait_for_completion(struct completion *);
int wait_for_completion_interruptible(struct completion *x);
unsigned long wait_for_completion_timeout(struct completion *x,
unsigned long timeout);
unsigned long wait_for_completion_interruptible_timeout(
struct completion *x, unsigned long timeout);
此外还提供了如下几个改进过的变体。
- 通常进程在等待事件的完成时处于不可中断状态,但如果使用
wait_for_completion_interruptible
,可以改变这一设置。如果进程被中断,该函数返回-ERESTARTSYS
,否则返回0
。 wait_for_completion_timeout
等待一个完成事件发生,但提供了超时设置(以jiffies
为单位),如果等待时间超出了这一设置,则取消等待。这有助于防止无限等待某一事件。如果在超时之前事件已经完成,则函数返回剩余的时间,否则返回0
。wait_for_completion_interruptible_timeout
是前两种变体的组合。
在请求由内核的另一部分处理之后,必须调用complete
或complete_all
来唤醒等待的进程。因为每次调用只能从完成量的等待队列移除一个进程,对n个等待进程来说,必须调用该函数n次。另一方面,complete_all
将唤醒所有等待该完成的进程。 complete_and_exit
是一个小的包装器,首先调用complete
,接下来调用do_exit
结束内核线程。
/**
* complete: - signals a single thread waiting on this completion
* @x: holds the state of this particular completion
*
* This will wake up a single thread waiting on this completion. Threads will be
* awakened in the same order in which they were queued.
*
* See also complete_all(), wait_for_completion() and related routines.
*
* If this function wakes up a task, it executes a full memory barrier before
* accessing the task state.
*/
void complete(struct completion *x)
{
unsigned long flags;
spin_lock_irqsave(&x->wait.lock, flags);
if (x->done != UINT_MAX)
x->done++;
__wake_up_locked(&x->wait, TASK_NORMAL, 1);
spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete);
/**
* complete_all: - signals all threads waiting on this completion
* @x: holds the state of this particular completion
*
* This will wake up all threads waiting on this particular completion event.
*
* If this function wakes up a task, it executes a full memory barrier before
* accessing the task state.
*
* Since complete_all() sets the completion of @x permanently to done
* to allow multiple waiters to finish, a call to reinit_completion()
* must be used on @x if @x is to be used again. The code must make
* sure that all waiters have woken and finished before reinitializing
* @x. Also note that the function completion_done() can not be used
* to know if there are still waiters after complete_all() has been called.
*/
void complete_all(struct completion *x)
{
unsigned long flags;
spin_lock_irqsave(&x->wait.lock, flags);
x->done = UINT_MAX;
__wake_up_locked(&x->wait, TASK_NORMAL, 0);
spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete_all);
void complete_and_exit(struct completion *comp, long code)
{
if (comp)
complete(comp);
do_exit(code);
}
EXPORT_SYMBOL(complete_and_exit);
complete
、complete_all
和complete_and_exit
需要一个指向struct completion
实例的指针作为参数,标识所述的完成量。
struct completion
中done
的语义是什么呢?每次调用complete
时,该计数器都加1
,仅当done
等于0
时,wait_for
系列函数才会使调用进程进入睡眠。实际上,这意味着进程无须等待已经完成的事件。complete_all
的工作方式类似,但它会将计数器设置为最大可能值(UINT_MAX/2
,这是无符号整数最大值的一半,因为计数器也可能取负值),这样,在事件完成后调用wait_
系列函数的进程将永远不会睡眠。
4.3 工作队列
工作队列是将操作延期执行的另一种手段。因为它们是通过守护进程在用户上下文执行,函数可以睡眠任意长的时间,这与内核是无关的。在内核版本2.5开发期间,设计了工作队列,用以替换此前使用的keventd
机制。
每个工作队列都有一个数组,数组项的数目与系统中处理器的数目相同。每个数组项都列出了将延期执行的任务。
对每个工作队列来说,内核都会创建一个新的内核守护进程,延期任务使用上文描述的等待队列机制,在该守护进程的上下文中执行。
新的工作队列通过调用create_workqueue
或create_workqueue_singlethread
函数来创建。前一个函数在所有CPU上都创建一个工作线程,而后者只在系统的第一个CPU上创建一个线程。两个函数在内部都使用了alloc_workqueue
:
#define create_workqueue(name) \
alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
#define create_singlethread_workqueue(name) \
alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name)
name
参数表示创建的守护进程在进程列表中显示的名称。
所有推送到工作队列上的任务,都必须打包为work_struct
结构的实例,从工作队列用户的角度来看,该结构的下述成员是比较重要的:
struct work_struct;
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
entry
照例用作链表元素,用于将几个work_struct
实例群集到一个链表中。func
是一个指针,指向将延期执行的函数。该函数有一个参数,是一个指针,指向用于提交该工作的work_struct
实例。这使得工作函数可以获得work_struct
的data
成员,该成员可以指向与work_struct
相关的任意数据。
为什么内核使用atomic_long_t
作为指向任意数据的指针的数据类型,而不是通常的void *
?实际上,此前的内核版本定义的work_struct
如下:
struct work_struct {
...
void (*func)(void *);
void *data;
...
};
正如所料,data
是用指针表示的。但内核使用了一点小技巧,显然有点近乎于“肮脏”,以便将更多信息放入该结构,而又不付出更多代价。因为指针在所有支持的体系结构上都对齐到4字节边界,而前两个比特位保证为0。因而可以“滥用”这两个比特位,将其用作标志位。剩余的比特位照旧保存指针的信息。以下的宏用于屏蔽标志位:
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
当前只定义了一个标志:WORK_STRUCT_PENDING
用来查找当前是否有待决(该标志位置位)的可延迟工作项。辅助宏work_pending(work)
用来检查该标志位。请注意,将data设置为原子数据类型,确保对该比特位的修改不会带来并发问题。
为简化声明和填充该结构的静态实例所需的工作,内核提供了INIT_WORK(work, func)
宏,它向一个现存的work_struct
实例提供一个延期执行函数。如果需要data
成员,则需要稍后设置。有两种方法可以向一个工作队列添加work_struct
实例,分别是queue_work
和queue_work_ delayed
。第一个函数的原型如下:
int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)
它将work
添加到工作队列wq
,work
本身所指定的工作,其执行时间待定(在调度器选择该守护进程时执行)。
为确保排队的工作项将在提交后指定的一段时间内执行,需要扩展work_struct
,添加一个定时器。显然的解决方案如下:
struct delayed_work {
struct work_struct work;
struct timer_list timer;
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
};
queue_delayed_work
用于向工作队列提交delayed_work
实例。它确保在延期工作执行之前,至少会经过由delay
指定的一段时间(以jiffies
为单位)。
/**
* queue_delayed_work - queue work on a workqueue after delay
* @wq: workqueue to use
* @dwork: delayable work to queue
* @delay: number of jiffies to wait before queueing
*
* Equivalent to queue_delayed_work_on() but tries to use the local CPU.
*/
static inline bool queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}
/**
* queue_delayed_work_on - queue work on specific CPU after delay
* @cpu: CPU number to execute work on
* @wq: workqueue to use
* @dwork: work to queue
* @delay: number of jiffies to wait before queueing
*
* Return: %false if @work was already on a queue, %true otherwise. If
* @delay is zero and @dwork is idle, it will be scheduled for immediate
* execution.
*/
bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay)
{
struct work_struct *work = &dwork->work;
bool ret = false;
unsigned long flags;
/* read the comment in __queue_work() */
local_irq_save(flags);
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
__queue_delayed_work(cpu, wq, dwork, delay);
ret = true;
}
local_irq_restore(flags);
return ret;
}
EXPORT_SYMBOL(queue_delayed_work_on);
该函数首先创建一个内核定时器,它将在delayed jiffies
之内超时。相关的处理程序接下来使用queue_work
,按通常的方式将工作添加到工作队列。
内核创建了一个标准的工作队列,称为events
。内核的各个部分中,凡是没有必要创建独立的工作队列者,均可使用该队列。内核提供了以下两个函数,可用于将新的工作添加该标准队列,这里不会详细讨论其实现:
int schedule_work(struct work_struct *work)
int schedule_delayed_work(struct delay_work *dwork, unsigned long delay)
5. 总结
内核可以用同步或异步方式激活。而本文中是第二种激活内核的方式,即从硬件使用中断触发来异步激活内核。
在硬件想要通知内核一些情况时,可以使用中断,而中断的物理实现有很多方法。在讨论了各种可能性之后,我们分析了内核用于管理中断的通用数据结构,并看到了如何对不同的IRQ类型实现电流处理。内核必须为IRQ提供服务例程,而服务例程即ISR的实现要颇为谨慎。最重要的一点是,必须使这些处理程序的执行尽可能快速,因而通常将工作划分为两部分,即快速的上半部和低速的下半部(通常在中断上下文之外执行)。
内核提供了一些方法来将操作延迟到未来的时刻执行,本章讨论了相应的可能方法:软中断是硬件IRQ的软件等价物,tasklet
基于该机制。虽然它们能够使内核将工作推迟到稍后执行,但不允许睡眠。然而对等待队列及其衍生机制而言,睡眠是允许的。
参考
《深入Linux内核架构》