高并发的中断下半部tasklet实例解析-阿里云开发者社区

开发者社区> AliDataOps> 正文

高并发的中断下半部tasklet实例解析

简介: 最近为了解决一个技术问题,需要用到内核里中断下半部的tasklet机制,使用过程遇到了非常有趣的问题。在解决问题过程中,也逐步加深了对tasklet机制的理解。本文把这些收获记录下来和大家一起分享,经3.10测试通过

最近为了解决一个技术问题,需要用到内核里中断下半部的tasklet机制,使用过程遇到了非常有趣的问题。在解决问题过程中,也逐步加深了对tasklet机制的理解。本文把这些收获记录下来和大家一起分享,经3.10测试通过

一、问题发生的场景

出于排查磁盘IO方面问题的原因,需要利用内核tracepoint技术详细地监测磁盘IO的明细行为信息,如下git为代码示例。

image.png

其中关键部分代码摘要如下。

image.png

这里不必对tracepoint机制进行深究,只需要了解blk_add_trace_rq_insert1回调函数对应于linux内核函数中的block_rq_insert静态探针点,block_rq_insert探针点在__elv_add_request内核函数的开头处打点(trace_block_rq_insert处),如下代码所示。每一次__elv_add_request函数的调用,都有一次blk_add_trace_rq_insert1回调函数与之对应执行。

image.png

代码运行后会出现如下的效果,可以看到我们成功的获取了每一次IO请求的SIZE大小,并且还获取了对应的bio结构体指针之一。

image.png

类似中断下半部的tasklet机制对中断处理函数的延迟处理,下半部tasklet也可以应用到tracepoint回调函数上,从而提升回调函数blk_add_trace_rq_insert1的并发处理能力。

为了本文中意思表达更加准确,下文对tracepoint回调函数约定称为上半部处理函数,对tasklet处理函数约定称为下半部处理函数。

二、丢失的tasklet下半部

初学tasklet时,对它的理解并不深入。查阅国内外各种kernel的经典教材中的中断下半部tasklet部分内容,在介绍使用tasklet时,都需要静态或动态创建一个全局tasklet全局变量。其中静态创建方法是使用DECLARE_TASKLET宏的方法,动态创建tasklet方法见如下代码。

image.png

照葫芦画瓢,初步实现了如下代码的tasklet代码。

image.png

其中关键部分代码摘要如下:

image.png

接下来执行以上代码,查看运行效果。

image.png

满怀希望的运行,却发现了一个令人意想不到的结果,tasklet的下半部处理函数调用次数远小于上半部处理函数的调用次数。

三、丢失tasklet的原因

针对这个部分下半部tasklet丢失的问题,再次查阅kernel的经典教材,在《Linux Kernel Development 3rd Edition》的8.3.2小节中发现了Robert Love写下的这么一段话。

After a tasklet is scheduled, it runs once at some time in the near future. If the same tasklet is scheduled again, before it has had a chance to run, it still runs only once.

那么问题关键就聚焦在什么是“相同的tasklet(the same tasklet)”。为了搞清楚这个问题,我们来分析一下tasklet_schedule()函数的源码。

image.png

函数中关键部分是 if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) 这一行代码。设置tasklet类型的结构体对象t的state状态属性的TASKLET_STATE_SCHED位为1,同时返回tasklet类型的结构体对象t的state状态属性的TASKLET_STATE_SCHED位的原值。如果这个原值是1,就说明这个tasklet类型的结构体对象已经被调度到另一个CPU上去等待执行了。考虑到一个tasklet结构体对象同一时刻只能由一个CPU来执行,因此tasklet_schedule()不做任何操作,就直接返回了。反之,如果原值是0,那么就会执行__tasklet_schedule()调度函数。

顺藤摸瓜,我们很容易找到tasklet执行完毕后,清除TASKLET_STATE_SCHED位值的地方,如下代码。

image.png

总结一下,通过分析tasklet_schedule()函数的源码可知,一个tasklet就是指一个tasklet_struct结构体的指针对象。

在本例的整个代码中就声明了一个my_tasklet结构体指针对象,因此只有一个tasklet。所有的上半部处理函数和下半部处理函数的联系都是通过一个相同的tasklet。当前一次tasklet的下半部处理函数没有得到及时执行时,后一次上半部处理函数中再次执行tasklet_schedule进行调度时,很容易被内核丢弃。

四、高并发的下半部tasklet

明确了导致问题的原因,下面还要找到解决问题的方法。

既然在整个代码中只申请一个全局的tasklet结构体指针对象会导致下半部丢失的问题,那么我们我们可以考虑在上半部处理函数中每次都单独申请一个tasklet结构体指针对象。同时需要在tasklet下半部处理函数中及时释放tasklet结构体指针对象。为了下半部处理函数中及时释放指针对象,还需要把上半部处理函数中声明的tasklet结构体指针对象传递给下半部处理函数。同时也要把上半部处理函数中获取的内核blk层request结构体相关的信息传递给下半部处理函数,便于在下半部处理函数中提取相关IO信息。

非常幸运的是tasklet给我们提供了这样一个传参的方法,tasklet_init函数的第三个参数unsigned long data可以帮助我们实现传参的目标。

image.png

在64位操作系统中,unsigned long数据类型可以用来存储指针数据类型的指针值本身,如下实例所示,在64位系统中无符号长整型和指针类型都占用8字节。

image.png

通过以上的初步分析,最终我们实现如下代码。

image.png

其中关键部分代码摘要如下。

image.png
image.png

接下来执行以上代码,查看运行效果。

image.png

再次满怀希望的运行,这次tasklet的下半部处理函数调用次数等于上半部处理函数的调用次数,完全符合预期。

下面对代码进行一些重点分析:

定义一个iodump_struct结构体,其中包含struct tasklet_struct *tasklet结构体成员和其他一些结构体成员。tasklet结构体指针成员用于在上半部和下半部间传递tasklet结构体指针对象。

在上半部处理函数中声明和初始化tasklet_struct和iodump_struct类型的结构体指针对象。

使用tasklet_init函数的第三个参数,将iodump_struct类型结构体指针对象传递给下半部处理函数。

在下半部处理函数中解析出各个参数,包括tasklet_struct类型的结构体指针对象。

完成下半部处理函数中的任务后,分别释放iodump_struct类型和tasklet_struct类型的结构体指针对象。

五、内核中的常见实现

至此问题已经顺利解决,但实现方案是否完美,还需要做一些思考。经验丰富的同学都知道linux内核代码有2000多万行,其中很多模块的代码实现都十分经典,是一部编程的百科全书。

按照这样的思路,我们不难从内核usb驱动部分找到一段中断下半部tasklet的经典使用场景。

image.png

image.png

从usbatm的代码实例中,我们可以了解到tasklet也是使用了tasklet_init的第三个参数实现了中断上半部和下半部之间的参数传递。充分体现了tasklet_init函数第三个参数unsigned long data的强大作用。

细心的读者可能会发现,我们的concurrent_tasklet.git实例尽管支持了高并发的tasklet,但是也存在一些不足。由于每次上半部都会申请内存,而下半部会释放内存。这样频繁申请和释放内存,也会存在一定的性能开销。而内核驱动usbatm部分就相对较好的解决了这个内存频繁申请和释放的问题。如果你的项目需要追求更加极致的并发性能,可以参考usbatm部分的代码实例。

另一方面,内核中有类似传参场景的地方还有很多,不过大都是通过void *类型指针参数实现的,如下2处即是。

image.png

通过tasklet_init函数第三个参数unsigned long data的例子,告诉我们unsigned long类型的传参,也可以实现void *类型传参的作用。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
+ 订阅

官方博客
官网链接