熟悉又陌生的udelay

申請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)內核開發中使用除法運算時要考慮清楚哦。

 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章