本文是继task-queue的诞生的第二篇,上回说到工作队列的老祖先task-queue是怎么来的,它为什么而来?解决了什么样的问题,这里不再赘述。
首先taskqueue的出现解决了异步执行任务的问题,但是它执行时机是在内核的固定触发点,所以在使用过程中会有不少问题,继续探究。
还记得第一篇文章说的task-queue触发的4个场景吧,我们来一一看看这个老古董有什么问题。
1、tq_timer/tq_immediate队列
这2个队列是在中断上下文中运行的,tq_timer是基于时钟中断会定期触发,而tq_immediate在加入队列后需要手动mark bottom halt。但是执行时机都是中断底半步中,所以要求我们的工作任务不能有睡眠。这2个队列约束了我们的工作任务要尽快完成。
2、tq_scheduler队列
该队列是在schedule函数中触发执行的,这个做法很不明智,原因有2个:
(1)如果任务比较耗时,会导致进程调用schedule延迟,进程切换时间增加,实时性降低。
(2)如果任务带有睡眠,会导致schedule会反复递归调用,增加重入及递归风险,而且会引起内核bug。
3、tq_disk队列
虽然该队列可以在进程上下文中执行,但触发时机是极不确定的,若没有文件系统相关操作,可能任务就永远不会执行,或者延迟很久才会执行。
看来task-queue这老家伙缺陷还真不少。知道了问题的所在,解决也就不难了。很快,就有人提交了补丁代码,这次是红帽公司的Genesis,
他在内核里创建了一个后台进程,专门用于在进程上下文执行工作任务,同时新增了一个全新的tq_context队列,该进程负责处理tq_context队里上的任务,(詹姆斯笑到:老子早就想到了)
这样以来,工作队列已经满足了大部分使用的场景了,任何work都可以挂入工作队列执行了。自那以后,这个后台进程有了一个稳固的身份。
内核为它取了个名字,叫“keventd”,并持续了很多年。keventd活下了整个linux-2.4时代。
Genesis提交的代码是kernel/context.c,当时只有150行代码,但这已经实现了linux-workqueue的最基本思想,也就是将任务推迟到进程上下文中执行。
但那时还不叫workqueue,队列仍然以task-queue形式存在,但我们离工作队列越来越近了。
后来由于tq_scheduler队列存在严重缺陷,2.4内核已经将它彻底废弃掉。只保留tq_timer,tq_immediate,tq_context,tq_disk这几个全局队列。(以及一些其他自定义的tq)
随着版本不断演进,keventd也越来越强大,作为内核重要的基础设施,没日没夜的为大众服务着。
2002年内核大神Ingo Molnar,重构了taskqueue及keventd代码。正式更名为workqueue,同时将“keventd”后台进程更名为“events”。由此工作队列诞生了。
由于工作队列的频繁使用及CPU硬件不断发展,在SMP系统上,keventd单个的后台线程显得不够用,任务压力较大,为了work能及时响应,Ingo Molnar创建和CPU个数目等同的后台线程,加快了工作任务的处理,那个时候系统启动后会出现多个events后台进程,如:events/0、events/1、events/2、events/3。这些event其实就是现在内核中的kworker。
workqueue出现后,taskqueue、keventd彻底退出历史舞台。
早期workqueue引入的几个重要特性:
1、由于work是异步执行的,开发者想知道work是否已经执行完,内核增加了flush_task接口,用于等待work完成。
2、添加api,提供了work延迟执行机制,工作项可以推迟到固定时间执行。
3、2003年,出现了cpu hotplug技术,内核为workqueue增加了cpu上下线的处理。
4、由于workqueue存在多个后台,为提高性能,每个events/%u后台线程与cpu绑定。
5、添加api,可以向特定cpu投递任务并调度执行。
2.6.0内核工作队列api:
EXPORT_SYMBOL_GPL(__create_workqueue);EXPORT_SYMBOL_GPL(queue_work);EXPORT_SYMBOL_GPL(queue_delayed_work);EXPORT_SYMBOL_GPL(flush_workqueue);EXPORT_SYMBOL_GPL(destroy_workqueue);EXPORT_SYMBOL(schedule_work);EXPORT_SYMBOL(schedule_delayed_work);EXPORT_SYMBOL(schedule_delayed_work_on);EXPORT_SYMBOL(flush_scheduled_work);
__create_workqueue用于创建工作队列,当用户调用create_workqueue创建工作队列时,会自动创建后台线程,为该队列服务。
struct workqueue_struct *__create_workqueue(const char *name,int singlethread){int cpu, destroy = 0;struct workqueue_struct *wq;struct task_struct *p;BUG_ON(strlen(name) > 10);wq = kmalloc(sizeof(*wq), GFP_KERNEL);if (!wq)return NULL;memset(wq, 0, sizeof(*wq));wq->name = name;/* We don't need the distraction of CPUs appearing and vanishing. */lock_cpu_hotplug();if (singlethread) {INIT_LIST_HEAD(&wq->list);p = create_workqueue_thread(wq, 0);if (!p)destroy = 1;elsewake_up_process(p);} else {spin_lock(&workqueue_lock);list_add(&wq->list, &workqueues);spin_unlock(&workqueue_lock);for_each_online_cpu(cpu) {p = create_workqueue_thread(wq, cpu);if (p) {kthread_bind(p, cpu);wake_up_process(p);} elsedestroy = 1;}}unlock_cpu_hotplug();/** Was there any error during startup? If yes then clean up:*/if (destroy) {destroy_workqueue(wq);wq = NULL;}return wq;}
内核初始化时,会创建一个默认的工作队列,并生成events后台线程。
void init_workqueues(void){hotcpu_notifier(workqueue_cpu_callback, 0);keventd_wq = create_workqueue("events");BUG_ON(!keventd_wq);}
schedule_work接口用于调度一个工作项,被调度的work项将在events线程中运行,该接口在当前5.x内核仍在被使用。但代码体积已经膨胀了几十倍。
/* Preempt must be disabled. */static void __queue_work(struct cpu_workqueue_struct *cwq,struct work_struct *work){unsigned long flags;spin_lock_irqsave(&cwq->lock, flags);work->wq_data = cwq;list_add_tail(&work->entry, &cwq->worklist);cwq->insert_sequence++;wake_up(&cwq->more_work);spin_unlock_irqrestore(&cwq->lock, flags);}/** Queue work on a workqueue. Return non-zero if it was successfully* added.** We queue the work to the CPU it was submitted, but there is no* guarantee that it will be processed by that CPU.*/int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work){int ret = 0, cpu = get_cpu();if (!test_and_set_bit(0, &work->pending)) {if (unlikely(is_single_threaded(wq)))cpu = 0;BUG_ON(!list_empty(&work->entry));__queue_work(wq->cpu_wq + cpu, work);ret = 1;}put_cpu();return ret;}/* 默认的全局队列 */static struct workqueue_struct *keventd_wq;int fastcall schedule_work(struct work_struct *work){return queue_work(keventd_wq, work);}
workqueue初期的代码是非常简单的,感兴趣的同学可以自行研究,老衲就不多啰嗦了。截止到目前,你已经见到workqueue真身了,但他还非常年轻,还有很多不足之处,随着kernel的发展它会更加复杂,我们下章继续讨论。
总结:
工作队列从出现到演变成workqueue,主要经历了3个阶段。即早期的taskqueue、中期keventd后台、及后期的workqueue,最终以多个events后台任务的呈现。
值得注意的一点,工作队列不仅仅是一个队列,还要有他的运行载体,也就是events后台,随着kernel版本的升高,该后台任务会继续演进,越来越高大上,需要我们继续修炼。




