面试珠玑 tasklet和工作队列 区别

tasklet tasklet  vs  内核定时器 相同:始终在中断期间运行,始终会在调度他们的同一CPU上运行,而且都接收一个unsigned long参数不同:不可以要求tasklet在某一给定的时间执行

tasklet对中断处理例程来说尤其有用。中断处理例程必须尽可能快的管理硬件中断,而大部分数据管理则可以安全的延迟到其后的时间。


实际上,与内核定时器类似,tasklet也会在“软件中断”上下文以原子模式执行。软件中断指打开硬件中断的同时执行某些异步任务的内核机制。

tasklet以数据结构形式存在,并在使用前必须初始化。调用特定的函数或者使用特定的宏来声明该结构,即可完成tasklet的初始化:

#include<linux/interrupt.h>

struct tasklet_struct{

         /*......*/

         void(*func)(unsigned long);

         unsigned long data;

};

void tasklet_init(struct tasklet_struct *t,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 }

void tasklet_disable(struct tasklet_struct *t); 
/*函数暂时禁止给定的 tasklet被 tasklet_schedule 调度,直到这个 tasklet 被再次被enable;若这个 tasklet 当前在运行, 这个函数忙等待直到这个tasklet退出*/


void tasklet_disable_nosync(struct tasklet_struct *t); 
/*和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU */


void tasklet_enable(struct tasklet_struct *t); 
/*使能一个之前被disable的 tasklet;若这个 tasklet 已经被调度, 它会很快运行。 tasklet_enable 和tasklet_disable必须匹配调用, 因为内核跟踪每个 tasklet 的"禁止次数"*/


void tasklet_schedule(struct tasklet_struct *t); 
/*调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己*/


void tasklet_hi_schedule(struct tasklet_struct *t); 
/*和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期*/


void tasklet_kill(struct tasklet_struct *t); 
/*确 保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync*/

 

tasklet特性:

1.一个tasklet可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet才会被执行。

2.和定时器类似,tasklet可以自己注册自己。

3.tasklet可被调度以在通常的优先级或者高优先级执行。高优先级的tasklet总会优先执行。

4.如果系统负荷不重,则tasklet会立即执行,但始终不会晚于下一个定时器滴答

5.一个tasklet可以和其它tasklet并发,但对自身来讲是严格串行处理的,也就是说,同一tasklet永远不会在多个处理器上同时运行:tasklet始终会调度自己在同一CPU上运行;

工作队列

表面来看,工作队列类似于tasklet:允许内核代码请求某个函数在将来的时间被调用。

但其实还是有很多不同:

1.tasklet在软中断上下文中运行,因此,所有的tasklet代码都是原子的。相反,工作队列函数在一个特殊的内核进程上下文中运行,因此他们有更好的灵活性

尤其是,工作队列可以休眠!

2.tasklet始终运行在被初始提交的统一处理器上,但这只是工作队列的默认方式

3.内核代码可以请求工作队列函数的执行延迟给定的时间间隔

4.tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。

 

两者的关键区别:tasklet会在很短的时间内很快执行,并且以原子模式执行,而工作队列函数可以具有更长的延迟并且不必原子化。两种机制有各自适合的情形。

 

工作队列有 struct workqueue_struct 类型,在 <linux/workqueue.h> 中定义。一个工作队列必须明确的在使用前创建,宏为:

struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);

 

每个工作队列有一个或多个专用的进程("内核线程"), 这些进程运行提交给这个队列的函数。 若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。

提交一个任务给一个工作队列,在这里《LDD3》介绍的内核2.6.10和我用的新内核2.6.22.2已经有不同了,老接口已经不能用了,编译会出错。这里我只讲2.6.22.2的新接口,至于老的接口我想今后内核不会再有了。从这一点我们可以看出内核发展。

 

/*需要填充work_struct或delayed_work结构,可以在编译时完成, 宏如下: */

struct work_struct {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0        /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
    struct list_head entry;
    work_func_t func;
};

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;
};

DECLARE_WORK(n, f)    
/*n 是声明的work_struct结构名称, f是要从工作队列被调用的函数*/
DECLARE_DELAYED_WORK(n, f)
/*n是声明的delayed_work结构名称, f是要从工作队列被调用的函数*/

/*若在运行时需要建立 work_struct 或 delayed_work结构, 使用下面 2 个宏定义:*/
INIT_WORK(struct work_struct *work, void (*function)(void *)); 
PREPARE_WORK(struct work_struct *work, void (*function)(void *)); 
INIT_DELAYED_WORK(struct delayed_work *work, void (*function)(void *)); 
PREPARE_DELAYED_WORK(struct delayed_work *work, void (*function)(void *));
 
/* INIT_* 做更加全面的初始化结构的工作,在第一次建立结构时使用. PREPARE_* 做几乎同样的工作, 但是它不初始化用来连接 work_struct或delayed_work 结构到工作队列的指针。如果这个结构已经被提交给一个工作队列, 且只需要修改该结构,则使用 PREPARE_* 而不是 INIT_* */

/*有 2 个函数来提交工作给一个工作队列:*/
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay);

/* 每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队列; 若为0,则意味着这个 work 已经在队列中等待,不能再次加入*/

在将来的某个时间, 这个工作函数将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠 (你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:

int cancel_delayed_work(struct delayed_work *work); 
void cancel_work_sync(struct work_struct *work)

如果这个入口在它开始执行前被取消,则返回非零。内核保证给定入口的执行不会在调用 cancel_delay_work 后被初始化. 如果 cancel_delay_work 返回 0, 但是, 这个入口可能已经运行在一个不同的处理器, 并且可能仍然在调用 cancel_delayed_work 后在运行. 要绝对确保工作函数没有在 cancel_delayed_work 返回 0 后在任何地方运行, 你必须跟随这个调用来调用:

void flush_workqueue(struct workqueue_struct *queue);

在 flush_workqueue 返回后, 没有在这个调用前提交的函数在系统中任何地方运行。
而cancel_work_sync会取消相应的work,但是如果这个work已经在运行那么cancel_work_sync会阻塞,直到work完成并取消相应的work。

当用完一个工作队列,可以去掉它,使用:

void destroy_workqueue(struct workqueue_struct *queue);

共享队列

在许多情况下, 设备驱动不需要它自己的工作队列。如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的默认的队列可能更有效。若使用共享队列,就必须明白将和其他人共享它,这意味着不应当长时间独占队列(不能长时间睡眠), 并且可能要更长时间才能获得处理器。

使用的顺序:
(1) 建立 work_struct 或  delayed_work

static struct work_struct jiq_work;
static struct delayed_work jiq_work_delay;

    /* this line is in jiq_init() */
INIT_WORK(&jiq_work, jiq_print_wq);
INIT_DELAYED_WORK(&jiq_work_delay, jiq_print_wq);

(2)提交工作

int schedule_work(&jiq_work);/*对于work_struct结构*/
int schedule_delayed_work(&jiq_work_delay, delay);/*对于delayed_work结构*/

/*返回值的定义和 queue_work 一样*/

若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:

void flush_scheduled_work(void);

因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。


你可能感兴趣的:(面试珠玑 tasklet和工作队列 区别)