-
Notifications
You must be signed in to change notification settings - Fork 0
Kernel Labs
Linux 4.19, arm64, ARM FVP-Base. 22 loadable-module labs under
drivers/misc/mb_labs/, run by labs-run.sh. Core concepts first (most
interviewable), then each lab with mechanism + gotcha + likely follow-up.
The kernel distinguishes contexts by bitfields in current->preempt_count,
queried with the in_*() macros.
| # | Context | Macro | May sleep? | Entered from |
|---|---|---|---|---|
| 1 | Process/task | in_task() |
yes | syscall, kthread, workqueue handler, module init/exit |
| 2 | Softirq | in_softirq() |
no | tasklets, timer softirq, net RX/TX, RCU callbacks |
| 3 | Hardirq | in_irq() |
no | device IRQ, timer tick, IPI callback, hrtimer callback |
| 4 | NMI | in_nmi() |
no | pseudo-NMI (arm64), perf, hard-lockup watchdog |
- Contexts 2–4 = interrupt context (
in_interrupt()); none may sleep — nomsleep,mutex_lock,kmalloc(GFP_KERNEL),copy_*_user. -
5th practical state — process-but-atomic: a task holding a spinlock, or in
preempt_disable()/local_bh_disable().in_task()==1butin_atomic()==1→ must not sleep even though you never left process context. - "Why can't a hardirq sleep?" No backing schedulable entity to switch away from, and it may have interrupted a lock holder → sleeping risks deadlock and there is nothing to schedule back.
One per-task word, split into fields (include/linux/preempt.h):
bits 0-7 preempt-disable count (spin_lock, preempt_disable, get_cpu)
bits 8-15 softirq count SOFTIRQ_OFFSET = 0x0000_0100
bits 16-19 hardirq count HARDIRQ_OFFSET = 0x0001_0000
bit 20 NMI NMI_OFFSET = 0x0010_0000
context_lab prints it live: 0x0 process · 0x1 +spinlock · 0x101 softirq ·
0x10001 hardirq — each context lights exactly one field.
-
in_atomic()=preempt_count != 0(any cause). Mechanically == "preemption disabled now", but its purpose is "cannot sleep". Caveat: onCONFIG_PREEMPT=n,spin_lockdoes not bump the count, soin_atomic()can be false while holding a lock → it's unreliable; the real sleep gate ismight_sleep()/CONFIG_DEBUG_ATOMIC_SLEEP. -
in_softirq()= softirq field != 0 → also true when merely BH-disabled, not only when serving a softirq. The precise test isin_serving_softirq(). Trick:local_bh_disable()adds2*SOFTIRQ_OFFSET, serving a softirq adds1*— the low bit separates "serving" from "disabled".
-
currentis a macro →get_current()→mrs sp_el0, cast tostruct task_struct *. - The kernel runs with
SPSel=1, so its stack isSP_EL1(orSP_EL2under VHE). That leaves SP_EL0 unused as a stack, so Linux repurposes it to hold thecurrentpointer.__switch_towrites the next task there on every switch →currentalways tracks the running task with a single cheap register read. -
get_current()uses non-volatile asm on purpose so the compiler can cachecurrent(its value is invariant for a running thread). Opposite ofREAD_ONCE, which forces re-reads. -
kthread:
currentis valid (the kthread's own task), butcurrent->mm == NULL(no userspace) — it borrowsactive_mm(lazy TLB).mm==NULLdoes not mean "not process context"; schedulability comes from having a task, not an mm. - syscall / module init:
currentis the calling user process (modprobe), with a realmm— it runs "on behalf of" that task. A kthread/kworker does not.
-
READ_ONCE/WRITE_ONCE= volatile cast; force exactly one real load/store. Stop the compiler from caching / tearing / fusing shared accesses. Not atomic. Not ordering. Minimum for any lockless shared variable. -
atomic_t+atomic_inc= atomic read-modify-write (arm64ldxr/stxrexclusives, or LSEstadd). Fixes lost updates. -
smp_store_release/smp_load_acquire,rcu_assign_pointer/rcu_dereference= ordering across CPUs ("publish data, then pointer"). - spinlock / mutex = mutual exclusion; inside a lock, plain accesses are fine.
struct item { int id; struct list_head node; }; /* node EMBEDDED in the object */
LIST_HEAD(head); /* sentinel; empty = points to self */
list_add_tail(&it->node, &head); /* O(1); list_add = prepend */
struct item *it = container_of(ptr, struct item, node); /* node* -> object* */
list_for_each_entry(it, &head, node) { ... } /* hides container_of */
list_for_each_entry_safe(it, tmp, &head, node) { list_del(&it->node); kfree(it); }-
Intrusive: links live inside the object (not a separate node holding a
pointer). → no per-node alloc, and one object can be on several lists at once
(multiple
list_headmembers). Opposite of a C++std::list. -
container_of(ptr, type, member)=ptr - offsetof(type, member); the whole trick. Recovers the object from any embedded member. (Same macroworkqueue_labuses onwork_struct.) -
Circular + doubly-linked with a sentinel head: the ends aren't special-cased
(no NULL), so
list_add/list_delare branchless.list_empty()= head points to itself. - Use
_safewhen deleting during iteration (it cachesnextfirst). RCU variants:list_add_rcu/list_for_each_entry_rcu(pairs withrcu_lab). - Also:
hlist_head(single-pointer head) for hash tables — half the head size.
ptr is a pointer to the embedded member; container_of returns a pointer
to the whole struct that contains it.
container_of(ptr, type, member)
| | |
| | +- name of the field inside the struct (e.g. "node")
| +-------- the struct type (e.g. "struct item")
+-------------- a pointer to that field
struct item { int id; struct list_head node; };
struct list_head *ptr = &it->node; /* points at the member */
struct item *obj = container_of(ptr, struct item, node); /* obj == it */
#define container_of(ptr, type, member) \
((type *)( (char *)(ptr) - offsetof(type, member) ))
struct item { int id; struct list_head node; };
^0 ^8
offsetof(struct item, id) == 0
offsetof(struct item, node) == 8 /* int id (4) + 4 padding, then node */
-
Why: the member sits at a fixed offset in the struct, so subtract that
offset from the member's address to reach the struct's start. Live proof from
list_lab:
node@…508 → item@…500(node at offset 8:int id+ padding). -
Why it exists: a callback/iterator only receives a pointer to the embedded
member (a
list_head*,work_struct*,timer_list*) because the generic facility doesn't know your object type.container_ofwalks back to the object.list_for_each_entryand the workqueue handler both use it internally. - Caveat: pure pointer arithmetic — no type check. Wrong member/type → silently bogus address. Names must match the real embedding.
| Mechanism | Context | Sleep? | Notes |
|---|---|---|---|
| softirq (raw) | softirq | no | 10 fixed vectors, core subsystems only, reentrant across CPUs |
| tasklet (deprecated) | softirq | no | client of TASKLET softirq; serialized (one CPU at a time) |
| timer_list | softirq | no | TIMER softirq, jiffy granularity |
| hrtimer | hardirq | no | ns precision (non-RT); red-black tree |
| workqueue | process | yes | kworker pool; modern default replacement for tasklets |
| threaded IRQ | process | yes |
request_threaded_irq → dedicated irq/<n> kthread |
| irq_work | hardirq | no | defer from NMI/hardirq to a safe hardirq point |
BH workqueue (WQ_BH) |
softirq | no | sanctioned tasklet successor, kernel 6.9+ (not in 4.19) |
- A tasklet runs in softirq context but is not a softirq — it's a callback the TASKLET-softirq handler dispatches. A module can't register a softirq vector (fixed enum) → it uses a tasklet to reach softirq context.
- Choose by "can it sleep?": yes → workqueue / threaded IRQ; no & low-latency →
softirq (core) / tasklet (legacy) /
WQ_BH(6.9+).
-
vma_lab — kernel vs userspace view of one address space.
kernel_addr.kowalksmm->mmap(the VMA list) and prints code/data/stack;user_addr(userspace) prints its own/proc/self/maps-style view. Also showscurrent == SP_EL0. Follow-up: what's a VMA? → a contiguous region with one set of perms/backing inmm_struct. -
mmap_lab — zero-copy sharing. Char/misc device
.mmapusesremap_pfn_range()to map a kernel page into userspace; kernel and user then read/write the same physical page.user_mmapproves the round-trip. -
ptwalk_lab — resolve a user VA by hand:
pgd → pud → pmd → pte, print the physical addr and PTE attribute bits (AF/young, UXN/PXN, RO/RW, nG). arm64 4.19 predatesp4d(sopud_offset(pgd,...)). Follow-up: 4-level 4 KB paging, 39/48-bit VAs; block mappings end the walk early. -
procfs_lab —
/proc/my_boolknob.proc_create+file_operations(copy_to_user,kstrtoint_from_user); one-shot read via*ppos. 4.19 predatesproc_ops.
-
completion_lab — one-shot handshake.
wait_for_completion()blocks init until the workercomplete()s. Gotcha: an on-stack completion + a still-running worker (e.g. with_timeout) = use-after-free; useDECLARE_COMPLETION_ONSTACKonly when the waiter outlives the completer. -
waitqueue_lab — sleep until a condition. Consumer
wait_event(wq, cond); producer sets cond +wake_up().wait_eventre-checks the condition on every wake (handles spurious wakeups). -
workqueue_lab — defer to process context.
alloc_workqueue,queue_work; thework_structis embedded in an object, recovered withcontainer_of. Handler runs on a kworker and may sleep. Follow-up: workqueue vs tasklet? → process ctx (sleepable) vs softirq (not). -
locking_lab — spinlock vs mutex, both give exact counts under contention.
sleep_in_atomic=1sleeps under the spinlock →BUG: scheduling while atomic(and can livelock: a sleeper holding a spinlock spins every other CPU). Mutex owners may sleep; spinlock holders may not. -
lockdep_lab — take locks A→B then B→A in one thread. With
CONFIG_PROVE_LOCKING, lockdep prints "possible circular locking dependency" on the possibility — no actual deadlock needed. Teaches lock ordering (ABBA). -
atomics_lab — plain
int++(load-add-store, races) vsatomic_inc(ldxr/stxr). On FVP it loses 0 updates — the functional simulator runs each CPU in quanta, so the load/store windows rarely interleave. Lesson: "my test passes" ≠ correct for concurrency. -
seqlock_lab — lockless readers that retry instead of block.
read_seqbegin/read_seqretry; writer bumps an even/odd sequence around the update. Readers never see a torn (half-updated) value. Great for rarely-written, often-read data (e.g. timekeeping). -
rcu_lab — lockless read + safe reclaim. Reader:
rcu_read_lock/rcu_dereference/rcu_read_unlock. Writer:kmallocnew,rcu_assign_pointer,synchronize_rcu(wait for readers), then free old. Readers pay ~nothing; writers pay the grace period.
-
list_lab — the kernel's intrusive doubly-linked list (
list_head), the most-used structure in the kernel. Node embedded in the object; build withlist_add_tail, walk withlist_for_each_entry, tear down withlist_for_each_entry_safe+list_del+kfree;container_ofrecovers the object from its node. See "Kernel linked list" in Part 1. -
radix_tree_lab — sparse index→pointer map.
radix_tree_insert/lookup,radix_tree_for_each_slotunderrcu_read_lock. Follow-up: replaced by XArray in v4.20 (same semantics, nicer API).
-
context_lab — the headline lab. Runs one report fn from process, spinlock,
softirq (tasklet), hardirq (hrtimer); prints
preempt_count+ allin_*flags so each context shows its bitfield. NMI documented as unreachable from a module on 4.19 arm64 (needs pseudo-NMI +request_nmi, v5.1+). -
current_lab —
currentfrom init (=modprobe,mm=user), a kthread (its own task,mm=NULL), and a hardirq (the interrupted task).match=1every time provescurrent == SP_EL0. -
sysreg_lab —
read_sysregof MIDR_EL1 (impl/part), MPIDR_EL1 (affinity), CNTVCT/CNTFRQ (virtual counter), CurrentEL. On FVP CurrentEL = EL2 → kernel runs at EL2 via VHE. -
ipi_lab — interrupt a chosen CPU.
smp_call_function_single(cpu, fn, info, wait=1); delivered as a GIC SGI, callback runs in hardirq on the target (in_irq=1). If sender==target it runs the callback directly (process ctx,in_irq=0) — no IPI. Follow-up:_asyncvariant is callable from atomic/hardirq (non-blocking); sync form needs process context. -
kprobe_lab — dynamic tracing.
register_kprobeondo_sys_open; kprobes patches a BRK into the first instruction;pre_handlerruns on every hit. Counts file opens without recompiling/rebooting.
-
hrtimer_lab — hrtimer vs timer_list side by side. hrtimer callback fires in
hardirq (
in_irq=1), timer_list callback in softirq (in_softirq=1). Periodic hrtimer re-arms withhrtimer_forward_now; measures per-expiry jitter (µs on FVP). Use hrtimer when the deadline matters; timer_list when "roughly later" is fine. -
irqthread_lab — threaded IRQ with no hardware.
irq_simhands out a real irq number;request_threaded_irqgives a primary top half (hardirq, returnsIRQ_WAKE_THREAD) and athread_fnbottom half (its ownirq/<n>kthread, sleeps). Fired from software viairq_sim_fire. Kconfig note:IRQ_SIMis promptless → the lab mustselectit (can't be set by a fragment/menuconfig). -
pacing_lab — hrtimer packet pacing, the
sch_fqpattern, no NIC. The hrtimer fires in hardirq and does onlytasklet_schedule()(it can't transmit — that's heavy/sleepable); the tasklet (softirq) "sends" and re-arms the hrtimer using absolute time so per-packet jitter doesn't accumulate into drift. Achieves µs-accurate spacing. Key point: hrtimer supplies the precise "when"; the softirq does the heavy "what" — top-half/bottom-half split.
-
smp_call_function_single(blocks, process ctx only) vs..._single_async(returns immediately, callable from atomic/hardirq) vssmp_call_function_many/on_each_cpu(broadcast). -
msleep/usleep_range(sleep, process) vsudelay/mdelay(busy-wait, any ctx incl. atomic). -
spin_lockvsspin_lock_bh(also excludes softirqs on this CPU) vsspin_lock_irqsave(also excludes hardirqs)._bh/_irqare local; the spinlock itself covers other CPUs. -
current_uid()(own creds, read directly) vstask_uid(t)(any task, RCU-safe). -
get_online_cpus()/put_online_cpus()= CPU-hotplug read lock (freeze the online set) vsget_cpu()/put_cpu()= pin which CPU you're on (preempt-disable).
EL0 user · EL1 kernel (or EL2 under VHE, as on FVP) · EL2 hypervisor ·
EL3 secure firmware (TF-A). syscall = svc (EL0→kernel); firmware/PSCI = smc
(→EL3). Boot chain: BL1 → BL2 → BL31 (TF-A) → U-Boot → Linux.
- Kernel config fragment = a
*.cfgfile inSRC_URI;kernel-yoctoauto-merges it (extension.cfgis the trigger). -
selectvsdepends on:select Xforces X on (ignores X's own deps — can create invalid configs);depends on Xmeans you must enable X (and its chain) first. A promptless symbol (no menu entry, e.g.IRQ_SIM) cannot be set by a fragment ormenuconfig— it must beselected by another symbol. - Reliable config workflow:
bitbake -c menuconfig virtual/kernel, thenbitbake -c diffconfig virtual/kernel→ auto-generates the fragment (deps already resolved). - QA
buildpaths: a packaged file embeds$TMPDIR(absolute build path) → breaks reproducibility / leaks host info. Fix with-fdebug-prefix-map, or a narrowINSANE_SKIP:${PN}-src += "buildpaths"for harmless debug-source cases.
- "On arm64
currentis justmrs sp_el0— SP_EL0 is free because the kernel runs on SP_EL1/EL2." - "
preempt_countis the single word that answers 'can I sleep here'." - "A tasklet runs in softirq context but isn't a softirq."
- "hrtimer = precise when (hardirq); the softirq/workqueue does the what."
- "lockdep flags the possibility of deadlock, not the occurrence."