概述:

等待队列、工作队列、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>
  • 1
  • 定义和初始化等待队列头(workqueue):
    静态的,用宏:
#define DECLARE_WAIT_QUEUE_HEAD(name) \wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
  • 1

动态的,也是用宏:

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

如:

wait_queue_head_t wq;
init_waitqueue_head(&wq);
  • 1
  • 阻塞接口:
    都是些宏:
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)
  • 1

接口版本比较多,各自都有自己合适的应用场合,但是常用的是前面四个。
其中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;
  • 1

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)
  • 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)
  • 1

(2)动态:

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

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);
  • 1

(回调有什么作用?)
用下面函数将等待队列,加入到等待队列头(带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);
  • 1

上面的阻塞和解除阻塞接口,只能是对当前进程阻塞/解除阻塞,有了这几个灵活的接口,我们可以单独定义一个等待队列,只要获取进程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);
  • 1

就能将任意进程挂起,当然,还需要将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)
  • 1

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>
  • 1
  • 创建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)
  • 1

这两个宏都会返回一个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");
  • 1
  • 定义work:
    (1)静态(其实,将这个宏,放到局部变量里面,也是个动态的):
#define DECLARE_WORK(n, f) \struct work_struct n = __WORK_INITIALIZER(n, f)
  • 1

用法例子:

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

(2)动态定义:

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

用法例子:

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

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

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

如:

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

修改绑定的函数后,当下次调度到,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);
  • 1

两个函数的返回值:
返回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);
  • 1

注意:调用这个函数,必须确保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);
  • 1

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

销毁workqueue
销毁函数:

void destroy_workqueue(struct workqueue_struct *wq);
  • 1

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

void flush_workqueue(struct workqueue_struct *wq);
  • 1

总结:工作队列步骤,首先是创建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);
  • 1

例子:

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

取消还是用:

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);
  • 1

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

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

void flush_scheduled_work(void);
  • 1

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

Tasklet

  • 头文件:
#include <linux/interrupt.h>
  • 1
  • 定义和初始化:
    (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 }
  • 1

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);
  • 1

用法:

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

函数接口:

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);
  • 1

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驱动---等待队列、工作队列、Tasklets相关推荐

  1. linux wait函数头文件_手把手教Linux驱动9-等待队列waitq

    在上一篇<手把手教Linux驱动8-Linux IO模型>我们已经了解了阻塞.非阻塞.同步和异步等相关概念,本文主要讲解如何通过等待队列实现对进程的阻塞. 应用场景: 当进程要获取某些资源 ...

  2. linux驱动面试题整理

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

  3. Linux驱动笔试知识

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

  4. 华清远见嵌入式Linux驱动开发培训班

    课程背景 开放的 Linux 受到广泛的欢迎,得到越来越多公司的支持,但是阻碍 Linux 在各个领域广泛应用的主要因素就是内核/驱动高端人才极度缺乏,Linux源代码中85%是设备驱动,嵌入式系统中 ...

  5. 最全Linux驱动开发全流程详细解析(持续更新)

    Linux驱动开发详细解析 一.驱动概念 驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁. 具体任务 读写设备寄存器(实现控制的方式) 完成设备的轮询.中断处理.DMA通信(CPU与外设通信 ...

  6. Linux驱动修炼之道-SPI驱动框架源码分析(上)

    Linux驱动修炼之道-SPI驱动框架源码分析(上)   SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设 ...

  7. 嵌入式linux硬件成本,嵌入式Linux驱动和固件有何区别?供应商是如何用固件压缩成本的?...

    原标题:嵌入式Linux驱动和固件有何区别?供应商是如何用固件压缩成本的? 作为一个驱动开发者, 你可能发现你面对一个设备必须在它能支持工作前下载固件到它里面. 硬件市场的许多地方的竞争是如此得强烈, ...

  8. Linux设备驱动开发-linux驱动中的阻塞访问方式

    阻塞与非阻塞是设备访问的两种不同的模式.什么是阻塞操作呢?其是指在执行设备操作的时候,如果不能获得资源,则挂起进程直到满足可操作的条件后再进行操作.而非阻塞操作则是在进程不能进行设备操作时,并不挂起到 ...

  9. Linux驱动编程 step-by-step (八) 阻塞型字符设备驱动

    阻塞型字符设备驱动 前面说到了 如何实现read write 等操作,但如果设备缓冲已满,现在想而用户此时又想写入设备,此请求就无法立即执行,那怎么办呢? 第一种情况是:驱动程序想用户返回请求失败的信 ...

最新文章

  1. php 直接定义json,PHP json_dncode()函数定义与使用方法
  2. 云脑人力资源管理软件EHR选型手记(即时连载)
  3. c++中的数组和指针,引用
  4. java component创建_spring--打印hello--注解component--自动创建对象
  5. 多变异位自适应遗传算法(MMAdapGA)的算法原理、算法步骤和matlab实现
  6. python文件名带日期变量_Python实现文件按照日期命名的方法
  7. CentOS编译安装PHP 7.0
  8. 最值得程序员get的30本行业干货
  9. JavaScript学习总结(七)——JavaScript函数(function)
  10. mysql5.6.24怎么打开_mysql 5.6.24 安装配置方法图文教程
  11. Android高德地图自定义Markers的例子
  12. Nginx学习总结(3)——Nginx配置及应用场景之高级配置
  13. 带透明png转换成c数组
  14. Kinect 开发 —— 近距离探测
  15. Linux终端界面Screen实现桌面共享
  16. 图片识别出处_搜图神器!你还在问图片的出处吗?
  17. 月是故乡明,每逢佳节倍思亲,近乡情更怯
  18. 2005年9月10日。
  19. 计算机词汇app有哪些,APP推荐 | 有哪些APP独得学霸恩宠?
  20. 五大常用算法一(回溯,随机化,动态规划)

热门文章

  1. MySQL 的物理备份、逻辑备份、增量备份
  2. Lemon静态链接库
  3. [USACO 1.5] 跳棋的挑战
  4. 险绝冠天下---华山
  5. 前后端分离学习笔记(5) ---[表单的增删改操作;以及为管理员上传头像]
  6. 神奇!一篇不足700字的论文,竟然能发表在Science上
  7. web前端案例——携程网首页flex制作
  8. 智慧会议解决方案-最新全套文件
  9. 自动写文章生成器,为你一键生成原创文章!
  10. 想清楚这3个问题,更好实现公众号引流