COMP 3150Operating Systems

01 Objectives

By the end of this laboratory you will be able to

  • Create processes with fork and understand the return-value distinction between parent and child.
  • Replace a process image with execvp and pass arguments correctly.
  • Wait for children with wait and reap zombies.
  • Inspect a live process's PCB through /proc/<pid>.
  • Distinguish a CPU-bound process from an I/O-bound one using state and switch counters.
  • Implement a minimal shell that reads a command, forks, execs, and waits.

02 Background

The process is the fundamental abstraction of the whole course. This laboratory has you interact with the process interface directly. Every step corresponds to a concept from Unit 3 — the fork/exec/wait cycle, the process state model, the PCB, the CPU-bound vs. I/O-bound distinction, and the context switch.

The laboratory is organised as four parts. Part A writes small programs to explore fork and exec. Part B examines process states and PCBs on live processes. Part C compares CPU-bound and I/O-bound processes. Part D implements a minimal shell, which is the capstone exercise for this unit.

Before you begin

Complete Lessons 3.1 through 3.5. The laboratory assumes you understand the state model, the PCB, and the difference between voluntary and involuntary context switches. If any of these is unclear, review the relevant lesson before continuing.

03 Setup

$ mkdir -p ~/comp3150/labs/lab-2
$ cd ~/comp3150/labs/lab-2
$ git init
$ cat > .gitignore << 'EOF'
*.o
fork_demo
exec_demo
zombie_demo
states_demo
minishell
EOF
$ git add .gitignore
$ git commit -m "Initialise Lab 2 workspace"

Take a snapshot named before-lab-2.

04 Part A — fork, exec, and wait

A.1 · fork: creating a child

Write a program fork_demo.c that:

  1. Prints the parent's PID and the current time.
  2. Calls fork().
  3. In both parent and child, prints the process's own PID, its parent's PID, and the value returned by fork.
  4. In both processes, sleeps for two seconds so that they overlap.
  5. In both, prints whether it is the parent or the child and exits.

Compile with gcc -Wall -Wextra -g -o fork_demo fork_demo.c. Run the program five times. Record in your lab report:

  • Whether the order of the parent and child output varies between runs.
  • Which process runs first in each run, and whether it is predictable.
  • What value fork returned in the parent and in the child.

Then answer: why is the order not deterministic, and what does that tell you about the scheduler?

A.2 · exec: replacing the process image

Write a program exec_demo.c that:

  1. Prints its own PID.
  2. Calls execvp("ls", args) with the argument list {"ls", "-la", "/tmp", NULL}.
  3. If execvp returns, prints an error message to standard error and exits with status 1.

Compile and run. Observe that:

  • The PID printed by the program before execvp is the same PID that appears in the ls output's process context. (You can verify this by wrapping the run with strace -e trace=execve.)
  • The program's original code is not resumed after execvp — the process image has been replaced.
  • The error message is not printed, because execvp succeeded.

Now modify the program to call execvp("this_program_does_not_exist", args). Rebuild and run. Confirm that the error message is printed and the exit status is 1.

A.3 · zombies and reaping

Write a program zombie_demo.c that:

  1. Forks a child.
  2. In the child, exits immediately with status 42.
  3. In the parent, sleeps for 30 seconds without calling wait.

While the parent sleeps, run ps aux | grep Z in another terminal. Record the state code of the child. Then kill the parent with kill -9 and observe that the child disappears (the kernel reparents it to PID 1, which reaps it).

Now modify the program so the parent calls wait(&status) and prints the child's exit status. Rebuild and run. Confirm that no zombie appears during the sleep, and that the reported exit status is 42. Record the observation.

05 Part B — Process states and PCBs

B.1 · reading the state field

Write a program states_demo.c that:

  1. Forks a child that prints its PID and then sleeps for 30 seconds.
  2. In the parent, prints the child's PID and then sleeps for 30 seconds too.

While both processes are sleeping, in another terminal:

  1. Look up the child's PID with ps -p <PID> -o pid,stat,cmd. Record the state code.
  2. Read the state field directly with awk '{print $3}' /proc/<PID>/stat. Record the value.
  3. Send SIGSTOP to the child with kill -STOP <PID>. Re-check the state. What changed?
  4. Send SIGCONT to resume it with kill -CONT <PID>. Re-check the state.
  5. Kill the child with SIGKILL. Re-check the state — but this time observe that the parent still has not called wait, so the child remains a zombie. Record the state code.

In your lab report, describe the sequence of state codes you observed and map each to the corresponding state in the five-state model.

B.2 · inspecting a PCB

Choose any long-running process on your system (your shell is a good candidate). For that PID, extract and interpret the following fields:

  • From /proc/<PID>/status:
    • Name, Pid, PPid, State
    • Uid (all four variants — what do they mean?)
    • VmRSS, VmSize (what is the difference?)
    • voluntary_ctxt_switches, nonvoluntary_ctxt_switches
  • From /proc/<PID>/stat:
    • Field 14 (utime), field 15 (stime) — the process's own CPU time
    • Field 10 (minflt), field 12 (majflt) — page faults
    • Field 42 (nvcsw), field 43 (nivcsw) — the same switch counters as above
  • From /proc/<PID>/sched:
    • se.vruntime — the CFS virtual runtime
    • nr_switches, nr_voluntary_switches, nr_involuntary_switches

Write a short paragraph in your lab report explaining what each group of fields is for, referencing the four PCB groups from Lesson 3.3.

06 Part C — CPU-bound vs. I/O-bound processes

Compare two processes with opposite workload profiles. In one terminal, start:

$ yes > /dev/null &
$ PID_CPU=$!
$ echo "CPU-bound PID: $PID_CPU"

In the same terminal or another, start:

$ find / -type f > /dev/null 2>&1 &
$ PID_IO=$!
$ echo "I/O-bound PID: $PID_IO"

Wait thirty seconds so both have accumulated meaningful statistics. Then run:

for p in $PID_CPU $PID_IO; do
    echo "===== PID $p ====="
    ps -p $p -o pid,stat,cmd
    grep -E 'voluntary_ctxt_switches|nonvoluntary_ctxt_switches' /proc/$p/status
    awk '{print "utime:", $14, "stime:", $15, "nvcsw:", $42, "nivcsw:", $43}' /proc/$p/stat
done

Record the observations and answer the following questions in your lab report:

  1. Which process has accumulated more CPU time?
  2. Which process has more voluntary context switches? Why?
  3. Which process has more involuntary context switches? Why?
  4. Which process is currently in state R, and which is in state S? Explain in terms of the five-state model.
  5. What does the ratio between voluntary and involuntary switches tell you about each process's workload profile?

Now change the priority of the CPU-bound process:

renice +10 -p $PID_CPU
ps -p $PID_CPU -o pid,ni,pri,stat

Observe the nice value and priority before and after. Then run the CPU and I/O comparison again and note whether the CPU-bound process receives less CPU time. Explain the result.

Kill both processes when finished: kill $PID_CPU $PID_IO.

07 Part D — A minimal shell

Write a program minishell.c that implements the core of a shell. This is the capstone exercise for the unit — it exercises the fork / exec / wait cycle on which the whole UNIX process model rests.

The shell must:

  1. Print a prompt (for example, minishell> ) to standard output and read a line from standard input.
  2. Parse the line into a command name and arguments, using whitespace as the separator. You may use strtok or write your own tokenizer.
  3. Exit the shell when the user types exit or when end-of-file is reached.
  4. Otherwise, fork a child. In the child, call execvp with the parsed command and arguments. In the parent, call wait for the child, and then loop back to read the next command.
  5. If execvp fails (command not found), print an error message to standard error and exit the child with a non-zero status.
  6. Print the exit status of the child after each command completes (for example, [exit 0]).

Build and run your shell:

$ gcc -Wall -Wextra -g -o minishell minishell.c
$ ./minishell
minishell> ls /tmp
minishell> echo hello world
minishell> /bin/true
[exit 0]
minishell> /bin/false
[exit 1]
minishell> this_command_does_not_exist
minishell> exit

Extensions (required for full credit)

  1. Add cd support. The command cd <path> should change the shell's own working directory (not a child's). Implement this by calling chdir directly in the shell, without forking.
  2. Add pwd support. The command pwd should print the shell's current working directory. You may call getcwd directly (without forking), or simply exec the system /bin/pwd. The former is more instructive.
  3. Add basic redirection. Support the syntax cmd > file and cmd < file. In the child, before calling execvp, open the file and use dup2 to make it the new standard output or standard input, then close the original descriptor.

Test each extension. Then commit the finished shell to your Git repository.

08 Deliverables

  • Source files: fork_demo.c, exec_demo.c, zombie_demo.c, states_demo.c, minishell.c, and a Makefile that builds all of them.
  • Lab report with the following sections:
    1. Setup. The VM configuration and any issues encountered.
    2. Part A. The five runs of fork_demo, the observation about non-deterministic ordering, the exec_demo output, the zombie observation.
    3. Part B. The sequence of state codes for the child in states_demo, mapped to the five-state model. The PCB field analysis for a live process.
    4. Part C. The comparison table for the CPU-bound and I/O-bound processes, with explanations of each difference. The nice value experiment.
    5. Part D. The full source of minishell.c, a session transcript showing each required feature, and a short description of how the extensions were implemented.
    6. Reflection. A 250–350 word reflection on the fork / exec / wait cycle and what it reveals about the UNIX process model.

09 Grading rubric

Lab 2 grading rubric
Criterion Weight What it evaluates
Part A — fork, exec, wait20%All three programs work correctly. Non-deterministic ordering observed and explained. Zombie lifecycle demonstrated.
Part B — states and PCBs20%State transitions observed correctly and mapped to the model. PCB field analysis thorough and accurate.
Part C — process profiles20%Comparison is complete and reasoning is correct. Nice experiment interpreted correctly.
Part D — minimal shell30%Shell implements the fork / exec / wait cycle, handles all required features, and the extensions work. Code is correct and readable.
Report quality10%Clear structure, correct use of terminology, reflection demonstrates understanding of the process model.
Total100%

10 Summary

  • You created processes with fork, replaced their images with exec, and reaped them with wait.
  • You observed the five-state model on live processes, including the zombie state.
  • You inspected a PCB through /proc/<pid> and interpreted each field in terms of the four PCB groups.
  • You distinguished a CPU-bound process from an I/O-bound one using state, CPU time, and context-switch counters.
  • You built a minimal shell that implements the fork / exec / wait cycle — the same mechanism that any real shell uses.
👤