execvp
Principles of Computer Systems
Winter 2020
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Nick Troccoli
Reading: Bryant & O'Hallaron, Chapters 10 and 8
Creating processes and running other programs
Inter-process communication
Signals
Race Conditions
fork()
pid_t pidOrZero = fork();
// both parent and child run code here onwards
printf("This is printed by two processes.\n");
fork()
What happens to variables and addresses?
// fork-copy.c
int main(int argc, char *argv[]) {
char str[128];
strcpy(str, "Hello");
printf("str's address is %p\n", str);
pid_t pid = fork();
if (pid == 0) {
// The child should modify str
printf("I am the child. str's address is %p\n", str);
strcpy(str, "Howdy");
printf("I am the child and I changed str to %s. str's address is still %p\n", str, str);
} else {
// The parent should sleep and print out str
printf("I am the parent. str's address is %p\n", str);
printf("I am the parent, and I'm going to sleep for 2 seconds.\n");
sleep(2);
printf("I am the parent. I just woke up. str's address is %p, and its value is %s\n", str, str);
}
return 0;
}
fork()
How can the parent and child use the same address to store different data?
$ ./fork-copy
str's address is 0x7ffc8cfa9990
I am the parent. str's address is 0x7ffc8cfa9990
I am the parent, and I'm going to sleep for 2 seconds.
I am the child. str's address is 0x7ffc8cfa9990
I am the child and I changed str to Howdy. str's address is still 0x7ffc8cfa9990
I am the parent. I just woke up. str's address is 0x7ffc8cfa9990, and its value is Hello
fork()
Isn't it expensive to make copies of all memory when forking?
$ ./fork-copy
str's address is 0x7ffc8cfa9990
I am the parent. str's address is 0x7ffc8cfa9990
I am the parent, and I'm going to sleep for 2 seconds.
I am the child. str's address is 0x7ffc8cfa9990
I am the child and I changed str to Howdy. str's address is still 0x7ffc8cfa9990
I am the parent. I just woke up. str's address is 0x7ffc8cfa9990, and its value is Hello
int main(int argc, char *argv[]) {
// Initialize the random number with a "seed value"
// this seed state is used to generate future random numbers
srandom(time(NULL));
printf("I'm unique and just get printed once.\n");
pid_t pidOrZero = fork();
exitIf(pidOrZero == -1, kForkFailure, stderr, "Call to fork failed... aborting.\n");
// force exactly one of the two to sleep (why exactly one?)
bool isParent = pidOrZero != 0;
int result = random() % 2;
if ((result == 0) == isParent) {
sleep(1);
}
printf("I get printed twice (this one is being printed from the %s).\n", isParent ? "parent" : "child");
return 0;
}
int main(int argc, char *argv[]) {
// Initialize the random number with a "seed value"
// this seed state is used to generate future random numbers
srandom(time(NULL));
printf("I'm unique and just get printed once.\n");
pid_t pidOrZero = fork();
exitIf(pidOrZero == -1, kForkFailure, stderr, "Call to fork failed... aborting.\n");
// force exactly one of the two to sleep (why exactly one?)
bool isParent = pidOrZero != 0;
int result = random() % 2;
if ((result == 0) == isParent) {
sleep(1);
}
printf("I get printed twice (this one is being printed from the %s).\n", isParent ? "parent" : "child");
return 0;
}
Key Idea: all state is copied from the parent to the child, even the random number generator seed! Both the parent and child will get the same return value from random().
int main(int argc, char *argv[]) {
printf("Starting the program\n");
pid_t pidOrZero1 = fork();
pid_t pidOrZero2 = fork();
if (pidOrZero1 != 0 && pidOrZero2 != 0) {
printf("Hello\n");
}
if (pidOrZero2 != 0) {
printf("Hi there\n");
}
return 0;
}
How many processes run in total?
a) 1 b) 2 c) 3 d) 4
How many times is "Hello" printed?
a) 1 b) 2 c) 3 d) 4
How many times is "Hi there" printed?
a) 1 b) 2 c) 3 d) 4
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);
waitpid()
We can use WIFEXITED and WEXITSTATUS (among others) to extract info from the status.
(full program, with error checking, is right here)
// waitpid.c
int main(int argc, char *argv[]) {
printf("Before.\n");
pid_t pidOrZero = fork();
printf("After.\n");
if (pidOrZero == 0) {
printf("I'm the child, and the parent will wait up for me.\n");
return 110;
} else {
int status;
int result = waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Child terminated abnormally.\n");
}
return 0;
}
}
The output will be the same every time this program runs
$ ./waitpid
Before.
After.
After.
I am the child, and the parent will wait up for me.
Child exited with status 110.
$
waitpid()
A parent process should always wait on its children processes.
waitpid()
(Wait what?)
/**
* Reads the specified sector (e.g. block) from the disk. Returns the number of bytes read,
* or -1 on error.
*/
int diskimg_readsector(int fd, int sectorNum, void *buf);
One function that can be tricky to write is the following:
/**
* Gets the location of the specified file block of the specified inode.
* Returns the disk block number on success, -1 on error.
*/
int inode_indexlookup(struct unixfilesystem *fs, struct inode *inp, int blockNum);
For the assignment, you will also have to search through directories to locate a particular file.
We now know the answers to the following questions:
A parent can call fork
multiple times, but must reap all the child processes.
Demo: Let's see how we might use this (reap-as-they-exit.c)
What if we want to wait for children in the order in which they were created?
Check out the abbreviated program below (full program with error checking right here):
// reap-in-fork-order.c
int main(int argc, char *argv[]) {
pid_t children[8];
for (size_t i = 0; i < 8; i++) {
if ((children[i] = fork()) == 0) exit(110 + i);
}
for (size_t i = 0; i < 8; i++) {
int status;
pid_t pid = waitpid(children[i], &status, 0);
assert(pid == children[i]);
assert(WIFEXITED(status) && (WEXITSTATUS(status) == (110 + i)));
printf("Child with pid %d accounted for (return status of %d).\n",
children[i], WEXITSTATUS(status));
}
return 0;
}
reap-in-fork-order
executable. The pids change between runs, but even those are guaranteed to be published in increasing order.myth60$ ./reap-as-they-exit
Child with pid 4689 accounted for (return status of 110).
Child with pid 4690 accounted for (return status of 111).
Child with pid 4691 accounted for (return status of 112).
Child with pid 4692 accounted for (return status of 113).
Child with pid 4693 accounted for (return status of 114).
Child with pid 4694 accounted for (return status of 115).
Child with pid 4695 accounted for (return status of 116).
Child with pid 4696 accounted for (return status of 117).
myth60$
The most common use for fork is not to spawn multiple processes to split up work, but instead to run a completely separate program under your control and communicate with it.
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.path
and argv[0]
end up being the same exact string.execvp
fails to cannibalize the process and install a new executable image within it, it returns -1 to express failure.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[]);
We are going to use execvp to implement our own shell program!
Demo: first-shell.c
mysystem
spawns a child process to perform some task and waits for it to complete.execvp
, because we know that if it returns at all, it returns a -1. If that happens, we need to handle the error and make sure the child process terminates, via an exposed exit(0)
call.execvp
inside parent and forgo the child process altogether? Becauseexecvp
would consume the calling process, and that's not what we want.static int mysystem(const char *command) {
pid_t pid = fork();
if (pid == 0) {
char *arguments[] = {"/bin/sh", "-c", (char *) command, NULL};
execvp(arguments[0], arguments);
printf("Failed to invoke /bin/sh to execute the supplied command.");
exit(0);
}
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status);
}
Next time: inter-process communication