申请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, r0
ENTRY(__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 routine
ENTRY(__delay)
subs r0, r0, #1
bhi __delay
mov pc, lr
ENDPROC(__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)内核开发中使用除法运算时要考虑清楚哦。