原文:http:///blog/category/linux-os/kernel/interrupt-kernel/ 如有侵权,请告知,谢谢. irq_desc[]数组是linux内核中用于维护IRQ资源的管理单元,它存储了某IRQ号对应的哪些处理函数,属于哪个PIC管理、来自哪个设备、IRQ自身的属性、资源等,是内核中断子系统的一个核心数组,习惯上称其为“irq数组”(个人爱好,下标就irq号)。本篇博客着重学习irq_desc[]数组的一些操作的过程和方法,如初始化、中断处理、中断号申请、中断线程等,而对于辅助性的8259A和APIC等设备的初始化过程,不详细讨论,对于某些图片或代码,也将其省略掉了。 本文中出现的irq_desc->和desc->均表示具体的irq数组变量,称其中的一个个体为irq_desc[]数组元素,描述个体时也直接时用字符串desc。为了区别PIC的handle和driver的handle,将前者称为中断处理函数(对应desc->handle_irq,实际上对应handle_xxx_irq()),而将后者称为中断处理操作(对应desc->action)。本文中将以irq_descp[]数组为操作对象的层称为irq层。本文使用的内核代码版本为3.10.9。 一篇好的博客应该是尽量多的说明,配少量的核心代码,这里偷懒了,很多部分实际是代码分析的过程,也没有省略掉。本篇博客耗时48小时。
一、irq_desc结构和irq_desc[]数组
|
1 2 3 4 5 6 7 | 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_desc结构体,如下图所示:
struct irq_desc结构体(以前的版本结构体的名字是irq_desc_t)定义如下所示(简化过,include/linux/irqdesc.h)。大部分成员都是辅助性的,关键的成员是irq_data、handle_irqs、action、depth、lock、istat,所谓irq_desc[]数组的初始化,看其主要成员的初始化的过程,在这里做简单的说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct irq_desc { struct irq_data irq_data; irq_flow_handler_t handle_irq; ... struct irqaction *action; /* IRQ action list */ unsigned int status_use_accessors; unsigned int core_internal_state__do_not_mess_with_it; unsigned int depth; /* nested irq disables */ raw_spinlock_t lock; ... struct module *owner; const char *name; } ____cacheline_internodealigned_in_smp; |
这里还是看一下irqaction结构体,action的handler是具体的中断服务程序,next指针用于指向同一个链上的后一个的irqaction,thread_fn用于描述软中断处理函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | typedef irqreturn_t (*irq_handler_t)(int, void *); 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; 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; |
这意味着所有的驱动在写中断处理函数时,必须以irqreturn_t为类型:
1 2 3 4 5 6 7 8 | // intel e1000 static irqreturn_t e1000_intr(int irq, void *data); // acpi static irqreturn_t acpi_irq(int irq, void *dev_id) // hd static irqreturn_t hd_interrupt(int irq, void *dev_id) // ac97 static irqreturn_t atmel_ac97c_interrupt(int irq, void *dev) |
在这里,很容易产生一个问题,就是驱动程序处理的数据在哪?总要有些数据要处理,是从void参数吗?那么这个数据怎么获取的?handle_irq_event_percpu()函数里有具体的action的调用方式:
1 | res = action->handler(irq, action->dev_id); |
那么,void *参数来自action->dev_id,而dev_id是驱动程序注册时,调用request_irq()函数传递给内核的。而这个dev_id通常指向一个device设备,驱动程序就通过该device设备将需要的数据接收上来,并进行处理。
irq_desc[]数组是内核维护中断请求资源的核心数组,它必须在合适的时机予以初始化。内核起动后,有步骤的初始化内核各个子系统,init_IRQ()函数主要负责完成内核中断子系统的主要初始化。irq_desc[]数组伴随着init_IRQ()函数的执行而完成其一部分的初始化。
init_IRQ()函数的调用路径为main()->...->start_kernel()->init_IRQ()->native_init_IRQ()。init_IRQ()函数与irq_desc[]数组初始化或者IDT、interrupt[]数组的设置有关的函数或过程,关于init_IRQ的内部调用关系,如下图所示:
下面是具体的代码分析过程:
从init_IRQ()函数开始分析,init_IRQ在arch/x86/kernel/irqinit.c中定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void __init init_IRQ( void ) { int i; /* * We probably need a better place for this, but it works for * now ... */ x86_add_irq_domains(); /* * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15. * If these IRQ's are handled by legacy interrupt-controllers like PIC, * then this configuration will likely be static after the boot. If * these IRQ's are handled by more mordern controllers like IO-APIC, * then this vector space can be freed and re-used dynamically as the * irq's migrate etc. */ for (i = 0; i < legacy_pic->nr_legacy_irqs; i++) per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i; x86_init.irqs.intr_init(); } |
x86_add_irq_domains()直接略过。这里的注释还时很有用的,这里说开始时使用8259A注册这些中断向量号,如果系统使用IO APIC,将覆盖这些中断向量号,并且能够动态的重新使用。vector_irq为在arch/x86/include/asm/hw_irq.h中定义的per_cpu整形数组,长度为256,用于描述每个CPU的中断向量号,即vector_irq[](vector_irq[]元素初始化时被赋值为-1)中存储着系统可以使用的中断向量号。这里需要注意,vector_irq[]数组时PER_CPU的。
legacy_pic字面意思为“遗留的PIC”,就是指8259A,legacy_pic定义在arch/x86/kernel/i8259.c,其中NR_IRQS_LEGACY为16:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct legacy_pic default_legacy_pic = { .nr_legacy_irqs = NR_IRQS_LEGACY, .chip = &i8259A_chip, .mask = mask_8259A_irq, .unmask = unmask_8259A_irq, .mask_all = mask_8259A, .restore_mask = unmask_8259A, .init = init_8259A, .irq_pending = i8259A_irq_pending, .make_irq = make_8259A_irq, }; struct legacy_pic *legacy_pic = &default_legacy_pic; |
init_IRQ()将vector_irq[]逐个赋值(就赋值了16个,从0x30到0x39)。x86_init为x86架构初始化时的一个全局变量,记录了各个子系统(irq,paging,timer,iommu,pci等)初始化使用的具体函数。而实际的x86_init.irqs.intr_init指针指向native_init_IRQ()函数(arch/x86/kernel/irqinit.c):
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 | void __init native_init_IRQ( void ) { int i; /* Execute any quirks before the call gates are initialised: */ x86_init.irqs.pre_vector_init(); apic_intr_init(); /* * Cover the whole vector space, no vector can escape * us. (some of these will be overridden and become * 'special' SMP interrupts) */ i = FIRST_EXTERNAL_VECTOR; for_each_clear_bit_from(i, used_vectors, NR_VECTORS) { /* IA32_SYSCALL_VECTOR could be used in trap_init already. */ set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]); } if (!acpi_ioapic && !of_ioapic) setup_irq(2, &irq2); #ifdef CONFIG_X86_32 irq_ctx_init(smp_processor_id()); #endif } |
x86_init.irqs.pre_vector_init指针指向init_ISA_irqs()函数,主要完成8259A/Local APIC的初始化,apic_intr_init()函数主要完成apic相关的中断的初始化。接着,native_init_IRQ()函数将调用set_intr_gate()函数设置中断门,将interrupt[]数组设置的地址设置到相应的中断门。注意,这里只是对没有used_vectors进行set_intr_gate()的赋值,并不是从FIRST_EXTERNAL_VECTOR到NR_VECTORS全部赋值,因为有些特殊情况会预留(关于used_vectors和vector_irq的关系,详见“七、中断向量、锁和CPU”)。余下的两个接口处理了一些特殊情况,这里不展开了。
实际上init_IRQ()主要调用了native_init_IRQ(),除了使用set_intr_gate()来初始化Interrupt describptor外,后者主要干了两件事:init_ISA_irqs()和apic_intr_init()。先从简单的看起,apic_intr_init()函数实际上是一系列的set_intr_gate,但不通过interrupt[]数组,也不通过irq_desc[](这就是native_init_IRQ()函数中所为的“特殊情况”,属于used_vectors的范围):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | static void __init apic_intr_init(void) { smp_intr_init(); #ifdef CONFIG_X86_THERMAL_VECTOR alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt); # endif ... #ifdef CONFIG_HAVE_KVM /* IPI for KVM to deliver posted interrupt */ alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi); # endif ... } |
而smp_intr_init()函数如下执行apic_intr_intr()函数类似的操作,也通过set_intr_gate()函数设置了一些中断门。
这些中断门没有通过interrupt数组,也没有irq_desc数组,而是直接使用set_intr_gate()接口将其IDT中的中断门描述符初始化。而这些中断在/proc/interrupt中显示比较特殊,并不以中断向量号的形式显示,而是以名字的形式,比如NMI,本身也不连接任何的PIC(截取一部分):
1 2 3 4 5 6 7 8 9 10 | [rock3@e4310 linux-stable]$ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 ... 44: 66 80 77 72 PCI-MSI-edge snd_hda_intel 45: 14948296 0 0 0 PCI-MSI-edge iwlwifi NMI: 1539 19912 17314 17232 Non-maskable interrupts LOC: 45133746 42836772 33584448 33666542 Local timer interrupts SPU: 0 0 0 0 Spurious interrupts PMI: 1539 19912 17314 17232 Performance monitoring interrupts IWI: 641572 409182 330064 302186 IRQ work interrupts |
然后看比较复杂的init_ISA_irqs()函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void __init init_ISA_irqs( void ) { struct irq_chip *chip = legacy_pic->chip; const char *name = chip->name; int i; #if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC) init_bsp_APIC(); #endif legacy_pic->init(0); for (i = 0; i < legacy_pic->nr_legacy_irqs; i++) irq_set_chip_and_handler_name(i, chip, handle_level_irq, name); } |
legacy_pic->init指针指向init_8259A()函数,因此init_ISA_irqs执行了init_8259A(0)。irq_set_chip_and_handler_name()函数用于设置irq_desc[]数组的handle_irq、name、chip等成员。因此init_ISA_irqs()函数做了三件事:init_bsp_APIC()、init_8259A()、irq_set_chip_and_handler_name()。此时legacy_pic->nr_legacy_irqs为16。
init_bsp_APIC()为对Local APIC的某种初始化操作,与irq_desc[]数组初始化无关,不讨论了。
init_8259A(0)为对8259A的某种初始化操作,与Irq_desc[]数组的初始化无关,不讨论了。
irq_set_chip_and_handler_name()函数如下(kernel/irq/chip.c):
1 2 3 4 5 6 7 | void irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle, const char *name) { irq_set_chip(irq, chip); __irq_set_handler(irq, handle, 0, name); } |
irq_set_chip()将irq_descp[]数组的*action的chip成员,主要是__irq_set_handler()函数(kernel/irq/chip.c),看下__irq_set_handler()函数都设置了irq_desc[]数组的什么成员:
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 | void __irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained, const char *name) { unsigned long flags; struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0); if (!desc) return ; if (!handle) { handle = handle_bad_irq; } else { if (WARN_ON(desc->irq_data.chip == &no_irq_chip)) goto out; } /* Uninstall? */ if (handle == handle_bad_irq) { if (desc->irq_data.chip != &no_irq_chip) mask_ack_irq(desc); irq_state_set_disabled(desc); desc->depth = 1; } desc->handle_irq = handle; desc->name = name; if (handle != handle_bad_irq && is_chained) { irq_settings_set_noprobe(desc); irq_settings_set_norequest(desc); irq_settings_set_nothread(desc); irq_startup(desc, true ); } out: irq_put_desc_busunlock(desc, flags); } |
主要就设置了两个成员:handle_irq全部设置为handle_level_irq,name设置为“XT-PIC”(8259A)。而irq_desc[]数组中的handle_irq成员在do_IRQ()中被调用来执行具体的ISA。这个位置使用了buslock,也即desc->irq_data.chip->irq_bus_lock,而不是desc->lock。buslock用于中断控制器的操作,desc->lock用于IRQ中断处理函数的操作。
IO APIC的handle_irq通过setup_IO_APIC_irqs()函数初始化。调用过程是start_kernel()->rest_init()->kernel_thread(kernel_init)->kernel_init_freeable()->smp_init()->APIC_init_uniprocessor()->setup_IO_APIC()->setup_IO_APIC_irqs()->__io_apic_setup_irqs()->io_apic_setup_irq_pin()->setup_ioapic_irq()->ioapic_register_intr()->irq_set_chip_and_handler_name()。中间经过了太多的过程,其中主要的入口有setup_IO_APIC()用于初始化IO APIC,在初始化IO APIC的过程中完成了对IO APIC的irq_desc[]数组的初始化(setup_IO_APIC_irqs()),最终调用了irq_set_chip_and_handler_name()函数,完成了对irq_desc[]数组的初始化,其初始化desc->handle默认为handle_fasteoi_irq()或handle_edge_irq(),desc->name分别对应fasteoi或edge。
当然,这个过程在8259A调用irq_set_chip_and_handler_name()之后,那么根据__irq_set_handler()的实现,handle_irq可以更新,因此后注册的IO APIC替代了先前的8259A。
这里还有个IRQ个数的问题,8259A初始化了16个irq_desc[]数组(0x30到0x39),而APIC应该时224个,但是实际上在setup_IO_APIC_irqs()函数执行时,轮询了系统侦测到的所有的IO APIC,对每个IO APIC在__io_apic_setup_irqs()函数中,又轮询该IO APIC上注册的所有的设备,对于每个注册者,执行io_apic_setup_irq_pin()->setup_ioapic_irq()->ioapic_register_intr()->irq_set_chip_and_handler_name()的过程,而对于每个IO APIC上的每个注册者,对应的irq号,就通过pin_2_irq()接口确认。这意味着,IO APIC要将哪些irq_desc[]数组初始化。
IO APIC的hanle_irq有两种,分别是handle_fasteoi_irq()和handle_edge_irq(),最终也都调用了handle_irq_event()->handle_irq_event_percpu(),在irq_desc[]初始化上与8259A一致,在“四、desc->handle_irq”部分会详细分析。
这样就存在一个问题,因为IO APIC并非每个irq_desc[]数组元素都去初始化,而是只初始化那些连接有设备的,那么如何能保证这些irq号就是驱动申请的irq号那?
经过“irq_desc[]的初始化”部分的描述desc->handle_irq已经初始化完毕,而desc->handle_irq接口实际上可以挂接几个函数(8259A和IO APIC):
? 字面意思说明三种处理函数分别代表电平触发、xxx触发、边沿触发,他们之间各有不同但最终均调用了handle_irq_event()。本文中将以以上函数称为handle_xxx_irq()系列函数(还有其他几个handle_xxx_irq()函数,也属于此系列,但是不是x86平台时用或者不是8259A或IO APIC时用)。他们三者之间在何时屏蔽IRQ线、是否要响应发出中断信号的硬件等处有微小的区别。
下面以handle_level_irq()函数为例,看下它具体干了什么事情,其他handle_xxx_irq()函数做了基本相同的工作:
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 | void handle_level_irq(unsigned int irq, struct irq_desc *desc) { raw_spin_lock(&desc->lock); mask_ack_irq(desc); if (unlikely(irqd_irq_inprogress(&desc->irq_data))) if (!irq_check_poll(desc)) goto out_unlock; desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING); kstat_incr_irqs_this_cpu(irq, desc); /* * 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; } handle_irq_event(desc); cond_unmask_irq(desc); out_unlock: raw_spin_unlock(&desc->lock); } |
首先,raw_spin_lock先获取锁(关于desc->lock将在“七、中断向量、锁和CPU“部分详细介绍),退出前释放锁,然后按照下列次序进行处理:
handle_irq_event()代码如下(kernel/irq/handle.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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; } |
handle_irq_event()函数首先将irq_desc[]数组的istate清除IRQ_PENDING标志,然后设置desc->irq_data->state_use_accessors增加IRQD_IRQ_INPROGRESS,然后执行handle_irq_event_percpu()函数,逐个cpu执行action,执行完毕后,清除desc->irq_data->state_use_accessors的IRQD_IRQ_INPROGRESS标志,说明IRQD_IRQ_INPROGRESS标志表示正在执行某个具体中断处理操作,也即正在执行action。注意此处锁的位置,更新desc->irq_data->state_use_accessors的标志时,锁,执行action的时候不锁,进入handle_irq_event()时,就已经锁住了。下面来看handle_irq_event_percpu()函数:
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 46 47 48 49 | irqreturn_t handle_irq_event_percpu( struct irq_desc *desc, struct irqaction *action) { irqreturn_t retval = IRQ_NONE; unsigned int flags = 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: /* * 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; action = action->next; } while (action); add_interrupt_randomness(irq, flags); if (!noirqdebug) note_interrupt(irq, desc, retval); return retval; } |
trace_irq_handler_entry()函数和trace_irq_handler_entry()函数用于找出action中那些不合法的(网上有人这么说,没找到,具体不详)并强行disble他们。具体中断处理调用了action->handler(),反复执行将整个chain上的所有action都执行一遍,所有的返回值或起来作为总的返回值。针对IRQ_WAKE_THREAD,说明驱动程序使用了软断的方式(设置了thread_fn),那么就要调用irq_wake_thread(),尝试在调度器中激活action->thread_fn。关于irq_wake_thread(),详见“八、中断线程”部分。最后,在默认情况下,通过note_interrupt()接口更新desc->action的结果,并决定是否触发“伪中断”轮询函数poll_spurious_irq()。
从上述分析不难看处,Linux内核在实现内核中断处理函数函数时层次分明:
从上述描述可以看出,irq_desc[]数组的name,handle_irq,irq_data->chip伴随着8259A和IO APIC的初始化而初始化,而irq_desc[]数组的其他部分,如actions还没有注册,actions是驱动程序通过request_irq()函数项内核的IRQ系统申请一个IRQ号,并初始化该号的irq_desc[]数组元素中的action(include/linux/interrupt.h):
1 2 3 4 5 6 | 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()函数代码真不少,删除参数校验和调试的部分,主要完成了申请action内存,并初始化之,然后挂接action和irq_desc[]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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) { ... 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; chip_bus_lock(desc); retval = __setup_irq(irq, desc, action); chip_bus_sync_unlock(desc); if (retval) kfree(action); ... return retval; } |
自此,irq_desc[]数组的初始化基本完成。
request_irq()函数需要携带irq号这个变量用于注册,但是驱动程序本身并清楚哪个中断号是可用的,也不清楚自己的IRQ线对应的哪个irq_desc[]元素,怎么解决这个问题?一方面,驱动程序一般都有自己的默认中断向量号,但不止一个,调用request_irq()时,会调用irq_settings_can_request()函数(给省略掉了)来检测该desc是否可以被申请,如果不能被申请,则返回-EINVAL,驱动程序会再重新申请;另一方面,内核提供了一种irq号侦测机制,auto probe,详见“六、istate状态”。
Linux内核中的中断子系统有四种状态、特性,分别是:
此处重点介绍istate,但首先还是irq_desc->action->flags。include/linux/interrupt.h文件中定义了handling routines的一些标志(并没有完全列出,还有一些IRQF_TRIGGER_的表示触发方式的标志):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* * These flags used only by the kernel as part of the * irq handling routines. * * IRQF_DISABLED - keep irqs disabled when calling the action handler. * DEPRECATED. This flag is a NOOP and scheduled to be removed * 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 * 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_,表示IRQ Flags。在request_irq()函数执行时,需要填写flags变量,也就是这些标志,这些标志用于说明你申请的handler的特性,request_irq的flags总是带有IRQF_DISABLED的标志,其他特性让其他IRQ操作函数按不同的路径执行。如IRQ_SHARED标志表示可以共享IRQ号,IRQF_NO_THREAD标志表示中断处理函数不能被线程化。
然后是istate状态,kernel/irq/internals.h中定义了这些“状态”,使用IRQS_前缀,他们可以同时存在,而并不一定相互转化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* * Bit masks for desc->state * * IRQS_AUTODETECT - autodetection in progress * IRQS_SPURIOUS_DISABLED - was disabled due to spurious interrupt * detection * IRQS_POLL_INPROGRESS - polling in progress * IRQS_ONESHOT - irq is not unmasked in primary handler * IRQS_REPLAY - irq is replayed * IRQS_WAITING - irq is waiting * IRQS_PENDING - irq is pending and replayed later * IRQS_SUSPENDED - irq is suspended */ enum { IRQS_AUTODETECT = 0x00000001, IRQS_SPURIOUS_DISABLED = 0x00000002, IRQS_POLL_INPROGRESS = 0x00000008, IRQS_ONESHOT = 0x00000020, IRQS_REPLAY = 0x00000040, IRQS_WAITING = 0x00000080, IRQS_PENDING = 0x00000200, IRQS_SUSPENDED = 0x00000800, }; |
这些状态存储在irq_desc->istate,也就是core_internal_state__do_not_mess_with_it。从字面意思看,有些状态不好理解,通过他们在linux内核中的调用关系搞清楚来龙去脉:
IRQS_AUTODETECT状态和IRQS_WAITING状态
? AUTODETECT状态在函数probe_irq_on()中被开启,在probe_ifq_off()函数或probe_ifq_mask()函数中被关闭。而probe_irq_on()和probe_irq_off()是驱动与内核申请可用的irq号的调用函数,一个驱动程序可以通过request_irq()向内核申请注册中断处理函数到desc->action,此时需要携带irq号作为参数,而irq号可以自是预先设定(Intel Enternet drvier),也可以通过一种叫自动侦测(auto probe)的过程来完成:
? 如果再扣的细一点,还有两个问题:
对问题1,涉及到IRQS_WAITING标志的含义,IRQS_WAITING标志在probe_irq_on()接口中被设置,在执行irq_desc->handle_irq,也即具体的handle_xxx_irq()接口时被清除,猜测async_synchronize_full()接口可能会等待所有的中断处理函数执行完毕吧(此处还有问题,如何等待源源不断的中断处理执行完成?那得看async_synchronize_full的细节了。)
对问题2,在init_8259A()函数或者setup_IO_APIC_irqs()函数中,都调用了irq_set_chip_and_handler_name()函数,后者将desc->irq_data.chip和desc->handle_irq以及desc->name给予初始化,这意味着与irq_desc[]关联的PIC已经完全驱动起来,并且与irq_desc[]建立了关联关系,当硬件产生中断信号时,PIC可以接收到该信号,并通知CPU,CPU查找到IDT里的中断处理程序后,就执行了do_IRQ()函数,然后调用了handle_IRQ_event()接口,然后到具体的irq_desc->handle_irq,如handle_level_irq(),而在具体的handle_xxx_irq()接口中,就会清除掉IRQS_WAITING标志。
总结一下,IRQS_AUTODETECT表示某个irq_desc[]变量处于自动侦测状态,通过probe_irq_on()函数设置此状态,通过probe_irq_off()清除此状态。IRQS_WAITING表示某个irq_desc[]变量处于等待状态,也即等待被处理,等待irq_desc->handle_irq的执行,此状态通过probe_irq_on()设置,通过irq_desc->handle_irq,也即handle_xxx_irq()系列函数清除。
IRQS_SPURIOUS_DISABLED状态
前面说IRQS_AUTODETECT状态时,提到“伪中断”,这个IRQS_SPURIOUS_DISABLED就是指这种情况。根据wiki上的说法:“一类不希望被产生的硬件中断。发生的原因有很多种,如中断线路上电气信号异常,或是中断请求设备本身有问题。”,猜测是哪些PIC上确实侦听到了中断信号,但实际上没有发生中断,或者没有找到中断处理函数的情况。Linux内核将某个irq_desc[]元素发生了10万次中断,却有9.9万次没有处理的情况,视为“伪中断”,会将其置位,意味着因为“伪中断”而被禁用(紧接着会执行irq_disable(desc))。中断发生次数统计通过desc->irq_count,中断发生却没有处理的统计通过desc->irq_unhandled。内核在note_interrupt()函数中处理此情况。
setup_irq()函数用于驱动程序将具体的中断处理函数挂接到desc->action下,该函数执行时,将清除IRQS_SPURIOUS_DISABLED标志。
对于“伪中断”,内核并不是扔掉不管,而是有一套自己的处理方法,有人还对其做过优化。目前,内核采用poll_spurious_irqs()的方法来处理被IRQS_SPURIOUS_DISABLED的desc。poll_spurious_irqs()函数将轮询所有的“伪中断”,并尝试在本地core上执行一次(通过try_one_irq()函数)。try_one_irq()函数有选择的执行handle_irq_event(),(有些情况的伪中断不予执行,比如PER_CPU的、嵌套的等等)。poll_spurious_irqs()函数并不清除IRQS_SPURIOUS_DISABLED标志,而是尝试轮询并执行他们一次。
? 总结一下,IRQS_SPURIOUS_DISABLED标志意味着某irq_desc[]元素被视为“伪中断”,并被禁用。该标志被note_interrupt()函数设置,被setup_irq()函数清除。对于哪些伪中断,系统尝试时用poll_spurious_irqs()函数在本地CPU上轮询并执行他们一次(在中断线被PIC禁用时,仍可以执行中断处理函数,即是中断处理函数执行完毕,也不清除此标志)。
IRQS_POLL_INPROGRESS状态
? in progress表示正处于过程当中,poll in progress字面意思就是中断处理函数目前正在以poll的方式执行。硬件中断处理函数通常是立即执行,而软中断才留在后面执行。
前面提到的try_one_irq()函数,在其执行handle_irq_event()函数前,将设置此标志表示中断处理函数正在以poll的方式被执行,在其执行完毕handle_irq_event()后清除此标志,表示中断处理函数执行poll完毕。而调用try_one_irq()函数的还由misrouted_irq()函数(用于尝试执行一次可能时misrouted的情况,硬件产生了中断信号,内核却将其对应到错误的irq_desc[]元素上的情况)函数中被调用。这是对于“伪中断”的轮询,但对正常的中断处理,并没有采用poll的方法(NAPI采用了,另说),而是在具体的handle_xxx_irq()函数中需要执行irq_check_poll()方法,等待中断处理函数poll完毕,因为驱动实现的中断处理函数未必是可重入的。
总结一下,IRQS_POLL_INPROGRESS,表示一个irq_desc[]元素正处于轮询调用action链的阶段。此标志只在try_one_irq()函数(被poll_spurious_irqs()函数和misrouted_irq()函数调用)调用handle_irq_event()前被设置,在其调用handle_irq_event()后被清除。由于具体的中断处理函数(desc->action)的设计未必是可重入的,因此desc->handle_irq,如handle_xxx_irq()需要等待其上的轮询完毕后才能执行。这意味着,仅仅“伪中断”才会被轮询,并且一个中断处理函数可以同时被“伪中断”轮询执行,也可以正常执行,但必须排队执行。
这个地方还是不理解,既然被定为“伪中断”,那么就会被irq_disable()——从硬件上屏蔽该中断线,怎么还会接收到中断、并执行中断处理函数那?这可能就是“伪中断”神奇的地方。
IRQS_ONESHOT状态?
开一枪状态?应该是个不太重要的状态。该状态表示irq_desc[]元素的主处理函数不是非屏蔽的(直接说mask不就完了,难道除了mask,unmask还有半mask?)。在handle_fasteoi_irq()函数(其他handle_xxx_irq()中没有,fasteoi一定是一种比较特殊的情况)中,如果该标志设置,就需要mask_irq()执行一下,然后就是硬件操作了。此处的mask应该是这样的,mask意味着屏蔽中断,也就是在中断处理函数执行的时后,从CPU的角度短暂的禁止该中断(应该是中断向量,通过设置CPU的中断屏蔽寄存器IMR,来完成此过程)。而是否需要mask中断,是中断处理函数的本身的需要,因此,也就是应该是desc->action->flags里的设置(IRQF_ONESHOT标志),而IRQS_ONESHOT状态应该是源于IRQF_ONESHOT标志的,在setup_irq()函数被执行用来为desc添加action的时候,在非共享的方式下,如果发现action->flags中设置了IRQF_ONESHOT标志,则为desc->istate设置此状态。
在irq_finalize_oneshot()函数中,将会执行unmask_irq(),并清除该标志。irq_finalize_oneshot()函数在one shot中断处理函数运行完毕(action->thread_fn)时被调用。
总结,one shot,字面意思开一枪,这枪肯定是早期的步枪,没能实现自动填装弹药。IRQS_ONESHOT更像一种属性,而非状态(在其被设置时,并不是中断被屏蔽的时刻,而时表示此action是one shot类型的),在setup_irq()的时后被条件设置,在handle_fasteoi_irq()执行mask_irq()操作,在irq_finalize_oneshot()中执行unmask_irq()操作,并清除此标志。
IRQS_REPLAY状态?
从字面意思上讲,应该是重新发生一次中断的意思。该标志在handle_xxx_irq()函数中的开始部分就被清除(避免多个core同时执行),在check_irq_resend()函数中,若判断desc->istate包含IRQS_PENDING标志,则设置该状态。IRQS_REPLAY状态仅在check_irq_resend()函数中被设置,check_irq_resend()函数通过重新激发中断控制器上的中断信号来完成此过程(中断硬件将不发送中断信号,仅仅由PIC重新向CPU发送),对handle_level_irq()函数触发的中断,不予重新发送(不知道原因)。而check_irq_resend()函数又被__enable_irq()和irq_startup()所调用,最终被irq_set_chip_and_handler_name()和irq_set_handler()调用,他们的调用关系如下图所示:
总结,IRQS_REPLAY状态表示需要重新激发一次中断信号(正在重新发送IRQ信号),它在desc->handle_irq被初始化最后时刻被设置(被irq_set_chip_and_handler_name()函数设置):完成desc->handle_irq的挂接后要由PIC自动产生一次中断,利用重新注册action的机会,对“挂起”(IRQS_PENDING)状态的中断再触发一遍,然后由handle_xxx_irq来查看中断处理函数是否正常,该标志在handle_xxx_irq()时被清除。
IRQS_PENDING状态
字面意思“正在挂起”,为什么要挂起?在什么情况下挂起?在handle_xxx_irq()函数(handle_ege_irq()函数和handle_edge_eoi_irq()函数比较特殊)中,如果发现以下情况中的一种,则挂起,挂起后直接释放锁并推出,并不执行handle_irq_event()函数:
还有一种情况会“挂起”中断处理,在probe_irq_on()时,对于所有的没有分配action并且可以被PROBE的desc,需要重新irq_startup()一下,清掉此前mask it self的longstanding interrupt。在irq_startup()函数中,硬件会尝试挂起该中断向量,如果成功的话,desc->istate也需要置位IRQS_PENDING。
另一种需要“挂起”的情况是try_one_irq()函数中,如果desc->irq_data->state_use_accessors包含IRQD_IRQ_INPROGRESS标志(表示IRQ不处于),则为desc->istate置挂起标志并推出。
以下是清除IRQS_PENDING的场景:
? 以下是校验IRQS_PENDING的场景:
? 总结,IRQS_PENDING表示中断处理被挂起,通常是因为没有中断处理函数或者中断控制器被禁用。通常由handle_xxx_irq()设置,由handle_irq_event()清除。
IRQS_SUSPENDED状态
字面意思为“暂停”,下面是置位IRQS_SUSPENDED状态的场景:
? 清除IRQS_SUSPENDED状态场景:
校验IRQS_SUSPENDED状态的场景:
? 总结,IRQS_SUSPENDED标志表示所有中断线均被disable,通过suspend_device_irqs()函数置位,通过resume_device_irqs()函数清除。
总体看来这些状态代表着irq_desc[]的一些操作阶段,但是他们之间是可以共存的,下图粗略的描述了irq_desc->istate的各种状态之间的关系,状态的输入箭头表示状态置位操作,状态的输出箭头表示状态的清除操作,函数的输入箭头表示被上游函数调用,函数的输出箭头表示调用了下游函数,虚线表示mask_irq()操作实际上是handle_xxx_irq的一个特例handle_fasteoi_irq()中的操作,直线没箭头表示对该状态ONESHOT的直接操作,关于ONESHOT状态,实际上是desc->action->thread_fn的一个属性,将会在“八、中断线程中详细讨论”:
中断向量这个词经常说,那么到底什么是中断向量那?内核中有两个概念,一个叫vector,另一个叫irq。vector指的就是“中断向量”,也就是PIC向CPU传递的用于表示发生中断的IRQ线的一种编号。而irq,也就是常说的irq号,是内核维护的中断请求的数组下标。以下说法或变量是等价的(x86架构):
硬件维护的中断向量号,在x86平台上,本质上是PIC向CPU传递的用于表示发生中断的IRQ线的一种向量编号,CPU通过该向量找到IDT表中的对应的中断门描述符。由于Intel保留了前0x20个中断、异常号,所以内核就没必要再维护这块了,所以内核中的数组下标从0开始,就对应了中断向量的0x20号。但是vector_irq[]数组比较特殊,它的下标代表vector,而内容代表irq,实际上就是irq到vector的一个影射:
vector_irq[]数组在arch/x86/include/asm/hw_irq.h文件中定义:
1 2 | typedef int vector_irq_t[NR_VECTORS]; DECLARE_PER_CPU(vector_irq_t, vector_irq); |
通过setup_vector_irq()函数初始化:
1 2 3 4 5 6 7 8 9 10 11 | void setup_vector_irq(int cpu) { #ifndef CONFIG_X86_IO_APIC int irq; ... for (irq = 0; irq < legacy_pic->nr_legacy_irqs; irq++) per_cpu(vector_irq, cpu)[IRQ0_VECTOR + irq] = irq; # endif __setup_vector_irq(cpu); } |
前文中提到在init_IRQ()函数中,会首先初始化vector_irq[]数组,而且vector_irq[]数组是PER_CPU的。vector_irq[]在声明时被全部初始化为-1,-1表示未被使用。在init_IRQ()函数中,16个(0x30~0x3f,为什么是0x30开始,就不讨论了)被初始化为0到15。
used_vectors按照内核注释“used_vectors is BITMAP for irq is not managed by percpu vector_irq”的说法,used_vectors是vector_irq[]不管的中断向量。used_vectors通过alloc_intr_gate()函数置位,alloc_intr_gate()函数的主要功能是set_intr_gate()。used_vectors[]在trap_init()时,前0x20个被置位。系统调用0x80随后也被used_vectors置位,从名字上看,used_vectors应该时叫system vectors,就时IO设备中断以外的中断和异常。实际上used_vectors标明了锁有的中断、异常哪些是IO device中断,哪些是非IO device中断,这些非IO device中断中包含了Intel预留的前0x20个中断、异常,也包含Linux内核选取的0x80系统调用软件中断,当然还包括一些其他的中断和异常,但并不做具体的区分。
因此,used_vectors和vector_irq[]的关系就清晰了,下面是关于并发的操作。
ULK上说:”简而言之,当硬件设备产生了一个中断信号时,多APIC系统就选择其中的一个CPU,并把信号传递给相应的本地APIC,本地APIC又依次中断它的CPU。这个时间不通报给其他所有的CPU“。我想这个地方所谓的”多APIC系统“,是指多IO APIC系统,而IO APIC选择一个CPU,将信号传递给其Local APIC,然后接收到信号的Local APIC再选择一个本地的Core通知其中断发生,那么就意味着在多IO APIC系统中,选择哪个CPU的哪个Core来处理IO设备中断,是由IO APIC决定的,而且从硬件上就决定了其不会同时由两个以上的Core收到相同的中断信号。那么,Linux内核应该无需担心两个core同时处理一个中断信号引起的desc->handle_irq的情况了,但是会发生如下情况:
对于第一种情况,起码要有一种机制保证正常的丢失中断,而不是总被自己打断,而无法完成正常的中断处理。而对第二种情况,应该尽量避免,因为中断处理函数不一定是可重入的,因此必须顺序执行,一次没处理完,不能并发处理下一次。Linux内核通过自旋锁来完成这个工作,也即在handle_xxx_irq()系列函数开始时,获取锁,退出时,释放锁。Linux内核中断子系统中至少使用到了4种锁用于同步不同的资源,分别是:
前面说的锁就是desc->lock的用处。irq_desc->lock类型为raw_spinlock_t,是一个自选锁。首先,得看一下自旋锁的特点和操作。自旋锁有以下特点:
自旋锁可以时用下列方法去操作:
内核在desc->handle_irq到handle_irq_event()接口的过程中,通常这样处理:
为什么要着要么做?避免同时对调用handle_irq_event(),奇怪的是在handle_irq_event()里,在irqd_set(IRQD_IRQ_INPROGRESS)后,又解锁,这是为什么?显然,内核并不是害怕两个CPU同时执行了desc->action(除了handle_xxx_irq()系列函数,内核还有其他位置调用了desc->action),而是害怕在锁与解锁之间的操作被交叉操作,这些操作包括(以handle_level_irq()为例,上图中没有标出):
看样子,desc->lock只保护desc->istate状态变化的操作,硬件中断处理中PIC对中断线的一些操作,并不保护desc->action。这意味着
Linux内核的硬中断处理程序(desc->handle_irq)将处理中断信号发生后需要立即完成的事情,如网卡数据包的拷贝,响应中断等,而将不太紧急的工作留给软中断去完成。在“六、istate状态”一节中有提到一种ONESHOT状态,就与中断线程相关。使用ps aux | grep ksoftirqd命令,会发现系统中正运行着几个(core总个数)ksoftirq进程:
1 2 3 4 5 | [rock3@e4310 linux-stable]$ ps aux | grep ksoftirqd root 3 0.0 0.0 0 0 ? S 11月12 0:37 [ksoftirqd/0] root 13 0.0 0.0 0 0 ? S 11月12 0:34 [ksoftirqd/1] root 18 0.0 0.0 0 0 ? S 11月12 0:23 [ksoftirqd/2] root 23 0.0 0.0 0 0 ? S 11月12 0:22 [ksoftirqd/3] |
这些进程就是内核启动的中断线程,他是一种内核线程,每个core都有一个,用于处理不太紧急的中断事务,本节就讨论从硬中断处理转向软中断处理的过程,这得从setup_irq()函数(用于设置desc->action)说起。先看下调用关系:
实际上干事的是__setup_irq()函数,__setup_irq(irq,desc,new)将new加入到desc->action链表中。该函数判断如果加入的new->thread_fn不为空,则使用kthread_create()函数创建一个内核线程并将该线程加入到内核线程kthread_create_list链表中,kthread_create()函数返回task_struct结构体指针,该指针被赋值为new->thread,在函数退出__setup_irq()函数返回前,调用wake_up_process()函数来唤醒该new->thread。
当然创建的线程是irq_thread,不过irq_thread()函数将new->thread_fn又包裹了一层:irq_thread()通过irq_thread_fn()函数或者irq_forced_thread_fn()函数来调用new->thread_fn,而irq_thread_fn/irq_forced_thread_fn的补充操作为irq_finalize_oneshot(),即根据ONESHOT状态来执行unmaks_irq()。irq_thread()函数还有不少其他操作,这里就不分析了。
通过kthread_create()创建内核线程,到wake_up_process()唤醒它,中断子系统进入了软中断(softirq)的阶段,该阶段以内核线程的方式来处理中断,被调度器调度。软中断通常有tasklet、工作队列等具体方式。具体原理请详见“软中断原理”一文。
本篇博客实际上是逆向学习Linux中断子系统的过程,本意为看下irq_desc[]数组初始化的过程,结果有很多不明白的地方,就一路跟踪过来,虽然也学到了一些内核中断子系统的东西,但整体上对框架和细节的把握并不到位,估计以后还需要重新系统学习:
1、关于锁的问题,还是没想明白,从找到interrupt[i]的内容,执行do_IRQ()函数,到handle_xxx_irq(),再到handle_irq_event(),在handle_xxx_irq()开始处加锁,而在desc->action处解锁,那么说ISR并不是临界区,可以在两个CPU上执行,而desc->lock只保护desc->istate变化以及PIC的一些操作,而不保护ISR。这个问题很重要,涉及到SMP架构下对中断的处理,涉及到如何与软中断配合。
网上查找了一下,说ISR is not a critical section,但是do_IRQ()是critical section。不明白。desc->action不一定是可重入的,两个CPU同时执行它的时后,就有可能出现错误。
2、转向软中断的一些细节。
参考资料:
1、Understanding Linux Kernel
2、Professional Linux Kernel Architecture
|