A process is an executing program. It has a memory address space, a
program image, handles to files, and the operating system state needed
for interprocess communication.
Every process has a parent. In traditional UNIX systems, the top of the
process tree is init. On Windows, several system processes fill
similar roles, including system.exe for the kernel, csrss.exe for
user-mode Win32 support, wininit.exe for system services,
explorer.exe for the shell, and winlogin.exe for login services.
A process address space is normally divided into a few major regions.
The text segment contains the machine instructions of the program. The
data segment contains initialized static data and constants. The BSS
segment contains uninitialized static data.
The heap is used for dynamic memory allocation. A process may have one
or more heaps. The stack contains local variables, call frames, and
return values. Kernel-mode threads normally have one stack per thread.
User-mode threading libraries may multiplex many user threads over a
smaller number of kernel-thread stacks.
The loader is responsible for laying out the text, data, BSS, initial
heap, and initial stack. After the process starts, stack and heap
management are handled by the program, its runtime library, and the
operating system.
The code examples are maintained at gkthiruvathukal/systems-code-examples. You can
use gitclonehttps://github.com/gkthiruvathukal/systems-code-examples to clone them into a folder named
systems-code-examples.
Full working demonstrations are referenced by directory name, such as
systems-code-examples/fork. To build most examples, install gcc,
cmake, and make. The examples have been tested on Ubuntu Linux,
macOS, and Windows Subsystem for Linux 2 with Ubuntu.
Most examples use the same pattern:
$ cd systems-code-examples/<example-name>
$ cmake .
$ make
The output executable is normally written to the example’s bin
subdirectory.
The systems-code-examples/c_intro example is a small C++ program used
to examine process layout and the basic build process. It is also a
useful first example because it has separate source files and a CMake
build.
Get and build the code:
$ git clone https://github.com/SoftwareSystemsLaboratory/systems-code-examples
$ cd systems-code-examples/c_intro
$ cmake .
$ make
Run the layout shell script to show the size of the text, data, and
BSS sections in bytes:
When a program is loaded, the loader allocates memory for the executable
text, data, BSS, heap, and stack. It then loads the program image into
memory.
The loader also handles shared libraries. It asks the operating system
which shared libraries are already loaded and loads the ones that are
not. Each shared library has its own text, data, and BSS sections. The
loader then resolves external symbols so references in the executable
point to the correct locations in memory.
The nm command shows symbols in a compiled object file or executable.
After the executable is ready, the loader invokes _start().
_start() calls _init() for each shared library, initializes static
constructors for global objects, and then calls main().
The systems-code-examples/hello-exe example is the smallest complete
executable example in the repository. It is useful for confirming the
build process before looking at larger programs.
Libraries also have text, data, and BSS sections. Shared libraries must
use position-independent memory references so the same library can be
mapped at different addresses in different processes. GCC uses the
-fpic or -fPIC flags for this. Newer GCC versions often make PIC
the default. Use -fno-pic when you need to compare against non-PIC
output.
The dynamic linker resolves position-independent memory accesses by
writing entries in the global offset table, or GOT, for each process.
This is necessary because a shared library may be loaded at different
offsets in different executions, while still allowing the operating
system to share the library’s text segment among processes.
The systems-code-examples/hello-lib example separates a function into
a small library and calls it from an executable. This is the simplest
repository example for discussing the boundary between application code
and library code.
Generate position-independent code (PIC) suitable for use in a shared
library, if supported for the target machine. Such code accesses all
constant addresses through a global offset table (GOT). The dynamic
loader resolves the GOT entries when the program starts. If the GOT size
for the linked executable exceeds a machine-specific maximum size, the
linker reports that -fpic does not work. In that case, recompile with
-fPIC instead.
Static libraries do not contain position-independent code. A static
library is a collection of unlinked .o object files. When the program
is linked, the linker copies the needed object code into the final
executable.
Shared libraries reduce memory use when multiple processes load the same
library because the text segment can be reused. They also allow programs
to benefit from library updates without rebuilding the executable.
The cost is complexity. Shared libraries require virtual memory support,
dynamic linking, position-independent code, and careful handling of
version and path compatibility.
Static libraries make sense when reuse is not important, when deployment
must avoid runtime dependencies, or when the program must be self
contained. Static linking can also avoid version and path problems.
The main cost is memory use. Common code is copied into each executable,
so there is less sharing across processes.
Dynamic linking is usually chosen for memory footprint, code reuse,
smaller executables, and easier library updates. Static linking is useful
when deployment simplicity matters more than sharing. It can also make
whole-program optimization and obfuscation easier because more code is
available to the linker at once.
Modern operating systems use virtual memory and privilege separation to
protect processes from each other and from the kernel.
One process cannot read another process’s memory unless the operating
system explicitly permits it. A process can manage the memory it owns,
including allocation, deallocation, garbage collection, calling
conventions, and stack management. A crash, exception, resource
starvation, or deadlock in one process does not directly modify another
process. Even though kernel memory may be mapped into the process address
space, user code cannot modify kernel memory or other protected pages.
fork() creates a new process by duplicating the calling process. The
new process is the child. The calling process is the parent. The child has
its own process ID, and its parent process ID is the parent’s process ID.
The parent’s threads are not recreated in the child.
In Linux, the C library implementation of fork() uses clone().
From the programmer’s point of view, fork() returns the child PID to
the parent, returns 0 to the child, and returns -1 if the child
cannot be created.
The systems-code-examples/fork_exec example creates a pipe, forks,
redirects standard input and standard output with dup2(), and then
uses execvp() to replace each process image. The parent runs wc.
The child runs echo.
1#include<stdio.h> 2#include<unistd.h> 3#include<fcntl.h> 4#include<stdlib.h> 5 6intmain(intargc,char*argv[]) 7{ 8 9intpipes[2];10pipe(pipes);1112intinputPartOfPipe=pipes[0];13intoutputPartOfPipe=pipes[1];1415intpid=fork();1617if(pid>0)//parent process18{19dup2(inputPartOfPipe,0);// redirect STDIN20close(outputPartOfPipe);// close unused half of pipe2122char*wcArgs[]={"-l",NULL};23execvp("wc",wcArgs);2425fprintf(stderr,"should not be able to reach here!\n");26}27elseif(pid==0)//child process28{29dup2(outputPartOfPipe,1);// redirect STDOUT30close(inputPartOfPipe);// close unused half of pipe3132char*echoArgs[]={"-ne","\"hello\\nworld\\n\"\n",NULL};33execvp("echo",echoArgs);3435fprintf(stderr,"should not be able to reach here!\n");36}37else38{39printf("fork failed!\n");40}4142//don't worry about closing remaining pipes,43//process exit does this for us4445return0;46}4748
Key points:
pipe() creates a kernel buffer with one file descriptor for reading
and one for writing.
dup2() redirects standard input or standard output before the
process image is replaced.
execvp() replaces the current program with another executable; code
after a successful execvp() call does not run.
clone() is similar to fork() because it creates a child execution
context. Unlike fork(), it allows selected parts of the parent process
to be shared with the child.
Linux uses flags to control what is shared. CLONE_FS shares file
system information such as chroot, chdir, and umask.
CLONE_FILES shares the file descriptor table. CLONE_SIGHAND
shares signal handlers. CLONE_VM shares the page table. These flags
are part of how Linux creates lightweight processes and kernel threads.
glibc’s fork() calls clone() without these sharing flags.
clone() is Linux-specific and is not present in every UNIX-like
operating system.
Windows does not expose the same process-sharing model as UNIX
fork() and Linux clone(). CreateProcess() creates a new
process and is closest to calling fork() followed by execve() on
UNIX. CreateThread() creates a new thread and is closest to using
clone() with thread-sharing flags.
For most programs, this is not a practical disadvantage. Most UNIX uses
of fork() are followed by exec(), and most uses of clone() are
for thread creation.
Cygwin implements fork() on Windows by combining CreateProcess(),
saved register state, copied memory regions, and synchronization. The
parent creates a suspended child process, saves register state with
setjmp(), copies the BSS and data sections to the child, wakes the
child, and then coordinates stack, heap, and memory-mapped region copies.
This approach is closer to “copy on fork” than modern UNIX copy-on-write.
It is useful for compatibility, but it is more expensive than a native
UNIX fork() implementation.
The systems-code-examples/syscall example compares three ways to
write output: the C library printf(), the POSIX write() wrapper,
and a direct syscall() invocation.
1#include<stdio.h> 2#include<string.h> 3#include<unistd.h> 4#include<sys/syscall.h> 5 6intmain(intargc,char*argv[]) 7{ 8char*hello_with_syscall="Hello World with syscall\n"; 9char*hello_without_syscall="Hello World without syscall\n";10char*hello_with_printf="Hello World with printf\n";1112write(1,hello_without_syscall,strlen(hello_without_syscall));1314syscall(SYS_write,1,hello_with_syscall,strlen(hello_with_syscall));1516printf("%s",hello_with_printf);1718return0;19}2021
Key points:
printf() is a C library call, not a direct system call.
write() is the usual POSIX wrapper for the kernel’s write system
call.
syscall(SYS_write,...) bypasses the wrapper and invokes the system
call interface directly.
The systems-code-examples/getopt example shows how a process receives
command-line arguments through argc and argv and turns them into
structured options.
A process can terminate normally by returning from main(). It can
also terminate with an error code, by encountering a fatal error, or by
being terminated externally by another process.
Fatal errors include segmentation faults or bus errors from invalid
memory access, stack overflow, protection faults from privileged
instructions, and instruction faults such as divide by zero.
The systems-code-examples/wait example shows how a parent process can
wait for a child process to terminate.
1#include<sys/types.h> 2#include<sys/wait.h> 3#include<stdio.h> 4#include<stdlib.h> 5#include<unistd.h> 6 7 8intmain(intargc,char*argv[]) 9{10pid_tpid=fork();11if(pid==0)12{13abort();//child process exits14}15intstatus;16wait(&status);// wait for child to exit17if(WIFEXITED(status))18{19printf("normal exit. exit code = %d\n",WEXITSTATUS(status));20}21elseif(WIFSIGNALED(status))22{23printf("abnormal termination, signal number = %d\n",WTERMSIG(status));24}25elseif(WIFSTOPPED(status))26{27printf("child stopped, signal number = %d\n",WSTOPSIG(status));28}29}3031
Key points:
The parent process uses wait() or waitpid() to collect child
exit status.
Waiting also prevents terminated children from remaining as zombie
processes.
The status value encodes how the child terminated.
The systems-code-examples/fork_sigchld example handles SIGCHLD to
notice when a child process exits. It uses wait() in the signal
handler to collect the child’s exit status.
sigaction() installs a handler for SIGCHLD before the fork.
The kernel sends SIGCHLD to the parent when the child exits.
The handler calls wait() to collect the child’s exit status.
The systems-code-examples/fork_sigchld2 example extends this pattern
to multiple child processes. It uses waitpid() with WNOHANG so the
handler can collect every child that has exited without blocking.
Some examples in the repository are best treated as short systems C case
studies. They support the process chapter because they make memory,
calling conventions, and low-level representation concrete.
The systems-code-examples/objects example shows object-style
programming in C using struct values and functions.