Linux内核add_timer的可靠性在一个jiffy的分辨率?

sda*_*aau 16 linux timer linux-kernel

在下面给出的代码中,有一个简单的Linux内核模块(驱动程序),它add_timer以1 jiffy的分辨率重复调用一个函数10次(也就是说,计时器计划发射jiffies + 1).使用该bash脚本rerun.sh,然后从打印输出中获取时间戳syslog,并使用它来显示它们gnuplot.

在大多数情况下,我得到这样的syslog输出:

[ 7103.055787] Init testjiffy: 0 ; HZ: 250 ; 1/HZ (ms): 4
[ 7103.056044]  testjiffy_timer_function: runcount 1 
[ 7103.060045]  testjiffy_timer_function: runcount 2 
[ 7103.064052]  testjiffy_timer_function: runcount 3 
[ 7103.068050]  testjiffy_timer_function: runcount 4 
[ 7103.072053]  testjiffy_timer_function: runcount 5 
[ 7103.076036]  testjiffy_timer_function: runcount 6 
[ 7103.080044]  testjiffy_timer_function: runcount 7 
[ 7103.084044]  testjiffy_timer_function: runcount 8 
[ 7103.088060]  testjiffy_timer_function: runcount 9 
[ 7103.092059]  testjiffy_timer_function: runcount 10 
[ 7104.095429] Exit testjiffy
Run Code Online (Sandbox Code Playgroud)

...结果与时间序列和delta直方图这样:

_testjiffy_00001.png

这基本上是我期望从代码中获得的时序质量.

然而 - 每隔一段时间,我会得到一个捕获:

[ 7121.377507] Init testjiffy: 0 ; HZ: 250 ; 1/HZ (ms): 4
[ 7121.380049]  testjiffy_timer_function: runcount 1 
[ 7121.384062]  testjiffy_timer_function: runcount 2 
[ 7121.392053]  testjiffy_timer_function: runcount 3 
[ 7121.396055]  testjiffy_timer_function: runcount 4 
[ 7121.400068]  testjiffy_timer_function: runcount 5 
[ 7121.404085]  testjiffy_timer_function: runcount 6 
[ 7121.408084]  testjiffy_timer_function: runcount 7 
[ 7121.412072]  testjiffy_timer_function: runcount 8 
[ 7121.416083]  testjiffy_timer_function: runcount 9 
[ 7121.420066]  testjiffy_timer_function: runcount 10 
[ 7122.417325] Exit testjiffy
Run Code Online (Sandbox Code Playgroud)

...结果如下:

_testjiffy_00002.png

......我想:"WHOOOOOAAAAAAA ......等一下......" - 序列中没有脉冲掉落吗?意味着add_timer 错过了一个插槽,然后在接下来的4毫秒插槽中启动了该功能?

有趣的是,在运行这些测试时,除了终端,Web浏览器和文本编辑器之外别无其他 - 所以我真的看不到任何运行,可能会占用OS /内核; 因此,我真的看不出为什么内核会发生如此大的错过(整个jiffy时期)的原因.当我读到Linux内核时序时,例如" 所有定时器中最简单和最不准确的......是定时器API ",我读到"最不准确"为:"不要指望恰好是 4毫秒的时间段"(就此而言)例子) - 我没有,我对(第一)直方图中显示的方差很好; 但我不指望整整一段时间都会错过!?

所以我的问题是:

  • 这个预期的行为来自add_timer这个决议(偶尔会错过一段时间)吗?
  • 如果有的话,有没有办法add_timer在每个4ms插槽中"强制" 触发该功能,如此平台上的jiffy所指定的那样?
  • 是否有可能出现"错误"的时间戳 - 例如,时间戳反映了syslog的实际"打印"发生的时间,而不是实际触发的时间?
  • 请注意,我不是要寻找低于jiffy的周期分辨率(在这种情况下,4ms); 当代码正常工作时,我也不希望减少delta方差.所以我看来,我没有"高分辨率计时器"的要求,也没有"硬实时"的要求 - 我只是想要add_timer可靠地开火.如果不采用内核的特殊"实时"配置,是否可以在此平台上实现?

奖金问题:在rerun.sh下面,你会注意到两个sleep标有MUSTHAVE; 如果其中任何一个被遗漏/注释,OS /内核冻结,并需要重启.我无法理解为什么 - 从bash 运行rmmod之后是否真的可能这么快,它会与正常的模块加载/卸载过程冲突?insmod


平台信息:

$ cat /proc/cpuinfo | grep "processor\|model name\|MHz\|cores"
processor   : 0       # (same for 1)
model name  : Intel(R) Atom(TM) CPU N450   @ 1.66GHz
cpu MHz             : 1000.000
cpu cores   : 1
$ echo $(cat /etc/issue ; uname -a)
Ubuntu 11.04 \n \l Linux mypc 2.6.38-16-generic #67-Ubuntu SMP Thu Sep 6 18:00:43 UTC 2012 i686 i686 i386 GNU/Linux
$ echo $(lsb_release -a 2>/dev/null | tr '\n' ' ')
Distributor ID: Ubuntu Description: Ubuntu 11.04 Release: 11.04 Codename: natty
Run Code Online (Sandbox Code Playgroud)

码:

$ cd /tmp/testjiffy
$ ls
Makefile  rerun.sh  testjiffy.c
Run Code Online (Sandbox Code Playgroud)

Makefile:

obj-m += testjiffy.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Run Code Online (Sandbox Code Playgroud)

testjiffy.c:

/*
 *  [http://www.tldp.org/LDP/lkmpg/2.6/html/lkmpg.html#AEN189 The Linux Kernel Module Programming Guide]
 */


#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */
#include <linux/init.h>     /* Needed for the macros */
#include <linux/jiffies.h>
#include <linux/time.h>
#define MAXRUNS 10

static volatile int runcount = 0;
static struct timer_list my_timer;

static void testjiffy_timer_function(unsigned long data)
{
  int tdelay = 100;

  runcount++;
  if (runcount == 5) {
    while (tdelay > 0) { tdelay--; } // small delay
  }

  printk(KERN_INFO
    " %s: runcount %d \n",
    __func__, runcount);

  if (runcount < MAXRUNS) {
    my_timer.expires = jiffies + 1;
    add_timer(&my_timer);
  }
}


static int __init testjiffy_init(void)
{
    printk(KERN_INFO
    "Init testjiffy: %d ; HZ: %d ; 1/HZ (ms): %d\n",
               runcount,      HZ,        1000/HZ);

  init_timer(&my_timer);

    my_timer.function = testjiffy_timer_function;
    //my_timer.data = (unsigned long) runcount;

  my_timer.expires = jiffies + 1;
    add_timer(&my_timer);
    return 0;
}

static void __exit testjiffy_exit(void)
{
    printk(KERN_INFO "Exit testjiffy\n");
}

module_init(testjiffy_init);
module_exit(testjiffy_exit);

MODULE_LICENSE("GPL");
Run Code Online (Sandbox Code Playgroud)

rerun.sh:

#!/usr/bin/env bash

set -x
make clean
make
# blank syslog first
sudo bash -c 'echo "0" > /var/log/syslog'
sleep 1   # MUSTHAVE 01!
# reload kernel module/driver
sudo insmod ./testjiffy.ko
sleep 1   # MUSTHAVE 02!
sudo rmmod testjiffy
set +x

# copy & process syslog

max=0;
for ix in _testjiffy_*.syslog; do
  aa=${ix#_testjiffy_};
  ab=${aa%.syslog} ;
  case $ab in
    *[!0-9]*) ab=0;;          # reset if non-digit obtained; else
    *) ab=$(echo $ab | bc);;  # remove leading zeroes (else octal)
  esac
  if (( $ab > $max )) ; then
    max=$((ab));
  fi;
done;
newm=$( printf "%05d" $(($max+1)) );
PLPROC='chomp $_;
if (!$p) {$p=0;}; if (!$f) {$f=$_;} else {
  $a=$_-$f; $d=$a-$p;
  print "$a $d\n" ; $p=$a;
};'

set -x
grep "testjiffy" /var/log/syslog | cut -d' ' -f7- > _testjiffy_${newm}.syslog
grep "testjiffy_timer_function" _testjiffy_${newm}.syslog \
  | sed 's/\[\(.*\)\].*/\1/' \
  | perl -ne "$PLPROC" \
  > _testjiffy_${newm}.dat
set +x

cat > _testjiffy_${newm}.gp <<EOF
set terminal pngcairo font 'Arial,10' size 900,500
set output '_testjiffy_${newm}.png'
set style line 1 linetype 1 linewidth 3 pointtype 3 linecolor rgb "red"
set multiplot layout 1,2 title "_testjiffy_${newm}.syslog"
set xtics rotate by -45
set title "Time positions"
set yrange [0:1.5]
set offsets graph 50e-3, 1e-3, 0, 0
plot '_testjiffy_${newm}.dat' using 1:(1.0):xtic(gprintf("%.3se%S",\$1)) notitle with points ls 1, '_testjiffy_${newm}.dat' using 1:(1.0) with impulses ls 1
binwidth=0.05e-3
set boxwidth binwidth
bin(x,width)=width*floor(x/width) + width/2.0
set title "Delta diff histogram"
set style fill solid 0.5
set autoscale xy
set offsets graph 0.1e-3, 0.1e-3, 0.1, 0.1
plot '_testjiffy_${newm}.dat' using (bin(\$2,binwidth)):(1.0) smooth freq with boxes ls 1
unset multiplot
EOF
set -x; gnuplot _testjiffy_${newm}.gp ; set +x
Run Code Online (Sandbox Code Playgroud)

编辑:通过启发由@granquet此评论,我试图获得调度统计/proc/schedstat/proc/sched_debug,通过使用dd通过call_usermodehelper; 请注意,这大部分时间都"跳过"(即,由于函数的第7次,第6次或第X次运行而丢失的文件); 但是我设法获得了两次完整的运行,并将它们发布在https://gist.github.com/anonymous/5709699中(因为我注意到gist可能更喜欢在上面使用pastebin),因为输出有点大; 该*_11*文件记录一个正确的运行,该*_17*文件记录运行与"降".

注意我也切换到mod_timer_pinned模块中,并没有多大帮助(使用此功能的模块获得了主要日志).以下是这些变化testjiffy.c:

#include <linux/kmod.h> // usermode-helper API
...
char fcmd[] = "of=/tmp/testjiffy_sched00";
char *dd1argv[] = { "/bin/dd", "if=/proc/schedstat", "oflag=append", "conv=notrunc", &fcmd[0], NULL };
char *dd2argv[] = { "/bin/dd", "if=/proc/sched_debug", "oflag=append", "conv=notrunc", &fcmd[0], NULL };
static char *envp[] = {
      "HOME=/",
      "TERM=linux",
      "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };

static void testjiffy_timer_function(unsigned long data)
{
  int tdelay = 100;
  unsigned long tjnow;

  runcount++;
  if (runcount == 5) {
    while (tdelay > 0) { tdelay--; } // small delay
  }

  printk(KERN_INFO
    " %s: runcount %d \n",
    __func__, runcount);

  if (runcount < MAXRUNS) {
    mod_timer_pinned(&my_timer, jiffies + 1);
    tjnow = jiffies;
    printk(KERN_INFO
      " testjiffy expires: %lu - jiffies %lu => %lu / %lu\n",
      my_timer.expires, tjnow, my_timer.expires-tjnow, jiffies);
    sprintf(fcmd, "of=/tmp/testjiffy_sched%02d", runcount);
    call_usermodehelper( dd1argv[0], dd1argv, envp, UMH_NO_WAIT );
    call_usermodehelper( dd2argv[0], dd2argv, envp, UMH_NO_WAIT );
  }
}
Run Code Online (Sandbox Code Playgroud)

......这个在rerun.sh:

...
set +x

for ix in /tmp/testjiffy_sched*; do
  echo $ix | tee -a _testjiffy_${newm}.sched
  cat $ix >> _testjiffy_${newm}.sched
done
set -x ; sudo rm /tmp/testjiffy_sched* ; set +x

cat > _testjiffy_${newm}.gp <<EOF
...
Run Code Online (Sandbox Code Playgroud)

我将使用这篇文章进行详细回复.

@CL.:非常感谢你的回答.很高兴它确认"你的计时器功能可能会在以后的jiffy中被调用"; 通过记录jiffies,我也意识到计时器函数会在以后被调用 - 除此之外,它本身并没有任何"错误".

很高兴知道时间戳; 我想知道是否有可能:定时器函数在正确的时间点击,但是内核抢占了内核日志记录服务(我相信它klogd),所以我得到一个延迟的时间戳?但是,我正在尝试创建一个"循环"(或更确切地说是周期性)定时器函数来写入硬件,我首先通过实现PC不会在USB总线上以特定间隔写入数据来注意这种"丢弃".并且鉴于时间戳确认了这种行为,这可能不是问题(我猜).

我修改了计时器功能,因此它相对于最后一个计时器(my_timer.expires)的预定时间触发- 再次通过mod_timer_pinned而不是add_timer:

static void testjiffy_timer_function(unsigned long data)
{
  int tdelay = 100;
  unsigned long tjlast;
  unsigned long tjnow;

  runcount++;
  if (runcount == 5) {
    while (tdelay > 0) { tdelay--; } // small delay
  }

  printk(KERN_INFO
    " %s: runcount %d \n",
    __func__, runcount);

  if (runcount < MAXRUNS) {
    tjlast = my_timer.expires;
    mod_timer_pinned(&my_timer, tjlast + 1);
    tjnow = jiffies;
    printk(KERN_INFO
      " testjiffy expires: %lu - jiffies %lu => %lu / %lu last: %lu\n",
      my_timer.expires, tjnow, my_timer.expires-tjnow, jiffies, tjlast);
  }
}
Run Code Online (Sandbox Code Playgroud)

......而且前几次尝试,它无可挑剔 - 然而,最终,我得到了这个:

[13389.775508] Init testjiffy: 0 ; HZ: 250 ; 1/HZ (ms): 4
[13389.776051]  testjiffy_timer_function: runcount 1 
[13389.776063]  testjiffy expires: 3272445 - jiffies 3272444 => 1 / 3272444 last: 3272444
[13389.780053]  testjiffy_timer_function: runcount 2 
[13389.780068]  testjiffy expires: 3272446 - jiffies 3272445 => 1 / 3272445 last: 3272445
[13389.788054]  testjiffy_timer_function: runcount 3 
[13389.788073]  testjiffy expires: 3272447 - jiffies 3272447 => 0 / 3272447 last: 3272446
[13389.788090]  testjiffy_timer_function: runcount 4 
[13389.788096]  testjiffy expires: 3272448 - jiffies 3272447 => 1 / 3272447 last: 3272447
[13389.792070]  testjiffy_timer_function: runcount 5 
[13389.792091]  testjiffy expires: 3272449 - jiffies 3272448 => 1 / 3272448 last: 3272448
[13389.796044]  testjiffy_timer_function: runcount 6 
[13389.796062]  testjiffy expires: 3272450 - jiffies 3272449 => 1 / 3272449 last: 3272449
[13389.800053]  testjiffy_timer_function: runcount 7 
[13389.800063]  testjiffy expires: 3272451 - jiffies 3272450 => 1 / 3272450 last: 3272450
[13389.804056]  testjiffy_timer_function: runcount 8 
[13389.804072]  testjiffy expires: 3272452 - jiffies 3272451 => 1 / 3272451 last: 3272451
[13389.808045]  testjiffy_timer_function: runcount 9 
[13389.808057]  testjiffy expires: 3272453 - jiffies 3272452 => 1 / 3272452 last: 3272452
[13389.812054]  testjiffy_timer_function: runcount 10 
[13390.815415] Exit testjiffy
Run Code Online (Sandbox Code Playgroud)

...这样渲染:

_testjiffy_00027

...所以,基本上我在+ 8ms插槽有一个延迟/"下降"(应该是@ 3272446 jiffies),然后有两个功能在+ 12ms插槽运行(这将是@ 3272447 jiffies); 你甚至可以看到情节上的标签为"更加粗体".这是更好的,在"丢弃"序列现在与正确的非丢弃序列同步(正如你所说:"以避免一个后期定时器功能转移所有后续定时器调用") - 然而,我仍然错过了一个节拍; 因为我必须在每次节拍时将字节写入硬件,所以我保持一个持续的,恒定的传输速率,遗憾的是这对我没什么帮助.

至于另一个建议,"使用十个定时器" - 因为我的最终目标(使用周期性的lo-res定时器功能写入硬件); 我一开始认为它不适用 - 但如果没有别的可能(除了做一些特殊的实时内核准备),那么我肯定会尝试一个方案,我有10个(或N个)定时器(可能存储在一个接一个地定期发射的数组).


编辑:只需添加剩余的相关评论:

USB传输要么提前安排(等时),要么没有时间保证(异步).如果您的设备不使用等时传输,则会严重错误设计. - CL.6月5日10:47

感谢评论,@ CL. - "...提前安排(等时)......"我清除了一个混乱.我(最终)瞄准FT232,它只有BULK模式 - 只要每个定时器命中的字节数很少,我实际上可以用add_timer"欺骗"我的方式通过"流"数据; 然而,当我将大量字节转移到接近消耗带宽时,这些"失火"开始变得明显,因为下降.所以我有兴趣测试它的限制,为此我需要一个可靠的重复"定时器"功能 - 还有什么我可以尝试拥有一个可靠的"计时器"吗? - sdaau 6月6日12:27

@sdaau批量转移不适合流式传输.您无法使用其他类型的软件计时器来修复硬件协议中的缺陷. - CL.6月5日13:50

......以及我对@CL的回应.:我知道我无法解决缺点; 我更感兴趣的是观察这些缺点 - 比如说,如果内核函数进行周期性的USB写操作,我可以在示波器/分析仪上观察信号,并希望看到散装模式在哪种意义上是不合适的.但首先,我必须相信该函数可以(至少在某种程度上)以一个周期性的速率可靠地重复(即"生成"一个时钟/滴答) - 直到现在我还不知道我真的不能相信add_timer在jiffies分辨率(因为它能够相对容易地跳过整个时期).但是,似乎转向Linux的高分辨率计时器(hrtimer)确实在这个意义上给了我一个可靠的周期函数 - 所以我想这解决了我的问题(在下面的答案中发布).

sda*_*aau 11

非常感谢所有的评论和答案; 他们都指出了必须考虑的事情 - 但鉴于我有点像一个永远的菜鸟,我仍然需要做更多的阅读,然后才能获得一些理解(我希望是正确的).此外,我无法找到任何具体的定期"滴答"功能 - 所以我会在这里发布一个更详细的答案.

简而言之 - 对于一个快速分辨率的可靠的定期Linux内核函数,不要使用add_timer(<linux/time.h>),因为它可能"丢弃"整个句点; 使用高分辨率计时器(<linux/hrtimer.h>)代替.更详细:

我有可能得到一个"错误"的时间戳 - ......?

@CL.:日志中的时间戳是将该字符串打印到日志的时间.

所以,也许它是可能的 - 但结果证明,这不是问题所在:

这个解决方案中add_timer的这种预期行为(有时会错过一段时间)吗?

我想,事实证明 - 是的:

如果是这样,有没有办法"强制"add_timer在每个4ms插槽中触发该函数,如此平台上的jiffy所指定的那样?

......(我猜再次),事实证明 - 没有.

现在,其原因有点微妙 - 我希望如果我没有把它们弄好,有人会纠正我.首先,我遇到的第一个误解是"时钟只是一个时钟"(在某种意义上:即使它被实现为计算机代码) - 但这并不完全正确.内核基本上必须在某个地方"排队"一个"事件",每次add_timer都使用类似的东西; 这个请求可能来自任何真实的东西:来自驱动程序的任何(和所有)排序,甚至可能来自用户空间.

问题在于这种"排队"成本 - 因为除了内核必须处理(相当于)遍历和插入(和移除)数组中的项目之外,它还必须处理跨越几个数量级的定时器延迟(来自比如说毫秒到10秒钟; 事实上,一些驱动程序(显然,那些用于网络协议的驱动程序)显然会排队很多计时器事件,这些事件通常在运行之前被取消 - 而其他类型可能需要完全不同的行为(就像在我的情况下一样 - 在周期性功能中) ,您希望在大多数情况下,事件通常不会被取消;并且您还会逐个排队事件).最重要的是,内核需要为单处理器与SMP与多处理器平台处理此问题.因此,在内核中实现定时器处理涉及成本 - 收益权衡.

事实证明,jiffies /周围的架构add_timer旨在处理最常见的设备 - 对于它们来说,精确的分辨率不是问题; 但这也意味着用这种方法不能指望在单个jiffy的分辨率下有一个可靠的计时器.内核通过将它们(有点)视为中断服务请求(IRQ)来处理这些"事件队列",这也加剧了这一点.并且内核中的IRQ处理有多个级别的优先级,其中较高优先级的例程可以抢占较低优先级的例程(即:中断并暂停较低优先级的例程,即使它当时正在执行 - 并且允许更高优先级的例程来开展业务).或者,如前所述:

@granquet:定时器在软irq上下文中运行,这意味着它们具有最高优先级并且它们抢占CPU上运行/可运行的所有内容...但是在维护软irq时未禁用的硬件中断.所以你可能(最可能的解释)在这里和那里获得硬件中断,抢占你的计时器......因此你得到一个在正确的时间没有服务的中断.

@CL.:确实有可能你的计时器函数在稍后的jiffy中被调用,而不是在设置到期时调用.可能的原因是调度延迟,其他驱动程序禁用中断太长时间(图形和WLAN驱动程序通常是罪魁祸首),或者一些糟糕的BIOS执行SMI代码.

我现在也这么认为 - 我认为这可能是一个例子:

  • jiffies 更改为10000(== 40000 ms @ 250 Hz)
  • 假设计时器功能(排队add_timer)即将开始运行 - 但还没有开始运行
  • 我们在这里说,网卡生成(无论出于何种原因)硬件中断
  • 具有更高优先级的硬件中断触发内核抢占(停止和暂停)定时器功能(可能现在开始,只有少量指令);
  • 这意味着内核现在必须重新安排定时器函数,以便稍后运行 - 因为一个只能在内核中使用整数运算,并且这种事件的时间分辨率是jiffies - 它能做的最好是重新安排它为jiffies + 1(10001 == 40004 ms @ 250 Hz)
  • 现在内核将上下文切换到网卡驱动程序的IRQ服务例程,它就是它的业务
  • 假设IRQ服务程序在200μs完成 - 这意味着现在我们(在"绝对"条件下)在40000.2毫秒 - 但是,我们仍然在10000 jiffies
  • 如果内核现在将上下文切换计时器功能,它就会完成 - 没有必要注意延迟;
  • ...但是,这不会发生,因为计时器功能安排在下一个jiffy!
  • 因此内核可以在接下来的大约3.8毫秒内完成其业务(可能正在休眠)
  • jiffies 更改为10001(== 40004 ms @ 250 Hz)
  • (以前重新安排的)计时器功能运行 - 这次完成不会中断

我还没有真正做过详细的分析,看看事件的顺序是否完全如上所述; 但我非常相信它是接近的 - 换句话说,解决问题 - 特别是因为高分辨率计时器方法似乎没有显示出这种行为.获得一个调度程序日志并确切地知道发生什么事情会导致先发制人,这确实很棒- 但是我怀疑为了响应@granquet的评论我在OP编辑中尝试的用户空间的往返是正确的事情.

无论如何,回到这个:

请注意,我不是要寻找低于jiffy的周期分辨率(在这种情况下,4ms); 当代码正常工作时,我也不希望减少delta方差.所以我看到它,我没有"高分辨率计时器"的要求,也没有"硬实时"要求......

......这是我犯的一个错误 - 正如上面的分析所示,我确实有"高分辨率"的要求!如果我早些时候意识到,我可能会更早地找到相关的阅读材料.无论如何,一些相关的文档 - 即使他们没有特别讨论周期性函数 - 对我来说是:

  • LDD3:5.3.信号量和互斥量 - (在描述具有不同要求的驱动程序时):" 不会从中断处理程序或其他异步上下文进行访问.没有特定的延迟(响应时间)要求; 应用程序员理解I/O请求是通常不满意 "
  • Documentation/timers/hrtimers.txt - " timers.c代码非常"紧密编码"围绕jiffies和32位假设,并且针对相对较窄的用例(在相对较窄的HZ范围内的jiffies)进行了磨练和微优化)多年 - 因此即使很小的扩展也很容易打破轮子的概念 "
  • T. Gleixner,D.豪斯Hrtimers和超越:转变Linux的时间子系统(PDF) - (最详细的,也见图表内)"1997年实现的级联定时轮(CTW)取代了原始的时间排序双链表,以解决链表的O(N)插入时间的可扩展性问题......目前Linux中定时器管理的方法是很好地满足了极其广泛的要求,但它无法提供某些情况下所需的服务质量,因为它必须满足如此广泛的要求......超时相关的定时器保存在现有的定时轮和为(高分辨率)计时器要求优化的新子系统实现了hrtimers.hrtimers完全基于人类时间(单位:纳秒)......它们按时间排序,按CPU列表保存,实现为红黑树."
  • 高分辨率计时器API [LWN.net] - " 在其核心,hrtimer机制保持不变.而不是使用"计时器轮"数据结构,hrtimers生活在一个按时间排序的链表上,下一个计时器到一个单独的红/黑树也用于启用计时器事件的插入和删除而不扫描列表.但是当核心保持不变时,几乎所有其他事情都发生了变化,至少表面上看. "
  • 软件中断和实时[LWN.net] - " softirq机制旨在处理几乎 - 但不完全 - 与处理硬件中断同样重要的处理.Softirqs以高优先级运行(尽管有一个有趣的例外,描述但是,由于启用了硬件中断,它们通常会抢占任何工作,除了响应"真正的"硬件中断......然而,从3.0实时补丁集开始,该功能消失了......作为回应, 3.6.1-rt1,softirqs的处理再次发生变化. "
  • 高(但不是太高)分辨率超时[LWN.net] - "_ pix()和epoll_wait()采用整数毫秒; select()采用微秒分辨率的结构时间,以及ppoll()和pselect( )采用纳秒级分辨率的struct timespec.它们都是相同的,因为它们将这个超时值转换为jiffies,最大分辨率在1到10毫秒之间.程序员可能编程一个10纳秒的pselect()调用超时,但是在10毫秒之后调用可能不会返回,即使在没有CPU争用的情况下....这是一个有用的功能,但它的代价是一些重大的API更改._"

从引言中可以清楚地看出,内核中的高分辨率计时工具仍处于活跃开发阶段(使用API​​更改) - 我担心,也许我必须安装一个特殊的"实时补丁"内核.值得庆幸的是,我的2.6.38-16 SMP内核中的高分辨率计时器似乎可用(并且正常工作),没有任何特殊更改.下面是修改后的testjiffies.c内核模块的列表,该模块现在使用高分辨率计时器,但是保持与确定的相同的周期jiffies.为了测试,我做了200次循环(而不是OP中的10次); 并运行rerun.sh脚本大约20-30次,这是我得到的最糟糕的结果:

_testjiffy_00037.png

时间序列现在显然是不可读的,但直方图仍然可以告诉我们:对于最大偏差,取0.00435-0.004(= 0.004-0.00365)=350μs,它仅代表100*(350/4000)= 8.75%的预期期限; 我当然没有问题.此外,我从未得到过丢失(或相应地,整个2*周期= 8毫秒延迟),或0毫秒延迟 - 我得到的捕获,否则是OP中第一张图像上显示的质量.现在,我当然可以进行更长时间的测试,更准确地看到它的可靠性 - 但这是我期望/需要看到的这个简单案例的可靠性; 与OP形成鲜明对比的是,在那里我只有10个循环,有投掷硬币的可能性 - 每隔二或三次运行rerun.sh 脚本,我会得到一个下降 - 即使在操作系统资源使用率低的情况下!

最后,请注意下面的源应该有问题,由@CL发现.:" 您的模块有问题:您必须确保在卸载模块之前计​​时器未挂起 ",已修复(在上下文中hrtimer).这似乎回答了我的奖金问题,因为它不需要脚本sleep中的任何一个"MUSTHAVE" rerun.sh.但是,请注意,如果我们想要一个完整的200个刻度线捕获,那么200个循环@ 4毫秒需要0.8秒 - sleep介于两者之间insmod并且rmmod是必需的(否则,在我的机器上,我只获得了7个刻度线).

好吧,希望我现在能够做到这一点(至少大部分都是这样) - 如果没有,欢迎更正 :)

testjiffy(-hr).c

#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */
#include <linux/init.h>     /* Needed for the macros */
#include <linux/jiffies.h>
#include <linux/time.h>
#define MAXRUNS 200

#include <linux/hrtimer.h>


static volatile int runcount = 0;

//~ static struct timer_list my_timer;
static unsigned long period_ms;
static unsigned long period_ns;
static ktime_t ktime_period_ns;
static struct hrtimer my_hrtimer;


//~ static void testjiffy_timer_function(unsigned long data)
static enum hrtimer_restart testjiffy_timer_function(struct hrtimer *timer)
{
  int tdelay = 100;
  unsigned long tjnow;
  ktime_t kt_now;
  int ret_overrun;

  runcount++;
  if (runcount == 5) {
    while (tdelay > 0) { tdelay--; } // small delay
  }

  printk(KERN_INFO
    " %s: runcount %d \n",
    __func__, runcount);

  if (runcount < MAXRUNS) {
    tjnow = jiffies;
    kt_now = hrtimer_cb_get_time(&my_hrtimer);
    ret_overrun = hrtimer_forward(&my_hrtimer, kt_now, ktime_period_ns);
    printk(KERN_INFO
      " testjiffy jiffies %lu ; ret: %d ; ktnsec: %lld \n",
      tjnow, ret_overrun, ktime_to_ns(kt_now));
    return HRTIMER_RESTART;
  }
  else return HRTIMER_NORESTART;
}


static int __init testjiffy_init(void)
{
  struct timespec tp_hr_res;
  period_ms = 1000/HZ;
  hrtimer_get_res(CLOCK_MONOTONIC, &tp_hr_res);
  printk(KERN_INFO
    "Init testjiffy: %d ; HZ: %d ; 1/HZ (ms): %ld ; hrres: %lld.%.9ld\n",
               runcount,      HZ,        period_ms, (long long)tp_hr_res.tv_sec, tp_hr_res.tv_nsec );

  hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
  my_hrtimer.function = &testjiffy_timer_function;
  period_ns = period_ms*( (unsigned long)1E6L );
  ktime_period_ns = ktime_set(0,period_ns);
  hrtimer_start(&my_hrtimer, ktime_period_ns, HRTIMER_MODE_REL);

  return 0;
}

static void __exit testjiffy_exit(void)
{
  int ret_cancel = 0;
  while( hrtimer_callback_running(&my_hrtimer) ) {
    ret_cancel++;
  }
  if (ret_cancel != 0) {
    printk(KERN_INFO " testjiffy Waited for hrtimer callback to finish (%d)\n", ret_cancel);
  }
  if (hrtimer_active(&my_hrtimer) != 0) {
    ret_cancel = hrtimer_cancel(&my_hrtimer);
    printk(KERN_INFO " testjiffy active hrtimer cancelled: %d (%d)\n", ret_cancel, runcount);
  }
  if (hrtimer_is_queued(&my_hrtimer) != 0) {
    ret_cancel = hrtimer_cancel(&my_hrtimer);
    printk(KERN_INFO " testjiffy queued hrtimer cancelled: %d (%d)\n", ret_cancel, runcount);
  }
  printk(KERN_INFO "Exit testjiffy\n");
}

module_init(testjiffy_init);
module_exit(testjiffy_exit);

MODULE_LICENSE("GPL");
Run Code Online (Sandbox Code Playgroud)