中断的分类
中断分为硬中断和软中断,其分类依据是实现机制,而不是触发机制,比如CPU硬中断,它是由CPU这个硬件实现的中断机制,但它的触发可以通过外部硬件(比如GPIO),软件的 INT 指令,或者CPU执行检测(访问非法地址、除法异常)。一些资料会把以上三种方式做区分,把INT n这种方式叫做软件中断,因为是由软件程序主动触发的,把中断和异常叫做硬件中断,因为他们都是硬件自动触发的。关于硬中断的具体实现,就是 CPU 在每一条指令周期的最后,都会留一个CPU时钟周期去查看是否有中断,如果有,就把中断号取出,去中断向量表中寻找中断处理程序,然后跳过去执行。
类似地,软中断是由软件实现的中断,是纯粹由软件实现的一种类似中断的机制,实际上是模仿硬件,在内存中存储着一组软中断的标志位,然后由内核的一个进程检查这些标志位,如果有哪个标志位有效,则再去执行这个软中断对应的中断处理程序。
软中断
Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:
- 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。
- 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。
上半部会打断CPU正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个CPU对应一个软中断内核线程,名字为“ksoftirqd/CPU编号”,比如说, 0 号 CPU 对应的软中断内核线程的名字就是 ksoftirqd/0。可以并发运行在多个CPU上(即使同一类型的也可以),所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保其数据结构。
软中断不只包括了硬件设备中断处理程序的下半部,一些内核自定义的事件也属于软中断,比如内核调度和 RCU 锁等
proc 文件系统。它是一种内核空间和用户空间进行通信的机制,可以用来查看内核的数据结构,或者用来动态修改内核的配置。其中:
- /proc/softirqs提供了软中断的运行情况;
- /proc/interrupts 提供看硬中断的运行情况。
软中断(softirq) 之所以性能高的原因,在 SMP 系统下多个 cpu 同时并发处理,并且软中断上下文不会调用任何阻塞接口。如网卡的 fifo 半满中断触发,被 cpu0 处理,cpu0 会在关闭中断后,将数据从网卡的 fifo 拷贝到 ram 之后触发软中断,再打开中断,基于谁触发谁处理原则,cpu0 会继续执行软中断服务函数。若网卡的 fifo 全满中断有再次触发,就会被 cpu1 处理,同样是关闭中断后拷贝数据再开启中断,再去触发和执行软中断进行网卡数据包处理。若此时 cpu0\cpu1 都还在软中断处理数据,网卡再次产生中断,那么 cpu2 就会继续相同的流程。由此可见,软中断充分利用的多 cpu 进行并发处理,因此性能非常高,但也同时因为并发的存在,就需要考虑临界区的问题。
内核代码软中断实现
软中断类型
enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, IRQ_POLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS };
软中断模块的初始化,默认开启 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 的软中断
void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); }
开启特定类型的软中断
void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action = action; }
触发软中断
void raise_softirq(unsigned int nr) { unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags); }
实际上即以软中断类型nr作为偏移量置位每cpu变量irq_stat[cpu_id]的成员变量__softirq_pending,这也是同一类型软中断可以在多个cpu上并行运行的根本原因。
static struct smp_hotplug_thread softirq_threads = { .store = &ksoftirqd, .thread_should_run = ksoftirqd_should_run, .thread_fn = run_ksoftirqd, .thread_comm = "ksoftirqd/%u", };
static int smpboot_thread_fn(void *data) { struct smpboot_thread_data *td = data; struct smp_hotplug_thread *ht = td->ht; while (1) { set_current_state(TASK_INTERRUPTIBLE); preempt_disable(); ...if (!ht->thread_should_run(td->cpu)) { preempt_enable_no_resched(); schedule(); } else { __set_current_state(TASK_RUNNING); preempt_enable(); ht->thread_fn(td->cpu); } } }
asmlinkage __visible void __softirq_entry __do_softirq(void) { unsigned long end = jiffies + MAX_SOFTIRQ_TIME; unsigned long old_flags = current->flags; int max_restart = MAX_SOFTIRQ_RESTART; ... current->flags &= ~PF_MEMALLOC; pending = local_softirq_pending(); softirq_handle_begin(); in_hardirq = lockdep_softirq_start(); account_softirq_enter(current); restart: /* Reset the pending bitmask before enabling irqs */... while ((softirq_bit = ffs(pending))) { ... h->action(h); // 执行特定软中断对应的中断服务函数 ... h++; pending >>= softirq_bit; } ... pending = local_softirq_pending(); if (pending) { // 检查是否在上面代码执行期间,软中断被重新置位 if (time_before(jiffies, end) && !need_resched() && --max_restart) // 检查函数执行时间未超时,并且次数未超次数,重新执行中断服务函数 goto restart; wakeup_softirqd(); } ... }
__do_softirq 会遍历执行每个置位的软中断的中断服务函数,执行完后,再次判断是否有在执行中断服务函数期间重新置位的软中断,在 __do_softirq 执行时间未超时,执行次数未超次数的情况下,会再次遍历执行每个置位的软中断的中断服务函数。否则执行wakeup_softirqd,放置到就绪进程的列表末尾,等待调度器下次调度。
tasklet
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,临界区必须用自旋锁保护。因为多个处理器可以同时且独立地处理软中断,同一个软中断的处理程序例程可以在几个 CPU 上同时运行。作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。tasklet具有以下特性:
- 一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
- 多个不同类型的tasklet可以并行在多个CPU上。
- 从软中断的实现机制可知,软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,如果不需要软中断的并行特性,tasklet就是最好的选择。也就是说tasklet是软中断的一种特殊用法,即延迟情况下的串行执行。
t->state 的判断同一个 tastlet 只在一个CPU上运行
static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); }