中断 本文目标:linux中断有哪些类别?分别用在什么场景?怎么使用?
中断有哪些类别?
为了解决中断处理执行时间过长和中断丢失的问题,中断又分为:
上半部(top half,th)
下半部(bottom half,bh)
其中上半部是硬中断hardirq,下半部可以是softirq、tasklet、workqueue
硬中断 hardirq 硬中断主要来自外围设备,比如每个处理器上的定时器
通过/proc/interrupts可以查看硬中断的运行情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 root@ubuntu-server:~ CPU0 CPU1 0: 14 0 IO-APIC 2-edge timer 1: 0 111 IO-APIC 1-edge i8042 8: 0 0 IO-APIC 8-edge rtc0 9: 0 0 IO-APIC 9-fasteoi acpi 12: 413 176 IO-APIC 12-edge i8042 14: 0 0 IO-APIC 14-edge ata_piix 15: 0 0 IO-APIC 15-edge ata_piix 16: 9378 7914 IO-APIC 16-fasteoi vmwgfx, snd_ens1371 17: 108849 192469 IO-APIC 17-fasteoi ehci_hcd:usb1, ioc0 18: 627 1362 IO-APIC 18-fasteoi uhci_hcd:usb2 19: 11143 61727 IO-APIC 19-fasteoi ens33 24: 0 0 PCI-MSI 344064-edge PCIe PME, pciehp 25: 0 0 PCI-MSI 346112-edge PCIe PME, pciehp 26: 0 0 PCI-MSI 348160-edge PCIe PME, pciehp
不同种类的中断控制器的访问方法存在差异,linux通过irq_chip结构体统一:include/linux/irq.h
1 2 3 4 5 6 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);
ARM提供了标准的中断控制器GIC(generic interrupt controller),其irq_chip如下:drivers/irqchip/irq-gic.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static const struct irq_chip gic_chip = { .irq_mask = gic_mask_irq, .irq_unmask = gic_unmask_irq, .irq_eoi = gic_eoi_irq, .irq_set_type = gic_set_type, .irq_retrigger = gic_retrigger, .irq_set_affinity = gic_set_affinity, .ipi_send_mask = gic_ipi_send_mask, .irq_get_irqchip_state = gic_irq_get_irqchip_state, .irq_set_irqchip_state = gic_irq_set_irqchip_state, .irq_print_chip = gic_irq_print_chip, .flags = IRQCHIP_SET_TYPE_MASKED | IRQCHIP_SKIP_SET_WAKE | IRQCHIP_MASK_ON_SUSPEND, };
一个系统可能有多个中断控制器,且控制器之间可能存在级联关系,为了将硬件中断号映射到唯一的linux虚拟中断号,内核定义了中断域irq_domain,每个中断控制器有自己的中断域。
硬件中断号和linux中断号映射接口:include/linux/irqdomain.h
1 2 static inline unsigned int irq_create_mapping (struct irq_domain *host, irq_hw_number_t hwirq) ;static inline unsigned int irq_find_mapping (struct irq_domain *domain, irq_hw_number_t hwirq) ;
arm64在dts中描述硬件中断控制器信息,以timer为例:arch/arm64/boot/dts/arm/foundation-v8.dtsi
1 2 3 4 5 6 7 8 timer { compatible = "arm,armv8-timer" ; interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>, <GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>, <GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>, <GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>; clock-frequency = <100000000>; };
其中,GIC_PPI为中断类型;13为中断号;(GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)为中断触发方式。
GIC中断类型有哪些:
软件生成的中断(Software Generated Interrupt,SGI),中断号0~15,处理器间中断
私有外设中断(Private Peripheral Interrupt,PPI),中断号16~31,处理器私有的中断源
共享外设中断(Shared Peripheral Interrupt,SPI),中断号32~1020,中断控制器可以将中断转发给多个处理器
局部特定外设中断(Locality-specific Peripheral Interrupt,LPI),基于消息的中断
中断触发方式:边沿触发、电平触发
linux硬中断处理函数有2层,第1层是handle_irq,根据中断类型设置对应的处理函数;第2层是irq_desc.action,一般是设备驱动程序注册
include/linux/irqdesc.h
1 2 3 4 5 6 7 8 9 10 struct irq_desc { ... irq_flow_handler_t handle_irq; struct irqaction *action ; struct irqaction { irq_handler_t handler; void *dev_id; struct irqaction *next ;
硬中断怎么用?
1、注册硬中断:include/linux/interrupt.h
1 2 3 static inline int __must_checkrequest_irq (unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
2、执行硬中断:以GIC v2控制器为例
1 2 3 4 5 6 7 8 9 10 11 12 13 el0_interrupt handle_arch_irq gic_handle_irq generic_handle_domain_irq return handle_irq_desc(irq_resolve_mapping(domain, hwirq)); generic_handle_irq_desc desc->handle_irq(desc); handle_fasteoi_irq handle_irq_event __handle_irq_event_percpu for_each_action_of_desc(desc, action) { res = action->handler(irq, action->dev_id); }
软中断 softirq softirq类别:include/linux/interrupt.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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, NR_SOFTIRQS };
通过/proc/softirqs可以查看软中断的运行情况:
1 2 3 4 5 6 7 8 9 10 11 12 root@ubuntu-server:~ CPU0 CPU1 CPU2 HI: 143 1 TIMER: 153720 89373 NET_TX: 21 827 NET_RX: 19078 71117 BLOCK: 203462 163429 IRQ_POLL: 0 0 TASKLET: 918 529 SCHED: 248027 200577 HRTIMER: 0 0 RCU: 416872 349341
softirq类别介绍:
NET_TX_SOFTIRQ、NET_RX_SOFTIRQ:网络收发报文的软中断
BLOCK_SOFTIRQ:块设备软中断
从这里可以看出,softirq是内核编译时就定义好的,运行时不能添加或删除。
softirq怎么用?
1、注册softirq:kernel/softirq.c
1 2 3 4 void open_softirq (int nr, void (*action)(void )) { softirq_vec[nr].action = action; }
2、触发softirq:kernel/softirq.c
1 2 3 4 5 6 7 8 void raise_softirq (unsigned int nr) { unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags); }
local_irq_save/restore是做什么用的?
local_irq_save(flags):将当前的中断状态保存到参数flags中,为后续恢复中断状态做准备。
local_irq_restore(flags):恢复中断状态
local_irq_disable():禁止中断
local_irq_enable():开启中断
以上接口仅能处理本处理器的中断,无法处理其他处理器的中断。local_irq_disable/enable不能嵌套使用,local_irq_save/restore可以。
以上接口也无法作用于不可屏蔽中断(NMI,Non Maskable Interrupt)
以上接口作用所有中断,还有一组仅作用于单个中断的接口:
1 2 extern void enable_irq (unsigned int irq) ;extern void disable_irq (unsigned int irq) ;
3、执行softirq:
通过中断处理程序执行
通过软中断线程执行
通过local_bh_enable函数开启softirq时执行
以下针对每一条路径进行说明:
通过中断处理程序执行:irq_exit -> __irq_exit_rcu -> invoke_softirq -> __do_softirq -> handle_softirqs
通过软中断线程执行:
每个处理器都有一个软中断线程
1 2 3 root@ubuntu-server:~ root 13 2 0 09:09 ? 00:00:00 [ksoftirqd/0] root 22 2 0 09:09 ? 00:00:03 [ksoftirqd/1]
线程执行run_ksoftirqd():run_ksoftirqd -> handle_softirqs
kernel/softirq.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static struct smp_hotplug_thread softirq_threads = { .store = &ksoftirqd, .thread_should_run = ksoftirqd_should_run, .thread_fn = run_ksoftirqd, .thread_comm = "ksoftirqd/%u" , }; static void run_ksoftirqd (unsigned int cpu) { ksoftirqd_run_begin(); if (local_softirq_pending()) { handle_softirqs(true ); ksoftirqd_run_end(); cond_resched(); return ; } ksoftirqd_run_end(); }
通过local_bh_enable函数开启软中断时执行:
local_bh_enable -> __local_bh_enable_ip -> __do_softirq -> handle_softirqs
以上3个softirq执行路径最终都调用了handle_softirqs,其实现为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static void handle_softirqs (bool ksirqd) { ... pending = local_softirq_pending(); ... restart: set_softirq_pending(0 ); local_irq_enable(); h = softirq_vec; while ((softirq_bit = ffs(pending))) { ... h->action(); ... } local_irq_disable(); pending = local_softirq_pending(); if (pending) { if (time_before(jiffies, end) && !need_resched() && --max_restart) goto restart; wakeup_softirqd(); } ... }
tasklet tasklet是基于softirq扩展实现的,但tasklet和softirq又有区别:
可在运行时添加或删除
同一时刻只会在一个处理器上执行,不要求处理函数是可以重入的
tasklet数据结构:include/linux/interrupt.h
1 2 3 4 5 6 7 8 9 10 11 12 struct tasklet_struct { struct tasklet_struct *next ; unsigned long state; atomic_t count; bool use_callback; union { void (*func)(unsigned long data); void (*callback)(struct tasklet_struct *t); }; unsigned long data; };
tasklet链表:kernel/softirq.c
1 2 3 4 5 6 7 struct tasklet_head { struct tasklet_struct *head ; struct tasklet_struct **tail ; }; static DEFINE_PER_CPU (struct tasklet_head, tasklet_vec) ; static DEFINE_PER_CPU (struct tasklet_head, tasklet_hi_vec) ;
1、定义tasklet:
1 2 3 4 5 6 7 #define DECLARE_TASKLET(name, _callback) #define DECLARE_TASKLET_DISABLED(name, _callback) void tasklet_init (struct tasklet_struct *t, void (*func)(unsigned long ), unsigned long data)
2、注册tasklet(加入到链表尾部):
1 2 3 4 static inline void tasklet_schedule (struct tasklet_struct *t) static inline void tasklet_hi_schedule (struct tasklet_struct *t)
3、执行tasklet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static void tasklet_action_common (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 (tasklet_clear_sched(t)) { if (t->use_callback) { trace_tasklet_entry(t, t->callback); t->callback(t); trace_tasklet_exit(t, t->callback); } else { trace_tasklet_entry(t, t->func); t->func(t->data); trace_tasklet_exit(t, t->func); } } 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(); } }
workqueue workqueue和tasklet的区别:
tasklet运行在softirq上下文中,而workqueue运行在内核进程上下文中,这说明wq不能像tasklet那样是原子的
takslet永远运行在指定处理器,这是初始化时就确定了,而wq可以通过配置修改这种行为
workqueue可以通过以下命令查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@ubuntu-server:~ root 8 2 0 09:09 ? 00:00:00 [kworker/0:0H-events_highpri] root 24 2 0 09:09 ? 00:00:00 [kworker/1:0H-events_highpri] root 91 2 0 09:09 ? 00:00:27 [kworker/0:1H-kblockd] root 140 2 0 09:09 ? 00:00:27 [kworker/1:1H-kblockd] root 155 2 0 09:09 ? 00:00:00 [kworker/u257:0-hci0] root 702 2 0 09:10 ? 00:00:00 [kworker/u257:1-hci0] root 7892 2 0 10:46 ? 00:00:11 [kworker/0:1-inode_switch_wbs] root 7899 2 0 10:46 ? 00:00:09 [kworker/1:0-cgroup_destroy] root 9259 2 0 12:27 ? 00:00:00 [kworker/u256:2-events_unbound] root 11349 2 0 12:47 ? 00:00:04 [kworker/1:2-events] root 11351 2 0 12:47 ? 00:00:06 [kworker/0:0-events] root 11529 2 0 13:19 ? 00:00:00 [kworker/u256:1-events_power_efficient] root 11534 2 0 13:25 ? 00:00:00 [kworker/u256:0-events_power_efficient]
1、定义work:include/linux/workqueue.h
1 2 3 4 5 6 7 #define DECLARE_WORK(n, f) #define DECLARE_DELAYED_WORK(n, f) #define INIT_WORK(_work, _func) #define INIT_DELAYED_WORK(_work, _func)
2、注册work:include/linux/workqueue.h
1 2 3 4 5 6 7 8 9 10 11 12 13 static inline bool schedule_work (struct work_struct *work) { return queue_work(system_wq, work); } static inline bool schedule_work_on (int cpu, struct work_struct *work) { return queue_work_on(cpu, system_wq, work); } __printf(1 , 4 ) struct workqueue_struct * alloc_workqueue (const char *fmt, unsigned int flags, int max_active, ...) ;
其中,在申请队列时flags指定队列类型:
1 2 3 4 5 6 7 8 9 10 enum wq_flags { WQ_BH = 1 << 0 , WQ_UNBOUND = 1 << 1 , WQ_FREEZABLE = 1 << 2 , WQ_MEM_RECLAIM = 1 << 3 , WQ_HIGHPRI = 1 << 4 , WQ_CPU_INTENSIVE = 1 << 5 , WQ_SYSFS = 1 << 6 , WQ_POWER_EFFICIENT }
max_active表示每个处理器最大可同时执行的work数量。
workqueue整体设计涉及以下几个概念:
work:处理函数
workqueue:持有多个pool_workqueue,通过pwq = rcu_dereference(*per_cpu_ptr(wq->cpu_pwq, cpu))
可以取出当前处理器的workqueue
worker:每个工人对应一个内核线程
worker_pool:持有多个workers
pool_workqueue:类似一个中介,持有1个worker_pool和1个workqueue(谁持有了我)
工人线程处理工作:kernel/workqueue.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static int worker_thread (void *__worker) { struct worker *worker = __worker; struct worker_pool *pool = worker->pool; do { struct work_struct *work = list_first_entry(&pool->worklist, struct work_struct, entry); if (assign_work(work, worker, NULL )) process_scheduled_works(worker); } while (keep_working(pool)); ... }
process_scheduled_works -> process_one_work
1 2 3 4 5 6 7 8 9 static void process_one_work (struct worker *worker, struct work_struct *work) { worker->current_work = work; worker->current_func = work->func; worker->current_pwq = pwq; ... worker->current_func(work); ... }
参考
Linux 中断(IRQ/softirq)基础:原理及内核实现(2022)
《Linux内核深度解析》