我在一家生产自动机器的公司工作,我帮助维护他们控制机器的软件。该软件运行在实时操作系统上,由并发运行的多个线程组成.代码库是遗留的,并且有大量的技术债务。在代码库显示的所有问题中,有一个在我看来是相当奇怪的;大多数涉及计算经过的时间以实现诸如超时、延迟、记录特定状态下的时间等常见时间特性的计时算法基本上采取以下形式:
unsigned int shouldContinue = 1;
unsigned int blockDuration = 1; // Let's say 1 millisecond.
unsigned int loopCount = 0;
unsigned int elapsedTime = 0;
while (shouldContinue)
{
.
. // a bunch of statements, selections and function calls
.
blockingSystemCall(blockDuration);
.
. // a bunch of statements, selections and function calls
.
loopCount++;
elapsedTime = loopCount * blockDuration;
}blockingSystemCall函数可以是为指定的blockDuration挂起当前线程的任何操作系统的API。elapsedTime变量随后通过基本上将loopCount乘以blockDuration或任何等效算法来计算。
对我来说,这种定时算法是错误的,在大多数情况下是不可接受的。循环中的所有指令,包括循环的条件,都是顺序执行的,每条指令都需要可测量的CPU时间来执行。因此,在循环开始后,在任何给定实例中,实际运行的时间都严格大于elapsedTime的值。因此,假设执行循环中所有语句所需的CPU时间(以d表示)是常数。然后,elapsedTime滞后于loopCount·d对任何loopCount >0所经过的实际时间;也就是说,偏差是根据算术级数增长的。这设置了偏差的下限,因为实际上,根据其他因素,线程调度和时间切片会导致额外的延迟。
事实上,就在不久前,当我们测试一种依赖于机器运行时间的数据驱动的预测维护功能时,我们发现软件报告的操作时间在机器连续运行两天后,比标准参考时钟慢了3小时。正是通过这个测试,我发现了上面概述的算法,我很快就确定了这个算法的根本原因。
在一个背景中,我曾经使用计时器中断在裸金属系统上实现计时算法,这允许CPU在计时器进程并行运行时继续执行业务逻辑,但令我震惊的是,我发现在行业中使用了导言中概述的算法来计算运行时间,尤其是当一个典型的操作系统已经将计时器功能封装成各种易于使用的公共API的形式时,将程序员从通过硬件寄存器配置定时器、通过中断服务例程引发事件的麻烦中解放出来。
上述框架代码中所示的定时算法至少在两个代码库中找到,这两个代码库由位于两个不同城市的两个子公司的两个不同的软件工程团队独立开发,尽管处于同一状态。这令我怀疑,这究竟是业界通常的做法,还是只是个别的个案,并不普遍。
因此,问题是,上面所示的算法在计算运行时间时是通用的还是可以接受的,考虑到底层操作系统已经提供了高度优化的时间管理系统调用,这些调用可以立即用于精确测量经过的时间,或者甚至用作创建更高层次的计时设施的基本构建块,从而提供类似于计时器类 in C#的更直观的方法?
发布于 2022-07-15 05:14:53
你说得对,用这种方法计算经过的时间是不准确的--因为它假定阻塞调用将占用所指示的时间,并且阻塞系统调用之外发生的一切都不会花费时间,这只有在一台速度无限快的机器上才是正确的。由于实际机器不是无限快的,因此以这种方式计算的时间总是比实际运行的时间要少一些。
至于这是否可以接受,这将取决于您的程序需要多少计时精度。如果它只是做一个粗略的估计,以确保一个函数不会运行“太长”,这可能是可以的。如果OTOH正在尝试精确性(特别是在很长一段时间内的准确性),那么这个方法就不会提供这个功能。
使用更常见(更准确)的方法来测量经过的时间,如下所示:
const unsigned int startTime = current_clock_time();
while (shouldContinue)
{
loopCount++;
elapsedTime = current_clock_time() - startTime;
}这样做的优点是不会随着时间的推移而“偏离”精确的值,但它确实假定您有可用的current_clock_time()类型的函数,在循环中调用它是可以接受的。(如果current_clock_time()非常昂贵,或者没有提供调用例程所需的一些实时性能保证,这可能是不这样做的原因之一)
发布于 2022-07-15 14:16:20
我不认为这些循环能像你想的那样做。
在RTOS中,这样的循环的目的通常是定期执行任务。
blockingSystemCall(N)可能不会像你想象的那样只睡上N毫秒。它可能会在线程最后一次醒来后的N毫秒后才能入睡。
更准确地说,您的线程自启动以来所执行的所有休眠都添加到线程启动时间,以获得操作系统试图唤醒线程的时间。如果您的线程由于I/O事件而醒来,那么可以使用最后一次,而不是线程启动时间。关键是,所有这些启动时间中的不准确之处都得到了纠正,因此您的线程每隔一段时间就会醒来,并且根据RTOS主时钟,经过的时间测量是完全准确的。
除了简单性外,还可以有很好的理由用RTOS主时钟来测量经过的时间,而不是更精确的挂钟时间。这是因为RTOS提供的所有保证(这就是您首先使用RTOS的原因)都是在这个时间范围内提供的。一个任务所花费的时间会影响到您保证可以用于其他任务的时间量,如此时钟所测量的。
您的RTOS主时钟每2天慢运行3小时,这可能是一个问题,也可能不是一个问题。
https://stackoverflow.com/questions/72989112
复制相似问题