zoukankan      html  css  js  c++  java
  • 六、系统软中断、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。性能如果是最重要的,那还是软中断吧。

  • 相关阅读:
    HbuildeX项目打包
    百度地图(vue)
    压缩图片
    安卓移动端唤起拨号与邮箱
    上传图片 原生或vant
    git从零开始
    HbuilderX,监视物理按键
    gitlab备份and恢复数据
    ⑨.kubernetes 之ingress
    ⑧.kubernetes pod调度
  • 原文地址:https://www.cnblogs.com/timemachine213/p/12862651.html
Copyright © 2011-2022 走看看