不過認證通過給了自己一份動力,在博客上分享更多自己的所學,與大家學習交流。
內核開發中經常用到延時函數,最熟悉的是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)
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)內核開發中使用除法運算時要考慮清楚哦。