Slide 1: CS110 Lecture 06: Pipes, Signals, and Concurrency

Principles of Computer Systems

Winter 2020

Stanford University

Computer Science Department

Instructors: Chris Gregg and

                            Nick Troccoli

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

Slide 3: Learning About Processes

Creating processes and running other programs

Inter-process communication

Signals

Race Conditions

1/15

Today

1/27

1/29

Slide 4: Today's Learning Goals

Slide 5: Plan For Today

Slide 6:
fork()

pid_t pidOrZero = fork();
// both parent and child run code here onwards
printf("This is printed by two processes.\n");

Slide 7:
waitpid()

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

 

The function returns when the specified child process exits.

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.

Slide 8: execvp()

Slide 9: 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;
}

Slide 10: Plan For Today

Slide 11: Revisiting mysystem

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

Slide 12: 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);
    }
}

Slide 13: 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.

Slide 14: 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.

Slide 15: 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.

Slide 16: 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.

Slide 17: 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.

Slide 18: 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.

Slide 19: first-shell Takeaways

Slide 20: More Shell Functionality

Shells have a variety of supported commands:

Slide 21: Plan For Today

Slide 22: 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);
    }
}

Slide 23: 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.

Slide 24: 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.

Slide 25: 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.

Slide 26: Plan For Today

Slide 27: Announcements

Slide 28: Mid-Lecture Checkin

Now we can answer the following questions:

Slide 29: Plan For Today

Slide 30: Interprocess Communication

Slide 31: Pipes

Slide 32: pipe()

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

Slide 33: 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;
}

Slide 34: 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;
}

Slide 35: 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;
}

Slide 36: 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;
}

Slide 37: 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;
}

Slide 38: 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;
}

Slide 39: pipe()

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

Slide 40: pipe (man page section 2) code example

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

Slide 41: pipe()

This is how a shell can support piping between processes (e.g. cat file.txt | uniq | 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);

Slide 42: pipe()

Slide 43: combining pipe() and dup2()

stdin stdout stderr

parent

stdin stdout stderr

child

Slide 44: subprocess File Descriptor Diagram

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

stdin stdout stderr

parent

stdin stdout stderr

child

terminal in

pipe read

terminal out

terminal err

supplyfd

pipe write

Slide 45: 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!

Slide 46: Demo: subprocess

Slide 47: Questions about pipes?

Slide 48: Plan For Today

Slide 49: UNIX Signals

Slide 50: Some Signals

Slide 51: A Systems Mystery

$ grep error file.txt > errors.txt &

[1] 4287

$

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

 

Slide 52: SIGCHLD

Slide 53: 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;
    }
  }

Slide 54: 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++;
}

Slide 55: 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$

Slide 56: Signal Handling Semantics

Slide 57: 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$

 

 

 

 

 

Slide 58: 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++;
  }
}

Slide 59: Concurrency

Slide 60: Lecture Recap

 

Next time: more signals