AOSP Framework & Internals
3 min read

Threads in Linux Kernel

Learn how Linux implements threads under the hood and how they differ from standard processes.

In traditional operating systems like Windows, processes and threads are treated as entirely different concepts. A process is a heavy container, and threads are lightweight execution units that live inside that container.

The Linux kernel takes a brilliantly unified approach. In Linux, there is absolutely no technical difference between a process and a thread at the structural level.

Threads are just Processes

To the Linux scheduler, a thread is just another task_struct in the execution queue.

When a Java Android app creates a new Thread to perform a background download, the Android runtime executes the clone() system call. The kernel creates a new task_struct for the thread, assigning it a unique Thread ID (TID).

The only difference between this new thread and a completely separate process is memory sharing.

  • When Zygote forks a new app, the kernel gives the new app a completely isolated virtual memory space.
  • When an app creates a thread via clone(), the kernel sets a flag telling the new task_struct to point to the exact same virtual memory space as the parent.

Because they share the same memory pointers (mm_struct), threads can easily read and write to the same variables, making them "lightweight" and incredibly fast to create.

Thread IDs vs Process IDs

Because threads and processes use the same task_struct backend, their IDs can be confusing for developers looking at kernel logs.

  • TID (Thread ID): The unique kernel identifier for the specific execution thread.
  • PID (Process ID): The identifier for the overall application.
  • TGID (Thread Group ID): In a multi-threaded app, all threads share the same TGID, which is equal to the TID of the original "main" thread.

When you run ps in the ADB shell, the "PID" column is actually displaying the TGID. If you want to see every individual thread running inside an Android app, you must use the -T flag.

Inspecting Threads via ADB

Let's look at the threads inside the system_server (the core of the Android framework).

adb shell ps -T -p <SYSTEM_SERVER_PID>

Output:

USER           PID   TID CMD
system        1500  1500 system_server
system        1500  1505 HeapTaskDaemon
system        1500  1510 ReferenceQueueD
system        1500  1542 ActivityManager
system        1500  1545 PackageManager
system        1500  1580 binder:1500_1

Here, you can clearly see that system_server is just a collection of threads. Notice how the TID is unique for every thread, but they all share the same PID (TGID) of 1500.

Android Thread Pools

Android applications rely heavily on threads to keep the UI responsive. The Main Thread (UI Thread) handles drawing the screen at 60 FPS. If you run a heavy database query on the Main Thread, the app will freeze and trigger an ANR (Application Not Responding) crash.

To manage this, the Android framework heavily utilizes Thread Pools. Instead of asking the kernel to create and destroy a new task_struct every time a short background task is needed (which involves expensive context switching), Android creates a pool of sleeping threads.

When a task arrives via an ExecutorService, it wakes up an existing thread, assigns the task, and puts it back to sleep when finished, dramatically reducing kernel overhead.

// Example of an Android Thread Pool in action
ExecutorService threadPool = Executors.newFixedThreadPool(4);

threadPool.execute(new Runnable() {
    @Override
    public void run() {
        // This runs on a pre-warmed background thread
        performHeavyDatabaseQuery();
    }
});