1. 程式人生 > >Linux kernel的中斷子系統之(五):驅動申請中斷API

Linux kernel的中斷子系統之(五):驅動申請中斷API

總結:二重點區分了搶佔式核心和非搶佔式核心的區別:搶佔式核心可以在核心空間進行搶佔,通過對中斷處理進行執行緒化可以提高Linux核心實時性。

三介紹了Linux中斷註冊函式request_threaded_irq,其實request_irq也是對request_threaded_irq的封裝。

四對request_threaded_irq進行了詳細分析,兩種型別中斷(Cascaded IRQ和Nested IRQ),以及如何相容BottomHalf和TopHalf,是否強制執行緒化,如何進行執行緒化,如何處理共享中斷。

一、前言

本文主要的議題是作為一個普通的驅動工程師,在撰寫自己負責的驅動的時候,如何向Linux Kernel中的中斷子系統註冊中斷處理函式?為了理解註冊中斷的介面,必須瞭解一些中斷執行緒化(threaded interrupt handler)的基礎知識,這些在第二章描述。第三章主要描述了驅動申請 interrupt line介面API request_threaded_irq的規格。第四章是進入request_threaded_irq的實現細節,分析整個程式碼的執行過程。

二、和中斷相關的linux實時性分析以及中斷執行緒化的背景介紹

1、非搶佔式linux核心的實時性

在遙遠的過去,linux2.4之前的核心是不支援搶佔特性的,具體可以參考下圖:

sxw

事情的開始源自高優先順序任務(橘色block)由於要等待外部事件(例如網路資料)而進入睡眠排程器排程了某個低優先順序的任務(紫色block)執行。該低優先順序任務歡暢的執行,直到觸發了一次系統呼叫(例如通過read()檔案介面讀取磁碟上的檔案等)而進入了核心態。仍然是熟悉的配方,仍然是熟悉的味道,低優先順序任務正在執行不會變化,只不過從user space切換到了kernel space。外部事件總是在你不想讓它來的時候到來,T0時刻,高優先順序任務等待的那個中斷事件發生了

中斷雖然發生了,但軟體不一定立刻響應,可能由於在核心態執行的某些操作不希望被外部事件打斷而主動關閉了中斷(或是關閉了CPU的中斷,或者MASK了該IRQ number),這時候,中斷訊號沒有立刻得到響應,軟體仍然在核心態執行低優先順序任務系統呼叫的程式碼。在T1時刻,核心態程式碼由於退出臨界區而開啟中斷(注意:上圖中的比例是不協調的,一般而言,linux kernel不會有那麼長的關中斷時間,上面主要是為了表示清楚,同理,從中斷觸發到具體中斷服務程式的執行也沒有那麼長,都是為了表述清楚),中斷一旦開啟,立刻跳轉到了異常向量地址,interrupt handler搶佔了低優先順序任務的執行,進入中斷上下文

(雖然這時候的current task是低優先順序任務,但是中斷上下文和它沒有任何關係)。

從CPU開始處理中斷到具體中斷服務程式被執行還需要一個分發的過程。這個期間系統要做的主要操作包括確定HW interrupt ID,確定IRQ Number,ack或者mask中斷,呼叫中斷服務程式等。T0到T2之間的delay被稱為中斷延遲(Interrupt Latency),主要包括兩部分,一部分是HW造成的delay(硬體的中斷系統識別外部的中斷事件並signal到CPU),另外一部分是軟體原因(核心程式碼中由於要保護臨界區而關閉中斷引起的)

中斷的服務程式執行完畢(在其執行過程中,T3時刻,會喚醒高優先順序任務,讓它從sleep狀態進入runable狀態),返回低優先順序任務的系統呼叫現場,這時候並不存在一個搶佔點,低優先順序任務要完成系統呼叫之後,在返回使用者空間的時候才出現搶佔點。漫長的等待之後,T4時刻,排程器排程高優先順序任務執行。有一個術語叫做任務響應時間(Task Response Time)用來描述T3到T4之間的delay。

2、搶佔式linux核心的實時性

2.6核心和2.4核心顯著的不同是提供了一個CONFIG_PREEMPT的選項,開啟該選項後,linux kernel就支援了核心程式碼的搶佔(當然不能在臨界區),其行為如下:

pre

T0到T3的操作都是和上一節的描述一樣的,不同的地方是在T4。對於2.4核心,只有返回使用者空間的時候才有搶佔點出現,但是對於搶佔式核心而言,即便是從中斷上下文返回核心空間的程序上下文,只要核心程式碼不在臨界區內,就可以發生排程,讓最高優先順序的任務排程執行

在非搶佔式linux核心中,一個任務的核心態是不可以被其他程序搶佔的。這裡並不是說kernel space不可以被搶佔,只是說程序通過系統呼叫陷入到核心的時候,不可以被其他的程序搶佔。實際上,中斷上下文當然可以搶佔程序上下文(無論是核心態還是使用者態),更進一步,中斷上下文是擁有至高無上的許可權,它甚至可以搶佔其他的中斷上下文。引入搶佔式核心後,系統的平均任務響應時間會縮短,但是,實時性更關注的是:無論在任何的負載情況下,任務響應時間是確定的。因此,更需要關注的是worst-case的任務響應時間。這裡有兩個因素會影響worst case latency:

(1)為了同步,核心中總有些程式碼需要持有自旋鎖資源,或者顯式的呼叫preempt_disable來禁止搶佔,這時候不允許搶佔

(2)中斷上下文(並非只是中斷handler,還包括softirq、timer、tasklet)總是可以搶佔程序上下文

因此,即便是打開了PREEMPT的選項,實際上linux系統的任務響應時間仍然是不確定的。一方面核心程式碼的臨界區非常多,我們需要找到,系統中持有鎖,或者禁止搶佔的最大的時間片。另外一方面,在上圖的T4中,能順利的排程高優先順序任務並非易事,這時候可能有觸發的軟中斷,也可能有新來的中斷,也可能某些driver的tasklet要執行,只有在沒有任何bottom half的任務要執行的時候,排程器才會啟動排程。

Notes:搶佔式核心和非搶佔式核心的區別在於,搶佔式核心多了一箇中斷上下文返回核心空間程序上下文搶佔點。高優先順序程序可以更早的得到排程。

3、進一步提高linux核心的實時性

通過上一個小節的描述,相信大家都確信中斷對linux 實時性的最大的敵人。那麼怎麼破?我曾經接觸過一款RTOS,它的中斷handler非常簡單,就是傳送一個inter-task message到該driver thread,對任何的一個驅動都是如此處理。這樣,每個中斷上下文都變得非常簡短,而且每個中斷都是一致的。在這樣的設計中,外設中斷的處理執行緒化了,然後,系統設計師要仔細的為每個系統中的task分配優先順序,確保整個系統的實時性。

在Linux kernel中,一個外設的中斷處理被分成top half和bottom half,top half進行最關鍵,最基本的處理,而比較耗時的操作被放到bottom half(softirq、tasklet)中延遲執行。雖然bottom half被延遲執行,但始終都是先於程序執行的。為何不讓這些耗時的bottom half和普通程序公平競爭呢?因此,linux kernel借鑑了RTOS的某些特性,對那些耗時的驅動interrupt handler進行執行緒化處理,在核心的搶佔點上,讓執行緒(無論是核心執行緒還是使用者空間建立的執行緒,還是驅動的interrupt thread)在一個舞臺上競爭CPU

Notes:通過將中斷bottom half執行緒化,提高Linux的實時性。

三、request_threaded_irq的介面規格

1、輸入引數描述

輸入引數 描述
irq 要註冊handler的那個IRQ number。這裡要註冊的handler包括兩個,一個是傳統意義的中斷handler,我們稱之primary handler,另外一個是threaded interrupt handler
handler primary handler。需要注意的是primary handler和threaded interrupt handler不能同時為空,否則會出錯
thread_fn threaded interrupt handler。如果該引數不是NULL,那麼系統會建立一個kernel thread,呼叫的function就是thread_fn
irqflags 參見本章第三節
devname
dev_id 參見第四章,第一節中的描述。

Notes:request_threaded_irq和request_irq差異就在與多了一個thread_fn,request_irq的下半部是在上半部handler中呼叫的。request_threaded_irq的thread_fn就相當於下半部,將其執行緒化。

2、輸出引數描述

0表示成功執行,負數表示各種錯誤原因。

3、Interrupt type flags

flag定義 描述
IRQF_TRIGGER_XXX 描述該interrupt line觸發型別的flag
IRQF_DISABLED 首先要說明的是這是一個廢棄的flag,在新的核心中,該flag沒有任何的作用了。具體可以參考:Disabling IRQF_DISABLED 
舊的核心(2.6.35版本之前)認為有兩種interrupt handler:slow handler和fast handle。在request irq的時候,對於fast handler,需要傳遞IRQF_DISABLED的引數,確保其中斷處理過程中是關閉CPU的中斷,因為是fast handler,執行很快,即便是關閉CPU中斷不會影響系統的效能。但是,並不是每一種外設中斷的handler都是那麼快(例如磁碟),因此就有 slow handler的概念,說明其在中斷處理過程中會耗時比較長。對於這種情況,在執行interrupt handler的時候不能關閉CPU中斷,否則對系統的performance會有影響。 
新的核心已經不區分slow handler和fast handle,都是fast handler,都是需要關閉CPU中斷的,那些需要後續處理的內容推到threaded interrupt handler中去執行。
IRQF_SHARED

這是flag用來描述一個interrupt line是否允許在多個裝置中共享。如果中斷控制器可以支援足夠多的interrupt source,那麼在兩個外設間共享一個interrupt request line是不推薦的,畢竟有一些額外的開銷(發生中斷的時候要逐個詢問是不是你的中斷,軟體上就是遍歷action list),因此外設的irq handler中最好是一開始就啟動判斷,看看是否是自己的中斷,如果不是,返回IRQ_NONE,表示這個中斷不歸我管。 早期PC時代,使用8259中斷控制器,級聯的8259最多支援15個外部中斷,但是PC外設那麼多,因此需要irq share。現在,ARM平臺上的系統設計很少會採用外設共享IRQ方式,畢竟一般ARM SOC提供的有中斷功能的GPIO非常的多,足夠用的。 當然,如果確實需要兩個外設共享IRQ,那也只能如此設計了。對於HW,中斷控制器的一個interrupt source的引腳要接到兩個外設的interrupt request line上,怎麼接?直接連線可以嗎?當然不行,對於低電平觸發的情況,我們可以考慮用與門連線中斷控制器和外設。

IRQF_PROBE_SHARED IRQF_SHARED用來表示該interrupt action descriptor是允許和其他device共享一個interrupt line(IRQ number),但是實際上是否能夠share還是需要其他條件:例如觸發方式必須相同。有些驅動程式可能有這樣的呼叫場景:我只是想scan一個irq table,看看哪一個是OK的,這時候,如果即便是不能和其他的驅動程式share這個interrupt line,我也沒有關係,我就是想scan看看情況。這時候,caller其實可以預見sharing mismatche的發生,因此,不需要核心列印“Flags mismatch irq……“這樣冗餘的資訊
IRQF_PERCPU 在SMP的架構下,中斷有兩種mode,一種中斷是在所有processor之間共享的,也就是global的,一旦中斷產生,interrupt controller可以把這個中斷送達多個處理器。當然,在具體實現的時候不會同時將中斷送達多個CPU,一般是軟體和硬體協同處理,將中斷送達一個CPU處理。但是一段時間內產生的中斷可以平均(或者按照既定的策略)分配到一組CPU上。這種interrupt mode下,interrupt controller針對該中斷的operational register是global的,所有的CPU看到的都是一套暫存器,一旦一個CPU ack了該中斷,那麼其他的CPU看到的該interupt source的狀態也是已經ack的狀態。 
和global對應的就是per cpu interrupt了,對於這種interrupt,不是processor之間共享的,而是特定屬於一個CPU的。例如GIC中interrupt ID等於30的中斷就是per cpu的(這個中斷event被用於各個CPU的local timer),這個中斷號雖然只有一個,但是,實際上控制該interrupt ID的暫存器有n組(如果系統中有n個processor),每個CPU看到的是不同的控制暫存器。在具體實現中,這些暫存器組有兩種形態,一種是banked,所有CPU操作同樣的暫存器地址,硬體系統會根據訪問的cpu定向到不同的暫存器,另外一種是non banked,也就是說,對於該interrupt source,每個cpu都有自己獨特的訪問地址
IRQF_NOBALANCING 這也是和multi-processor相關的一個flag。對於那些可以在多個CPU之間共享的中斷,具體送達哪一個processor是有策略的,我們可以在多個CPU之間進行平衡。如果你不想讓你的中斷參與到irq balancing的過程中那麼就設定這個flag
IRQF_IRQPOLL
IRQF_ONESHOT one shot本身的意思的只有一次的,結合到中斷這個場景,則表示中斷是一次性觸發的,不能巢狀。對於primary handler,當然是不會巢狀,但是對於threaded interrupt handler,我們有兩種選擇,一種是mask該interrupt source,另外一種是unmask該interrupt source。一旦mask住該interrupt source,那麼該interrupt source的中斷在整個threaded interrupt handler處理過程中都是不會再次觸發的,也就是one shot了。這種handler不需要考慮重入問題。 
具體是否要設定one shot的flag是和硬體系統有關的,我們舉一個例子,比如電池驅動,電池裡面有一個電量計,是使用HDQ協議進行通訊的,電池驅動會註冊一個threaded interrupt handler,在這個handler中,會通過HDQ協議和電量計進行通訊。對於這個handler,通過HDQ進行通訊是需要一個完整的HDQ互動過程,如果中間被打斷,整個通訊過程會出問題,因此,這個handler就必須是one shot的。
IRQF_NO_SUSPEND 這個flag比較好理解,就是說在系統suspend的時候,不用disable這個中斷,如果disable,可能會導致系統不能正常的resume。
IRQF_FORCE_RESUME 系統resume的過程中強制必須進行enable的動作,即便是設定了IRQF_NO_SUSPEND這個flag。這是和特定的硬體行為相關的。
IRQF_NO_THREAD 有些low level的interrupt是不能執行緒化的(例如系統timer的中斷),這個flag就是起這個作用的。另外,有些級聯的interrupt controller對應的IRQ也是不能執行緒化的(例如secondary GIC對應的IRQ),它的執行緒化可能會影響一大批附屬於該interrupt controller的外設的中斷響應延遲。
IRQF_EARLY_RESUME 提前resume IRQ到syscore中,而不是在device resume中進行。
IRQF_TIMER

四、request_threaded_irq程式碼分析

1、request_threaded_irq主流程

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)
{
    if ((irqflags & IRQF_SHARED) && !dev_id)---------(1)
        return -EINVAL;

    desc = irq_to_desc(irq); -----------------(2)
    if (!desc)         return -EINVAL;

    if (!irq_settings_can_request(desc) || ------------(3)
        WARN_ON(irq_settings_is_per_cpu_devid(desc)))
        return -EINVAL;

    if (!handler) { ----------------------(4)
        if (!thread_fn)
            return -EINVAL;
        handler = irq_default_primary_handler;
    }

    action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);

    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); -----------(5)
    chip_bus_sync_unlock(desc);
}

(1)對於那些需要共享的中斷,在request irq的時候需要給出dev id,否則會出錯退出。為何對於IRQF_SHARED的中斷必須要給出dev id呢?實際上,在共享的情況下,一個IRQ number對應若干個irqaction,當操作irqaction的時候,僅僅給出IRQ number就不是非常的足夠了,這時候,需要一個ID表示具體的irqaction,這裡就是dev_id的作用了。我們舉一個例子:

void free_irq(unsigned int irq, void *dev_id)

當釋放一個IRQ資源的時候,不但要給出IRQ number,還要給出device ID。只有這樣,才能精準的把要釋放的那個irqaction 從irq action list上移除。dev_id在中斷處理中有沒有作用呢?我們來看看source code:

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

    do {
        irqreturn_t res;
        res = action->handler(irq, action->dev_id);

……
        action = action->next;
    } while (action);

……
}

linux interrupt framework雖然支援中斷共享,但是它並不會協助解決識別問題,它只會遍歷該IRQ number上註冊的irqaction的callback函式,這樣,雖然只是一個外設產生的中斷,linux kernel還是把所有共享的那些中斷handler都逐個呼叫執行。為了讓系統的performance不受影響,irqaction的callback函式必須在函式的最開始進行判斷,是否是自己的硬體裝置產生了中斷(讀取硬體的暫存器),如果不是,儘快的退出。

需要注意的是,這裡dev_id並不能在中斷觸發的時候用來標識需要呼叫哪一個irqaction的callback函式,通過上面的程式碼也可以看出,dev_id有些類似一個引數傳遞的過程,可以把具體driver的一些硬體資訊,組合成一個structure,在觸發中斷的時候可以把這個structure傳遞給中斷處理函式。

(2)通過IRQ number獲取對應的中斷描述符。在引入CONFIG_SPARSE_IRQ選項後,這個轉換變得不是那麼簡單了。在過去,我們會以IRQ number為index,從irq_desc這個全域性陣列中直接獲取中斷描述符。如果配置CONFIG_SPARSE_IRQ選項,則需要從radix tree中搜索。CONFIG_SPARSE_IRQ選項的更詳細的介紹請參考IRQ number和中斷描述符

(3)並非系統中所有的IRQ number都可以request,有些中斷描述符被標記為IRQ_NOREQUEST,標識該IRQ number不能被其他的驅動request。一般而言,這些IRQ number有特殊的作用,例如用於級聯的那個IRQ number是不能request。irq_settings_can_request函式就是判斷一個IRQ是否可以被request。

irq_settings_is_per_cpu_devid函式用來判斷一箇中斷描述符是否需要傳遞per cpu的device ID。per cpu的中斷上面已經描述的很清楚了,這裡不再細述。如果一箇中斷描述符對應的中斷 ID是per cpu的,那麼在申請其handler的時候就有兩種情況,一種是傳遞統一的dev_id引數(傳入request_threaded_irq的最後一個引數),另外一種情況是針對每個CPU,傳遞不同的dev_id引數。在這種情況下,我們需要呼叫request_percpu_irq介面函式而不是request_threaded_irq。

(4)傳入request_threaded_irq的primary handler和threaded handler引數有下面四種組合:

primary handler threaded handler 描述
NULL NULL 函數出錯,返回-EINVAL
設定 設定 正常流程。中斷處理被合理的分配到primary handler和threaded handler中。
設定 NULL 中斷處理都是在primary handler中完成
NULL 設定 這種情況下,系統會幫忙設定一個default的primary handler:irq_default_primary_handler,協助喚醒threaded handler執行緒

(5)這部分的程式碼很簡單,分配struct irqaction,賦值,呼叫__setup_irq進行實際的註冊過程。這裡要羅嗦幾句的是鎖的操作,在核心中,有很多函式,有的是需要呼叫者自己加鎖保護的,有些是不需要加鎖保護的。對於這些場景,linux kernel採取了統一的策略:基本函式名字是一樣的,只不過需要呼叫者自己加鎖保護的那個函式需要增加__的字首,例如核心有有下面兩個函式:setup_irq和__setup_irq。這裡,我們在setup irq的時候已經呼叫chip_bus_lock進行保護,因此使用lock free的版本__setup_irq。

chip_bus_lock定義如下:

static inline void chip_bus_lock(struct irq_desc *desc)
{
    if (unlikely(desc->irq_data.chip->irq_bus_lock))
        desc->irq_data.chip->irq_bus_lock(&desc->irq_data);
}

大部分的interrupt controller並沒有定義irq_bus_lock這個callback函式,因此chip_bus_lock這個函式對大多數的中斷控制器而言是沒有實際意義的。但是,有些interrupt controller是連線到慢速總線上的,例如一個i2c介面的IO expander晶片(這種晶片往往也提供若干有中斷功能的GPIO,因此也是一個interrupt controller),在訪問這種interrupt controller的時候需要lock住那個慢速bus(只能有一個client在使用I2C bus)。

2、註冊irqaction

(1)nested IRQ的處理程式碼

在看具體的程式碼之前,我們首先要理解什麼是nested IRQ。nested IRQ不是cascade IRQ,在GIC程式碼分析中我們有描述過cascade IRQ這個概念,主要用在interrupt controller級聯的情況下。為了方便大家理解,我還是給出一個具體的例子吧,具體的HW block請參考下圖:

SOC-INT

上圖是一個兩個GIC級聯的例子,所有的HW block封裝在了一個SOC chip中。為了方便描述,我們先進行編號:Secondary GIC的IRQ number是A,外設1的IRQ number是B,外設2的IRQ number是C。對於上面的硬體,linux kernel處理如下:

(a)IRQ A的中斷描述符被設定為不能註冊irqaction(不能註冊specific interrupt handler,或者叫中斷服務程式)

(b)IRQ A的highlevel irq-events handler(處理interrupt flow control)負責進行IRQ number的對映在其irq domain上翻譯出具體外設的IRQ number,並重新定向到外設IRQ number對應的highlevel irq-events handler

(c)所有外設驅動的中斷正常request irq,可以任意選擇執行緒化的handler,或者只註冊primary handler。

需要注意的是,對root GIC和Secondary GIC暫存器的訪問非常快,因此ack、mask、EOI等操作也非常快。

我們再看看另外一個interrupt controller級聯的情況:

nested

IO expander HW block提供了有中斷功能的GPIO,因此它也是一個interrupt controller,有它自己的irq domain和irq chip。上圖中外設1和外設2使用了IO expander上有中斷功能的GPIO,它們有屬於自己的IRQ number以及中斷描述符。IO expander HW block的IRQ line連線到SOC內部的interrupt controller上,因此,這也是一個interrupt controller級聯的情況,對於這種情況,我們是否可以採用和上面GIC級聯的處理方式呢?

Notes:外設1的終端遮蔽時間=IO expander highlevel handler+外設1highlevel handler(通過I2C)+外設1primary handler。

不行,對於GIC級聯的情況,如果secondary GIC上的外設1產生了中斷整個關中斷的時間是IRQ A的中斷描述符的highlevel irq-events handler處理時間+IRQ B的的中斷描述符的highlevel irq-events handler處理時間+外設1的primary handler的處理時間。所幸對root GIC和Secondary GIC暫存器的訪問非常快,因此整個關中斷的時間也不是非常的長。但是如果是IO expander這個情況,如果採取和上面GIC級聯的處理方式一樣的話,關中斷的時間非常長。我們還是用外設1產生的中斷為例子好了。這時候,由於IRQ B的的中斷描述符的highlevel irq-events handler處理設計I2C的操作,因此時間非常的長,這時候,對於整個系統的實時性而言是致命的打擊。對這種硬體情況,linux kernel處理如下:

(a)IRQ A的中斷描述符的highlevel irq-events handler根據實際情況進行設定,並且允許註冊irqaction。對於連線到IO expander上的外設,它是沒有real time的要求的(否則也不會接到IO expander上),因此一般會進行執行緒化處理。由於threaded handler中涉及I2C操作,因此要設定IRQF_ONESHOT的flag。

(b)在IRQ A的中斷描述符的threaded interrupt handler中進行進行IRQ number的對映,在IO expander irq domain上翻譯出具體外設的IRQ number,並直接呼叫handle_nested_irq函式處理該IRQ。

(c)外設對應的中斷描述符設定IRQ_NESTED_THREAD的flag,表明這是一個nested IRQ。nested IRQ沒有highlevel irq-events handler,也沒有primary handler,它的threaded interrupt handler是附著在其parent IRQ的threaded handler上的

具體的nested IRQ的處理程式碼如下:

static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{

……
nested = irq_settings_is_nested_thread(desc);
    if (nested) {
        if (!new->thread_fn) {
            ret = -EINVAL;
            goto out_mput;
        }
        new->handler = irq_nested_primary_handler;

    } else {
……
    }

……

}

如果一箇中斷描述符是nested thread type的,說明這個中斷描述符應該設定threaded interrupt handler(當然,核心是不會單獨建立一個thread的,它是藉著其parent IRQ的interrupt thread執行),否則就會出錯返回。對於primary handler,它應該沒有機會被呼叫到,當然為了除錯,kernel將其設定為irq_nested_primary_handler,以便在呼叫的時候列印一些資訊,讓工程師直到發生了什麼狀況。

(2)forced irq threading處理

具體的forced irq threading的處理程式碼如下:

static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{

……
    nested = irq_settings_is_nested_thread(desc);
    if (nested) {
……
    } else {
        if (irq_settings_can_thread(desc))
            irq_setup_forced_threading(new);

    }

……

}

forced irq threading其實就是將系統中所有可以被執行緒化的中斷handler全部執行緒化,即便你在request irq的時候,設定的是primary handler,而不是threaded handler。當然那些不能被執行緒化的中斷(標註了IRQF_NO_THREAD的中斷,例如系統timer)還是排除在外的。irq_settings_can_thread函式就是判斷一箇中斷是否可以被執行緒化,如果可以的話,則呼叫irq_setup_forced_threading在set irq的時候強制進行執行緒化。具體程式碼如下:

static void irq_setup_forced_threading(struct irqaction *new)
{
    if (!force_irqthreads)-------------------------------(a)
        return;
    if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))-------(b)
        return;

    new->flags |= IRQF_ONESHOT; -------------------------(d)

    if (!new->thread_fn) {-------------------------------(c)
        set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
        new->thread_fn = new->handler;
        new->handler = irq_default_primary_handler;
    }
}

(a)系統中有一個強制執行緒化的選項:CONFIG_IRQ_FORCED_THREADING,如果沒有開啟該選項,force_irqthreads總是0,因此irq_setup_forced_threading也就沒有什麼作用,直接return了。如果打開了CONFIG_IRQ_FORCED_THREADING,說明系統支援強制執行緒化,但是具體是否對所有的中斷進行強制執行緒化處理還是要看命令列引數threadirqs。如果kernel啟動的時候沒有傳入該引數,那麼同樣的,irq_setup_forced_threading也就沒有什麼作用,直接return了。只有bootloader向核心傳入threadirqs這個命令列引數,核心才真正在啟動過程中,進行各個中斷的強制執行緒化的操作

(b)看到IRQF_NO_THREAD選項你可能會奇怪,前面irq_settings_can_thread函式不是檢查過了嗎?為何還要重複檢查?其實一箇中斷是否可以進行執行緒化可以從兩個層面看:一個是從底層看,也就是從中斷描述符、從實際的中斷硬體拓撲等方面看。另外一個是從中斷子系統的使用者層面看,也就是各個外設在註冊自己的handler的時候是否想進行執行緒化處理。所有的IRQF_XXX都是從使用者層面看的flag,因此如果使用者通過IRQF_NO_THREAD這個flag告知kernel,該interrupt不能被執行緒化,那麼強制執行緒化的機制還是尊重使用者的選擇的。

PER CPU的中斷都是一些較為特殊的中斷,不是一般意義上的外設中斷,因此對PER CPU的中斷不強制進行執行緒化。IRQF_ONESHOT選項說明該中斷已經被執行緒化了(而且是特殊的one shot型別的),因此也是直接返回了。

(c)強制執行緒化只對那些沒有設定thread_fn的中斷進行處理,這種中斷將全部的處理放在了primary interrupt handler中(當然,如果中斷處理比較耗時,那麼也可能會採用bottom half的機制),由於primary interrupt handler是全程關閉CPU中斷的,因此可能對系統的實時性造成影響,因此考慮將其強制執行緒化。struct irqaction中的thread_flags是和執行緒相關的flag,我們給它打上IRQTF_FORCED_THREAD的標籤,表明該threaded handler是被強制threaded的。new->thread_fn = new->handler這段程式碼表示將原來primary handler中的內容全部放到threaded handler中處理,新的primary handler被設定為default handler。

(d)強制執行緒化是一個和實時性相關的選項,從我的角度來看是一個很不好的設計(個人觀點),各個驅動工程師在撰寫自己的驅動程式碼的時候已經安排好了自己的上下文環境。有的是程序上下文,有的是中斷上下文,在各自的上下文環境中,驅動工程師又選擇了適合的核心同步機制。但是,強制執行緒化導致原來執行在中斷上下文的primary handler現在執行在程序上下文,這有可能導致一些難以跟蹤和定位的bug。

當然,作為核心的開發者,既然同意將強制執行緒化這個特性併入linux kernel,相信他們有他們自己的考慮。我猜測這是和一些舊的驅動程式碼維護相關的。linux kernel中的中斷子系統的API的修改會引起非常大的震動,因為核心中成千上萬的驅動都是需要呼叫舊的介面來申請linux kernel中斷子系統的服務,對每一個驅動都進行修改是一個非常耗時的工作,為了讓那些舊的驅動程式碼可以執行在新的中斷子系統上,因此,在kernel中,實際上仍然提供了舊的request_irq介面函式,如下:

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);
}

介面是OK了,但是,新的中斷子系統的思路是將中斷處理分成primary handler和threaded handler,而舊的驅動程式碼一般是將中斷處理分成top half和bottom half,如何將這部分的不同抹平?linux kernel是這樣處理的(這是我個人的理解,不保證是正確的):

(d-1)核心為那些被強制執行緒化的中斷handler設定了IRQF_ONESHOT的標識。這是因為在舊的中斷處理機制中,top half是不可重入的,強制執行緒化之後,強制設定IRQF_ONESHOT可以保證threaded handler是不會重入的。

(d-2)在那些被強制執行緒化的中斷執行緒中,disable bottom half的處理。這是因為在舊的中斷處理機制中,botton half是不可能搶佔top half的執行,強制執行緒化之後,應該保持這一點。

(3)建立interrupt執行緒。

程式碼如下:

if (new->thread_fn && !nested) {
    struct task_struct *t;
    static const struct sched_param param = {
        .sched_priority = MAX_USER_RT_PRIO/2,
    };

    t = kthread_create(irq_thread, new, "irq/%d-%s", irq,------------------(a)
               new->name);

    sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);

    get_task_struct(t);---------------------------(b)
    new->thread = t;
    set_bit(IRQTF_AFFINITY, &new->thread_flags);---------------(c)
}

if (!alloc_cpumask_var(&mask, GFP_KERNEL)) {----------------(d)
    ret = -ENOMEM;
    goto out_thread;
}
if (desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)-----------(e)
    new->flags &= ~IRQF_ONESHOT;

(a)呼叫kthread_create來建立一個核心執行緒,並呼叫sched_setscheduler_nocheck來設定這個中斷執行緒的排程策略和排程優先順序。這些是和程序管理相關的內容,我們留到下一個專題再詳細描述吧。

(b)呼叫get_task_struct可以為這個threaded handler的task struct增加一次reference count,這樣,即便是該thread異常退出也可以保證它的task struct不會被釋放掉。這可以保證中斷系統的程式碼不會訪問到一些被釋放的記憶體。irqaction的thread 成員被設定為剛剛建立的task,這樣,primary handler就知道喚醒哪一個中斷執行緒了。

(c)設定IRQTF_AFFINITY的標誌,在threaded handler中會檢查該標誌並進行IRQ affinity的設定

(d)分配一個cpu mask的變數的記憶體,後面會使用到

(e)驅動工程師是撰寫具體外設驅動的,他/她未必會了解到底層的一些具體的interrupt controller的資訊。有些interrupt controller(例如MSI based interrupt)本質上就是就是one shot的(通過IRQCHIP_ONESHOT_SAFE標記),因此驅動工程師設定的IRQF_ONESHOT其實是畫蛇添足,因此可以去掉。

(4)共享中斷的檢查。

程式碼如下:

old_ptr = &desc->action;
old = *old_ptr;

if (old) {
    if (!((old->flags & new->flags) & IRQF_SHARED) ||-----------------(a)
        ((old->flags ^ 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 {------------------------------------(b)
        thread_mask |= old->thread_mask;
        old_ptr = &old->next;
        old = *old_ptr;
    } while (old);
    shared = 1;
}

(a)old指向註冊之前的action list,如果不是NULL,那麼說明需要共享interrupt line。但是如果要共享,需要每一個irqaction都同意共享(IRQF_SHARED),每一個irqaction的觸發方式相同(都是level trigger或者都是edge trigger),相同的oneshot型別的中斷(都是one shot或者都不是),per cpu型別的相同中斷(都是per cpu的中斷或者都不是)。

(b)將該irqaction掛入佇列的尾部。

(5)thread mask的設定。

程式碼如下:

if (new->flags & IRQF_ONESHOT) {
        if (thread_mask == ~0UL) {------------------------(a)
            ret = -EBUSY;
            goto out_mask;
        }
        new->thread_mask = 1 << ffz(thread_mask);

    } else if (new->handler == irq_default_primary_handler &&
           !(desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)) {--------(b)
        ret = -EINVAL;
        goto out_mask;
    }

對於one shot型別的中斷,我們還需要設定thread mask。如果一個one shot型別的中斷只有一個threaded handler(不支援共享),那麼事情就很簡單(臨時變數thread_mask等於0),該irqaction的thread_mask成員總是使用第一個bit來標識該irqaction。但是,如果支援共享的話,事情變得有點複雜。我們假設這個one shot型別的IRQ上有A,B和C三個irqaction,那麼A,B和C三個irqaction的thread_mask成員會有不同的bit來標識自己。例如A的thread_mask成員是0x01,B的是0x02,C的是0x04,如果有更多共享的irqaction(必須是oneshot型別),那麼其thread_mask成員會依次設定為0x08,0x10……

(a)在上面“共享中斷的檢查”這個section中,thread_mask變數儲存了所有的屬於該interrupt line的thread_mask,這時候,如果thread_mask變數如果是全1,那麼說明irqaction list上已經有了太多的irq action(大於32或者64,和具體系統和編譯器相關)。如果沒有滿,那麼通過ffz函式找到第一個為0的bit作為該irq action的thread bit mask。

(b)irq_default_primary_handler的程式碼如下:

static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
    return IRQ_WAKE_THREAD;
}

程式碼非常的簡單,返回IRQ_WAKE_THREAD,讓kernel喚醒threaded handler就OK了。使用irq_default_primary_handler雖然簡單,但是有一個風險:如果是電平觸發的中斷,我們需要操作外設的暫存器才可以讓那個asserted的電平訊號消失,否則它會一直持續。一般,我們都是直接在primary中操作外設暫存器(slow bus型別的interrupt controller不行),儘早的clear interrupt,但是,對於irq_default_primary_handler,它僅僅是wakeup了threaded interrupt handler,並沒有clear interrupt,這樣,執行完了primary handler,外設中斷仍然是asserted,一旦開啟CPU中斷,立刻觸發下一次的中斷,然後不斷的迴圈。因此,如果註冊中斷的時候沒有指定primary interrupt handler,並且沒有設定IRQF_ONESHOT,那麼系統是會報錯的。當然,有一種情況可以豁免,當底層的irq chip是one shot safe的(IRQCHIP_ONESHOT_SAFE)。

(6)使用者IRQ flag和底層interrupt flag的同步(TODO)

相關推薦

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

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

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

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

Linux kernel中斷子系統綜述

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

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中斷子系統IRQ number和中斷描述符

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

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

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

Linux小小白入門教程顯示和進入資料夾

以下操作在Linux終端進行。Linux因為許可權非常嚴格,所以暫時所有的命令操作全部是在/home資料夾下的/yangjw資料夾下進行。/yangjw資料夾就是登入使用者名稱所在的資料夾,出了此資料

linux平臺學x86彙編使用gdb除錯彙編程式

本部落格專注於原創(或翻譯), 轉載本部落格文章請保留文章宣告,文章僅供學習與參考,未經允許情況下嚴禁用於商業用途!! 本部落格地址: blog.csdn.net/shallnet 或 blog.csdn.net/gentleliu email : liuy0711

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

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

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

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

Docker系列使用Docker Compose編排容器

http://www.cnblogs.com/ee900222/p/docker_5.html 1. 前言 Docker Compose 是 Docker 容器進行編排的工具,定義和執行多容器的應用,可以一條命令啟動多個容器。 使用Compose 基本上分為三步:

ZooKeeper系列領導者工作模式

領導者就是Leader,是整個叢集的寫事務流程負責人。 一輪選舉結束時產生新的Leader,並且Epoch加1。同時新的Lead

Linuxcentos 7系列----maven的安裝和配置

   最近需要做個Jenkins的自動部署,因此需要在伺服器上配置maven,下面是我的配置過程:     1.切換你要存放壓縮包的資料夾     2.選擇線上安裝    wget http://mirror

【原創】Linux虛擬化KVM-Qemu分析記憶體虛擬化

# 背景 - `Read the fucking source code!` --By 魯迅 - `A picture is worth a thousand words.` --By 高爾基 說明: 1. KVM版本:5.9.1 2. QEMU版本:5.0.0 3. 工具:Source Insight

Linux操作系統基礎知識

狀態 -exec acer res ifconfig 查找 mas 配置文件 update ifconfig 命令查看網絡信息eth0 eth1em1 em2p2p2 p2p3 systemctl status network 查看網絡狀態systemctl start n

把握linux內核設計思想十三內存管理進程地址空間

color 區域 left ons 文章 進程的地址空間 tmp ica interval 【版權聲明:尊重原創,轉載請保留出處:blog.csdn.net/shallnet。文章僅供學習交流,請勿用於商業用途】 進程地址空間由進程可尋址的虛擬內存組成

linux常用命令整理shell基礎

程序猿 逆向 多條 希望 正則表達 group 運行 ls命令 交互式 大家好,我是會唱歌的程序猿~~~~~~ 最近在學習linux,閑暇之余就把這些基本的命令進行了整理,希望大家能用的上,整理的的目的是在忘了的時候翻出來看看^?_?^,前後一共分為五個部分

Unity3DMecanim動畫系統學習筆記Animator Controller

浮點 key 發現 菜單 融合 stat mon 好的 project 簡介 Animator Controller在Unity中是作為一種單獨的配置文件存在的文件類型,其後綴為controller,Animator Controller包含了以下幾種功能: 可以對

CSS+div左中右經典布局

doc src png .com image border blog ack alt <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <titl

我的C#跨平臺使用IoC依賴註入實現

啟動 nuget alt 接口 one gin 分享 lis 技術分享 引入NuGet包:Unity 實現接口:IDependencyResolver 在啟動類中註入依賴的類: 註意:左框中的內容為接口或抽象類,右框中為實際要註入的