linux 内核udelay详解

文章转发自:https://blog.csdn.net/skyflying2012/article/details/46763779

内核开发中经常用到延时函数,最熟悉的是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中,如下:

 

 
  1. #ifndef MAX_UDELAY_MS

  2. #define MAX_UDELAY_MS 5

  3. #endif

  4.  
  5. #ifndef mdelay

  6. #define mdelay(n) (\

  7. (__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : \

  8. ({unsigned long __ms=(n); while (__ms--) udelay(1000);}))

  9. #endif

  10.  
  11. #ifndef ndelay

  12. static inline void ndelay(unsigned long x)

  13. {

  14. udelay(DIV_ROUND_UP(x, 1000));

  15. }

  16. #define ndelay(x) ndelay(x)

  17. #endif

  18.  
  19. #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中。

 

 
  1. #define MAX_UDELAY_MS 2

  2.  
  3. #define udelay(n) \

  4. (__builtin_constant_p(n) ? \

  5. ((n) > (MAX_UDELAY_MS * 1000) ? __bad_udelay() : \

  6. __const_udelay((n) * ((2199023U*HZ)>>11))) : \

  7. __udelay(n))


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

 

 

 
  1. .LC0:       .word   loops_per_jiffy

  2. .LC1:       .word   (2199023*HZ)>>11

  3.  
  4.  
  5. /*

  6.  * r0  <= 2000

  7.  * lpj <= 0x01ffffff (max. 3355 bogomips)

  8.  * HZ  <= 1000

  9.  */

  10.  
  11.  
  12. ENTRY(__udelay)

  13.         ldr r2, .LC1

  14.         mul r0, r2, r0

  15. ENTRY(__const_udelay)               @ 0 <= r0 <= 0x7fffff06

  16.         mov r1, #-1

  17.         ldr r2, .LC0

  18.         ldr r2, [r2]        @ max = 0x01ffffff

  19.         add r0, r0, r1, lsr #32-14

  20.         mov r0, r0, lsr #14     @ max = 0x0001ffff

  21.         add r2, r2, r1, lsr #32-10

  22.         mov r2, r2, lsr #10     @ max = 0x00007fff

  23.         mul r0, r2, r0      @ max = 2^32-1

  24.         add r0, r0, r1, lsr #32-6

  25.         movs    r0, r0, lsr #6

  26.         moveq   pc, lr

  27.  
  28.  
  29.  
  30.  
  31. 上面这段汇编运算规则可以总结为下面这个计算公式,n为传入参数:

  32. loops = ( ( (n *((2199023*HZ)>>11)) >> 14 ) * (loops_per_jiffy >> 10) ) >> 6 

  33.  
  34.  
  35. /*

  36.  * loops = r0 * HZ * loops_per_jiffy / 1000000

  37.  *

  38.  * Oh, if only we had a cycle counter...

  39.  */

  40.  
  41.  
  42. @ Delay routine

  43. ENTRY(__delay)

  44.         subs    r0, r0, #1

  45.         bhi __delay

  46.         mov pc, lr

  47. ENDPROC(__udelay)

  48. ENDPROC(__const_udelay)

  49. 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)内核开发中使用除法运算时要考虑清楚哦。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值