背景
系统内核是操作系统最核心的任务,外围硬件处理速度往往与CPU处理速度不在同一个量级上,因此内核不会同步等待外围硬件处理请求,而是通过异步调用机制。外围硬件在需要的时候向内核发出信号,变内核主动为硬件主动,这就是中断机制。
中断
硬件设备生成中断的时候并不考虑与处理器的时钟同步(也就是说中断随时可以产生)。因此内核随时可能因为新的中断而被打断。
每个中断都通过唯一一个数字标志,从而使得操作系统能够根据中断号对中断进行区分。这个中断值通常被称为中断请求(IRQ)线。每个IRQ线都会被关联一个数值量。
异常也是一种特殊的中断——是由系统调用处理程序异常(系统陷入内核空间)。它的工作方式与硬件中断类似,其差异只在于是由软件而不是硬件引起的。异常在产生时必须考虑与处理器时钟同步(也称同步中断)。
中断处理程序
Interrupt Handler
中断处理程序与其他内核程序区别在于:中断处理程序是被内核调用来响应中断的,它们运行于中断上下文中,该上下文的执行代码不可阻塞。中断处理完成,将控制权交还给系统被中断前原先运行的程序。
中断上半部与下半部
一般将中断切为两部分,上半部与下半部。其中上半部接受到一个中断快速中心,并且严格控制工作时长。能够延时处理的工作会推迟到下半部,在合适的时机,下半部会开中断执行。
注册中断处理程序
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
第一个参数 irq 表示要分配的终端号。
第二个参数 handler 是一个指针,指向处理这个中断的实际中断处理程序。typedef irqreturn_t (*irq_handler_t)(int, void *)
第三个参数 flags 是一个或者多个标志的位掩码:
* IRQF_DISABLED - 意味着内核在处理中断处理程序本身期间,禁止所有其他中断。
* IRQF_SAMPLE_RANDOM - 此标志标明这个设备产生的中断对内核熵池有贡献。
* IRQF_TIMER - 系统定时器专属标志位。
* IRQF_SHARED - 多个中断处理器之间共享中短线。
第四个参数 name 是与中断相关的设备ASCII文本表示。
第五个参数 dev 用于共享中断线。当一个中断处理程序需要释放时,dev将提供唯一的标志信息,以便删除指定中断处理程序。
释放中断处理程序
void free_irq(unsigned int irq, void *dev);
编写中断处理程序
static irqreturn_t intr_handler(int irq, void *dev) { }
第一个参数 irq 终端号。
第二个参数 dev 中断设备号,与中断注册时的 dev 保持一致。
重入和中断处理程序
中断处理程序是无需重入的,当一个给定中断处理程序正在执行时,响应中断线所在处理器上都会被屏蔽掉,以防止在中断线上接收到另一个新的中断。
中断处理程序示例
/* 对 rtc_irq 注册 rtc——interrupt */
if (request_irq(rtc_irq, rtc_interrupt, IRQF_SHARED, "rtc", (void *)&rtc_port)) {
printk(KERN_ERR "rtc: cannot register IRQ %d\n", rtc_irq);
return -EIO;
}
static irqreturn_t rtc_interrupt(int irq, void *dev) {
spin_lock (&rtc_lock);
rtc_irq_data += 0x100; // 更新时间
rtc_irq_data &= ~ 0xff;
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);
if (rtc_status & RTC_TIMER_ON)
mod_timer(&rtc_irq_timer, jiffies + HZ/rtc_freq + 2*HZ/100); // RTC 周期性定时器
spin_unlock(&rtc_lock);
// 执行其余操作
spin_lock(&rtc_task_lock);
if (rtc_callback)
rtc_callback->func(rtc_callback->private_data);
spin_unlock(&rtc_task_lock);
wake_up_interruptible(&trc_wait);
kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);
return IRQ_HANDLED;
}
中断上下文
中断上下文不同于进程上下文。
进程上下文是一种内核所处的操作模式,此时内核代表进程执行。此外,进程是以进程上下文上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。
与之相反,中断上下文和进程无关,一旦切出,就无法被重新调度,因此,在中断上下文中不能调用一些函数(比如睡眠函数)。
中断·处理程序共用中断进程的内核栈,因此尽量节约内核栈空间。
设备产生中断->中断控制器接受到中断->发送到处理器->处理器停止手头工作->关闭中断->跳转到预定义中断程序入口点->中断号写入寄存器->内核调用do_IRQ()->do_IRQ调用handle_IRQ_event()运行所安装中断处理程序。
对于每条中断线,处理器都会跳到对应的一个唯一位置,这样内核就可以知道所接收中断的IRQ。
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
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, action);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
oid handle_bad_irq(unsigned int irq, struct irq_desc *desc)
{
print_irq_desc(irq, desc);
kstat_incr_irqs_this_cpu(irq, desc);
ack_bad_irq(irq);
}
/*
* Special, empty irq handler:
*/
irqreturn_t no_action(int cpl, void *dev_id)
{
return IRQ_NONE;
}
static void warn_no_thread(unsigned int irq, struct irqaction *action)
{
if (test_and_set_bit(IRQTF_WARNED, &action->thread_flags))
return;
printk(KERN_WARNING "IRQ %d device %s returned IRQ_WAKE_THREAD "
"but no thread function available.", irq, action->name);
}
static void irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
{
/*
* Wake up the handler thread for this action. In case the
* thread crashed and was killed we just pretend that we
* handled the interrupt. The hardirq handler has disabled the
* device interrupt, so no irq storm is lurking. If the
* RUNTHREAD bit is already set, nothing to do.
*/
if (test_bit(IRQTF_DIED, &action->thread_flags) ||
test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
return;
desc->threads_oneshot |= action->thread_mask;
wake_up_process(action->thread);
}
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
irqreturn_t retval = IRQ_NONE;
unsigned int random = 0, irq = desc->irq_data.irq;
do {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
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:
/*
* Set result to handled so the spurious check
* does not trigger.
*/
res = IRQ_HANDLED;
/*
* 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:
random |= action->flags;
break;
default:
break;
}
retval |= res;
action = action->next;
} while (action);
if (random & IRQF_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
if (!noirqdebug)
note_interrupt(irq, desc, retval);
return retval;
}
下半部和推后执行的工作
下半部的任务就是执行与中断处理密切相关但中断处理程序本身并不执行的工作。
对于中断上半部和下半部之间划分工作,原则:
- 如果一个工作对时间非常敏感,则将其放在中断·处理程序中执行。
- 如果一个任务和硬件相关,则将其放在中断处理程序中执行。
- 如果一个任务要保证不被其他中断打断,则将其放在中断处理程序中执行。
- 其他所有任务,考虑放在中断下半部执行。
下半部机制
下半部机制 | 状态· |
---|---|
BH(Bottom Half) | 在2.5中去除 |
任务队列(Task queues) | 在2.5中去除 |
软中断(Softirq) | 从2.3中引入·1 |
tasklet | 从2.3中引入 |
工作队列(Work queues) | 从2.3中引入 |
软中断
- 软中断介绍
软中断是在编译期间静态分配。不能像tasklet那样动态的注册或者销毁。在
其中softirq_action结构定义在
struct softirq_action {
void (*action) (struct softirq_action*);
}
- 注册处理程序
通过调用open_softirq()注册软中断处理程序,该函数有两个参数:软中断索引号和处理函数。
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
- 触发软中断
通过调用 raise_softirq(NET_TX_SOFTIRQ)触发软中断,该函数将一个软中断设置为挂起状态,下一个内核调用do_softirq()函数时投入运行。
- 执行软中断
通常中断处理程序会在返回前标记它的软中断,使其在稍后被执行。于是在合适的时刻,该中断就会执行。在下列地方,待处理的软中断会被检查和执行:
- 从一个硬件中断代码处返回时
- 从ksoftirqd内核线程zhong
- 在那些显示检查和执行待处理的软中断的代码中,如网络子系统中
不管是用什么办法唤起,软中断都要在do_softirq()中执行。软中断处理程序执行的时候,允许响应中断,但是不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止,但是其他的处理器扔可以执行别的软中断。这就意味着软中断共享数据都要严格的锁保护,为了避免加锁,大部分软中断采用单处理器数据等技巧避免显式地加锁。
/*
如果有待处理的软中断,do_softirq会循环遍历每一个,并调用它们的处理程序。
*/
do_softirq() {
u32 pending;
pending = local_softirq_pending();
if (pending) {
struct softing_action *h;
/*重置待处理的位图*/
set_softirq_pending(0);
h = softirq_vec;
do {
if (pending & 1)
h->action(h);
h++;
pending >>= 1;
} while (pending);
}
}
tasklet
tasklet是基于软中断实现的一种下半部机制。它由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者唯一区别就是优先级,第一种优先级较高。
tasklet结构体
struct tasklet_struct {}
struct tasklet_struct *next;
unsigned long state; // tasklet 状态: 0、SCHED、RUN
atomic_t count; // 引用计数器 为0时才可以被调用,非0不允许执行
void (*func) (unsigned long); // tasklet 处理函数
unsigned long data; // 处理函数参数
;
调度tasklet
tasklet_schedule执行步骤:
- ksoftirqd
每个处理器都有一组辅助处理的内核线程。当内核存在大量软中断出现的情况,内核会唤醒一组内核线程来处理重新触发的软中断。这些线程在最低的优先级上运行,这样能够避免他们跟其他重要的任务抢夺资源。所有线程的名字都叫做ksoftirqd/n,区别在于n,它对应于处理器编号。线程初始化后,主要处理代码:
for (;;) {
if (!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while (softirq_pending(cpu)) {
do_softirq();
if (need_resched())
schedule();
}
set_current_state(TASK_INTERRUPTIBLE);
}
工作队列(work queue)
工作队列和软中断/tasklet的区别在于,工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是在进程上下文中执行。这样通过工作队列执行的代码能够利用进程上下文的优势,也就是允许重新调度甚至睡眠。