熟悉又陌生的udelay

时间:2022-07-02 19:36:40
申请CSDN博客认证专家通过,着实让我受宠若惊,自己还是有这份自知之明,与专家 大牛这些词汇还是有很长距离。
不过认证通过给了自己一份动力,在博客上分享更多自己的所学,与大家学习交流。

内核开发中经常用到延时函数,最熟悉的是mdelay msleep。虽然经常会使用,但是具体实现却不了解,今天来研究下。

这2个函数在实现上有着天壤之别。


msleep实现是基于调度,延时期间调用schedule_timeout产生调度,待时间到期后继续运行,该函数实现在kernel/timer.c中。

由于linux内核不是实时系统,因此涉及调度的msleep肯定不会精确。

今天不细说msleep,有时间再来分析它,今天重点来学习mdelay。
mdelay是使用最多的延时函数。它的实现是忙循环,利用了内核loop_peer_jiffy,延时相对于msleep更加准确。

mdelay ndelay都是基于udelay来实现的。在include/linux/delay.h中,如下:

#ifndef MAX_UDELAY_MS#define MAX_UDELAY_MS   5
#endif

#ifndef mdelay
#define mdelay(n) (\
(__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : \
({unsigned long __ms=(n); while (__ms--) udelay(1000);}))
#endif

#ifndef ndelay
static inline void ndelay(unsigned long x)
{
udelay(DIV_ROUND_UP(x, 1000));
}
#define ndelay(x) ndelay(x)
#endif

#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

gcc的内建函数__builtin_constant_p用于判断n是否为编译时常数,如果n是常数,返回 1,否则返回 0。
mdelay实现,如果参数为常数,且小于5,则直接调用udelay,说明udelay最大支持5000us延时。否则则循环调用udelay达到延时目的。

ndelay实现可以看出非常不精确,经过计算调用udelay。因此ndelay最少也是延时1us。


所以接下来来看udelay实现。这里讨论基于ARM处理器架构的实现,udelay实现在arch/arm/include/asm/delay.h中。

#define MAX_UDELAY_MS 2#define udelay(n)                           \    (__builtin_constant_p(n) ?                  \      ((n) > (MAX_UDELAY_MS * 1000) ? __bad_udelay() :      \            __const_udelay((n) * ((2199023U*HZ)>>11))) :    \      __udelay(n))

最终会调用__const_udelay或者__udelay,2者实现在arch/arm/lib/delay.s中,如下:

.LC0:       .word   loops_per_jiffy.LC1:       .word   (2199023*HZ)>>11/* * r0  <= 2000 * lpj <= 0x01ffffff (max. 3355 bogomips) * HZ  <= 1000 */ENTRY(__udelay)        ldr r2, .LC1        mul r0, r2, r0ENTRY(__const_udelay)               @ 0 <= r0 <= 0x7fffff06        mov r1, #-1        ldr r2, .LC0        ldr r2, [r2]        @ max = 0x01ffffff        add r0, r0, r1, lsr #32-14        mov r0, r0, lsr #14     @ max = 0x0001ffff        add r2, r2, r1, lsr #32-10        mov r2, r2, lsr #10     @ max = 0x00007fff        mul r0, r2, r0      @ max = 2^32-1        add r0, r0, r1, lsr #32-6        movs    r0, r0, lsr #6        moveq   pc, lr上面这段汇编运算规则可以总结为下面这个计算公式,n为传入参数:loops = ( ( (n *((2199023*HZ)>>11)) >> 14 ) * (loops_per_jiffy >> 10) ) >> 6 /* * loops = r0 * HZ * loops_per_jiffy / 1000000 * * Oh, if only we had a cycle counter... */@ Delay routineENTRY(__delay)        subs    r0, r0, #1        bhi __delay        mov pc, lrENDPROC(__udelay)ENDPROC(__const_udelay)ENDPROC(__delay)


__udelay的实现利用了loop_per_jiffy,该变量是内核全局变量,在内核启动时调用calibrate_delay计算得出,表示处理器在一个jiffy中loop数。
calibrate-delay实现之前写过一篇文章来分析,链接如下:
http://blog.csdn.net/skyflying2012/article/details/16367983

loop_per_jiffy内核下转换为bogoMIPS反馈给用户,我们执行命令cat /proc/cpuinfo,可以看到bogoMIPS,表征处理器每秒执行百万指令数,是一个cpu性能测试数。

根据上面汇编实现可以看出,先计算出延时us所需的loop数,最后调用__delay循环递减完成延时,很明显,udelay实现最终就是一个处理器忙循环。

这里需要注意一个细节,calibrate_delay实现中也是通过调用__delay来实现,参数即为loops_per_jiffy。
loops_per_jiffy的单位即为__delay,也就是说一个loop就是一个__delay。
__delay实现就是将参数一直subs递减,反复跳转。
所以我的理解,一个loop就是一条arm递减指令+跳转指令。


但是对于__udelay实现最大的疑问在于有一个奇怪的数字(2199023*HZ)>>11是什么意思,并且汇编中实现的计算规则各种移位又是什么意思呢。

首先最常规的方式,借助loop_per_jiffy根据延时us计算loop数,计算公式应该是汇编注释中那样:
 loops = n * HZ * loops_per_jiffy / 1000000
 HZ表征内核每秒jiffy个数,则HZ*loops_per_jiffy/1000000代表了1us中的loop数。

查找各种资料找到原因,对于处理器这个公式有一个极大的缺陷,如果处理器没有浮点处理单元,即非浮点处理器(整型处理器),运行时,这个公式计算很容易变为0。
因为除数1000000极大,loops_per_jiffy * HZ / 1000000=0。无能你想要延迟多少微秒,总为0。
内核的解决方法是,除1000000变为乘1/1000000,为保持精度,1/1000000要先左移30位, 变为
(1/1000000)<<30  =  2^30 / 1000000 = 2199023U>>11


这就明白了(2199023*HZ)>>11来源啦。

汇编中出现的反复移位则是为了把2199023U>>11实现中向左移的30位移回来。考虑到溢出,所以分成了>>14 , >>10, >>6,最后等同于 >>30 。

到此处就彻底明白汇编实现的loops计算公式的巧妙之处了,也就明白了arm的udelay实现方法。

可以看出内核在处理大数据除法运算时不直接除,而是运用了移位运算,我理解原因可能有两点:
(1)如上面遇到的问题,精度问题,除数很大,计算结果可能出现0.
(2)之前驱动开发中遇到的一种情况,内核编译时编译器对于除法会替换为gcc.so库的数学运算函数__aeabi_ldivmod,但是内核编译不依赖任何库,所以会出现编译错误。倒是可以使用内核提供的do_div替换。


udelay分析就到这里,2点小启发:
(1)内核的delay函数实现的确就是个忙循环。不同于sleep函数。
(2)内核开发中使用除法运算时要考虑清楚哦。