CS110 Lecture 06: Pipes, Signals, and Concurrency

Principles of Computer Systems

Winter 2020

Stanford University

Computer Science Department

Instructors: Chris Gregg and

                            Nick Troccoli

CS110 Topic 2: How can our programs create and interact with other programs?

Learning About Processes

Creating processes and running other programs

Inter-process communication

Signals

Race Conditions

1/15

Today

1/27

1/29

Today's Learning Goals

  • Get more practice with using fork() and execvp
  • Learn about pipe and dup2 to create and manipulate file descriptors
  • Introduce signals as another way for processes to communicate

Plan For Today

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland

fork()

  • A system call that creates a new child process
  • The "parent" is the process that creates the other "child" process
  • From then on, both processes are running the code after the fork
  • The child process is identical to the parent, except:
    • it has a new Process ID (PID)
    • for the parent, fork() returns the PID of the child; for the child, fork() returns 0
    • fork() is called once, but returns twice
pid_t pidOrZero = fork();
// both parent and child run code here onwards
printf("This is printed by two processes.\n");

waitpid()

A function that a parent can call to wait for its child to exit:

pid_t waitpid(pid_t pid, int *status, int options);
  • pid: the PID of the child to wait on, or -1 to wait on any of our children
  • status: where to put info about the child's termination (or NULL)
  • options: optional flags to customize behavior (always 0 for now)

 

The function returns when the specified child process exits.

  • the return value is the PID of the child that exited, or -1 on error (e.g. no child to wait on)
  • If the child process has already exited, this returns immediately - otherwise, it blocks
  • It's important to wait on all children to clean up system resources

execvp is a function that lets us run another program in the current process.

 

 

It runs the specified program executable, completely cannibalizing the current process.

  • path identifies the name of the executable to be invoked.
  • argv is the argument vector that should be passed to the new executable's main function.
  • For the purposes of CS110, path and argv[0] end up being the same exact string.
  • If execvp fails to cannibalize the process and install a new executable image within it, it returns -1 to express failure.
  • If execvp succeeds, it never returns in the calling process.
  • execvp has many variants (execle, execlp, and so forth. Type man execvp to see all of them). We generally rely on execvp in this course.
int execvp(const char *path, char *argv[]);

execvp()

execvp() Example

What does the following code output, assuming execvp executes successfully?

 

 

int main(int argc, char *argv[]) {
    char *args[] = {"/bin/ls", "-l", "/usr/class/cs110", NULL};
    execvp(args[0], args);
    printf("Hello world!\n");
    return 0;
}
  • This process will be completely consumed by the new program being run (ls).
  • Lines 4+ will never execute unless an error occurs in execvp.

Plan For Today

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland

Revisiting mysystem

mysystem is our own version of the built-in function system.

  • It takes in a terminal command (e.g. "ls -l /usr/class/cs110"), executes it in a separate process, and returns when that process is finished.
    • We can use fork to create the child process
    • We can use execvp in that child process to execute the terminal command
    • We can use waitpid in the parent process to wait for the child to terminate

Revisiting mysystem

static int mysystem(char *command) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, wait for the child
    int status;
    waitpid(pidOrZero, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return -WTERMSIG(status);
    }
}

Revisiting mysystem

static int mysystem(char *command) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, wait for the child
    int status;
    waitpid(pidOrZero, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return -WTERMSIG(status);
    }
}

Line 2: First, fork off a child process.

Revisiting mysystem

static int mysystem(char *command) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, wait for the child
    int status;
    waitpid(pidOrZero, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return -WTERMSIG(status);
    }
}

Lines 4-6: In the child, execute the /bin/sh program, which can execute any shell command.

Revisiting mysystem

static int mysystem(char *command) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, wait for the child
    int status;
    waitpid(pidOrZero, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return -WTERMSIG(status);
    }
}

Line 8: The child will only get to this line if execvp fails.

Revisiting mysystem

static int mysystem(char *command) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, wait for the child
    int status;
    waitpid(pidOrZero, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return -WTERMSIG(status);
    }
}

Lines 11-13: In the parent, wait for the child to terminate.

Revisiting mysystem

static int mysystem(char *command) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, wait for the child
    int status;
    waitpid(pidOrZero, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return -WTERMSIG(status);
    }
}

Lines 14-18: In the parent, after the child terminates, return its status.

Revisiting first-shell

int main(int argc, char *argv[]) {
    char command[kMaxLineLength];
    while (true) {
        printf("> ");
        fgets(command, sizeof(command), stdin);
    
        // If the user entered Ctl-d, stop
        if (feof(stdin)) {
            break;
        }
    
        // Remove the \n that fgets puts at the end
        command[strlen(command) - 1] = '\0';

        int commandReturnCode = mysystem(command);
        printf("return code = %d\n", commandReturnCode);
    }
  
    printf("\n");
    return 0;
}

Our first-shell program is a loop in main that parses the user input and passes it to mysystem.

first-shell Takeaways

  • A shell is a program that repeats: read command from the user, execute that command
  • In order to execute a program and continue running the shell afterwards, we fork off another process and run the program in that process
  • We rely on fork, execvp, and waitpid to do this!
  • Real shells have more advanced functionality that we will add going forward.
  • For your fourth assignment, you'll build on this with your own shell, stsh ("Stanford shell") with much of the functionality of real Unix shells.

More Shell Functionality

Shells have a variety of supported commands:

  • emacs &  - create an emacs process and run it in the background
  • cat file.txt | uniq | sort - pipe the output of one command to the input of another
  • uniq < file.txt | sort > list.txt - make file.txt the input of uniq and output sort to list.txt
  • Let's see how we can implement these - but first, a demo.

Plan For Today

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland

Supporting Background Execution

static void executeCommand(char *command, bool inBackground) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, either wait or return immediately
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitpid(pidOrZero, NULL, 0);
    }
}

Supporting Background Execution

static void executeCommand(char *command, bool inBackground) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, either wait or return immediately
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitpid(pidOrZero, NULL, 0);
    }
}

Line 1: Now, the caller can optionally run the command in the background.

Supporting Background Execution

static void executeCommand(char *command, bool inBackground) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        // If we are the child, execute the shell command
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        // If the child gets here, there was an error
        exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
    }

    // If we are the parent, either wait or return immediately
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitpid(pidOrZero, NULL, 0);
    }
}

Lines 11-16: The parent waits on a foreground child, but not a background child.

Supporting Background Execution

int main(int argc, char *argv[]) {
    char command[kMaxLineLength];
    while (true) {
        printf("> ");
        fgets(command, sizeof(command), stdin);
    
        // If the user entered Ctl-d, stop
        if (feof(stdin)) {
            break;
        }
    
        // Remove the \n that fgets puts at the end
        command[strlen(command) - 1] = '\0';

        if (strcmp(command, "quit") == 0) break;

        bool isbg = command[strlen(command) - 1] == '&';
        if (isbg) {
            command[strlen(command) - 1] = '\0';
        }

        executeCommand(command, isbg);
    }
  
    printf("\n");
    return 0;
}

In main, on lines 15-22, we check for the "quit" command, and also for whether to run the command in the background.

Plan For Today

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland

Announcements

  • Assign2 due tomorrow at 11:59PM PST
  • Assign3 goes out tomorrow - all about multiprocessing
    • This Monday's lecture needed for the last part
  • Section 2 starts tomorrow
    • previous week's section solutions released tomorrow

Mid-Lecture Checkin

Now we can answer the following questions:

  • when writing a shell, why is it essential to call execvp in the child process?
  • how can we update our shell to support background execution of commands?

Plan For Today

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland

Interprocess Communication

  • It's useful for a parent process to be able to communicate with its child (and vice versa)
  • There are two key ways we will learn to do this: pipes and signals
    • Pipes let two processes send and receive arbitrary data
    • Signals let two processes send and receive certain "signals" that indicate something special has happened.

Pipes

  • How can we let two processes send arbitrary data back and forth?
  • A core Unix principle is how many things can be modeled as files.  Could we use a "file"?
  • Idea: what if we used a file that one process could write to, and another process could read from?
  • Problem: we don't want to clutter the filesystem with actual files every time two processes want to communicate.
  • Solution: have the operating system set this up for us.  
    • It will give us two new file descriptors - one for writing, another for reading.
    • If someone writes data to the write FD, it can be read from the read FD.
    • It's not actually a physical file on disk - we are just using files as an abstraction
  • The pipe system call takes an uninitialized array of two integers and populates it with two file descriptors such that everything written to fds[1]can be read from fds[0].
  • pipe can allow parent processes to communicate with spawned child processes.
    • Because they're file descriptors, there's no global name for the pipe (another process can't "connect" to the pipe)
    • The parent's table is replicated in the child, so the child automatically gets access to the same file descriptors
int pipe(int fds[]);

pipe()

Here's an example program showing how pipe works (which you can find here):

pipe()

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

Here's an example program showing how pipe works (which you can find here):

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

pipe()

Lines 2-3: We ask the operating system to create a pipe for us.  This gives us two file descriptors, one for reading and one for writing.

 

Tip: you learn to read before you learn to write (read = fds[0], write = fds[1]).

Here's an example program showing how pipe works (which you can find here):

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

pipe()

Lines 13-14: after forking, in the parent we close the reader FD, and write to the writer FD to send a message to the child.

Here's an example program showing how pipe works (which you can find here):

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

pipe()

Lines 15-16: after sending a message, we wait for the child to receive it and terminate.  Then we clean it up and close the writer FD.

Here's an example program showing how pipe works (which you can find here):

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

pipe()

Line 4: when we fork, the child gets an ​identical copy of the parent's file descriptor table.  This means its file descriptor table entries point to the same open file table entries.

This means the open file table entries for the two pipe FDs both have reference counts of 2.

Here's an example program showing how pipe works (which you can find here):

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

pipe()

Lines 6-8: after forking, the child closes the writer FD and reads 6 bytes from the reader FD.

Here's an example program showing how pipe works (which you can find here):

int main(int argc, char *argv[]) {
  int fds[2];
  pipe(fds);
  pid_t pid = fork();
  if (pid == 0) {
    close(fds[1]);
    char buffer[6];
    read(fds[0], buffer, sizeof(buffer));
    printf("Read from pipe bridging processes: %s.\n", buffer);
    close(fds[0]);
    return 0;
  }
  close(fds[0]);
  write(fds[1], "hello", 6);
  waitpid(pid, NULL, 0);
  close(fds[1]);
  return 0;
}

pipe()

Lines 9-11: after reading data, it prints it to the screen, closes the reader FD, and terminates.

pipe (man page section 2) code example

This method of communication between processes relies on the fact that file descriptors are duplicated when forking.

  • each process has its own copy of both file descriptors for the pipe
  • each process must therefore close both file descriptors for the pipe when finished
  • both processes could read or write to the pipe if they wanted.

pipe()

This is how a shell can support piping between processes (e.g. cat file.txt | uniq | sort):

  • Shell creates three child processes: cat, uniq and sort
  • Shell creates two pipes: one between cat and uniq, one between uniq and sort

cat

uniq

sort

terminal in

terminal out

pipe1

pipe2

Process stdin stdout
cat terminal pipe1[1]
uniq pipe1[0] pipe2[1]
sort pipe2[0] terminal
int pipe1[2];

int pipe2[2];

pipe(pipe1);

pipe(pipe2);

pipe()

  • Using pipe, fork, dup2, execvp, close, and waitpid, we can implement the subprocess function, which spawns a child process that we can communicate with (full implementation of everything is right here).​
  • Same as mysystem, but now we can send a message to the child that it can read via STDIN
  • Let's see how we can do this.

combining pipe() and dup2()

stdin
stdout
stderr

parent

stdin
stdout
stderr

child

subprocess File Descriptor Diagram

All processes are configured with FDs 0-2 for STDIN, STDOUT and STDERR, respectively.

  • Processes assume these indexes are for these methods of communication
  • We can change one of these file descriptor table entries to change where its STDIN, STDOUT or STERR read from/write to!
stdin
stdout
stderr

parent

stdin
stdout
stderr

child

terminal in

pipe read

terminal out

terminal err

supplyfd

pipe write

subprocess File Descriptor Diagram

dup2 lets us make a copy of a file descriptor entry and put it in another file descriptor index.  If the second parameter is an already-open file descriptor, it is closed before being used.  We can use dup2 to copy the pipe read file descriptor into child's standard input!

dup2(fds[0], STDIN_FILENO);

Demo: subprocess

Questions about pipes?

Plan For Today

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland
  • A signal is a way to notify a process that an event occurred.
    • The kernel sends many signals (SIGSEGV, SIGBUS, SIGINT, ...)
      • Everyone who's programmed in C has unintentionally dereferenced a NULL pointer.
      • The kernel delivers a  SIGSEGV, informally known as a segmentation fault (or a SEGmentation Violation, or SIGSEGV, for short).
      • Unless you install a custom signal handler to manage the signal differently, a SIGSEGV terminates the program and generates a core dump.
    • Processes can send each other signals as well (SIGSTOP, SIGKILL)
  • A signal handler is a function that executes when the signal arrives
    • Some signals have default handler(e.g., SIGSEGV terminates process and dumps core)
    • You can install custom handlers for most signals
  • Each signal is represented internally by some number (e.g. SIGSEGV is 11).

UNIX Signals

  • SIGFPE: whenever a process commits an integer-divide-by-zero (and, in some cases, a floating-point divide by zero on older architectures), the kernel hollers and issues a SIGFPE signal to the offending process. By default, the program handles the SIGFPE by printing an error message announcing the zero denominator and generating a core dump.
  • SIGINT: when you type ctrl-c, the kernel sends a SIGINT to the foreground process group. The default handler terminates the process group.
  • SIGTSTP: when you type ctrl-z, the kernel issues a SIGTSTP to the foreground process group. The foreground process group is halted until a SIGCONT signal.
  • SIGPIPE: when a process attempts to write data to a pipe after the read end has closed, the kernel delivers a SIGPIPE. The default SIGPIPE handler prints a message identifying the pipe error and terminates the program.

Some Signals

A Systems Mystery

$ grep error file.txt > errors.txt &

[1] 4287

$

[1]+ Done          grep error file.txt > errors.txt

 

  • How does this work?
    • The shell returns control to the user after forking the child (not calling waitpid on the child)
    • But the shell still knows when the child completes
  • There must be a way for the shell to learn about when things have happened to its children
  • Whenever a child process changes state—that is, it exits, crashes, stops, or resumes from a stopped state, the kernel sends a SIGCHLD signal to the process's parent.
    • By default, the signal is ignored. We've ignored it until right now and gotten away with it.
  • This particular signal type is instrumental to allowing forked child processes to run in the background while keeping the parent immediately aware of when something happens.
  • Custom SIGCHLD handlers can call waitpid, which tells them the pids of child processes that gave changed state. If the child process terminated, either normally or abnormally, the waitpid also cleans up/frees the child.

SIGCHLD

  • Here's an example of when you might want to use a SIGCHLD handler.
  • The premise? Dad takes his five kids out to play. Each of the five children plays for a different length of time. When all five kids are done playing, the six of them all go home.
    • If Dad has stuff to do (rather than nap), this is a very simple analogy to many parallel data processing applications (if Dad only naps just call wait)
  • The parent is dad, and subprocesses are children. (Full program is right here.)

Signals at Disneyland

static const size_t kNumChildren = 5;
static size_t numDone = 0;

int main(int argc, char *argv[]) {
  printf("Let my five children play while I take a nap.\n");
  signal(SIGCHLD, reapChild);
  for (size_t kid = 1; kid <= 5; kid++) {
    if (fork() == 0) {
      sleep(3 * kid); // sleep emulates "play" time
      printf("Child #%zu tired... returns to dad.\n", kid);
      return 0;
    }
  }
  • Our first signal handler example: Disneyland
    • The program is crafted so each child process exits at three-second intervals. reapChild, handles each of the SIGCHLD signals delivered as each child process exits.
    • The signal prototype doesn't allow for state to be shared via parameters, so we have no choice but to use global variables.

Signals at Disneyland

  // code below is a continuation of that presented on the previous slide
  while (numDone < kNumChildren) {
    printf("At least one child still playing, so dad nods off.\n");
    sleep(5);
    printf("Dad wakes up! ");
  }
  printf("All children accounted for.  Good job, dad!\n");
  return 0;
}

static void reapChild(int unused) {
  waitpid(-1, NULL, 0);
  numDone++;
}
  • Here's the output of the above program.
    • Dad's wakeup times (at t = 5 sec, t = 10 sec, etc.) interleave the various finish times (3 sec, 6 sec, etc.) of the children, and the output published below reflects that.
    • Understand that the SIGCHLD handler is invoked 5 times, each in response to some child process finishing up.

Signals at Disneyland

cgregg@myth60$ ./five-children 
Let my five children play while I take a nap.
At least one child still playing, so dad nods off.
Child #1 tired... returns to dad.
Dad wakes up! At least one child still playing, so dad nods off.
Child #2 tired... returns to dad.
Child #3 tired... returns to dad.
Dad wakes up! At least one child still playing, so dad nods off.
Child #4 tired... returns to dad.
Child #5 tired... returns to dad.
Dad wakes up! All children accounted for.  Good job, dad!
cgregg@myth60$
  • A signal is not like a function call
    • Signals aren't handled immediately (there can be delays)
    • If a signal is delivered multiple times, the handler is only called once
    • There's a bitmask in the kernel
      • Delivering a signal sets the bit
      • Handling the signal clears the bit
      • If multiple instances of the signal are delivered before handling, handler executes once
  • Signals execute asynchronously: the kernel can push a stack frame onto the process stack that causes it to execute a handler, then return back to what it was doing
    • This makes signals sort-of-concurrent (technically, preemptive)
    • Keep your signal handlers simple or you will regret it
  • This is much like how hardware behaves with interrupts -- POSIX brings that model to software

Signal Handling Semantics

  • Consider the scenario where the five kids run about Disneyland for the same amount of time. Restated, sleep(3 * kid) is now sleep(3) so all five children flashmob dad when they're all done.
    • Dad never detects all five kids are present and accounted for, and the program runs forever because dad keeps going back to sleep. Why?

Example of Tricky Signal Semantics

cgregg*@myth60$ ./broken-pentuplets 
Let my five children play while I take a nap.
At least one child still playing, so dad nods off.
Kid #1 done playing... runs back to dad.
Kid #2 done playing... runs back to dad.
Kid #3 done playing... runs back to dad.
Kid #4 done playing... runs back to dad.
Kid #5 done playing... runs back to dad.
Dad wakes up! At least one child still playing, so dad nods off.
Dad wakes up! At least one child still playing, so dad nods off.
Dad wakes up! At least one child still playing, so dad nods off.
Dad wakes up! At least one child still playing, so dad nods off.
^C # I needed to hit ctrl-c to kill the program that loops forever!
cgregg@myth60$
  • Calling waitpid repeatedly fixes the problem, but it changes the behavior of the program.
    • Calls to waitpid can prevent dad from returning to his nap. For real programs, this means they can't continue to do work (e.g., respond to shell commands.
  • We need to instruct waitpid to only reap children that have exited but to return without blocking, even if there are more children still running. We use WNOHANG for this, as with:

 

 

 

 

 

  • Why not just call waitpid with WNOHANG in the main loop?
    • Mostly a style question: keeps main loop logic simpler.
    • Also means waitpid is called promptly, not determined by main loop.
    • Also, it means we learn about more than just termination (stopped, resumed, etc.).

Waiting without blocking

static void reapChild(int unused) {
  while (true) {
    pid_t pid = waitpid(-1, NULL, WNOHANG);
    if (pid <= 0) break; // note the < is now a <=
    numDone++;
  }
}
  • One of the seven key systems principles we'll be covering this quarter
  • Concurrency: performing multiple actions at the same time
     
  • Concurrency is extremely powerful: it can make your systems faster, more responsive, and more efficient. It's fundamental to all modern software.
  • But it's also very tricky to program -- we will spend a good deal of the quarter showing you all of the challenges and the mechanisms we use to tackle them (starting next lecture)
    • It boils down to shared data, and making sure code always sees that data in a consistent state, e.g., doesn't see counter_1 and counter_2 be different
    • Data analytics frameworks make it possible to massively parallelize computations by defining  a data model where there is almost no shared data: the data is split into many independent chunks that are processed in parallel

Concurrency

Lecture Recap

  • Review: fork() and execvp()
  • Practice: Revisiting first-shell
  • Running in the background
  • Break: Announcements
  • Introducing Pipes
  • Practice: Implementing subprocess
  • Introducing Signals
  • Demo: Disneyland

 

Next time: more signals

CS110 Lecture 06: Pipes, Signals and Concurrency (w20)

By Nick Troccoli

CS110 Lecture 06: Pipes, Signals and Concurrency (w20)

  • 1,180