Operating Systems: Three Easy Pieces (Process API)
Operating Systems: Three Easy Pieces
These are exercises for learning the UNIX APIs for managing processes. The focus is on learning these APIs rather than writing production C code.
1. Write a program that calls fork(). Before calling fork(), have the main process access a variable (e.g. x) and set its value to something (e.g. 100). What value is the variable in the child process? What happens to the variable when both the child and parent change the value of x?
The child process created by fork() gets its own memory space containing the same content as the parent’s memory space. Memory writes in one process do not affect the other, so changing the value of x in a process only affects the output of that process.
Program:
#include <stdio.h>
#include <unistd.h>
int main() {
int x = 100;
int rc = fork();
if (rc == 0) {
printf("Initial value of x in child process: %d\n", x);
x = 200;
printf("New value of x in child process: %d\n", x);
} else {
printf("Initial value of x in parent process: %d\n", x);
x = 300;
printf("New value of x in parent process: %d\n", x);
}
return 0;
}
Output:
Initial value of x in parent process: 100
New value of x in parent process: 300
Initial value of x in child process: 100
New value of x in child process: 200
2. Write a program that opens a file (with the open() system call) and then calls fork() to create a new process. Can both the child and parent access the file descriptor returned by open()? What happens when they are writing to the file concurrently, i.e. at the same time?
The file descriptor is stored in the parent’s the memory space before fork() is called, meaning it will be copied into the child process’s memory space so that both processes can access it. The opened file is shared by both processes. Both can write to it and the order in which they write to it is system dependant (on mine the parent appears always to write to it first).
Program:
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int fd = open("output.txt", O_CREAT | O_TRUNC | O_WRONLY, 0644);
int rc = fork();
if (rc == 0) {
printf("fd in child process: %d\n", fd);
write(fd, "!\n", 2);
} else {
printf("fd in parent process: %d\n", fd);
write(fd, "Hello World", 11);
}
return 0;
}
Output:
fd in parent process: 3
fd in child process: 3
cat output.txt:
Hello World!
3. Write another program using fork(). The child process should print “hello”; the parent process should print “goodbye”. You should try to ensure the child process always prints first; can you do this without calling wait() in the parent?
Calling waitpid() here is essentially the same as calling wait() given that there’s only one child process to wait on. Another option would be to call sleep() in the parent process, but does this ensure that the child process prints first? Or even with a substantial sleep duration, is it possible on some systems or in a contrived situation to see the parent finish sleeping and printing its message before the child process runs?
Program:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int rc = fork();
if (rc == 0) {
printf("hello");
} else {
waitpid(rc, NULL, 0);
printf("goodbye");
}
return 0;
}
Output:
hellogoodbye
4. Write a program that calls fork() and then calls some form of exec() to run the program /bin/ls. See if you can try all of the variants of exec(), including execl(), execle(), execlp(), execv(), execvp() and execve(). Why do you think there are so many variants of the same basic call?
A call to an exec() variant will replace the current process image with the program selected to run, so only the first call made below works. I’ve included them all just to comment on them.
#include <string.h>
#include <unistd.h>
int main() {
int rc = fork();
if (rc == 0) {
char* args[] = {"ls", "-l", NULL};
char* env[] = {"BLOCKSIZE=K", NULL};
// The first argument here is the path to the program file.
// Like the other `execl` functions, it's variadic and takes
// the arguments to the program to run as individual arguments
// to itself, terminated by a null pointer.
execl("/bin/ls", "ls", "-l", (char *)NULL);
// The 'p' is for "PATH", as in the PATH environment variable
// which is used by this function to locate the file with the
// name given as the first argument to `execlp`.
execlp("ls", "ls", "-l", (char *)NULL);
// The final argument here is an array of environment variables
// which will be accessible to the selected program.
execle("/bin/ls", "ls", "-l", (char *)NULL, env);
// The `execlv` functions take the arguments to the program to
// run as an array.
execv("/bin/ls", args);
// The version that locals the file by name using PATH.
execvp("ls", args);
// The version that takes an array of environment variables
// for the selected program to access.
execve("/bin/ls", args, env);
}
return 0;
}
5. Now write a program that uses wait() to wait for the child process to finish in the parent. What does wait() return? What happens if you use wait() in the child?
A call to wait() returns the PID of the terminated child process or on failure it returns -1. If you use wait() in the child it fails and returns -1.
Program:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int pid;
int rc = fork();
if (rc == 0) {
pid = wait(NULL);
printf("hello %d\n", pid);
} else {
pid = wait(NULL);
printf("goodbye %d\n", pid);
}
return 0;
}
Output:
hello -1
goodbye <pid>
6. Write a slight modification of the previous program, this time using waitpid() instead of wait(). When would waitpid() be useful?
waitpid() is useful if the caller wants to specify the PID of the child process to wait for and receive the return status of that process. The parent process can then take actions according to the outcome of a specific child process.
Program:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int rc = fork();
int status;
if (rc == 0) {
exit(EXIT_FAILURE);
} else {
waitpid(rc, &status, 0);
printf("child exited with status of %d\n", WEXITSTATUS(status));
}
return 0;
}
Output:
child exited with status of 1
7. Write a program that creates a child process, and then in the child closes standard output (STDOUT_FILENO). What happens if the child calls printf() to print some output after closing the descriptor?
Since no other file has been opened to direct the output of printf() to, nothing is output by the child process.
Program:
#include <stdio.h>
#include <unistd.h>
int main() {
int rc = fork();
if (rc == 0) {
close(STDOUT_FILENO);
printf("Hello World!\n");
}
return 0;
}
Output:
8. Write a program that creates two children, and connects the standard output of one to the standard input of the other, using pipe() system call.
- Create a pipe
- Start two child processes
- In the first child process, change STDOUT_FILENO to refer to the pipe’s output and print something
- In the second child process, change STDIN_FILENO to refer to the pipe’s input, read from stdin and print the buffered input
Program:
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2];
char buf[6];
pid_t rc1;
pid_t rc2;
pipe(pipefd);
rc1 = fork();
rc2 = fork();
if (rc1 == 0) {
dup2(pipefd[1], STDOUT_FILENO);
printf("Hello");
} else if (rc2 == 0) {
dup2(pipefd[0], STDIN_FILENO);
read(STDIN_FILENO, buf, sizeof(buf)-1);
printf("%s World!\n", buf);
}
return 0;
}
Output:
Hello World!