16

在下面给出的代码中,有一个简单的 Linux 内核模块(驱动程序),它重复调用一个函数 10 次,使用add_timer分辨率为 1 jiffy(即,定时器计划在 触发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

...结果是时间序列和增量直方图,如下所示:

_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

...结果如下:

_testjiffy_00002.png

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

有趣的是,在运行这些测试时,我只有一个终端、网络浏览器和一个文本编辑器启动了——所以我看不到任何正在运行的东西,这可能会占用操作系统/内核;因此,我真的看不出内核为什么会出现如此大的失误(整个短暂的时期)。当我阅读有关 Linux 内核计时的信息时,例如“所有计时器中最简单和最不准确的......是计时器 API ”,我将“最不准确”读为:“不要期望正好4 毫秒的周期”(根据这个示例) - 我没有,我对(第一个)直方图中显示的方差很好;但我不指望会错过整个时期!?

所以我的问题是:

  • 这是此分辨率的预期行为add_timer(偶尔会错过一段时间)吗?
  • 如果是这样,有没有办法“强制”add_timer在每个 4ms 时隙触发函数,正如这个平台上的 jiffy 所指定的那样?
  • 我是否有可能得到一个“错误”的时间戳 - 例如反映实际“打印”到系统日志的时间戳,而不是函数实际触发的时间?
  • 请注意,我不是在寻找低于对应于 jiffy(在本例中为 4ms)的周期分辨率;当代码正常工作时,我也不希望减少增量方差。所以在我看来,我没有“高分辨率计时器”的要求,也没有“硬实时”的要求——我只想add_timer可靠地发射。在这个平台上这是否可能,而无需求助于内核的特殊“实时”配置?

额外问题:在rerun.sh下面,您会注意到两个sleep标有MUSTHAVE; 如果其中任何一个被遗漏/注释,操作系统/内核会冻结,并且需要重启。而且我不明白为什么 -从 bashrmmod之后运行真的有可能如此之快,以至于它会与模块加载/卸载的正常过程发生冲突吗?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

代码:

$ cd /tmp/testjiffy
$ ls
Makefile  rerun.sh  testjiffy.c

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

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");

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

编辑:受到@granquet 的评论的启发,我尝试从/proc/schedstatand获取调度程序统计信息/proc/sched_debug,通过使用ddthrough call_usermodehelper; 请注意,这在大多数情况下会“跳过”(即,由于函数的第 7 次、第 6 次或第 X 次运行而导致的文件将丢失);但我设法获得了两次完整的运行,并将它们发布在https://gist.github.com/anonymous/5709699中(因为我注意到在 SO 上 gist 可能比 pastebin 更受欢迎),因为输出有点大;文件记录了*_11*正确的运行,*_17*文件记录了带有“drop”的运行。

注意我也在mod_timer_pinned模块中切换到,并没有太大帮助(gist日志是使用该功能的模块获得的)。这些是 中的变化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 );
  }
}

...这在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
...

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

@CL。: 非常感谢您的回答。很高兴它确认“您的计时器函数可能会在稍后的瞬间被调用”;通过记录 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);
  }
}

...在最初的几次尝试中,它的效果无可挑剔-但是,最终,我得到了这个:

[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

...呈现如下:

_testjiffy_00027

...所以,基本上我在 +8ms 插槽(应该是 @3272446 jiffies)有一个延迟/“丢弃”,然后在 +12ms 插槽(应该是 @3272447 jiffies)运行两个函数;您甚至可以因此将绘图上的标签视为“更粗体”。这更好,因为“丢弃”序列现在与正确的非丢弃序列同步(正如您所说:“避免一个迟到的定时器函数转移所有后续定时器调用”) - 但是,我仍然错过一个节拍;而且由于我必须在每次节拍时将字节写入硬件,所以我保持持续、恒定的传输速率,不幸的是,这对我没有多大帮助。

至于另一个建议,“使用十个定时器” - 因为我的最终目标(使用周期性低分辨率定时器功能写入硬件);起初我认为它不适用 - 但如果没有其他可能(除了做一些特殊的实时内核准备),那么我肯定会尝试一个我有 10 个(或 N 个)定时器的方案(可能存储在一个数组)一个接一个周期性地触发。


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

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

感谢您的评论,@CL。- “......提前安排(同步)......”消除了我的困惑。我(最终)瞄准了一个只有 BULK 模式的 FT232——只要每个定时器命中的字节数很低,我实际上可以用 add_timer 在“流式传输”数据中“作弊”;但是,当我传输接近消耗带宽的字节时,这些“失火”开始随着下降而变得明显。所以我有兴趣测试它的限制,为此我需要一个可靠的重复“计时器”功能 - 还有什么我可以尝试拥有一个可靠的“计时器”吗?– sdaau 6 月 5 日 12:27

@sdaau 批量传输不适合流式传输。您无法通过使用另一种软件定时器来修复硬件协议中的缺点。- CL。6月5日 13:50

...作为我对@CL 的回应。: 我知道我无法弥补缺点;我对观察这些缺点更感兴趣——比如,如果一个内核函数进行定期 USB 写入,我可以观察示波器/分析仪上的信号,并希望看到批量模式在什么意义上是不合适的。但首先,我必须相信该函数可以(至少在某种程度上)可靠地以周期性速率重复(即“生成”一个时钟/滴答声)——直到现在我才意识到我不能真正相信add_timer在 jiffies 分辨率下(因为它能够相对轻松地跳过整个周期)。但是,似乎转移到 Linux'hrtimer)。

4

2 回答 2

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:计时器在软中断上下文中运行,这意味着它们具有最高优先级,并且它们抢占 CPU 上正在运行/可运行的所有内容......但是在服务软中断时不会禁用硬件中断。因此,您可能(最可能的解释)在这里和那里获得一个抢占您的计时器的硬件中断......因此您会获得一个未在正确时间提供服务的中断。

@CL。:确实有可能您的计时器函数被调用的时间比设置的过期时间晚。可能的原因是调度延迟,其他驱动程序禁用中断太久(图形和 WLAN 驱动程序通常是罪魁祸首),或者一些糟糕的 BIOS 执行 SMI 代码。

我现在也这么认为——我认为这可以说明发生了什么:

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

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

无论如何,回到这个:

请注意,我不是在寻找低于对应于 jiffy(在本例中为 4ms)的周期分辨率;当代码正常工作时,我也不希望减少增量方差。所以在我看来,我没有“高分辨率计时器”的要求,也没有“硬实时”的要求......

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

  • LDD3:5.3。信号量和互斥量- (在描述具有不同需求的驱动程序时):不会从中断处理程序或其他异步上下文进行访问。没有特定的延迟(响应时间)要求;应用程序程序员了解 I/O 请求是通常不会立即满足
  • Documentation/timers/hrtimers.txt - " timers.c 代码围绕 jiffies 和 32 位假设进行了非常“严格编码”,并且针对相对狭窄的用例(在相对狭窄的 HZ 范围内的 jiffies)进行了磨练和微优化)多年来 - 因此即使是对其进行小的扩展也很容易打破轮子的概念
  • T. Gleixner, D. Niehaus Hrtimers and Beyond: Transforming the Linux Time Subsystems (pdf) - (最详细,另见内图)1997年实现的Cascading Timer Wheel(CTW)取代了原来的时间有序双链表,解决了链表O(N)插入时间的可伸缩性问题……Linux中当前的定时器管理方法做了一个很好地满足了极其广泛的需求,但它无法提供某些情况下所需的服务质量,因为它必须满足如此广泛的需求......超时相关的定时器保存在现有的定时器轮和一个实施了针对(高分辨率)计时器要求优化的新子系统 hrtimers。hrtimers 完全基于人类时间(单位:纳秒)...它们保存在按时间排序的每个 CPU 列表中,实现为红黑树。"
  • 高分辨率定时器 API [LWN.net] - 在其核心,hrtimer 机制保持不变。hrtimers 不是使用“定时器轮”数据结构,而是存在于一个时间排序的链表上,下一个定时器expire 位于列表的头部。一个单独的红/黑树也用于启用定时器事件的插入和删除,而无需扫描列表。但是虽然核心保持不变,但其他一切都发生了变化,至少表面上。
  • 软件中断和实时 [LWN.net] - softirq 机制旨在处理与处理硬件中断几乎 - 但不完全 - 一样重要的处理。Softirqs 以高优先级运行(尽管有一个有趣的例外,描述下面),但启用了硬件中断。因此,它们通常会抢占除了对“真实”硬件中断的响应之外的任何工作......但从 3.0 实时补丁集开始,该功能消失了......作为回应,在3.6.1-rt1,softirqs的处理又变了。
  • 高(但不是太高)分辨率超时 [LWN.net] - "_poll() 和 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*period = 8 ms 延迟)或 0 ms 延迟 - 我得到的捕获,否则与 OP 中第一张图像上显示的质量相同。现在,我当然可以进行更长的测试,更准确地了解它的可靠性——但这就是我期望/需要看到的这个简单案例的所有可靠性;与 OP 相比,我仅在 10 次循环中就会下降,并且有可能掷硬币 - 每第二次或第三次运行rerun.sh脚本,我会得到一个下降 - 即使在低操作系统资源使用的情况下!

最后,请注意下面的来源应该有问题,由@CL 发现。:“您的模块有问题:您必须确保在模块卸载之前计时器未挂起”,已修复(在 的上下文中hrtimer)。这似乎回答了我的额外问题,因为它消除了对脚本中任何一个 "MUSTHAVE"sleep的需要。rerun.sh但是,请注意,由于 200 个循环 @ 4 ms 需要 0.8 s -如果我们想要完整的 200 个滴答声捕获,则需要sleep介于两者之间(否则,在我的机器上,我只能捕获大约 7 个滴答声)。insmodrmmod

好吧,希望我现在就明白了(至少大多数情况下)——如果没有,欢迎指正:)

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");
于 2013-06-12T00:19:57.187 回答
2

确实有可能您的计时器函数被调用的时间比expires设置的要晚。可能的原因是调度延迟,其他驱动程序禁用中断太久(图形和 WLAN 驱动程序通常是罪魁祸首),或者一些糟糕的 BIOS 执行 SMI 代码。

如果您想避免一个迟到的计时器函数转移所有后续计时器调用,您必须安排相应的下一个计时器,而不是相对于当前时间 ( jiffies),而是相对于上一个计时器 ( my_timer.expires) 的计划时间。或者,使用十个计时器,您都在开始时安排在jiffies + 1, 2, 3, ...</p>

日志中的时间戳是该字符串打印到日志的时间。

您的模块有问题:您必须确保在卸载模块之前计​​时器未挂起。

于 2013-06-05T08:40:28.143 回答