• linux驱动---等待队列、工作队列、Tasklets【转】


    转自:https://blog.csdn.net/ezimu/article/details/54851148

    概述:

    等待队列、工作队列、Tasklet都是linux驱动很重要的API,下面主要从用法上来讲述如何使用API.

    应用场景:

    • 等待队列(waitqueue)
      linux驱动中,阻塞一般就是用等待队列来实现,将进程停止在此处并睡眠下,直到条件满足时,才可通过此处,继续运行。在睡眠等待期间,wake up时,唤起来检查条件,条件满足解除阻塞,不满足继续睡下去。

    • 工作队列(workqueue)
      工作队列,将一个work提交到workqueue上,而这个workqueue是挂到一个特殊内核进程上,当这个特殊内核进程被调度时,会从workqueue上取出work来执行。当然这里的work是与函数联系起来的。这个过程表现为,此刻先接下work,但不立刻执行这个work,等有时间再执行,而这个时间是不确定的。
      工作队列运行在进程上下文,可以睡眠。

    • Tasklet
      Tasklet,同样,也是先接下任务,但不立刻做任务,与work很类似。tasklet运行在软中断上下文。

      软中断:有这样三句话理解”硬中断是外部设备对CPU的中断”,”软中断通常是硬中断服务程序对内核的中断”,”信号则是由内核(或其他进程)对某个进程的中断”

      这三句话,是比较笼统的理解,现在回到linux具体来理解:

      • 软中断触发时机:
        (1)中断上下文触发(在中断服务程序中),在中断服务程序退出后,软中断会得到立马处理。
        (2)非中断上下文(也可以理解进程上下文),通过唤醒守护进程ksoftirqd,只有当守护进程得到调度后,软中断才会得到处理。
        不管是中断上下文,还是非中断上下文,最终都是调用__do_softirq实现的软中断,在这个函数里面是打开硬件中断,关闭内核抢占。这就是软中断上下文,即开硬件中断,关闭抢占。

      • tasklet是基于软中断实现的,用在中断服务程序触发tasklet,则就是中断下半部分,也是用得最多的情况。用在进程上下文触发tasklet,则很类似workqueue,但是tasklet不能有睡眠(因为关闭抢占的,不考虑硬件中断,就是原子性的),也不适合做非常耗时的,如果是非常耗时的,尽量交给workqueue(可以在tasklet回调里面用work,把更耗时,时间要求更不高的,交给workqueue)。

    软中断详细了解,可参考如下博文:
    linux软中断机制分析
    linux中断底半部之 softirq 原理与代码分析
    linux软中断与硬中断实现原理概述
    硬中断、软中断和信号

    等待队列(waitqueue)

    • 定义头文件:
    #include <linux/wait.h>

     

    • 定义和初始化等待队列头(workqueue):
      静态的,用宏:
    #define DECLARE_WAIT_QUEUE_HEAD(name) 
        wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

    动态的,也是用宏:

    #define init_waitqueue_head(q)              
        do {                        
            static struct lock_class_key __key; 
                                
            __init_waitqueue_head((q), #q, &__key); 
        } while (0)

    如:

    wait_queue_head_t wq;
    init_waitqueue_head(&wq);
    • 阻塞接口:
      都是些宏:
    wait_event(wq, condition)
    wait_event_timeout(wq, condition, timeout)
    wait_event_interruptible(wq, condition)
    wait_event_interruptible_timeout(wq, condition, timeout)
    wait_event_hrtimeout(wq, condition, timeout)
    wait_event_interruptible_hrtimeout(wq, condition, timeout)
    wait_event_interruptible_exclusive(wq, condition)
    wait_event_interruptible_locked(wq, condition)
    wait_event_interruptible_locked_irq(wq, condition)
    wait_event_interruptible_exclusive_locked(wq, condition)
    wait_event_interruptible_exclusive_locked_irq(wq, condition)
    wait_event_killable(wq, condition)
    wait_event_lock_irq_cmd(wq, condition, lock, cmd)
    wait_event_lock_irq(wq, condition, lock)
    wait_event_interruptible_lock_irq_cmd(wq, condition, lock, cmd)
    wait_event_interruptible_lock_irq(wq, condition, lock)
    wait_event_interruptible_lock_irq_timeout(wq, condition, lock,  timeout)

    接口版本比较多,各自都有自己合适的应用场合,但是常用的是前面四个。
    其中wq是我们定义的等待队列头,condition为条件表达式,当wake up后,condition为真时,唤醒阻塞的进程,为假时,继续睡眠。
    wait_event:不可中断的睡眠,条件一直不满足,会一直睡眠。
    wait_event_timeout:不可中断睡眠,当超过指定的timeout(单位是jiffies)时间,不管有没有wake up,还是条件没满足,都要唤醒进程,此时返回的是0。在timeout时间内条件满足返回值为timeout或者1;
    wait_event_interruptible:可被信号中断的睡眠,被信号打断唤醒时,返回负值-ERESTARTSYS;wake up时,条件满足的,返回0。除了wait_event没有返回值,其它的都有返回,有返回值的一般都要判断返回值。如下例:

    int flag = 0;
    if(wait_event_interruptible(&wq,flag == 1))
        return -ERESTARTSYS;
    

    wait_event_interruptible_timeout:是wait_event_timeout和wait_event_interruptible_timeout的结合版本,有它们两个的特点。

    其他的接口,用的不多,有兴趣可以自己看看。

    • 解除阻塞接口(唤醒)
      接口也是些宏:
    #define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)
    #define wake_up_nr(x, nr)       __wake_up(x, TASK_NORMAL, nr, NULL)
    #define wake_up_all(x)          __wake_up(x, TASK_NORMAL, 0, NULL)
    #define wake_up_locked(x)       __wake_up_locked((x), TASK_NORMAL, 1)
    #define wake_up_all_locked(x)       __wake_up_locked((x), TASK_NORMAL, 0)
    
    #define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
    #define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
    #define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
    #define wake_up_interruptible_sync(x)   __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)

    wake_up:一次只能唤醒挂在这个等待队列头上的一个进程
    wake_up_nr:一次唤起nr个进程(等待在同一个wait_queue_head_t有很多个)
    wake_up_all:一次唤起所有等待在同一个wait_queue_head_t上所有进程
    wake_up_interruptible:对应wait_event_interruptible版本的wake up
    wake_up_interruptible_sync:保证wake up的动作原子性,wake_up这个函数,很有可能函数还没执行完,就被唤起来进程给抢占了,这个函数能够保证wak up动作完整的执行完成。
    其他的也是与对应阻塞接口对应的。

    • 灵活的添加删除等待队列头中的等待队列:
      这小节,可以不看,对应用,不是很重要。
      (1)定义:
      静态:
    #define DECLARE_WAITQUEUE(name, tsk)                    
        wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

    (2)动态:

    wait_queue_t wa;
    init_waitqueue_entry(&wa,&tsk);

    tsk是进程结构体,一般是current(linux当前进程就是用这个获取)。还可以用下面的,设置自定义的等待队列回调函数,上面的是linux默认的一个回调函数default_wake_function(),不过默认的用得最多:

    wait_queue_t wa;
    wa->private = &tsk;
    int func(wait_queue_t *wait, unsigned mode, int flags, void *key)
    {
        //
    }
    init_waitqueue_func_entry(&wa,func);

    (回调有什么作用?)
    用下面函数将等待队列,加入到等待队列头(带remove的是从工作队列头中删除工作队列):

    extern void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
    extern void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait);
    extern void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

    上面的阻塞和解除阻塞接口,只能是对当前进程阻塞/解除阻塞,有了这几个灵活的接口,我们可以单独定义一个等待队列,只要获取进程task_struct指针,我们可以将任何进程加入到这个等待队列,然后加入到等待队列头,我们能将其它任何进程(不仅仅是当前进程),挂起睡眠,当然唤醒时,如果用wake_up_all版本的话,也会一同唤起。这种情况,阻塞不能用上面的接口了,我们需要用下一节讲述的接口(schedule()),解除阻塞可以用wake_up,wake_up_interruptible等。

    • 更高级灵活的阻塞:
      阻塞当前进程的原理:用函数set_current_state()修改当前进程为TASK_INTERRUPTIBLE(不可中断睡眠)或TASK_UNINTERRUPTIBLE(可中断睡眠)状态,然后调用schedule()告诉内核重新调度,由于当前进程状态已经为睡眠状态,自然就不会被调度。schedule()简单说就是告诉内核当前进程主动放弃CPU控制权。这样来,就可以说当前进程在此处睡眠,即阻塞在这里。

    在上一小节“灵活的添加删等待队列头中的等待队列”,将任意进程加入到waitqueue,然后类似用:

    task_struct *tsk;
    wait_queue_t wa;
    //假设tsk已经指向某进程控制块
    p->state = TASK_INTERRUPTIBLE;//or TASK_UNINTERRUPTIBLE
    init_waitqueue_entry(&wa,&tsk);

    就能将任意进程挂起,当然,还需要将wa,挂到等待队列头,然后用wait_event(&wa),进程就会从就绪队列中退出,进入到睡眠队列,直到wake up时,被挂起的进程状态被修改为TASK_RUNNING,才会被再次调度。(主要是schedule()下面会说到)。

    先看下wait_event实现:

    #define __wait_event(wq, condition)                     
    do {                                    
        DEFINE_WAIT(__wait);                        
                                        
        for (;;) {                          
            prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    
            if (condition)                      
                break;                      
            schedule();                     
        }                               
        finish_wait(&wq, &__wait);                  
    } while (0)
    
    #define wait_event(wq, condition)                   
    do {                                    
        if (condition)                          
            break;                          
        __wait_event(wq, condition);                    
    } while (0)

    prepare_to_wait:
    定义:void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
    功能:将工作队列wait加入到工作队列头q,并将当前进程设置为state指定的状态,一般是TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE状态(在这函数里有调用set_current_state)。
    第一个参数:工作队列头
    第二个参数:工作队列
    第三个参数:当前进程要设置的状态

    DEFINE_WAIT:定义一个工作队列。
    finish_wait:用了prepare_to_wait之后,当退出时,一定要用这个函数清空等待队列。

    从这个宏的实现,可以看出睡眠进程过程,prepare_to_wait先修改进程到睡眠状态,条件不满足,schedule()就放弃CPU控制权,睡眠,当wake up的时候,阻塞在wq(也可以说阻塞在wait_event处)等待队列头上的进程,再次得到运行,接着执行schedule()后面的代码,这里,显然是个循环,prepare_to_wait再次设置当前进程为睡眠状态,然后判断条件是否满足,满足就退出循环,finish_wait将当前进程恢复到TASK_RUNNING状态,也就意味着阻塞解除。不满足,继续睡下去。如此反复等待条件成立。

    明白这个过程,用prepare_to_wait和schedule()来实现更为灵活的阻塞,就很简单了,解除阻塞和前面的一样用wake_up,wake_up_interruptible等。

    wait_queue_t成员flage重要的标志WQ_FLAG_EXCLUSIVE,表示:

    • 当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾
      部. 没有这个标志的入口项, 添加到开始.
    • 当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标
      志的进程后停止.

    wait_event默认总是将waitqueue加入开始,而wake_up时总是一个一个的从开始处唤醒,如果不断有waitqueue加入,那么最开始加入的,就一直得不到唤醒,有这个标志,就避免了这种情况。

    prepare_to_wait_exclusive()就是加入了这个标志的。

    工作队列:

    • 头文件:
    #include <linux/workqueue.h>

     

    • 创建workqueue:
    #define create_workqueue(name)                      
        alloc_workqueue((name), WQ_MEM_RECLAIM, 1)
    
    #define create_singlethread_workqueue(name)             
        alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)

    这两个宏都会返回一个workqueue_struct结构体的指针,并且都会创建进程(“内核线程”)来执行加入到这个workqueue的work。
    create_workqueue:多核CPU,这个宏,会在每个CPU上创建一个专用线程。
    create_singlethread_workqueue:单核还是多核,都只在其中一个CPU上创建线程。

    用法例子:

    struct workqueue *wq,*ws;
    wq = create_workqueue("wqname");
    ws = create_singlethread_workqueue("wsname");
    • 定义work:
      (1)静态(其实,将这个宏,放到局部变量里面,也是个动态的):
    #define DECLARE_WORK(n, f)                      
        struct work_struct n = __WORK_INITIALIZER(n, f)

    用法例子:

    void func(struct work_struct *work)
    {
    
    }
    DECLARE_WORK(wo,func);

    (2)动态定义:

    #define INIT_WORK(_work, _func)                     
        do {                                
            __INIT_WORK((_work), (_func), 0);           
        } while (0)

    用法例子:

    void func(struct work_struct *work)
    {   
    }
    struct work_struct wo;
    INIT_WORK(&wo,func);

    还用如下宏,用来修改work绑定的函数:

    #define PREPARE_WORK(_work, _func)                  
        do {                                
            (_work)->func = (_func);                
        } while (0)

    如:

    void func(struct work_struct *work){}
    void funca(struct work_struct *work){}
    struct work_struct wo;
    INIT_WORK(&wo,func);
    PREPARE_WORK(&wo,funca);

    修改绑定的函数后,当下次调度到,funca函数被调度,不再是func。

    (3)将work加入到workqueue
    有两个函数:

    bool queue_work(struct workqueue_struct *wq,struct work_struct *work);
    bool queue_delayed_work(struct workqueue_struct *wq,
                          struct delayed_work *dwork,
                          unsigned long delay);

    两个函数的返回值:
    返回0,表示work在这之前,已经在workqueue中了
    返回非0,表示work成功加入到workqueue中了
    queue_delayed_work表示不是马上把work加入到workqueue中,而是延后delay(时间单位jiffies),再加入。注意它的work(dwork)要用宏(静态)DECLARE_DELAYED_WORK来定义和初始化,动态的可以用INIT_DELAYED_WORK,用法和没有延后的差不多。

    需要注意:当这个work被调度一次后,就从workqueue中取消了,如果还需要work被调度到(即work中的函数再被调用),需要重新加入到workqueue中,一般可以直接在work绑定的函数,最后一行调用这个两个函数再次加入。

    (4)取消work
    有两个版本
    queue_work对应的版本:

    bool cancel_work_sync(struct work_struct *work);

     

    注意:调用这个函数,必须确保work所在的workqueue没被销毁,调用这函数的进程会等待这个work执行完成(得不到执行,进程会阻塞等待),再取消这个work。这个函数返回后,work肯定是被执行了。

    queue_delayed_work对应的版本:

    bool cancel_delayed_work(struct delayed_work *dwork);
    bool cancel_delayed_work_sync(struct delayed_work *dwork);

    cancel_delayed_work:返回后,work并不一定被取消,有可能还在运行。
    cancel_delayed_work_sync:返回后,work肯定已经被取消了。等到work被执行后,取消完成才返回。

    销毁workqueue
    销毁函数:

    void destroy_workqueue(struct workqueue_struct *wq);

     

    在销毁前,最好调用flush_workqueue来确保在这workqueue上的work都处理完了:

    void flush_workqueue(struct workqueue_struct *wq);

     

    总结:工作队列步骤,首先是创建workqueue和定义初始化work,然后将work加入到workqueue中。最后,不要时,销毁workqueue。

    共享工作队列
    共享队列,就是系统创建了默认的workqueue,只需要定义初始化work,调用接口就完成。
    两个接口:

    bool schedule_work(struct work_struct *work);
    bool schedule_delayed_work(struct delayed_work *dwork,
                         unsigned long delay);

    例子:

    void func(struct work_struct *work)
    {   
    }
    struct work_struct wo;
    INIT_WORK(&wo,func);
    schedule_work(&wo);

    取消还是用:

    bool cancel_work_sync(struct work_struct *work);
    bool cancel_delayed_work(struct delayed_work *dwork);
    bool cancel_delayed_work_sync(struct delayed_work *dwork);

    对应版本接口,用对应版本接口取消。

    取消后,一般需要调用下面接口,确保work完成,并取消了:

    void flush_scheduled_work(void);

     

    flush_scheduled_work能确保在系统默认创建的workqueue上所有的work都完成了。

    Tasklet

    • 头文件:
    #include <linux/interrupt.h>

     

    • 定义和初始化:
      (1)静态:**
    struct tasklet_struct
    {
        struct tasklet_struct *next;
        unsigned long state;
        atomic_t count;
        void (*func)(unsigned long);
        unsigned long data;
    };
    
    #define DECLARE_TASKLET(name, func, data) 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
    
    #define DECLARE_TASKLET_DISABLED(name, func, data) 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

    name:定义的tasklet_struct结构体变量
    func:回调函数void (*func)(unsigned long);
    data:私有数据可以是具体一个整数,或者指针。没有一般为0。

    DECLARE_TASKLET定义是直接可以用tasklet_schedule()加入到调度的。
    DECLARE_TASKLET_DISABLED定义的,用这个tasklet_schedule()也无法调度到,需要使用tasklet_enable()使能,才可以被调度运行。

    (2)动态:

    void tasklet_init(struct tasklet_struct *t,
                 void (*func)(unsigned long), unsigned long data);

    用法:

    struct tasklet_struct tl;
    void func(unsigned long){}
    tasklet_init(&tl,func,0);

    函数接口:

    void tasklet_schedule(struct tasklet_struct *t);
    void tasklet_hi_schedule(struct tasklet_struct *t);
    void tasklet_hi_schedule_first(struct tasklet_struct *t);
    void tasklet_disable_nosync(struct tasklet_struct *t);
    void tasklet_disable(struct tasklet_struct *t);
    void tasklet_enable(struct tasklet_struct *t);
    void tasklet_kill(struct tasklet_struct *t);
    void tasklet_init(struct tasklet_struct *t,
                 void (*func)(unsigned long), unsigned long data);

    tasklet_schedule:将tasklet加入到调度链表里面,tasklet就能得到执行,每调用这个函数一次,tasklet只能执行一次,要再次执行需要重新调用这个函数。
    tasklet_hi_schedule:比tasklet_schedule优先级更高,可以得到更快处理。
    tasklet_hi_schedule_first:和tasklet_hi_schedule差不多,只是更安全。
    tasklet_disable:禁止tasklet,即使tasklet_schedule已经把tasklet调度链表里,也得不到执行,必须要用tasklet_enable使能才可以。如果当前tasklet正在运行,tasklet_disable会等待执行完,然后禁止,返回。
    tasklet_disable_nosync:和tasklet_disable一样,如果当前tasklet在运行,这个函数不会等待完成就先返回,当tasklet完成退出后,再禁止。
    tasklet_enable:使能tasklet,和tasklet_disable要成对使用。
    tasklet_kill:设备关闭和模块卸载的时候,调用来杀死tasklet。如果当前tasklet在运行,会等待完成后,再杀死。
    tasklet_init:初始化tasklet。

    tasklet步骤:定义初始化绑定函数,然后调用接口把tasklet加入到调度,在这个过程中,可以使能和禁止。

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/eZiMu/article/details/54851148
  • 相关阅读:
    2013工作回望
    在Skyline 控件上面显示Web信息窗体
    Extjs xtype 为lable 设置
    纪念第一篇博客
    前端技术学习经验分享(第二、三天---学习过程)
    前端技术学习经验分享(第一天---布置学习环境)
    JS开发HTML5游戏《悠悠考拉》(三)
    JS开发HTML5游戏《悠悠考拉》(二)
    JS开发HTML5游戏《悠悠考拉》(一)
    产品经理C端转B端,我后悔了
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/9099813.html
Copyright © 2020-2023  润新知