找回密码
 立即注册
  • QQ空间
  • 回复
  • 收藏

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

admin 2019-10-10 07:00 83人围观 Linux相关


嵌入式linux QQ交流群:175159209,欢迎爱好者加入交流技术问题!

概述:
等待队列、工作队列、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加入到调度,在这个过程中,可以使能和禁止。

嵌入式Linux中文站
最专业的中文嵌入式Linux网站,8年磨剑,注册用户数万人!

分享 嵌入式 & Linux 技术干货、教程、资讯、高薪职位

订阅点击标题下方“嵌入式Linux中文站”

分享点击右上角分享按钮

投稿admin@embeddedlinux.org.cn

交流QQ群:175159209



      点击下方“阅读原文”查看更多



----------------------------------------------------------------------------------------------------------------------
我们尊重原创,也注重分享,文章来源于微信公众号:嵌入式Linux中文站,建议关注公众号查看原文。如若侵权请联系qter@qter.org。
----------------------------------------------------------------------------------------------------------------------

鲜花

握手

雷人

路过

鸡蛋

yafeilinux和他的朋友们微信公众号二维码

微信公众号

专注于Qt嵌入式Linux开发等。扫一扫立即关注。

Qt开源社区官方QQ群二维码

QQ交流群

欢迎加入QQ群大家庭,一起讨论学习!

我有话说......