Linux Process Basics, Task Structs, and Advanced File Descriptors

. 6 min read . Auxias

Kernel Linux C

Introduction: Understanding Processes and the Kernel

When you run ps aux on a Linux system, you see a snapshot of everything currently running:

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0  39580 13992 ?        Ss   Jan31   1:27 --/usr/lib/systemd/systemd --switched-root --system --deserialize=56 rhgb

NOTE

  • PID 1: The first userspace process started by the kernel (systemd), which acts as the manager of users, processes, and service states.
  • PID 2 (kthreadd): The parent and manager of all kernel threads. Any process or thread wrapped in brackets [...] is a kernel thread—it runs entirely in kernel space and has no backing executable on disk. Process 2 starts systemd with pid 1. which in turn loads other processes
  • VSZ & RSS: VSZ represents total virtual memory size (including mapped files, shared libraries, and unused space), while RSS (Resident Set Size) reflects actual physical RAM usage.
  • Modern system have shifted to using pidfd instead of pid to overcome the race condition problem pid had.

Process State Flags (STAT)

The process state dictates what the process is currently doing:

  • D: Uninterruptible sleep (typically waiting for I/O).
  • I: Idle kernel thread.
  • R: Running or runnable on the run queue.
  • S: Interruptible sleep (waiting for an event to complete).
  • T: Stopped by job control.
  • Z: Zombie process (terminated but not reaped by its parent).

The Process Control Block: task_struct and mm_struct

Inside the kernel, every process and thread is represented by a task_struct instance (defined in include/linux/sched.h).

struct task_struct {
    /* Scheduling */
    volatile long        state;        // TASK_RUNNING, TASK_INTERRUPTIBLE, etc.
    unsigned int         flags;        // PF_EXITING, PF_KTHREAD, etc.
    int                  prio;
    
    /* Identity */
    pid_t                pid;          // thread ID (what the kernel calls pid)
    pid_t                tgid;         // thread group ID (what userspace calls pid)
    struct task_struct  *group_leader; // points to main thread
    
    /* Memory */
    struct mm_struct    *mm;           // userspace memory descriptor (NULL for kthreads)
    struct mm_struct    *active_mm;    // mm currently in use
    
    /* Filesystem & Resources */
    struct fs_struct    *fs;           // root, cwd
    struct files_struct *files;        // open file descriptors
    
    /* Signals & Credentials */
    struct signal_struct    *signal;
    const struct cred       *cred;
    
    /* Namespaces & Thread Architecture */
    struct nsproxy          *nsproxy;  // mount, uts, ipc, pid, net namespaces
    struct thread_struct     thread;   // arch-specific: registers, FPU state, TLS
};

Do note that process sees the Virtual Memory layout, not the real mapping in ram

Memory Layout (mm_struct)

The mm_struct manages the virtual address space layout of a process:

struct mm_struct {
    struct maple_tree   mm_mt;       // VMA tree tracking memory regions
    unsigned long       mmap_base;   // base of mmap region
    unsigned long       task_size;   // size of task VM space
    pgd_t              *pgd;         // page global directory (top of page table)
    
    unsigned long       start_code, end_code;
    unsigned long       start_data, end_data;
    unsigned long       start_brk,  brk;       // heap
    unsigned long       start_stack;
    
    atomic_t            mm_users;   // # of processes using this mm
    atomic_t            mm_count;   // reference count
};
  • Processes vs. Threads: Cloning a process creates a distinct memory structure (mm_struct), whereas cloning a thread shares resources like memory and file tables.

File Descriptors: The Unix Abstraction

A file descriptor is simply an integer index pointing to an entry inside a process’s file descriptor table. Each entry acts as a pointer to an underlying kernel object.

Process
+-------------------------+
| FD Table                |
|-------------------------|
|0 -> kernel object A     |
|1 -> kernel object B     |
|2 -> kernel socket       |
|3 -> kernel file         |
|4 -> kernel pipe         |
+-------------------------+

Because diverse resources (sockets, files, pipes, timers) expose uniform system calls (read, write, close, poll, fcntl), the kernel can treat them interchangeably.


Advanced File Descriptors & Notification Subsystems

Modern Linux architectures lean heavily on specialized file descriptors that integrate directly into event-driven loops.

1. epoll

epoll is Linux’s scalable I/O event notification facility. Traditional methods like select() and poll() require scanning every monitored descriptor on every call, creating $O(N)$ overhead.

epoll bypasses this by maintaining a kernel-side ready queue. When an event fires, the kernel places the descriptor directly into the ready list.

  • Lifecycle: Create via epoll_create1(), register targets with epoll_ctl(), and wait using epoll_wait().
  • Versatility: It can monitor sockets, pipes, eventfds, timerfds, signalfds, and inotify descriptors uniformly.

2. eventfd

eventfd is a lightweight signaling mechanism for threads or processes. It functions as a kernel-maintained 64-bit counter:

  • Writing an integer adds to the counter.
  • Reading resets the counter to zero (blocking if the value is zero).
  • Use Case: A worker thread finishes a task and writes 1 to an eventfd. An event loop blocked in epoll_wait() wakes up instantly without the overhead of pipes or signals.

3. signalfd

Traditional signals (SIGINT, SIGTERM) interrupt process execution asynchronously, creating complex race conditions. signalfd converts signals into readable streams:

  1. Block the desired signals using sigprocmask().
  2. Create a signalfd associated with those signals.
  3. Read structured signalfd_siginfo data directly from the descriptor inside your event loop.

4. timerfd

timerfd transforms timers into regular file descriptors using timerfd_create() and timerfd_settime(). When the configured time elapses, the descriptor becomes readable, returning the number of expirations. This eliminates the need for dedicated timer threads or periodic polling loops.

5. inotify

inotify tracks filesystem modifications without directory polling. By adding watches via inotify_add_watch(), the kernel appends inotify_event records to an internal queue whenever files are created, modified, moved, or deleted, allowing text editors, build systems, and file sync tools to react instantly.


Putting It All Together: The Unified Event Loop

Imagine a high-performance server managing hundreds of thousands of concurrent connections:

  • epoll monitors client network sockets.
  • timerfd handles session timeouts and heartbeats.
  • signalfd listens for SIGTERM for graceful shutdowns.
  • eventfd wakes the main loop when worker threads finish background jobs.
  • inotify watches the configuration directory for live updates.

By calling epoll_wait() on a single unified structure, the kernel handles the underlying complexities, waking the process only when real work arrives.