linux内核calibrate_delay函数实现分析

公司使用的是mips处理器,每秒钟jiffs是250个,内核变量HZ值就是250,由此我引发一个疑问,一个jiffs时钟中断耗时4ms,没法做到毫秒微妙甚至纳秒的精度,那么内核延时函数mdelay udelay ndelya是如何实现的呢,于是研究了一下,就找到了linux内核中一个有趣的函数calibrate_delay()。 
calibrate_delay()函数可以计算出cpu在一秒钟内执行了多少次一个极短的循环,最终计算出来的值赋给变量loop_per_jiffs,经过处理后得到BogoMIPS 值,Bogo是Bogus(伪)的意思,MIPS是millions of instructions per second(百万条指令每秒)的缩写。这样我们就知道了其实这个函数是linux内核中一个cpu性能测试函数。对于内核延时函数,关键点就是loop_per_jiffs,udelay函数就是利用这个变量计算出自己延时所需要的短循环。来实现微秒级延时。由于内核对这个数值的要求不高,所以内 核使用了一个十分简单而有效的算法用于得到这个值。如果你想了解自己机器的BogoMIPS,你可以察看 /proc/cpuinfo文件中的bogomips。让我们一起来看看 calibrate_delay函数是怎么完成工作的。 
下面是2.6.21版本内核下calibrate_delay的源代码,这个函数是在内核启动函数start_kernel中调用。
108 /*
109  * This is the number of bits of precision for the loops_per_jiffy.  Each
110  * bit takes on average 1.5/HZ seconds.  This (like the original) is a little
111  * better than 1%
112  */
113 #define LPS_PREC 8
114 
115 void __devinit calibrate_delay(void)
116 {
117     unsigned long ticks, loopbit;
118     int lps_precision = LPS_PREC;
119 
120     if (preset_lpj) {
121         loops_per_jiffy = preset_lpj;
122         printk("Calibrating delay loop (skipped)... "
123             "%lu.%02lu BogoMIPS preset\n",
124             loops_per_jiffy/(500000/HZ),
125             (loops_per_jiffy/(5000/HZ)) % 100);
126     } else if ((loops_per_jiffy = calibrate_delay_direct()) != 0) {
127         printk("Calibrating delay using timer specific routine.. ");
128         printk("%lu.%02lu BogoMIPS (lpj=%lu)\n",
129             loops_per_jiffy/(500000/HZ),
130             (loops_per_jiffy/(5000/HZ)) % 100,
131             loops_per_jiffy);
132     } else {
133         loops_per_jiffy = (1<<12);
134 
135         printk(KERN_DEBUG "Calibrating delay loop... ");
136         while ((loops_per_jiffy <<= 1) != 0) {
137             /* wait for "start of" clock tick */
138             ticks = jiffies;
139             while (ticks == jiffies)
140                 /* nothing */;
141             /* Go .. */
142             ticks = jiffies;
143             __delay(loops_per_jiffy);
144             ticks = jiffies - ticks;
145             if (ticks)

146                 break;

147         }
148 
149         /*
150          * Do a binary approximation to get loops_per_jiffy set to
151          * equal one clock (up to lps_precision bits)
152          */
153         loops_per_jiffy >>= 1;
154         loopbit = loops_per_jiffy;
155         while (lps_precision-- && (loopbit >>= 1)) {
156             loops_per_jiffy |= loopbit;
157             ticks = jiffies;
158             while (ticks == jiffies)
159                 /* nothing */;
160             ticks = jiffies;
161             __delay(loops_per_jiffy);
162             if (jiffies != ticks)   /* longer than 1 tick */
163                 loops_per_jiffy &= ~loopbit;
164         }
165 
166         /* Round the value and print it */
167         printk("%lu.%02lu BogoMIPS (lpj=%lu)\n",
168             loops_per_jiffy/(500000/HZ),
169             (loops_per_jiffy/(5000/HZ)) % 100,
170             loops_per_jiffy);
171     }
172 
173 }

对calibrate_delay()函数逐行分析如下: 

118行 定义计算BogoMIPS的精度变量lps_precision,这个值越大,则计算出的lpj以及BogoMIPS越精确。 

120行 present_lpj是内核一个启动参数,可以在内核启动时指定该值大小,从而指定lpj值,这里我们不指定,而是内核自己计算,所以lps_precision=0,不走该分支

126行 调用calibrate_delay_direct来计算lpj值,这个函数特定于不同cpu平台实现,利用cpu的计数器来实现,查看这个函数实现,比如x86就利用rdtscll来读取tsc时间戳计数器来实现,但是该内核下mips没有实现利用计数器的函数,虽然mips处理器cp0的9号寄存器可以作为高精度计数器来使用。所以mips下宏ARCH_HAS_READ_CURRENT_TIMER为0,条件编译,calibrate_delay_direct直接返回0.跳到下一分支。

133行 loops_per_jiffy是该函数核心,为每jiffy执行一个极短的循环的次数,先赋一个初始值。   
133行-146行,是第一次计算loops_per_jiffy的值,这次计算只是一个粗略的计算,为下面的计算打好基础。 

136行,loops_per_jiffy每次循环增大一倍。

137行-142行,这里是用于等待一个新的定时器滴答的开始。也算是验证一下处理器时钟中断是否正常产生。产生时钟中断,jiffy增长1个,则跳出循环,往下执行。

142行-146行,在一个滴答的开始时, 立即重复执行一个极短的循环,当一个滴答结束时,这个循环执行了多少次就是我们要求的初步的loops_per_jiffy的值。 也就是当重复短循环结束后如果jiffs增加,则说明这个循环次数是一个jiffy短循环次数估值,如果jiffy没有增加,则loops_per_jiffy再翻倍测试。

这 个值误差太大,所以我们还要经过第二次计算。这里还要注意的是通过上面的分析,我们可以知道更加精确的loops_per_jiffy的值应该在现在的值与上次值也就是它的一半之间。 
149行-164行 这里开始就是第二次计算了。它用经典的折半查找法在我们上面所说的范围内计算出了更精确的loops_per_jiffy的值。 
153行 确定折半查找范围的最小值,就是上面粗略计算出来loop_per_jiffy的一半,把它称为起点。 
154行 定义查找范围loopbit,这样我们就可以看到更精确的loop_per_jiffy值在“起点”与“起点加范围(终点)”之间。 
156行 进入循环,将查找范围减半,也就是loop_per_jiffy加loopbit。 

157行-162行 跟前面算法一样测试,在重复循环后,如果jiffy不变,也就是jiffy=ticks。说明现在的loop_per_jiffy延时过短,那么精确的loop_per_jiffy应该比现在的loop_per_jiffy(也就是粗略计算值和它一半值的中间值)大,再次进入循环。loop_per_jiffy再增加它和粗略值的距离的一半,它将是通过不断的折半方式向前增大。直到jiffy不等于ticks,这说明延时过长,说明 loops_per_jiffy的值大了,163行将loop_per_jiffy的值重新返回原起点,当再次进入循环,由于范围减半,故可以达到减小的效果。通过不断折半向下减小。 

但是从这里我们可以看出它好像是个死循环,所以加入了lps_precision变量,来控制循环,即LPS_PREC越大, 循环次数越多,越精确。总的说来,它首先将loop_per_jiffy的值定为原估算值的1/2,作为起点值(我这样称呼它),以估算值 为终点值.然后找出起点值到终点值的中间值.用上面相同的方法执行一段时间的延时循环.如果延时超过了一个tick,说明loop_per_jiffy值偏 大,则仍以原起点值为起点值,以原中间值为终点值,以起点值和终点值的中间为中间值继续进行查找,如果没有超过一个tick,说明 loop_per_jiffy偏小,则以原中间值为起点值,以原终点值为终点值继续查找。 

根据loop_per_jiffy计算出BogoMIPS,并打印。

这就是calibrate_delay函数的实现,这里最终就会产生loop_per_jiffy全部变量来保存单jiffy的短循环数。

udelay实现就利用了lpj这个变量,在delay.h中定义如下:

 82 #define __udelay_val cpu_data[raw_smp_processor_id()].udelay_val
 83 
 84 #define udelay(usecs) __udelay((usecs),__udelay_val)

这个__udelay_val就是lpj的值,在start_kernel中调用check_debugs中江lpj的值付给了udelay_val。

udelay实现还需要具体分析。

ndelay和mdelay都是依赖于udelay实现的。mdelay好说就是udelay的1000倍即可。看一下ndelay实现如下:

34 #ifndef ndelay
 35 #define ndelay(x)   udelay(((x)+999)/1000)
 36 #endif

从这个实现看,在mips架构下没有实现ndelay,ndelay并不能精确到纳秒。

你可能感兴趣的:(linux内核calibrate_delay函数实现分析)