Decoded: timeout (coreutils)

[Back to Project Main Page]

Note: This page explores the design of command-line utilities. It is not a user guide.
[GNU Manual] [No POSIX requirement] [Linux man] [FreeBSD man]

Logical flow of timeout command (coreutils)

Summary

timeout - run a command with a time limit

[Source] [Code Walkthrough]

Lines of code: 578
Principal syscalls: fork(), execvp()
Support syscall: None
Options: 10 (3 short, 7 long)

Added to Coreutils in June 2008 [First version]
Number of revisions: 79 [Code Evolution]

The problem behind timeout is to execute a separate command while retaining external control. This problem is distinct from other 'loader' utilities such as nice, chroot, and env. The solution is to keep both processes alive using fork() with custom signal handlers for interprocess communication.

Helpers:
  • apply_time_suffix() - Processes an input time with a character multiplier suffix
  • block_cleanup_and_chld() - Stops the new signal handlers from firing
  • chld() - New (trivial) signal handler for SIGCHLD when child returns
  • cleanup() - New signal handler for both parent (significant) and child (trivial exit)
  • disable_core_dumps() - Attempts to prevent core dumps (unsets PR_SET_DUMPABLE)
  • install_cleanup() - Installs the new cleaup handler
  • install_sigchld() - Installs the SIGCHLD handler
  • parse_duration() - Handles time strings with suffix
  • send_sig() - Wrapper for parent to send signal to a process
  • settimeout() - Sets the timer alarm for the parent process
  • unblock_signal() - Ensures that the provided signal can be handled
External non-standard helpers:
  • die() - Exit with mandatory non-zero error and message to stderr
  • error() - Outputs error message to standard error with possible process termination
  • operand2sig() - Converts operand (number/name) to a number and fills in the name

Setup

timeout declares operating parameters as globals, including:

  • command - The target command to execute
  • foreground - Flag set if the command runs in the foreground
  • kill_after - The duration after which to kill the command
  • monitored_pid - The pid of the command to execute
  • preserve_status - Flag set if we should pass on the command return value
  • term_signal - The signal to send the command at timeout
  • timed_out - Flag set if the command ran beyond the timeout period
  • verbose - Flag set if the user wants a timeout notification

main() initializes the following:

  • c - The letter of the next option to process
  • signame - The string name of the signal requested by the user
  • timeout - The timeout value from the user after post-processing (time suffix, etc)

Parsing

Parsing builds the operating parameters from the following questions:

  • What signal should be sent to the child process at termination?
  • How long should we wait before execution terminates?
  • Should the target command run in the background or foreground?
  • How much feedback should be provide the user?

The only parsing failure is if the user inputs some non-sensical or poorly formatted duration. All other options have a default value if not provided. All parsing failures result in a short error message followed by the usage instructions.


Execution

The timeout utility forks a child process to run the desired command and sets an alarm to stop the child after a time has passed. The flow looks like this:

  • Prepare signal handlers used by parent or child, including:
    • The chosen termination signal
    • The timeout alarm signal (SIGALRM) for cleanup
    • The usual littany of exit signals to cleanup
    • tty signal handler blocks so the child can continue work
    • The handler for SIGCHLD for the parent to take action
  • Fork a child process
  • The parent:
    • Ensure SIGALARM is available
    • Set a timer to trigger killing child process (timer_settime() or alarm())
    • Prevent failure cleanups
    • Wait for child
    • Handle various results (exit, failure, timer kill, etc)
  • The child:
    • Restore default tty signal handlers (not done during execvp())
    • Execute target command (execvp())
    • Handle execution failure return values

Forks, Signals, and execvp()

It's useful to know how fork, signals, and child execution relate in order to follow the timeout utility.

  1. After fork(), both processes execute the same subsequent code (copied/shared code segment and IP):
    • The parent process will see a positive (pid_t) value returned from fork()
    • The child process will see a 0 from fork() under normal operation
  2. The child inherits all signal handlers from the parent, but not pending signals (different PIDs)
  3. After execvp(), custom signal handlers are lost (code segment was overwritten)
  4. A child process signals the parent via SIGCHLD when it exits (or interrupted)

*While these rules are generally true, you should confirm actual behavior for your platform.

Failure cases:

  • Unable to change signal masks
  • Failure to disable core dumps
  • Fork fails
  • Child could not execute target command
  • The parent detects a child core dump
  • Unknown error status returned
  • waitpid() fails

All failures at this stage output an error message to STDERR and return without displaying usage help


[Back to Project Main Page]