Skip to main content

Command Palette

Search for a command to run...

Linux Under the hood

Processes vs Threads

Updated
7 min readView as Markdown
Linux Under the hood

When the linux OS runs your program , it creates a process. That process gets its

. own Virtual address space

. own file descriptors

.own memory

Linux kernel (monolithic kernel) tracks all of this in a single data structure called task_struct { ... }. That isolation is not just software. The CPU has a register called CR3 that points to the process's page tables. Every memory address your program uses goes through tables. Another process has a completely different CR3 that points to its own page tables.

A thread is just another task_struct { ... } but created with one key difference. It shares the same page tables as its parent. Same heap, same globals, same file descriptors. The only things that are its own are its stack and its CPU registers.

Creating a process means copying the entire page table tree. That takes time and it scales with how much memory your process uses. Creating a thread skips all of that. It just sets up a stack and points at the same address space. That's why thread creation is 10 to 30 times faster. But sharing memory is exactly what causes race conditions.

When you spawn a process, the linux kernel calls fork() (a system call). It copies the entire page tables tree , every level , every entry. Your child process gets its own address space before it runs a single instruction. Threads skip all of that. They call clone with one flag Clone_VM i.e. clone(CLONE_VM). This tells the kernel to share the existing address space instead of copying it. No duplication and no setup. That's the reason thread creation is 10 times faster than fork().

Once both process and thread are running , the CPU scheduler sees no difference between them. Linux's CFS Scheduler keeps a sorted tree of tasks by how much CPU time each has consumed. It always picks the one first that got least time , process or thread , it does not matter. But switching between processes costs more time. Every context switch between processes loads a new value into CR3,(the register that points to page tables). This flushes the TLB (your CPU's cache of address translations). Every memory access after this is a miss until it warms up again.

In context switching between threads , the CR3 stays the same (doesn't change). TLB stays warm. You are just swapping registers and a stack pointer. That's why high performance servers use thread pools instead of forking per request. So, we have two tools fork() for isolation and clone() for speed.

In real world:

  1. Chrome runs each tab as a separate process, not a thread. If one tab hits a bug ,it seg faults (segmentation fault), the kernel kills that process. The other tabs keep running because they have different page tables. That's the isolation via fork(), as we discussed.

  2. Python has a global interpreter lock (GIL). Only one thread can execute Python byte code at a time. So, if you spawn 10 threads to crunch numbers, you still get 1 core. To actually use multiple cores in Python, we use use the multiprocessing module which calls fork() and gives each worker its own address space.

  3. Apache used to fork() a new process for every incoming request. Each request got full isolation, but fork() is expensive and memory usage stacked up fast.

  4. Nginx took a different approach. It uses a fixed pool of worker processes and inside each worker it handles 1000 of connections, using non-blocking IO on a single thread. So, Fixed workers -> 1 thread, 1000 conns. Less overhead, same isolation at the worker level.

  5. Most modern web frameworks use a thread pool inside a single process. Our Node JS server, Go HTTP handler, Java servlet container , all uses One process -> many threads. Shared heap , fast to spin up, fast to context switch. The trade-off is that a memory corruption bug in one handler can take down the whole server. So, the pattern is the same everywhere.

What happens in a Linux Terminal?

In a Linux terminal , if a linux user performs a simple task, a lot of things needs to happen to accomplish that task. For example, what happens when we simply read a file named hosts using command cat /etc/hosts. Following operations happens to accomplish a simple file read operation:

  1. The cat command must be read and loaded from disk in RAM. Only once it is in RAM, it can be used.

  2. Related libraries must be found and loaded in RAM also.

  3. The /etc/hosts file needs to be located on disk. Because that is what we want to read using cat.

  4. Permissions of the current user need to be checked on this file.

  5. If that is appropriate, then the file contents can be copied to RAM.

All of these different tasks are provided through system calls and library calls.

Understanding System calls

. Processes cannot access the kernel directly.

. System calls are used as an interface for processes to the kernel. glibc provides a library interface to use system calls from programs.

. Common tasks like opening, listing, reading and writing to files all involve system calls.

. The fork() and exec() system calls determine how a process starts

. fork(): the kernel creates an almost identical copy of the current process and replaces that. That's how a process starts a child process. It firsts copy itself and then it will remove its code to be replaced by the code of the child process.

. exec(): the kernel starts a program, which replaced the current process

Understanding Library calls

. System calls are provide by kernel to give access to restricted parts.

. Library calls come from shared libraries and provide functionality.

. There is a large number of library calls, virtually unlimited because it all depends on the shared libraries that are loaded. Many of these library functions are part of the standard C library (libc). Some functions are part of other libraries e.g. math library, libm, or real time library, librt.

Strict seperation between user space and kernel space

. This seperation has been created so that hardware access is restricted to the kernel only. If you want your computer to be reliable , then application should not have an option to interface directly with hardware. The only interface to hardware is linux kernel and the drivers that has been verified to work with linux kernel.

. These drivers must be verified and that is also why Linux does not like proprietary drivers because linux kernel has no means of verifying what the proprietary drivers is doing. That is why you may have heard about Dented Kernels which are kernels that contains proprietary drivers , which will give you problems anyway.

. The kernel provides system calls for users and processes to access hardware. These system calls are the only way , for users and processes , to access hardware .

. User space is memory that is allocated by the kernel for user processes.

. Several elements are all running in user space. For example:

.Network configuration

.Services like a web server

. Applications

. User interfaces