Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Parallel

DutyCycle: Running Programs Part-time to Avoid the Heat


Jeffrey L. Taylor has been programming for fun for 40 years and for profit for 35 years. From toy mechanical computers up through scientific mainframes, he has built, bought, and programmed computers big and small. He currently is doing freelance writing after decades of C/C++ programming with sidetrips through university teaching. Jeffery can be contacted at [email protected].


Thin and light laptops are great for an urban nomadic lifestyle of planes and coffee houses with WiFi. But the cooling is marginal for large compiles/builds (e.g., the kernel) and totally inadequate for CPU- and GPU-intensive games like recent versions of Doom or Quake. The thermal protection kicks before the first level is completed. My interim solution has been to run the original Doom, it runs hot but not too hot. Given that laptops are expected to outsell desktops this year, I expect I am not alone in my frustration at not being able to play games and worrying about frying my laptop.

For a non-realtime program like a large compile/build, running part-time (run a bit, pause a bit, repeat) seems like a reasonable approach. While there are some research efforts using the throttling built into the Linux kernel and Intel CPUs, most are thermal management oriented and throttle down all processes. I want just the CPU intensive program throttled. And I like to keep my programs as hardware and operating system independent as possible. Plus research code in my only computer's operating system makes me nervous.

Many mechanical and electronic systems run part-time -- air conditioning, for instance. The dutycycle is the fraction of time the system is on, typically expressed as a percentage, from 0 percent (always off) to 100 percent (always on). A related metric is the length of an on-off cycle, the cycle time. The dutycycle program in Listing 1 lets you control both. At the default setting of 50 percent dutycycle and a 100 millisecond cycle time (i.e., on for 50 millisecond, off for 50 milliseconds), the CPU temperature rise during kernel builds is cut almost in half.

#include <errno.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define VERSION "1.0.0"

static volatile pid_t pgrp;	/* process group of children */

/* catch-all signal handler, terminates all child processes */
static void terminate_children(int signum)
{
  signal(signum, SIG_DFL);
  kill(-pgrp, signum);
  raise(signum);
}

int main(int argc, char* const argv[])
{
  int c;
  pid_t childpid;
  int errflg = 0;
  int status;
  int debug = 0;
  long dutycycle = 50;	/* default: 50% */
  double cycle_time = 0.100;	/* default: 100 milliseconds */

  while ((c = getopt(argc, argv, "+p:c:vD")) != -1) {
    char *endptr;

    switch (c) {
    case 'p':	/* duty cycle percentage */
      dutycycle = strtol(optarg, &endptr, 10);
      if (0 < dutycycle && dutycycle <= 100 && *endptr == '\0')
	;
      else {
	fprintf(stderr,
		"dutycycle must be integer, 1 to 100 (percent)\n");
	++errflg;
      }
      break;
    case 'c':
      cycle_time = strtod(optarg, &endptr);
      if (0.0 < cycle_time && *endptr == '\0')
	;
      else {
	fprintf(stderr,
		"cycle time must be a positive floating point number\n");
	++errflg;
      }
      break;
    case 'v':
      printf("Version: %s\n", VERSION);
      exit(EXIT_SUCCESS);
    case 'D':
      debug = 1;
      break;
    case ':':       /* option without operand */
      fprintf(stderr,
	      "Option -%c requires an operand\n", optopt);
      errflg++;
      break;
    case '?':
      fprintf(stderr,
	      "Unrecognized option: -%c\n", optopt);
      errflg++;
      break;
    default:
      assert(0);
    }
  }

  if (errflg) {
    fprintf(stderr, "usage: dutycycle [-p #] [-c #] [-v] [-D] command ...\n");
    exit(2);
  }

  if((childpid = fork()) == -1) {
      perror("fork");
      exit(EXIT_FAILURE);
  }

  if(childpid == 0) {    /* child */
    status = setpgrp();
    if (status != 0) {
      perror(argv[0]);
      exit(EXIT_FAILURE);
    }

    if (debug) {
      fprintf(stderr, "CHILD: process = %d, group = %d, session = %d\n",
	      getpid(), getpgrp(), getsid());
    }

    execvp(argv[optind], argv + optind);
  } else {    /* parent */
    double whole_seconds;
    double fractional_seconds;

    pgrp = getpgid(childpid);	/* child(ren) process group */

    if (signal(SIGTERM, &terminate_children) == SIG_ERR) {
      perror(argv[0]);
      exit(EXIT_FAILURE);
    }
    if (signal(SIGQUIT, &terminate_children) == SIG_ERR) {
      perror(argv[0]);
      exit(EXIT_FAILURE);
    }
    if (signal(SIGINT, &terminate_children) == SIG_ERR) {
      perror(argv[0]);
      exit(EXIT_FAILURE);
    }

 
    fractional_seconds = modf(cycle_time * dutycycle/100.0,  &whole_seconds);
    struct timespec const on_time = {
      (time_t)(whole_seconds), 
      (long)(1.0e9 * fractional_seconds)
    };

    fractional_seconds = modf(cycle_time * (100-dutycycle)/100.0,  &whole_seconds);
    struct timespec const off_time = {
      (time_t)(whole_seconds),
      (long)(1.0e9 * fractional_seconds)
    };

    if (debug) {
      fprintf(stderr, "%fs cycle, %d%% on.\n", cycle_time, dutycycle);
      fprintf(stderr, "Process %d is controlling child process %d\n", getpid(), childpid);
      fprintf(stderr, "PARENT: process = %d, group = %d, session = %d\n", getpid(), getpgid(), getsid());
    }

    close(0);    /* close stdin */
    close(1);    /* close stdout */

    /* while forked child process running ... */
    while (waitpid(childpid, &status, WNOHANG)== 0) {
      struct timespec remainder;

      /* sleep until on-cycle time up */
      remainder = on_time;
      /* keep calling nanosleep until not interupted */
      do {
	status = nanosleep(&remainder, &remainder);
      } while (status == -1 && errno == EINTR);
      if (status != 0) {
	perror(argv[0]);
	exit(EXIT_FAILURE);
      }

      if (debug)
	fprintf(stderr, "STOP\n");

      /* pause child process group */
      status = kill(-pgrp, SIGSTOP);
      if (status != 0) {
	perror(argv[0]);
	exit(EXIT_FAILURE);
      }

      /* sleep until off-cycle time complete */
      remainder = off_time;
      do {
	status = nanosleep(&remainder, &remainder);
      } while (status == -1 && errno == EINTR);
      if (status != 0) {
	perror(argv[0]);
	exit(EXIT_FAILURE);
      }

      if (debug)
	fprintf(stderr, "CONT\n");

      /* resume the child process group */
      status = kill(-pgrp, SIGCONT);
      if (status != 0) {
	perror(argv[0]);
	exit(EXIT_FAILURE);
      }
    }
  }
  return EXIT_SUCCESS;
}

Listing 1: dutycycle.c

The basic idea of dutycycle is to fork off the controlled program, sleep for a bit, pause the controlled program, sleep a bit more, then resume it, repeating until the controlled program exits. The POSIX standard signals STOP and CONT (continue), respectively pause and resume a process. That is OS independent enough for me and requires no experimental kernel code. This code has only been tested on Linux, but should work on most Unix work-alikes and other sufficiently POSIX-compliant operating systems. (POSIX is a bunch of standards, not just one; how many parts and how well implemented they are varies.)

As is frequently the case, the swamp turned out easier to get into than get out of. For example, the make program recursively spawns child processes for each level of subdirectories and to run the compiler/linker/etc. Starting and stopping the make process will not effect any child processes. However, signalling the whole process group will affect them all.

Just when you think you have the denizens of the swamp (i.e, the STOP and CONTsignals) tamed, their siblings (TERM, QUIT, and INT) show up and want attention too. Dutycycle needs to catch these and pass them on to the whole process group so the whole mess can be gracefully stopped. Otherwise, dutycycle exits but the controlled program(s) keeps running. The terminate_children() handler does this. Another complication is that the nanosleep() calls can be interrupted and must be restarted, hence the inner loops.

Dutycyle is invoked like the nice program:


dutycycle [OPTION] [COMMAND [ARG]...]

The program, it's options if any, and the controlled program and it's options and arguments. Dutycycle's options are:

-p #

on percentage, integer between 1 and 100, default 50.

-c #.#

full on/off cycle time, positive floating point number in seconds,

 

default 0.100, i.e., 100 milliseconds.

-v

print version number on standard output and exit.

-D

enable debug information printout.

-?

print usage message and exit.

 

Part of being a intelligent tool user is knowing when not to use them and how to fudge the edge cases. When the controlled program is a client for a CPU-intensive server, dutycycle may not help much. In some cases where the controlled program is making many lightweight server calls, it can help. Use a very low dutycycle, e.g., 5 percent. The server will complete each call then return to the client which hopefully will take long enough to setup the next server call for the CPU to cool.

Building dutycycle is easy. Copy and paste the source from Listing 1. Generally no compiler options are needed to build, e.g.:


cc -o dutycycle dutycycle.c

After all the development was done and committed I found a pleasant surprise. With a very short cycle time (e.g. 1/100 second) some action games are still playable and run cool enough. The difference is not as dramatic as with builds, e.g., the original Doom runs about 1/3 cooler with a 50 percent dutycycle. I suspect this is because action games stress the GPU in addition to the CPU. Now excuse me while I see if I can now run Quake III Arena.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.