CS110 – Lecture 6 Exceptional Control Flow: Fork, Wait Announcements: ● Assignment 1 due Monday at midnight There are *no* late days allowed for this assignment. Make sure to test run the submit script early so that you don't run into trouble at the last second. Assignment 2 will go out that day. You will take your assignment 1 code and optimize it, so make sure you're happy with the code you have for assignment 1 ● Topics for Today 1) See an example of a real-world program that uses fork extensively. 2) Finish up Jerry's fork examples 3) Introduce the new waitpid system call that lets us clean up or “reap” the resources used by terminated “zombie” processes. What can we do with fork? What can we do with fork? What can we do with fork? What can we do with fork? What can we do with fork? What can we do with fork? What can we do with fork? What can we do with fork? What can we do with fork? To recap: ● ● We can use a master process to fork off multiple logically separate subtasks in a way that if one crashes it doesn't crash the rest of the program. We can use information in the parent process to detect and report errors in the child processes. Fork Where we left off... (lecture 4 – slide 2) static const int kForkFailed = 1; int main(int argc, char *argv[]) { printf("Greetings from process %d! (parent %d)\n", getpid(), getppid()); pid_t pid = fork(); exitIf(pid == -1, kForkFailed, stderr, "fork function failed.\n"); printf("Bye-bye from process %d! (parent %d)\n", getpid(), getppid()); return 0; } Fork static const int kForkFailed = 1; int main(int argc, char *argv[]) { printf("Greetings from process %d! (parent %d)\n", getpid(), getppid()); pid_t pid = fork(); exitIf(pid == -1, kForkFailed, stderr, "fork function failed.\n"); printf("Bye-bye from process %d! (parent %d)\n", getpid(), getppid()); return 0; } Fork-puzzle //From lecture 4 – slide 3 static const char const *kTrail = "abcd"; static const int kForkFail = 1; int main(int argc, char *argv[]) { size_t trailLength = strlen(kTrail); for (size_t i = 0; i < trailLength; i++) { printf("%c\n", kTrail[i]); pid_t pid = fork(); exitIf(pid == -1, kForkFail, stderr, "Call to fork failed."); } return 0; } Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); fork(); printf(“c\n”); fork(); printf(“d\n”); return 0; } Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); fork(); printf(“c\n”); fork(); printf(“d\n”); return 0; } “a” Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); fork(); printf(“c\n”); fork(); printf(“d\n”); return 0; } “a” fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); fork(); printf(“c\n”); “b” fork(); printf(“d\n”); return 0; } “a” “b” fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); Don't forget that both processes will call fork()! printf(“b\n”); fork(); printf(“c\n”); “b” fork(); printf(“d\n”); return 0; } “a” “b” fork fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); “c” fork(); printf(“c\n”); “b” “c” fork(); printf(“d\n”); return 0; } “c” “a” “b” fork “c” fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); “c” fork(); printf(“c\n”); “b” “c” fork(); printf(“d\n”); return 0; } “c” “a” “b” fork “c” fork fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); printf(“b\n”); “c” fork(); printf(“c\n”); “b” “c” fork(); printf(“d\n”); return 0; } “d” “d” “d” “d” “d” “d” “d” “d” “c” “a” “b” fork “c” fork fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); Key question: What guarantees do we have about the ordering of these print statements? printf(“b\n”); “c” fork(); printf(“c\n”); “b” “c” fork(); printf(“d\n”); return 0; } “d” “d” “d” “d” “d” “d” “d” “d” “c” “a” “b” fork “c” fork fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); Next key question: Where did that shell prompt come from? printf(“b\n”); “c” fork(); printf(“c\n”); “b” “c” fork(); printf(“d\n”); return 0; } “d” “d” “d” “d” “d” “d” “d” “d” “c” “a” “b” fork “c” fork fork Fork-puzzle (simplified) int main(int argc, char *argv[]) { printf(“a\n”); fork(); Next key question: Where did that shell prompt come from? printf(“b\n”); “d” “c” fork(); printf(“c\n”); “b” “d” “d” “c” “d” fork(); “c” printf(“d\n”); return 0; } “a” “b” “c” “d” “d” “d” “d” prompt() tcsh wait Wait Meet your new system call: pid_t waitpid(pid_t pid, int *status, int options); Wait Meet your new system call: pid_t waitpid(pid_t pid, int *status, int options); Brief PSA: For meticulous detailed information about anything you would need to know about waitpid you can refer to the manual page by running “man waitpid” in your shell, or check out section 1.4.3 in the B & O reader, or 8.4.3 in the full textbook. Wait Meet your new system call: pid_t waitpid(pid_t pid, int *status, int options); If we call waitpid from the parent process and pass in the pid of its child, then we will block the parent process, pausing execution until that child exits. When the child exits, that process enters a “zombie” state that, despite having terminated, will continue to take up resources until we “reap” it by calling wait. (We'll talk about the return value, as well as the status and options arguments in a little bit) Simple wait example I've omitted the error checking here for brevity. Do *not* do this in your assignments! See lecture 4 – slide 4 for the real code. int main(int argc, char *argv[]) { printf("I get printed once!\n”); pid_t pid = fork(); bool parent = (pid != 0); if ((random() % 2 == 0) == parent) sleep(1); // Force one to sleep if (parent) waitpid(pid, NULL, 0); printf(“I'm printing from the %s.\n”, parent ? “parent” : “child”); return 0; } Simple wait example int main(int argc, char *argv[]) { printf("I get printed once!\n”); 2 Important Notes: 1) The only difference between the two processes is the pid returned from fork. (The random number generator even gave the same number for both processes!) pid_t pid = fork(); bool parent = (pid != 0); if ((random() % 2 == 0) == parent) sleep(1); // Force one to sleep if (parent) waitpid(pid, NULL, 0); printf(“I'm printing from the %s.\n”, parent ? “parent” : “child”); return 0; } Simple wait example int main(int argc, char *argv[]) { 2 Important Notes: 2) By calling waitpid we guarantee that the child prints and exits before the parent does. printf("I get printed once!\n”); pid_t pid = fork(); bool parent = (pid != 0); if ((random() % 2 == 0) == parent) sleep(1); // Force one to sleep if (parent) waitpid(pid, NULL, 0); printf(“I'm printing from the %s.\n”, parent ? “parent” : “child”); return 0; } Wait – with status pid_t waitpid(pid_t pid, int *status, int options); We can pass in the address of an int variable as the status parameter and waitpid will give us information about how and why our child process exited. (If we pass in NULL it's not an error, wait just assumes we aren't interested in the information) Wait – with status (See separate.c on lecture 5 slide 2 for the real error-checking) int main(int argc, char *argv[]) { pid_t pid = fork(); if (pid == 0) { return 110; // contrived status number } else { int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf(“Child exited with status %d.\n”, WEXITSTATUS(status)); } else { printf(“Child terminated abnormally\n”); } return 0; } } Wait – with status (See separate.c on lecture 5 slide 2 for the real error-checking) 2 Takeaways: int main(int argc, char *argv[]) { 1) If we want the parent and child pid_t pid = fork(); processes to do different things, if (pid == 0) { we have to use the pid to send return 110; // contrived status number them down different control } else { paths. int status; 2) We can use macros like waitpid(pid, &status, 0); WIFEXITED and WEXITSTATUS if (WIFEXITED(status)) { to extract useful information from printf(“Child exited with status %d.\n”, WEXITSTATUS(status)); the status integer. } else { (For a list of all of these macros printf(“Child terminated abnormally\n”); and their correct use, check the } man page or chapter 1.4.3 / 8.4.3 return 0; of B & O) } } Wait – return value pid_t waitpid(pid_t pid, int *status, int options); waitpid will return the pid of the child that was reaped if successful, or -1 in the case of an error. (It can also return 0 if we use some special options values, but we'll get to that later). Wait – Multiple children (See reap-in-fork-order.c on lecture 5 slide 4 for the full version) int main(int argc, char *argv[]) { pid_t children[kNumChildren]; for (size_t i = 0; i < kNumChildren; i++) { children[i] = fork(); if (children[i] == 0) exit(110 + i); // Using exit is like returning from main } for (size_t i = 0; i < kNumChildren; i++) { int status; pid_t pid = waitpid(childen[i], &status, 0); assert(pid == children[i]); // Must have the correct child assert(WIFEXITED(status) && WEXITSTATUS(status) == 110 + i); } return 0; } Wait – Multiple children (See reap-in-fork-order.c on lecture 5 slide 4 for the full version) 2 Takeaways: int main(int argc, char *argv[]) { 1) We need to use both the return pid_t children[kNumChildren]; value of wait and the value in for (size_t i = 0; i < kNumChildren; status i++) { to do robust error children[i] = fork(); checking. if (children[i] == 0) exit(110 + i); // Using like from main 2)We will exit waitison allreturning of the children } in the order they were forked. for (size_t i = 0; i < kNumChildren; That i++) means { that even if children int status; 2-8 exit first, we can't reap them pid_t pid = waitpid(childen[i], &status, 0); until child 1 exits. assert(pid == children[i]); // Must have the correct child assert(WIFEXITED(status) && WEXITSTATUS(status) == 110 + i); } return 0; } Wait – the “wait set” pid_t waitpid(pid_t pid, int *status, int options); Rather than force the parent to wait for a particular child, maybe we simply want to wait for any child. We can pass -1 for pid as a sign that we want to wait for any process in our wait set, which is all of our children by default. Wait – Multiple children (See reap-as-they-exit.c on lecture 5 slide 3 for the full version) int main(int argc, char *argv[]) { for (size_t i = 0; i < 8; i++) { pid_t pid = fork(); if (pid == 0) exit(110 + i); // Using exit is like returning from main } while (true) { int status; pid_t pid = waitpid(-1, &status, 0); // wait for any child if (pid == -1) break; // Either an error, or no more children if (WIFEXITED(status)) { printf(“Child %d exited with status: %d.\n”, pid, WEXITSTATUS(status)); } else { printf(“Child %d terminated abnormally\n”, pid); } } assert(errno == ECHILD); return 0; } Wait – Multiple children (See reap-as-they-exit.c on lecture 5 slide 3 for the full version) 2 Takeaways: int main(int argc, char *argv[]) { 1) We don't know what pid we are for (size_t i = 0; i < 8; i++) { expecting from waitpid anymore, pid_t pid = fork(); onlyfrom need if (pid == 0) exit(110 + i); // Using exit is so like we returning mainto ensure it isn't } -1 to signal an error. while (true) { 2) When the operating system int status; that we have no more pid_t pid = waitpid(-1, &status, 0); // waitknows for any child if (pid == -1) break; // Either an error, or no more children children to wait for, waitpid if (WIFEXITED(status)) { returns -1, signaling an error. It printf(“Child %d exited with status: %d.\n”, pid, WEXITSTATUS(status)); also sets the global variable } else { errno printf(“Child %d terminated abnormally\n”, pid); to ECHILD. We want to } check that errno == ECHILD to return 0; ensure we didn't exit for some } other error. assert(errno == ECHILD); } Recap: pid_t fork() pid_t waitpid(pid_t pid, int *status, int options); You now know how to use fork() to create new processes that are a clone of the current process. And you've seen how to use waitpid to help manage and clean up these child processes from the parent process. Next time: int execvp(const char *path, char *argv[]); You'll learn how to use the very cool execvp function to take a process and replace all of its data with that of some new program. The vast majority of fork() calls that happen in real code create a clone just so that it can be immediately replaced with something else. Then we will use the trio of fork, waitpid, and execvp to implement a shell just like the one you use when you ssh into myth!
© Copyright 2024