如何使用Linux工作队列workqueue

如何使用Linux工作队列workqueue,第1张

创建一个per-CPU *编译期间静态创建一个per-CPU DEFINE_PER_CPU(type, name) 创建一个名为name,数据类型为type的per-CPU,比如static DEFINE_PER_CPU(struct sk_buff_head, bs_cpu_queues),此时每个CPU都有一个名叫bs_cpu_queues,数据结构为sk_buff_head的变量副本。每个副本都是在自己的CPU上工作。 * 动态创建per-CPU,以下代码是内核create_workqueue实现的片断 struct workqueue_struct *__create_workqueue(const char *name, int singlethread) { int cpu, destroy = 0struct workqueue_struct *wqstruct task_struct *pwq = kzalloc(sizeof(*wq), GFP_KERNEL)if (!wq) return NULLwq->cpu_wq = alloc_percpu(struct cpu_workqueue_struct)if (!wq->cpu_wq) { kfree(wq)return NULL} …… }创建一个名为name,数据类型为type的per-CPU,比如static DEFINE_PER_CPU(struct sk_buff_head, bs_cpu_queues),此时每个CPU都有一个名叫bs_cpu_queues,数据结构为sk_buff_head的变量副本。每个副本都是在自己的CPU上工作。Linux 2.6内核使用了不少工作队列来处理任务,他在使用上和 tasklet最大的不同是工作队列的函数可以使用休眠,而tasklet的函数是不允许使用休眠的。工作队列的使用又分两种情况,一种是利用系统共享的工作队列来添加自己的工作,这种情况处理函数不能消耗太多时间,这样会影响共享队列中其他任务的处理另外一种是创建自己的工作队列并添加工作。第二步:创建一个工作结构体变量,并将处理函数和参数的入口地址赋给这个工作结构体变量如果不想要在编译时就用DECLARE_WORK()创建并初始化工作结构体变量,也可以在程序运行时再用INIT_WORK()创建struct work_struct my_work//创建一个名为my_work的结构体变量,创建后才能使用INIT_WORK()INIT_WORK(&my_work,my_func,&data)//初始化已经创建的my_work,其实就是往这个结构体变量中添加处理函数的入口地址和data的地址,通常在驱动的open函数中完成INIT_WORK(&my_work, my_func, &data)//创建一个工作结构体变量并初始化,和第一种情况的方法一样//作用与schedule_work()类似,不同的是将工作添加入p_queue指针指向的工作队列而不是系统共享的工作队列work queue是一种bottom half,中断处理的后半程,强调的是动态的概念,即work是重点,而queue是其次。wait queue是一种「任务队列」,可以把一些进程放在上面睡眠等待某个事件,强调静态多一些,重点在queue上,即它就是一个queue,这个queue如何调度,什么时候调度并不重要等待队列在内核中有很多用途,尤其适合用于中断处理,进程同步及定时。这里只说,进程经常必须等待某些事件的发生。例如,等待一个磁盘 *** 作的终止,等待释放系统资源,或者等待时间经过固定的间隔。等待队列实现了在事件上的条件等待,希望等待特定事件的进程把放进合适的等待队列,并放弃控制权。因此。等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒进程。等待队列由循环链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头,等待队列头是一个类型为wait_queue_head_t的数据结构。等待队列链表的每个元素代表一个睡眠进程,该进程等待某一事件的发生,描述符地址存放在task字段中。然而,要唤醒等待队列中所有的进程有时并不方便。例如,如果两个或多个进程在等待互斥访问某一个要释放的资源,仅唤醒等待队列中一个才有意义。这个进程占有资源,而其他进程继续睡眠可以用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新的等待队列,该宏静态地声明和初始化名为name的等待队列头变量。 init_waitqueue_head()函数用于初始化已动态分配的wait queue head变量等待队列可以通过DECLARE_WAITQUEUE()静态创建,也可以用init_waitqueue_head()动态创建。进程放入等待队列并设置成不可执行状态。工作队列,workqueue,它允许内核代码来请求在将来某个时间调用一个函数。用来处理不是很紧急事件的回调方式处理方法.工作队列的作用就是把工作推后,交由一个内核线程去执行,更直接的说就是写了一个函数,而现在不想马上执行它,需要在将来某个时刻去执行,那就得用工作队列准没错。如果需要用一个可以重新调度的实体来执行下半部处理,也应该使用工作队列。是唯一能在进程上下文运行的下半部实现的机制。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O *** 作时,都会非常有用。

工作队列中是即将要调度到的任务队列,等待队列是暂时被挂起的任务队列,或者有些任务无事可做休眠状态的任务,它们会在某些条件触发时恢复换入工作队列并进入执行状态,同样在工作队列中的任务在某个时刻也可以被换入到等待队列中

写程序的时候,我们常常说某个系统调用是阻塞调用。从用户层的角度,基本理解是:进程在执行某个系统调用的时候,因为需要的资源不满足(IO *** 作,加锁等等),导致进程“停”在那里。等到资源就绪了或者设置的timeout时间超时了,进程得以继续执行。

从内核的角度,面对用户层对阻塞调用的需求,需要实现哪些机制呢?

1.首先,进程陷入内核,内核发现进程所要求的资源暂时无法满足,需要将其设置为睡眠状态,然后调度其他进程执行。这里引出一个问题:(1)内核如何将一个进程睡眠?

2.再来,等待资源就绪时,我们需要唤醒等待在该资源上面的进程。这里存在两个问题:(2)内核是怎么知道资源就绪的?以及,(3)某个资源就绪了,内核怎么找到对应等待的进程的?

问题一:内核如何将一个进程睡眠

进程的task_struct结构有一个状态成员。将其设置为“睡眠”,并将task_struct结构从就绪队列中移走,内核就不会调度其执行,也就相当于睡眠。

问题二:内核怎么知道资源就绪的

中断。 内核的所有的工作都是由中断驱动的。 不管是系统调用陷入内核,还是调度,还是其他的内核活动,都是由各种各样的中断来触发执行的。对于设备IO,如果设备空闲了,会触发一个外部中断,该中断触发内核执行处理程序,通知等待进程、执行回调等等。

问题三:资源就绪了,内核怎么找到对应等待的进程

答案是等待队列。我们将一个资源和一个等待队列关联起来。如果进程所请求的资源还未就绪,就先加入到该资源的等待队列中。等到资源就绪了,就唤醒等待队列中的进程,加入到调度。

等待队列就是一个普通的双向链表,该链表的每个节点都代表一个进程task_struct的封装。每个资源都会有相应的等待队列。

“惊群”的基本行为是:有多个进程或者线程等待在同一个资源上,而且该资源一次只能有一个进程处理,比如文件描述符的写 *** 作,accept一个新连接等。那么在资源就绪的时候。如果内核采取的策略是唤醒所有的进程。这样,只有一个进程获取了该资源,其他进程发现没有资源就绪,继续进入睡眠(所谓虚假唤醒)。这样的行为浪费了系统的CPU资源。

那是不是,内核在资源就绪的时候,就唤醒一个进程不就得了。其实也不是,因为不是所有资源都是互斥的。比如,某个文件的读 *** 作。

那么,惊群问题怎么解决?

在用户态,可以有不同的解决方式。或者忽略惊群所带来的开销,或者使用锁方式保证一次只有一个进程来阻塞在一个资源上。而对于内核来说,在等待队列上增加了一个是否“互斥等待”的标志。

如果是互斥等待的,一次唤醒一个进程。如果不是互斥等待的,一次唤醒所有进程。

互斥等待的经典例子:accept。因为我们很明确知道,对一个listen fd的accept,肯定是一次只有一个进程可以处理。那么,我们在listen fd上的等待队列,就毫无疑问可以设置为“互斥等待”。所以,现今版本的linux内核,解决了accept的惊群问题。

但是像epoll_wait的惊群问题,就无法从等待队列的互斥等待来解决。首先,epoll fd上也有一个等待队列,代表epoll fd所监听的其他若干文件描述符(资源)就绪时,唤醒等待队列上的资源。因为我们无法确定,这些资源是不是都是互斥访问的,还是都不是。所以,只好唤醒所有进程。更多的惊群问题,可以查阅相关资料。

https://shunlqing.github.io/2018/05/19/2018_5_19LinuxKernel_WaitQueue/


欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/yw/7478966.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-06
下一篇 2023-04-06

发表评论

登录后才能评论

评论列表(0条)

保存