1. 基本介绍
1.1:work_queue(相关接口在linux/kernel/workqueue.c和 linux/include/linux/workqueue.h中)
内核驱动中,一般的小型任务(work)都不会自己起一个线程来处理,而是扔到workqueu中处理。workqueue的主要工作就是用进程上下文来处理内核中大量的小任务(本质也是调用了)。
工作队列是一种将工作推后执行的形式(中断下半部),它和tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。
那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet.
所以workqueue的主要设计思想:一个是并行,多个work不要相互阻塞;另外一个是节省资源,多个work尽量共享资源(进程、调度、内存),不要造成系统过多的资源浪费。workqueue有如下多种优先级,常用system_wq(工作者)即可,每个工作者即一个线程.
工作放入工作队列之后,由管理此工作队列的工作者来执行这些work,通过alloc_workqueue或create_singlethread_workqueue来创建工作者线程,它最后调用kthread_create创建线程,其线程名为alloc_workqueue中指定的name,system_wq的线程名字即为"events" 其举例如下:
static int __init init_workqueues(void)
{
unsigned int cpu;
int i;
...
system_wq = alloc_workqueue("events", 0, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_nrt_wq = alloc_workqueue("events_nrt", WQ_NON_REENTRANT, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",
WQ_FREEZABLE, 0);
BUG_ON(!system_wq || !system_long_wq || !system_nrt_wq ||
!system_unbound_wq || !system_freezable_wq);
return 0;
}
workqueue是对内核线程封装的用于处理各种工作项的一种处理方法, 由于处理对象是用链表拼接一个个工作项, 依次取出来处理, 然后从链表删除,就像一个队列排好队依次处理一样, 所以也称工作队列,所谓封装可以简单理解一个中转站, 一边指向“合适”的内核线程, 一边接受你丢过来的工作项, 用结构体 workqueue_srtuct表示, 而所谓工作项也是个结构体 -- work_struct, 里面有个成员指针, 指向你最终要实现的函数,
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
当然使用者在实现自己函数功能后可以直接调用,或者通过kthread_create()把函数当做新线程的主代码, 或者add_timer添加到一个定时器延时处理,
那为何要弄个work_struct工作项先封装函数, 然后再丢到workqueue_srtuct处理呢? 这就看使用场景了, 如果是一个大函数, 处理事项比较多, 且需要重复处理, 可以单独开辟一个内核线程处理; 对延时敏感的可以用定时器;
如果只是简单的一个函数功能, 且函数里面有延时动作的, 就适合放到工作队列来处理了, 毕竟定时器处理的函数是在中断上下文,不能delay或者引发进程切换的API, 而且开辟一个内核线程是耗时且耗费资源的, 一般用于函数需要while(1) 不断循环处理的,
不然处理一次函数后退出,线程又被销毁, 简直就是浪费!
将自己的工作项挂到已有的工作队列需要注意的是由于这些队列是共享的, 各个驱动都有可能将自己的工作项放到同个队列, 会导致队列的项拥挤, 当有些项写的代码耗时久或者调用delay()延时特别久, 你的项将会迟迟得不到执行!
所以早期很多驱动开发人员都是自己创建workqueue, 添加自己的work。 在Linux-2.XXX时代, 创建workqueue时会创建属于workqueue自己的内核线程, 这些线程是“私有的”, 虽然是方便了驱动开发人员, 但每个驱动都“一言不合”就
创建workqueue导致太多线程, 严重占用系统资源和效率, 所以在Linux-3.XXX时代, 社区开发人员将workqueue和内核线程剥离! 内核会自己事先创建相应数量的线程(后面详解), 被所有驱动共享使用。 用户调用alloc_workqueue();
1.2 :wait_queue
等待队列在linux内核中有着举足轻重的作用,很多linux驱动都或多或少涉及到了等待队列。因此,对于linux内核及驱动开发者来说,掌握等待队列是必须课之一。 Linux内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。它有两种数据结构:等待队列头(wait_queue_head_t)和等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个list_head类型的域作为”连接件”。它通过一个双链表和把等待task的头,和等待的进程列表链接起来,在内核里面,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。可以使用等待队列在实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等。
2. 使用示例
.h中:
struct work_struct retreive_frame_work;
driver init中:
INIT_WORK(&retreive_frame_work, retrieve_desc_task_callback);
init_waitqueue_head(&frame_wq);
中断ISR中:
{
queue_work(system_wq, &retreive_frame_work);
}
void retrieve_desc_task_callback(struct work_struct *work){
add_desc(iav, 0); //生产frame_desc
wake_up_interruptible_all(&frame_wq); //通知消费者,frame_desc已经生产完毕,可以去取了
}
消费由 retrieve_desc_task_callback 生产的 frame_desc(blocking 的方式)
wait_event_interruptible(frame_wq,
(desc = iav_find_frame_desc()));
参考链接如下:作者如需要删除请联系我,谢谢!
1. https://www.cnblogs.com/vedic/p/11069249.html
2. https://blog.csdn.net/morixinguan/article/details/69666642 ,包含诸如断tasklet, workqueue,softirq,中断下半部(bottom half)等概念