This lab was developed by Prof. L. Felipe Perrone based on materials created by Prof. Phil Kearns for CSCI 315 at The College of William & Mary. Permission to reuse this material in parts or in its entirety is granted provided that this credits note is not removed. Additional students files associated with this lab, as well as any existing solutions can be provided upon request by e-mail to: perrone[at]bucknell[dot]edu
It should go without saying that all the work that you will turn in for this lab will be yours. Do not use AI to do the work for you; do not surf the web to get inspiration for this assignment; do not include code that was not written by you. You should try your best to debug your code on your own: use gdb, use printf, use your reasoning. It’s fine to get help from a colleague as long as that means getting assistance to identify the problem and doesn’t go as far as receiving source code to fix it (in writing or orally).
In this lab, we start experimenting with a few Unix system calls. The first one, we will look at is fork(2), which is used by a process to spawn an identical copy of itself. When we learn to use a system call or a library function, it is helpful to follow the simple workflow described as follows.
Start by reading the man page of the system call or library function in which you are interested. This will help you begin to understand how it works, but it will also show you some practical details that are essential to using it successfully. In the man page, pay close attention to the SYNOPSIS; it will tell you:
For instance, if we’re dealing with fork, you’ll see something like:
FORK(2) Linux Programmer's Manual FORK(2) NAME fork - create a child process SYNOPSIS #include <sys/types.h> #include <unistd.h> pid_t fork(void); DESCRIPTION fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process. ...
From this we learn that any program calling fork will need to #include the file unistd.h. The “angle brackets” indicate that these files reside in an include directory owned by the system (most often /usr/include).
We also learn that the fork call:
Once we have tried our best to understand that information, we should not be so bold as to throw code into a large program to see how things work out. It is often more productive to write a small program just to test that we have the right understanding about the behavior of the function. Once we have experimented a bit with this program and are convinced that the function does what we expect and that we have learned to use it effectively, we can use it in a larger context.
Here is a first experiment with fork aimed at understanding what a child process inherits from a parent.
#include <unistd.h> // need this for fork
#include <stdio.h> // need this for printf and fflush
int i = 7;
double x = 3.1415926;
int pid;
int main (int argc, char* argv[]) {
int j = 2;
double y = 0.12345;
if (pid = fork()) {
// parent code
printf("parent process -- pid= %d\n", pid); fflush(stdout);
printf("parent sees: i= %d, x= %lf\n", i, x); fflush(stdout);
printf("parent sees: j= %d, y= %lf\n", j, y); fflush(stdout);
} else {
// child code
printf("child process -- pid= %d\n", pid); fflush(stdout);
printf("child sees: i= %d, x= %lf\n", i, x); fflush(stdout);
printf("child sees: j= %d, y= %lf\n", j, y); fflush(stdout);
}
return(0);
}
This code is provided to you in file fork-test.c. Looking at this code, you may be inclined to think that you can infer the order of execution of these lines of C code. For instance: you might say that that parent executes first and the child executes next; or you might say that the order of execution is the one in which the program was written.
Don’t make the mistake of thinking that you can predict the order of execution of the actions in your processes! The process scheduler in the kernel will determine what executes when and your code should not rely on any assumptions of order of execution.
Create a Makefile that builds all the programs you created or modified for Pre-Lab 1 and Lab 1. You will work on this file incrementally to build the code of every subsequent problem in this lab assignment. Add to your git repo now and commit and push as you grow your Makefile.
Let’s start slowly by investigating what a child process may be inheriting from its parent process. First, let’s get this code to compile!
a) Take a look at the program given to you in file fork.c . Compile and execute the program. Add code to have both the child and the parent print out the value of the pid returned by the fork() system call.
int main(int argc, char *argv[]) {
int pid; int num;
if (--1 == (pid = fork())) {
perror("something went wrong in fork");
exit(-1);
} else if (0 == pid) {
for (num=0; num < 20; num++) {
printf("child: %d\n", num); fflush(stdout);
sleep(1);
}
} else {
for (num=0; num < 20; num+=3) {
printf("parent: %d\n", num); fflush(stdout);
sleep(1);
}
}
}
b) The variable num is declared before the call to fork() as shown in this program. After the call to fork(), when a new process is spawned, does there exist only one instance of num in the memory space of the parent process shared by the two processes or do there exist two instances: one in the memory space of the parent and one in the memory space of the child? Discuss your conclusion in lab01.txt.
Now, let’s experiment with forcing a specific order of termination of the processes. As given to you, the code for this problem makes no guarantee that the child will terminate before the parent does! With the concepts we have covered so far in class, we can use a very basic mechanism to establish order in process creation (with fork) and in process termination (with wait or waitpid).
c) Copy fork.c to file fork-wait.c and modify it so that you can guarantee that the parent process will always terminate after the child process has terminated. Your solution cannot rely on the termination condition of the for loops or on the use of sleep. The right way to handle this is using a syscall such as wait or waitpid – read their man pages before jumping into this task. One more thing: Modify the child process so that it makes calls to getpid(2) and getppid(2) and prints out the values returned by these calls.
When you have completed the problem, do the following:
This problem will help you remember some material you studied in CSCI 206 Computer Organization & Programming. Do you remember that every running program (aka. process) defines four segments of memory: text, data, stack, and heap?
a) Read fork-data.c carefully, then compile and run it. In lab01.txt explain in which segment of your running program the following variables reside: pid, x, y, i, and j.
b) In lab01.txt, discuss whether running fork-data.c allows you to conclude: (1) if the data segment and the stack segment of a parent process are copied over to the child process; (2) whether changes made to these variables by the child are seen by the parent. What you discover for (2) will tell you whether parent and child share the same memory for data and stack segments or if they each have their own separate segments.
Next, this problem will get you to investigate more deeply what a child process may inherit from its parent. This time, we will be working with files rather than variables. If a parent process has opened a file and then goes on to spawn a child process, you should wonder if the child will see the same file in open state. That is, will the file descriptor that the parent received after a call to open be usable in the child? Furthermore, by reading from a file shared by inheritance, does a process affect the “state” of this file that another process may be reading?
c) Copy the file given to you as fork-data.c to a new file called fork-file.c. Modify your new program so that before the if/fork structure, main creates and opens a file called data.txt and writes into it the string “this is a test for processes created with fork\nthis is another line”.
Note on opening to read or write a file. Be sure to use open(2), read(2), and write(2). Close the file right before the fork call (this will guarantee that all the writes to the file are flushed to disk). Immediately following the close call, open the file again for reading. Inside the parent code section of the program, have the parent issue a single call to read to get 5 characters from the file and print them to the terminal. Inside the child code of the program, have the child process issue a single call to read to get 5 characters from the file and print them to the terminal. Compile your code and run it to observe what happens.
d) In case it is not obvious: the file you open in main is visible in both child and parent processes. Experiment with your modified fork-file.c and write in your lab01.txt file the answers to the following questions: (1) if one process closes the file, can the other still read from it?; (2) say the child process reads from the “inherited” file; does that affect what the parent will read from the same file descriptor?
And now for something completely different. Every time you make a syscall or invoke a library function, take a look at the RETURN VALUE section of its man page. If it says something like “On error, -1 is returned, and errno is set appropriately,” you should consider wrapping that call with a function of your own. By doing so, you can effectively replace calls that your program makes to these services to your customized version of that function, which will react to errors in a standardized manner. In the remainder of this problem, you will write your first wrapper to a syscall. Many more will follow as we get into the semester.
e) Create a new function (outside main) in the file fork-file.c to wrap the call to fork and perform some basic error detection. This function should have the same prototype as the fork system call, but its name will start with a capital letter, that is, its prototype will be:
pid_t Fork(void);
In this new function, you will invoke the system call fork and check if the return value is -1. When that is the case, your function should invoke perror to print out a human readable error message and then call the library function exit with argument -1 to abort the program. This will terminate the process that called Fork and pass a return code to the creator of that process. (Hint: make sure to read the man pages to perror, exit, and any other system or library calls used in this lab and to #include in your code the header files you need.) After you create a wrapper for ANY function in this class, be sure that your programs use YOUR wrappers instead of the original functions.
When you have verified that your program works, do:
Before you get into this problem, let’s talk about a process’ termination status. In Problem 2, you were asked to check the return value of the call to fork(2) and to terminate the parent process when it fails. The mechanism for termination we suggested was to invoke the exit library call with argument -1. By convention, when a Unix process terminates without error, it returns or exits with status 0. When the termination status is different from 0, this convention indicates that some kind of error condition arose.
You can see how this works out in practice with a little experiment that you can run from your bash shell. The cat system utility, which resides in the /bin directory, can be used to display the content of a text file on the terminal. Try this out:
$ /bin/cat fork-file.c
Note that we are using the absolute path to invoke the cat utility and we are doing it just so that you don’t execute any other program with the same name in your PATH. If the file you passed to cat via the command line (that is, fork-file.c) is in your current working directory, its contents will appear on your terminal screen. When that is the case, the program cat terminates successfully and exits with status 0. You can learn what was the terminations status of the last program you executed by inspecting a shell variable, as follows:
$ echo $?
When all goes well in your execution of cat, this echo command will show 0 for termination status. Now, try to cat a file that doesn’t exist:
$ /bin/cat bogus.nuthin
You don’t have a file with that weird name, hopefully! See how you got a termination status different from 0? It was probably 1, in this case. Anyway, what this value in $? indicates is that something went wrong in the execution of the previous program. From now on, remember to use the termination status of a program to your advantage: make your processes use exit or have the main function in your programs return a non-zero value when things go so awfully wrong that you have to abort them.
You will practice exactly that in this problem. And you will also learn to create processes that are not identical clones of their parent. This latter part means you will practice using a function from the exec family.
a) Create a file called catcount.c in which you will write a program that receives one command line argument of type string: the name of a text file. Your program will work as described below.
We wrap up this assignment with a little bit of practice in working with a double pointer (that is, a pointer to pointer kind of variable). If you read the man page for execlp, you will notice in the SYNOPSIS section the definition of a variable that your programs will see when they #include the appropriate header file. The variable is defined in unistd.h as:
char **environ;
In order to use it in your program, you will need to define the variable as “extern”, as indicated below:
extern char **environ;
The keyword extern tells the linker that this variable is defined in a separately compiled module (in a library, in this case). Because of C’s duality between pointers and arrays, you can view environ as an array of pointers, where each element is a string that matches the following format:
KEYWORD=VALUE
For this problem, we don’t actually care about the format of each individual string. We are interested in being able to write code to print all these strings to the terminal. To achieve this goal, we have to traverse the array from first to last element printing each string we find followed by the “\n” (newline) character. Since this is a variable size array, the pointer value NULL is used as sentinel to mark the end of the array.
b) In your catcount.c program, create a function with the prototype shown below.
void print_environment(void);
Have your program call this function at the very start of main. The code of the function should be a simple loop to iterate through the array of strings environ up until its last element, printing to the terminal each of the elements it finds (again, follow each of these strings with a “\n”).
When you are done with this problem, do
Before turning in your work for grading, create a text file in your Lab 1 directory called submission.txt. In this file, provide a list to indicate to the grader, problem by problem, if you completed the problem and whether it works to specification. Wrap everything up by turning in this file and your Makefile from Problem 0 (if you haven’t done so already):
A word about submitting your work: it is your responsibility to ensure that everything that is necessary to build your programs is added, committed, and pushed to your git repository. You never add object or executable files to a git repo, but everything else you want graders to see must be there. It is unfair to our graders to expect that they should hound you to submit all the files needed for them to compile and test your work.