转自: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【转】的更多相关文章

  1. Linux 驱动开发

    linux驱动开发总结(一) 基础性总结 1, linux驱动一般分为3大类: * 字符设备 * 块设备 * 网络设备 2, 开发环境构建: * 交叉工具链构建 * NFS和tftp服务器安装 3, ...

  2. Linux驱动设计—— 中断与时钟

    中断和时钟技术可以提升驱动程序的效率 中断 中断在Linux中的实现 通常情况下,一个驱动程序只需要申请中断,并添加中断处理函数就可以了,中断的到达和中断函数的调用都是内核实现框架完成的.所以程序员只 ...

  3. linux驱动面试题整理

    1.字符型驱动设备你是怎么创建设备文件的,就是/dev/下面的设备文件,供上层应用程序打开使用的文件? 答:mknod命令结合设备的主设备号和次设备号,可创建一个设备文件. 评:这只是其中一种方式,也 ...

  4. Linux驱动面试题

    1. Linux设备中字符设备与块设备有什么主要的区别?请分别列举一些实际的设备说出它们是属于哪一类设备. 字符设备:字符设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种 ...

  5. 编写linux驱动所用到的头文件(转)

    转自:http://blog.csdn.net/lufeiop02/article/details/6448497 关于linux驱动(应用)程序头文件使用 收藏 驱动程序: #include < ...

  6. Linux内核等待队列

    在Linux驱动程序设计中,可以使用等待队列来实现进程的阻塞,等待队列可看作保存进程的容器,在阻塞进程时,将进程放入等待队列,当唤醒进程时,从等待等列中取出进程. Linux 2.6内核提供了如下关于 ...

  7. linux驱动面试题目汇总

    http://blog.csdn.net/blueice8601/article/details/7666427 1.linux驱动分类 2.信号量与自旋锁 3.platform总线设备及总线设备如何 ...

  8. linux驱动工程面试必问知识点

    linux内核原理面试必问(由易到难) 简单型 1:linux中内核空间及用户空间的区别?用户空间与内核通信方式有哪些? 2:linux中内存划分及如何使用?虚拟地址及物理地址的概念及彼此之间的转化, ...

  9. Linux驱动之定时器在按键去抖中的应用

    机械按键在按下的过程中会出现抖动的情况,如下图,这样就会导致本来按下一次按键的过程会出现多次中断,导致判断出错.在按键驱动程序中我们可以这么做: 在按键驱动程序中我们可以这么做来取消按键抖动的影响:当 ...

随机推荐

  1. 软件工程之四则运算--Github

    由于现在配置问题,然后借用同学电脑将代码上传至Github,网址为:https://github.com/be821/RealFour 参考相关Github文档: 1. http://my.oschi ...

  2. 基于 Java Web 的毕业设计选题管理平台--测试报告与用户手册

    一.测试报告 1.兼容性测试 功能 描述 效果 Chrome浏览器 FireFox浏览器 IE浏览器 war 端浏览器 管理员登录 管理员用户登录功能 弹出“登录成功”对话框,进入到毕业设计选题管理平 ...

  3. Manjaro Linux 没有声音

    在Multimedia中的PulseAudio Volume Control中的设置可以解决

  4. npm 报错unable to verify the first certificate

    npm总是报错:unable to verify the first certificate 原创 2017年09月30日 11:06:10 https://blog.csdn.net/nicexib ...

  5. Spring注入的不同方式

    1.直接创建一个Bean <bean id="dboperate" class="study.spring2.Test"></bean> ...

  6. swagger error: Conflicting schemaIds: Duplicate schemaIds detected for types A and B

    使用Web API并使用swashbuckle生成swagger文档,我在两个不同的命名空间中定义了两个具有相同名称的不同类.当我在浏览器中打开swagger页面时,它说: Conflicting s ...

  7. VC++ 常见问题及其解决方法

    1. 无法找到“XXX.exe”的调试信息,或者调试信息不匹配: 选择 配置属性->链接器->调试->生成调试信息 改为 是 选择 配置属性->C/C++ ->常规-&g ...

  8. Hello 2018 A,B,C,D

    A. Modular Exponentiation time limit per test 1 second memory limit per test 256 megabytes input sta ...

  9. (转)DATATABLE(DATASET)与实体类之间的互转.

    转自:http://www.cnblogs.com/zzyyll2/archive/2010/07/20/1781649.html dataset和实体类 之间的转换 //dataset转实体类  代 ...

  10. 【暴力Treap 或 离线归并】子串计数(genies)

    子串计数(genies) Description 给出一段含有n个元素的序列a,要求求出子串和小于等于t的子串个数 Input Data 输入共两行第一行包含两个整数,n,t分别表示序列a元素的个数和 ...