Skip to content
Courses

Processes, Jobs, and Signals

Linux FoundationsLesson 6 of 960 minutesLessonBuilds on: Users, Groups, and PermissionsNo lab next

The question: which exact process will receive this action?

Section titled “The question: which exact process will receive this action?”

A program file just sits there doing nothing. When Linux starts one, the kernel builds a process around it, with an address space, open resources, a security identity, some execution state, and a numeric process ID that everyone just calls a PID. Start the same program twice and you get two processes (each running instance needs its own state).

PIDs are temporary labels rather than permanent names, and Linux will happily hand the same number to something else once the original process exits. So a PID you wrote down needs fresh evidence about the command and its arguments before you send it anything. Check that the PID still belongs to the command you started; otherwise, the signal could affect unrelated work.

We’ll use sleep here precisely because it has nothing interesting to do. That leaves process state as the only moving part, instead of burying the lesson under some application’s own behavior.

Start a process long enough to inspect and save its PID in a shell variable immediately:

Terminal window
sleep 600 &
lesson_pid=$!
printf 'lesson_pid=%s\n' "$lesson_pid"

The shell prints a job number and a PID, and $! expands to the most recent background PID in this shell.

The trailing & asks the shell to start the command without waiting around for it to finish, and the shell then records that background activity in a private structure of its own called the job table. Linux knows about the process itself. The shell adds the job abstraction on top, so a person can manage related work from one terminal session.

Verify before acting:

Terminal window
ps -p "$lesson_pid" -o pid=,ppid=,stat=,comm=,args=

You should see your recorded PID, a status containing S, the command sleep, and the argument 600. If no row comes back at all, the process either ended or the PID was wrong. Start a new sleep 600, replace lesson_pid, and check again.

PPID is the parent process ID, and for this particular target it will usually point straight back at the shell that started sleep in the first place. STAT is a compact process-state field, where an S means the process is waiting in an interruptible sleep. That’s exactly what we’d expect from a program whose entire job is waiting.

A process family and a graceful stop signal
PID 2100bash
PID 2142sleep 600
SIGTERM → 2142

Linux presents every single running process as a directory underneath /proc, with the directory named after that process’s own PID. These aren’t files sitting on a disk anywhere (the kernel builds them on demand out of the live process). So /proc/<pid>/ is a plain-text window into something running, readable with tools you already know.

The status file lists a process’s identity and state as readable, newline-separated fields:

Terminal window
cat /proc/$lesson_pid/status

The output is long, and the first few rows are the ones that matter here. Name, State, Pid, and PPid should all agree with the ps evidence you recorded a moment ago. If the directory doesn’t exist at all, the process has already exited and your recorded PID is stale, so start a fresh sleep 600, check the new PID, and try again.

Run:

Terminal window
jobs -l

The current shell should list the background job along with its PID. A different terminal won’t inherit this shell’s job table at all, though ps can still see the process perfectly well from anywhere.

That difference is easy to miss, and it causes a lot of confusion later. A job number like %1 only means anything to the shell that created the entry. A PID belongs to the kernel’s system-wide view. The two identifiers can point at the same running activity, and they still come from different layers and have different lifetimes.

Bash job control explains the shell’s job identifiers. Use it when a job number and a Linux PID appear to name the same command.

A signal is a small asynchronous notification, delivered to a process whether or not it was expecting anything. The receiving process responds according to the signal’s meaning and any handler it has installed, rather than interpreting a shell command. Each signal has defined semantics, and a program can handle some of them while others have fixed effects it gets no say in.

SIGSTOP forces a process straight into a stopped state, and a program gets no say in it at all. The process is still there and still holds its memory and its PID. It just doesn’t continue ordinary execution. SIGCONT makes a stopped process eligible to run again. Together they let us change process state without creating anything new.

Before sending a signal, predict which STAT letter will appear after a stop signal.

Terminal window
kill -STOP "$lesson_pid"
ps -p "$lesson_pid" -o pid=,stat=,comm=

The status should contain a T, which means stopped. So the observation is that the process kept the same PID and the same command, and it is no longer scheduled to continue ordinary execution.

Resume it:

Terminal window
kill -CONT "$lesson_pid"
ps -p "$lesson_pid" -o pid=,stat=,comm=

The T should disappear again. Notice the only thing that changed between those two observations was the signal.

SIGTERM requests termination, and a program that needs to clean up after itself is perfectly free to handle the signal and do so. So it’s a much better first choice than SIGKILL, which the process gets no chance to handle at all. “Graceful” is still a request rather than a promise, and the only observable result is whether this particular process actually exits.

Predict whether the PID row will remain after SIGTERM, and predict whether the number wait hands back will be zero, then run:

Terminal window
kill -TERM "$lesson_pid"
wait "$lesson_pid"
printf 'wait reported %s\n' "$?"
ps -p "$lesson_pid" -o pid=,stat=,comm=

ps should print no process row at all. wait may report that the job was terminated, and that’s evidence of the signal outcome rather than a new failure.

wait lets the shell collect the child’s final status. The empty ps result answers a different question entirely: does that PID still identify a live process? Keeping the two observations separate stops us treating one message as proof of the whole lifecycle.

Every process hands back one small number as it ends, and that number is its exit status. Zero means the program considered its own work successful, and anything else is a failure code whose meaning belongs to that particular program. The shell keeps only the most recent one, in $?, so read it on the very next line or lose it. That’s why the printf sits directly under the wait above, and putting the ps command between them would leave you reading the status of ps instead.

Your printf should have reported 143, and not 0 or 15. A process killed by a signal never reached the point of choosing a status for itself, so the shell reports 128 plus the signal number instead. SIGTERM is signal 15, and 128 plus 15 is 143.

This convention helps interpret later failures. In Bash on this VM, termination by signal 11 (SIGSEGV) is reported as 139. A program can also return 139 explicitly, so check the surrounding evidence before concluding that it crashed.

Clear the stale shell variable:

Terminal window
unset lesson_pid

Without copying the preceding sequence line by line:

  1. Start sleep 600 in the background.
  2. Save its PID under a different variable name.
  3. Prove its command and argument before acting.
  4. Pause it and prove the state.
  5. Resume it and prove the state.
  6. Terminate only that PID and prove it is gone.
  7. Read the exit status, and say which signal it accounts for.
  • No ps row comes back: never signal an unverified number. Start a new controlled target and replace the variable.
  • kill: arguments must be process or job IDs: the variable is empty. Inspect it with printf '<%s>\n' "$lesson_pid", and restart the target if you need to.
  • wait says the PID isn’t a child: go back to the terminal that started it. Verify with ps, and only use kill if cleanup is still needed.
  • The process is still stopped when you try to clean up: send CONT, verify its identity again, and then send TERM.
  • You lost the variable entirely: use jobs -l in the original shell. Don’t reach for killall or pkill as a shortcut, however tempting it looks.

You’re ready to move on when your evidence artifact shows every signal went to a process you had verified, and that no practice target is still running.

Next we’ll follow bytes through the standard input, output, and error streams of commands we control.

Knowledge check

Question 1 of 2

You run a failing command, then a successful printf, then inspect $?. Which status do you see?