Clocks, timers, and getting a kick every so often

It's time to take a look at everything related to time in the BlackBerry 10 OS. We'll see how and why you use timers and the theory behind them. Then we'll take a look at getting and setting the realtime clock.

We are using a ticksize of 10 ms, but BlackBerry 10 OS now uses a 1 ms ticksize by default on most systems. This doesn't affect the substance of the issues being discussed.

Clocks and timers

Let's look at a typical system, say a car. In this car, we have a bunch of programs, most of which are running at different priorities. Some of these need to respond to actual external events (like the brakes or the radio tuner), while others need to operate periodically (such as the diagnostics system).

Operating periodically

So how does the diagnostics system operate periodically? You can imagine some process in the car's CPU that does something similar to the following:

// Diagnostics Process

int
main (void)     // ignore arguments here
{
    for (;;) {
        perform_diagnostics ();
        sleep (15);
    }
    
    // You won't get here.
    return (EXIT_SUCCESS);
}

Here we see that the diagnostics process runs forever. It performs one round of diagnostics and then goes to sleep for 15 seconds, wakes up, goes through the loop again, and again, ...

Way back in the dim, dark days of single-tasking, where one CPU was dedicated to one user, these sorts of programs were implemented by having the sleep (15); code do a busy-wait loop. You had to calculate how fast your CPU was and then write your own sleep() function:

void
sleep (int nseconds)
{
    long    i;

    while (nseconds--) {
        for (i = 0; i < CALIBRATED_VALUE; i++) ;
    }
}

In those days, since nothing else was running on the machine, this didn't present much of a problem, because no other process cared that you were hogging 100% of the CPU in the sleep() function.

Even today, we sometimes hog 100% of the CPU to do timing functions. Notably, the nanospin() function is used to obtain very fine-grained timing, but it does so at the expense of burning CPU at its priority. Use with caution!

If you did have to perform some form of multitasking, it was usually done via an interrupt routine that would hang off the hardware timer or be performed within the busy-wait period, somewhat affecting the calibration of the timing. This usually wasn't a concern.

Luckily we've progressed far beyond that point. Recall, from Scheduling and the real world, what causes the kernel to reschedule threads:

  • A hardware interrupt
  • A kernel call
  • A fault (exception)

We're concerned with the first two items on the list: the hardware interrupt and the kernel call.

When a thread calls sleep(), the C library contains code that eventually makes a kernel call. This call tells the kernel, put this thread on hold for a fixed amount of time. The call removes the thread from the running queue and starts a timer.

Meanwhile, the kernel has been receiving regular hardware interrupts from the computer's clock hardware. Let's say, for argument's sake, that these hardware interrupts occur at exactly 10-millisecond intervals.

Let's restate: every time one of these interrupts is handled by the kernel's clock interrupt service routine (ISR), it means that 10 ms have gone by. The kernel keeps track of the time of day by incrementing its time-of-day variable by an amount corresponding to 10 ms every time the ISR runs.

So when the kernel implements a 15-second timer, all it's really doing is:

  1. Setting a variable to the current time plus 15 seconds.
  2. In the clock ISR, comparing this variable against the time of day.
  3. When the time of day is the same as (or greater than) the variable, putting the thread back onto the READY queue.

When multiple timers are outstanding, as is the case if several threads all needed to be wakened at different times, the kernel queues the requests, sorting them by time order — the nearest one is at the head of the queue, and so on. The variable that the ISR looks at is the one at the head of this queue.

Clock interrupt sources

So where does the clock interrupt come from? Here's a diagram that shows the hardware components (and some typical values for a PC) responsible for generating these clock interrupts:

Diagram showing clock interrupts.

As you can see, there's a high-speed (MHz range) clock produced by the circuitry in the PC. This high-speed clock is then divided by a hardware counter (the 82C54 component in the diagram), which reduces the clock rate to the kHz or hundreds of Hz range (that is, something that an ISR can actually handle). The clock ISR is a component of the kernel and interfaces directly with the data structures and code of the kernel itself. On non-x86 architectures, a similar sequence of events occurs; some chips have clocks built into the processor.

Note that the high-speed clock is being divided by an integer divisor. This means the rate isn't going to be exactly 10 ms, because the high-speed clock's rate isn't an integer multiple of 10 ms. Therefore, the kernel's ISR in our example above might actually be interrupted after 9.9999296004 ms.

Big deal, right? Well, sure, it's fine for our 15-second counter. 15 seconds is 1500 timer ticks — doing the math shows that it's approximately 106 µs off the mark:

15 s - 1500 × 9.9999296004 ms
= 15000 ms - 14999.8944006 ms
= 0.1055994 ms
= 105.5994 µs

Unfortunately, continuing with the math, that amounts to 608 ms per day, or about 18.5 seconds per month, or almost 3.7 minutes per year!

You can imagine that with other divisors, the error could be greater or smaller, depending on the rounding error introduced. Luckily, the kernel knows about this and corrects for it.

The point of this story is that regardless of the nice round value shown, the real value is selected to be the next faster value.

Base timing resolution

Let's say that the timer tick is operating at just slightly faster than 10 ms. Can you reliably sleep for 3 milliseconds? Nope.

Consider what happens in the kernel. You issue the C-library delay() call to go to sleep for 3 milliseconds. The kernel has to set the variable in the ISR to some value. If it sets it to the current time, this means the timer has already expired and that you should wake up immediately. If it sets it to one tick more than the current time, this means that you should wake up on the next tick (up to 10 milliseconds away).

The moral of this story is: Don't expect timing resolution any better than the input timer tick rate.

Getting more precision

Under BlackBerry 10 OS, a program can adjust the value of the hardware divisor component in conjunction with the kernel (so that the kernel knows what rate the timer tick ISR is being called at).

Timing jitter

There's one more thing you have to worry about. Let's say the timing resolution is 10 ms and you want a 20 ms timeout. Are you always going to get exactly 20 milliseconds worth of delay from the time that you issue the delay() call to the time that the function call returns?

Absolutely not.

There are two good reasons why. The first is fairly simple: when you block, you're taken off the running queue. This means that another thread at your priority may now be using the CPU. When your 20 milliseconds have expired, you are placed at the end of the READY queue for that priority so you are at the mercy of whatever thread happens to be running. This also applies to interrupt handlers running or higher-priority threads running — just because you are READY doesn't mean that you're consuming the CPU.

The second reason is a bit more subtle. The following diagram helps explain why:

Diagram showing clock jitter.

The problem is that your request is asynchronous to the clock source. You have no way to synchronize the hardware clock with your request. Therefore, you get from just over 20 milliseconds to just under 30 milliseconds worth of delay, depending on where in the hardware's clock period you started your request.

This is a key point. Clock jitter is a sad fact of life. The way to get around it is to increase the system's timing resolution so your timing is within tolerance. (See how to do this in Getting and setting the realtime clock .) Keep in mind that jitter takes place only on the first tick — a 100-second delay with a 10-millisecond clock delays for greater than 100 seconds and less than 100.01 seconds.

Types of timers

The type of timer that we showed you previously is a relative timer. The timeout period selected is relative to the current time. If you want the timer to delay your thread until January 20, 2005 at 12:04:33 EDT, you have to calculate the number of seconds from now until then, and set up a relative timer for that number of seconds. Because this is a fairly common function, BlackBerry 10 OS implements an absolute timer that delays until the specified time (instead of for the specified time, like a relative timer).

What if you want to do something while you're waiting for that date to come around? Or, what if you want to do something and get a kick every 27 seconds? You certainly can't afford to be asleep!

As we discussed in Processes and threads, you can start up another thread to do the work, and your thread can take the delay. However, since we're talking about timers, we'll look at another way of doing this.

You can do this with a periodic or one-shot timer, depending on your objectives. A periodic timer is one that goes off periodically, notifying the thread (over and over again) that a certain time interval has elapsed. A one-shot timer is one that goes off just once.

The implementation in the kernel is still based on the same principle as the delay timer that we used in our first example. The kernel takes the absolute time (if you specified it that way) and stores it. In the clock ISR, the stored time is compared against the time of day in the usual manner.

However, instead of your thread being removed from the running queue when you call the kernel, your thread continues to run. When the time of day reaches the stored time, the kernel notifies your thread that the designated time has been reached.

Last modified: 2014-11-17



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus