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.
What You Will Learn
- Why Linux uses
task_structinstead of separate “process” and “thread” descriptors - What information is stored inside
task_structand how it is organized - The key fields of
task_structrelevant to kernel module developers - How to access the current thread’s
task_structinside a kernel module using thecurrentmacro - 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.
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.
(P2 · thrd2)
User or Kernel Mode
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:
tgid = 1001
comm = “myapp”
← Thread Leader
(main thread)
tgid = 1001
comm = “myapp”
← Thread 2
tgid = 1001
comm = “myapp”
← Thread 3
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:
| Term | Field in task_struct | What It Really Means | What getpid() / gettid() Returns |
|---|---|---|---|
| PID (kernel) | pid | Unique ID for this specific task (what most people call Thread ID) | gettid() returns this |
| TGID (kernel) | tgid | Thread Group ID — shared by all tasks in the same process group | getpid() returns this |
| PPID | real_parent->tgid | Parent process ID | getppid() returns this |
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
| Field | Type | Description |
|---|---|---|
pid | pid_t | Per-task unique ID (what kernel calls PID, user space calls TID) |
tgid | pid_t | Thread Group ID — the “process ID” seen by user space via getpid() |
comm | char[TASK_COMM_LEN] | Short task name (first 15 chars of executable name). TASK_COMM_LEN=16 in Linux 6.x |
Scheduling and Priority
| Field | Type | Description |
|---|---|---|
__state | unsigned int | Current task state: TASK_RUNNING, TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE, etc. |
prio | int | Dynamic scheduling priority (changes based on nice value + scheduler boosting) |
static_prio | int | Fixed priority from nice value. Range 100 (nicest = -20) to 139 (least nice = +19) |
policy | unsigned int | Scheduling policy: SCHED_NORMAL, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE |
cpus_ptr | cpumask_t* | Which CPUs this task is allowed to run on (CPU affinity mask) |
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
| Field | Type | Description |
|---|---|---|
mm | struct mm_struct * | Points to the memory descriptor — the full VAS description for this task. NULL for kernel threads. |
active_mm | struct 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
| Field | Type | Description |
|---|---|---|
files | struct files_struct * | Open file descriptor table. Threads in the same process share this. |
fs | struct fs_struct * | Filesystem information — current working directory, root directory. |
Relationships Between Tasks
| Field | Type | Description |
|---|---|---|
real_parent | struct task_struct * | The task that actually created this task (biological parent) |
parent | struct task_struct * | The task receiving SIGCHLD when this task exits (may differ after ptrace) |
children | struct list_head | Linked list of all child tasks |
sibling | struct list_head | Links this task into the parent’s children list |
group_leader | struct 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
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).
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 Type | task->mm | task->active_mm | Examples |
|---|---|---|---|
| User space thread | Points to the process’s mm_struct (non-NULL) | Same as mm | Any user application thread |
| Kernel thread | NULL (no user VAS) | Borrowed from last user task on this CPU | kworker, 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.
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
- 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 borrowedtask_structpointer - Use
rcu_read_lock()/rcu_read_unlock()when iterating the task list viafor_each_process - Access
commviaget_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
- Do not store raw
task_structpointers 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
prioorcommwithout understanding the kernel’s internal invariants — use the proper kernel APIs - Do not use task state field name
statein 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 ininclude/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
currentmacro 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()andput_task_struct()to safely hold references to task structures - Use RCU locking when iterating the task list with
for_each_processorfor_each_process_thread - The
__statefield (renamed fromstatein Linux 5.14) tracks whether a task is running, sleeping, stopped, or exiting
Authoritative References
- Linux kernel source: include/linux/sched.h — task_struct definition (Elixir cross-reference)
- Linux Kernel Scheduler Documentation (kernel.org)
- gettid(2) man page — Linux Thread ID explained (man7.org)
- Linux RCU Documentation — safe task list traversal (kernel.org)
Frequently Asked Questions
Free Linux Kernel & Device Drivers Course
Learn Linux kernel programming, device drivers, embedded systems — all for free at EmbeddedPathashala
Visit EmbeddedPathashala