What Is task_struct in Linux? – Free Linux Device Driver Training

Linux task_struct Explained: The Root of Every Thread | Free Linux Kernel Course – EmbeddedPathashala

Linux task_struct: The Root of Every Thread
Free Linux Kernel Development Course – EmbeddedPathashala
⏱ ~30 min read
💻 Kernel 6.x
📚 Intermediate

In this free Linux kernel programming tutorial, we explore one of the most important data structures in all of Linux: the task_struct. Every single thread running on your Linux system — whether a user application thread or a kernel background thread — is represented inside the kernel by exactly one task_struct. It is the root of everything the kernel knows about a running task.

Understanding task_struct is essential for anyone taking a free Linux kernel development course, writing Linux device drivers, or working in embedded Linux. Once you understand it, you can read the kernel scheduler, understand how signals work, see how CPU affinity is tracked, and write kernel modules that safely inspect running processes.

Topics Covered:
task_struct Linux Thread Model Process vs Thread current macro sched.h Kernel Module Free Linux Kernel Course Linux 6.x

What You Will Learn

  • Why Linux uses task_struct instead of separate “process” and “thread” descriptors
  • What information is stored inside task_struct and how it is organized
  • The key fields of task_struct relevant to kernel module developers
  • How to access the current thread’s task_struct inside a kernel module using the current macro
  • How to walk the list of all task structures in the kernel
  • The difference between PID, TID, and TGID in Linux — and why it matters
  • How kernel threads differ from user threads in terms of their task_struct
  • Real kernel module code examples you can try on your Linux 6.x system

Prerequisites

  • Basic C programming — especially structs and pointers
  • Familiarity with writing a simple Linux kernel module (LKM)
  • Understanding of what a process and thread are at the OS level
  • Recommended: read our previous lecture on Linux Process Virtual Address Space (VAS)

What Is task_struct and Why Does It Exist?

When the Linux kernel needs to run your program, it doesn’t just load the binary and jump to main(). It creates a detailed record of everything about that execution context — the program’s memory layout, its CPU state, its file descriptors, its signal handlers, its scheduling priority, and much more. This record is called the task structure, represented as struct task_struct in the kernel source.

The task structure lives in the header file include/linux/sched.h in the kernel source tree. In Linux 6.x, this is an extremely large structure — it has hundreds of fields covering every aspect of a thread’s state. As a kernel developer, you don’t need to memorize every field. You need to understand the major categories and know where to look when you need a specific piece of information.

Historical Note — “Process Descriptor” confusion: Older books and tutorials often call task_struct the “process descriptor.” This is misleading. In Linux, there is no separate structure for processes versus threads at the kernel level. Both are tasks. A process is simply a task that happens to be the leader of a group of tasks sharing the same memory space. Always use the term task structure to be precise.
task_struct — The Root Metadata for Every Thread
Thread
(P2 · thrd2)
Executing in CPU
User or Kernel Mode
↓
struct task_struct
pid_t pid;           /* thread ID */
pid_t tgid;        /* process ID */
char comm[16];    /* name */
long state;        /* run/sleep/stop */
int prio;          /* priority */
struct mm_struct *mm; /* memory */
struct files_struct *files; /* FDs */
struct signal_struct *signal;
cpumask_t cpus_allowed;
… hundreds more fields

Each thread → exactly one task_struct. The kernel keeps all task_structs in a doubly-linked list.

The Linux Thread Model: Everything Is a Task

Here is something that surprises many people learning Linux kernel programming: Linux does not have a separate concept of “thread” at the kernel level. The kernel only knows about tasks. What we call a “thread” in user space is simply a task that shares its virtual address space, file descriptor table, and signal handlers with other tasks in the same group.

What we call a “process” with multiple threads looks like this to the kernel:

How Linux Sees a Multi-Threaded Process
Process P2 (user space view: “one process, 3 threads”)
task_struct
pid = 1001
tgid = 1001
comm = “myapp”
← Thread Leader
(main thread)
task_struct
pid = 1002
tgid = 1001
comm = “myapp”
← Thread 2
task_struct
pid = 1003
tgid = 1001
comm = “myapp”
← Thread 3
All three task_structs point to the same mm_struct (shared VAS), same files_struct (shared FDs), same signal_struct (shared signals)

The kernel sees 3 separate tasks. User space sees “one process with 3 threads”.

PID, TID, and TGID — Clearing Up the Confusion

This is one of the most confusing aspects of Linux for beginners. Let’s lay it out clearly:

TermField in task_structWhat It Really MeansWhat getpid() / gettid() Returns
PID (kernel)pidUnique ID for this specific task (what most people call Thread ID)gettid() returns this
TGID (kernel)tgidThread Group ID — shared by all tasks in the same process groupgetpid() returns this
PPIDreal_parent->tgidParent process IDgetppid() returns this
This trips up everyone: In user space, when you call getpid() in a multithreaded program, all threads in the process return the same value — that’s the TGID in the kernel. When you call gettid() (available since glibc 2.30 without a syscall wrapper), each thread returns a different value — that’s the per-task PID in the kernel. The kernel’s internal naming is unfortunately the reverse of what most people expect.

Verifying This From User Space

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>

void *thread_fn(void *arg) {
    /* getpid() returns TGID — same for all threads in the process */
    printf("Thread: getpid()  = %d  (kernel TGID)\n", getpid());
    /* SYS_gettid returns per-thread kernel PID */
    printf("Thread: gettid()  = %ld (kernel PID for this task)\n",
           syscall(SYS_gettid));
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    printf("Main:   getpid()  = %d  (kernel TGID)\n", getpid());
    printf("Main:   gettid()  = %ld (kernel PID for main task)\n",
           syscall(SYS_gettid));
    pthread_create(&t1, NULL, thread_fn, NULL);
    pthread_create(&t2, NULL, thread_fn, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return 0;
}

Compile and run this with gcc -pthread -o pid_demo pid_demo.c && ./pid_demo. You will see that getpid() returns the same value for all three tasks, while gettid() returns three different values.

Key Fields of task_struct That Every Kernel Developer Should Know

The full task_struct in Linux 6.x is enormous. Let’s focus on the fields you’ll encounter regularly when writing kernel modules or Linux device drivers:

Identity and Naming

FieldTypeDescription
pidpid_tPer-task unique ID (what kernel calls PID, user space calls TID)
tgidpid_tThread Group ID — the “process ID” seen by user space via getpid()
commchar[TASK_COMM_LEN]Short task name (first 15 chars of executable name). TASK_COMM_LEN=16 in Linux 6.x

Scheduling and Priority

FieldTypeDescription
__stateunsigned intCurrent task state: TASK_RUNNING, TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE, etc.
priointDynamic scheduling priority (changes based on nice value + scheduler boosting)
static_priointFixed priority from nice value. Range 100 (nicest = -20) to 139 (least nice = +19)
policyunsigned intScheduling policy: SCHED_NORMAL, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE
cpus_ptrcpumask_t*Which CPUs this task is allowed to run on (CPU affinity mask)
Linux 6.x Change: In older kernels (before 5.14), the task state field was called state. Since Linux 5.14, it has been renamed to __state to make direct access from outside the scheduler harder and to encourage use of proper accessor functions. In kernel modules, use task_is_running(task) or check the state via exported helper functions rather than reading __state directly.

Memory Information

FieldTypeDescription
mmstruct mm_struct *Points to the memory descriptor — the full VAS description for this task. NULL for kernel threads.
active_mmstruct mm_struct *The currently active mm. For kernel threads, this is borrowed from the last user task that ran on this CPU.

File System and I/O

FieldTypeDescription
filesstruct files_struct *Open file descriptor table. Threads in the same process share this.
fsstruct fs_struct *Filesystem information — current working directory, root directory.

Relationships Between Tasks

FieldTypeDescription
real_parentstruct task_struct *The task that actually created this task (biological parent)
parentstruct task_struct *The task receiving SIGCHLD when this task exits (may differ after ptrace)
childrenstruct list_headLinked list of all child tasks
siblingstruct list_headLinks this task into the parent’s children list
group_leaderstruct task_struct *Points to the main thread of the process (the thread group leader)

Accessing task_struct in a Kernel Module: The current Macro

The single most important thing to know for kernel module development: you can always get a pointer to the task_struct of the currently executing thread using the current macro.

current is defined in include/asm-generic/current.h (with architecture-specific optimizations). It returns a pointer of type struct task_struct * pointing to the task that is executing the kernel module code right now. For example, if your kernel module’s read() function is called by a user process making a read() system call, then inside your read() handler, current points to that user process’s task structure.

Simple Kernel Module: Reading Current Task Info

// file: show_task_info.c
// Simple kernel module to print info about the current task
// Tested on Linux 6.x

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>          /* task_struct, current */
#include <linux/sched/signal.h>   /* for_each_process */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Demonstrate task_struct access via current macro");

static int __init show_task_init(void)
{
    /* 'current' gives us a pointer to the task_struct of
     * whoever is executing this code right now.
     * When loading the module via insmod, that is the
     * shell process running insmod. */

    pr_info("=== Task Info via current ===\n");
    pr_info("Task name   : %s\n",  current->comm);
    pr_info("Kernel PID  : %d\n",  current->pid);
    pr_info("Kernel TGID : %d\n",  current->tgid);
    pr_info("Task state  : %u\n",  (unsigned int)current->__state);

    /* Check if this task has a user-space VAS.
     * Kernel threads have mm == NULL */
    if (current->mm)
        pr_info("Has user VAS : YES (user-space process)\n");
    else
        pr_info("Has user VAS : NO  (kernel thread)\n");

    /* Print the parent task's name */
    pr_info("Parent name : %s\n",
            current->real_parent ? current->real_parent->comm : "none");

    return 0;
}

static void __exit show_task_exit(void)
{
    pr_info("show_task_info: module unloaded\n");
}

module_init(show_task_init);
module_exit(show_task_exit);

Makefile for This Module

obj-m := show_task_info.o

KDIR := /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

Build and load it:

make
sudo insmod show_task_info.ko
dmesg | tail -20
sudo rmmod show_task_info
What you will see: The task name will be insmod (the program that loaded your module), and the PID/TGID will be the PID of your insmod process. This perfectly illustrates how current always reflects who is currently executing kernel code — there is no separate kernel “main thread.”

Walking All Tasks in the System

The kernel keeps all task_structs linked together in a circular doubly-linked list. The list is rooted at init_task — the very first task created at boot (PID 1’s ancestor). The macro for_each_process() iterates over all thread group leaders (i.e., all processes). The macro for_each_process_thread() iterates over every individual task (every thread).

Locking Required: Before iterating the task list in a kernel module, you must hold the tasklist_lock read lock (or use RCU locking). Failing to do so risks reading partially-updated data if tasks are being created or destroyed concurrently. The example below shows the correct approach using RCU.
// file: list_all_tasks.c
// List all tasks in the system — with proper RCU locking
// Linux 6.x

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <linux/rcupdate.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("List all processes and threads in the system");

static int __init list_tasks_init(void)
{
    struct task_struct *task;
    struct task_struct *thread;
    unsigned long count = 0;

    pr_info("=== All tasks in the system ===\n");
    pr_info("%-20s %6s %6s %s\n", "Name", "PID", "TGID", "State");
    pr_info("--------------------------------------------\n");

    /* RCU read lock protects the task list during iteration */
    rcu_read_lock();

    /*
     * for_each_process iterates over thread group leaders (processes).
     * for_each_thread iterates over all threads within a process.
     */
    for_each_process(task) {
        for_each_thread(task, thread) {
            pr_info("%-20s %6d %6d %u\n",
                    thread->comm,
                    thread->pid,
                    thread->tgid,
                    (unsigned int)thread->__state);
            count++;
        }
    }

    rcu_read_unlock();

    pr_info("--------------------------------------------\n");
    pr_info("Total tasks listed: %lu\n", count);
    return 0;
}

static void __exit list_tasks_exit(void)
{
    pr_info("list_all_tasks: module unloaded\n");
}

module_init(list_tasks_init);
module_exit(list_tasks_exit);

Kernel Threads vs User Process Tasks: How to Tell Them Apart

Both kernel threads and user process threads have a task_struct. But there is one reliable way to distinguish them: check the mm field.

Task Typetask->mmtask->active_mmExamples
User space threadPoints to the process’s mm_struct (non-NULL)Same as mmAny user application thread
Kernel threadNULL (no user VAS)Borrowed from last user task on this CPUkworker, ksoftirqd, kthreadd, migration
/* Safe check for kernel thread vs user task */
if (task->mm == NULL) {
    pr_info("%s is a kernel thread\n", task->comm);
} else {
    pr_info("%s is a user-space task (TGID=%d)\n",
            task->comm, task->tgid);
}

Task States: What Does a Thread Do Between Running?

A thread is not always running on the CPU. The __state field in task_struct tracks the current state. Understanding task states is fundamental for debugging hangs, high CPU issues, and blocked I/O in both user applications and kernel modules.

Linux Task State Transitions (Linux 6.x)
TASK_RUNNING
On CPU or in run queue, ready to execute. This state means “runnable” — not just “currently on CPU”.
TASK_INTERRUPTIBLE
Sleeping, waiting for an event. Can be woken by a signal. Most blocking I/O uses this state.
TASK_UNINTERRUPTIBLE
Deep sleep — signals are ignored. Used for waits that must not be interrupted (disk I/O). The dreaded “D” state in ps.
TASK_STOPPED / TRACED
Stopped by SIGSTOP or being debugged via ptrace (e.g., gdb breakpoint).
EXIT_ZOMBIE / EXIT_DEAD
Task has exited but task_struct is still present waiting for parent to call wait(). Then moves to EXIT_DEAD and is freed.
⇄ scheduler picks task → TASK_RUNNING → runs on CPU
sleep() / wait for I/O → TASK_INTERRUPTIBLE
signal arrives → wakes from INTERRUPTIBLE
disk I/O wait → TASK_UNINTERRUPTIBLE (signals blocked)
SIGSTOP / ptrace attach → TASK_STOPPED
exit() called → EXIT_ZOMBIE → parent wait() → EXIT_DEAD → freed
Practical Tip: The ps aux output’s STAT column maps to these states: R = TASK_RUNNING, S = TASK_INTERRUPTIBLE (sleeping), D = TASK_UNINTERRUPTIBLE (uninterruptible sleep — often disk I/O), T = TASK_STOPPED, Z = EXIT_ZOMBIE. High count of “D” state tasks usually means a storage I/O issue.

Best Practices for Working with task_struct in Kernel Modules

✅ Do This
  • Always use get_task_struct(task) to increment the reference count before storing a pointer to a task — otherwise the task may be freed while you hold the pointer
  • Call put_task_struct(task) when you’re done with a borrowed task_struct pointer
  • Use rcu_read_lock() / rcu_read_unlock() when iterating the task list via for_each_process
  • Access comm via get_task_comm(buf, task) for a safe, NUL-terminated copy of the task name
  • Check for NULL before dereferencing task->mm — kernel threads have NULL mm
❌ Avoid This
  • Do not store raw task_struct pointers without bumping the reference count — the task can exit and the structure can be freed
  • Do not iterate the task list without proper locking — you will cause race conditions or kernel oops
  • Do not sleep inside an RCU read-side critical section (between rcu_read_lock and rcu_read_unlock)
  • Do not directly modify fields like prio or comm without understanding the kernel’s internal invariants — use the proper kernel APIs
  • Do not use task state field name state in kernel 5.14+ — it has been renamed to __state

Performance Considerations

task_struct is a hot data structure — the scheduler accesses it thousands of times per second per CPU core. For this reason, the kernel carefully places the most frequently accessed fields (like __state, prio, and the run queue links) at the beginning of the structure, so they fit in the same or adjacent cache lines. When you write kernel code that iterates over many task structures, be aware of cache effects — accessing many task_structs sequentially may cause significant cache pressure on systems with thousands of threads.

Security Considerations

The task_struct contains the security context for each task. The security field (a void pointer) hooks into Linux Security Modules (LSM) like SELinux or AppArmor. Each task’s security label is stored here and consulted on every access control decision. The cred field points to struct cred, which holds the task’s UID, GID, capabilities, and security namespace. Kernel modules that modify credentials or bypass security checks are a common source of local privilege escalation vulnerabilities — never modify current->cred directly; use the proper kernel credential management APIs.

Summary and Key Takeaways

  • struct task_struct, defined in include/linux/sched.h, is the root metadata structure for every thread in Linux
  • Linux has no separate “process” and “thread” descriptors — everything is a task
  • A “process” with N threads = N task_structs in the kernel, all sharing the same TGID, mm_struct, files_struct, and signal_struct
  • The current macro gives you a pointer to the task_struct of the thread currently executing your kernel code
  • Kernel threads have mm == NULL; user-space tasks have a valid mm pointer
  • Always use get_task_struct() and put_task_struct() to safely hold references to task structures
  • Use RCU locking when iterating the task list with for_each_process or for_each_process_thread
  • The __state field (renamed from state in Linux 5.14) tracks whether a task is running, sleeping, stopped, or exiting

Authoritative References

Frequently Asked Questions

Q1: Is task_struct the same as a process control block (PCB)?
Conceptually yes — it serves the same role as the PCB described in OS textbooks. But in Linux, task_struct is per-thread, not per-process. This is a key implementation difference from some other Unix systems where the PCB is per-process.
Q2: How large is task_struct in Linux 6.x?
It varies with kernel configuration, but on a typical 64-bit Linux 6.x kernel with debugging options, task_struct is around 9–10 KB. With all optional fields, it can be larger. This is why the kernel does not allocate one per CPU cycle — it reuses them carefully with proper reference counting.
Q3: Can I read another process’s task_struct from a kernel module?
Yes, you can iterate the task list and access other tasks’ task_structs. You must use proper locking (RCU or tasklist_lock) and you must increment the reference count with get_task_struct() if you store the pointer. Reading is safe; modifying another task’s fields without deep kernel knowledge is dangerous.
Q4: What is the difference between real_parent and parent in task_struct?
real_parent is the task that actually forked this task. parent is the task that will receive SIGCHLD when this task exits. They differ when a debugger (like gdb or strace) attaches to a process via ptrace — in that case, parent changes to the debugger’s task, but real_parent remains the original creator.
Q5: Why was the state field renamed to __state in Linux 5.14?
The rename was intentional — the double underscore prefix is a convention in C that signals “internal use, do not access directly.” The kernel maintainers wanted to discourage kernel modules and drivers from reading the raw state field, and instead push them toward using proper accessor macros that handle memory ordering correctly. The double underscore is not enforced by the compiler but serves as a strong warning.
Q6: Where can I learn more Linux kernel programming for free?
EmbeddedPathashala offers a completely free Linux kernel development course covering kernel modules, Linux device drivers, memory management, process management, and embedded Linux. All tutorials are available at embeddedpathashala.com — no registration or payment required.
Q7: How does kthreadd relate to task_struct?
kthreadd (PID 2) is the kernel thread daemon — every kernel thread you see in ps output (kworker, ksoftirqd, etc.) has kthreadd as its parent. Each of these kernel threads has its own task_struct with mm == NULL. You can see them all with ps aux and look for entries with [brackets] in the COMMAND column.
Q8: Is it safe to use current outside of process context in a kernel module?
Be careful — current is always valid as a pointer, but its value may not be meaningful in certain interrupt contexts or softirq handlers. In hard interrupt context, current refers to whatever task happened to be running when the interrupt arrived, which may be completely unrelated to the interrupt’s purpose. For interrupt handlers, do not rely on current to represent a meaningful application context.
{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ {“@type”:”Question”,”name”:”Is task_struct the same as a process control block (PCB)?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Conceptually yes, but in Linux task_struct is per-thread, not per-process — a key implementation difference from textbook OS descriptions.”}}, {“@type”:”Question”,”name”:”How large is task_struct in Linux 6.x?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Approximately 9–10 KB on a typical 64-bit Linux 6.x build with debug options. It varies with kernel config.”}}, {“@type”:”Question”,”name”:”Why was the state field renamed to __state in Linux 5.14?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”To signal that it is an internal field not to be accessed directly, encouraging use of proper accessor macros that handle memory ordering.”}}, {“@type”:”Question”,”name”:”Where can I learn Linux kernel programming for free?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”EmbeddedPathashala at embeddedpathashala.com offers a completely free Linux kernel development course — no registration needed.”}} ] }

Free Linux Kernel & Device Drivers Course

Learn Linux kernel programming, device drivers, embedded systems — all for free at EmbeddedPathashala

Visit EmbeddedPathashala

Leave a Reply

Your email address will not be published. Required fields are marked *