Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Process and Thread Representation in the Linux Kernel

Recommended resources

Introduction

Welcome to the second module of our course, "Process & Scheduler Internals." In the previous module, we established a solid foundation by exploring how the Linux kernel is built, how it boots, and how it discovers the hardware it runs on using the Device Tree. We focused on the static, physical world the kernel interacts with.

Now, we pivot from hardware to software. This module dives into the dynamic world of execution, starting with the most fundamental concept: the process. Today's lesson is designed to help you describe the task_struct data structure and explain how the kernel represents processes and threads.

Understanding task_struct is absolutely critical. It is the kernel's central data structure for managing all execution. Nearly every major subsystem—scheduling, memory management, security, I/O—interacts with it. A deep understanding of how Linux models processes and threads is a prerequisite for advanced kernel development and a frequent topic in high-level embedded interviews.


1. From Program to Process: The Basics

Before we can manage execution, we need a clear abstraction for it. We'll start by defining what a "process" is in the context of an operating system.

Linux Processes Explained: Process Table, PCB, Memory Maps & More

This video from Tech Fairy provides a clear distinction between a program stored on disk and a process running in memory. It also introduces the concept of a Process Control Block (PCB), the generic OS term for the data structure that holds all information about a process.

Please watch from the beginning to 05:51. As you watch, focus on two key points: The transformation of a static program file into a dynamic process with its own virtual address space (stack, heap, text, etc.). The introduction of the Process Control Block (PCB) and the confirmation that in Linux, this is specifically called a process descriptor, and its data structure is task_struct.

As the video explains, a process is an instance of a program in execution. The operating system needs a way to keep track of all the information associated with each running process. This "master record" is generically called the Process Control Block (PCB). In Linux, this is the task_struct. Every single thing the kernel needs to know about a process—its ID, state, memory map, open files, pending signals—is stored in, or pointed to from, its task_struct.


2. The Need for Threads

Managing concurrency by creating a separate process for every concurrent task can be inefficient. Your experience in embedded systems has likely shown you scenarios where you need multiple execution flows to handle I/O or other events without the heavy overhead of spawning new processes. This is where threads come in.

Why Are Threads Needed On Single Core Processors

The 'Core Dumped' channel has a great video that explains the motivation for threads and how they differ from processes.

Watch the segment from 02:57 to 06:40, which discusses the inefficiency of the multi-process model for a server. Then continue from 07:41 to 10:19 to see how threads solve this problem. \nPay attention to the key distinction: threads within the same process share the process's address space but maintain their own independent execution context (program counter, register set, and stack).

To summarize the key points:

  • Processes are heavyweight. Creating a new process involves setting up a new virtual address space, which is time-consuming and memory-intensive.
  • Threads are lightweight. They are units of execution within a process.
  • Shared Resources: All threads in a process share the same virtual address space (code, data, heap), file descriptors, and other resources.
  • Independent Context: Each thread has its own unique program counter, stack, and set of CPU registers. This allows them to be scheduled and run independently by the kernel.

3. The Linux Model: Everything is a "Task"

Many operating systems have distinct data structures for processes and threads. Linux takes a more elegant and unified approach.

Why Are Threads Needed On Single Core Processors

Let's return to the 'Core Dumped' video for a crucial insight into the Linux implementation.

Watch this short clip from 11:26 to 12:02. It directly explains the Linux philosophy of representing both processes and threads.

This is a fundamental concept in Linux: there is no separate data structure for a thread. Both traditional processes and threads are represented by struct task_struct. To the Linux scheduler, everything is simply a "task" or "schedulable entity."

So, how does the kernel distinguish between them? The difference lies in what resources they share.

Processes — The Linux Kernel documentation

The Linux Kernel Labs documentation explains this unified model very clearly. It contrasts the Linux approach with other OS designs and shows how resource sharing is implemented.

Read the section titled 'Threads'. Focus on the diagram and the explanation that follows. The key is that task_struct doesn't contain the resources themselves, but pointers to them. Threads from the same process will have these pointers pointing to the same resource instances (e.g., the same memory map).

When a new task is created using the clone() system call (which both fork() and pthread_create() use under the hood), flags can be passed to specify what should be shared.

  • If the CLONE_VM flag is set, the new task shares the virtual memory space of the parent. This is the defining characteristic of a thread.
  • If CLONE_VM is not set, the new task gets a copy of the parent's memory space (using copy-on-write). This is what we consider a traditional process.

4. A Tour of struct task_struct

Now that we understand the "what" and "why," let's dive into the task_struct itself. It is one of the largest and most complex data structures in the kernel, residing in the include/linux/sched.h header file.

Linux Kernel task_struct Definition Snippet
This image shows a portion of the `task_struct` definition from the kernel source code. It highlights the complexity and variety of information the kernel tracks for each task.

Given your goal of deep kernel knowledge, we'll go beyond a surface-level description and examine the key fields that are crucial for understanding kernel operations.

Linux Context Switching Internals: Part 1 - Process State and Memory

The blog 'Coding Confessions' has an excellent, detailed breakdown of task_struct and its most important fields. This reading will serve as our guide for this tour.

Read the following sections from the article. We will use them to explore the key components of task_struct: Process State Management in Linux: An introduction to the structure. Process Execution States: Describes the __state field, which holds the task's current state (e.g., running, sleeping). Process Kernel Stack: Explains the stack field, which points to the task's private kernel stack. Memory Management State of the Process: Describes the mm field, the link to the task's virtual address space. Scheduler Entity and Runtime Management: Touches on the se field, used by the scheduler. CPU State and Register Management: Explains the thread_struct field for architecture-specific state.

Let's consolidate the key fields from that reading into logical groups:

  • State and Identification:

    • pid_t pid: The unique Process ID.
    • volatile long __state: The task's current state. This determines if a task is eligible to run. Key states include:
      • TASK_RUNNING: Either running on a CPU or waiting in a run queue.
      • TASK_INTERRUPTIBLE: Sleeping, but can be woken by a signal.
      • TASK_UNINTERRUPTIBLE: Sleeping and cannot be woken by a signal, typically waiting for I/O.
      • __TASK_STOPPED: The task is stopped (e.g., by a debugger).
      • __TASK_TRACED: The task is being traced by another process.
    • int exit_code: The exit code returned by the task upon termination.
  • Scheduling:

    • struct sched_entity se: The "scheduler entity" used by the Completely Fair Scheduler (CFS). It contains fields like vruntime to track how much CPU time the task has consumed. We will explore this in detail in a future lesson.
    • int prio, int static_prio, int normal_prio: Fields related to the task's priority.
  • Memory Management:

    • struct mm_struct *mm: A pointer to the memory descriptor, which defines the task's entire virtual address space. For a traditional process, this is unique. For threads within the same process, this pointer is identical, pointing to the same mm_struct.
    • struct mm_struct *active_mm: The active memory context. For a normal user process, this is the same as mm. For a kernel thread, mm is NULL, but active_mm might point to the mm_struct of the user process it was last executing on behalf of.
  • Execution Context & Credentials:

    • struct thread_struct thread: This is crucial. It holds the architecture-specific hardware context of the task. When the kernel switches away from this task, it saves the CPU registers (like the stack pointer and instruction pointer) into this structure. This is how a task's execution can be paused and resumed. Your background in embedded systems makes understanding this hardware interface particularly relevant.
    • const struct cred *real_cred, const struct cred *cred: Pointers to structures holding the task's credentials (user ID, group ID, capabilities).
Linux Kernel Process and Thread Data Structures
This diagram shows the relationship between `task_struct`, the architecture-specific `thread_struct`, and the `cpu_context` where registers are saved. This is the core mechanism for context switching.
  • Relationships:

    • struct task_struct __rcu *real_parent, struct task_struct __rcu *parent: Pointers to the parent task.
    • struct list_head children, struct list_head sibling: Linked lists that form the process tree.
  • Resources:

    • struct files_struct *files: A pointer to the structure containing all the file descriptors opened by this task.

The current Macro

A common requirement in kernel code is to get a pointer to the task_struct of the currently executing task. This is done via a highly optimized, architecture-specific macro called current.

Processes — The Linux Kernel documentation

Accessing the currently running task is a frequent operation in the kernel. This section from the Linux Kernel Labs documentation explains how the current macro works.

Read the section 'Accessing the current process' to understand the purpose and importance of the current macro.

For example, if you were writing a system call and needed the PID of the calling process, you would simply use current->pid.


5. Interview Scenario

Let's put this knowledge into a practical interview context.

Question: "You're writing a character device driver. In your read() implementation, you need to determine if the process calling read() has a specific capability (e.g., CAP_SYS_ADMIN) before allowing a privileged operation. How would you access this information?"

Your Thought Process:

  1. I need information about the process that is currently executing my driver code. The kernel provides the current macro for this. current gives me a pointer to the task_struct of the calling process.
  2. The task_struct contains all information about the process, including its security credentials. I'd look for a field related to credentials. From my knowledge of the structure, this is the cred field.
  3. current->cred gives me a pointer to a struct cred. This structure contains the user/group IDs and, importantly, the task's capabilities.
  4. The kernel provides helper functions to check capabilities. The standard function is capable().
  5. Therefore, the solution would be to call capable(CAP_SYS_ADMIN) within my driver's read() function. This function implicitly operates on the current task's credentials.

Answer: "I would use the capable() kernel helper function, passing it the CAP_SYS_ADMIN macro. This function automatically checks the credentials of the currently running task, which are accessible via the cred field of the task_struct pointed to by the current macro. If capable() returns true, I would proceed with the privileged operation."


Conclusion

In this lesson, we established the core concepts of process and thread representation in Linux. You are now equipped with a detailed mental model of the task_struct and its central role in the kernel.

Key Takeaways:

  • The task_struct is the Linux kernel's "Process Control Block" or "process descriptor."
  • Linux uses a unified model where both processes and threads are represented as tasks via struct task_struct.
  • The distinction between a process and a thread is based on resource sharing. Threads in the same process share their mm_struct (address space), files_struct (file descriptors), and other key resources.
  • The task_struct is a comprehensive data structure holding a task's state, scheduling information, memory map (mm_struct), credentials (cred), and architecture-specific hardware state (thread_struct).
  • The current macro is the standard, efficient way for kernel code to get a pointer to the task_struct of the currently executing task.

In our next lesson, we will build directly on this foundation to explain the lifecycle of a process in Linux, including creation via fork/clone, execution, and termination. We will see how the task_struct is allocated and initialized when a new task is born.