《LINUX3.0內核源代碼分析》第二章:中斷和異常(2)



摘要:本文主要講述linux如何處理ARM cortex A9多核處理器的中斷處理過程的C函數部分。主要是在中斷上下文和線程上下文處理ISR的過程。

 

法律聲明:《LINUX3.0內核源代碼分析》系列文章由謝寶友(scxby@163.com)發表於http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代碼遵循GPL協議。除此以外,文檔中的其他內容由作者保留所有版權。謝絕轉載。

 

本連載文章並不是為了形成一本適合出版的書籍,而是為了向有一定內核基本的讀者提供一些linux3.0源碼分析。因此,請讀者結合《深入理解LINUX內核》第三版閱讀本連載。

 

1.1.1       中斷處理(C函數)
1.1.1.1          一般中斷的處理

大多數中斷是由asm_do_IRQ函數處理:

/**

 * 我們暫且可以認為,絕大部分中斷是從匯編跳到本函數處理的。當然,IPIlocal_timer不是。

 *                 irq:            產生中斷的外部中斷號。

 *                  regs:                  被中斷打斷的寄存器現場。

 */

asmlinkage void __exception_irq_entry

asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

{

    /**

     * 將當前正在處理的中斷現場保存到每CPU變量__irq_regs中去。

     * 這樣做的目的,是為了在其他代碼中,直接讀取__irq_regs中的值,找到中斷前的現場。

     * 而不用將regs參數層層傳遞下去。

     */

    struct pt_regs *old_regs = set_irq_regs(regs);

 

    /**

     * 調用irq_enter表示進入中斷處理過程。該函數進行如下處理:

     *              1rcu模塊記錄內部計數,表示當前退出了NOHZ狀態。關於rcu,需要整整一本書來描述,請從http://xiebaoyou.download.csdn.net下載<深入理解並行編程>了解更多內容。

     *              2、處理NOHZ事件。

     *              3、將當前任務的搶占計數中的硬中斷計數加1.該計數表示當前中斷嵌套層次。

     *              4、調試信息,表示當前已經進入中斷的事實。

     */

    irq_enter();

 

    /*

     * Some hardware gives randomly wrong interrupts.  Rather

     * than crashing, do something sensible.

     */

    if (unlikely(irq >= nr_irqs)) {/* 中斷號錯誤,超過最大中斷號,這會發生嗎? */

             if (printk_ratelimit())/* 調用printk_ratelimit,控制在一定時間內,不重復打印警告信息,防止打印風暴將系統阻塞 */

                       printk(KERN_WARNING "Bad IRQ%u\n", irq);

             ack_bad_irq(irq);/* 錯誤的中斷號並不需要應答,只需要對錯誤中斷進行計數。 */

    } else {

             generic_handle_irq(irq);/* 這里進行正常的中斷處理 */

    }

 

    /* AT91 specific workaround */

    irq_finish(irq);/* 這是提供給各個體系結構自行實現的回調鈎子,一般沒有 */

 

    /**

     * 中斷退出過程,主要處理以下內容:

     *              1、調試鈎子,記錄退出中斷的事實。

     *              2、在任務的搶占計數字段中,遞減中斷計數

     *              3、處理軟中斷

     *              4、調用rcu模塊的函數,表示已經退出中斷。

     */

    irq_exit();

    /**

     * 恢復__irq_regsCPU變量的內容。

     */

    set_irq_regs(old_regs);

}

 

一般情況下,會調用generic_handle_irq來處理中斷,這個函數是對generic_handle_irq_desc簡單封裝:

/**

 * 處理特定的中斷。

 *                  irq:   要處理的中斷號。

 */

int generic_handle_irq(unsigned int irq)

{

    /**

     * 根據中斷號,取得中斷描述符。

     */

    struct irq_desc *desc = irq_to_desc(irq);

 

    /**

     * 如果中斷描述符為空,說明irq編號過大,或者對某些特定體系結構來說,該中斷還不被支持。直接返回即可。

     */

    if (!desc)

             return -EINVAL;

    /**

     * 根據中斷描述符中的信息,得到該中斷ISR,並處理中斷。

     */

    generic_handle_irq_desc(irq, desc);

    return 0;

}

 

generic_handle_irq_desc調用中斷描述符的handle_irq回調函數。對於不同的中斷類型,handle_irq回調函數可能是handle_simple_irqhandle_level_irqhandle_fasteoi_irqhandle_edge_irqhandle_edge_eoi_irqhandle_percpu_irq。這里我們以handle_level_irq為例解釋中斷處理流程。

/**

 * 處理電平觸發類型的中斷

 *                  irq:            中斷號

 *                  desc:                  中斷描述符

 */

void

handle_level_irq(unsigned int irq, struct irq_desc *desc)

{

    /**

     * 這里鎖住描述符的自旋鎖,有兩個原因:

     *              其他核可能同時收到了中斷,將所有同一中斷號的中斷交給同一個CPU處理,可以避免ISR中做復雜的同步。這個原因是由於unix系統歷史原因造成的。

     *              其他核可能在調用request_irq等函數注冊ISR,需要使用該鎖保護desc中的數據不被破壞。

     * 注意:這里使用的是raw_spin_lock而不是spin_lock,因為實時內核中,spin_lock已經可以睡眠了。而目前處於硬中斷中,不能睡眠。

     */

    raw_spin_lock(&desc->lock);

    /**

     * 應答中斷,並同時屏障該中斷源。

     */

    mask_ack_irq(desc);

 

    /**

     * 如果其他核正在處理該中斷,則退出。

     * 雖然本函數處於中斷描述符的lock鎖保護之中,但是handle_irq_event函數在調用ISR時,會將鎖打開。

     * 也就是說,其他核在處理ISR時,本核可能進入鎖保護的代碼中來。

     */

    if (unlikely(irqd_irq_inprogress(&desc->irq_data)))

             if (!irq_check_poll(desc))

                       goto out_unlock;

 

    /**

     * IRQS_REPLAY標志是為了挽救丟失的中斷。這個幾乎不會碰上,暫時不深入分析這個標志。運行到此,中斷已經在處理了,就不必考慮挽救丟失的中斷了。

     * IRQS_WAITING標志表示初始化進程正在等待中斷的到來,當探測一些老式設備時,驅動用此方法確定硬件產生的中斷號。

     * 比如一些老式的鼠標、鍵盤、ISA設備需要這么做。它們不是PCI設備,必須用中斷探測的方法確定其中斷號。

     * 當然,運行到這里,可以將IRQS_WAITING標志去除了,以通知初始化函數,相應的中斷號已經觸發。

     */

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

    /**

     * 記錄下中斷在本CPU上觸發的次數、本CPU總中斷次數。在/proc中要用到這些統計值。

     */

    kstat_incr_irqs_this_cpu(irq, desc);

 

    /*

     * If its disabled or no action available

     * keep it masked and get out of here

     */

    /**

     * action是中斷描述符表的頭指針,如果為空則說明本中斷沒有掛接處理函數。

     * irqd_irq_disabled表示相應的中斷已經被屏蔽,也退出。

     */

    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data)))

             goto out_unlock;

 

    /**

     * handle_irq_event要么是在中斷上下文調用ISR,要么是喚醒處理線程處理中斷。

     * 注意:這個函數會臨時打開中斷描述符的自旋鎖。

     */

    handle_irq_event(desc);

 

    /**

     * 如果中斷沒有被屏障,並且不是一次性的中斷,那么需要將該中斷線重新打開。

     */

    if (!irqd_irq_disabled(&desc->irq_data) && !(desc->istate & IRQS_ONESHOT))

             unmask_irq(desc);

out_unlock:

    /**

     * 釋放自旋鎖。

     */

    raw_spin_unlock(&desc->lock);

}

 

 

 

handle_irq_event如下所示:

/**

 * 處理ISR

 */

irqreturn_t handle_irq_event(struct irq_desc *desc)

{

    struct irqaction *action = desc->action;

    irqreturn_t ret;

 

    /**

     * 清除掛起標志。當本核正在調用ISR的過程中,如果發生了同樣的中斷,那么其他核在收到中斷時,會發現本核將IRQD_IRQ_INPROGRESS設置到描述符中。

     * 那么其他核會設置IRQS_PENDING並退出。本核在處理完ISR后,會判斷此標志並重新執行ISR,在重新執行ISR前,應當將IRQS_PENDING標志清除。

     */

    desc->istate &= ~IRQS_PENDING;

    /**

     * 在釋放自旋鎖前,設置IRQD_IRQ_INPROGRESS標志,表示本核正在處理該中斷。其他核不應當再處理同樣的中斷。

     */

    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);

    /**

     * 在開始調用ISR前,釋放自旋鎖,避免其他核被長時間掛起。

     */

    raw_spin_unlock(&desc->lock);

 

    /**

     * handle_irq_event_percpu會遍歷中斷描述符中的ISR鏈表,並回調ISR

     */

    ret = handle_irq_event_percpu(desc, action);

 

    /**

     * 重新獲得自旋鎖,以維持中斷描述符中的標志。

     */

    raw_spin_lock(&desc->lock);

    /**

     * 清除IRQD_IRQ_INPROGRESS標志。此時其他核如果產生同樣的中斷,則將中斷交給其他核處理。

     * 如果在處理中斷期間,其他核產生了同樣的中斷,並設置了IRQS_PENDING,則由上層函數繼續循環處理中斷。

     */

    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

    return ret;

}

 

/**

 * 喚醒中斷處理線程或者在中斷上下文處理中斷

 *                  desc:         中斷描述符

 *                  action:      ISR鏈表頭。

 */

irqreturn_t

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

{

    /**

     * retval表示中斷處理結果,默認設置為IRQ_NONE表示該中斷沒有被ISR響應。

     */

    irqreturn_t retval = IRQ_NONE;

    unsigned int random = 0, irq = desc->irq_data.irq;

 

    /**

     * 這里的循環是遍歷ISR鏈表,循環調用ISR處理函數。

     */

    do {

             irqreturn_t res;

 

             /**

              * trace_irq_handler_entrytrace_irq_handler_exit這兩個調試函數應當是內核設計者對驅動開發者不太放心。

              * 因此在這些跟蹤所有驅動注冊的ISR

              */

             trace_irq_handler_entry(irq, action);

             /**

              * 這里調用驅動注冊的ISR對中斷進行處理。

              */

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

             trace_irq_handler_exit(irq, action, res);

 

             /**

              * 老版本的內核會根據ISR注冊時的標志,在中斷處理函數中將中斷打開或者關閉。

              * 新版本內核應當是完全實現了中斷線程化,長時間運行的中斷ISR放到線程中去了,也就是說,在中斷上下文都應當是關中斷運行。

              * 這里的警告應當是找出那些不符合新版本要求的ISR,在這里打印警告,並強制將中斷關閉。

              */

             if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",

                             irq, action->handler))

                       local_irq_disable();

 

             switch (res) {/* 根據ISR的返回值決定下一步操作。主要是處理中斷線程化。 */

             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;

                       }

 

                       /**

                        * 喚醒本ISR對應的中斷線程,由線程執行真正的處理函數。

                        */

                       irq_wake_thread(desc, action);

 

                       /* Fall through to add to randomness */

             case IRQ_HANDLED:/* 已經在中斷上下文中處理了ISR */

                       random |= action->flags;/* 將所有ISR標志取或,只要其中一個ISRIRQF_SAMPLE_RANDOM標志,就將本中斷作為一個中斷源 */

                       break;

 

             default:

                       break;

             }

 

             /**

              * 將所有ISR的返回值取或,那么,只要有一個返回了IRQ_HANDLED,上層都會認為中斷得到了正確的處理。

              */

             retval |= res;

             /**

              * 處理下一個ISR.

              */

             action = action->next;

    } while (action);

 

    /**

     * 如果本中斷是一個隨機源,則處理隨機種子。

     */

    if (random & IRQF_SAMPLE_RANDOM)

             add_interrupt_randomness(irq);

 

    /**

     * 在舊版本的內核中,如果多次產生中斷而且沒有ISR對中斷進行響應,那么就會認為是有問題主板並禁用該中斷。

     * 新版本內核允許通過調試標志來關閉這個功能。

     * 如果打開了這個功能,則進行中斷統計,並在合適的時候關閉該中斷。

     */

    if (!noirqdebug)

             note_interrupt(irq, desc, retval);

    return retval;

}

 

1.1.1.1          中斷線程化

中斷線程化功能已經從實時補丁合入到主流內核中。在linux3.0中,驅動可以將ISR注冊為在中斷上下文運行,或者在線程上下文中運行。

執行ISR的線程主函數是irq_thread:

/**

 * 線程化中斷主處理函數

 *                  data:         線程參數,即ISR描述符

 */

static int irq_thread(void *data)

{

    /**

     * 線程優先級,默認為50.

     */

    static const struct sched_param param = {

             .sched_priority = MAX_USER_RT_PRIO/2,

    };

    struct irqaction *action = data;

    struct irq_desc *desc = irq_to_desc(action->irq);

    irqreturn_t (*handler_fn)(struct irq_desc *desc,

                       struct irqaction *action);

    int wake;

 

    if (force_irqthreads & test_bit(IRQTF_FORCED_THREAD,

                                         &action->thread_flags))

             handler_fn = irq_forced_thread_fn;

    else

             handler_fn = irq_thread_fn;

 

    sched_setscheduler(current, SCHED_FIFO, &param);

    current->irqaction = action;

 

    /**

     * 等待中斷將本線程喚醒。或者線程被中止則退出。

     */

    while (!irq_wait_for_interrupt(action)) {

 

             /**

              * 檢查中斷親和性是否被重新設置,如果設置了,則將線程遷移到相應的CPU上。

              */

             irq_thread_check_affinity(desc, action);

 

             /**

              * 記錄活動中斷線程個數。

              */

             atomic_inc(&desc->threads_active);

 

             /**

              * 獲取中斷描述符的自旋鎖。並關中斷。這是因為我們將要判斷中斷標志,這些標志在其他核或者中斷中都可能被修改。

              */

             raw_spin_lock_irq(&desc->lock);

             if (unlikely(irqd_irq_disabled(&desc->irq_data))) {/* 中斷被禁止 */

                       /*

                        * CHECKME: We might need a dedicated

                        * IRQ_THREAD_PENDING flag here, which

                        * retriggers the thread in check_irq_resend()

                        * but AFAICT IRQS_PENDING should be fine as it

                        * retriggers the interrupt itself --- tglx

                        */

                       desc->istate |= IRQS_PENDING;/* 此時必須退出,但是確實產生了中斷,導致中斷丟失,這里設置掛起標志,待下次處理。 */

                       raw_spin_unlock_irq(&desc->lock);

             } else {

                       irqreturn_t action_ret;

 

                       raw_spin_unlock_irq(&desc->lock);

                       /**

                        * 在線程上下文調用ISR回調函數。

                        */

                       action_ret = handler_fn(desc, action);

                       /**

                        * 這里是檢測中斷線是否有問題。如果遇到有問題的主板,這里禁止相應的中斷線。

                        */

                       if (!noirqdebug)

                                note_interrupt(action->irq, desc, action_ret);

             }

 

             /**

              * 遞減活動線程計數。如果已經沒有活動線程,則設置wake為true。

              */

             wake = atomic_dec_and_test(&desc->threads_active);

 

             /**

              * 已經沒有活動線程了,並且有線程正在等待IRQ處理結束,則喚醒等待的線程,告知它所有中斷都已經處理完畢。

              */

             if (wake && waitqueue_active(&desc->wait_for_threads))

                       wake_up(&desc->wait_for_threads);

    }

 

    /* Prevent a stale desc->threads_oneshot */

    irq_finalize_oneshot(desc, action, true);

 

    /*

     * Clear irqaction. Otherwise exit_irq_thread() would make

     * fuzz about an active irq thread going into nirvana.

     */

    current->irqaction = NULL;

    return 0;

}

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com