六、系統軟中斷、tasklet、工作隊列work queue的區別及使用


 前言:這篇文章不會對系統軟中斷、tasklet、工作隊列work queue的內核實現機制進行深入分析,僅僅是談一下這幾種機制的不同以及簡單的使用。有描述不對的地方,歡迎大家指出。

說明:在分析具體代碼時候,用I.MX6Q平台的串口驅動代碼來進行分析,內核版本是3.0.35版本

一、系統軟中斷

講軟中斷之前,我們先來了解一下兩個術語,“中斷上半部”,“中斷下半部”。中斷上半部,也就是我們在裸機開發里面經常提到的中斷處理函數,這些中斷梳理函數都有一個顯著的共同點,就是要求快進快出,只做少量和硬件相關的操作,數據處理以及其他耗時的工作通常放到其他地方來做。如果中斷處理函數執行時間過長,會丟失本CPU中斷,因為在進入中斷處理函數時候一般都禁止了本地CPU中斷,當下一個中斷到來的時候,中斷函數還沒執行完。在Linux系統中,除了基本的快進快出之外,還要求中斷處理函數中不能休眠,否則在中斷處理函數中休眠會引發系統崩潰。為了解決這個問題,引入了中斷下半部的概念,把耗時的工作放到中斷下半部來執行,想當於是把本次的中斷延長了,下半部的工作基本上都是對時間要求不是那么嚴格。

看一下I.MX6Q串口中斷的相關代碼(我這里只取出部分代碼片段,完整的代碼建議直接看源碼),文件路徑是:drivers/tty/serial/imx.c

//I.MX6系列串口接收中斷處理函數
static irqreturn_t imx_rxint(int irq, void *dev_id)
{
    struct imx_port *sport = dev_id;
    unsigned int rx, flg, ignored = 0;
    struct tty_port *port = &sport->port.state->port;
    unsigned long flags, temp;

    spin_lock_irqsave(&sport->port.lock, flags);//保存中斷狀態,禁止本地中斷,並獲取自旋鎖

    while (readl(sport->port.membase + USR2) & USR2_RDR) {//讀取數據寄存器
        flg = TTY_NORMAL;
        sport->port.icount.rx++;//記錄接收數量

        rx = readl(sport->port.membase + URXD0);

        temp = readl(sport->port.membase + USR2);
        if (temp & USR2_BRCD) {
            writel(USR2_BRCD, sport->port.membase + USR2);
            if (uart_handle_break(&sport->port))
                continue;
        }

        if (uart_handle_sysrq_char(&sport->port, (unsigned char)rx))
            continue;
        ....
.... rx
&= (sport->port.read_status_mask | 0xFF);         //判斷接收數據是否正常 if (rx & URXD_BRK) flg = TTY_BREAK; else if (rx & URXD_PRERR) flg = TTY_PARITY; else if (rx & URXD_FRMERR) flg = TTY_FRAME; if (rx & URXD_OVRRUN) flg = TTY_OVERRUN; #ifdef SUPPORT_SYSRQ sport->port.sysrq = 0; #endif } if (sport->port.ignore_status_mask & URXD_DUMMY_READ) goto out; tty_insert_flip_char(port, rx, flg);//將數據放入tty } out: spin_unlock_irqrestore(&sport->port.lock, flags);//將中斷狀態恢復到以前的狀態,激活本地中斷,釋放自旋鎖 tty_flip_buffer_push(port);//里面調度一個工作隊列,類似於告知tty可以來取數據了 return IRQ_HANDLED; }

上面這個函數就是I.MX6Q的串口接收中斷處理函數,可以看到,函數中基本上就只做了硬件寄存器的判斷及字節接收,沒有多余的操作。

所以總結以下中斷上半部和下半部的一些區別及使用場景:

中斷上半部:

(1)對時間要求比較高的工作

(2)硬件相關操作

(3)不能被中斷打斷,因為進入中斷時候一般都會禁止本地CPU

中斷下半部:

(1)可延遲執行的操作(對時間要求不高)

PS:中斷下半部可以被其他中斷打斷。

那中斷下半部是依賴什么機制實現的呢?那就是需要系統的軟中斷來保證了,軟中斷還有一種說法叫“可延遲函數”,下面我們要講到的tasklet就是在軟中斷的基礎上實現的。不過在這之前我們還是先來看一下軟中斷相關的代碼。

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

目前Linux系統最多支持32個軟中斷,系統已經定義使用了10個,剩下的用戶可以自己指定,但是看一下前面的說明!避免自己創建軟中斷,如果不是需要高頻率的線程工作調度,一般來說系統提供的軟中斷已經夠我們使用了,我們在日常開發中最好還是遵循系統給出的指導建議,避免出現異常,日常的學習調試,我們可以創建自己的軟中斷,加深對這方面知識的理解。上面列出的軟中斷類型越靠前優先級越高,其中有兩個需要關注一下,就是HI_SOFTIRQ和TASKLET_SOFTIRQ,系統已經幫我們初始化好了,tasklet就是基於這兩個軟中斷去實現的。具體代碼如下:

 

//init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    /*
     * Need to run as early as possible, to initialize the
     * lockdep hash:
     */
    lockdep_init();
    set_task_stack_end_magic(&init_task);
    smp_setup_processor_id();
    debug_objects_early_init();

    /*
     * Set up the the initial canary ASAP:
     */
    boot_init_stack_canary();

    cgroup_init_early();

    local_irq_disable();
    early_boot_irqs_disabled = true;

    ....
....
/* * These use large bootmem allocations and must precede * kmem_cache_init() */ setup_log_buf(0); pidhash_init(); vfs_caches_init_early(); sort_main_extable(); trap_init(); mm_init(); ....
.... early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init();
//初始化軟中斷 timekeeping_init(); time_init(); .... .... .... #ifdef CONFIG_X86_ESPFIX64 /* Should be run before the first non-init thread is created */ init_espfix_bsp(); #endif thread_info_cache_init(); cred_init(); fork_init(); ....
.... check_bugs(); acpi_subsystem_init(); sfi_init_late();
if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_late_init(); efi_free_boot_services(); } ftrace_init(); /* Do the rest non-__init'ed, we're now alive */ rest_init(); } //kernel/softirq.c void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); }

 

其中:

open_softirq(TASKLET_SOFTIRQ, tasklet_action);

open_softirq(HI_SOFTIRQ, tasklet_hi_action);

就是系統為我們初始化好的和tasklet相關的軟中斷。我們也可以自己定義屬於自己的軟中斷,方法如下:

1.添加我們自己的軟中斷

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    MY_SOFTIRQ,      /*我自己添加的軟中斷*/ 
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

2.在kernel/softirq.c中定義自己的軟中斷處理函數

//我自己定義的軟中斷處理函數
static void my_softirq_action(struct softirq_action *a)
{
    ...
}

3.初始化

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
    open_softirq(MY_SOFTIRQ, tasklet_hi_action);//我自己定義的軟中斷
}

4.激活

raise_softirq(MY_SOFTIRQ);

以上就是自己定義的軟中斷的流程。

定義了軟中斷,那跟系統自帶的軟中斷,它們在什么時候得到執行呢?我們來看一下 do_softirq函數:

文件路徑:kernel/softirq.c

 

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())//判斷當前是否處於中斷狀態
        return;

    local_irq_save(flags);//保存中斷標記

    pending = local_softirq_pending();

    if (pending)//循環處理已經注冊的軟中斷
        __do_softirq();

    local_irq_restore(flags);
}

 

由此可以知道,軟中斷並不是立即被執行,當系統調度到它的時候才能得到執行。

 

 二、tasklet

前面我們講到,tasklet也是軟中斷的一種,tasklet的實現利用到了軟中斷的機制,下面我們來講一下軟中斷的使用。

我們將I.MX6Q的串口驅動改造一下,使用tasklet機制將數據push到tty的線路規程(I.MX6的串口驅動和tty驅動這里就不展開講了,我前面的文章有分析過)

1.首先在串口驅動結構體中定義一個tasklet類型的結構體變量。

struct imx_port {
    struct uart_port    port;
    struct timer_list    timer;
    unsigned int        old_status;
    int            txirq,rxirq,rtsirq;
    unsigned int        have_rtscts:1;
    unsigned int        use_dcedte:1;
    unsigned int        use_irda:1;
    unsigned int        irda_inv_rx:1;
    unsigned int        irda_inv_tx:1;
    unsigned short        trcv_delay; /* transceiver delay */
    struct clk        *clk;

    /* DMA fields */
    int            enable_dma;
    struct imx_dma_data    dma_data;
    struct dma_chan        *dma_chan_rx, *dma_chan_tx;
    struct scatterlist    rx_sgl, tx_sgl[2];
    void            *rx_buf;
    unsigned int        rx_bytes, tx_bytes;
    struct work_struct    tsk_dma_rx, tsk_dma_tx;
    unsigned int        dma_tx_nents;
    bool            dma_is_rxing;
    wait_queue_head_t    dma_wait;
    /*使用tasklet機制的串口接收中斷下半部*/
    struct tasklet_struct my_tasklet_rx;
};

2.編寫軟中斷處理函數(注意函數的參數類型,這些都是參照系統中其他人寫的驅動來寫的,一般來說都是通用的)

/*tasklet機制實現的下半部處理函數*/
static void my_tasklet_fun(unsigned long arg)
{
    struct tty_struct *tty = (struct tty_struct *)arg;

    unsigned long flags;
    spin_lock_irqsave(&tty->buf.lock, flags);
    if (tty->buf.tail != NULL)
        tty->buf.tail->commit = tty->buf.tail->used;
    spin_unlock_irqrestore(&tty->buf.lock, flags);

    flush_to_ldisc(&tty->buf.work);
}

3.初始化,將tasklet軟中斷處理函數和tasklet掛鈎

static int imx_startup(struct uart_port *port)
{
    ....
    ....
    tasklet_init(&sport->my_tasklet_rx, my_tasklet_fun, (unsigned long) sport);//初始化tasklet
    /* Enable the SDMA for uart. */
    if (sport->enable_dma) {
        int ret;
        ret = imx_uart_dma_init(sport);
        if (ret)
            goto error_out3;

        sport->port.flags |= UPF_LOW_LATENCY;
        INIT_WORK(&sport->tsk_dma_tx, dma_tx_work);
        INIT_WORK(&sport->tsk_dma_rx, dma_rx_work);
        init_waitqueue_head(&sport->dma_wait);
    }
    ....

    if (sport->enable_dma) {
        temp = readl(sport->port.membase + UCR4);
        temp |= UCR4_IDDMAEN;
        writel(temp, sport->port.membase + UCR4);
    }
    ....
}

4.在串口接收中斷中調用tasklet_schedule觸發調度tasklet

static irqreturn_t imx_rxint(int irq, void *dev_id)
{
    struct imx_port *sport = dev_id;
    unsigned int rx,flg,ignored = 0;
    struct tty_struct *tty = sport->port.state->port.tty;
    unsigned long flags, temp;

    spin_lock_irqsave(&sport->port.lock,flags);
        ....
        ....
out:
    spin_unlock_irqrestore(&sport->port.lock,flags);
    //tty_flip_buffer_push(tty);
    tasklet_schedule(&sport->my_tasklet_rx);//調度tasklet
    return IRQ_HANDLED;
}

定義tasklet變量,實現軟中斷處理函數,初始化,調度,以上這些就是tasklet的使用步驟了,內核幫我們省略了很多麻煩的實現,所以使用起來比較簡單。

 三、工作隊列work queue

前面已經講了軟中斷還有tasklet了,那這里的工作隊列和它們有什么區別呢?為什么會存在工作隊列機制?

存在即是合理,既然存在那肯定是用來彌補前兩者的缺陷的,所以我們先來分析看看前兩者有什么缺點。

軟中斷和tasklet是運行於中斷上下文的,它們屬於內核態沒有進程的切換,因此在執行過程中不能休眠,不能阻塞,一旦休眠或者阻塞,則系統直接掛死。比如我調試驅動時候,曾經在中斷處理函數中調用spi同步數據的函數,系統直接掛死了,后來看代碼的說明才明白,不能在中斷中調用休眠,阻塞的函數。因此軟中斷和tasklet是有一定的使用局限性的,工作隊列的出現正是用在軟中斷和tasklet不能使用的場合,比如需要調用一個具有可延遲函數的特質,但是這個函數又有可能引起休眠、阻塞。

下面簡單講一下工作隊列的使用,還是以I.MX6Q的串口接收中斷處理函數作為修改對象,因為I.MX6Q的串口驅動,如果開啟的串口DMA的話,就是使用工作隊列來實現的,我們直接參考串口DMA相關代碼。

1.定義一個工作隊列對象

struct imx_port {
    struct uart_port    port;
    struct timer_list    timer;
        ....
        ....
    struct work_struct    tsk_dma_rx, tsk_dma_tx;
    unsigned int        dma_tx_nents;
    bool            dma_is_rxing;
    wait_queue_head_t    dma_wait;
    /*使用tasklet機制的串口接收中斷下半部*/
    //struct tasklet_struct my_tasklet_rx;
    /*工作隊列機制*/
    struct work_struct    my_work_tsk,
};

2.編寫工作隊列處理函數

/*工作隊列機制(如何從參數中獲取設備信息,在這里我參考了串口DMA的做法,就是調用container_of函數來實現)*/
static void my_work_fun(struct work_struct *w)
{
    struct imx_port *sport = container_of(w, struct imx_port, my_work_tsk);
    struct tty_struct *tty = sport->port.state->port.tty;

    unsigned long flags;
    spin_lock_irqsave(&tty->buf.lock, flags);
    if (tty->buf.tail != NULL)
        tty->buf.tail->commit = tty->buf.tail->used;
    spin_unlock_irqrestore(&tty->buf.lock, flags);

    flush_to_ldisc(&tty->buf.work);
}

3.初始化工作隊列

static int imx_startup(struct uart_port *port)
{    
    ....
    ....
    /*初始化工作隊列*/
    //tasklet_init(&sport->my_tasklet_rx, my_tasklet_fun, (unsigned long)sport);
        //初始化工作隊列
    INIT_WORK(&sport->my_work_tsk, my_work_fun);
    /* Enable the SDMA for uart. */
    if (sport->enable_dma) {
        int ret;
        ret = imx_uart_dma_init(sport);
        if (ret)
            goto error_out3;

        sport->port.flags |= UPF_LOW_LATENCY;
        INIT_WORK(&sport->tsk_dma_tx, dma_tx_work);//串口DMA也是通過工作隊列實現數據接收
        INIT_WORK(&sport->tsk_dma_rx, dma_rx_work);
        init_waitqueue_head(&sport->dma_wait);
    }
    ....
    ....
}

4.調度工作隊列

static irqreturn_t imx_rxint(int irq, void *dev_id)
{
    struct imx_port *sport = dev_id;
    unsigned int rx,flg,ignored = 0;
    struct tty_struct *tty = sport->port.state->port.tty;
    unsigned long flags, temp;
        ....
        ....
out:
    spin_unlock_irqrestore(&sport->port.lock,flags);
    //tty_flip_buffer_push(tty);
    //tasklet_schedule(&sport->my_tasklet_rx);    //調度tasklet
    schedule_work(&sport->my_work_tsk);        //調度工作隊列
    return IRQ_HANDLED;
}

總結:工作隊列和work queue和tasklet的使用思路是差不多的,各有局限性,實際的使用需要根據自身情況來選擇。工作隊列還有其他的用法,比如創建一個固定周期調度的工作隊列,這個是tasklet無法做到的,下一篇來分享一下延遲工作隊列的創建。

在這里我引用別人總結的比較直觀的一句話:我們在做驅動的時候,關於這三個下半部(也就是以上的三種機制)實現,需要考慮兩點:首先,是不是需要一個可調度的實體來執行需要推后完成的工作(即休眠的需要),如果有,工作隊列就是唯一的選擇,否則最好用tasklet。性能如果是最重要的,那還是軟中斷吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM