Skip to content

Process Thread Memory & Libraries kernel userspace

MarekBykowski edited this page Jul 9, 2026 · 5 revisions

Process, Thread, Memory & Libraries — kernel/userspace wrap-up

A synthesis of how a process/thread is represented, how fork and pthread_create differ, how libraries and files are wired, and the kernel structs behind it all. Companion to the mb_labs; each section notes the lab that exercises it.


1. fork vs pthread_create — both are clone()

   fork()                              pthread_create()
     |  glibc                             |  glibc
     v                                    v
   clone(SIGCHLD)                       clone(CLONE_VM | CLONE_FILES | CLONE_FS |
   (no sharing flags)                         CLONE_SIGHAND | CLONE_THREAD |
     |                                        CLONE_SETTLS | ...)
     v                                    v
   kernel: copy_process() -> new task_struct; per resource: SHARE or COPY

copy_process()

/* simplified from kernel/fork.c */
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
    if (clone_flags & CLONE_VM) {
        mm = oldmm;              /* SHARE: same mm_struct, bump refcount  (pthread) */
        atomic_inc(&mm->mm_users);
    } else {
        mm = dup_mm(tsk, oldmm); /* COPY: new mm_struct, COW page tables  (fork)    */
    }
    tsk->mm = mm;
}

Rule in copy_process(): flag set (1) -> share the pointer; flag clear (0) -> copy.

resource (task_struct field) flag fork (=0) pthread (=1)
address space mm CLONE_VM copy (new mm, COW) share (same mm)
fd table files CLONE_FILES copy share
cwd/root fs CLONE_FS copy share
signal handlers sighand CLONE_SIGHAND copy share
thread group / tgid CLONE_THREAD new tgid same tgid
TLS (TPIDR_EL0) CLONE_SETTLS inherited set per-thread
  • fork: new task_struct, own mm (copied, COW), new tgid -> a PROCESS.
  • pthread: new task_struct, shared mm (same pointer), same tgid -> a THREAD.
  • The kernel schedules both identically as tasks; everything else falls out of the mm decision.

Identity: getpid() == kernel tgid (process); gettid() == kernel pid (thread). The word "pid" flips meaning between layers. (Lab: procthread_lab.)


2. The structs (structural view)

task_struct   -- ONE per thread (the schedulable entity)
  |- state, flags(PF_KTHREAD), pid, tgid, comm[16], real_parent, group_leader
  |- stack ------> kernel stack
  |- mm ---------> mm_struct        (address space; NULL for kthreads)
  |- active_mm --> installed mm     (kthreads borrow one)
  |- files ------> files_struct     (fd table)
  |
  (mm) mm_struct  -- ONE per process (shared by its threads)
        |- mmap ------> vm_area_struct   (head of the VMA list)
        |- mm_rb -----> rbtree of the VMAs (fast find_vma)
        |- pgd -------> page tables (-> TTBR0)
        |- mm_users --> # threads sharing it
        |- map_count, mmap_sem, start_code/start_stack ...
        |
        (mmap) vm_area_struct  -- ONE per memory region
                |- vm_start, vm_end   the VA range [start,end)
                |- vm_flags           VM_READ/WRITE/EXEC/SHARED
                |- vm_page_prot       -> PTE permission bits
                |- vm_file            backing struct file (NULL = anonymous)
                |- vm_pgoff           offset into vm_file
                |- vm_ops             .fault/.open/.close handlers
                |- vm_next/prev, vm_rb, vm_mm
  • task_struct = the thread: identity + pointers. mm NULL for kthreads.
  • mm_struct = the address space: page tables (pgd) + the list of VMAs (mmap). Shared by threads (CLONE_VM), copied by fork.
  • vm_area_struct (VMA) = one contiguous region with uniform perms/backing. A .so = several VMAs (text r-xp shared, data rw-p COW).
  • files_struct = the fd table (array of struct file *).
  • struct file = one open-file instance (offset, flags, f_op); your char devices' .read/.write/.mmap are reached through it.

Labs: pid/tgid/comm/mm -> procthread_lab,current_lab,findtask_lab; mm->mmap/VMAs -> vma_lab; pgd/page tables -> ptwalk_lab; vm_ops/.mmap -> mmap_lab; struct file/file_operations -> procfs/sysfs/mmap/procthread labs.


3. Libraries: static vs dynamic, userspace vs kernel

Userspace

static .a dynamic .so
linked build time (copied into binary) runtime (ld.so mmaps it)
binary size larger smaller
memory across processes own copy each ONE shared RO-text copy
fix the lib relink everything drop-in, all benefit

A .so's read-only text is mapped shared across every process using it -> one physical copy via the page cache. Writable data is COW (per-process).

Kernel (no libc; freestanding)

userspace kernel analog
static .a in the binary built-in (obj-y -> vmlinux)
dynamic .so via ld.so module .ko via insmod (linked vs kernel symtab)
libc: printf/malloc kernel: printk/kmalloc (EXPORT_SYMBOL)
ldd modinfo/depmod

A .ko resolves undefined symbols against the kernel symbol table (only EXPORT_SYMBOLed ones) + loaded modules; CRC (modversions) guards the ABI. "Unknown symbol" = the needed export isn't present (e.g. IRQ_SIM before built in). Bridge: the vDSO (linux-vdso.so.1) is a kernel-provided .so mapped into every process for fast syscalls.


4. How a .so connects to a process (it's just VMAs)

task_struct has no "library" concept. ld.so mmap()s the .so, creating file-backed VMAs in the process's mm:

Process A                          Process B
 mm_A (own VMA list)                mm_B (own VMA list)
   libc VMA_A  vm_start=0x7fAA..      libc VMA_B  vm_start=0x7fBB..  (ASLR: may differ)
   vm_file --\                        vm_file --\
              \----------- struct file / inode: /lib/libc.so ----------/
                                   |
                                   v
                        page cache: libc text pages  (ONE physical copy)
                          /                       \
                   A page tables            B page tables   (map to SAME phys pages)
  • The VMA is per-process (own descriptor, own virtual address; .so must be PIC so any load address works). The file + physical pages are shared.
  • Execution is demand paging: PC jumps into the text VMA -> page fault -> find_vma sees a file-backed VMA -> read the page from vm_file via the page cache -> shared across all mappers. (Lab: ptwalk_lab walks these page tables.)
  • Symbols/relocations (PLT/GOT) are entirely userspace ld.so work; the kernel only ever sees file-backed memory mappings.

Threads vs processes here: threads share the mm -> the same VMA list and same virtual addresses (one mapping). Processes have separate mms -> separate VMAs that merely point their vm_file at the same file. Both share the .so's physical pages; only threads share the mapping itself.


5. Files: fd -> struct file -> file_operations

userspace:  fd = open("/dev/x");  write(fd, ...)
                |  (fd = index)
                v
 task_struct.files -> files_struct        (the fd TABLE: array of struct file*)
                        fd[3] -> struct file   (one open: offset, flags, f_op)
                                   f_op -> file_operations  (.open/.read/.write/.mmap)
                                             |
                                             v
 kernel:  YOUR handler runs (procthread_lab pt_write, mmap_lab .mmap)
          struct file -> inode (the file) -> dentry (its name)   [VFS]
  • fd is just an index into the process's files_struct.
  • The fd table is the files row of the fork/pthread table: threads share one files_struct; fork copies it (but entries point at the same struct file, so inherited fds share the offset).
  • Every char-device lab plugs into this VFS path via the file_operations it registers.

One-paragraph synthesis

A process is a task_struct whose mm points to an mm_struct (page tables + a list of vm_area_struct regions). fork copies the mm (own address space, new tgid -> process); pthread_create shares it (same mm, same tgid -> thread). A shared library is not referenced as a "library" — ld.so mmaps it into file-backed VMAs (vm_file -> the .so), so each process has its own mapping but they all fault onto the same page-cache pages. Files reach kernel code through the VFS: an fd indexes files_struct -> a struct file -> your file_operations. Userspace uses static (.a, copied in) or dynamic (.so, shared) libs; the kernel uses built-in (obj-y) or modules (.ko, linked against exported symbols) — no libc. The kernel schedules threads and processes identically as tasks; every difference you observe flows from whether the mm (and tgid) is shared or copied.

Clone this wiki locally