irq_desc操作

田園詩人之園發表於2016-09-27

本部落格轉載自:http://rock3.info/blog/2013/11/17/irq_desc陣列的初始化過程/
這篇部落格解析非常精闢,再次表達對原作者的敬意。

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[]陣列
irq_desc[]陣列,在kernel/irq/irqdesc.c中宣告,用於核心管理中斷請求,例如中斷請求來自哪個裝置,使用什麼函式處理,同步資源等:

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結構體,如下圖所示:irq_desc

struct irq_desc結構體(以前的版本結構體的名字是irq_desc_t)定義如下所示(簡化過,include/linux/irqdesc.h)。大部分成員都是輔助性的,關鍵的成員是irq_data、handle_irqs、action、depth、lock、istat,所謂irq_desc[]陣列的初始化,看其主要成員的初始化的過程,在這裡做簡單的說明:

action指標指向具體的裝置驅動提供的中斷處理操作,就是所為的ISR,action本身是一個單向連結串列結構體,由next指標指向下一個操作,因此action實際上是一個操作鏈,可以用於共享IRQ線的情況。
handle_irq是irq_desc結構中與PIC相關的中斷處理函式的介面,通常稱作”hard irq handler“。此函式對應了PIC中的handle_xxx_irq()系列函式(xxx代表觸發方式),do_IRQ()就會呼叫該函式,此函式最終會執行desc->action。
irq_data用於描述PIC方法使用的資料,irq_data下面有兩個比較重要的結構:chip和state_use_accessors,前者表示此irq_desc[]元素時用的PIC晶片型別,其中包含對該晶片的基本操作方法的指標;後者表示該chip的狀態和屬性,其中有些用於判斷irq_desc本身應該所處的狀態。
lock用於SMP下不同core下的同步。
depth表示中斷巢狀深度,也即一箇中斷打斷了幾個其他中斷。
istate表示該desc目前的狀態,將在“六、istate狀態”中描述。

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用於描述軟中斷處理函式。

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為型別:

// 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[]的初始化——8259A
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()函式開始分析,init_IRQ在arch/x86/kernel/irqinit.c中定義:

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:

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;
a) native_inti_IRQ()
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):

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的範圍):

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(擷取一部分):

[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()函式,程式碼如下:

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):

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[]陣列的什麼成員:

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中斷處理函式的操作。

三、irq_desc[]的初始化——IO APIC
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號那?

四、desc->handle_irq
經過“irq_desc[]的初始化”部分的描述desc->handle_irq已經初始化完畢,而desc->handle_irq介面實際上可以掛接幾個函式(8259A和IO APIC):

void handle_level_irq(unsigned int irq, struct irq_desc *desc);(8259A)
void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc);(IO APIC)
void handle_edge_irq(unsigned int irq, struct irq_desc *desc);(IO APIC)
​ 字面意思說明三種處理函式分別代表電平觸發、xxx觸發、邊沿觸發,他們之間各有不同但最終均呼叫了handle_irq_event()。本文中將以以上函式稱為handle_xxx_irq()系列函式(還有其他幾個handle_xxx_irq()函式,也屬於此係列,但是不是x86平臺時用或者不是8259A或IO APIC時用)。他們三者之間在何時遮蔽IRQ線、是否要響應發出中斷訊號的硬體等處有微小的區別。

下面以handle_level_irq()函式為例,看下它具體幹了什麼事情,其他handle_xxx_irq()函式做了基本相同的工作:

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“部分詳細介紹),退出前釋放鎖,然後按照下列次序進行處理:

用mask_irq_ack()函式向產生中斷的硬體發出ACK響應,並暫時遮蔽該中斷線(handle_level_irq()獨有操作)。
用irqd_irq_inprogress()判斷中斷是否處於inprogress階段,如果處於inprogress階段,則校驗並等待“偽中斷”輪詢完畢(關閉“偽中斷”輪詢,詳見“六、istate狀態”)。
去掉desc->istate中的IRQS_REPLAY和IRQS_WAITING標誌(詳見“六、istate狀態”)。
用kstat_incr_irqs_this_cpu()函式更新desc關於cpu的統計資料。
如果desc->action為空或者desc->irq_data處於DISABLE狀態,則將該irq_desc[]元素掛起(desc->istate置位IRQS_PENDING)並返回。
執行handle_irq_event(desc),迴圈呼叫desc->action。
用cond_unmask_irq()函式恢復IRQ線。
handle_irq_event()程式碼如下(kernel/irq/handle.c):

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()函式:

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核心在實現核心中斷處理函式函式時層次分明:

desc->handle_irq,負責與PIC觸發方式相關的操作,並作desc->istate相關校驗、置位;
handle_irq_event(),負責設定desc->irq_data以及解鎖避免其他CPU忙等。
handle_irq_event_percpu(),負責具體的在CPU上執行desc->action以及轉向軟中斷、轉向“偽中斷”輪詢的過程。
五、中斷申請函式request_irq()
從上述描述可以看出,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):

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[]:

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狀態”。

六、istate狀態
Linux核心中的中斷子系統有四種狀態、特性,分別是:

用於描述irq_desc的istate。
用於描述irq_desc->action->handler的flags。
用於描述irq_desc->action->thread_fn的thread_flags。
用於描述irq_desc->irq_data的state_use_accessors。
此處重點介紹istate,但首先還是irq_desc->action->flags。include/linux/interrupt.h檔案中定義了handling routines的一些標誌(並沒有完全列出,還有一些IRQF_TRIGGER_的表示觸發方式的標誌):

/*
* 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_字首,他們可以同時存在,而並不一定相互轉化:

/*
* 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)的過程來完成:

首先,其驅動程式呼叫probe_irq_on()介面,開始auto probe的過程。probe_irq_on()函式將返回一個32bits的irq_desc是否可用的掩碼mask(此處因為返回值型別為int,所以只能返回32位的掩碼)。probe_irq_on()介面首先選出所有可以被偵測(desc->irq_data->status_use_accessors是否包含IRQD_IRQ_NOPROBE標誌)、並且沒有action函式的irq_desc[],並將這些irq_desc->istate設定IRQS_AUTODETECT標誌和IRQS_WAITING標誌;然後過濾掉所有的偽中斷irq_desc[],是否可用通過判斷desc->istate包含IRQS_WAITING狀態(偽中斷經過handle_xxx_irq()處理將清除IRQS_WAITING狀態)。上面的工作對於auto probe都是輔助性的,但最重要的是probe_irq_on()函式在開始時就呼叫了async_synchronize_full()介面,來同步核心中所有非同步呼叫的函式(這樣就不存在發生了中斷,但沒有被記錄的情況)。
其次,驅動程式收到mask掩碼後(當然也可以利用mask做一些事情,但並非所有的驅動都這樣做),主動產生一次硬體中斷。
然後,驅動程式呼叫probe_irq_off(mask),後者偵測所有的irq_desc[],檢視到底是哪些irq_desc[]元素髮生了硬體中斷,如果只發生了一次硬體中斷,那麼就返該irq_desc[]的下標,也即irq號;如果沒有偵測到中斷髮生,則返回0;如果偵測到多次中斷髮生,則返回一個負數(中斷髮生次數的相反數)。probe_irq_off()函式通過desc->istate是否包含IRQS_WAITING標誌來判斷是否發生了一次硬體中斷(包含就沒發生,不包含就發生了)。
最後,驅動程式通過probe_irq_off()的返回值,來判斷是否是一個可用的irq號,如果可用,則通過request_irq()來申請註冊中斷處理函式。
​ 如果再扣的細一點,還有兩個問題:

問題1,async_synchronize_full()函式能夠使所有的irq_desc[]的istate都去掉IRQS_WAITING標誌,這是為什麼?
問題2,probe_irq_off()函式為什麼可以使用帶有IRQS_WAITING標誌來判斷哪個irq描述符發生了硬體中斷?
對問題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()呼叫,他們的呼叫關係如下圖所示:

resend

總結,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()函式:

desc->action = NULL,也即無具體的中斷處理函式;
desc->irq_data->state_use_accessors包含IRQD_IRQ_DISABLED,也即中斷控制器被禁用;
還有一種情況會“掛起”中斷處理,在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的場景:

handle_irq_event()函式開始處即清除IRQS_PENDING標誌;
check_irq_resend()函式,會在PIC重新觸發、向CPU產生中斷訊號前,清除掉IRQS_PENDING標誌;
​ 以下是校驗IRQS_PENDING的場景:

try_one_irq()函式中,如果desc->irq_data->state_use_accessors並不包含IRQD_IRQ_INPROGRESS標誌,但是desc->istate卻包含IRQS_PENDING標誌,並且desc->action不為空,則返回執行action鏈上的函式。
check_wakeup_irq()函式中,對所有的desc,如果desc->depth==1並且置位IRQS_PENDING標誌,則返回-EBUSY(這個地方一定有特殊含義,就不細研究了)。
​ 總結,IRQS_PENDING表示中斷處理被掛起,通常是因為沒有中斷處理函式或者中斷控制器被禁用。通常由handle_xxx_irq()設定,由handle_irq_event()清除。

IRQS_SUSPENDED狀態

字面意思為“暫停”,下面是置位IRQS_SUSPENDED狀態的場景:

在__disable_irq()函式中,如果引數suspend設定為true,則置位IRQS_SUSPENDED狀態。suspend_device_irqs()函式呼叫__disable_irq()函式時,會設定此標誌。
​ 清除IRQS_SUSPENDED狀態場景:

在__enable_irq()函式中,如果引數resume為ture,則清除IRQS_SUSPENDED狀態。resume_device_irqs()函式呼叫__enable_irq()函式時,清除此標誌。
校驗IRQS_SUSPENDED狀態的場景:

在__enable_irq()函式中,如果desc->depth=1,則校驗IRQS_SUSPENDED狀態,如果置位,則退出,不執行irq_enable()。
在check_wakeup_irqs()函式中,具備IRQS_SUSPENDED狀態成為是否執行mask_irq()的一個條件。
在suspend_device_irqs()函式,對所有具備IRQS_SUSPENDED狀態的desc執行 synchronize_irq()。
​ 總結,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的一個屬性,將會在“八、中斷執行緒中詳細討論”:

istate

七、中斷向量、鎖和CPU
中斷向量這個詞經常說,那麼到底什麼是中斷向量那?核心中有兩個概念,一個叫vector,另一個叫irq。vector指的就是“中斷向量”,也就是PIC向CPU傳遞的用於表示發生中斷的IRQ線的一種編號。而irq,也就是常說的irq號,是核心維護的中斷請求的陣列下標。以下說法或變數是等價的(x86架構):

中斷向量號
PIC向CPU傳遞的關於中斷向量號
IDT表的索引號
interrupt[]陣列的下標+0x20
irq_desc[]陣列的下標+0x20
irq_desc->action->irq+0x20
vector_irq[]陣列的下標
irq號+0x20
硬體維護的中斷向量號,在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檔案中定義:

typedef int vector_irq_t[NR_VECTORS];
DECLARE_PER_CPU(vector_irq_t, vector_irq);
通過setup_vector_irq()函式初始化:

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的情況了,但是會發生如下情況:

某CPU在處理某中斷,此刻發生了相同的中斷,要相同的CPU來處理;丟中斷;
某CPU在處理某中斷,此刻發生了相同的中斷,要不同的CPU來處理;避免這種情況;
某CPU在處理某中斷,此刻發生了不同的中斷,要相同的CPU來處理;中斷巢狀;
某CPU在處理某中斷,此刻發生了不同的中斷,要不同的CPU來處理;正常處理;
對於第一種情況,起碼要有一種機制保證正常的丟失中斷,而不是總被自己打斷,而無法完成正常的中斷處理。而對第二種情況,應該儘量避免,因為中斷處理函式不一定是可重入的,因此必須順序執行,一次沒處理完,不能併發處理下一次。Linux核心通過自旋鎖來完成這個工作,也即在handle_xxx_irq()系列函式開始時,獲取鎖,退出時,釋放鎖。Linux核心中斷子系統中至少使用到了4種鎖用於同步不同的資源,分別是:

desc->irq_data.chip->irq_bus_lock
desc->lock
irq_domain_mutex
gc_lock
前面說的鎖就是desc->lock的用處。irq_desc->lock型別為raw_spinlock_t,是一個自選鎖。首先,得看一下自旋鎖的特點和操作。自旋鎖有以下特點:

自旋,同時只能被一個程式持有,只能由持有該鎖的程式解鎖;
忙等,如果其他程式想要獲取鎖,則會一致處於等待狀態而無法做其他事情。
遞迴死鎖,以上兩條說明會有這個現象。
自旋鎖可以時用下列方法去操作:

spin_lock()和spin_unlock()
raw_spin_lock()和raw_spin_unlock()
raw_spin_lock_irq()和raw_spin_unlock_irq()
raw_spin_lock_irqsave()和raw_spin_unlock_irqrestore()
核心在desc->handle_irq到handle_irq_event()介面的過程中,通常這樣處理:

handle_xxx_irq

為什麼要著要麼做?避免同時對呼叫handle_irq_event(),奇怪的是在handle_irq_event()裡,在irqd_set(IRQD_IRQ_INPROGRESS)後,又解鎖,這是為什麼?顯然,核心並不是害怕兩個CPU同時執行了desc->action(除了handle_xxx_irq()系列函式,核心還有其他位置呼叫了desc->action),而是害怕在鎖與解鎖之間的操作被交叉操作,這些操作包括(以handle_level_irq()為例,上圖中沒有標出):

mask_ack_irq(desc),向發出中斷的硬體ACK,並且暫時遮蔽該中斷;
如果irqd_irq_inprogress(),則執行irq_check_poll(),這表示該中斷可能正在“偽中斷”輪詢函式輪詢。(不知道有沒有這種情況,偽中斷輪詢函式沒有輪詢該中斷,desc->irq_data狀態卻不是IN_PROGRESS)
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
kstat_incr_irqs_this_cpu(irq, desc)更新cpu相關的中斷統計;
如果desc->action為空,或者desc->irq_data處於DISABLE的狀態,執行desc->istate |= IRQS_PENDING;
看樣子,desc->lock只保護desc->istate狀態變化的操作,硬體中斷處理中PIC對中斷線的一些操作,並不保護desc->action。這意味著

八、中斷執行緒
Linux核心的硬中斷處理程式(desc->handle_irq)將處理中斷訊號發生後需要立即完成的事情,如網路卡資料包的拷貝,響應中斷等,而將不太緊急的工作留給軟中斷去完成。在“六、istate狀態”一節中有提到一種ONESHOT狀態,就與中斷執行緒相關。使用ps aux | grep ksoftirqd命令,會發現系統中正執行著幾個(core總個數)ksoftirq程式:

[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)說起。先看下呼叫關係:softirq

實際上幹事的是__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[]陣列初始化的過程,結果有很多不明白的地方,就一路跟蹤過來,雖然也學到了一些核心中斷子系統的東西,但整體上對框架和細節的把握並不到位,估計以後還需要重新系統學習:

帶著問題學,逆向思維很重要。
逆向學習的過程應該是先縱向、再橫向,但本篇部落格中很多地方是先縱橫不分,類似籃球裡的“盯球不盯人”,不好。
假設一定要驗證,“我以為”是失敗之母;
用20%的時間學會整體框架,而不是用80%的時間瞭解細節。
從別人那學,會快,但不深刻。
不要學究。
十、遺留問題
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