Time Management in System Programming

Fundamental Concepts of Time

The kernel measures the passage of time using three distinct metrics:

  • Wall time (real time): The actual time and date in the real world. Useful for interfacing with users and timestamping events.
  • Process time: The time a process spends executing on a processor.
    • User time: Time spent executing the process’s own code.
    • System time: Time the kernel spends executing on behalf of the process.
  • Monotonic time: A strictly linearly increasing time source, such as system uptime. Immune to user modification or clock skew adjustments. Optimal for calculating relative time differences.

Time representations fall into two formats:

  • Relative time: A value measured relative to a benchmark, such as the current instant.
  • Absolute time: A specific, fixed point in time. Represented in Unix systems as the number of elapsed seconds since the epoch (00:00:00 UTC on January 1, 1970).

The kernel tracks the progression of time via a software clock, managed by a periodic system timer.

  • Tick (jiffy): The unit of time corresponding to a single timer interval.
  • Jiffies counter: A 64-bit counter tracking elapsed ticks since boot.
  • HZ: The frequency of the system timer, varying by architecture and kernel version (e.g., 100, 250, or 1000 hertz). Higher values provide greater resolution but incur increased timer overhead.

To manage and manipulate these fundamental time representations, the system defines specific data structures that provide varying levels of precision.

Time Data Structures

System interfaces utilize a hierarchy of data structures to represent time, moving from second-level precision to nanosecond-level precision.

  • time_t: An integer type (usually long) representing the number of elapsed seconds since the epoch.
  • struct timeval: Provides microsecond precision.
    • tv_sec: Seconds.
    • tv_usec: Microseconds (suseconds_t).
  • struct timespec: Provides nanosecond precision, replacing timeval in modern interfaces.
    • tv_sec: Seconds.
    • tv_nsec: Nanoseconds (long).
  • struct tm: Represents “broken-down” time for human-readable calendar date conversion.
    • Contains fields for seconds (tm_sec), minutes (tm_min), hour (tm_hour), day of month (tm_mday), month (tm_mon), year since 1900 (tm_year), day of week (tm_wday), day in year (tm_yday), and daylight savings flag (tm_isdst).
  • clock_t: An integer type representing process time in clock ticks.

These precision data structures serve as the underlying types for the POSIX clock interfaces, which standardize discrete timekeeping sources.

POSIX Clocks

POSIX defines a standard for representing and querying system time sources, identified by the clockid_t type. Linux supports five primary clocks:

  • CLOCK_REALTIME: The system-wide real time (wall time) clock.
  • CLOCK_MONOTONIC: A monotonically increasing clock representing elapsed time since system boot. Not settable by user processes.
  • CLOCK_MONOTONIC_RAW: A Linux-specific monotonic clock ineligible for slewing (correction for clock skew).
  • CLOCK_PROCESS_CPUTIME_ID: A high-resolution, per-process clock measuring execution time.
  • CLOCK_THREAD_CPUTIME_ID: A high-resolution, per-thread execution clock.

The resolution of a specific POSIX clock is obtained using clock_getres():

  • Returns the resolution in a struct timespec.
  • Resolutions for CLOCK_REALTIME and CLOCK_MONOTONIC tie directly to the frequency of the system timer.
  • Resolutions for CLOCK_PROCESS_CPUTIME_ID rely on hardware registers (e.g., the TSC on x86) and often provide true nanosecond precision.

POSIX clocks form the foundation for accurately retrieving the current time at the required level of precision.

Retrieving Time

Interfaces for retrieving the current time correspond directly to the data structures introduced earlier, offering increasing degrees of precision.

  • time(): Returns the current absolute time as a time_t (seconds since the epoch).
  • gettimeofday(): Returns the current absolute time in a struct timeval (microsecond resolution). The timezone parameter is obsolete and should be passed as NULL.
  • clock_gettime(): Returns the current time of a specified clockid_t source in a struct timespec (nanosecond resolution).
  • times(): Retrieves process time for the invoking process and its waited-upon children, populating a struct tms.
    • Separates values into user time (tms_utime), system time (tms_stime), child user time (tms_cutime), and child system time (tms_cstime).

Conversely, privileged processes use a corresponding set of interfaces to modify the system time.

Setting the System Time

Setting the system time requires the CAP_SYS_TIME capability (typically held by root). The interfaces mirror the retrieval functions:

  • stime(): Sets the system time using a time_t value.
  • settimeofday(): Sets the system time using a struct timeval value.
  • clock_settime(): Sets the system time for a specified clockid_t source using a struct timespec. Only CLOCK_REALTIME is generally settable.

Raw time values retrieved or set by these functions often require conversion to human-readable strings or calendar components for application logic.

Time Conversion and Manipulation

The C library provides a family of functions to translate between time_t epoch seconds and struct tm broken-down time, as well as ASCII strings.

  • gmtime() / gmtime_r(): Converts time_t to a struct tm expressed in UTC.
  • localtime() / localtime_r(): Converts time_t to a struct tm expressed in the user’s local time zone.
  • mktime(): Converts a local struct tm into a time_t epoch value.
  • asctime() / asctime_r(): Converts a struct tm into an ASCII string.
  • ctime() / ctime_r(): Converts a time_t directly into an ASCII string.
  • difftime(): Calculates the difference in seconds between two time_t values, returning a double.
  • Thread Safety Constraint: Functions lacking the _r suffix utilize statically allocated memory and are inherently thread-unsafe. Multithreaded programs must use the _r variants.

While absolute time can be set directly via the earlier interfaces, maintaining system clock synchronization without disrupting time-dependent applications requires gradual adjustments.

Tuning the System Clock

Abrupt jumps in wall clock time negatively impact applications tracking absolute time (e.g., build systems like make evaluating file modification timestamps). The system combats clock skew dynamically without disruptive jumps.

  • Slewing: The gradual adjustment of the system clock to converge on a corrected time, ensuring the clock remains monotonically increasing.
  • adjtime(): Adjusts the system time gradually by a delta provided in a struct timeval.
    • Positive delta: Kernel speeds up the system clock.
    • Negative delta: Kernel slows down the system clock.
  • adjtimex(): Implements the complex RFC 1305 clock-adjustment algorithm using a struct timex. Modifies phase-locked loop (PLL) parameters, frequencies, and tolerances.

Beyond managing the underlying system clock, applications frequently rely on time tracking to suspend execution or trigger future events.

Sleeping and Waiting

Processes utilize sleep interfaces to suspend execution for a targeted delay. Modern interfaces utilize the timespec structure and POSIX clocks to achieve nanosecond precision and prevent race conditions.

  • sleep(): Suspends execution for a requested number of seconds.
  • usleep(): Suspends execution for a requested number of microseconds. (Deprecated in modern POSIX).
  • nanosleep(): Suspends execution with nanosecond resolution using a struct timespec.
    • Resilient to signal interruptions: Returns the remaining unslept time in an output struct timespec parameter, allowing the process to safely resume the sleep.
  • clock_nanosleep(): Advanced sleep utilizing clockid_t time sources.
    • Supports relative sleep (default) or absolute sleep via the TIMER_ABSTIME flag.
    • Absolute Sleep advantage: Eliminates the race condition between calculating a relative delay and initiating the sleep. The process wakes precisely when the target clock reaches the absolute timestamp.
  • Timer Overruns: Occur when the timer granularity is coarser than the requested sleep interval. A timer with period produces an average overrun of .

When simple execution suspension is insufficient, asynchronous timer notifications provide precise scheduling for background events.

Timers

Timers notify a process when a specific delay has elapsed, dispatching an event asynchronously (typically a signal).

  • alarm(): The simplest timer. Schedules a SIGALRM signal after a specified number of seconds.
  • Interval Timers: Armed via setitimer() and queried via getitimer(). Utilize struct itimerval to specify both an initial expiration (it_value) and a repeating interval (it_interval). Interval timers operate in three modes:
    • ITIMER_REAL: Measures real time. Dispatches SIGALRM.
    • ITIMER_VIRTUAL: Decrements only while process user-space code executes. Dispatches SIGVTALRM.
    • ITIMER_PROF: Decrements during both user-space and kernel-space execution. Dispatches SIGPROF.
  • POSIX Advanced Timers: Deconstructs timer operations into creation, initialization, and deletion, utilizing clockid_t sources.
    • timer_create(): Instantiates a timer. Configures the expiration notification via a struct sigevent.
      • SIGEV_NONE: Silent expiration.
      • SIGEV_SIGNAL: Dispatches a specific signal, allowing custom payload integration.
      • SIGEV_THREAD: Spawns a new POSIX thread to execute a provided callback function.
    • timer_settime(): Arms the timer using a struct itimerspec. Supports absolute expirations via TIMER_ABSTIME.
    • timer_gettime(): Retrieves the remaining time until expiration.
    • timer_getoverrun(): Retrieves the count of timer expirations that occurred between the initial expiration and the process notification, mitigating issues caused by scheduling delays or signal masking.
    • timer_delete(): Destroys the timer and frees kernel resources.