工作队列(work_queue)
工作队列是linux 内核中一种不同于软中断机制及其衍生类的最简单、最常用的一种延迟机制。linux内核提供的工作延迟机制有:
●SoftIrq:执行在原子上下文
●Tasklet:执行在原子上下文
●工作队列:执行在进程上下文
可重入:如果代码执行期间随处可以中断,并且之后能够被再次安全地调用,就称为可再入
SoftIrq仅用于快速处理,直接使用的话需要考虑并发和可重入的问题,只有在网络和块设备子系统需要使用SoftIrq,其他情况下都可以使用Tasklet来代替SoftIrq。在绝大多数情况下,SoftIrq在硬件中断中被调用,这些硬件中断发生非常快,快过对他们的服务速度,因此内核有必要对他们进行排队,方便匹配对他们的服务速度。排队的中断会交给KsoftIrqd负责后期执行,Ksoftirqd是单CPU内核守护线程,用于处理未服务的软件中断。此外,SoftIrq是编译时静态分配的,不像Tasklet可以动态地进行分配和删除。
Tasklet是SoftIrq衍生的一种工作延迟机制,本质是不可重入的。相同的tasklet只能被一个cpu(调度它的cpu)执行,不同的Tasklet可以运行在不同的cpu上。也就是说Tasklet只能被串行执行,而SoftIrq可以被并发执行,因此Tasklet性能不如SoftIrq,但是使用比较简单。
工作队列与前面两种不同,他只能运行在进程上下文中,如果需要在中断的下半部睡眠,工作队列是惟一的选择,这里的睡眠指的是处理I/O数据、持有互斥锁延迟,以及可能导致睡眠或将任务移除运行队列的所有其他任务。工作队列又分为共享共享工作队列和专用工作队列。
工作队列中把需要推后执行的任务叫做work,使用work_struct来描述。
关于工作队列的类型选择?
除非别无选择,或者需要关键性能,又或者需要控制从工作队列初始化到每个工作队列调度的细节,如果只是偶尔提交任务,就应当使用共享工作队列。这种队列为整个系统共享,可以独占单不应该长时间独占该队列。
由于工作队列上挂起的任务在每个cpu上都是串行执行的,因此不应该任务不应该长时间睡眠,因为它长时间睡眠会导致其他任务无法被调度运行。共享工作队列中的任务由每个cpu上内核创建的event/n线程执行。工作队列机制不允许同一个工作多次加入到同一个队列或者多个队列中,只有当work正在执行或者已经执行完毕,才允许再次入队。所以不存在多cpu并发执行的问题。
共享队列必须使用INIT_WORK宏进行初始化
这个宏的作用是将自定义的数据结构中的struct work_struct初始化为我们想要添加的任务func。
下面四个函数可以调度共享工作队列上的工作,它们只是负责将work提交给cpu调度,什么时候执行需要看cpu是否繁忙。
上面所说的四个函数都是将工作添加到系统共享队列system_wq。已经提交到共享队列的工作可以通过函数cancel_delayed_work取消,然后再调用下面的函数刷新共享队列,避免出现混乱。
void flush_schedule_work(void);
整个共享队列被整个系统共享,因此在flush_shcedule_work返回前,不可能知道它会持续多久。
这里段代码:
这里面涉及到了等待队列和工作队列,其中INIT_WORK宏将work与回调函数work_handler绑定,schdule_work负责将work加入到共享工作队列,就是说当work被cpu的event线程执行时,执行的就是函数work_handler。
而在这个程序中,还调用了wait_event_interruptible将模块阻塞放进等待队列中,进程在wait_event_interruptible调用出停止并睡眠。直到被放进工作队列的work对应的回调函数被执行(work_handler),修改阻塞任务的唤醒条件,然后唤醒任务,将其状态设置为TASK_RUNNING,放入工作队列执行。
专用工作队列
这里,工作队列使用struct workqueue_struct的实例代表,在工作队列中排队的工作使用struct work_struct 的实例进行表示。在内核线程中调度工作工作前需要执行以下四步:
(1)声明初始化 struct workqueue_struct
(2)创建工作函数(内核线程执行的回调函数)
(3)创建struct work_struct,这样就可以把工作函数嵌入到work_struct中。
(4)把工作函数嵌入work_struct中。
相关的数据结构和函数定义在include/linux/workqueue.h中。
●声明工作和工作队列
struct workqueue_struct *myqueue;
struct work_struct thework;
●定义工作函数(处理程序)
void dowork(void *data);
●初始化工作队列,把工作嵌入到工作队列中
myqueue = create_signlethread_workqueue("mywork");
INIT_WORK(&thework,dowork,<data-pointer>);
使用create_workqueue和create_signlethread_workqueue两个函数都可以用来创建工作队列,只不过前者创建的工作队列会在每个可用的处理器上创建单独的内核线程。
●调度工作
queue_work(myqueue,&thework);
延迟指定时间后到指定工作现场排队
queue_delayed_work(myqueue,&thework,<delay>);
如果工作已经在队列内,会返回false,否则会返回true。入队前等待的jiffy数,可以使用辅助函数msecs_to_jiffies把标准的ms延迟转换为jiffy。例如要让工作在5ms后入队,则可以使用queue_delayed_work(myqueue,&thework,msecs_to_jiffies(5));
●等待指定队列上所有的工作都执行完毕
void flush_workqueue(struct worqueue_struct *wq);
通常使用这个函数关闭处理程序。
●清理
使用cancel_work_sync或者cancel_delayed_work_sync同步取消,他们将会取消还没有运行的工作,或者阻塞知道工作执行完成,即使工作重新入队,也会被取消。
linux v4.8以后的内核,增加了异步取消的方法。cancel_work和cancel_delayed_work实现异步取消,必须检查函数返回值是否为ture,确保工作自身没有再次入队。之后必须调用flush_workqueue显式刷新队列。
queue_work_on(int cpu,struct workqueue_struct *wq,struct work_struct work);
调度work,并指定运行的cpu
总结:在linux系统上,运行任何中断处理程序时,都会在所有处理器上禁用当前对应的中断线,有时甚至需要在实际运行中断处理程序的cpu上禁止所有中断。 但绝不希望错过任何中断,所以引入了半部的概念,将中断处理函数分为两个部分。
首先上半部,他使用request_irq申请中断资源,最终将根据需要屏蔽或者隐藏中断,执行快速操作,调度第二部分和下一部分,然后确认中短线,所有被禁用的中断都必须在退出下半部之前恢复启用。
下半部会处理一些比较耗时的任务,在他执行期间,中断会再次启用,这样就不会错过中断。下半部使用了工作延迟机制。SoftIrq\Tasklet\工作队列\线程Irq。