LearnLinux System CallsProcesses and Plumbing

Watching a Process Spawn

strace -f -e trace=process bash -c "ls" - a hands-on Linux lab on a real virtual machine.

Follow a program as it becomes a second program: clone, execve, wait, and the reason fork shows up in a trace as clone.

One program becoming another

Every time you type a command, something quietly remarkable happens. The shell you are typing into cannot turn itself into ls. A running program cannot swap out its own name and start over as a different one on a whim. So how does the shell run ls at all?

It does a three-step dance, and every program that starts another program does the same one. First it clones itself, making a second copy of the shell. Then that copy replaces its own program with ls. Then the original shell waits for ls to finish. Clone, replace, wait. That is how one program becomes another.

You are about to watch that dance happen, call by call. This lab has a tiny program called fork_demo that does exactly this: it makes a child, the child turns into echo, and the parent waits for it. You will trace it and see the three system calls with your own eyes.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. Every screen in this lesson was captured by booting this exact lab and running these exact commands. The one thing that will look different for you is the live trace: the child's process number and the long 0x... addresses change every single boot, and that is normal. The lab user is tracer and the machine is named syscall-lab. Your progress in the lab is tracked automatically, so type commands naturally.

Run the demo and see what it does

Before you trace anything, just run the program and watch what it prints. fork_demo is already built and waiting in your home directory. Run it with ./fork_demo.

./fork_demo

prompt: tracer@syscall-lab:~$ answer: ./fork_demo ||| ~/fork_demo ||| /home/tracer/fork_demo output: child running parent done hint: The program is in your home directory. Run it with ./fork_demo

Two lines, from two different programs. child running was printed by echo, which the child turned into. parent done was printed by the original program after it waited for the child to finish.

That ordering is the whole story in miniature. The child ran first and printed its line. The parent held back until the child was done, then printed its own. One program made another, let it run, and collected it. Now you will trace the same run and see the exact system calls behind those two lines.

Follow the child with -f

Here is the catch with tracing a program that makes a child. By default strace follows only the program you launched, the parent, and the child runs off untraced. To follow the child too, you add -f, which means follow forks.

Trace the demo with strace -f ./fork_demo. Because the trace lines carry process numbers and addresses that change every boot, read this as an example, not a screen to match. Yours will show different numbers.

execve("./fork_demo", ["./fork_demo"], 0x7ffe602d6e48 /* 10 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fc755f35a10) = 863
wait4(-1, strace: Process 863 attached
[pid   863] execve("/usr/local/bin/echo", ["echo", "child running"], 0x7ffdb28aeb08 /* 10 vars */) = -1 ENOENT (No such file or directory)
[pid   863] execve("/usr/bin/echo", ["echo", "child running"], 0x7ffdb28aeb08 /* 10 vars */) = 0
<... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 863

Your PIDs and addresses will differ. The number 863 is the child's process id on that one boot; yours will be some other number, and it appears both after clone and inside the [pid 863] prefixes strace adds to mark the child's lines.

Three system calls carry the whole dance. clone makes the child. execve turns the child into echo. wait4 is the parent waiting for the child to finish. Read them in that order and the trace tells the same story your eyes saw a moment ago.

The three calls, named

Take the three apart, because each one is a name worth knowing by sight.

clone is the call that brings a new process into being. It appears once, and its return value = 863 is the child's process id, handed back to the parent so the parent knows who to wait for.

execve is the call that loads a program. It replaces whatever program is running in a process with a new one. You see it twice near the top of the child's lines, which you will make sense of shortly.

wait4 is the parent pausing until the child is done. Notice it is split across two lines: wait4(-1, ... starts, then the child runs, then <... wait4 resumed>... = 863 finishes and hands back the same child id. The parent was asleep in wait4 the entire time the child was working.

Commit: what call made the child?

This is the single most surprising fact in the lesson, so take a position before you read the answer.

Why fork shows up as clone

It is worth sitting with this, because it is the kind of gap between what code says and what the kernel does that trips people up for years.

On old systems there really was a separate fork call. Modern Linux replaced it with one flexible call, clone, that can make anything from a full separate process to a lightweight thread depending on the flags you hand it. When you call the plain old fork(), the C library calls clone for you with the exact flags that reproduce classic fork behavior.

You can even read those flags in the trace: SIGCHLD in the flags means the child sends its parent a signal when it exits, which is what makes an ordinary child process rather than a thread. So fork is not gone, it is a specific way of calling clone. The lesson: trace the kernel, not the manual, when you want to know what really ran.

Commit: what does execve do to the child?

The child began life as an exact copy of fork_demo. Then it called execve. Predict what that did to it.

The two execve lines, and PATH

Look again at the child's two execve lines. The first one failed and the second one worked, and that is not a bug, it is the shell's habit made visible.

[pid   863] execve("/usr/local/bin/echo", ["echo", "child running"], ...) = -1 ENOENT (No such file or directory)
[pid   863] execve("/usr/bin/echo", ["echo", "child running"], ...) = 0

When you run a bare command name like echo, the system does not know which directory holds it. So it walks the list of directories in your PATH, in order, trying execve on each until one succeeds. /usr/local/bin comes first and has no echo, so that execve returns -1 ENOENT, meaning no such file. /usr/bin/echo exists, so the next execve returns 0 and the child becomes echo.

You are watching a PATH search happen one call at a time. Every command you have ever typed by its short name did this same walk.

Commit: was that first failure a problem?

wait4: the parent collects the child

One call is left, and it belongs to the parent. After clone returned the child's id, the parent called wait4 and went to sleep.

wait4 means: pause here until a child finishes, and tell me how it went. In the trace it starts on one line, the child does all its work while the parent sleeps, and then wait4 resumes and returns the child's id, 863, the same number clone handed back at the start. The WIFEXITED and WEXITSTATUS(s) == 0 inside it are the parent reading the result: the child exited, and its exit status was 0, meaning success.

This is why parent done printed last when you ran the program. The parent could not move on until wait4 came back. A parent that skips wait4 leaves a finished child lingering as a zombie, which is a story for another lesson. For now: clone makes it, execve fills it, wait4 collects it.

The tempting wrong picture

The natural way to imagine running a command is one clean step: the shell reaches out, grabs echo, and runs it. One action, one program appearing. The trace you just read is the proof that it does not work that way.

There is no single call that runs a command. There is clone, which makes a bare copy of the caller, an exact duplicate that is still the old program. Then there is execve inside that copy, which is what finally swaps in the new program. And there is wait4 in the original, holding until the copy is done. Three calls in two processes, not one call in one.

Once you see it this way, a lot of Linux stops being mysterious. A shell is a program that clones and execs, over and over, once for every command you type. When you understand these three calls, you understand the machinery under every command line you will ever use.

Write down the three names

Your lab scores that you can name the three calls. Three short files hold them: the call that makes the child, the call that loads the new program, and the call that waits. Write each name into its file, then read all three back.

echo clone  > answers/spawn_call.txt
echo execve > answers/loads_program.txt
echo wait4  > answers/waits.txt

prompt: tracer@syscall-lab:~$ answer: echo clone > answers/spawn_call.txt; echo execve > answers/loads_program.txt; echo wait4 > answers/waits.txt; echo 'spawn:'; cat answers/spawn_call.txt; echo 'loads:'; cat answers/loads_program.txt; echo 'waits:'; cat answers/waits.txt ||| echo clone > answers/spawn_call.txt; echo execve > answers/loads_program.txt; echo wait4 > answers/waits.txt; echo spawn:; cat answers/spawn_call.txt; echo loads:; cat answers/loads_program.txt; echo waits:; cat answers/waits.txt output: spawn: clone loads: execve waits: wait4 hint: Echo each name into its file, then cat the three files back with a label before each

Three files, three facts. spawn_call.txt holds clone, the call that made the child. loads_program.txt holds execve, the call that turned the child into echo. waits.txt holds wait4, the call the parent used to collect it. The grader reads these files, so writing them is you stating, in order, the three steps of one program becoming another.

Milestone: you can read a spawn

Stop and notice what changed. When you started, fork_demo printed two lines and that was all you knew. Now you can name every system call behind them. clone made a child and returned its id. execve walked PATH and replaced the child with echo. wait4 held the parent until the child was done. You followed all of it with one flag, -f.

That skill is not about fork_demo. Every shell, every service manager, every program that launches another program does this exact dance, and now you can trace any of them and read it call by call.

Challenge: trace it yourself

Scaffolding off. No command is printed from here on.

In the live lab, trace fork_demo with -f on your own and find the three calls in your own trace. Then look at the number clone returns and confirm it is the same number wait4 returns at the end. That number is the child's process id, and seeing it appear at both ends is seeing the parent recognize the exact child it made.

You can also trace a real shell running a command, strace -f -e trace=process bash -c "ls", and watch the same clone, execve, wait4 appear for an everyday command. The -e trace=process part filters the trace down to just the process calls so the three stand out.

The kit you just earned

You took the invisible thing that happens on every command line and read it, call by call. Here is every form this lesson taught:

CommandWhat it does
./fork_demoRun the demo: a child runs echo, the parent waits
strace -f ./fork_demoTrace the demo and follow the child with -f
strace -f -e trace=process bash -c "ls"Trace a real shell spawning a command, process calls only
echo clone > answers/spawn_call.txtRecord a call name as evidence for the grader

And the facts underneath all of it:

When you trace anything that starts other programs, reach for -f first or you will follow only the parent and miss the children entirely. Pair it with -e trace=process to hide the memory and file setup and leave just clone, execve, wait4, and exit.

You saw execve load a program and wait4 read an exit status of 0. The next lessons open up what a process is made of: the descriptors it inherits, and how pipes and redirection wire one program's output into another's input.

Ready to practice

This lesson walked you through one program becoming another. You ran fork_demo, followed its child with -f, and named the three calls: clone makes the child, execve loads the new program, wait4 collects it. You saw why the trace says clone where the code said fork, and why a bare command name causes a PATH search of failing execve calls until one succeeds.

The lab is that same work on a real machine with strace installed, fork_demo built, and an answers directory waiting. It scores five things: you followed the child with -f, you named the call that makes the child, you named execve, you named wait4, and you traced the demo with -f.

The objectives name the goal, not the command. You read what needs to be true, recall the form, and type it. Your progress is tracked automatically as you work, so type naturally, and run check-progress whenever you want to see your score.

Launch the lab and watch one program spawn another.

Practice Watching a Process Spawn in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.

More lessons in Processes and Plumbing