在之前我們介紹了linux中斷子系統向驅動程序提供了注冊中斷的API:
-
request_threaded_irq;
-
request_irq;
這一節我們將從源碼層面介紹講解它們的實現。
一、cascade IRQ和nested IRQ
linux中斷子系統在注冊中斷的時候,根據中斷控制器的結構實際上對中斷進行了分類,以適配不同的硬件情景,比如cascade IRQ和nested IRQ。
1.1 cascade IRQ
上面是一個兩個VIC級聯的例子,我們可以把VIC0當做S3C2440的中斷控制器,VIC1假設成其外部中斷。為了方便描述:
- VIC1的IRQ編號是EINT4~7;
- 外設1的IRQ編號是EINT4;
- 外設2的IRQ編號是EINT5;
- 外設3的IRQ編號是EINT6;
- 外設4的IRQ編號是EINT7;
對於級聯的中斷控制器,外側的中斷控制器不能直接向ARM內核遞交請求,但它可以向與之相連的內存的中斷控制器請求服務。
當外設1觸發中斷時,中斷信號會遞交給VIC1、然后VIC1會將中斷信號遞交給VIC0,然后再遞交給ARM內核。因此首先會執行EINT4~7中斷描述符desc->handle_irq方法,也就是s3c_irq_demux。
static void s3c_irq_demux(struct irq_desc *desc) { struct irq_chip *chip = irq_desc_get_chip(desc); struct s3c_irq_data *irq_data = irq_desc_get_chip_data(desc); struct s3c_irq_intc *intc = irq_data->intc; struct s3c_irq_intc *sub_intc = irq_data->sub_intc; unsigned int n, offset, irq; unsigned long src, msk; /* we're using individual domains for the non-dt case * and one big domain for the dt case where the subintc * starts at hwirq number 32. */ offset = irq_domain_get_of_node(intc->domain) ? 32 : 0; chained_irq_enter(chip, desc); src = readl_relaxed(sub_intc->reg_pending); msk = readl_relaxed(sub_intc->reg_mask); src &= ~msk; src &= irq_data->sub_bits; while (src) { n = __ffs(src); src &= ~(1 << n); irq = irq_find_mapping(sub_intc->domain, offset + n); generic_handle_irq(irq); } chained_irq_exit(chip, desc); }
然后再s3c_irq_demux會根據EINTPEND寄存器判斷出發生的是哪一個外部中斷,然后根據中斷域調用irq_find_mapping獲取到外部中斷的IRQ編號,最后通過generic_handle_irq調用外部中斷注冊中斷處理函數。
通過上面的分析我們可以總結出:
- 主中斷EINT4~7的中斷描述符被設定為不能注冊irqaction(因為就算注冊,也始終不會執行中斷描述符的irqaction鏈表);
- EINT4~EINT7外部中斷正常進行中斷注冊,通過request_threaded_irq注冊中斷,可以選擇thread interrupt handler,或者只注冊primary handler;
linux內核將EINT4、EINT5、EINT6、EINT7這種中斷稱為cascade IRQ。
1.2 nested IRQ
我們再來看一下另一種中斷控制器級聯的情況:
IO expander模塊提供了有中斷功能的GPIO,因此它也可以看做一個中斷控制器,有它自己的irq domain和irq chip。
上圖外設1和外設2使用了IO expander上有中斷功能的GPIO,它們有屬於自己的IRQ編號以及中斷描述符,IO expander模塊的IRQ line鏈接到SOC內部的中斷控制器上,因此這也是一個中斷控制器級聯的情況,對於這種情況,和我們上面介紹的VIC級聯的情況有何區別。
對於VIC級聯的情況,如果VIC1上的外設1產生了中斷,將會執行中斷描述符的handle_irq,以handle_level_irq流控處理函數為例,其在執行開始會關閉當前中斷,整個關中斷的時間=eint_4t7_desc->handle_irq + eint4->handle + eint4->action執行時間。由於VIC0、VIC1寄存器訪問速度很快,整個關中斷的時間不是很長。
但是如果是IO expander這種情況,如果采用和VIC級聯一樣的處理方式,關中斷的時間會非常長。這時候,由於外設1的的中斷描述符的handle_irq 處理涉及I2C的操作,因此時間非常的長,這時候,對於整個系統的實時性而言是致命的打擊。
為了方便描述:
- VIC的IRQ編號是A;
- 外設1的IRQ編號是B;
- 外設2的IRQ編號是C;
對這種硬件情況,linux kernel處理如下:
- IRQ A的中斷描述符的handle_irq 根據實際情況進行設定,並且允許注冊irqaction;
- 在IRQ A的中斷描述符的threaded interrupt handler中進行IRQ number的映射,在IO expander irq domain上翻譯出具體外設的IRQ number,並直接調用handle_nested_irq函數處理該IRQ;
- IRQ B、IRQ C對應的中斷描述符設定IRQ_NESTED_THREAD的flag,表明這是一個nested IRQ;
- nested IRQ沒有handle_irq,也沒有primary handler,它的threaded interrupt handler是附着在其parent IRQ的threaded interrupt handler上的;
linux內核將IRQ B、IRQ C這種中斷稱為nested IRQ。這里的 nested 並不是中斷嵌套,而是針對某一類特殊的中斷控制器做兼容。
二、中斷注冊
2.1 ISR的安裝
我們在編寫設備驅動程序時,如果需要使用到某個中斷,那么我們就需要在中斷到來之前注冊該中斷的處理處理函數,也就是ISR - Interrupt Service Routine。
由於早期處理器,存在IRQ編號共享問題,因此Linux的中斷子系統是按照服務中斷共享的模式設計的,因此ISR的安裝需要分為兩級:
- 第一級是針對這個IRQ線的,稱為generic handler(或者叫做highlevel irq-events handler),也就是中斷描述符的handler_irq(desc->handle_irq);
- 第二級是針對掛載在這個IRQ線上的不同設備的,稱為specific handler,也就是中斷描述符的irqaction鏈表(desc->action);
上一節我們分析了S3C2440中斷相關的代碼,Generic handler的初始化通過s3c24xx_irq_map中的irq_set_chip_and_handler函數實現的。
Specific handler的安裝則是由request_threaded_irq函數完成的。request_threaded_irq可以選擇線程化的handler,或者只注冊primary handler。
2.2 中斷線程化(threaded interrupt handler)
在linux中,中斷具有最高優先級,無論什么時刻,只要有中斷產生,內核將會立即執行相應的中斷處理處理,等到所有掛起的中斷和軟中斷處理完畢后才能執行正常的任務,因此可能造成實時任務得不到及時的處理。
中斷線程化之后,中斷將作為內核線程運行而被賦予不同的實時優先級,實時任務可以有比中斷線程更高的優先級。這樣,具有最高優先級的實時任務就能得到優先處理,即使在嚴重負載下仍有實時性保證。但是,並不是所有的中斷都可以線程化,比如時鍾中斷,主要用來維護系統時間以及定時器等,其中定時器是操作系統的脈搏,一旦被線程化,就有可能被掛起,這樣后果將不堪設想,所以不應當被線程化。
中斷線程化有一個內核配置選項是 CONFIG_IRQ_FORCED_THREADING,這個內核選項決定了系統中的中斷是不是會被強制線程化,如果該選項配置為 Y,意味着注冊的中斷默認都會創建一個線程。
2.3 request_threaded_irq函數原型
request_threaded_irq函數定義在kernel/irq/manage.c文件中,函數原型:
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)
該函數參數:
- irq:中斷號,所申請的中斷向量;
- handler:中斷處理服務函數 primary handler,該函數工作在中斷上下文中,如果不需要,可以傳入NULL,但是不可以和thread_fn同時為NULL;
- thread_fn:中斷線程化的處理函數 threaded interrupt handler,工作在內核進程上下文中,如果不需要,可以傳入NULL,但是不可以和handler同時為NULL;如果中斷處理函數的返回值是IRQ_WAKE_THREAD,則此時注冊的中斷線程處理函數將被調用,此函數是對中斷處理函數的補充;
- flags:設置中斷類型標志位,定義在include/linux/interrupt.h中;如果為0,則使用默認中斷類型,比如從設備節點interrupts屬性解析到的中斷觸發類型;
- name:指定中斷的名稱;用命令cat /proc/interrupts可查看系統中斷申請與使用情況;
- dev:傳入中斷處理程序的參數,可以為NULL,但在注冊共享中斷時,此參數不能為NULL。該參數可作為共享中斷時的中斷區別參數,還可以把其傳給一個結構體變量,用於保存一個設備的信息,使中斷處理函數可以獲得該設備的信息;
注:傳入request_threaded_irq的handler和thread_fn參數有下面四種組合:
handler | thread_fn | 描述 |
NULL | NULL | 函數出錯,返回-EINVAL |
設定 | 設定 | 正常流程。中斷處理被合理的分配到primary handler和threaded interrupt handler中。 |
設定 | NULL | 中斷處理都是在primary handler中完成,等同於request_irq() |
NULL | 設定 | 這種情況下,handler=irq_default_primary_handler,協助喚醒threaded interrupt handler線程 |
如果返回值是0則說明申請成功,如果申請不成功,則返回的值非零,一般為負數,可能的取值為-16、-38。例如,如果返回值是-16,則說明申請的中斷號在內核中已被占用。
參數flags可能取值如下,該函數需要根據硬件中斷的類型來決定設置哪些標志位:
/* * These correspond to the IORESOURCE_IRQ_* defines in * linux/ioport.h to select the interrupt line behaviour. When * requesting an interrupt without specifying a IRQF_TRIGGER, the * setting should be assumed to be "as already configured", which * may be as per machine or firmware initialisation. */ #define IRQF_TRIGGER_NONE 0x00000000 // 無 #define IRQF_TRIGGER_RISING 0x00000001 // 上升沿 #define IRQF_TRIGGER_FALLING 0x00000002 // 下降沿 #define IRQF_TRIGGER_HIGH 0x00000004 // 高電平 #define IRQF_TRIGGER_LOW 0x00000008 // 低電平 #define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \ // 四種觸發類型,通過或運算可以用來判斷有沒有設置中斷觸發類型 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING) #define IRQF_TRIGGER_PROBE 0x00000010 /* * 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 a 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編號時,設置該標志 #define IRQF_PROBE_SHARED 0x00000100 // 共享中斷允許不匹配發生時設置該標志位 #define __IRQF_TIMER 0x00000200 // 時鍾中斷標志位 #define IRQF_PERCPU 0x00000400 // 屬於某個特定CPU的中斷 #define IRQF_NOBALANCING 0x00000800 // 禁止在多CPU之間做中斷均衡 #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)
2.4 request_threaded_irq函數實現
/** * 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_f. 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); // 根據IRQ編號獲取中斷描述符 if (!desc) return -EINVAL; if (!irq_settings_can_request(desc) || // 判斷一個IRQ是否可以被request,標識位IRQ_NOREQUEST的中斷不能為請求 WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL; if (!handler) { if (!thread_fn) return -EINVAL; handler = irq_default_primary_handler; // 設置默認primary_handler,irq_default_primary_handler默認返回IRQ_WAKE_THREAD } action = kzalloc(sizeof(struct irqaction), GFP_KERNEL); // 分配irqaction結構 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); } return retval; }
主要實現以下功能:
- 首先調用irq_to_desc根據IRQ編號獲取中斷描述符desc;
- 然后分配一個irqaction結構,用參數handler,thread_fn,irqflags,devname,dev_id初始化irqaction結構的各字段;
- 最后把大部分工作委托給__setup_irq函數:
2.5 __setup_irq
__setup_irq函數也是定義在kernel/irq/manage。c文件中:

/* * Internal function to register an irqaction - typically used to * allocate special interrupts that are part of the architecture. * * Locking rules: * * desc->request_mutex Provides serialization against a concurrent free_irq() * chip_bus_lock Provides serialization for slow bus operations * desc->lock Provides serialization against hard interrupts * * chip_bus_lock and desc->lock are sufficient for all other management and * interrupt related functions. desc->request_mutex solely serializes * request/free_irq(). */ static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new) { struct irqaction *old, **old_ptr; unsigned long flags, thread_mask = 0; int ret, nested, shared = 0; if (!desc) return -EINVAL; if (desc->irq_data.chip == &no_irq_chip) return -ENOSYS; if (!try_module_get(desc->owner)) return -ENODEV; new->irq = irq; /* * If the trigger type is not specified by the caller, * then use the default for this interrupt. */ if (!(new->flags & IRQF_TRIGGER_MASK)) new->flags |= irqd_get_trigger_type(&desc->irq_data); /* * Check whether the interrupt nests into another interrupt * thread. */ nested = irq_settings_is_nested_thread(desc); if (nested) { if (!new->thread_fn) { ret = -EINVAL; goto out_mput; } /* * Replace the primary handler which was provided from * the driver for non nested interrupt handling by the * dummy function which warns when called. */ new->handler = irq_nested_primary_handler; } else { if (irq_settings_can_thread(desc)) { ret = irq_setup_forced_threading(new); if (ret) goto out_mput; } } /* * Create a handler thread when a thread function is supplied * and the interrupt does not nest into another interrupt * thread. */ if (new->thread_fn && !nested) { ret = setup_irq_thread(new, irq, false); if (ret) goto out_mput; if (new->secondary) { ret = setup_irq_thread(new->secondary, irq, true); if (ret) goto out_thread; } } /* * Drivers are often written to work w/o knowledge about the * underlying irq chip implementation, so a request for a * threaded irq without a primary hard irq context handler * requires the ONESHOT flag to be set. Some irq chips like * MSI based interrupts are per se one shot safe. Check the * chip flags, so we can avoid the unmask dance at the end of * the threaded handler for those. */ if (desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE) new->flags &= ~IRQF_ONESHOT; /* * Protects against a concurrent __free_irq() call which might wait * for synchronize_hardirq() to complete without holding the optional * chip bus lock and desc->lock. Also protects against handing out * a recycled oneshot thread_mask bit while it's still in use by * its previous owner. */ mutex_lock(&desc->request_mutex); /* * Acquire bus lock as the irq_request_resources() callback below * might rely on the serialization or the magic power management * functions which are abusing the irq_bus_lock() callback, */ chip_bus_lock(desc); /* First installed action requests resources. */ if (!desc->action) { ret = irq_request_resources(desc); if (ret) { pr_err("Failed to request resources for %s (irq %d) on irqchip %s\n", new->name, irq, desc->irq_data.chip->name); goto out_bus_unlock; } } /* * The following block of code has to be executed atomically * protected against a concurrent interrupt and any of the other * management calls which are not serialized via * desc->request_mutex or the optional bus lock. */ raw_spin_lock_irqsave(&desc->lock, flags); old_ptr = &desc->action; old = *old_ptr; if (old) { /* * Can't share interrupts unless both agree to and are * the same type (level, edge, polarity). So both flag * fields must have IRQF_SHARED set and the bits which * set the trigger type must match. Also all must * agree on ONESHOT. * Interrupt lines used for NMIs cannot be shared. */ unsigned int oldtype; if (desc->istate & IRQS_NMI) { pr_err("Invalid attempt to share NMI for %s (irq %d) on irqchip %s.\n", new->name, irq, desc->irq_data.chip->name); ret = -EINVAL; goto out_unlock; } /* * If nobody did set the configuration before, inherit * the one provided by the requester. */ if (irqd_trigger_type_was_set(&desc->irq_data)) { oldtype = irqd_get_trigger_type(&desc->irq_data); } else { oldtype = new->flags & IRQF_TRIGGER_MASK; irqd_set_trigger_type(&desc->irq_data, oldtype); } if (!((old->flags & new->flags) & IRQF_SHARED) || (oldtype != (new->flags & IRQF_TRIGGER_MASK)) || ((old->flags ^ new->flags) & IRQF_ONESHOT)) goto mismatch; /* All handlers must agree on per-cpuness */ if ((old->flags & IRQF_PERCPU) != (new->flags & IRQF_PERCPU)) goto mismatch; /* add new interrupt at end of irq queue */ do { /* * Or all existing action->thread_mask bits, * so we can find the next zero bit for this * new action. */ thread_mask |= old->thread_mask; old_ptr = &old->next; old = *old_ptr; } while (old); shared = 1; } /* * Setup the thread mask for this irqaction for ONESHOT. For * !ONESHOT irqs the thread mask is 0 so we can avoid a * conditional in irq_wake_thread(). */ if (new->flags & IRQF_ONESHOT) { /* * Unlikely to have 32 resp 64 irqs sharing one line, * but who knows. */ if (thread_mask == ~0UL) { ret = -EBUSY; goto out_unlock; } /* * The thread_mask for the action is or'ed to * desc->thread_active to indicate that the * IRQF_ONESHOT thread handler has been woken, but not * yet finished. The bit is cleared when a thread * completes. When all threads of a shared interrupt * line have completed desc->threads_active becomes * zero and the interrupt line is unmasked. See * handle.c:irq_wake_thread() for further information. * * If no thread is woken by primary (hard irq context) * interrupt handlers, then desc->threads_active is * also checked for zero to unmask the irq line in the * affected hard irq flow handlers * (handle_[fasteoi|level]_irq). * * The new action gets the first zero bit of * thread_mask assigned. See the loop above which or's * all existing action->thread_mask bits. */ new->thread_mask = 1UL << ffz(thread_mask); } else if (new->handler == irq_default_primary_handler && !(desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)) { /* * The interrupt was requested with handler = NULL, so * we use the default primary handler for it. But it * does not have the oneshot flag set. In combination * with level interrupts this is deadly, because the * default primary handler just wakes the thread, then * the irq lines is reenabled, but the device still * has the level irq asserted. Rinse and repeat.... * * While this works for edge type interrupts, we play * it safe and reject unconditionally because we can't * say for sure which type this interrupt really * has. The type flags are unreliable as the * underlying chip implementation can override them. */ pr_err("Threaded irq requested with handler=NULL and !ONESHOT for irq %d\n", irq); ret = -EINVAL; goto out_unlock; } if (!shared) { init_waitqueue_head(&desc->wait_for_threads); /* Setup the type (level, edge polarity) if configured: */ if (new->flags & IRQF_TRIGGER_MASK) { ret = __irq_set_trigger(desc, new->flags & IRQF_TRIGGER_MASK); if (ret) goto out_unlock; } /* * Activate the interrupt. That activation must happen * independently of IRQ_NOAUTOEN. request_irq() can fail * and the callers are supposed to handle * that. enable_irq() of an interrupt requested with * IRQ_NOAUTOEN is not supposed to fail. The activation * keeps it in shutdown mode, it merily associates * resources if necessary and if that's not possible it * fails. Interrupts which are in managed shutdown mode * will simply ignore that activation request. */ ret = irq_activate(desc); if (ret) goto out_unlock; desc->istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED | \ IRQS_ONESHOT | IRQS_WAITING); irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); if (new->flags & IRQF_PERCPU) { irqd_set(&desc->irq_data, IRQD_PER_CPU); irq_settings_set_per_cpu(desc); } if (new->flags & IRQF_ONESHOT) desc->istate |= IRQS_ONESHOT; /* Exclude IRQ from balancing if requested */ if (new->flags & IRQF_NOBALANCING) { irq_settings_set_no_balancing(desc); irqd_set(&desc->irq_data, IRQD_NO_BALANCING); } if (irq_settings_can_autoenable(desc)) { irq_startup(desc, IRQ_RESEND, IRQ_START_COND); } else { /* * Shared interrupts do not go well with disabling * auto enable. The sharing interrupt might request * it while it's still disabled and then wait for * interrupts forever. */ WARN_ON_ONCE(new->flags & IRQF_SHARED); /* Undo nested disables: */ desc->depth = 1; } } else if (new->flags & IRQF_TRIGGER_MASK) { unsigned int nmsk = new->flags & IRQF_TRIGGER_MASK; unsigned int omsk = irqd_get_trigger_type(&desc->irq_data); if (nmsk != omsk) /* hope the handler works with current trigger mode */ pr_warn("irq %d uses trigger mode %u; requested %u\n", irq, omsk, nmsk); } *old_ptr = new; irq_pm_install_action(desc, new); /* Reset broken irq detection when installing new handler */ desc->irq_count = 0; desc->irqs_unhandled = 0; /* * Check whether we disabled the irq via the spurious handler * before. Reenable it and give it another chance. */ if (shared && (desc->istate & IRQS_SPURIOUS_DISABLED)) { desc->istate &= ~IRQS_SPURIOUS_DISABLED; __enable_irq(desc); } raw_spin_unlock_irqrestore(&desc->lock, flags); chip_bus_sync_unlock(desc); mutex_unlock(&desc->request_mutex); irq_setup_timings(desc, new); /* * Strictly no need to wake it up, but hung_task complains * when no hard interrupt wakes the thread up. */ if (new->thread) wake_up_process(new->thread); if (new->secondary) wake_up_process(new->secondary->thread); register_irq_proc(irq, desc); new->dir = NULL; register_handler_proc(irq, new); return 0; mismatch: if (!(new->flags & IRQF_PROBE_SHARED)) { pr_err("Flags mismatch irq %d. %08x (%s) vs. %08x (%s)\n", irq, new->flags, new->name, old->flags, old->name); #ifdef CONFIG_DEBUG_SHIRQ dump_stack(); #endif } ret = -EBUSY; out_unlock: raw_spin_unlock_irqrestore(&desc->lock, flags); if (!desc->action) irq_release_resources(desc); out_bus_unlock: chip_bus_sync_unlock(desc); mutex_unlock(&desc->request_mutex); out_thread: if (new->thread) { struct task_struct *t = new->thread; new->thread = NULL; kthread_stop(t); put_task_struct(t); } if (new->secondary && new->secondary->thread) { struct task_struct *t = new->secondary->thread; new->secondary->thread = NULL; kthread_stop(t); put_task_struct(t); } out_mput: module_put(desc->owner); return ret; }
__setup_irq()首先做參數檢查,然后根據需要創建中斷內核線程,這期間處理nested irq、oneshot、中斷共享等問題。該函數實現代碼實在太長了,下面我們具體介紹。
三、__setup_irq
3.1 中斷觸發類型
如果沒有設置中斷觸發類型,則使用默認值;
/* * If the trigger type is not specified by the caller, * then use the default for this interrupt. */ if (!(new->flags & IRQF_TRIGGER_MASK)) new->flags |= irqd_get_trigger_type(&desc->irq_data);
3.1.1 irqd_get_trigger_type
irqd_get_trigger_type函數定義在include/linux/irq.h:
#define __irqd_to_state(d) ACCESS_PRIVATE((d)->common, state_use_accessors) static inline u32 irqd_get_trigger_type(struct irq_data *d) { return __irqd_to_state(d) & IRQD_TRIGGER_MASK; }
irq_common_data.state_use_accessors低4位表示中斷觸發類型,因此這里通過&操作獲取中斷觸發類型。
3.2 nested irq
/* * Check whether the interrupt nests into another interrupt * thread. */ nested = irq_settings_is_nested_thread(desc); // 對於設置了IRQ_NESTED_THREAD類型的中斷描述符,必指定定thread_fn if (nested) { if (!new->thread_fn) { // nested irq不需要handler,但是需要thread_fn ret = -EINVAL; goto out_mput; } /* * Replace the primary handler which was provided from * the driver for non nested interrupt handling by the * dummy function which warns when called. */ new->handler = irq_nested_primary_handler; // 設置默認primary handler } else { if (irq_settings_can_thread(desc)) { // 判斷當前中斷是否可以被線程化 ret = irq_setup_forced_threading(new); // 強制中斷線程化 if (ret) goto out_mput; } }
這段代碼分析如下:
- 在中斷子系統初始化的時候,如果存在這種非常特殊的中斷控制器,就會在相關的 irq desc中設置相應的IRQ_NESTED_THREAD標志,因此 irq_settings_is_nested_thread 函數其實是讀取 desc 中的相關標志以確定當前中斷是不是 nested 中斷;
-
如果IRQ為nested irq,需要提供 thread_fn,同時將 handler 設置為一個無效的 handle,該 handle 什么都不做,只是返回 IRQ_NONE;
-
如果IRQ不是nested irq,如果強制中斷線程化CONFIG_IRQ_FORCED_THREADING被設置,調用 irq_setup_forced_threading 函數:
3.2.1 irq_setup_forced_threading
irq_setup_forced_threading 函數定義如下:
static int irq_setup_forced_threading(struct irqaction *new) { if (!force_irqthreads) return 0; if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT)) // 1 return 0; new->flags |= IRQF_ONESHOT; if (new->handler != irq_default_primary_handler && new->thread_fn) { // 2 new->secondary = kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!new->secondary) return -ENOMEM; new->secondary->handler = irq_forced_secondary_handler; new->secondary->thread_fn = new->thread_fn; new->secondary->dev_id = new->dev_id; new->secondary->irq = new->irq; new->secondary->name = new->name; } set_bit(IRQTF_FORCED_THREAD, &new->thread_flags); new->thread_fn = new->handler; new->handler = irq_default_primary_handler; return 0; }
注1:內核配置強制中斷線程化只是一個默認的配置,如果用戶決定不使用中斷線程化,還是得尊重用戶的決定,有三種情況是不需要強制線程化的:
- IRQF_NO_THREAD 很好理解,就是直接指定不創建內核線程。;
- IRQF_PERCPU 中斷綁定到每個 CPU 的,這種中斷並不大適合線程化,假設一個 8 核的系統,每一個 percpu 的中斷線程化就得創建 8 個內核線程,明顯得不償失;
- IRQF_ONESHOT 這個標志表示在中斷處理程序完全處理完之后該中斷都是關閉的;
對於強制線程化的中斷而言,需要設置 ONESHOT 標志,也就是原本帶有 ONESHOT 標志的中斷不強制線程化,而強制線程化的在線程化過程中需要加上 ONESHOT 標志, 對於實際的處理來說,ONESHOT 類型的中斷處理就是在處理器 mask 中斷,用戶處理完成之后再 unmask 中斷。
注2:對於強制線程化的中斷而言,如果用戶注冊時使用 request_threaded_irq,同時給出了 handler 和 thread_fn,這種情況比較特殊,說明注冊者既希望在中斷上下文中處理一部分,又希望在內核線程中處理一部分,這時候使用了 action 中的以新的 struct irqaction secondary 成員來記錄一個新的線程,用來運行傳入的 thread_fn。
- 普通情況下,函數對用戶傳入的 handler 和 thread_fn 進行了修改:用戶傳入的 handler 變成了線程的 thread_fn,而對應的 handler 賦值為系統默認的 irq_default_primary_handler,根據中斷處理流程的描述, action->handler 會被執行,該函數返回 IRQ_WAKE_THREAD 時就會喚醒線程,這時候用戶傳入的 handler 就放到內核中執行了;
- 強制中斷線程化這個配置選項感覺有點一刀切的意思了,驅動開發者在編寫驅動程序的時候,通常會假設中斷處理程序運行在中斷上下文中,以這個前提來做一些並發或者優化的處理,把這些處理函數放到線程中處理倒也不是不行,只是感覺有些怪,暫時沒見過使用強制線程化配置的系統;
- 在默認不支持強制線程化的系統中,用戶要創建中斷線程就通過 request_threaded_irq 函數注冊中斷,傳入一個thread_fn 函數作為回調函數,但是這個函數並不是該線程的執行函數,和我們使用 kthread_create 傳入的 thread_fn 不是一個概念,只是作為一個在線程中會被調用到的函數;
3.3 中斷線程化
/* * Create a handler thread when a thread function is supplied * and the interrupt does not nest into another interrupt * thread。 */ if (new->thread_fn && !nested) { ret = setup_irq_thread(new, irq, false); // 創建中斷線程 if (ret) goto out_mput; if (new->secondary) { ret = setup_irq_thread(new->secondary, irq, true); if (ret) goto out_thread; } }
3.3.1 setup_irq_thread
其中創建中斷線程對應的源碼為:
static int setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary) { struct task_struct *t; struct sched_param param = { 。sched_priority = MAX_USER_RT_PRIO/2, }; if (!secondary) { t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name); } else { t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name); param。sched_priority -= 1; } if (IS_ERR(t)) return PTR_ERR(t); sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m); /* * We keep the reference to the task struct even if * the thread dies to avoid that the interrupt code * references an already freed task_struct。 */ get_task_struct(t); new->thread = t; /* * Tell the thread to set its affinity。 This is * important for shared interrupt handlers as we do * not invoke setup_affinity() for the secondary * handlers as everything is already set up。 Even for * interrupts marked with IRQF_NO_BALANCE this is * correct as we want the thread to move to the cpu(s) * on which the requesting code placed the interrupt。 */ set_bit(IRQTF_AFFINITY, &new->thread_flags); return 0; }
3.3.2 irq_thread
對應的線程函數為 irq_thread:
/* * Interrupt handler thread */ static int irq_thread(void *data) { struct callback_head on_exit_work; struct irqaction *action = data; // 獲取線程參數irqaction struct irq_desc *desc = irq_to_desc(action->irq); // 根據IRQ編號獲取中斷描述符 irqreturn_t (*handler_fn)(struct irq_desc *desc, // 函數指針 struct irqaction *action); if (force_irqthreads && test_bit(IRQTF_FORCED_THREAD, // 如果是強制線程化 &action->thread_flags)) handler_fn = irq_forced_thread_fn; else handler_fn = irq_thread_fn; init_task_work(&on_exit_work, irq_thread_dtor); task_work_add(current, &on_exit_work, false); irq_thread_check_affinity(desc, action); while (!irq_wait_for_interrupt(action)) { // 1 irq_thread_check_affinity(desc, action); action_ret = handler_fn(desc, action); //2 執行action->thread_fn() if (action_ret == IRQ_WAKE_THREAD) irq_wake_secondary(desc, action); //3 wake_threads_waitq(desc); // 喚醒所有等待當前中斷執行完成的線程 } /* * This is the regular exit path. __free_irq() is stopping the * thread via kthread_stop() after calling * synchronize_hardirq(). So neither IRQTF_RUNTHREAD nor the * oneshot mask bit can be set. */ task_work_cancel(current, irq_thread_dtor); return 0; }
注1:irq_thread 的 data 參數為 action,在申請時 action 可能有兩種:
- 一個是正常的由用戶傳遞參數生成的 action;
- 另一個是強制線程化生成的 action;
根據 action 類型的不同,handle_fn 被賦值為不同的回調函數irq_forced_thread_fn、irq_thread_fn,這部分屬於內核線程的初始化部分。
接下來就是一個 while 循環,判斷 irq_wait_for_interrupt 的返回值,這個函數中也是循環地判斷 action->thread_flags 中的 IRQTF_RUNTHREAD 標志位是否被置位,如果該標志沒有被置位,線程將陷入 TASK_INTERRUPTIBLE 類型的睡眠,如果置位,則會設置任務狀態為TASK_RUNNING,並返回0。
那么,誰來將這個標志位置位並喚醒當前線程呢?
還是要把目光放回到中斷處理流程中,回顧一 lowlevel 的中斷處理函數:
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags) { for_each_action_of_desc(desc, action) { irqreturn_t res; res = action->handler(irq, action->dev_id); // 執行primary handler switch (res) { case IRQ_WAKE_THREAD: __irq_wake_thread(desc, action); // 喚醒中斷線程 ... } ... } }
也就是說,lowlevel 中斷處理部分,不論是普通中斷處理還是線程化的中斷處理,action->handler 還是會被執行,只不過對於線程化中斷,中斷處理的返回值為 IRQ_WAKE_THREAD,這時候將調用 __irq_wake_thread 設置 IRQTF_RUNTHREAD 標志位,並執行 wake_up_process(action->thread) 喚醒中斷線程。
注2: 中斷線程被喚醒之后,將會調用初始化階段賦值的 handler_fn,如果是強制線程化的中斷,對應 irq_forced_thread_fn 否則對應 irq_thread_fn。
/* * Interrupts which are not explicitly requested as threaded * interrupts rely on the implicit bh/preempt disable of the hard irq * context. So we need to disable bh here to avoid deadlocks and other * side effects. */ static irqreturn_t irq_forced_thread_fn(struct irq_desc *desc, struct irqaction *action) { irqreturn_t ret; local_bh_disable(); // 關中斷下半部 ret = action->thread_fn(action->irq, action->dev_id); // 執行threaded interrupt handler if (ret == IRQ_HANDLED) atomic_inc(&desc->threads_handled); irq_finalize_oneshot(desc, action); local_bh_enable(); // 開中斷下半部 return ret; } /* * Interrupts explicitly requested as threaded interrupts want to be * preemtible - many of them need to sleep and wait for slow busses to * complete. */ static irqreturn_t irq_thread_fn(struct irq_desc *desc, struct irqaction *action) { irqreturn_t ret; ret = action->thread_fn(action->irq, action->dev_id); // 執行threaded interrupt handler if (ret == IRQ_HANDLED) atomic_inc(&desc->threads_handled); irq_finalize_oneshot(desc, action); return ret; }
這兩個函數的處理其實差不大多,都是調用 action->thread_fn 函數,也就是用戶傳入的回調函數,不同的是,如果是強制中斷線程化的處理,需要關中斷下半部,下半部主要是 softirq(tasklet 基於 softirq),為啥呢?
- 這是因為用戶調用 request_irq 接口注冊一個中斷,但是內核強制中斷線程化將這個中斷處理函數放到了線程中,這就有可能導致:在用戶看來,我的中斷處理函數就是在中斷上下文中執行的,因此優先級肯定比下半部高,在實際編碼過程中也是依據了這一准則,但實際上放到線程中執行之后,優先級就比中斷下半部低了,這就會導致死鎖或者其它的問題。
注3:如果上述的 handler_fn 依舊返回 IRQ_WAKE_THREAD,那么就會喚醒 secondary action,也就是在支持強制中斷線程化的系統中,用戶在注冊中斷時既傳入了 handler 又傳入了 thread_fn,而且 handler 的返回值為 IRQ_WAKE_THREAD,這種情況下,用戶就可以把一部分工作放到中斷環境下運行,另一部分工作放到內核線程環境中執行。
在所有工作處理完成之后,調用 wake_threads_waitq 喚醒所有等待當前中斷執行完成的進程。
3.4 共享中斷處理
如果一個IRQ編號被若干個設備共享,那么一個IRQ編號對應着若干個irqaction,在編寫設備驅動時,進行設備中斷注冊和釋放的時候我們需要通過dev_id區分具體是哪一個irqaction。
同樣當中斷發生的會后,linux中斷子系統會去遍歷IRQ編號上注冊的irqaction的handler回調函數,這樣,雖然只是一個外設產生的中斷,linux kernel還是把所有共享的那些中斷handler都逐個調用執行。為了讓系統的performance不受影響,irqaction的handler函數必須在函數的最開始進行判斷,是否是自己的硬件設備產生了中斷(讀取硬件的寄存器),如果不是,盡快的退出。
代碼通過判斷desc->action來識別這是不是一個共享中斷。如果desc->action不為空,說名這個中斷已經被其他設備申請過,也就是這是一個共享中斷。
接下來會判斷這個新申請的中斷與已經申請的舊中斷的以下幾個標志是否一致:
- 一定要設置了IRQF_SHARED標志(共享中斷必須設置該函數);
- 電氣觸發方式要完全一樣(IRQF_TRIGGER_XXXX);
- IRQF_PERCPU要一致;
- IRQF_ONESHOT要一致;
代碼如下:
/* * The following block of code has to be executed atomically * protected against a concurrent interrupt and any of the other * management calls which are not serialized via * desc->request_mutex or the optional bus lock。 */ raw_spin_lock_irqsave(&desc->lock, flags); old_ptr = &desc->action; old = *old_ptr; if (old) { /* * Can't share interrupts unless both agree to and are * the same type (level, edge, polarity)。 So both flag * fields must have IRQF_SHARED set and the bits which * set the trigger type must match。 Also all must * agree on ONESHOT。 * Interrupt lines used for NMIs cannot be shared。 */ unsigned int oldtype; if (desc->istate & IRQS_NMI) { pr_err("Invalid attempt to share NMI for %s (irq %d) on irqchip %s。\n", new->name, irq, desc->irq_data。chip->name); ret = -EINVAL; goto out_unlock; } /* * If nobody did set the configuration before, inherit * the one provided by the requester。 */ if (irqd_trigger_type_was_set(&desc->irq_data)) { oldtype = irqd_get_trigger_type(&desc->irq_data); } else { oldtype = new->flags & IRQF_TRIGGER_MASK; irqd_set_trigger_type(&desc->irq_data, oldtype); } if (!((old->flags & new->flags) & IRQF_SHARED) || // 校驗IRQF_SHARED、IRQF_ONESHOP是否一致 (oldtype != (new->flags & IRQF_TRIGGER_MASK)) || ((old->flags ^ new->flags) & IRQF_ONESHOT)) goto mismatch; /* All handlers must agree on per-cpuness */ if ((old->flags & IRQF_PERCPU) != // 校驗IRQF_PERCPU是否一致 (new->flags & IRQF_PERCPU)) goto mismatch; /* add new interrupt at end of irq queue */ do { /* * Or all existing action->thread_mask bits, * so we can find the next zero bit for this * new action。 */ thread_mask |= old->thread_mask; old_ptr = &old->next; old = *old_ptr; } while (old); shared = 1; }
檢查這些條件都是因為多個設備試圖共享一根中斷線,試想一下,如果一個設備要求上升沿中斷,一個設備要求電平中斷,然而只有一根中斷線,實際上只可能設置一種觸發類型。因此共享中斷必須具有相同電氣觸發類型(電平、邊沿、極性)、以及共同標志 IRQF_SHARED、IRQF_ONESHOT、IRQF_PERCPU。
完成檢查后,函數找出action鏈表中最后一個irqaction實例的指針。並設置共享中斷標志位shared=1。
3.5 非共享中斷處理
如果這不是一個共享中斷,或者是共享中斷的第一次申請,將進行如下操作:
- 調用init_waitqueue_head初始化irq_desc結構中斷線程等待隊列:wait_for_threads;用於中斷的同步,比如內核中執行 irq 的 disable 或者 free 時,需要等待最后一次中斷的完成,等待的進程就會睡眠在該等待隊列上;
- 設置觸發模式,用戶在注冊時可以通過傳入 IRQF_TRIGGER_LOW/IRQF_TRIGGER_HIGH 等標志位設置中斷源的觸發方式,如果用戶沒有設置,就使用默認的觸發方式,這個默認方式為低電平觸發。
- 中斷的使能,對於 arm 而言,中斷使能就是設置中斷控制器的相關寄存器,通過調用 irq_chip 的回調函數進行設置;
- 設置中斷的 CPU 親和性,一般來說,中斷會被默認設置分發到一個 CPU 上,而不會分發多個 CPU,畢竟如果分發到多個 CPU,一個中斷源觸發多個 CPU 進入到中斷處理模式,但是最后只有一個 CPU 真正執行處理,這是一種浪費。同時,用戶還可以設置中斷是否可遷移,斷的 target CPU 可以通過用戶空間的 /proc 目錄下的文件進行修改,用戶空間也存在 irq balancing 的 demon 進程,該進程可以根據 CPU 負載動態調整中斷的 CPU 親和性,基於某些實際應用的考慮,用戶可以設置禁止中斷在 CPU 之間的遷移;
代碼如下:
if (!shared) { // 非共享中斷 init_waitqueue_head(&desc->wait_for_threads); /* Setup the type (level, edge polarity) if configured: */ if (new->flags & IRQF_TRIGGER_MASK) { // 如果配置了中斷觸發方式 ret = __irq_set_trigger(desc, // 設置中斷觸發方式 new->flags & IRQF_TRIGGER_MASK); if (ret) goto out_unlock; } /* * Activate the interrupt。 That activation must happen * independently of IRQ_NOAUTOEN。 request_irq() can fail * and the callers are supposed to handle * that。 enable_irq() of an interrupt requested with * IRQ_NOAUTOEN is not supposed to fail。 The activation * keeps it in shutdown mode, it merily associates * resources if necessary and if that's not possible it * fails。 Interrupts which are in managed shutdown mode * will simply ignore that activation request。 */ ret = irq_activate(desc); if (ret) goto out_unlock; desc->istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED | \ IRQS_ONESHOT | IRQS_WAITING); irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); // 清除IRQD_IRQ_INPROGRESS if (new->flags & IRQF_PERCPU) { //如果設置了IRQF_PERCPU irqd_set(&desc->irq_data, IRQD_PER_CPU); irq_settings_set_per_cpu(desc); } if (new->flags & IRQF_ONESHOT) //如果設置了IRQS_ONESHOT desc->istate |= IRQS_ONESHOT; /* Exclude IRQ from balancing if requested */ if (new->flags & IRQF_NOBALANCING) { irq_settings_set_no_balancing(desc); irqd_set(&desc->irq_data, IRQD_NO_BALANCING); } if (irq_settings_can_autoenable(desc)) { irq_startup(desc, IRQ_RESEND, IRQ_START_COND); } else { /* * Shared interrupts do not go well with disabling * auto enable。 The sharing interrupt might request * it while it's still disabled and then wait for * interrupts forever。 */ WARN_ON_ONCE(new->flags & IRQF_SHARED); /* Undo nested disables: */ desc->depth = 1; } } else if (new->flags & IRQF_TRIGGER_MASK) { // 如果設置了電氣觸發類型 unsigned int nmsk = new->flags & IRQF_TRIGGER_MASK; unsigned int omsk = irqd_get_trigger_type(&desc->irq_data); if (nmsk != omsk) /* hope the handler works with current trigger mode */ pr_warn("irq %d uses trigger mode %u; requested %u\n", irq, omsk, nmsk); }
3.6 設置irqaction
如果是非共享中斷,則設置desc->action=new;否則把新的irqaction實例鏈接到action鏈表的最后:
*old_ptr = new; irq_pm_install_action(desc, new); /* Reset broken irq detection when installing new handler */ desc->irq_count = 0; desc->irqs_unhandled = 0; /* * Check whether we disabled the irq via the spurious handler * before。 Reenable it and give it another chance。 */ if (shared && (desc->istate & IRQS_SPURIOUS_DISABLED)) { desc->istate &= ~IRQS_SPURIOUS_DISABLED; __enable_irq(desc); } raw_spin_unlock_irqrestore(&desc->lock, flags); chip_bus_sync_unlock(desc); mutex_unlock(&desc->request_mutex); irq_setup_timings(desc, new);
然后,初始化該中斷相關的統計參數,如 irq_count/irqs_unhandled。
3.7 注冊/proc文件節點
最后,調用wake_up_process喚醒中斷線程,然后調用register_irq_proc注冊相關的/proc文件節點:
/* * Strictly no need to wake it up, but hung_task complains * when no hard interrupt wakes the thread up。 */ if (new->thread) wake_up_process(new->thread); if (new->secondary) wake_up_process(new->secondary->thread); register_irq_proc(irq, desc); new->dir = NULL; register_handler_proc(irq, new);
至此,irq的申請宣告完畢。
參考文章
[1]Linux中斷(interrupt)子系統之三:中斷流控處理層