c-foundations Lesson 2 20 min read

What are Processes and How Do I Manage Them?

Every command creates a process — here's how to see them, control them, and stop them

Reading: Linux Text: Ch. 10 §1–6 (Processes, ps, top, Job Control, Signals, killall), pp. 131–144

After this lesson, you will be able to:

  • Explain the difference between a program (file on disk) and a process (running instance with a PID)
  • Use ps and ps aux to view running processes and interpret output columns
  • Run processes in the background with & and manage them using jobs, fg, and bg
  • Use Ctrl+C, Ctrl+Z, kill, and kill -9 to send signals to processes
  • Describe the Unix process lifecycle: fork(), exec(), run, exit(), wait()
  • Use echo $? to check exit status and explain the convention that 0 means success

What’s Actually Running on Your Computer?

When you type ls and see a file listing, something happened between your keypress and the output: the operating system created a process, ran the ls program inside it, collected the output, and cleaned up. All in milliseconds.

Every command, every program, every background service is a process. Right now, your terminal is a process. Your shell (bash) is a process. When you compile with gcc, that’s a process. When your compiled program runs, that’s another process. Understanding processes is understanding how your computer actually works.

We’ll use sleep — a command that just waits for a specified number of seconds — as our running example throughout this lesson. It’s perfect for experimenting because it runs long enough for you to inspect, suspend, and kill it.


What Is a Process?

A process is a running instance of a program. Think of it this way: a program is a recipe (instructions sitting on disk), and a process is someone actually cooking that recipe (instructions being executed, using memory and CPU time).

Every process has:

  • PID — Process ID, a unique number assigned by the kernel
  • PPID — Parent Process ID, the PID of the process that created this one
  • UID — User ID, which user owns this process
  • State — Running, sleeping, stopped, or zombie

Key insight: A program and a process are different things. A program is a file on disk (like /usr/bin/ls). A process is that program loaded into memory and running. You can have multiple processes running the same program simultaneously — like two terminal windows both running bash.

From Java: In CSCD 210, you may have created processes with ProcessBuilder or Runtime.getRuntime().exec("cmd"). Those are Java wrappers around the same Unix system calls you’ll learn here. Under the hood, the JVM calls fork() and exec() — the same functions you’ll use directly in C during Week 10. In Unix/C, there’s no class hierarchy or exception handling around process creation. You call fork(), check the return value, and manage the child yourself.

Quick Check: Can two processes run the same program at the same time?

Yes. Each process gets its own copy of the program in memory, its own PID, and its own state. Two people can both run ls at the same time — each gets their own process with a different PID. This is fundamental to how multi-user Unix systems work.

Check Your Understanding
The file /usr/bin/ls sits on disk. You type ls in two different terminals at the same time. How many processes are created?
A One — the OS reuses the same process for both
B Two — each terminal gets its own process with a unique PID
C Zero — ls is a shell builtin, not a process
D It depends on how much memory is available
Answer: B. A program is a file on disk; a process is a running instance. The same program (/usr/bin/ls) can have multiple processes running simultaneously, each with its own PID, memory, and state. Option C is wrong — ls is an external program, not a builtin.

Viewing Processes: ps and top

The ps command shows a snapshot of running processes. Start with the simplest form:

ps

This shows only processes in your current terminal — probably just bash and ps itself. To see everything on the system:

ps aux

The output has several columns. Here’s what they mean:

USER    PID  %CPU %MEM    VSZ   RSS TTY   STAT START   TIME COMMAND
student 1234  0.0  0.1  12345  6789 pts/0 Ss   09:00   0:00 bash
student 5678  2.3  0.5  45678 12345 pts/0 S+   09:15   0:02 ./myprogram

The key columns: PID (process ID — you’ll need this to kill processes), %CPU (how much CPU it’s using), STAT (its state), and COMMAND (what program is running).

To find a specific process, combine ps with grep:

ps aux | grep 'myprogram'

For a live, continuously updating view, use top:

top

top refreshes every few seconds, showing the busiest processes at the top. Press q to quit, M to sort by memory usage, P to sort by CPU usage. It’s like the Activity Monitor (macOS) or Task Manager (Windows), but in the terminal.

VS Code comparison: In VS Code, you might have seen processes via Task Manager or Activity Monitor when a build was slow. ps is the command-line version — and unlike a GUI, you can script it. ps aux | grep 'gcc' instantly finds all active compilations across the system.

Check Your Understanding
What does ps aux | grep 'python' do?
A Starts a new Python process
B Lists all running processes that have "python" in their command line
C Kills all Python processes
D Searches inside Python files for the text "aux"
Answer: B. ps aux lists all running processes, then the pipe sends that list to grep 'python', which filters for lines containing "python." This is how you find a specific process when you know part of its name.

Background Processes and Job Control

Normally, when you run a command, the shell waits for it to finish before showing a new prompt. That’s called running in the foreground. But sometimes you want to start a long-running command and keep using your terminal.

Running in the Background

Add & after any command to start it in the background:

sleep 60 &

The shell prints the job number and PID, then gives you a new prompt immediately:

[1] 12345
$

The sleep command is running (it will wait for 60 seconds), but your terminal is free. Use jobs to see what’s running in the background:

jobs
[1]+  Running                 sleep 60 &

Suspending and Resuming

What if you started a command in the foreground and realized you need your terminal back? You don’t have to kill it:

  1. Press Ctrl+Z — this suspends (pauses) the process
  2. Type bg — this resumes it in the background
  3. Or type fg — this brings it back to the foreground

Let’s try it with our running example:

sleep 120          # Starts in foreground — terminal is locked

Press Ctrl+Z:

[1]+  Stopped                 sleep 120

Now resume it in the background:

bg
[1]+ sleep 120 &

The process is running again, but now in the background. Use fg to bring it back to the foreground when you want, or just let it finish on its own.

The trick: The Ctrl+Z then bg combo is something you’ll use constantly. You start a compilation or test run, realize it’s slow, and want your terminal back. Suspend it, background it, and keep working.

Quick Check: What's the difference between Ctrl+Z and Ctrl+C?

Ctrl+Z suspends the process — it’s paused but still alive. You can resume it with fg or bg. Ctrl+C terminates the process — it sends SIGINT, and the process dies. The key difference: Ctrl+Z is reversible, Ctrl+C is not.

Check Your Understanding
You run sleep 300 in the foreground and realize you need your terminal back. You press Ctrl+Z, then type bg. What is the state of the sleep process?
A Terminated — Ctrl+Z killed it
B Suspended — bg has no effect on stopped processes
C Running in the background — Ctrl+Z suspended it, then bg resumed it
D Running in the foreground — bg brings it back
Answer: C. Ctrl+Z suspends (pauses) the process. bg resumes it in the background, so it keeps running but your terminal is free. Use fg to bring it back to the foreground if needed. This Ctrl+Zbg combo is one of the most useful job control patterns.

Signals: Talking to Processes

Signals are short messages the kernel sends to processes. You’ve already used two of them without knowing it:

Signal Number Keyboard Effect
SIGINT 2 Ctrl+C Interrupt — politely asks the process to stop
SIGTSTP 20 Ctrl+Z Suspend — pauses the process
SIGTERM 15 Terminate — politely asks the process to exit
SIGKILL 9 Kill — forcefully destroys the process (cannot be caught)
SIGCONT 18 Continue — resumes a suspended process

Sending Signals with kill

Despite its name, kill doesn’t always kill — it sends a signal. By default, it sends SIGTERM:

sleep 300 &                # Start a background process
ps aux | grep 'sleep'      # Find its PID
kill 12345                 # Send SIGTERM (polite request to stop)

If the process ignores SIGTERM, escalate to SIGKILL:

kill -9 12345              # SIGKILL — forced termination, no cleanup

To kill all processes with a given name:

killall sleep              # Kills every process named "sleep"

Watch out: Always try kill (SIGTERM) before kill -9 (SIGKILL). SIGTERM lets the program clean up — close files, free memory, save state. SIGKILL terminates instantly with no cleanup, which can leave temporary files or corrupted data. Think of SIGTERM as knocking on the door; SIGKILL is kicking it down.

Check Your Understanding
You run ./slow_program and it's taking forever. You want to pause it and get your terminal back, but you don't want to kill it. What do you press?
A Ctrl+Z — this suspends the process so you can resume it later
B Ctrl+C — this pauses the process
C Ctrl+D — this suspends the process
D Ctrl+X — this moves the process to the background
Answer: A. Ctrl+Z sends SIGTSTP, which suspends (pauses) the process. You can then type bg to resume it in the background or fg to bring it back. Ctrl+C sends SIGINT, which terminates the process — you'd lose it. Ctrl+D sends EOF (end-of-file), not a signal.

The Process Lifecycle

Every process in Unix follows this lifecycle:

  1. fork() — the parent process creates a copy of itself (a new child process)
  2. exec() — the child replaces itself with a new program
  3. Run — the child executes the program
  4. exit() — the child finishes and returns an exit status
  5. wait() — the parent collects the exit status

When you type ls in bash, here’s what happens behind the scenes:

  1. Bash calls fork() — now there are two copies of bash
  2. The child calls exec("/usr/bin/ls") — it stops being bash and becomes ls
  3. ls runs, prints the file listing to stdout
  4. ls calls exit(0) — 0 means success
  5. Bash (the parent) calls wait(), collects the exit status, and shows a new prompt

Why this matters: The fork/exec model is the foundation of Unix. Every program you’ve ever run in a terminal was launched this way. In Week 10, you’ll implement this yourself in C — writing a mini-shell that forks, execs, and waits. The concepts you learn here as a user will become the system calls you write as a programmer. See Lesson 5.1: Fork & Exec for the full implementation.

Zombie Processes

What happens if a child process finishes but the parent never calls wait()? The child becomes a zombie — it’s done executing, but its entry stays in the process table because the parent hasn’t collected the exit status yet. Zombies don’t consume CPU or significant memory; they’re just leftover bookkeeping entries.

You can spot zombies with ps — they show a state of Z:

ps aux | grep ' Z'

Zombies are cleaned up when the parent eventually calls wait(), or when the parent itself exits (at which point the init process adopts and reaps the zombie). In practice, a few zombies are harmless. Thousands of them indicate a parent with a bug.

Check Your Understanding
When you type ls in bash, what sequence of operations does the shell use to run it?
A The shell loads ls first, then creates a new process for it
B The shell replaces itself with ls, then restarts when ls finishes
C The shell forks a child process, then the child replaces itself with ls
D Bash directly runs ls without creating a new process
Answer: C. The shell first forks a copy of itself (child process), then the child replaces itself with the ls program via exec. The parent (bash) waits for the child to finish. Option B is wrong because replacing the shell would destroy it. Option D is wrong because ls is an external program that needs its own process (built-in commands like cd are different — they run directly in the shell).

Exit Status

Every process returns an exit status (also called a return code) when it finishes. Check the most recent exit status with echo $?:

ls /home
echo $?               # 0 — success
ls /nonexistent
echo $?               # 2 — error (no such file or directory)

The convention is simple: 0 means success, any non-zero value means an error. Different programs use different non-zero values to indicate different kinds of errors.

From Java: In Java, System.exit(0) means success, System.exit(1) means error. C uses the exact same convention: return 0; from main() signals success to the operating system. The shell checks this value with $? to know if your program worked. You’ll use this in every C program you write.

Quick Check: What does exit status 0 mean? What about non-zero?

Exit status 0 means success. Any non-zero value means error or failure. Different non-zero values can indicate different errors (e.g., 1 for general errors, 2 for misuse of a command). Check it with echo $? after any command.

Check Your Understanding
You compile and run your C program. Then you type echo $? and see 0. What does this tell you?
A The program produced no output
B The program used zero bytes of memory
C The program exited successfully — return 0 from main()
D The program crashed with a segmentation fault
Answer: C. Exit status 0 means success. In C, return 0; from main() sets this. Any non-zero value indicates an error. A segfault typically produces exit status 139 (128 + signal 11). $? is how the shell (and scripts) check whether the last command worked.

Handling Infinite Loops

When you start writing C programs, you’ll inevitably write one with an infinite loop. Here’s your escape plan:

Option 1: Press Ctrl+C — this sends SIGINT, which terminates most programs:

./buggy_program        # Stuck! Infinite loop
# Press Ctrl+C         # SIGINT sent — program stops

Option 2: If Ctrl+C doesn’t work (rare, but possible if the program catches SIGINT), open a second terminal and kill it manually:

ps aux | grep 'buggy_program'    # Find the PID
kill 12345                        # Try SIGTERM first
kill -9 12345                     # SIGKILL if SIGTERM didn't work

The trick: When your C program freezes, Ctrl+C is almost always enough. Save kill -9 for truly stubborn processes. You’ll encounter this scenario more than once this quarter — now you know exactly what to do.

Check Your Understanding
Your C program is stuck in an infinite loop. You press Ctrl+C but it doesn't stop. What should you try next?
A Press Ctrl+C harder — it always works eventually
B Close the terminal window — the process will clean up automatically
C Open another terminal, find the PID with ps aux | grep, and use kill or kill -9
D Reboot the computer — there's no other way
Answer: C. If Ctrl+C (SIGINT) doesn't work, the program may be catching that signal. Open a second terminal, find the process with ps aux | grep programname, and send SIGTERM with kill PID. If that still doesn't work, kill -9 PID sends SIGKILL, which cannot be caught or ignored.

Try It Yourself

These exercises let you practice process control hands-on:

Exercise 1: Run sleep 100 & to start a background process. Use jobs to see it. Bring it to the foreground with fg. Suspend it with Ctrl+Z. Resume it in the background with bg. Finally, kill it with kill %1 (the %1 refers to job number 1).

Exercise 2: Run top and observe your system’s processes. Which process uses the most CPU? The most memory? Press M to sort by memory, P to sort by CPU. Press q to quit.

Exercise 3: Open two terminal windows. In the first, run sleep 500. In the second, use ps aux | grep 'sleep' to find the PID, then kill it. Verify it stopped by running ps aux | grep 'sleep' again.


Processes Connect Everything

Every Unix concept connects through processes. When you type a command, the shell forks a process. When you pipe commands together, each stage is a separate process connected by a pipe. When you Ctrl+C a stuck program, you’re sending a signal to a process. When you check echo $?, you’re reading a process’s exit status.

In Week 10, you’ll write C code that creates processes with fork(), launches programs with exec(), and connects them with pipe(). The concepts you just learned as a user are the same system calls you’ll implement as a programmer.

Next: the transition from Java to C. You’ve been using Unix tools written by other people — now it’s time to write your own. In Lesson 2.3, you’ll write your first C program and see how gcc transforms source code into a native binary that runs directly on the CPU.

Why this matters: Understanding processes explains why things work the way they do. Why ./myprogram is required (the shell needs a path to fork and exec). Why Ctrl+C kills a program (it sends a signal). Why gcc produces a file you can run directly (native binary, not bytecode running on a VM). Processes are the thread that connects the entire course.