Overview
The purpose of this lab is to become more familiar with the mechanisms used in Unix/Linux systems for process creation, execution management, blocking and termination. Specifically, it introduces and examines the fork()
, exec()
, wait()
, and exit()
system calls.
Objectives
- Explore process control related system calls in Unix/Linux
- Continue to explore and retrieve relevant information from manual pages
Prerequisites
This lab assumes that you:
- know how to open manual pages, search for important information within these pages
- understand how the manual pages are organized into different sections (1, 2, 3, ...)
Activities
Work your way through the following exercises, demonstrating your knowledge of the material by answering the numbered questions.
Some of the questions may require your skill in reading technical details from the manual pages
Submit a detailed lab report.
- Include the answers to the numbered questions.
- Attach all modified/written source code.
- The numbered questions (15 of them) can be answered as a group. However the programming assignment at the end of the lab must be completed individually
Be prepared to demo the Programming Assignment and to answer questions. about its operation.
TIP
The sample programs throughout the lab handout are provided in two languages: C and Rust. Only the C programs are required to complete the lab.
Use the Rust version of the sample program if you are planning to write the shell programming assignment (at the end of the this handout) in Rust as an extra credit. Some of the instructions below many not apply to the Rust sample programs and you are expected to figure out the correct instruction yourself.
You may refer to the Rust Quickstart or use another resource of your preference to setup your Rust project.
To compile the Rust sample program you must add the nix
crate ("package") as a new dependency in Cargo.toml
:
[dependencies]
nix = "0.15.0"
The online documentation of the nix crate shows you more modules/functions accessible to a Rust program.
Error Checking/Reporting
Among two headings in a manual page in section 2 (system calls) and section 3 (library functions) that programmers tend to overlook are "RETURN VALUES" and "ERRORS". Under these two headings you typically find detailed information that can be used to write the correct logic in response to calling a function.
- Open the man page of
scanf()
and navigate to its "ERRORS" sections and you'll find symbolic names such as EAGAIN, EBADF, etc. In general, these symbolic names are name with 'E' prefix, and they are actually integer constants defined in one of the system header files.
- Open other man pages from section 2 or 3 and look for more error codes and explain the meaning of each code.
To check these errors in your C/C++ program, check the most recent value of the global (predefined) variable errno
. Alternatively, you can use perror()
to print a more "human readable" error string (instead of printing the integer error code).
TIP
The perror()
function may be useful for debugging purposes in your programming assignments
Sample Program 0
The following sample program takes user input (a number) and prints its square root. Useful programming tricks in the following sample code that will help in debugging future programming assignments are checking the global variable errno
and calling perror()
. To use them be sure to include errno.h
in your C code.
File 1 of 1: sqrt-error.c
Download// Be sure to link this program with the math library (-lm)
#include <stdio.h>
#include <math.h>
#include <errno.h>
int main() {
double x, z;
printf ("Enter a number: ");
scanf ("%lf", &x);
z = sqrt(x);
if (errno != 0)
perror("Bad result");
else {
perror("Normal operation");
printf ("Result is %.2f\n", z);
}
return 0;
}
Compile the program and then run it twice:
- Enter a positive number the first time
- Enter a negative number the second time
- Open the man page of
sqrt()
, what is the symbolic error code that may be set intoerrno
by this math function?
Process Creation
All processes in UNIX are created, or spawned, from existing processes via the fork()
system call. Both processes (parent and child) continue execution to the next instruction after the fork()
call.
- Review your class notes, and read your textbook, the man pages, or a reference book to understand what the
fork()
call does and how it operates. - Familiarize yourself with the
ps
(Process Status) utility and its various options — it provides a great deal of useful information about a process. - Review the command-line mechanism (
&
) for inducing background execution of a process and thesleep()
function for temporarily suspending execution of a process.
Sample Program 1
The following sample program illustrates the operation of the fork()
system call.
Start up a Terminal session, which provides you with the UNIX command line interface. Perform the following operations and answer the questions:
- compile and run Sample Program 1 (use
clang
)
By comparing the manual pages of
puts()
andprintf()
(both are from section 3) list at least two differences between the two library functions.how many lines are printed by the program?
- How many lines by the parent process?
- How many lines by the child process?
describe what is happening to produce the answer observed for the above question
Understanding parent-child process relationship
Insert a long delay to the function
sleep()
after the fork in Sample Program 1 and recompile. This delay should be long enough for you to inspect the process properties using theps
command in the next twp steps belowrun Program 1 in the background (use
&
)bash./a.out & # run the process in background
Consult the man pages for the
ps
command; they will help you determine how to display and interpret the required information (often referred to as "verbose mode" or "long format"). Then with appropriate options, observe and report the PIDs and the status (i.e. state info) of your processes.
Provide a brief explanation process of your observations.
Sample Program 2
File 1 of 1: lab02_b.c
Download#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i, limit;
if (argc < 2) {
fputs("Usage: must supply a limit value\n", stderr);
exit(1);
}
limit = atoi(argv[1]);
fork();
fork();
pid_t who = getpid();
for (i = 0; i < limit; i++)
printf("PID:%d %d\n", who, i);
return 0;
}
Perform the following operations and answer the questions. Study the code for Sample Program 2 until you understand it.
create a diagram illustrating how Sample Program 2 executes (i.e. give a timeline diagram, similar to used in lecture). Include important events in each timeline.
- Suggestion: compile and run the program several times with a small input value (e.g. 10) so that you understand exactly what is happening.
- Then try using much larger values (1000, 5000 or more) for the command-line argument.
In the context of our classroom discussions on process state, process operations, process scheduling, etc., describe what you observed and try explain what is happening to produce the observed results. This is primarily an experiment; look for apparent anomalies and try to explain them based on classroom discussion of process operations.
Process Suspension and Termination
This section introduces the wait()
system call and the exit()
function, which are usually related as in parent and child. Note that there are several different versions of wait()
(e.g. some specify who to wait for). The exit()
function causes program termination; resources are recovered, files are closed, resource usage statistics are recorded, and the parent is notified via a signal (provided it has executed a wait
). Refer to the man pages to learn the syntax for using these functions. For the wait()
function also pay attention to its return values.
Sample Program 3
File 1 of 1: lab02_c.c
Download#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
// use these variables
pid_t pid, child;
int status;
if ((pid = fork()) < 0) {
perror("fork failure");
exit(1);
}
else if (pid == 0) {
printf("I am child PID %ld\n", (long) getpid());
/* insert an appropriate form of the exit() function here */
}
else {
/* insert an appropriate form of the wait() system call here */
printf("Child PID %ld terminated with return status %d\n", (long) child, WEXITSTATUS(status));
}
return 0;
}
Perform the following operations and answer the questions:
- add function calls to Sample Program 3 so that it correctly uses
wait()
andexit()
. Basically, implement the comments, making use of the pre-declared variables referenced in theprintf()
statement.
- what line of code did you insert for the
wait()
system call? - who prints first, the child or the parent? Why?
- what two values are printed out by the parent in Sample Program 3 (No, not the actual numbers, but what they mean.) In other words, describe the interaction between the
exit()
function and thewait()
system call. You may want to experiment by changing the value in theexit()
call to better understand the interaction.
Running a Binary Executable
The exec()
family of system calls provides a means of specifying that a process (typically the just-spawned child) should be replaced with a new (external) binary executable. Specifically, the child process image is replaced with the external binary and executes concurrently with its parent.
Note
Remember that exec__()
does not create a new process. Only fork()
does.
Sample Program 4
To understand how to use this family of system calls work, it is important that you first understand how command line arguments are passed to a C program. Compile and run the following program:
#include <stdio.h>
int main(int argc, char* argv[]) {
for (int k = 0; k < argc; k++) {
printf ("Arg-%d is [%s]\n", k, argv[k]);
}
return 0;
}
Run it using several times while supplying different number of command line arguments:
./a.out # No args
./a.out 4 # 1 arg
./a.out 4 ever # 2 args
As mentioned in class, there are several different forms of the exec()
system call.
- Those with a 'v' (e.g.
execve()
) require a vector/array of pointers. - Those with an 'l' (e.g.
execle()
) expect a list of pointers. - Those with an 'e' allow you to specify an environmental variable,
- Those with a 'p' allow you to specify a pathname to the executable.
Using exec*()
Type the following command to show a 3-month calendar around May of 2045.
cal -3 5 2045
and let's say you want to run the above command from a C program. The easiest would be to use execl()
where arguments are supplied as a list ending with a NULL:
#include <unistd.h>
#include <stdio.h>
int main() {
// The list of args must end with a NULL
execl("/usr/bin/cal", "ignored-by-execl", "-3", "5", "2045", NULL);
// The second arg is "ignored" by execl() or execlp(), it would not
// affect MOST of the commands we run via exec__().
// Typically the second arg is the name of the program
// execl("/usr/bin/cal", "cal", "-3", "5", "2045", NULL);
perror("After exec()");
printf ("Just checking\n");
return 0;
}
- Compile and run the above program the verify you see the correct output
- Change "ignored-by-execl" to another text of your preference, recompile and rerun and verify that the new text has no effect on the calendar output.
Observations
Why is the absolute path
/usr/bin/cal
needed?- The first argument to
exec*()
informs the system call where to find the binary executable. Unlikeexeclp()
,execl()
will NOT search for the executable file in the current search path.
TIP
Use
which
to locate the absolute path of any binary executable. Try the following:bashwhich cal which firefox
- The first argument to
Why the "random string"? While the first argument tells
execl()
the location of the binary executable, the 2nd, 3rd, 4th, .... arguments are supplied tocal
as its command line arguments, as if you are typingcal -3 5 2045
from the command line,- "some random string" is passed as
argv[0]
of the process running/usr/bin/cal
- "-3" is passed as its
args[1]
- "5" is passed as its
args[2]
- "2045" is passed as its
args[3]
- "some random string" is passed as
Do you see the output "Just checking" or
perror()
output after the 3-month calendar is printed? Why or why not? Explain what happened.Make the following changes to Sample 4:
- Replace
/usr/bin/cal
with/usr/bin/calculus
(or any non-existant file) - Recompile and rerun the program. What output do you see now?
Explain what just happened, compare this to your observation at the previous question.
- Replace
Make the following changes to Sample 4
- Comment out the
execl()
call and add the followingexeclp()
call:
cexeclp("cal", "some random string", "-3", "5", "2045", NULL);
- Recompile and rerun
- Comment out the
Make the following changes to Sample 4
- Comment out the
execlp()
call and add the followingexecvp()
call:
cchar* run_args[] = {"some random string", "-3", "5", "2045", NULL}; execvp("cal", run_args);
- Recompile and rerun
- Comment out the
IMPORTANT
Notice that in steps 14 and 15 above, the last argument supplied to exec__()
is always the NULL character. Pay attention to this requirement when you write your own shell program below.
Sample Program 5
The following sample program shows one form of the exec()
call. It is used to execute any command (e.g. ls
, ps
) that is issued by the user.
File 1 of 1: lab02_d.c
Download#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
if (argc < 2) {
fputs("Usage: must supply a command\n", stderr);
exit(1);
}
puts("Before the exec");
if (execvp(argv[1], &argv[1]) < 0) {
perror("exec failed");
exit(1);
}
puts("After the exec");
return 0;
}
Perform the following operations and answer the questions:
- compile, run and test Sample Program 5 using various commands (e.g.
date
,ls
,firefox
, etc.)
- explain how the second argument passed to
execvp()
is used?
Lab Programming Assignment (Simple Shell)
WARNING
This part of the lab be completed individually
At this point you should have a good understanding of the relationship between the fundamental system calls and library functions involved in process management on a UNIX system. You should now be able to write your own simple command interpreter, or shell. To do so, you will need to combine all of the ideas (creation, suspension, execution, termination) covered in class and in the lab.
Your program (in C or C++) should:
display a prompt to the user
get and parse the user input
read a line into a
WARNING
Watch out for the newline character ("\n") that these functions return into your string variable. It may interfere with correct execution of
exec*()
.tokenize the input string using
strtok()
c#include <string.h> #include <stdio.h> #include <stdlib.h> int main() { char input1[] = "A test\ninput string"; char *tok; printf ("Using strtok() on %s\n", input1); // On the initial call to strtok(), its first arg is the input string // the delimiter is a SPACE or a NEWLINE tok = strtok(input1, " \n"); while (tok != NULL) { printf ("[%s]\n", tok); // On subsequent calls to strtok(), its first arg is NULL tok = strtok(NULL, " \n"); } return 0; }
spawn a child process to execute the command
preferably use
execvp()/execve()
as in Sample Program 5 of this lab. You must create and pass a vector/array of pointers to arguments.or (for a lesser credit) use
execlp()/execle()
as in the example call below. You must pass a fixed list of arguments.cexeclp ("prog_name", "prog_name", ARG1, ARG2, /* and more */, (char *) 0); // the LAST argument must be a NULL
TIP
A common issue the
exec__()
system call is not executing the intended program is extra whitespace characters in its argument(s). Read the DEBUGGING TIPS below to diagnose this potential issue.Properly handle error condition when the user enters an unknown command
Find and use the appropriate system call to collect resource usage statistics about each executed process. Hint: use
man -k usage
to find the appropriate system call.output the "user CPU time used" for the most recent child process spawned by the shell
output the number of "involuntary context switches" experienced by the most recent child process spawned by the shell
repeat until the user advises to "
quit
"
You may use the following pseudocode as the skeleton of your simple shell. In addition, refer to the textbook examples and the code segments from this lab's Sample Programs.
Perform necessary initialization
while (true) {
prompt user for command
parse the command
if (command is "quit") break the loop
fork a new process
(a) the child calls exec*() to run the command and exit()
(b) the parent calls wait() to retrieve the child status
}
perform cleanup
WARNING
Be sure to also test your shell to run unknown commands
Debugging Tips
When working with string inputs, invisible whitespace characters (TAB, NEWLINE, etc) may interfere with your logic. When debugging your string variables using printf()
, consider adding extra characters around the %s
format specifier so it is easier to "see" whether whitespace characters are present in your string variables:
printf ("%s", my_data);
printf ("|%s|", my_data); // With extra characters around %s
For easy identification of these whitespace characters in your string data
Grading Rubrics
Feature | Point |
---|---|
Show prompt | 1 |
Parse command with no parameters (e.g. "ls") | 1 |
Parse input with 1 parameter (e.g "ls -a") | 1 |
Parse input with 2 or more parameters | 2 |
Use fork() correctly to spawn child process | 1 |
Quit | 1 |
Proper use of wait() to avoid orphans | 1 |
Show amount of CPU time used | 1 |
Show number of context switches | 1 |
Penalty | Point |
---|---|
Segmentation fault/program crash | -2 |
Memory leak | -1 per violation |
Compile error/warning with -Wall | -1 per warning/error |
Poor comment or poor indentation | max -2 |
Extra Credits
- (1 pt) Show the total number of unknown commands entered by the user when the shell terminates
- Implement typical shell builtin commands such as:
cd
,history
,prompt
, ... (1 pt per feature, max 3 points)
Deliverables
Submit your code and sample output as part of your lab report. Be prepared to demo and discuss your program.