Embedded Linux Basics

Linux vs Embedded Linux

There is no distinct "Embedded Linux”. What makes linux, an embedded linux is being limited by hardware resources and cloud be a none General purpose operating system, and i guess running on an embedded device.


Introduction

What is an operating system: An OS is a “system software” that manages hardware, other software application (e.g, a user application) and provides services like memory allocation, networking, storage.
Linux is Unix-like operating system, and what makes the Linux a Unix like is the almost total conformity to the POSIX specification, which defines system APIs/interfaces (system calls, headers …), C standard library, Shell and other utilities that makes it possible to create an executable that runs on other Unix operating systems that run on the same CPU architecture

shell

the term "shell" is an abstract interface specification, primarily defined by the POSIX.1 standard (IEEE Std 1003.1).

A shell is a program that serves as an intermediary interface between the user and the operating system kernel.  It interprets user commands entered via a Command-Line Interface (CLI) or a Graphical User Interface (GUI)

POSIX and the C Standard Library (libc):

POSIX (IEEE 1003) defines an operating system interface, not a library. When you perform "POSIX programming," you are relying on the C Standard Library (such as glibc, musl, or bionic) to provide the wrapper functions that implement both the ISO C standard (printf, malloc) and the POSIX standard (pthread_create, open, mmap).

Kernel programming :

Kernel programming takes place in a "freestanding" environment rather than a "hosted" one. You cannot use libc in the kernel because libc relies on the kernel to function. For example, user-space malloc relies on the kernel syscalls brk or mmap to request pages; user-space printf relies on the write syscall to push bytes to a file descriptor.

Because the kernel cannot invoke system calls on itself, it must be entirely self-contained. It relies on its own internal implementations for memory management, string manipulation, and synchronisation.


Linux kernel introduction

Fundamentals/Licensing


OS Architecture

Hardware Layer:

is the hardware that is running the OS: CPU, RAM, storage, NICs, GPUs, timers, interrupt controllers (APIC/GIC). The kernel abstracts all of this.

Firmware / Boot Layer:

On embedded devices, when power on, your CPU has a build in ROM often called first stage boot loader, that loads second boot loader (uboot, BareBox...).
On x86-64 Architecture The CPU does not contain a built-in boot ROM that acts as a first-stage bootloader. The CPU is hardwired to load firmware from ROM on the motherboard (BIOS/UEFI**)

Note: BIOS/UEFI initializes hardware, runs POST, hands off to boot loader (Grup. Lime, ..)

💡 POST (Power-On Self-Test) is a diagnostic test sequence a computer performs when first powered on. It checks if essential hardware components (CPU, memory, storage, etc.) are present and functioning correctly before booting the operating system.

vmlinuz breaks down:

So vmlinuz is not a raw ELF binary you can just execute or inspect with normal tools. It's a self-extracting compressed blob. Internally it contains a small decompression stub followed by the compressed kernel image. When GRUB loads it into memory and jumps to its entry point, that stub decompresses the real kernel into RAM and transfers control to it.

initramfs is a temporary root FS — before mounting the real root filesystem, loads early drivers, then pivots root

Unlike the older initrd method, which required a specific filesystem driver (like ext2) to be built into the kernel to read the image.

https://wiki.debian.org/initramfs

Kernel Space

User vs Kernel:

Kernel and user are two terms that are often used in operating systems. Their definition is pretty straight forward: The kernel is the part of the operating system that runs with higher privileges while user (space) usually means by applications running with low privileges.
However these terms are heavily overloaded and might have very specific meanings in some contexts.

💡On x86 The kernel core runs in ring 0, full cpu privilege, Userspace runs in ring 3

User mode and kernel mode are terms that may refer specifically to the processor execution mode. Code that runs in kernel mode can fully control the CPU while code that runs in user mode has certain limitations. For example, local CPU interrupts can only be disabled or enable while running in kernel mode. If such an operation is attempted while running in user mode an exception will be generated and the kernel will take over to handle it.

💡 some processors may have even higher privileges than kernel mode, e.g. a hypervisor mode, that is only accessible to code running in a hypervisor/bare-metal virtualization (virtual machine monitor)

https://serverfault.com/questions/23738/run-virtual-machines-without-a-host

The Linux kernel uses virtual memory, and it is mapped into the higher half (high addresses) of every process's virtual address space.

Kernel Space vs. User Space: Virtual addressing partitions memory into kernel space (accessible only in kernel mode) and user space (accessible by applications), ensuring that user applications cannot directly access or corrupt kernel memory.

image.png

User space and kernel space may refer specifically to memory protection or to virtual address spaces associated with either the kernel or user applications.

Grossly simplifying, the kernel space is the memory area that is reserved to the kernel while user space is the memory area reserved to a particular user process. The kernel space is accessed protected so that user applications can not access it directly, while user space can be directly accessed from code running in kernel mode.

The kernel code itself can be logically separated in core kernel code and device drivers code. Device drivers code is responsible of accessing particular devices while the core kernel code is generic. The core kernel can be further divided into multiple logical subsystems (e.g. file access, networking, process management, etc.)

Types of kernels

There are four main types of kernels in OS:

  1. Monolithic Kernel: All OS services (process management, memory management, file system, device drivers) run in a single address space (often called kernel space).

However, most monolithic kernels do enforce a logical separation between subsystems especially between the core kernel and device drivers with relatively strict APIs (but not necessarily fixed in stone) that must be used to access services offered by one subsystem or device drivers. This, of course, depends on the particular kernel implementation and the kernel's architecture.
However, most monolithic kernels do enforce a logical separation between subsystems especially between the core kernel and device drivers with relatively strict APIs (but not necessarily fixed in stone) that must be used to access services offered by one subsystem or device drivers. This, of course, depends on the particular kernel implementation and the kernel's architecture.

Linux is a monolithic, modular kernel OS.

  1. Microkernel: Only essential services (IPC, basic memory management, scheduling) run in kernel space. Other services run as user-space processes.


In a micro-kernel architecture the kernel contains just enough code that allows for message passing between different running processes. Practically that means implement the scheduler and an IPC mechanism in the kernel, as well as basic memory management to setup the protection between applications and services.
One of the advantages of this architecture is that the services are isolated and hence bugs in one service won't impact other services.
As such, if a service crashes we can just restart it without affecting the whole system. However, in practice this is difficult to achieve since restarting a service may affect all applications that depend on that service (e.g. if the file server crashes all applications with opened file descriptors would encounter errors when accessing them).

This architecture imposes a modular approach to the kernel and offers memory protection between services but at a cost of performance. What is a simple function call between two services on monolithic kernels now requires going through IPC and scheduling which will incur a performance penalty

https://lwn.net/Articles/220255/
image.png

  1. Hybrid Kernel: Combines aspects of both monolithic and microkernel architectures. Some non-essential services (like device drivers or file systems) can be dynamically loaded into the kernel space, but the core remains modular.

Linux is a monolithic kernel, not a hybrid kernel, although it incorporates modular elements that blur the distinction for some observers.  The Linux kernel runs all its core services (such as device drivers, file systems, and network protocols) in kernel space within a single address space, which is the defining characteristic of a monolithic architecture.

  1. Exokernel: Provides minimal hardware abstractions, allowing user-space applications to directly manage hardware resources.
Micro-kernels vs monolithic kernels

Advocates of micro-kernels often suggest that micro-kernel are superior because of the modular design a micro-kernel enforces. However, monolithic kernels can also be modular and there are several approaches that modern monolithic kernels use toward this goal:

There is a class of operating systems that (used to) claim to be hybrid kernels, in between monolithic and micro-kernels (e.g. Windows, Mac OS X). However, since all of the typical monolithic services run in kernel-mode in these operating systems, there is little merit to qualify them other then monolithic kernels.

Many operating systems and kernel experts have dismissed the label as meaningless, and just marketing. Linus Torvalds said of this issue:

"As to the whole 'hybrid kernel' thing - it's just marketing. It's 'oh, those microkernels had good PR, how can we try to get good PR for our working kernel? Oh, I know, let's use a cool name and try to imply that it has all the PR advantages that that other system has'."

1. Process Scheduler (Process & Thread Management)

Linux uses CFS (Completely Fair Scheduler), a red-black tree ordered by vruntime (virtual runtime). Each scheduling unit accumulates vruntime; the leftmost node (lowest vruntime) runs next.
The algorithm kind boils down to a simple thing: (among the tasks on a given runqueue) the task with the lowest vruntime is the task that most deserves to run, hence select it as 'next'.

Vruntime

It quantifies the amount of CPU time a task has consumed, scaled inversely by its scheduling weight (priority).

_attachments/Pasted image 20260607182207.png

Nice

The nice value of a process can range from -20 (high priority) to +19 (low priority). By default, processes start with a nice value of 0. Increasing the nice value lowers the process priority, while decreasing it raises the priority.
 The algo kind of boils down to a simple thing: (among the tasks on a given runqueue) the task with the lowest vruntime is the task that most deserves to run, hence select it as 'next'. (The actual implementation is done using an rbtree for efficiency).

Scheduling unit & task

The scheduler does differentiate between processes and threads they are trail the same and called task
E.i. Linux processes and threads are implemented particularly different than other kernels. There are no internal structures implementing processes or threads, instead there is a struct task_struct that describe an abstract scheduling unit called task.

Fundamentals/Threads
Fundamentals/Processes vs Threads
Fundamentals/Synchronisation

Processes in Linux are assigned priorities that influence how often the scheduler allows them to run. These priorities can be divided
into two types:

Preemptive

The scheduler can force a context switch while a thread is executing in kernel mode. If a high-priority hardware interrupt (IRQ) wakes a blocked process, the kernel can immediately swap out the currently executing kernel-mode thread. The execution time of a given task is proportional to it priority

💡 A task has pointers to resources, such as address space, file descriptors, IPC ids, etc. The resource pointers for tasks that are part of the same process point to the same resources, while resources of tasks of different processes will point to different resources.

This peculiarity, together with the clone() and unshare() system call allows for implementing new features such as namespaces.

Namespaces are used together with control groups (cgroup) to implement operating system virtualisation in Linux.

cgroup is a mechanism to organise processes hierarchically and distribute system resources along the hierarchy in a controlled and configurable manner.

Linux real-time scheduler

Linux supports real-time scheduling. A real-time tasks are given priority over normal tasks. There are two main real-time policies:

Batch scheduling

Batch scheduling is used for processes that are not interactive and don't need immediate execution. It's typically used for background or batch jobs, where completion time isn't as critical.

Idle scheduling

Idle scheduling is designed for tasks that should run only when no other tasks are running.
These tasks are assigned the lowest possible priority.

2. Memory Management (MM)

Embedded Linux Basics#Address space

3. Virtual File System (VFS)

Embedded Linux Basics#Linux Filesystem Hierarchy

4. Device Drivers & I/O

(not completed)
_attachments/Pasted image 20260607200131.png

5. Networking Stack

image.png

image.png

6. Inter-Process Communication (IPC)

(not completed)

note:

Application binary interface (ABI)

An application binary interface (ABI) is an interface exposed by software that is defined for in-process machine code access. Often, the exposing software is a library, and the consumer is a program.

Calling convention
each architecture has it own way of calling interrupts, passing argument between function and retuning values
you can find these information in the instruction set architecture (ISA)

x86_64: The int Instruction


irq_handler_x86:
    push rax            ; Hardware already pushed RIP, CS, RFLAGS, RSP, SS
    mov rax, 1          ; Do "work" (e.g., set a flag)
    pop rax
    iretq               ; Atomic return: pops the hardware frame and resumes

; --- The Trigger ---
trigger_interrupt:
    int 0x80            ; CPU traps here, transitions to ring 0, and jumps

ARMv8-A: The svc Instruction

; --- The Handler (located at VBAR_EL1 + 0x400) ---
sync_exception_entry:
    stp x0, x1, [sp, #-16]! ; Manual stack management (Pre-index)
    mov x0, #1              ; Do "work"
    ldp x0, x1, [sp], #16   ; Restore
    eret                    ; Restore PSTATE from SPSR and PC from ELR

; --- The Trigger ---
trigger_svc:
    svc #0                  ; CPU traps, updates SPSR/ELR, and jumps to VBAR offse

Fundamentals/ABI

Fundamentals/System Calls vs Signals

Putting It All Together:

Here is a simplified diagram about the architecture of a Linux Distribution with the relation with the components

image.png


Linux Filesystem Hierarchy

https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard

https://wiki.archlinux.org/title/File_systems

Virtual Filesystem (VFS)

The VFS defines the object model that all filesystems must implement to exist in the kernel.

The VFS is an abstraction layer that sits between the system call interface and concrete filesystem drivers. Its primary function is to multiplex standard POSIX file operations (like open(), read(), write(), stat()) across entirely different underlying filesystem implementations (ext4, NFS, FAT, procfs) without user-space needing to know the difference.

A pseudo-filesystem (or synthetic filesystem) is a concrete filesystem implementation that registers itself with the VFS but is not backed by persistent storage on a block device. Instead, it serves as an interface to internal kernel data structures, state, or RAM.

Kernel Interface: They are the primary mechanism for realizing the Unix "everything is a file" philosophy for kernel internals. For example:

image.png

Block I/O management

Block I/O is a method of data transfer where information is read from or written to storage devices in fixed-size chunks, known as blocks, rather than as a continuous stream of individual bytes.

creating block I/O requests, transforming block I/O requests (e.g. for software RAID or LVM), merging and sorting the requests and scheduling them via various I/O schedulers to the block device drivers.

image.png


Device Tree

In computing, a devicetree (also written device tree) is a data structure describing the hardware components of a particular computer so that the operating system's kernel can use and manage those components, including the CPU or CPUs, the memory, the buses and the integrated peripherals.

https://en.wikipedia.org/wiki/Devicetree


Base Permissions

Linux implements traditional UNIX Discretionary Access Control (DAC) via the i_mode field within the inode struct. Permissions are defined by a 9-bit matrix divided into three tiers: User (owner), Group, and Other. Each tier possesses three bits representing Read, Write, and Execute capabilities.

The VFS evaluates permissions using a strict short-circuit logic (e.g., in generic_permission()). The kernel checks if the process's effective UID matches the file's owner; if so, it applies the User bits and stops evaluating. If it fails, it checks the group credentials, and finally falls back to the Other bits. If a user owns a file with 000 permissions but is in a group with rwx permissions, access is denied because the kernel matches the User tier and immediately halts evaluation.

The Sticky Bit (S_ISVTX):

The sticky bit (octal 01000) alters VFS behavior depending on the inode type.

Effective User ID (EUID):

The EUID is the dynamic credential field within a process's struct cred that the kernel evaluates for I/O authorization and capability state transitions. It is architecturally decoupled from the Real UID (RUID), which tracks the permanent owner for resource limits and signal dispatching. The SetUID (S_ISUID, octal 04000) bit allows a process to initialize its EUID to the file owner's UID during an execve() system call.

Example:
The binary /usr/bin/passwd is owned by root with mode 4755 (-rwsr-xr-x).

  1. A user with RUID 1000 invokes execve() on the binary.
  2. The kernel detects S_ISUID, initializing the new process with RUID 1000 and EUID 0.
  3. The process invokes open("/etc/shadow", O_RDWR). The VFS evaluates the EUID (0) against the file owner (0) and permits the operation.

compilation

Fundamentals/Preprocessing, compiling, assembling, and linking


Linux Library Types

Compilation and Linking Dynamics

Static linking resolves all symbol dependencies during the final link phase (ld). The linker extracts the necessary object code from the archive and physically copies it into the .text segment of the resulting executable. The output binary is self-contained and standalone.

Shared linking defers symbol resolution. The linker verifies symbol presence to build the executable, but rather than copying code, it writes the required library names into the executable's DT_NEEDED ELF dynamic section. It also embeds the path to the runtime dynamic linker (typically /lib64/ld-linux-x86-64.so.2) into the .interp section.

Memory and Execution Models

Shared libraries optimize system memory via page sharing. When multiple processes load the same .so, the OS maps the physical pages containing the read-only sections (.text and .rodata) into the virtual address spaces of all dependent processes. Writable sections (.data and .bss) utilize Copy-on-Write (COW) semantics; they share physical memory until a process modifies a page, at which point a private copy is instantiated for that process.

Shared libraries utilitze Copy-on-Write, so a library should have little amount of data that can be modified to be worth making it a shared library.

elf vs dwarf

ELF (Executable and Linkable Format) is the file format used by Unix-like systems to store executables, object files, and shared libraries, containing the machine code, metadata, and section headers.  DWARF is not an executable format but rather the standard debugging information format that stores source-level details like variable types, function names, and line numbers within ELF files.

The search hierarchy the dynamic linker uses to locate and map shared objects

1. RPATH / RUNPATH

These are hardcoded directory paths stored in the ELF binary's .dynamic section. The precedence depends on which specific dynamic tag the linker embedded during the build:

  • DT_RPATH: The legacy tag. If present (and DT_RUNPATH is absent), the loader evaluates these paths before checking LD_LIBRARY_PATH.
  • DT_RUNPATH: The modern tag (invoked via -enable-new-dtags in ld). If present, the loader evaluates these paths after checking LD_LIBRARY_PATH.

2. LD_LIBRARY_PATH

An environment variable. The dynamic linker parses this at startup to override or supplement the default search paths, which is useful for testing or localized deployments.

3. System Cache (/etc/ld.so.cache)

A memory-mapped binary index generated and maintained by the ldconfig utility. Rather than executing expensive directory traversals across the paths defined in /etc/ld.so.conf, the loader queries this cache to immediately map a requested soname to its absolute file path.

4. System Default Directories (/lib, /usr/lib)

The ultimate fallback paths hardcoded into the dynamic linker itself. If the shared object remains unresolved after exhausting the explicit paths and the cache, the loader attempts a direct lookup here. The exact paths depend on the architecture and multilib configuration of the system; for example, an x86_64 ELF loader will target /lib64 and /usr/lib64 for 64-bit objects before looking at the generic /lib or /usr/lib directories.

Basic Makefile example:

CC = gcc

server: server.o libnetutils.so
gcc server.o -lnetutils -L$(shell pwd) -o server

server.o: server.c
gcc -c server.c

netutils.o: netutils.c
gcc -c netutils.c

libnetutils.so: netutils.o
gcc netutils.o -shared -fPIC -Wl,-soname=libnetutils.so.1 -o libnetutils.so.1.0
CC      = gcc
CFLAGS  = 
LDFLAGS = -lnetutils -L$(shell pwd)
EXEC    = server
LIB     = libnetutils.so
SONAME  = $(LIB).1
SOLIB   = $(SONAME).0

$(EXEC): server.o $(LIB)
	$(CC) $< $(LDFLAGS) -o $@

%.o: %.c
	$(CC) -c $<

$(LIB): netutils.o
	$(CC) $< -shared -fPIC -Wl,-soname=$(SONAME) -o $(SOLIB)
	ln -s $(SOLIB) $(SONAME)
	ln -s $(SONAME) $@

What are binutils? toolchain? SDK?

image.png


Address space

The address space term is an overload term that can have different meanings in different contexts.

_attachments/Pasted image 20260607193630.png

Memory management unit (MMU)

computer hardware unit having all memory references passed through itself, primarily performing the translation of virtual memory addresses to physical addresses. And usually integrated directly into the CPU.

translation lookaside buffer (TLB)

memory cache that is used to reduce the time taken to access a user memory location; part of the chip’s memory-management unit
The TLB ****is a memory cache that stores the recent translations of virtual memory addresses to physical memory addresses.

![https://en.wikipedia.org/wiki/Translation_lookaside_buffer

https://en.wikipedia.org/wiki/Translation_lookaside_buffer

image.png

The translation process

The memory management unit (MMU) inside the CPU stores a cache of recently used mappings from the operating system's page table. This is called the translation lookaside buffer (TLB), which is an associative cache.

When a virtual address needs to be translated into a physical address, the TLB is searched first. If a match is found, which is known as a TLB hit, the physical address is returned and memory access can continue. However, if there is no match, which is called a TLB miss, the MMU, the system firmware, or the operating system's TLB miss handler will typically look up the address mapping in the page table to see whether a mapping exists, which is called a page walk. If one exists, it is written back to the TLB, which must be done because the hardware accesses memory through the TLB in a virtual memory system, and the faulting instruction is restarted, which may happen in parallel as well. The subsequent translation will result in a TLB hit, and the memory access will continue.

💡 The lookup may fail if there is no translation available for the virtual address, meaning that virtual address is invalid. This will typically occur because of a programming error, and the operating system must take some action to deal with the problem. On modern operating systems, it will cause a segmentation fault signal (segfault) being sent to the offending program.

image.png

Page fault

page fault is an exception that the memory management unit (MMU) raises when a process accesses a memory page without proper preparations. Accessing the page requires a mapping to be added to the process's virtual address space. Furthermore, the actual page contents may need to be loaded from a back-up, e.g. a disk. The MMU detects the page fault, but the operating system's kernel handles the exception by making the required page accessible in the physical memory or denying an illegal memory access.

https://en.wikipedia.org/wiki/Page_fault

💡 Valid page faults are common and necessary to increase the amount of memory available to programs in any operating system that uses virtual memory, such as Windows and macOS.

Page fault Types

Minor (Soft) Page Fault

Occurs when the target virtual page already resides in physical memory but lacks a valid mapping in the faulting process's page table. The OS resolves this entirely in software by updating the Memory Management Unit (MMU) without initiating disk I/O. This typically manifests during shared memory access or when reclaiming pages still resident in the OS page cache.

Major (Hard) Page Fault

Triggered when the target page is absent from physical memory, necessitating demand paging. The page fault handler must allocate a physical frame, which may require evicting and writing a modified page to a backing store if memory is contended. The OS then performs synchronous disk I/O to load the page and updates the MMU mapping. The associated mass storage access introduces significant execution latency.

Invalid Page Fault

Arises from a memory access violation where the referenced virtual address falls outside the process's allocated virtual address space. A common trigger is a null pointer dereference, as the zero page is deliberately unmapped by the MMU. The OS intercepts the invalid access and typically dispatches a segmentation fault to the offending process, precipitating abnormal termination.

Pageable kernel memory

A kernel supports pageable kernel memory if parts of kernel memory (code, data, stack or dynamically allocated memory) can be swapped to disk.

Kernel stack

All process has a kernel stack that is used to maintain the function call chain and local variables state while it is executing in kernel mode, as a result of a system call.

The kernel stack is small (4KB - 12 KB) so the kernel developer has to avoid allocating large structures on stack or recursive calls that are not properly bounded.

Call stack / Stack Frame

A call stack is used for several related purposes, but the main reason for having one is to keep track of the point to which each active subroutine should return control when it finishes executing. An active subroutine is one that has been called, but is yet to complete execution, after which control should be handed back to the point of call. Such activations of subroutines may be nested to any level (recursive as a special case), hence the stack structure. For example, if a subroutine DrawSquare calls a subroutine DrawLine from four different places, DrawLine must know where to return when its execution completes. To accomplish this, the address following the instruction that jumps to DrawLine, the return address, is pushed onto the top of the call stack as part of each call.
image.png


Execution contexts / Operating modes

One of the most important jobs of the kernel is to service interrupts and to service them efficiently. This is so important that a special execution context is associated with it.

The kernel executes in interrupt context when it runs as a result of an interrupt. This includes the interrupt handler, but it is not limited to it, there are other special (software) constructs that run in interrupt mode.

Code running in interrupt context always runs in kernel mode and there are certain limitations that the kernel programmer has to be aware of (e.g. not calling blocking functions or accessing user space).

nterrupt context cannot access user-space memory due to address space volatility and the inability to handle page faults.

Address Space Volatility
Hard IRQs execute asynchronously, preempting arbitrary code. The active page directory (e.g., CR3 on x86) belongs to the interrupted task. Consequently, evaluating a user-space virtual address from an interrupt handler will resolve against the wrong process's page tables, leading to data corruption or segmentation faults.

Page Faults and Scheduling Constraints
User-space memory is demand-paged. Accessing a swapped-out or unallocated page triggers a page fault, requiring synchronous disk I/O and putting the executing thread to sleep. Interrupt handlers lack a backing thread structure (like task_struct in Linux) and are not schedulable entities. Attempting to yield the CPU from hard IRQ context leaves the scheduler with no state to save, violating the concurrency model and triggering an immediate kernel panic (e.g., "scheduling while atomic").

“Opposed” to interrupt context there is process context. Code that runs in process context can do so in user mode (executing application code) or in kernel mode (executing a system call).

image.png

Operating modes (e.g. arm Cortex-M3)

The processor supports two modes of operation, Thread mode and Handler mode:

https://developer.arm.com/documentation/ddi0337/h/programmers-model/modes-of-operation-and-execution/operating-modes

https://developer.arm.com/documentation/ddi0344/k/programmers-model/operating-modes

https://developer.arm.com/documentation/ddi0210/c/Programmer-s-Model/Operating-modes

Execution Modes (Privilege Levels)

Execution modes dictate what the currently executing instruction stream is allowed to do. They are enforced by the hardware's memory management unit (MMU) and instruction decoder. In x86, these are implemented as four protection rings (Ring 0 to Ring 3).

Multi-tasking

Multitasking is the ability of the operating system to "simultaneously" execute multiple programs. It does so by quickly switching between running processes.

Cooperative multitasking requires the programs to cooperate to achieve multitasking. A program will run and relinquish CPU control back to the OS, which will then schedule another program.

With preemptive multitasking the kernel will enforce strict limits for each process, so that all processes have a fair chance of running. Each process is allowed to run a time slice (e.g. 100ms) after which, if it is still running, it is forcefully preempted and another task is scheduled.

Preemptive kernel

Preemptive multitasking and preemptive kernels are different terms.

A kernel is preemptive if a process can be preempted while running in kernel mode.

However, note that non-preemptive kernels may support preemptive multitasking.

  • Non-Preemptive Kernel: A thread executing kernel code (via syscall or exception) cannot be forcibly descheduled. It retains the CPU until it voluntarily yields.
  • Preemptive Kernel: The scheduler can force a context switch while a thread is executing in kernel mode. If a high-priority hardware interrupt (IRQ) wakes a blocked process, the kernel can immediately swap out the currently executing kernel-mode thread. This is a prerequisite for Real-Time Operating Systems (RTOS).
  • Selective Preemptive Kernel: An implementation-specific middle ground. Mainline Linux, as noted in the thread, is not fully preemptive by default. Preemption is explicitly disabled during critical sections (e.g., when holding spinlocks or executing interrupt handlers) but allowed elsewhere in kernel space. Linux behavior is heavily implementation-specific and dictated by kernel build configurations (specifically the differences between CONFIG_PREEMPT_NONE, CONFIG_PREEMPT_VOLUNTARY, and CONFIG_PREEMPT_LLX).

https://stackoverflow.com/a/49700902

https://unix.stackexchange.com/questions/5180/what-is-the-difference-between-non-preemptive-preemptive-and-selective-preempti

💡 Preemption is the method used to ensure responsiveness; real-time is the property of meeting deadlines. Not all preemptive systems are real-time (e.g., general-purpose OSs use preemption for fairness), but real-time systems almost always require preemption to guarantee bounded interrupt response times.

What makes a kernel/OS real-time?
hard vs soft vs firm real time kernel

Difference between cores and threads: https://www.reddit.com/r/explainlikeimfive/comments/16729pd/eli5_what_are_cores_and_threads_in_cpus/

“Cores are CPUs, the physical thing you have on your computer is just multiple(4 or 8) smaler CPUs together, so its a 4 or 8-core CPU. Each of these work the same and can do the same tasks and they all work in parallel.

Threads are the same idea of paralell computing but in software, a thread is one task that runs on a computer, but because a computer has multiple cores it can run multipe threads at the same time.

But threads are realy jut a software concept, even a single core CPU can have multiple threads running on it. But in this case each thread is paused for some milliseconds to process the next one, so its not realy parallel, just switching between both tasks super fast.”

“I'll add to this that some CPUs are designed to have two threads "ready" for a core at any time. That means that some of the components like the registers are doubled.

In practice, that means that if a thread stalls, there is another one just waiting to be sent to the core.

A thread stalling means that it the CPU needs data that isn't available in the registers and needs to pause execution until the data it needs is fetched. In the mean time, thanks to having double the registers, the thread that was waiting can be processed. Yes, that thread can also stall.

That's what it means when you see a CPU with specifications like 6 cores / 12 threads. Intel named their implementation Hyper-Threading.

In practice, this means that if you have workloads on the CPU where threads don't stall, having two "threads" per core won't give any performance benefit. In other use cases, it can speed things up.”

Asymmetric MultiProcessing (ASMP)

Asymmetric MultiProcessing (ASMP) is a way of supporting multiple processors (cores) by a kernel, where a processor is dedicated to the kernel and all other processors run user space programs.

The disadvantage of this approach is that the kernel throughput (e.g. system calls, interrupt handling, etc.) does not scale with the number of processors and hence typical processes frequently use system calls. The scalability of the approach is limited to very specific systems (e.g. scientific applications).

image.png

Symmetric MultiProcessing (SMP)

As opposed to ASMP, in SMP mode the kernel can run on any of the existing processors, just as user processes. This approach is more difficult to implement, because it creates race conditions in the kernel if two processes run kernel functions that access the same memory locations.

In order to support SMP the kernel must implement synchronization primitives (e.g. spin locks) to guarantee that only one processor is executing a critical section.

image.png

Linux source code layout

image.png

These are the top level of the Linux source code folders:

Device Tree Blob

Prior to the widespread adoption of the Device Tree,

what is Device tree why it was created and how it is implemented. every new board variant required kernel source modifications and a unique kernel binary.
The Device Tree (adopted from Open Firmware/PowerPC) was introduced to completely decouple static hardware descriptions from the kernel binary (e.i. hard coding the relationship between the kernel and hardware). By abstracting the hardware topology into a separate configuration file, a single unified kernel image can boot across entirely different boards with same CPU architecture.

A Device Tree is a tree data structure (an acyclic graph) that describes the physical hardware topology of a system. While buses like PCIe and USB implement hardware-level enumeration protocols to dynamically report device IDs and capabilities, memory-mapped IP blocks (UARTs, I2C controllers, GPIO banks) on an SoC cannot be probed safely without prior knowledge of their memory locations. These blocks are called Memory-Mapped I/O (MMIO).

The Device Tree provides the kernel with the .

Hardware is authored in human-readable Device Tree Source (.dts) files. The kernel tree utilizes a heavily hierarchical structure using .dtsi (include) files. A common pattern is:

The Device Tree Compiler (dtc) parses the .dts files and compiles them into a binary format known as the Device Tree Blob (.dtb). The DTB (also referred to as the Flattened Device Tree, or FDT) is a highly compact, contiguous memory block consisting of a header, a memory reservation block, a structure block (the nodes and properties), and a strings block.

Linux Boot Sequence on an embedded device

BOOT SEQUENCE

Boot sequence for x86 device How Computers BOOT From Power Button to Operating

BOOT ROM /
Primary Program Loader
BOOT ROM /...
Second Program Loader (SPL)
/
First Stage Bootloader (FSBL)
Second Program Loader (SPL)...
kernel
kernel
DTB
DTB
+
+
RAM
RAM
Second Stage Boot Loader 
Second Stage Boot Loader 
Rootfs
Rootfs
1
1
3
3
5
5
2
2
4
4
6
6
7
7
8
8
Init
Init

https://stackoverflow.com/questions/31244862/what-is-the-use-of-spl-secondary-program-loader/31252989#31252989

  1. The Boot ROM executes in-place (XIP) from masked on-chip ROM at the Reset Vector upon power-on. It samples hardware to determine the peripheral polling sequence (e.g., eMMC, SDIO, SPI NOR) to locate the First Stage Bootloader (FSBL/SPL). Because the external DRAM controller is uninitialized, the Boot ROM copies the FSBL into internal SRAM and transfers execution to it.
  2. The FSBL/SPL executes from internal SRAM. It responsible for configuration of memory. Once external DRAM is accessible, the SPL reads the Second Stage Boot Loader (SSBL, e.g., U-Boot proper) from the boot media, copies it into the RAM, and jumps execution to the SSBL entry point in DRAM.
  3. The SSBL locate the kernel image and device tree on non-volatile storage.
  4. & 5. The SSBL copies the compressed kernel image and the Device Tree (.dtb) into RAM. The SSBL prepares the CPU state for kernel handoff (disabling interrupts, flushing/invalidating caches, setting CPU execution modes) and transfers execution to the kernel entry point.
  5. The kernel decompresses itself, sets up page tables, and enables the MMU. It parses the Device Tree to discover and initialize hardware topology.
  6. The kernel extracts and mounts a temporary RAM disk. Once the drivers are loaded, it mounts the physical Rootfs to transition to the permanent filesystem.
  7. The kernel executes the first user-space program, establishing PID 1, by searching predefined paths (/sbin/init, /etc/init, /bin/init, /bin/sh) or following the init= parameter.