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

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

... 我就像:“哇啊啊啊啊啊……等一下……” - 序列中是否有一个脉冲被省略了?这意味着add_timer 漏掉了一个插槽,然后在下一个4毫秒插槽中启动了该函数?

有趣的是,在运行这些测试时,我只打开了终端、网络浏览器和文本编辑器 - 所以我无法看到任何可能占用操作系统/内核的运行情况;因此,我真的看不出内核为什么会出现如此大的误差(整个嘀嗒周期)。当我阅读有关Linux内核时间的文献时,例如“所有定时器中最简单且最不准确的是定时器API”,我把“最不准确”理解为:“不要期望 完全 的4毫秒周期”(根据此示例)- 我也不期望如此,我对(第一个)直方图中显示的方差感到满意;但我不期望整个周期会被省略!?

因此,我的问题是:

  • add_timer在这个分辨率下的表现是否符合预期(偶尔会漏掉一个周期)?
  • 如果是,有没有一种方法可以“强制”add_timer在这个平台上按照每个4ms插槽指定的方式触发函数?
  • 是否可能会获得“错误”的时间戳 - 例如反映实际“打印”到系统日志发生的时间戳,而不是函数实际触发的时间戳?
  • 请注意,我不希望超过对应于嘀嗒的周期分辨率(在本例中为4ms);也不希望在代码正常工作时减少增量方差。因此,在我看来,我没有“高分辨率计时器”需求,也没有“硬实时”需求 - 我只想让add_timer可靠地触发。在这种平台上是否可能实现,而无需诉诸内核的特殊“实时”配置?

附加问题:在下面的rerun.sh中,您会注意到两个标有MUSTHAVEsleep;如果其中任何一个被忽略/注释掉,操作系统/内核就会冻结,并需要进行硬重启。我看不出原因 - 真的可能在从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

代码:

$ 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的评论启发,我尝试使用dd通过call_usermodehelper/proc/schedstat/proc/sched_debug获取调度器统计信息;注意,大多数情况下会“跳过”(也就是说,由于函数的第7次、第6次或X次运行而导致文件丢失); 但我设法获得了两次完整的运行,并将它们发布在https://gist.github.com/anonymous/5709699上(因为我注意到在SO上可能更喜欢使用gist而不是pastebin),鉴于输出非常庞大; *_11*文件记录了一次正确的运行,*_17*文件记录了一次“丢失”的运行。

请注意,我还切换到模块中的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.: 非常感谢您的答案。很高兴得到确认,"可能会在后面的jiffy中调用定时器函数";通过记录时间片(jiffies),我也意识到定时器函数在稍晚的时间被调用 - 除此之外,并没有任何 "错误"。

了解时间戳(timestamps)是很好的事情;我想知道是否有可能:定时器函数在正确的时间触发,但内核抢占了内核日志服务(我相信它是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

...所以,基本上我在+8毫秒时隔了一段时间 / “掉落”(应该是@3272446个jiffies),然后在+12毫秒时刻(应该是@3272447个jiffies)运行了两个函数;您甚至可以看到由于此原因,在绘图上的标签“更加粗体”。从这个意义上说,这更好,因为“掉落”序列现在与一个正确的、非丢失序列同步了(正如您所说:“为了避免一个延迟的计时器函数移动所有后续计时器调用”)。但是,我还是错过了节拍;而因为我需要在每个节拍上写入字节到硬件中,以保持持续的、恒定的传输速率,所以这对我帮助不大。

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


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

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

感谢您的评论,@CL.——“…提前安排(等时)…”消除了我所拥有的困惑。我最终的目标是针对FT232,它只有BULK模式-只要每个计时器命中的字节数较低,我实际上可以通过add_timer“欺骗”我的方式在“流式传输”中传递数据;然而,当我传输接近消耗带宽的字节数时,这些“误火”开始变得明显起来。因此,我对测试其极限很感兴趣,为此我需要一个可靠的重复“计时器”函数-还有其他任何我可以尝试以获得可靠“计时器”的方法吗? –sdaau 6月5日12:27

@sdaau,大容量传输不适用于流媒体。你不能通过使用另一种软件定时器来解决硬件协议中的缺陷。– CL. 6月5日13:50

...而作为我对@CL的回应:我知道我无法修复缺陷;我更感兴趣的是观察这些缺陷--比如,如果一个内核函数进行周期性的USB写操作,我可以在示波器/分析仪上观察信号,并希望看到何种意义上块模式不适用。但首先,我必须相信这个函数能够(至少有点)可靠地以周期性的速率重复执行(即“生成”时钟/节拍)-而直到现在,我并不知道我不能真正相信add_timer以jiffies分辨率(因为它很容易跳过整个周期)。然而,移动到Linux的高分辨率定时器(hrtimer)确实为我提供了一个可靠的周期函数,在这个意义上解决了我的问题(在我下面的答案中发布)。


1
打印调度器统计数据可能会有趣,以查看您的两个“情况”是否被长时间抢占。 - granquet
感谢您的评论,@granquet - 不幸的是,我不知道如何“打印调度器统计信息”;如果有任何指针,那就太好了!还要感谢您提到了“抢占长” - 我以前听过这个词,但我目前还不知道它是什么意思;一定会查找一下。干杯! - sdaau
嗨@granquet-我刚试着在每次定时器函数调用时从/proc/schedstat/proc/sched_debug中读取,并在两种情况下收集它;请参见编辑结束的原始帖子,其中包含日志链接。这是你所说的打印“调度器统计信息”吗?我能找到的少数几个相关链接是sched-stats.txtLinux Scheduler Statistics;然后,我最好读取/proc。干杯! - sdaau
1
抱歉,我没有时间详细查看你的转储(也许今天晚些时候)。只是一个快速的评论:计时器在软中断上下文中运行,这意味着它们具有最高优先级,并且会抢占CPU上运行/可运行的所有内容...但硬件中断在服务软中断时未被禁用。因此,您可能(最有可能的解释)会偶尔出现硬件中断,从而抢占您的计时器...因此您会得到一个未能在正确时间处理的中断。 - granquet
不用担心,@granquet - 我知道这些日志确实很冗长,我并不指望任何人深入研究它们;所以你在评论中提供的任何提示都非常受欢迎:例如,我之前不知道软中断/硬件中断的区别,现在我知道应该去了解一下。谢谢! - sdaau
2个回答

11

非常感谢所有的评论和答案;它们都指出了必须考虑的事情 - 但是由于我有点像永远的新手,所以我仍然需要做更多的阅读,才能获得一些理解(我希望是正确的)。此外,我确实找不到任何特定于周期性“滴答”函数的东西 - 因此我将在这里发布一个更详细的答案。

简而言之 - 对于分辨率为jiffy的可靠周期性Linux内核函数,请勿使用add_timer<linux/time.h>),因为它可能会“丢失”整个周期;改用高分辨率计时器(<linux/hrtimer.h>)。更详细地说:

我是否可能得到“错误”的时间戳 - ...?

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

所以,也许这是可能的 - 但事实证明,这不是问题所在:

在这个分辨率下,add_timer会偶尔错过周期性,这是否符合预期行为?
我猜是的:
如果是这样,有没有一种方法可以“强制”add_timer在此平台上的jiffy指定的每个4ms间隔触发函数呢?
...再次猜测,结果是否定的。
现在,这背后的原因有些微妙,如果我理解错了,希望有人能纠正我。首先,我一开始存在的一个误解是,“时钟只是时钟”(在计算机代码实现的意义上),但那不完全正确。内核基本上必须在某处“排队”事件,每当使用像add_timer这样的东西时;而这个请求可能来自任何东西:任何(和所有)类型的驱动程序,甚至可能来自用户空间。
问题在于这种“排队”成本很高 - 除了内核必须处理(相当于)遍历和插入(和删除)数组中的项目之外,它还必须处理跨越几个数量级的计时器延迟(从毫秒到可能达到10秒);而且一些驱动程序(例如网络协议的驱动程序)显然会排队大量计时器事件,这些事件通常在运行之前被取消 - 而其他类型则可能需要完全不同的行为(就像在我的情况下 - 在周期性函数中,您期望大多数时间事件通常不会被取消;并且您也一个接一个地排队事件)。除此之外,内核还需要为单处理器、SMP和多处理器平台处理此问题。因此,在内核中实现计时器处理涉及成本效益权衡。
原来,围绕着jiffies/add_timer的架构是为了处理最常见的设备而设计的-对于它们来说,以jiffy为分辨率的精度不是问题;但这也意味着使用这种方法无法期望以单个jiffy为分辨率的可靠定时器。这还受到内核处理这些“事件队列”的方式的影响,内核将其(在某种程度上)视为中断服务请求(IRQ);而且内核中有几个优先级级别用于处理IRQ,其中更高优先级的例程可以抢占较低优先级的例程(即:中断并挂起较低优先级的例程,即使它正在执行 - 并允许更高优先级的例程继续进行)。或者,如前所述: @granquet:定时器在软中断上下文中运行,这意味着它们具有最高的优先级,并且会抢占CPU上运行/可运行的所有任务...但是当处理软中断时,硬件中断没有被禁用。因此,你可能会偶尔遇到一个硬件中断来抢占你的定时器...因此导致中断没有在正确的时间被处理。 @CL.:实际上,你的定时器函数被调用的时间可能晚于设置的到期时间。可能的原因是调度延迟、其他驱动程序长时间禁用中断(图形和WLAN驱动程序通常是罪魁祸首),或者一些糟糕的BIOS执行SMI代码。
我现在也这样认为-我认为这可能是发生的情况的一个例证。
  • jiffies 改变为,比如说,10000(== 250 Hz @40000 ms)
  • 假设计时器函数(由add_timer排队)即将开始运行 - 但尚未开始运行
  • 假设此处,网络卡出于某种原因生成硬件中断
  • 硬件中断具有更高的优先级,触发内核抢占(停止和挂起)计时器函数(可能已经启动,并且只有少量指令);
  • 这意味着内核现在必须重新安排计时器函数,在稍后的时间点运行 - 由于内核中只使用整数操作,并且此类事件的时间分辨率以jiffies为单位 - 它所能做的最好的事情是将其重新安排为jiffies +1(10001 == 250 Hz @40004 ms)
  • 现在,内核将上下文切换到网络卡驱动程序的IRQ服务例程,并进行其业务
  • 假设IRQ服务例程在200微秒内完成 - 这意味着现在我们(在“绝对”术语中)在40000.2毫秒 - 但是,我们还是在10000个jiffies
  • 如果内核现在将上下文返回到计时器函数,则它将在没有我必须注意到延迟的情况下完成;
  • ...但是,这不会发生,因为计时器函数已安排到下一个jiffy!
  • 因此,内核将在接下来的约3.8毫秒内进行其业务(可能处于睡眠状态)
  • jiffies 改变为10001(== 250 Hz @40004 ms)
  • (先前重新安排的)计时器函数运行 - 这次没有中断就完成了

我并没有进行详细的分析以确定事件序列是否与上述描述完全相同;但我相信它是接近的 - 换句话说,这是一个解决问题 - 特别是因为高分辨率计时器方法似乎没有显示出这种行为。确实很好获得调度程序日志,并知道导致抢占的确切原因 - 但我怀疑在 OP 编辑中尝试响应 @granquet 的评论所做的用户空间往返不是正确的做法。

无论如何,回到这个问题:

请注意,我不需要低于对应于 jiffy(在本例中为 4ms)的周期分辨率;我也不希望在代码正常工作时减少 delta 方差。因此,我认为我没有“高分辨率计时器”需求,也没有“硬实时”需求...

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

  • LDD3: 5.3. Semaphores and Mutexes -(描述具有不同需求的驱动程序):"没有来自中断处理程序或其他异步上下文的访问。没有特定的延迟(响应时间)要求; 应用程序员知道 I/O 请求通常不会立即得到满足"
  • Documentation/timers/hrtimers.txt - " timers.c 代码非常紧密地编码在 jiffies 和 32 位假设周围,并且已经被磨练和微调了很多年,适用于相对狭窄的使用情况(jiffies 在相对狭窄的 HZ 范围内),因此即使是对其进行小的扩展也容易破坏轮子概念。"
  • T. Gleixner, D. Niehaus Hrtimers and Beyond: Transforming the Linux Time Subsystems (pdf) -(最详细,也请参见内部图表):"1997 年实施的级联计时器轮(CTW)取代了原始的按时间排序的双向链表,以解决链表 O(N)插入时间的可扩展性问题... Linux 中的超时相关计时器保留在现有的计时器轮中,并实现了一个用于(高分辨率)计时器要求的新子系统 hrtimers。hrtimers 完全基于人类时间(单位:纳秒)... 它们被保留在按时间排序的每个 CPU 列表中,实现为红黑树。"
  • The high-resolution timer API [LWN.net] - "在其核心部分,hrtimer 机制保持不变。它们不再使用“定时器轮”数据结构,而是生活在按时间排序的链接列表上,下一个到期的计时器位于列表的开头。单独的红/黑树也用于使计时器事件的插入和删除无需扫描整个列表。但是,尽管核心保持不变,但几乎所有其他内容都发生了变化,至少表面上发生了变化。"
  • Software interrupts and realtime [LWN.net] - "软中断机制旨在处理几乎与处理硬件中断一样重要的处理。软中断以高优先级运行(虽然有一个有趣的例外,下面会描述),但启用硬件中断。因此,它们通常会抢占除响应“真正”的硬件中断之外的任何工作... 从 3.0 实时补丁集开始,这种能力消失了... 作为回应,在 3.6.1-rt1 中,软中断的处理再次发生了变化。"
  • High- (but not too high-) resolution timeouts [LWN.net] - "_poll()和 epoll_wait()采用整数毫秒数;select()采用具有微秒分辨率的 struct timeval,并且 ppoll()和 pselect()采用具有纳秒分辨率的 struct timespec。然而,它们都相同,即将此超时值转换为 jiffies,分辨率最大为一到十毫秒之间。程序员可能会使用 10 纳秒的超时时间调用 pselect(),但即使在 CPU 没有争用的情况下,该调用也可能要等待 10 毫秒才能返回......

    从引用中可以清楚地看出,高分辨率计时设施仍在内核中积极开发(伴随着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微秒为最大偏差,它仅代表预期周期的100 *(350/4000)= 8.75%;我当然对此没有问题。此外,我从未遇到过丢失(或相应地,整个2 *周期= 8毫秒延迟)或0毫秒延迟 - 我得到的捕获结果,除了OP中第一张图所示的质量外,其他都很好。现在,当然我可以运行更长的测试并更精确地了解其可靠性 - 但这就是我对于这种简单情况所期望/需要看到的所有可靠性;与OP相比,在那里我只需进行10次循环即可出现丢失,并且在低操作系统资源使用情况下,每秒或第三次运行rerun.sh脚本时,我都会出现丢失的概率 - 这是一个对比!

    最后,注意下面的源代码应该有一个问题,由@CL.发现:“您的模块有漏洞:在卸载模块之前,必须确保计时器未挂起”,已经在hrtimer的上下文中修复。这似乎回答了我的奖励问题,因为它消除了rerun.sh脚本中任何一个“MUSTHAVE”sleep的需要。然而,请注意,由于200个循环@ 4毫秒需要0.8秒-如果我们想要完整的200个tick捕获(否则,在我的机器上,我只能捕获7个tick),则需要在insmodrmmod之间进行sleep

    好的,希望我现在理解得对(至少大部分是)-如果不是,欢迎更正:)

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

2

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

如果您想避免一个延迟的计时器函数导致所有后续计时器调用都发生偏移,您必须将下一个计时器相应地安排在上一个计时器的预定时间(my_timer.expires)而不是相对于当前时间(jiffies)。 或者,使用十个计时器,在jiffies + 123等开始时安排所有计时器。

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

您的模块有漏洞:您必须确保在模块卸载之前计时器没有挂起。


非常感谢您的回复,@CL。感谢您报告错误,我之前不知道;我会确保它最终被修复。关于您其他评论,我已经在这篇帖子中发布了更详细的回复。再次感谢 - 干杯! - sdaau
啊,我现在明白了——可能是因为有个 bug(在模块卸载前没有检查计时器是否挂起),所以当我尝试在 insmodrmmod 调用之间不加 sleep 1 运行 rerun.sh 脚本时,操作系统/内核会冻结;通过引入这个延迟,我(可能)确保在模块被 rmmod 卸载时没有计时器挂起,从而掩盖了这个 bug。希望我理解得没错 :) ——干杯! - sdaau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接