execvp
Principles of Computer Systems
Winter 2021
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
first-shell-soln.c
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?
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-copy.c
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
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[]) {
// Initialize the random number with a "seed value"
// this seed state is used to generate future random numbers
srandom(time(NULL));
printf("This program will make you question what 'randomness' means...\n");
pid_t pidOrZero = fork();
// Parent goes first - both processes *always* get the same roll (why?)
if (pidOrZero != 0) {
int diceRoll = (random() % 6) + 1;
printf("I am the parent and I rolled a %d\n", diceRoll);
sleep(1);
} else {
sleep(1);
int diceRoll = (random() % 6) + 1;
printf("I am the child and I'm guessing the parent rolled a %d\n", diceRoll);
}
return 0;
}
not-so-random.c
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()
int main(int argc, char *argv[]) {
printf("Before.\n");
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
sleep(2);
printf("I (the child) slept and the parent still waited up for me.\n");
} else {
pid_t result = waitpid(pidOrZero, NULL, 0);
printf("I (the parent) finished waiting for the child. This always prints last.\n");
}
return 0;
}
$ ./waitpid
Before.
I (the child) slept and the parent still waited up for me.
I (the parent) finished waiting for the child. This always prints last.
$
waitpid.c
waitpid()
The output will be the same every time! The parent will always wait for the child to finish before continuing.
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid == 0) {
printf("I'm the child, and the parent will wait up for me.\n");
return 110; // contrived exit status (not a bad number, though)
} else {
int status;
int result = waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Child terminated abnormally.\n");
}
return 0;
}
}
Pass in the address of an integer as the second parameter to get the child's status.
$ ./separate
I am the child, and the parent will wait up for me.
Child exited with status 110.
$
separate.c
A parent process should always wait on its children processes.
waitpid()
(Wait, what?)
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)
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):
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);
}
for (size_t i = 0; i < kNumChildren; i++) {
int status;
pid_t pid = waitpid(children[i], &status, 0);
assert(WIFEXITED(status));
printf("Child with pid %d accounted for (return status of %d).\n", children[i], WEXITSTATUS(status));
}
return 0;
}
reap-in-fork-order.c
$ ./reap-in-fork-order
Child with pid 12649 accounted for (return status of 110).
Child with pid 12650 accounted for (return status of 111).
Child with pid 12651 accounted for (return status of 112).
Child with pid 12652 accounted for (return status of 113).
Child with pid 12653 accounted for (return status of 114).
Child with pid 12654 accounted for (return status of 115).
Child with pid 12655 accounted for (return status of 116).
Child with pid 12656 accounted for (return status of 117).
$
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()
execvp
is a function that lets us run another program in the current process.
int execvp(const char *path, char *argv[]);
execvp()
It runs the executable at the specified path, completely cannibalizing the current process.
To run another executable, we must specify the (NULL-terminated) arguments to be passed into its main function, via the argv parameter.
execvp
has many variants (execle
, execlp
, and so forth. Type man
execvp
for more). We rely on execvp
in CS110.
int main(int argc, char *argv[]) {
char *args[] = {"/bin/ls", "-l", "/usr/class/cs110/lecture-examples", NULL};
execvp(args[0], args);
printf("This only prints if an error occurred.\n");
return 0;
}
$ ./execvp-demo
total 26
drwx------ 2 troccoli operator 2048 Jan 11 21:03 cpp-primer
drwx------ 3 troccoli operator 2048 Jan 15 12:43 cs107review
drwx------ 2 troccoli operator 2048 Jan 13 14:15 filesystems
drwx------ 2 troccoli operator 2048 Jan 13 14:14 lambda
drwxr-xr-x 3 poohbear root 2048 Nov 19 13:24 map-reduce
drwx------ 2 poohbear root 4096 Nov 19 13:25 networking
drwxr-xr-x 2 poohbear root 6144 Jan 22 08:58 processes
drwxr-xr-x 2 poohbear root 2048 Oct 29 06:57 threads-c
drwxr-xr-x 2 poohbear root 4096 Oct 29 06:57 threads-cpp
$
execvp()
execvp
is a function that lets us run another program in the current process.
int execvp(const char *path, char *argv[]);
execvp-demo.c
A shell is essentially a program that repeats asking the user for a command and running that command (Demo: first-shell-soln.c)
first-shell-soln.c
system()
The built-in system function can execute a given shell command.
int system(const char *command);
int main(int argc, char *argv[]) {
int status = system(argv[1]);
printf("system returned %d\n", status);
return 0;
}
$ ./system-demo "ls -l"
total 26
drwx------ 2 troccoli operator 2048 Jan 11 21:03 cpp-primer
drwx------ 3 troccoli operator 2048 Jan 15 12:43 cs107review
drwx------ 2 troccoli operator 2048 Jan 13 14:15 filesystems
drwx------ 2 troccoli operator 2048 Jan 13 14:14 lambda
drwxr-xr-x 3 poohbear root 2048 Nov 19 13:24 map-reduce
drwx------ 2 poohbear root 4096 Nov 19 13:25 networking
drwxr-xr-x 2 poohbear root 6144 Jan 21 19:38 processes
drwxr-xr-x 2 poohbear root 2048 Oct 29 06:57 threads-c
drwxr-xr-x 2 poohbear root 4096 Oct 29 06:57 threads-cpp
system returned 0
$
system-demo.c
mysystem()
We can implement our own version of system with fork(), waitpid() and execvp()!
int mysystem(const char *command);
One twist; not all shell commands are executable programs, and some need parsing.
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(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
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);
return WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status);
}
Here's the implementation, with minimal error checking (the full version is right here):
mysystem()
first-shell-soln.c
Next time: Interprocess communication
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
Parent
pidOrZero1 = nonzero
pidOrZero2 = nonzero
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;
}
First Child
pidOrZero1 = 0
pidOrZero2 = nonzero
Grandchild
pidOrZero1 = 0
pidOrZero2 = 0
Second Child
pidOrZero1 = nonzero
pidOrZero2 = 0
What if we want to spawn a single child and wait for that child before spawning another child?
static const int kNumChildren = 8;
int main(int argc, char *argv[]) {
for (size_t i = 0; i < kNumChildren; i++) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
printf("Hello from child %d!\n", getpid());
return 110 + i;
}
int status;
pid_t pid = waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
printf("Child with pid %d exited normally with status %d\n", pid, WEXITSTATUS(status));
} else {
printf("Child with pid %d exited abnormally\n", pid);
}
}
return 0;
}
spawn-and-reap.c
Check out the abbreviated program below (full program with error checking right here):