Laboratory 2 · Related unit: Unit 3
Process Management
Hands-on experience with the process abstraction. Create, observe, and terminate processes, examine their PCBs through /proc, compare a CPU-bound process against an I/O-bound one, and build a minimal shell that implements the fork / exec / wait cycle for real.
01 Objectives
By the end of this laboratory you will be able to
- Create processes with
forkand understand the return-value distinction between parent and child. - Replace a process image with
execvpand pass arguments correctly. - Wait for children with
waitand 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:
- Prints the parent's PID and the current time.
- Calls
fork(). - In both parent and child, prints the process's own PID, its parent's PID, and the value returned by
fork. - In both processes, sleeps for two seconds so that they overlap.
- 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
forkreturned 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:
- Prints its own PID.
- Calls
execvp("ls", args)with the argument list{"ls", "-la", "/tmp", NULL}. - If
execvpreturns, prints an error message to standard error and exits with status 1.
Compile and run. Observe that:
- The PID printed by the program before
execvpis the same PID that appears in thelsoutput's process context. (You can verify this by wrapping the run withstrace -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
execvpsucceeded.
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:
- Forks a child.
- In the child, exits immediately with status 42.
- 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:
- Forks a child that prints its PID and then sleeps for 30 seconds.
- In the parent, prints the child's PID and then sleeps for 30 seconds too.
While both processes are sleeping, in another terminal:
- Look up the child's PID with
ps -p <PID> -o pid,stat,cmd. Record the state code. - Read the state field directly with
awk '{print $3}' /proc/<PID>/stat. Record the value. - Send
SIGSTOPto the child withkill -STOP <PID>. Re-check the state. What changed? - Send
SIGCONTto resume it withkill -CONT <PID>. Re-check the state. - Kill the child with
SIGKILL. Re-check the state — but this time observe that the parent still has not calledwait, 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,StateUid(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 runtimenr_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:
- Which process has accumulated more CPU time?
- Which process has more voluntary context switches? Why?
- Which process has more involuntary context switches? Why?
- Which process is currently in state
R, and which is in stateS? Explain in terms of the five-state model. - 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:
-
Print a prompt (for example,
minishell>) to standard output and read a line from standard input. -
Parse the line into a command name and arguments, using whitespace as the separator. You may use
strtokor write your own tokenizer. -
Exit the shell when the user types
exitor when end-of-file is reached. -
Otherwise, fork a child. In the child, call
execvpwith the parsed command and arguments. In the parent, callwaitfor the child, and then loop back to read the next command. -
If
execvpfails (command not found), print an error message to standard error and exit the child with a non-zero status. -
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)
-
Add
cdsupport. The commandcd <path>should change the shell's own working directory (not a child's). Implement this by callingchdirdirectly in the shell, without forking. -
Add
pwdsupport. The commandpwdshould print the shell's current working directory. You may callgetcwddirectly (without forking), or simply exec the system/bin/pwd. The former is more instructive. -
Add basic redirection. Support the syntax
cmd > fileandcmd < file. In the child, before callingexecvp, open the file and usedup2to 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:
- Setup. The VM configuration and any issues encountered.
- Part A. The five runs of
fork_demo, the observation about non-deterministic ordering, theexec_demooutput, the zombie observation. - 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. - Part C. The comparison table for the CPU-bound and I/O-bound processes, with explanations of each difference. The nice value experiment.
- 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. - Reflection. A 250–350 word reflection on the fork / exec / wait cycle and what it reveals about the UNIX process model.
09 Grading rubric
| Criterion | Weight | What it evaluates |
|---|---|---|
| Part A — fork, exec, wait | 20% | All three programs work correctly. Non-deterministic ordering observed and explained. Zombie lifecycle demonstrated. |
| Part B — states and PCBs | 20% | State transitions observed correctly and mapped to the model. PCB field analysis thorough and accurate. |
| Part C — process profiles | 20% | Comparison is complete and reasoning is correct. Nice experiment interpreted correctly. |
| Part D — minimal shell | 30% | Shell implements the fork / exec / wait cycle, handles all required features, and the extensions work. Code is correct and readable. |
| Report quality | 10% | Clear structure, correct use of terminology, reflection demonstrates understanding of the process model. |
| Total | 100% |
10 Summary
- You created processes with
fork, replaced their images withexec, and reaped them withwait. - 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.