CS456 - Systems Programming

Job-control and the controlling terminal

The TTY (historically the name for a Tele-TYpe) is a driver for interfacing with a terminal (for output) and keyboard (for input), historically via a bi-directional serial interface. Often having to interface with a wide variety of hardware and software options, the TTY supports a wide variety of connection modes and flow control schemes in what are called line disciplines.

These days your TTY is often virtualized in what is called a pseudo-terminal which is a TTY interface that is often connected on one end to either a virtual terminal display (i.e. programs such as xterm, konsole, etc,) or via a network connection to a remote client (i.e. ssh or putty.)

It is reasonable that only one program should have access to your terminal at any given time, i.e. when you type something, the characters that you've typed should only go to the program that is currently in control, much in the same way that applications have focus in a graphical user interface.

The shell is typically the program that controls which program has access to your terminal. Programs that currently have full control of the terminal are called foreground jobs. Your shell may optionally allow a program to run without having (full) access to the terminal. Such programs are called background jobs. The process of allowing which program to control the terminal is called job control.

Process Groups

When the ISIG terminal attribute is set, typing certain keys such as Ctrl-C (SIGINT), Ctrl-\ (SIGQUIT) or Ctrl-Z (SIGTSTP) will send a signal to the process group that is controlling the terminal. A process group is a collection of processes with the same process group ID number. In a pipeline this should be every process in the pipeline to insure that one of the control keys will act on every process in the pipeline simultaneously.

The first process in a pipeline is usually designated the process group leader and its process ID is made the process group ID for it and all subsequently launched processes. If the process is not part of a pipeline, then it is still the process group leader, but there will be only one process in the process group, itself.

To set the process group ID for a process, use:

int setpgid(pid_t pid, pid_t pgid);

If pid is zero, then the process ID of the calling process is used, if pgid is zero then the process group ID is made the same as the process ID, thus:


will make the process it's own process group leader. Subsequent processes in a pipeline should have their process group ID set to the process ID of the first process in the pipeline (rightmost process.)

The Foreground Process

tcsetpgrp(int fd, pid_t pgrp);

The tcsetpgrp() function will make a process group the foreground process of a terminal. This should be done in the child process that will be the process leader (i.e. the first process in the pipeline) before any re-directions are performed (or at least before all access to the terminal (or /dev/tty) is lost. The file descriptor fd should be a descriptor (0,1 or 2) that is still connected to the terminal (TTY).

In the run() command we use the following to set the process group id of the process and to make the process the terminal owner if it is the process group leader.

// Adds the pgrp option, which is 0 for the process leader and the pid of the
// process leader for the remainder of the pipeline commands.
pid_t run(cmd_t *c, int *outfd, int inpipe, int pgrp)
  // ...

  // Set out process group id and asset control of the terminal if we're the
  // process group leader (pgrp == 0):
  setpgid(0, pgrp);
  if (pgrp == 0) tcsetpgrp(STDIN_FILENO, getpgid(0));

  // ...

If the process is to be a background process, the tcsetpgrp() function would not be called (pass an additional parameter to invoke and run, indicating that the job is to be a background process.)

Process and Job Book-keeping:

To keep track of processes (individual programs) and jobs (pipelines of programs) that are launched by the shell in order to perform job-control, we need to keep track of them. The process table is defined by:

// Status for processes and jobs:
typedef enum { FOREGROUND, BACKGROUND, STOPPED, DEAD } status_t;

// Process table entries:
typedef struct process {
  char *cmd;        // command word (argv[0])
  pid_t pid, pgid;  // Process id and group to which this process belongs
  status_t status;  // Status (foreground/background/stopped)
  struct process *next;
} proc_t;

// Job entries:
typedef struct job {
  char *cmd;        // Command
  pid_t pgid;       // Process group ID for all processes in this job
  int procs, stopped;   // Number of processes in job, # that are currently stopped
  status_t status;  // Status
  struct job *next;
} job_t;

Without going too much into the implementation, when a process is launched it is added to both a the process table (by its pid) and job table (by its process group id.) If the job already exists, the procs count is incremented instead. We modify sys() to add the process to the tables:

void sys(void)
  pid_t pid;
  int outfd = -1, pgid = 0;

  for(int cp = csp-1; cp >= 0; cp--) {
    if (cmdstack[cp]->argv[0] == NULL) continue;

    // Handles built-in functions, such as 'cd', 'jobs', 'fg', etc...
    builtin_t *b = isbuiltin(cmdstack[cp]->argv[0]);
    if (b != NULL) b->cmd(cmdstack[cp]->argc, cmdstack[cp]->argv);
    else {
      pid = run(cmdstack[cp], &outfd, cp > 0, pgid);
      if (pid < 0) break;
      if (pgid == 0) pgid = pid;
      // Adds process to process and job tables:
      addproc(pid, pgid, cmdstack[cp]->argv[0], FOREGROUND);
      addjob(pgid, cmdstack[cp]->argv[0], FOREGROUND);


And then add a SIGCHLD signal handler to remove or change the state of a process to stopped. waitall() is then modified to just wait for all foreground jobs to either complete or be stopped:

// Handles the SIGCHLD signal:
void child_handler(int sig)
  int status;
  pid_t pid;

  while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
    if (WIFEXITED(status) || WIFSIGNALED(status)) {
      // process has died:
      pid_t pgid = removeproc(pid);
    } else if (WIFSTOPPED(status)) {
      // Process has been stopped:

// Waits until all foreground processes have completed or been stopped:
void waitall()
  while(fgjobs) pause();


#include "shell.h"

int fgjobs = 0, bgjobs = 0, stjobs = 0;

// Process and job tables:
proc_t *procs = NULL;
job_t *jobs = NULL;

proc_t *findproc(pid_t pid)
  proc_t *p;
  for(p=procs; p != NULL; p = p->next)
    if (p->pid == pid) return p;
  return NULL;

void freeproc(proc_t *n)

pid_t removeproc(pid_t pid)
  pid_t pgid;
  proc_t *p, *c;

  for(p = c = procs; c != NULL; c = c->next) {
    if (c->pid == pid) {
      if (p == c) procs = c->next;
      else p->next = c->next;
      pgid = c->pgid;
      return pgid;
    p = c;
  return 0;

// Change the state of a process/job to stopped
void stopped_proc(pid_t pid)
  proc_t *p = findproc(pid);
  if (p == NULL) return;

  p->status = STOPPED;
  job_t *j = findjob(p->pgid);
  if (j == NULL) return;

  // A job is only "stopped" when all the processes in it are stopped:
  if (j->stopped == j->procs) {
    if (j->status == FOREGROUND) fgjobs--;
    if (j->status == BACKGROUND) bgjobs--;
    j->status = STOPPED;

// Add a process to the process table:
void addproc(pid_t pid, pid_t pgid, char *cmd, status_t status)
  proc_t *p = malloc(sizeof(proc_t));

  p->cmd = strdup(cmd);
  p->pid = pid;
  p->pgid = pgid;
  p->status = status;

  p->next = procs;
  procs = p;

job_t *findjob(pid_t pgid)
  for(job_t *j = jobs; j != NULL; j = j->next)
    if (j->pgid == pgid) return j;
  return NULL;

void freejob(job_t *n)

// Removes a job once the procs counter reaches zero:
void removejob(pid_t pgid)
  job_t *p, *c;

  for(p = c = jobs; c != NULL; c = c->next) {
    if (c->pgid == pgid) {
      if (--(c->procs) == 0) {
    if (p == c) jobs = c->next;
    else p->next = c->next;
    if (c->status == FOREGROUND) fgjobs--;
    if (c->status == BACKGROUND) bgjobs--;
    p = c;

// Adds a job to the job table:
void addjob(pid_t pgid, char *cmd, status_t status)
  job_t *j = findjob(pgid);

  if (j == NULL) {
    j = malloc(sizeof(job_t));
    j->cmd = strdup(cmd);
    j->pgid = pgid;
    j->procs = 1;
    j->stopped = 0;
    j->status = status;
    if (status == FOREGROUND) fgjobs++;
    else if (status == BACKGROUND) bgjobs++;

    j->next = jobs;
    jobs = j;

  char buf[K];
  sprintf(buf, "%s | %s", cmd, j->cmd);
  j->cmd = strdup(buf);