CSC374 Jan27

slide version

single file version

Contents

  1. waitpid review
  2. sleep and pause
  3. waiting for foreground job
  4. shell race condition
  5. sets of signals data type
  6. Sets of Signals
  7. blocking/unblocking signals
  8. Creating new process groups
  9. Sending Signals to a Process Group
  10. SIGINT and SIGTSTP signals
  11. Implementation for fg and bg
  12. Example

waitpid review[1] [top]

Shell Problem: How to manage terminated or stopped child process.

In both cases, a SIGCHLD is sent to the parent.

Signal handler for SIGCHLD is the one place to handle both events.

What needs to be done in this signal handler for the two cases?

When the SIGCHLD signal is received, the signal handler needs to know what child changed state and whether it terminated or stopped.

The handler can call waitpid with the right parameters to get this information.

Since signals are not queued, the handler must call waitpid for all processes that have already terminated or stopped, but must not block!

What are the correct parameters to use?

waitpid parameters wait set blocks if return value
waitpid(-1, &status, 0) terminated children children still exist, but none have terminated pid of a terminated child or -1 if wait set empty
waitpid(-1, &status, WUNTRACED) terminated OR stopped children children still exist, but none have terminated OR stopped pid of a child in the wait set or -1 if no more children
waitpid(-1, &status, WNOHANG) terminated children never blocks pid of a child in the wait set or
0 if children still exist, but none have terminated or
-1 no more children
waitpid(-1, &status, WNOHANG | WUNTRACED) terminated or stopped children never blocks pid of a child in the wait set or
0 if children still exist, but none are terminated or stopped or
-1 if no more children exist

sleep and pause[2] [top]

The waitpid is not the only function that can block a process.

Both the sleep(secs) function and pause() cause the calling process to block.

The pause() function unblocks and returns if the process receives and handles a signal (without terminating).

The sleep(sec) function unblocks and returns after sec seconds OR if the process receives and handles a signal (without terminating).

waiting for foreground job[3] [top]

When the foreground job terminates or is stopped, the SIGCHLD handler executes as usual.

The handler changes the state of the foreground process from foreground to either stopped or undefined (if it was terminated).

If the handler does this, the shell can wait for a foreground process by repeatedly checking if the state of the foreground process has changed.

shell race condition[4] [top]

Problem:

  1. The shell forks a child process to execute a program.
  2. The child begins execution of the program.
  3. Meanwhile, the shell adds the child process to the job table.
  4. When the child terminates, the shell's SIGCHLD handler removes the child process from the job table.

A problem occurs if the child execution is so short that it finishes before the shell can add it to the job table.

That is step 4 occurs before step 3 and so the shell would try to remove a process from the table before it is there - fails.

The step 3 occurs and the shell tries to add the job that has already terminated.

This is a "race" between the child and the shell. The job table can become corrputed if the child wins the race.

The solution:

Now the child will be guaranteed to be in the table BEFORE the shell tries to remove it.

sets of signals data type[5] [top]

How does your shell temporarily block the SIGCHLD signal?

       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
   

where the 'how' parameter should either be SIG_BLOCK or SIG_UNBLOCK
set is a pointer to the set of signals to be blocked or unblocked
and oldset is a pointer to a location to store the previous set of signals that were blocked.

Sets of Signals[6] [top]

The type sigset_t represents a set of signals.

To create a set with the single signal, SIGCHLD:

  1. declare a sigset_t variable and initialize it (to be the empty set of signals)
  2. add the SIGCHLD signal to the sigset_t

Example:

  sigset_t sset;
  
  sigemtpyset(&sset);
  sigaddset(&sset, SIGCHLD);
    

blocking/unblocking signals[7] [top]

To temporarily block SIGCHLD:

      
  sigset_t sset;
  sigset_t oldset;
  
  sigemtpyset(&sset);
  sigaddset(&sset, SIGCHLD);
      sigprocmask(SIG_BLOCK, &sset, 0);
      ...
      sigprocmask(SIG_UNBLOCK, &sset, 0);
    

Creating new process groups[8] [top]

A process has a process id and a group id.

If a process is a group leader then all its children and their children get the same group id.

In this case the group id is the process id of the group leader.

So potentially, all processes created by the shell to run programs are in the same group with the shell as the group leader.

The shell creates a process, and records it as a 'job'. But that process might create its own children.

We want all the processes in a job to be in the same group with the group leader being the process created by the shell.

We do NOT want the shell to be in the same group.

The following function puts the calling process into a new process group with the calling process as the group leader:

      int setpgrp()
    

It returns 0 for success and -1 on error.

Sending Signals to a Process Group[9] [top]

The kill function can send a signal to a process or to a process group.

Example

If a process has pid 1400 and is the leader of a group

Signals can easily be sent to a whole process group.

             int kill(pid_t pid, int sig);
    

If pid is positive, then signal sig is sent only to the process with pid.

If pid equals 0, then sig is sent to every process in the process group of the current process.

If pid is less than -1, then sig is sent to every process in the process group -pid.

SIGINT and SIGTSTP signals[10] [top]

The terminal driver will still think your shell is the foreground process.

So while your shell creates a "foreground" process to run a program, a SIGINT signal from the keyboard will be sent to the shell!

The signal handler should "forward" this signal to the process that the shell considers as the "foreground" process.

How, by using the kill function to send the SIGINT signal to the foreground job; that is, to the the whole process group.

Implementation for fg and bg[11] [top]

If a background process is stopped, applying either fg or bg to it should cause it to continue. To do so, the shell needs to send a SIGCONT signal to the process group (not just the process) to cause it to be ready to run again.

Example[12] [top]

If the first argument to kill is < -1, the signal is sent to all processes with process group id equal to the absolute value of the first argument.

Here is an example showing the pitfalls and the need changing the process group with setpgrp.


int main(int argc, char *argv[])
{
  int pid1;

  printf("my pid = %d\n", getpid());

  if ((pid1 = fork()) == 0) {
    printf("first child: pid = %d, gpid = %d\n", getpid(), getpgrp());
    int pid2;
    if ((pid2 = fork()) == 0) {
      printf("first child's child: pid = %d, gpid = %d\n", getpid(),
    getpgrp());
      sleep(10);
      printf("c\n");
      exit(0);
    }
    waitpid(pid2, 0, 0);
    printf("b\n");
    exit(1);
  }
  sleep(1);
  printf("pid1 = %d\n", pid1);
  kill(-pid1, SIGCHLD);
  waitpid(pid1, 0, 0);
  printf("a\n");
  return 0;

}