linux驱动---等待队列工作队列Tasklets

Posted sky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于linux驱动---等待队列工作队列Tasklets的主要内容,如果未能解决你的问题,请参考以下文章

《深入理解Linux内核》软中断/tasklet/工作队列

Linux 内核 tasklet 机制和工作队列zz

Linux内核中的软中断tasklet和工作队列详解

Linux内核中的软中断tasklet和工作队列具体解释

Linux内核中的软中断tasklet和工作队列详解(超详细~)

工作队列可以被中断吗