A process shows up in ps. It has a PID. It is not a zombie. It consumes almost no CPU. You send it a signal, nothing happens. You wait five minutes, still nothing. From the outside, it looks alive. From the inside, it might as well be frozen.

/

This is not rare. I have debugged services that appeared running but were making no progress, threads that looked fine until I checked their individual states, and I/O paths stalled by a filesystem mount that had failed several layers down. The tools I reach for first - ps, top, and htop - give me snapshots, but they do not tell me what a task is waiting for, whether it can make progress, or which thread in a multithreaded application is stuck. That is the gap I am going to close.

What "Doing Nothing" Means

The Gap Between State and Progress

When I run ps, I see columns like PID, STAT, TIME, and CMD. The default view is compact by design. It shows whether the kernel considers a task runnable, sleeping, or dead, but it does not show whether the task is making forward progress.

A process can sit in S (interruptible sleep) for hours, waiting on a socket that will never produce data. Another can be in D (uninterruptible sleep), blocked in a kernel function while the storage layer below it has stopped responding. Both look alive in the process table, but neither is doing useful work.

The kernel tracks task state, scheduler eligibility, and wait queues. If a task is waiting for a condition that never arrives, it can remain there indefinitely. That is why ps is a snapshot of task state, not a diagnosis of task health. The real question is always: what is this task waiting for, and will that condition ever be satisfied?

Why ps Hides So Much

ps reads from /proc, which exposes kernel data in a human-readable format. But /proc contains much more detail than ps shows by default.

A single ps line might tell me that a process is sleeping. /proc/[PID]/status tells me how many threads it has, which signals are blocked, how many file descriptors are allocated, and how many context switches have occurred. /proc/[PID]/wchan shows the kernel function where the task is sleeping. /proc/[PID]/fd shows its open files, sockets, pipes, and devices. /proc/[PID]/io exposes cumulative I/O counters.

When I debug a stalled process, I correlate task state, thread state, open resources, wait location, and I/O activity. That is how I move from "the process looks stuck" to "this specific resource path is causing the stall."

Sleeping vs. Blocked vs. Stuck

Linux distinguishes between several forms of inactivity:

  • S - interruptible sleep. The task is waiting for an event and can be woken by a signal.

  • D - uninterruptible sleep. The task is waiting for a kernel operation, commonly I/O, and normal signals are deferred.

  • Z - zombie. The task has exited and is waiting for its parent to read the exit status.

  • T - stopped, usually by a signal such as SIGSTOP or by a debugger.

The word "stuck" is not a kernel concept. It is a human judgment. A task in S might be a healthy server waiting for its next request, or it might be waiting on a futex that will never be released. The state code alone cannot tell me which case I am seeing. I need the wait location, open resources, thread states, and evidence that the rest of the system is still moving.

State Codes Explained

The Core States

The base state is useful, but it is not the whole STAT column. ps can also show modifiers such as l for multithreaded, s for session leader, + for a foreground process group, and < for high priority.

The core states I care about most are:

  • R - running or runnable, meaning the task is executing or eligible to be scheduled.

  • S - interruptible sleep, waiting for an event.

  • D - uninterruptible sleep, usually waiting on I/O or a kernel resource.

  • T - stopped by a signal or being traced.

  • Z - zombie, meaning the task has exited but has not been reaped.

  • I - idle kernel thread.

Why D Is Feared

A task in uninterruptible sleep is blocked in the kernel and will not respond to normal termination signals while the wait is active. SIGTERM and even SIGKILL can be queued, but they are not delivered until the blocking operation completes or times out.

The operation might involve a block device, an NFS or CIFS mount, filesystem metadata, a page fault, or a kernel lock. A slow disk is one problem. A disk that has stopped responding is another. A remote filesystem with high latency is tolerable; one that has disappeared from the network can leave tasks in D indefinitely.

This behavior is intentional. Some kernel operations cannot be safely abandoned midway through. The problem is that the resource being waited on may be broken.

When I see D, I check the wait function, open filesystems and devices, kernel messages, and other tasks. If several processes are blocked in the same path, the problem is probably systemic rather than an application-specific deadlock.

Why S Is Not Always Fine

Interruptible sleep is normal for most idle tasks. A web server waiting in accept(), a worker waiting on a condition variable, and a shell waiting for input will all spend much of their time in S.

But S can also mean functional freeze. A task might be waiting on a futex that is never signaled, a pipe that is never written to, or a socket whose peer has vanished. The kernel cannot distinguish a valid wait from one that will never complete.

That is why I check wchan even for tasks in S:

  • futex_wait_queue_me points to a user-space synchronization wait.

  • ep_poll usually means an event loop is waiting for activity.

  • pipe_wait indicates a pipe read or write wait.

  • do_wait means the task is waiting for a child process.

The function name is not a complete diagnosis, but it tells me which subsystem to inspect next.

WCHAN as a Clue

The WCHAN column in ps shows the kernel function where a task is sleeping, if kernel symbols are available. It may be empty or show a zero address when the task is not sleeping or symbols cannot be resolved.

Common patterns include:

  • futex_wait_queue_me - waiting on a futex, usually pointing to application synchronization.

  • io_schedule or wait_on_page_locked - waiting for block I/O or a page operation.

  • nfs_* - waiting on a network filesystem operation.

  • ep_poll or poll_schedule_timeout - waiting in an event loop.

  • do_wait - waiting for a child process to exit.

WCHAN only identifies the current wait function. It does not show the full stack or arguments. For that, I need tools such as ftrace, bpftrace, or a debugger. For many operational incidents, the function name is enough to narrow the search dramatically.

Threads and Task Structure

Linux Thinks in Tasks

The kernel schedules task_struct objects, not "processes" in the way users typically imagine them. A process is often a thread group: several tasks sharing an address space, file descriptors, and other resources. The thread group leader provides the process ID, while each thread has its own task ID.

This matters because a process-level ps line can hide a blocked thread. The service may still have some threads handling requests while a critical coordinator or lock holder is completely stuck.

I use ps -eLf, ps -T, or ps H to see threads separately. If the STAT column contains l, the process is multithreaded. Once I suspect a stall, I switch to a per-thread view immediately.

One Stuck Thread, One Broken Service

A single blocked thread can make an entire application unresponsive if it owns a critical lock or coordinates work distribution. I have seen services where the main process remained in the process table and its socket remained open, but a coordinator thread was stuck in D waiting on a filesystem operation.

Per-thread state makes this visible. For example:

  • Thread 1 is in S, waiting on ep_poll.

  • Thread 2 is in S, waiting on futex_wait.

  • Thread 3 is in D, waiting on io_schedule.

Now I know which thread is abnormal and which subsystem is involved. I can obtain this information with ps -eLf or by inspecting /proc/[PID]/task/*/status.

Thread Counts and FDSize

/proc/[PID]/status includes Threads, which tells me how many threads the process has, and FDSize, which shows the number of allocated file descriptor slots.

A high thread count may indicate legitimate parallelism or uncontrolled thread creation. A large FDSize may reflect many connections or a file descriptor leak. Neither is automatically wrong, but both are useful clues.

If all threads are stuck in the same state, the cause is likely systemic, such as a hung filesystem or resource exhaustion. If only one thread is stuck, I can usually isolate the problem by comparing its state and wchan with the rest.

What /proc Reveals

/proc/[PID]/status

The status file is a human-readable summary containing task state, parent and thread IDs, thread count, signal masks, memory information, CPU affinity, and context switch counters.

The fields I check most often are:

  • State - the current state code and its description.

  • Threads - the number of threads.

  • FDSize - allocated file descriptor slots.

  • SigPnd and ShdPnd - pending signals for the task and thread group.

  • SigBlk - blocked signals.

  • voluntary_ctxt_switches and nonvoluntary_ctxt_switches - voluntary yields versus scheduler preemptions.

High voluntary switches usually mean the task sleeps and wakes frequently, which is normal for event-driven software. High nonvoluntary switches can indicate CPU contention or priority pressure.

/proc/[PID]/stat

The stat file is less readable but exposes more raw fields, including state, parent ID, priority, nice value, thread count, scheduling policy, and block I/O delay.

The delayacct_blkio_ticks field is useful when delay accounting is enabled. If it is large and increasing, the task has spent significant time waiting on block I/O. If it remains near zero, the stall may instead involve a lock, futex, scheduler, or non-blocking resource.

Scheduling policy and priority matter too. A real-time task can starve other work, while a low-priority task may be runnable but receive little CPU time under load.

/proc/[PID]/wchan

The wchan file contains the kernel function where the task is sleeping, or zero if it is not sleeping. It provides the same basic information as ps without requiring me to parse command output.

I use it to distinguish between waits such as io_schedule, futex_wait_queue_me, and nfs_wait_on_request. The result is not a full stack trace, but it usually identifies the subsystem responsible.

Open File Descriptors

The /proc/[PID]/fd directory contains one symlink per open descriptor. The targets show whether the process holds:

  • A socket connected to an unreachable peer.

  • A file on a stale or hung mount.

  • A pipe whose other end is no longer active.

  • A device node backed by unresponsive hardware.

If a task is in D and one descriptor points into /mnt/nfs, while the NFS mount is failing, I have a strong correlation between the abstract task state and the concrete resource causing the stall.

/proc/[PID]/fdinfo

The fdinfo directory provides descriptor details such as position, flags, mount ID, and inode. It helps when the symlink target is not enough.

For a socket or file that appears blocked, I can check whether it is configured for blocking or non-blocking operation. A blocking descriptor can wait indefinitely for data. A non-blocking descriptor should return immediately with EAGAIN, so an apparent stall may instead indicate a broken event loop or unexpected descriptor state.

I/O Counters

/proc/[PID]/io exposes cumulative I/O counters:

  • rchar - bytes requested through read operations.

  • wchar - bytes requested through write operations.

  • read_bytes - bytes actually read from storage.

  • write_bytes - bytes actually written to storage.

  • cancelled_write_bytes - write bytes cancelled before reaching storage.

These values are cumulative, so I compare them over time. If read_bytes or write_bytes is growing, the process is making I/O progress, even if slowly. If the counters are frozen while wchan shows io_schedule, the underlying storage or filesystem may be stalled.

When Waiting Becomes Stuck

Common Causes of D

The most common causes of uninterruptible sleep I see are:

  • Slow or hung block devices - failing disks, degraded RAID arrays, or repeated device timeouts.

  • Network filesystems - unreachable or highly latent NFS and CIFS mounts.

  • Swap activity - page faults waiting on a slow or unavailable swap device.

  • Filesystem operations - journal, metadata, fsync, or sync operations that cannot complete.

  • Kernel lock contention - a task holding a kernel lock while it waits on another resource.

In each case, the kernel cannot safely abandon the operation. The task remains in D until the operation completes or the relevant timeout expires.

Why Signals Do Not Help

kill -9 is not a magic escape from kernel waits. If a task is in user space or interruptible sleep, SIGKILL is usually delivered promptly. If it is in uninterruptible sleep, the signal is queued until the task returns from the wait.

The practical options are to fix the resource: restore the NFS server, repair or reset the block device, resolve the filesystem issue, or reboot if the task cannot recover. This is why D state is feared: control shifts from the process owner to the kernel and the hardware or filesystem being waited on.

Scheduler Starvation Is Different

A task can also appear stuck while remaining runnable. In R, it is eligible to run, but it may receive too little CPU time because:

  • There are more runnable tasks than CPU cores.

  • It has a low priority or high nice value.

  • A higher-priority task is monopolizing a CPU.

  • Its cgroup has a restrictive CPU quota or share.

This is not an I/O wait or kernel block. The task is simply not being scheduled often enough. Context switch counters, CPU usage, scheduling policy, priority, and system load help distinguish this case from a true blocked state.

Debugging Workflow

Start with Thread-Aware ps

When I suspect a process is stuck, I start with ps -eLf or ps -T. I look for threads in D or S, then compare their wchan values.

One thread in D while the others are in S suggests a localized problem. Many threads in D waiting in the same function suggests a shared filesystem, device, or kernel resource. This step takes seconds and often identifies the scope of the incident immediately.

Inspect /proc

Next, I inspect /proc/[PID]/status for thread count, descriptor allocation, signal masks, and context switch counters. If the process is multithreaded, I inspect /proc/[PID]/task/*/status and /proc/[PID]/task/*/wchan to find the abnormal thread.

I then list /proc/[PID]/fd and inspect /proc/[PID]/fdinfo when necessary. The goal is to connect the wait location to a real file, socket, pipe, device, or mount.

Finally, I compare /proc/[PID]/io counters over time. Growing counters indicate ongoing I/O. Frozen counters suggest that the task is waiting elsewhere or that the I/O subsystem itself is no longer progressing.

Interpret the Full Picture

Once I correlate state, wchan, thread information, file descriptors, I/O counters, and signals, the problem usually falls into one of these categories:

  • Normal idle - the process is in S, waiting for a legitimate event.

  • I/O blocked - the process is in D, waiting on slow or hung storage.

  • Lock contention - a thread is waiting on a futex or lock held by another blocked thread.

  • Scheduler starvation - the process is runnable but receives too little CPU time.

  • Zombie - the process has exited and its parent has not reaped it.

A single state code is never a complete diagnosis. Even D does not tell me whether the wait is transient, whether hardware or configuration is at fault, or whether the application is fundamentally broken. That is why I do not stop at ps.

Join 1,000+ engineers becoming better DevOps & SRE professionals.

I’m Yoshik, a Site Reliability Engineer currently working at a US-based startup.

Before this, I worked on infrastructure at PhonePe, where the systems handled massive scale on-prem infra, and at KayHosting, where I learned what it actually takes to keep systems running when resources, time, and people are limited.

  • How I'd approach problems differently (real projects, real mistakes)

  • Career moves that actually work (not LinkedIn motivational posts)

  • Technical deep-dives that change how you think about infrastructure

No fluff. No roadmaps. Just what works when you're building real systems.

👋 Find me on Twitter | Linkedin | Connect 1:1

Thank you for supporting this newsletter.
Y’all are the best.

Keep Reading