1. 程式人生 > >Linux kernel的中斷子系統之(四):High level irq event handler

Linux kernel的中斷子系統之(四):High level irq event handler

總結:從架構相關的彙編處理跳轉到Machine/控制器相關的handle_arch_irq,generic_handle_irq作為High level irq event handler入口。

一介紹了進入High level irq event handler的路徑__irq_svc-->irq_handler-->handle_arch_irq,generic_handle_irq是入口函式,在generic_handle_irq_desc指向desc->handle_irq,這裡面就根據中斷型別不同採取不同的handler。

二介紹了IRQ自動探測、IRQ重發等機制。

三介紹了CPU和中斷控制器之間的介面,中斷控制器和外設之間的介面。

四接著一重點介紹了電平和邊沿觸發兩種型別中斷的High level irq event handler。

一、前言

當外設觸發一次中斷後,一個大概的處理過程是:

1、具體CPU architecture相關的模組會進行現場保護然後呼叫machine driver對應的中斷處理handler

2、machine driver對應的中斷處理handler中會根據硬體的資訊獲取HW interrupt ID,並且通過irq domain模組翻譯成IRQ number

3、呼叫該IRQ number對應的high level irq event handler,在這個high level的handler中,會通過和interupt controller互動,進行中斷處理的flow control

(處理中斷的巢狀、搶佔等),當然最終會遍歷該中斷描述符的IRQ action list呼叫外設的specific handler來處理該中斷

4、具體CPU architecture相關的模組會進行現場恢復

注:這份文件充滿了猜測和空想,很多地方描述可能是有問題的,不過我還是把它發出來,拋磚引玉,希望可以引發大家討論。

一、如何進入high level irq event handler

1、從具體CPU architecture的中斷處理到machine相關的處理模組

說到具體的CPU,我們還是用ARM為例好了。對於ARM,我們在ARM中斷處理文件中已經有了較為細緻的描述。這裡我們看看如何從從具體CPU的中斷處理到machine相關的處理模組 ,其具體程式碼如下:

Notes:從中斷向量表中__irq_svc-->irq_handler-->handle_arch_irq,其中handle_arch_irq是由具體的中斷控制器實現。

    .macro    irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
    ldr    r1, =handle_arch_irq
    mov    r0, sp
    adr    lr, BSYM(9997f)
    ldr    pc, [r1]
#else
    arch_irq_handler_default
#endif
9997:
    .endm

其實,直接從CPU的中斷處理跳轉到通用中斷處理模組是不可能的,中斷處理不可能越過interrupt controller這個層次。一般而言,通用中斷處理模組會提供一些通用的中斷程式碼處理庫,然後由interrupt controller這個層次的程式碼呼叫這些通用中斷處理的完成整個的中斷處理過程。“interrupt controller這個層次的程式碼”是和硬體中斷系統設計相關的,例如:系統中有多少個interrupt contrller,每個interrupt controller是如何控制的?它們是如何級聯的?我們稱這些相關的驅動模組為machine interrupt driver。

在上面的程式碼中,如果配置了MULTI_IRQ_HANDLER的話,ARM中斷處理則直接跳轉到一個叫做handle_arch_irq函式,如果系統中只有一個型別的interrupt controller(可能是多個interrupt controller,例如使用兩個級聯的GIC),那麼handle_arch_irq可以在interrupt controller初始化的時候設定。程式碼如下:

……

if (gic_nr == 0) {
        set_handle_irq(gic_handle_irq);
}

……

gic_nr是GIC的編號,linux kernel初始化過程中,每發現一個GIC,都是會指向GIC driver的初始化函式的,不過對於第一個GIC,gic_nr等於0,對於第二個GIC,gic_nr等於1。當然handle_arch_irq這個函式指標不是per CPU的變數,是全部CPU共享的,因此,初始化一次就OK了。

當使用多種型別的interrupt controller的時候(例如HW 系統使用了S3C2451這樣的SOC,這時候,系統有兩種interrupt controller,一種是GPIO type,另外一種是SOC上的interrupt controller),則不適合在interrupt controller中進行設定,這時候,可以考慮在machine driver中設定。在這種情況下,handle_arch_irq 這個函式是在setup_arch函式中根據machine driver設定,具體如下:

handle_arch_irq = mdesc->handle_irq;

關於MULTI_IRQ_HANDLER這個配置項,我們可以再多說幾句。當然,其實這個配置項的名字已經出賣它了。multi irq handler就是說系統中有多個irq handler,可以在run time的時候指定。為何要run time的時候,從多個handler中選擇一個呢?HW interrupt block難道不是固定的嗎?我的理解(猜想)是:一個kernel的image支援多個HW platform,對於不同的HW platform,在執行時檢查HW platform的型別,設定不同的irq handler。

2、interrupt controller相關的程式碼

我們還是以2個級聯的GIC為例來描述interrupt controller相關的程式碼。程式碼如下:

static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
    u32 irqstat, irqnr;
    struct gic_chip_data *gic = &gic_data[0];-------------------------------獲取root GIC的硬體描述符
    void __iomem *cpu_base = gic_data_cpu_base(gic); --------------獲取root GIC mapping到CPU地址空間的資訊

    do {
        irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);------獲取HW interrupt ID
        irqnr = irqstat & ~0x1c00;

        if (likely(irqnr > 15 && irqnr < 1021)) {-------------------------------SPI和PPI的處理
            irqnr = irq_find_mapping(gic->domain, irqnr);------------------將HW interrupt ID轉成IRQ number
            handle_IRQ(irqnr, regs);---------------------------------------------處理該IRQ number
            continue;
        }
        if (irqnr < 16) {------------------------------------------------------------IPI型別的中斷處理
            writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
#ifdef CONFIG_SMP
            handle_IPI(irqnr, regs);
#endif
            continue;
        }
        break;
    } while (1);
}

更多關於GIC相關的資訊,請參考linux kernel的中斷子系統之(七):GIC程式碼分析。對於ARM處理器,handle_IRQ程式碼如下:

void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{

……
        generic_handle_irq(irq);

……
}

3、呼叫high level handler

呼叫high level handler的程式碼邏輯非常簡單,如下:

int generic_handle_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq); ----------通過IRQ number獲取該irq的描述符

    if (!desc)
        return -EINVAL;
    generic_handle_irq_desc(irq, desc);----------------呼叫high level的irq handler來處理該IRQ
    return 0;
}

static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
    desc->handle_irq(irq, desc);---------------------------這裡面的handle_irq指向哪裡呢?
}

Notes:根據不同的中斷號,handle_irq是不同的。在irq_domain_ops的map中處理。

const struct irq_domain_ops gic_irq_domain_ops = {
    .map = gic_irq_domain_map,
    .xlate = gic_irq_domain_xlate,
};

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
                irq_hw_number_t hw)
{
    if (hw < 32) {--------------------------------------------小於32的中斷HW id
        irq_set_percpu_devid(irq);
        irq_set_chip_and_handler(irq, &gic_chip,--------------設定irq對應的irq_desc成員handle_irq為handle_percpu_devid_irq
                     handle_percpu_devid_irq);
        set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN);
    } else {--------------------------------------------------大於等於32的HW id
        irq_set_chip_and_handler(irq, &gic_chip,--------------設定irq對應的irq_desc成員handle_irq為handle_fasteoi_irq,
                     handle_fasteoi_irq);
        set_irq_flags(irq, IRQF_VALID | IRQF_PROBE);
    }
    irq_set_chip_data(irq, d->host_data);
    return 0;
}

其中handle_fasteoi_irq-->handle_irq_event-->handle_irq_event_percpu,下面看看handle_percpu_devid_irq和handle_irq_event_percpu。

有irq號找到對應的irq_desc,irq_desc->action->handler就是對應中斷號的處理函式。

void handle_percpu_devid_irq(unsigned int irq, struct irq_desc *desc)
{
    struct irq_chip *chip = irq_desc_get_chip(desc);
    struct irqaction *action = desc->action;
    void *dev_id = __this_cpu_ptr(action->percpu_dev_id);
    irqreturn_t res;

    kstat_incr_irqs_this_cpu(irq, desc);

    if (chip->irq_ack)
        chip->irq_ack(&desc->irq_data);

    trace_irq_handler_entry(irq, action);
    res = action->handler(irq, dev_id);
    trace_irq_handler_exit(irq, action, res);

    if (chip->irq_eoi)
        chip->irq_eoi(&desc->irq_data);
}


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);
...
return retval;
}

我們在註冊函式的時候使用setup_irq,有兩個引數一個是IRQ number,另一個是irqaction。

    ret=setup_irq(AP_TIMER4_INT, &at4_irq);


static struct irqaction at4_irq = 
{
    .name        = "at4_irq",
    .flags        = IRQF_DISABLED | IRQF_TRIGGER_RISING, 
    .handler    = at4_isr,-------------------------------------對應的中斷處理函式。
};

這樣子就將從HW id到IRQ number,然後通過中斷控制器驅動,再到通過中斷模組,最後呼叫到對應中斷的處理函式。

二、理解high level irq event handler需要的知識準備

1、自動探測IRQ

一個硬體驅動可以通過下面的方法進行自動探測它使用的IRQ:

unsigned long irqs;
int irq;

irqs = probe_irq_on();--------啟動IRQ自動探測
驅動那個打算自動探測IRQ的硬體產生中斷
irq = probe_irq_off(irqs);-------結束IRQ自動探測

如果能夠自動探測到IRQ,上面程式中的irq(probe_irq_off的返回值)就是自動探測的結果。後續程式可以通過request_threaded_irq申請該IRQ。probe_irq_on函式主要的目的是返回一個32 bit的掩碼,通過該掩碼可以知道可能使用的IRQ number有哪些,具體程式碼如下:

unsigned long probe_irq_on(void)
{

……
    for_each_irq_desc_reverse(i, desc) { ----scan 從nr_irqs-1 到0 的中斷描述符
        raw_spin_lock_irq(&desc->lock);
        if (!desc->action && irq_settings_can_probe(desc)) {--------(1)
            desc->istate |= IRQS_AUTODETECT | IRQS_WAITING;-----(2)
            if (irq_startup(desc, false))
                desc->istate |= IRQS_PENDING;
        }
        raw_spin_unlock_irq(&desc->lock);
    }
    msleep(100); --------------------------(3)

    for_each_irq_desc(i, desc) {
        raw_spin_lock_irq(&desc->lock);

        if (desc->istate & IRQS_AUTODETECT) {------------(4)
            if (!(desc->istate & IRQS_WAITING)) {
                desc->istate &= ~IRQS_AUTODETECT;
                irq_shutdown(desc);
            } else
                if (i < 32)------------------------(5)
                    mask |= 1 << i;
        }
        raw_spin_unlock_irq(&desc->lock);
    }

    return mask;
}

(1)那些能自動探測IRQ的中斷描述符需要具體兩個條件:

a、該中斷描述符還沒有通過request_threaded_irq或者其他方式申請該IRQ的specific handler(也就是irqaction資料結構)

b、該中斷描述符允許自動探測(不能設定IRQ_NOPROBE)

(2)如果滿足上面的條件,那麼該中斷描述符屬於備選描述符。設定其internal state為IRQS_AUTODETECT | IRQS_WAITING。IRQS_AUTODETECT表示本IRQ正處於自動探測中。

(3)在等待過程中,系統仍然允許,各種中斷依然會觸發。在各種high level irq event handler中,總會有如下的程式碼:

desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

這裡會清除IRQS_WAITING狀態。

(4)這時候,我們還沒有控制那個想要自動探測IRQ的硬體產生中斷,因此處於自動探測中,並且IRQS_WAITING並清除的一定不是我們期待的IRQ(可能是spurious interrupts導致的),這時候,clear IRQS_AUTODETECT,shutdown該IRQ。

(5)最大探測的IRQ是31(mask是一個32 bit的value),mask返回的是可能的irq掩碼。

我們再來看看probe_irq_off的程式碼:

int probe_irq_off(unsigned long val)
{
    int i, irq_found = 0, nr_of_irqs = 0;
    struct irq_desc *desc;

    for_each_irq_desc(i, desc) {
        raw_spin_lock_irq(&desc->lock);

        if (desc->istate & IRQS_AUTODETECT) {----只有處於IRQ自動探測中的描述符才會被處理
            if (!(desc->istate & IRQS_WAITING)) {----找到一個潛在的中斷描述符
                if (!nr_of_irqs)
                    irq_found = i;
                nr_of_irqs++;
            }
            desc->istate &= ~IRQS_AUTODETECT; ----IRQS_WAITING沒有被清除,說明該描述符
            irq_shutdown(desc);                                     不是自動探測的那個,shutdown之
        }
        raw_spin_unlock_irq(&desc->lock);
    }
    mutex_unlock(&probing_active);

    if (nr_of_irqs > 1) ------如果找到多於1個的IRQ,說明探測失敗,返回負的IRQ個數資訊
        irq_found = -irq_found;

    return irq_found;
}

因為在呼叫probe_irq_off已經觸發了自動探測IRQ的那個硬體中斷,因此在該中斷的high level handler的執行過程中,該硬體對應的中斷描述符的IRQS_WAITING標緻應該已經被清除,因此probe_irq_off函式scan中斷描述符DB,找到處於auto probe中,而且IRQS_WAITING標緻被清除的那個IRQ。如果找到一個,那麼探測OK,返回該IRQ number,如果找到多個,說明探測失敗,返回負的IRQ個數資訊,沒有找到的話,返回0。

2、resend一箇中斷

一個ARM SOC總是有很多的GPIO,有些GPIO可以提供中斷功能,這些GPIO的中斷可以配置成level trigger或者edge trigger。一般而言,大家都更喜歡用level trigger的中斷。有的SOC只能是有限個數的GPIO可以配置成電平中斷,因此,在專案初期進行pin define的時候,大家都在爭搶電平觸發的GPIO。

電平觸發的中斷有什麼好處呢?電平觸發的中斷很簡單、直接,只要硬體檢測到硬體事件(例如有資料到來),其assert指定的電平訊號,CPU ack該中斷後,電平訊號消失。但是對於邊緣觸發的中斷,它是用一個上升沿或者下降沿告知硬體的狀態,這個狀態不是一個持續的狀態,如果軟體處理不好,容易丟失中斷。

什麼時候會resend一箇中斷呢?我們考慮一個簡單的例子:

(1)CPU A上正在處理x外設的中斷

(2)x外設的中斷再次到來(CPU A已經ack該IRQ,因此x外設的中斷可以再次觸發),這時候其他CPU會處理它(mask and ack),並設定該中斷描述符是pending狀態,並委託CPU A處理該pending狀態的中斷。需要注意的是CPU已經ack了該中斷,因此該中斷的硬體狀態已經不是pending狀態,無法觸發中斷了,這裡的pending狀態是指中斷描述符的軟體狀態。

(3)CPU B上由於同步的需求,disable了x外設的IRQ,這時候,CPU A沒有處理pending狀態的x外設中斷就離開了中斷處理過程。

(4)當enable x外設的IRQ的時候,需要檢測pending狀態以便resend該中斷,否則,該中斷會丟失的

具體程式碼如下:

void check_irq_resend(struct irq_desc *desc, unsigned int irq)
{
    if (irq_settings_is_level(desc)) {-------電平中斷不存在resend的問題
        desc->istate &= ~IRQS_PENDING;
        return;
    }
    if (desc->istate & IRQS_REPLAY)----如果已經設定resend的flag,退出就OK了,這個應該
        return;                                                和irq的enable disable能多層巢狀相關
    if (desc->istate & IRQS_PENDING) {-------如果有pending的flag則進行處理
        desc->istate &= ~IRQS_PENDING;
        desc->istate |= IRQS_REPLAY; ------設定retrigger標誌

        if (!desc->irq_data.chip->irq_retrigger ||
            !desc->irq_data.chip->irq_retrigger(&desc->irq_data)) {----呼叫底層irq chip的callback
#ifdef CONFIG_HARDIRQS_SW_RESEND
也可以使用軟體手段來完成resend一箇中斷,具體程式碼省略,有興趣大家可以自己看看
#endif
        }
    }
}

在各種high level irq event handler中,總會有如下的程式碼:

desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

這裡會清除IRQS_REPLAY狀態,表示該中斷已經被retrigger,一次resend interrupt的過程結束。

3、unhandled interrupt和spurious interrupt

在中斷處理的最後,總會有一段程式碼如下:

irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{

……

    if (!noirqdebug)
        note_interrupt(irq, desc, retval);
    return retval;
}

note_interrupt就是進行unhandled interrupt和spurious interrupt處理的。對於這類中斷,linux kernel有一套複雜的機制來處理,你可以通過command line引數(noirqdebug)來控制開關該功能。

當發生了一箇中斷,但是沒有被處理(有兩種可能,一種是根本沒有註冊的specific handler,第二種是有handler,但是handler否認是自己對應的裝置觸發的中斷),怎麼辦?毫無疑問這是一個異常狀況,那麼kernel是否要立刻採取措施將該IRQ disable呢?也不太合適,畢竟interrupt request訊號線是允許共享的,直接disable該IRQ有可能會下手太狠,kernel採取了這樣的策略:如果該IRQ觸發了100,000次,但是99,900次沒有處理,在這種條件下,我們就是disable這個interrupt request line。多麼有情有義的策略啊!相關的控制資料在中斷描述符中,如下:

struct irq_desc {
……
    unsigned int        irq_count;--------記錄發生的中斷的次數,每100,000則回滾
    unsigned long        last_unhandled;-----上一次沒有處理的IRQ的時間點
    unsigned int        irqs_unhandled;------沒有處理的次數
……
}

irq_count和irqs_unhandled都是比較直觀的,為何要記錄unhandled interrupt發生的時間呢?我們來看具體的程式碼。具體的相關程式碼位於note_interrupt中,如下:

void note_interrupt(unsigned int irq, struct irq_desc *desc,  irqreturn_t action_ret)
{
    if (desc->istate & IRQS_POLL_INPROGRESS ||  irq_settings_is_polled(desc))
        return;

    if (action_ret == IRQ_WAKE_THREAD)----handler返回IRQ_WAKE_THREAD是正常情況
        return;

    if (bad_action_ret(action_ret)) {-----報告錯誤,這些是由於specific handler的返回錯誤導致的
        report_bad_irq(irq, desc, action_ret);
        return;
    }

    if (unlikely(action_ret == IRQ_NONE)) {-------是unhandled interrupt
        if (time_after(jiffies, desc->last_unhandled + HZ/10))---(1)
            desc->irqs_unhandled = 1;---重新開始計數
        else
            desc->irqs_unhandled++;---判定為unhandled interrupt,計數加一
        desc->last_unhandled = jiffies;-------儲存本次unhandled interrupt對應的jiffies時間
    }

if (unlikely(try_misrouted_irq(irq, desc, action_ret))) {---是否啟動Misrouted IRQ fixup
    int ok = misrouted_irq(irq);
    if (action_ret == IRQ_NONE)
        desc->irqs_unhandled -= ok;
}

    desc->irq_count++;
    if (likely(desc->irq_count < 100000))-----------(2)
        return;

    desc->irq_count = 0;
    if (unlikely(desc->irqs_unhandled > 99900)) {--------(3)
        __report_bad_irq(irq, desc, action_ret);---報告錯誤
        desc->istate |= IRQS_SPURIOUS_DISABLED;
        desc->depth++;
        irq_disable(desc);

        mod_timer(&poll_spurious_irq_timer,----------(4)
              jiffies + POLL_SPURIOUS_IRQ_INTERVAL);
    }
    desc->irqs_unhandled = 0;
}

(1)是否是一次有效的unhandled interrupt還要根據時間來判斷。一般而言,當硬體處於異常狀態的時候往往是非常短的時間觸發非常多次的中斷,如果距離上次unhandled interrupt的時間超過了10個jiffies(如果HZ=100,那麼時間就是100ms),那麼我們要把irqs_unhandled重新計數。如果不這麼處理的話,隨著時間的累計,最終irqs_unhandled可能會達到99900次的,從而把這個IRQ錯誤的推上了審判臺。

(2)irq_count每次都會加一,記錄IRQ被觸發的次數。但只要大於100000才啟動 step (3)中的檢查。一旦啟動檢查,irq_count會清零,irqs_unhandled也會清零,進入下一個檢查週期。

(3)如果滿足條件(IRQ觸發了100,000次,但是99,900次沒有處理),disable該IRQ。

(4)啟動timer,輪詢整個系統中的handler來處理這個中斷(輪詢啊,絕對是真愛啊)。這個timer的callback函式定義如下:

static void poll_spurious_irqs(unsigned long dummy)
{
    struct irq_desc *desc;
    int i;

    if (atomic_inc_return(&irq_poll_active) != 1)----確保系統中只有一個excuting thread進入臨界區
        goto out;
    irq_poll_cpu = smp_processor_id(); ----記錄當前正在polling的CPU

    for_each_irq_desc(i, desc) {------遍歷所有的中斷描述符
        unsigned int state;

        if (!i)-------------越過0號中斷描述符。對於X86,這是timer的中斷
             continue;

        /* Racy but it doesn't matter */
        state = desc->istate;
        barrier();
        if (!(state & IRQS_SPURIOUS_DISABLED))----名花有主的那些就不必考慮了
            continue;

        local_irq_disable();
        try_one_irq(i, desc, true);---------OK,嘗試一下是不是可以處理
        local_irq_enable();
    }
out:
    atomic_dec(&irq_poll_active);
    mod_timer(&poll_spurious_irq_timer,--------一旦觸發了該timer,就停不下來
          jiffies + POLL_SPURIOUS_IRQ_INTERVAL);
}

三、和high level irq event handler相關的硬體描述

1、CPU layer和Interrupt controller之間的介面

從邏輯層面上看,CPU和interrupt controller之間的介面包括:

(1)觸發中斷的signal。一般而言,這個(些)訊號是電平觸發的。對於ARM CPU,它是nIRQ和nFIQ訊號線,對於X86,它是INT和NMI訊號線,對於PowerPC,這些訊號線包括MC(machine check)、CRIT(critical interrupt)和NON-CRIT(Non critical interrupt)。對於linux kernel的中斷子系統,我們只使用其中一個訊號線(例如對於ARM而言,我們只使用nIRQ這個訊號線)。這樣,從CPU層面看,其邏輯動作非常的簡單,不區分優先順序,觸發中斷的那個訊號線一旦assert,並且CPU沒有mask中斷,那麼軟體就會轉到一個異常向量執行,完畢後返回現場

(2)Ack中斷的signal。這個signal可能是物理上的一個連線CPU和interrupt controller的銅線,也可能不是。對於X86+8259這樣的結構,Ack中斷的signal就是nINTA訊號線,對於ARM+GIC而言,這個訊號就是總線上的一次訪問(讀Interrupt Acknowledge Register暫存器)。CPU ack中斷標識cpu開啟啟動中斷服務程式(specific handler)去處理該中斷。對於X86而言,ack中斷可以讓8259將interrupt vector資料送到資料匯流排上,從而讓CPU獲取了足夠的處理該中斷的資訊。對於ARM而言,ack中斷的同時也就是獲取了發生中斷的HW interrupt ID,總而言之,ack中斷後,CPU獲取了足夠開啟執行中斷處理的資訊。

(3)結束中斷(EOI,end of interrupt)的signal。這個signal用來標識CPU已經完成了對該中斷的處理(specific handler或者ISR,interrupt serivce routine執行完畢)。實際的物理形態這裡就不描述了,和ack中斷signal是類似的。

(4)控制匯流排和資料匯流排介面。通過這些介面,CPU可以訪問(讀寫)interrupt controller的暫存器。

2、Interrupt controller和Peripheral device之間的介面

所有的系統中,Interrupt controller和Peripheral device之間的介面都是一個Interrupt Request訊號線。外設通過這個訊號線上的電平或者邊緣向CPU(實際上是通過interrupt controller)申請中斷服務。

四、幾種典型的high level irq event handler

本章主要介紹幾種典型的high level irq event handler,在進行high level irq event handler的設定的時候需要注意,不是外設使用電平觸發就選用handle_level_irq,選用什麼樣的high level irq event handler是和Interrupt controller的行為以及外設電平觸發方式決定的。介紹每個典型的handler之前,我會簡單的描述該handler要求的硬體行為,如果該外設的中斷系統符合這個硬體行為,那麼可以選擇該handler為該中斷的high level irq event handler。

Notes:不同型別的觸發方式體現在handle_irq,通過irq_set_chip_and_handler註冊。有handle_edge_irq、handle_level_irq、handle_simple_irq、handle_percpu_devid_irq、handle_fasteoi_irq。

註冊介面有如下,最接近中斷處理函式的是handle_irq_event_percpu和handle_percpu_devid_irq。

1.handle_edge_irq-->
2.handle_level_irq-->
3.handle_simple_irq-->
4.handle_fasteoi_irq-->
    handle_irq_event-->
        handle_irq_event_percpu
5.handle_percpu_devid_irq

1、邊緣觸發的handler。

使用handle_edge_irq這個handler的硬體中斷系統行為如下:

xyz

我們以上升沿為例描述邊緣中斷的處理過程(下降沿的觸發是類似的)。當interrupt controller檢測到了上升沿訊號,會將該上升沿狀態(pending)鎖存在暫存器中,並通過中斷的signal向CPU觸發中斷。需要注意:這時候,外設和interrupt controller之間的interrupt request訊號線會保持高電平,這也就意味著interrupt controller不可能檢測到新的中斷訊號(本身是高電平,無法形成上升沿)。這個高電平訊號會一直保持到軟體ack該中斷(呼叫irq chip的irq_ack callback函式)。ack之後,中斷控制器才有可能繼續探測上升沿,觸發下一次中斷

ARM+GIC組成的系統不符合這個型別。雖然GIC提供了IAR(Interrupt Acknowledge Register)暫存器來讓ARM來ack中斷,但是,在呼叫high level handler之前,中斷處理程式需要通過讀取IAR暫存器獲得HW interrpt ID並轉換成IRQ number,因此實際上,對於GIC的irq chip,它是無法提供本場景中的irq_ack函式的。很多GPIO type的interrupt controller符合上面的條件,它們會提供pending狀態暫存器,讀可以獲取pending狀態,而向pending狀態暫存器寫1可以ack該中斷,讓interrupt controller可以繼續觸發下一次中斷。

handle_edge_irq程式碼如下:

void handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock); -----------------(0)

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);----參考上一章的描述
    if (unlikely(irqd_irq_disabled(&desc->irq_data) ||-----------(1)
             irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {
        if (!irq_check_poll(desc)) {
            desc->istate |= IRQS_PENDING;
            mask_ack_irq(desc);
            goto out_unlock;
        }
    }
    kstat_incr_irqs_this_cpu(irq, desc); ---更新該IRQ統計資訊

    desc->irq_data.chip->irq_ack(&desc->irq_data); ---------(2)

    do {
        if (unlikely(!desc->action)) { -----------------(3)
            mask_irq(desc);
            goto out_unlock;
        }

        if (unlikely(desc->istate & IRQS_PENDING)) { ---------(4)
            if (!irqd_irq_disabled(&desc->irq_data) &&
                irqd_irq_masked(&desc->irq_data))
                unmask_irq(desc);
        }

        handle_irq_event(desc); -------------------(5)

    } while ((desc->istate & IRQS_PENDING) &&
         !irqd_irq_disabled(&desc->irq_data)); -------------(6)

out_unlock:
    raw_spin_unlock(&desc->lock); -----------------(7)
}

(0) 這時候,中斷仍然是關閉的,因此不會有來自本CPU的併發,使用raw spin lock就防止其他CPU上對該IRQ的中斷描述符的訪問。針對該spin lock,我們直觀的感覺是raw_spin_lock和(7)中的raw_spin_unlock是成對的,實際上並不是,handle_irq_event中的程式碼是這樣的:

irqreturn_t handle_irq_event(struct irq_desc *desc)
{

    raw_spin_unlock(&desc->lock); -------和上面的(0)對應

    處理具體的action list

    raw_spin_lock(&desc->lock);--------和上面的(7)對應
}

實際上,由於在handle_irq_event中處理action list的耗時還是比較長的,因此處理具體的action list的時候並沒有持有中斷描述符的spin lock。在如果那樣的話,其他CPU在對中斷描述符進行操作的時候需要spin的時間會很長的。

(1)判斷是否需要執行下面的action list的處理。這裡分成幾種情況:

a、該中斷事件已經被其他的CPU處理了

b、該中斷被其他的CPU disable了

c、該中斷描述符沒有註冊specific handler。這個比較簡單,如果沒有irqaction,根本沒有必要呼叫action list的處理

如果該中斷事件已經被其他的CPU處理了,那麼我們僅僅是設定pending狀態(為了委託正在處理的該中斷的那個CPU進行處理),mask_ack_irq該中斷並退出就OK了,並不做具體的處理。另外正在處理該中斷的CPU會檢查pending狀態,並進行處理的。同樣的,如果該中斷被其他的CPU disable了,本就不應該繼續執行該中斷的specific handler,我們也是設定pending狀態,mask and ack中斷就退出了。當其他CPU的程式碼離開臨界區,enable 該中斷的時候,軟體會檢測pending狀態並resend該中斷。

這裡的irq_check_poll程式碼如下:

static bool irq_check_poll(struct irq_desc *desc)
{
    if (!(desc->istate & IRQS_POLL_INPROGRESS))
        return false;
    return irq_wait_for_poll(desc);
}

IRQS_POLL_INPROGRESS標識了該IRQ正在被polling(上一章有描述),如果沒有被輪詢,那麼返回false,進行正常的設定pending標記、mask and ack中斷。如果正在被輪詢,那麼需要等待poll結束。

(2)ack該中斷。對於中斷控制器,一旦被ack,表示該外設的中斷被enable,硬體上已經準備好觸發下一次中斷了。再次觸發的中斷會被排程到其他的CPU上。現在,我們可以再次回到步驟(1)中,為什麼這裡用mask and ack而不是單純的ack呢?如果單純的ack則意味著後續中斷還是會觸發,這時候怎麼處理?在pending+in progress的情況下,我們要怎麼處理?記錄pending的次數,有意義嗎?由於中斷是完全非同步的,也有可能pending的標記可能在另外的CPU上已經修改為replay的標記,這時候怎麼辦?當事情變得複雜的時候,那一定是本來方向就錯了,因此,mask and ack就是最好的策略,我已經記錄了pending狀態,不再考慮pending巢狀的情況。

(3)在呼叫specific handler處理具體的中斷的時候,由於不持有中斷描述符的spin lock,因此其他CPU上有可能會登出其specific handler,因此do while迴圈之後,desc->action有可能是NULL,如果是這樣,那麼mask irq,然後退出就OK了

(4)如果中斷描述符處於pending狀態,那麼一定是其他CPU上又觸發了該interrupt source的中斷,並設定了pending狀態,“委託”本CPU進行處理,這時候,需要把之前mask住的中斷進行unmask的操作一旦unmask了該interrupt source,後續的中斷可以繼續觸發,由其他的CPU處理(仍然是設定中斷描述符的pending狀態,委託當前正在處理該中斷請求的那個CPU進行處理)。

(5)處理該中斷請求事件

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
    struct irqaction *action = desc->action;
    irqreturn_t ret;

    desc->istate &= ~IRQS_PENDING;----CPU已經準備處理該中斷了,因此,清除pending狀態
    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);--設定INPROGRESS的flag
    raw_spin_unlock(&desc->lock);

    ret = handle_irq_event_percpu(desc, action); ---遍歷action list,呼叫specific handler

    raw_spin_lock(&desc->lock);
    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);---處理完成,清除INPROGRESS標記
    return ret;
}

(6)只要有pending標記,就說明該中斷還在pending狀態,需要繼續處理。當然,如果有其他的CPU disable了該interrupt source,那麼本次中斷結束處理。

2、電平觸發的handler

使用handle_level_irq這個handler的硬體中斷系統行為如下:

level

我們以高電平觸發為例。當interrupt controller檢測到了高電平訊號,並通過中斷的signal向CPU觸發中斷。這時候,對中斷控制器進行ack並不能改變interrupt request signal上的電平狀態一直要等到執行具體的中斷服務程式(specific handler),對外設進行ack的時候,電平訊號才會恢復成低電平在對外設ack之前,中斷狀態一直是pending的,如果沒有mask中斷,那麼中斷控制器就會assert CPU

handle_level_irq的程式碼如下:

void handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    mask_ack_irq(desc); ---------------------(1)

    if (unlikely(irqd_irq_inprogress(&desc->irq_data)))---------(2)
        if (!irq_check_poll(desc))
            goto out_unlock;

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);--和retrigger中斷以及自動探測IRQ相關
    kstat_incr_irqs_this_cpu(irq, desc);

    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {-----(3)
        desc->istate |= IRQS_PENDING;
        goto out_unlock;
    }

    handle_irq_event(desc);

    cond_unmask_irq(desc); --------------(4)

out_unlock:
    raw_spin_unlock(&desc->lock);
}

(1)考慮CPU<------>interrupt controller<------>device這樣的連線方式中,我們認為high level handler主要是和interrupt controller互動,而specific handler(request_irq註冊的那個)是和device進行互動。Level型別的中斷的特點就是只要外設interrupt request line的電平狀態是有效狀態,對於interrupt controller,該外設的interrupt總是active的。由於外設檢測到了事件(比如資料到來了),因此assert了指定的電平訊號,這個電平訊號會一直保持,直到軟體清除了外設的狀態暫存器。但是,high level irq event handler這個層面只能操作Interrupt controller,不能操作具體外設的暫存器(那應該屬於具體外設的specific interrupt handler處理內容,該handler會掛入中斷描述符中的IRQ action list)。直到在具體的中斷服務程式(specific handler中)操作具體外設的暫存器,才能讓這個asserted電平訊號訊息

正是因為level trigger的這個特點,因此,在high level handler中首先mask並ack該IRQ。這一點和邊緣觸發的high level handler有顯著的不同,在handle_edge_irq中,我們僅僅是ack了中斷,並沒有mask,因為邊緣觸發的中斷稍縱即逝,一旦mask了該中斷,容易造成中斷丟失。而對於電平中斷,我們不得不mask住該中斷,如果不mask住,只要CPU ack中斷,中斷控制器將持續的assert CPU中斷(因為有效電平狀態一直保持)。如果我們mask住該中斷,中斷控制器將不再轉發該interrupt source來的中斷,因此,所有的CPU都不會感知到該中斷,直到軟體unmask。這裡的ack是針對interrupt controller的ack,本身ack就是為了clear interrupt controller對該IRQ的狀態暫存器,不過由於外部的電平仍然是有效訊號,其實未必能清除interrupt controller的中斷狀態,不過這是和中斷控制器硬體實現相關的。

(2)對於電平觸發的high level handler,我們一開始就mask並ack了中斷,因此後續specific handler因該是序列化執行的,為何要判斷in progress標記呢?不要忘記spurious interrupt,那裡會直接呼叫handler來處理spurious interrupt。

(3)這裡有兩個場景

a、沒有註冊specific handler。如果沒有註冊handler,那麼保持mask並設定pending標記(這個pending標記有什麼作用還沒有想明白)。

b、該中斷被其他的CPU disable了。如果該中斷被其他的CPU disable了,本就不應該繼續執行該中斷的specific handler,我們也是設定pending狀態,mask and ack中斷就退出了。當其他CPU的程式碼離開臨界區,enable 該中斷的時候,軟體會檢測pending狀態並resend該中斷。

(4)為何是有條件的unmask該IRQ?正常的話當然是umask就OK了,不過有些threaded interrupt(這個概念在下一份文件中描述)要求是one shot的(首次中斷,specific handler中開了一槍,wakeup了irq handler thread,如果允許中斷巢狀,那麼在specific handler會多次開槍,這也就不是one shot了,有些IRQ的handler thread要求是one shot,也就是不能巢狀specific handler)。

3、支援EOI的handler

TODO

相關推薦

Linux kernel中斷子系統High level irq event handler

總結:從架構相關的彙編處理跳轉到Machine/控制器相關的handle_arch_irq,generic_handle_irq作為High level irq event handler入口。 一介紹了進入High level irq event handler的路徑__irq_svc-->irq_

Linux kernel中斷子系統驅動申請中斷API

思路 esc 設計師 數組 還需 申請 進一步 time num 一、前言本文主要的議題是作為一個普通的驅動工程師,在撰寫自己負責的驅動的時候,如何向Linux Kernel中的中斷子系統註冊中斷處理函數?為了理解註冊中斷的接口,必須了解一些中斷線程化(threaded i

Linux kernel中斷子系統綜述

lock www. api cdc 電平 還需 結構 現在 ces 一、前言一個合格的linux驅動工程師需要對kernel中的中斷子系統有深刻的理解,只有這樣,在寫具體driver的時候才能:1、正確的使用linux kernel提供的的API,例如最著名的request

Linux kernel中斷子系統IRQ number和中斷描述符

總結: 二描述了中斷處理示意圖,以及關中斷、開中斷,和IRQ number重要概念。 三介紹了三個重要的結構體,irq_desc、irq_data、irq_chip及其之間關係。 四介紹了irq_desc這個全域性變數的初始化,五是操作中斷描述符相關結構體的API介面介紹。 一、前言 本文主要圍繞IRQ

Linux kernel中斷子系統驅動申請中斷API

總結:二重點區分了搶佔式核心和非搶佔式核心的區別:搶佔式核心可以在核心空間進行搶佔,通過對中斷處理進行執行緒化可以提高Linux核心實時性。 三介紹了Linux中斷註冊函式request_threaded_irq,其實request_irq也是對request_threaded_irq的封裝。 四對requ

Linux kernel中斷子系統ARM中斷處理過程

總結:二中斷處理經過兩種模式:IRQ模式和SVC模式,這兩種模式都有自己的stack,同時涉及到異常向量表中的中斷向量。 三ARM處理器在感知到中斷之後,切換CPSR暫存器模式到IRQ;儲存CPSR和PC;mask irq;PC指向irq vector。 四進入中斷的IRQ模式相關處理,然後根據當前處於使用

Linux核心同步機制spin lock[轉]

內容轉自蝸窩科技-http://www.wowotech.net/kernel_synchronization/spinlock.html,並進行適當地排版。 0 前言 在linux kernel的實現中,經常會遇到這樣的場景:共享資料被中斷上下文和程序上下文訪問,該如何保

Linux 筆記 - 第十三章 Linux 系統日常管理Linux 數據備份工具 rsync 和網絡配置

方法 target speed cnblogs rsync -av html links 布爾值 單個 博客地址:http://www.moonxy.com 一、前言 sync 命令是一個遠程數據同步工具,可通過 LAN/WAN 快速同步多臺主機間的文件,可以理解為 rem

嵌入式Linux驅動——SPI子系統解讀

static long spidev_ioctl(struct file *filp,unsigned int cmd,unsigned long arg) { int err = 0; int retval = 0; struct spidev_data *spid

linux下oracle11G DG搭建興許驗證操作

歸檔 驗證 補充 over nts content -s 環境 fontsize linux下oracle11G DG搭建(四):興許驗證操作 環境 名稱 主庫 備庫 主機名 bjsrv shsrv 軟件版本號 RedH

基於Ubuntu Server 16.04 LTS版本安裝和部署Django安裝MySQL數據庫

ins cli 遠程訪問 lib root 版本 連接 str ibm 1.安裝mysql以及插件: sudo apt-get install mysql-server mysql-client sudo apt-get install libmysqld-devsud

談談單元測試測試工具 TestNG

前言 上一篇文章《測試工具 JUnit 4》中提到了 JUnit 4,並對 JUnit 4 做了簡單的討論,這篇文章我們將要圍繞另一款測試工具討論 —— TestNG。其實,這篇文章應該寫在《測試工具 JUnit 3》之後,和《測試工具 JU

linux平臺學x86彙編從“hello world!”開始

        如其它高階語言一樣,組合語言程式在連結為可執行程式時,連結器必須要知道程式中的起點是什麼,就像c語言中的main函式一樣。GNU彙編器使用一個預設標籤_start作為應用程式的入口點,如果連結器找不到這個標籤就會生成錯誤訊息。如果編寫被外部組合語言或C語言程式使用的一組工具,需要使用.glo

Coursera-Deep Learning Specialization 課程Convolutional Neural Networks: -weak4程式設計作業

人臉識別 Face Recognition for the Happy House from keras.models import Sequential from keras.layers import Conv2D, ZeroPadding2D,

Linux小小白入門教程Linux終端

Linux的操作基本都是命令操作,學習Linux其實大部分時間是在學習Linux命令。而命令是在Linux終端上操作。 一、 Linux終端簡介 Linux終端像是windows的cmd命令視窗。因為Linux的視覺化做得沒有windows好,所以我們使用w

Linux vm執行引數OOM相關的引數

一、前言 本文是描述Linux virtual memory執行引數的第二篇,主要是講OOM相關的引數的。為了理解OOM引數,第二章簡單的描述什麼是OOM。如果這個名詞對你毫無壓力,你可以直接進入第三章,這一章是描述具體的引數的,除了描述具體的引數,我們引用了一些具體的

ZooKeeper系統跟隨者工作模式

當ZooKeeper叢集啟動之後,需要完成leader和follower之間的資料同步。 首先leader和observer有一

Linux 學習管道、重定向、正則

管道及IO重定向 運算器、控制器:CPU 儲存器:RAM 輸入裝置/輸出裝置 程式:指令和資料 控制器:指令 運算器: 儲存器: 地址匯流排:記憶體定址 資料匯流排:傳輸資料 控制匯流排:控制指令 暫存器:CPU暫時儲存器 I/O:硬碟 系

Linuxcentos 7系列----設定系統自動連線網路

  今天開機的時候發現虛擬機器沒有自動連線網路,因此需要對系統自動連線網路進行設定,下面是設定的步驟。     用root使用者登入系統,輸入命令:vim /etc/sysconfig/network-scripts/ifcfg-ens33,最後的是檔名

從新手到系統管理員Linux Shell指令碼程式設計數學Part I

本文由 [茶話匯] – [Qing] 編譯自 [Avishek Kumar] 轉載請註明出處 這部分主要討論數學相關的shell指令碼程式設計。 加法運算 新建一個檔案“Addition.sh”,輸入下面的內容並賦予其可執行的許可權。 [code language=”bash”] #!/bin/b