A program I was debugging had six POSIX threads, ps showed one process, and /proc showed several task directories. This produced the inevitable question: are Linux threads implemented in userspace or in the kernel? The unhelpful but accurate answer is both.

The pthread library provides the interface the application sees: pthread_create, mutexes, condition variables, thread-local state, and POSIX rules. With NPTL, creating a thread eventually uses the kernel’s clone() facility to create another schedulable task that shares selected resources with its siblings.

The kernel does not need a completely different object named “thread.” It schedules tasks. Flags passed to clone() determine whether tasks share an address space, file descriptor table, signal handlers, and other state. Tasks in the same thread group form what userspace presents as one process.

This is visible under procfs:

ls /proc/$(pidof myprogram)/task

Each entry is a task ID. The main thread’s task ID is also the process ID normally shown by tools. Updated ps can display either the process-oriented view or individual threads, depending on its options. Neither view is lying; they answer different questions.

Synchronization also crosses the boundary selectively. An uncontended NPTL mutex is normally handled with atomic operations in userspace. Entering the kernel for every lock and unlock would be expensive. When a thread cannot acquire the lock, a futex operation lets it sleep in the kernel; an unlock can wake a waiter when necessary.

This “fast userspace, kernel on contention” pattern explains why futex means fast userspace mutex. It does not mean the kernel knows nothing about waiting threads. The kernel supplies the queueing and scheduling that userspace cannot safely improvise.

Signal handling is where abstractions become less tidy. Some signals target the process or thread group, while others target a particular task. The library and kernel cooperate to provide POSIX behaviour. Code should use pthread signal operations instead of guessing that numeric task IDs are interchangeable with old Linux process IDs.

The same warning applies to debugging. If one thread blocks, inspect the task view; if the question concerns resource ownership, remember that file descriptors and memory may be shared. Killing a random task because it looks like a child process is not thread debugging. It is experimental program termination.

I find the model easier once I stop asking whether a thread is “really” a process. In Linux it is a schedulable task with a particular set of shared resources, grouped so userspace can provide process and pthread semantics. That is a mechanism, not a philosophical position, and it matches what the tools are showing.