当前位置: 首页 > 科技观察

Linuxworkqueue工作队列是什么鬼?

时间:2023-03-12 23:59:49 科技观察

分类什么是工作队列驱动程序编译测试别人的经验,我们的阶梯!Linux中断处理可以总结为下图:图中描述了中断处理后半部分的机制,以及如何根据实际业务场景和约束进行选择。可以看出,这些不同的实现有的是重复的,有的是相互替代的。也正是因为如此,它们之间的使用方法几乎是一样的,至少在API接口函数的使用上,从使用的角度来说,都是非常相似的。在本文中,我们将通过实际的代码操作来演示如何使用workqueue。什么是工作队列?工作队列是Linux操作系统中处理中断后半部分的重要途径!从名字就可以猜到:工作队列就像是业务层常用的消息队列,里面有很多工作项等待处理。工作队列中有两个重要的结构体:工作队列(workqueue_struct)和工作项(work_struct):/*I:workqueuename*/.../*hotfieldsusedduringcommandissue,alignedtocacheline*/unsignedintflags____cacheline_aligned;/*WQ:WQ_*flags*/structpool_workqueue__percpu*cpu_pwqs;/*I:per-cpupwqs*/structpool_workqueue__rcu*numa_pwqs];/*q_tbl[PWR:unboundpwqsindexedbynode*/};structwork_struct{atomic_long_tdata;structlist_headentry;work_func_tfunc;//指向处理函数#ifdefCONFIG_LOCKDEPstructlockdep_maplockdep_map;#endif};在内核中,工作队列中的所有工作项通过链表串在一起,等待操作系统中的某个线程被一个一个取出来处理。这些线程可以是驱动程序通过kthread_create创建的线程,也可以是操作系统预先创建的线程。这里涉及一个权衡问题。如果我们的处理功能简单,那么就没有必要单独创建一个线程来进行处理。有两个原因:创建内核线程非常耗费资源。如果功能很简单,执行完不久就关闭线程是不值得的;如果每个驱动程序编写者都无节制地创建内核线程,那么内核中就会出现大量不必要的线程,当然这本质上是系统资源消耗和执行效率的问题;为了避免这种情况,操作系统预先为我们创建了一些工作队列和内核线程。我们只需要将需要处理的工作项直接添加到这些预先创建好的工作队列中,它们就会被相应的内核线程取出来进行处理。例如下面这些工作队列,就是内部默认创建的(include/linux/workqueue.h):/**System-wideworkqueueswhicharealwayspresent.**system_wqistheoneusedbyschedule[_delayed]_work[_on]().*Multi-CPUmulti-threaded.Thereareuserswhichexpectrelatively*shortqueueflushtime.Don'tqueueworkswhichcanrunfortoo*long.**system_highpri_wqissimilartosystem_wqbutforworkitemswhich*requireWQ_HIGHPRI.**system_long_wqissimilartosystem_wqbutmayhostlongrunning*works.Queueflushingmighttakerelativelylong.**system_unbound_wqisunboundworkqueue.Workersarenotboundto*anyspecificCPU,notconcurrencymanaged,andallqueuedworksare*executedimmediatelyaslongasmax_activelimitisnotreachedand*resourcesareavailable.**system_freezable_wqisequivalenttosystem_wqexceptthatit's*freezable.***_power_efficient_wqareinclinedtowardssavingpowerandconverted*intoWQ_UNBOUNDvariantsif'wq_power_efficient'已启用;否则,*它们是相同的非节能对应部件-例如*system_power_efficient_wqisidenticaltosystem_wqif*'wq_power_efficient'isdisabled.SeeWQ_POWER_EFFICIENTformoreinfo.*/externstructworkqueue_struct*system_wq;externstructworkqueue_struct*system_highpri_wq;externstructworkqueue_struct*system_long_wq;externstructworkqueue_struct*system_unbound_wq;externstructworkqueue_struct*system_freezable_wq;externstructworkqueue_struct*system_power_efficient_wq;externstructworkqueue_struct*system_freezable_power_efficient_wq;以上这些默认工作队列的创建代码是(kernel/workqueue.c):int__initworkqueue_init_early(void){...system_wq=alloc_workqueue("events",0,0);system_highpri_wq=alloc_workqueue("events_highpri",WQ_HIGHPRI,0);system_long_wq=alloc_workqueue(“events_long”,0,0);system_unbound_wq=alloc_workqueue(“events_unbound”,WQ_UNBOUND,WQ_UNBOUND_MAX_ACTIVE);system_freezable_wq=alloc_workqueue(“events_freezable”,WQ_FREEZABLE,0);system_power_efficient_wq=alloc_workqueue(“events_FIQENT,0POWER_efficient”);system_freezable_power_eefficient_wq=alloc_workqueue("events_freezable_power_efficient",WQ_FREEZABLE|WQ_POWER_EFFICIENT,0);...}另外,由于工作队列system_wq的使用频率很高,所以内核封装了一个简单的函数(schedule_work)供我们使用:/***schedule_work-putworktaskinglobalworkqueue*@work:jobtobedone**Returns%falseif@workwasalreadyonthekernel-globalworkqueueand*%trueotherwise.**Thisputsajobinthekernel-globalworkqueueifitwasnotalready*queuedandleavesitinthesamepositiononthekernel-global*workqueueotherwise.*/staticinlineboolschedule_work(structwork_struct*work){  returnqueue_work(system_wq,work);}当然,任何事情都有优点和缺点!由于内核默认创建的工作队列是所有驱动共享的,如果所有驱动都将等待处理的工作项委托给它们,会造成某个工作队列过度拥挤。根据先来先服务的原则,工作队列中后面加入的工作项可能无法保证及时性,因为前面工作项的处理函数执行时间太长。因此,这里存在系统平衡的问题。关于工作队列的基本知识点就介绍到这里,下面会进行实际操作验证。在驱动的前几篇文章中,在驱动中测试中断处理的操作过程都是一样的,这里不再重复操作过程。这里直接给出驱动的全图代码,然后查看dmesg的输出信息。创建驱动源文件和Makefile:$cdtmp/linux-4.15/drivers$mkdirmy_driver_interrupt_wq$touchmy_driver_interrupt_wq.c$touchMakefile示例代码整体测试场景:加载驱动模块后,如果检测到键盘上的ESC键被按下,则添加一个工作项到内核默认的工作队列system_wq,然后观察是否调用了该工作项对应的处理函数。#include#include#includestaticintirq;staticchar*devname;staticstructwork_structmywork;//接收参数module_param(irq,int,0644);module_param(devname,charp,0644);//定义驱动的ID,在中断处理函数中使用,判断是否需要处理#defineMY_DEV_ID1226//驱动数据结构structmyirq{intdevid;};structmyirqmydev={MY_DEV_ID};#defineKBD_DATA_REG0x60#defineKBD_STATUS_REG0x64#defineKBD_SCANCODE_MASK0x7f#defineKBD_STATUS_MASK0x80//工作项绑定的处理函数staticvoidmywork_handler(structwork_struct*work){printk("mywork_handleriscalled.\n");//dosomeotherthings}//中断处理函数staticirqreturn_tmyirq_handler(intirq,void*dev){structmyirqmydev;unsignedcharkey_code;mydev=*(structmyirq*)dev;//检查设备id,只有相等才需要处理if(MY_DEV_ID==mydev.devid){//读取键盘扫描码key_code=inb(KBD_DATA_REG);if(key_code==0x01){printk("ESCkeyispressed!\n");//初始化工作itemsINIT_WORK(&mywork,mywork_handler);//加入工作队列system_wqschedule_work(&mywork);}}returnIRQ_HANDLED;}//驱动模块初始化函数staticint__initmyirq_init(void){printk("myirq_initiscalled.\n");//注册中断处理程序if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0){printk("registerirq[%d]handlerfailed.\n",irq);return-1;}printk("registerirq[%d]handlersuccess.\n",irq);return0;}//驱动模块退出函数staticvoid__exitmyirq_exit(void){printk("myirq_exitiscalled.\n");//释放中断处理程序free_irq(irq,&mydev);}MODULE_LICENSE("GPL");module_init(myirq_init);module_exit(myirq_exit);Makefileifneq($(KERNELRELEASE),)obj-m:=my_driver_interrupt_wq.oelseKERNELDIR?=/lib/modules/$(shelluname-r)/buildPWD:=$(shellpwd)default:$(MAKE)-C$(KERNELDIR)M=$(PWD)modulesclean:$(MAKE)-C$(KERNEL_PATH)M=$(PWD)cleanendif编译测试$make$sudoinsmodmy_driver_interrupt_wq.koirq=1devname=mydev检查驱动模块是否加载成功:$lsmodgrepmy_driver_interrupt_wqmy_driver_interrupt_wq163840再看dmesg的输出:$dmesg...[638.247myirq_initiscalled.[188.247642]注册irq[1]处理程序成功。说明:myirq驱动初始化函数_init已被调用,1号中断的处理程序已成功注册。此时,按下键盘上的ESC键。操作系统捕获到键盘中断后,会依次调用这个中断的所有中断处理程序,包括我们注册的myirq_handler函数。在这个函数中,当判断按下ESC键时,会初始化一个工作项(绑定一个结构体work_struct类型的变量到一个处理函数),然后扔到预先创建好的工作队列(system_wq)中操作系统。处理,如下:if(key_code==0x01){printk("ESCkeyispressed!\n");INIT_WORK(&mywork,mywork_handler);schedule_work(&mywork);}因此,当相应的内核线程从这个工作队列(system_wq)取出要处理的工作项(mywork),将调用函数mywork_handler。现在查看dmesg的输出:[305.053155]ESCkeyispressed![305.053177]mywork_handleriscalled。可以看到mywork_handler函数被正确调用了。完美的!