Skip to content

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:

yaml
[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.
  1. 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.

[Gist] File 1 of 1: sqrt-error.cDownload
// 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
  1. Open the man page of sqrt(), what is the symbolic error code that may be set into errno 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 the sleep() function for temporarily suspending execution of a process.

Sample Program 1

The following sample program illustrates the operation of the fork() system call.

[Gist] File 1 of 1: lab02_a.cDownload
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    puts("Before fork");
    fork();
    puts("After fork");
    return 0;
}

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)
  1. By comparing the manual pages of puts() and printf() (both are from section 3) list at least two differences between the two library functions.

  2. how many lines are printed by the program?

    • How many lines by the parent process?
    • How many lines by the child process?
  3. describe what is happening to produce the answer observed for the above question

  4. 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 the ps command in the next twp steps below

    • run 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

[Gist] File 1 of 1: lab02_b.cDownload
#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.

  1. 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.
  2. 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

[Gist] File 1 of 1: lab02_c.cDownload
#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() and exit(). Basically, implement the comments, making use of the pre-declared variables referenced in the printf() statement.
  1. what line of code did you insert for the wait() system call?
  2. who prints first, the child or the parent? Why?
  3. 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 the wait() system call. You may want to experiment by changing the value in the exit() 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:

c
#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:

bash
./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.

bash
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:

c
#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. Unlike execlp(), 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:

    bash
    which cal
    which firefox
  • Why the "random string"? While the first argument tells execl() the location of the binary executable, the 2nd, 3rd, 4th, .... arguments are supplied to cal as its command line arguments, as if you are typing cal -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]
  1. Do you see the output "Just checking" or perror() output after the 3-month calendar is printed? Why or why not? Explain what happened.

  2. 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.

  3. Make the following changes to Sample 4

    • Comment out the execl() call and add the following execlp() call:
    c
    execlp("cal", "some random string", "-3", "5", "2045", NULL);
    • Recompile and rerun
  4. Make the following changes to Sample 4

    • Comment out the execlp() call and add the following execvp() call:
    c
    char* run_args[] = {"some random string", "-3", "5", "2045", NULL};
    execvp("cal", run_args);
    • Recompile and rerun

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.

[Gist] File 1 of 1: lab02_d.cDownload
#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.)
  1. 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

    • character array (preferably using fgets() or
    • C++ string using (preferably using getline()

    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.

      c
      execlp ("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.

c

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:

c
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

FeaturePoint
Show prompt1
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 parameters2
Use fork() correctly to spawn child process1
Quit1
Proper use of wait() to avoid orphans1
Show amount of CPU time used1
Show number of context switches1
PenaltyPoint
Segmentation fault/program crash-2
Memory leak-1 per violation
Compile error/warning with -Wall-1 per warning/error
Poor comment or poor indentationmax -2

Extra Credits

  1. (1 pt) Show the total number of unknown commands entered by the user when the shell terminates
  2. 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.