下一页 上一页 目录

4. 高分辨率定时

4.1 延迟

首先,我应该说明,由于 Linux 的多任务特性,您无法保证用户模式进程对时间控制的精确性。您的进程可能在任何时候被调度出去,时间从大约 10 毫秒到几秒(在负载非常高的系统上)。然而,对于大多数使用 I/O 端口的应用程序来说,这实际上并不重要。为了尽量减少这种情况,您可能希望将您的进程 nice 化为一个高优先级值(请参阅 nice(2) 手册页)或使用实时调度(见下文)。

如果您想要比普通用户模式进程提供的更精确的定时,那么有一些针对用户模式“实时”支持的规定。Linux 2.x 内核具有软实时支持;有关详细信息,请参阅 sched_setscheduler(2) 的手册页。有一个特殊的内核支持硬实时;请参阅 http://luz.cs.nmt.edu/~rtlinux/ 以获取更多相关信息。

睡眠:sleep()usleep()

现在,让我从更简单的定时调用开始。对于几秒的延迟,您最好的选择可能是使用 sleep()。对于至少几十毫秒的延迟(大约 10 毫秒似乎是最小延迟),usleep() 应该可以工作。这些函数将 CPU 让给其他进程(“睡眠”),因此不会浪费 CPU 时间。有关详细信息,请参阅 sleep(3)usleep(3) 的手册页。

对于大约 50 毫秒以下的延迟(取决于您的处理器和机器的速度以及系统负载),放弃 CPU 会花费太多时间,因为 Linux 调度器(对于 x86 架构)通常至少需要大约 10-30 毫秒才能将控制权返回给您的进程。因此,在较小的延迟中,usleep(3) 通常会延迟比您在参数中指定的量稍长的时间,并且至少大约 10 毫秒。

nanosleep()

在 Linux 内核 2.0.x 系列中,有一个新的系统调用 nanosleep()(请参阅 nanosleep(2) 手册页),它允许您睡眠或延迟较短的时间(几微秒或更长时间)。

对于 <= 2 毫秒的延迟,如果(且仅当)您的进程设置为软实时调度(使用 sched_setscheduler()),nanosleep() 使用忙等待循环;否则它会像 usleep() 一样睡眠。

忙等待循环使用 udelay()(许多内核驱动程序使用的内部内核函数),循环的长度是使用 BogoMips 值计算的(这种忙等待循环的速度是 BogoMips 准确测量的东西之一)。有关其工作原理的详细信息,请参阅 /usr/include/asm/delay.h)。

使用端口 I/O 延迟

另一种延迟少量微秒的方法是端口 I/O。从端口 0x80 输入或输出任何字节(有关如何操作,请参见上文)应该等待几乎正好 1 微秒,这与您的处理器类型和速度无关。您可以多次执行此操作以等待几微秒。端口输出应该对任何标准机器都没有有害的副作用(并且一些内核驱动程序使用它)。这就是 {in|out}[bw]_p() 通常执行延迟的方式(请参阅 asm/io.h)。

实际上,在 0-0x3ff 范围内的大多数端口上的端口 I/O 指令几乎正好需要 1 微秒,因此,例如,如果您直接使用并行端口,只需从该端口执行额外的 inb() 以进行延迟。

使用汇编指令延迟

如果您知道程序将在其上运行的机器的处理器类型和时钟速度,则可以通过运行某些汇编指令来硬编码更短的延迟(但请记住,您的进程可能随时被调度出去,因此延迟可能偶尔会更长)。对于下表,内部处理器速度决定了所花费的时钟周期数;例如,对于 50 MHz 处理器(例如 486DX-50 或 486DX2-50),一个时钟周期需要 1/50000000 秒(=200 纳秒)。

Instruction   i386 clock cycles   i486 clock cycles
xchg %bx,%bx          3                   3
nop                   3                   1
or %ax,%ax            2                   1
mov %ax,%ax           2                   1
add %ax,0             2                   1

奔腾处理器的时钟周期应该与 i486 相同,但 Pentium Pro/II 除外,add %ax, 0 可能仅需 1/2 个时钟周期。有时它可以与另一条指令配对(由于乱序执行,这甚至不一定是指令流中的紧随其后的指令)。

表中的指令 nopxchg 应该没有副作用。其余指令可能会修改标志寄存器,但这应该没关系,因为 gcc 应该会检测到它。xchg %bx, %bx 是延迟指令的安全选择。

要使用这些指令,请在您的程序中调用 asm("instruction")。指令的语法与上表中的相同;如果您想在单个 asm() 语句中使用多条指令,请用分号分隔它们。例如,asm("nop ; nop ; nop ; nop") 执行四个 nop 指令,在 i486 或奔腾处理器上延迟四个时钟周期(或在 i386 上延迟 12 个时钟周期)。

asm() 由 gcc 转换为内联汇编代码,因此没有函数调用开销。

在 Intel x86 架构中,比一个时钟周期更短的延迟是不可能的。

rdtsc 用于奔腾处理器

对于奔腾处理器,您可以使用以下 C 代码(它执行名为 RDTSC 的 CPU 指令)获取自上次重启以来经过的时钟周期数


   extern __inline__ unsigned long long int rdtsc()
   {
     unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
   }

您可以在忙等待循环中轮询此值,以延迟您想要的任意多个时钟周期。

4.2 测量时间

对于精确到一秒的时间,使用 time() 可能是最简单的。对于更精确的时间,gettimeofday() 精确到大约一微秒(但请参阅上面关于调度的内容)。对于奔腾处理器,上面的 rdtsc 代码片段精确到一个时钟周期。

如果您希望您的进程在一段时间后收到信号,请使用 setitimer()alarm()。有关详细信息,请参阅这些函数的手册页。


下一页 上一页 目录