skip to content

A long-running daemon on a Linux host cannot be restarted, and you need to know which config file it opened, what open-file limit it is actually running under, and whether it is leaking descriptors. How does /proc/<pid> answer all three?

level: seniorimportance: should knowfreq 52%

answer

  1. the kernel's live view, not the app's
  2. one symlink per open descriptor
  3. limits are per-process and inherited
  4. count it twice before calling it a leak
  5. (deleted) in a symlink target

basics

~20 s

The kernel publishes each process's live state under /proc/<pid>: fd/ holds a symlink per open descriptor, limits shows that process's own soft and hard rlimits, and counting fd/ entries against the limit reveals a descriptor leak. All three are read without touching the process.

solid answer

~50 s

Every running process has a directory /proc/<pid> that the kernel fills with its live state. `ls -l /proc/<pid>/fd` shows one symlink per open descriptor pointing at the actual file, socket or pipe — that is how you find which config file it opened and where. `/proc/<pid>/limits` prints the soft and hard resource limits of *that* process, which matters because the `ulimit` you see in your own shell describes your shell, not a daemon started years ago under a different environment. Counting the entries in fd/ and comparing against the open-files limit, repeatedly over time, tells you whether the count is climbing — a leak — or merely high. Supporting entries fill in the picture: cmdline for the exact arguments it was exec'd with, cwd and exe as symlinks to its working directory and binary, and environ for the environment it inherited.

code

bash · 5 lines
bash
PID=$(pgrep -n sshd)
tr '\0' '\n' < /proc/$PID/cmdline
sudo ls -l /proc/$PID/cwd /proc/$PID/exe
sudo ls /proc/$PID/fd | wc -l
grep 'Max open files' /proc/$PID/limits

go deeper

for a junior

Know that /proc/<pid> exists per process and that fd/ lists its open files as symlinks, and be able to find a running process's working directory and command line there.

for a middle

Explain that limits are per-process and inherited at creation, that fd/ entries reveal sockets and pipes as well as files, and that the deleted suffix means the inode is still held open.

for a senior

Show investigative judgment: sample the descriptor count over time rather than once, group entries by target to name the leak, and use /proc/<pid>/limits to settle whether a limits change ever reached the running service.

for a principal

Push past the incident — decide what the platform should expose by default so this is answered from telemetry rather than by shelling into a box, and set the policy for service-level limits and hidepid-style visibility restrictions.

## The kernel's own view of a process /proc/<pid> is not a log, a cache or a report written by the process — it is the kernel's live accounting, formatted on read. That is what makes it authoritative during an incident: it cannot be stale, it cannot be lying about what the process intended to do, and it does not require the process to cooperate. Tools such as `lsof` are readers of these same files. Use `/proc/self` when you want the directory of the process doing the reading, and note that a numeric directory vanishes the instant the process exits. ## Which file did it open `/proc/<pid>/fd/` contains one entry per open file descriptor, named by its number, and each is a symlink to the object behind it: ``` $ sudo ls -l /proc/4711/fd lrwx------ 1 svc svc 64 Aug 20 10:02 0 -> /dev/null l-wx------ 1 svc svc 64 Aug 20 10:02 2 -> /var/log/app/stderr.log lr-x------ 1 svc svc 64 Aug 20 10:02 7 -> /etc/app/app.yaml lrwx------ 1 svc svc 64 Aug 20 10:02 9 -> 'socket:[184223]' ``` Three things to read out of this. First, the target is the path the kernel resolves *now*, so if the config was replaced under the running process the symlink still points at the inode it holds — and if the file was deleted, the target is suffixed with ` (deleted)`, which is how you discover a service still holding a removed log file. Second, non-file descriptors show as `socket:[inode]`, `pipe:[inode]` or `anon_inode:...`, so you can tell how much of the count is sockets. Third, `/proc/<pid>/fdinfo/<n>` gives the current file offset and flags for each descriptor. If you only need the working directory or the binary, `cwd` and `exe` are symlinks to them; `exe` still resolves even if the binary was replaced or unlinked by a package upgrade, which is how you catch a service running a version that no longer exists on disk. ## What limit is it actually under `/proc/<pid>/limits` is the entry people forget, and it settles a very common argument: ``` $ cat /proc/4711/limits Limit Soft Limit Hard Limit Units Max open files 1024 524288 files Max processes 31234 31234 processes ``` Resource limits are per-process and inherited from whatever started the process. A daemon launched by the service manager at boot has the limits its unit configuration gave it, not the limits your interactive login session has, and not whatever you edited in a limits configuration file afterwards — those apply to new sessions. So `ulimit -n` in your shell is evidence about your shell only, and a service that still hits "too many open files" after a limits change is usually a process that was never restarted to pick it up. Reading its /proc entry ends the debate in one command. ## Is it leaking The count of entries in `fd/` is the process's current open-descriptor total. A single sample tells you how close it is to the soft limit; a series of samples tells you whether it is leaking: ``` $ for i in 1 2 3; do ls /proc/4711/fd | wc -l; sleep 60; done ``` A number that climbs monotonically under steady load, and never falls after work completes, is the signature of descriptors that are opened and never closed. Grouping by target — how many are sockets, how many are the same file opened repeatedly — usually names the culprit immediately: repeated opens of one config or certificate file point at a reload path that never closes, and a growing pile of sockets points at connections that are never closed or at a client pool without an upper bound. ## The rest of the directory, and its limits `cmdline` holds the exact argv the process was exec'd with, NUL-separated, so `tr '\0' '\n' < /proc/4711/cmdline` renders it readably; this is more trustworthy than a wrapper script or a unit file, because it is what actually ran. `environ` holds the environment as of exec — it is a snapshot, not live, so variables changed inside the process are not reflected. `status` gives the human-readable summary including UIDs, GIDs, thread count and memory figures; `task/` holds one subdirectory per thread. Two access caveats. Reading another user's `environ`, `fd/` or `cwd` requires being root or the same user, because those entries can contain secrets. And the proc filesystem supports a `hidepid` mount option that restricts which processes' directories a user can see at all; on a hardened host you may need to be root even to list a process directory that would normally be world-visible.

  • A descriptor in /proc/<pid>/fd points at a path ending in "(deleted)". What does that tell you?
    The process still holds an open descriptor on a file whose last directory entry was removed. The inode and its data blocks survive until the final descriptor closes, so the space is still consumed even though the path is gone — the classic reason `df` shows a full disk that `du` cannot account for. Truncating through the descriptor, or restarting the holder, releases it.
  • You raised the open-file limit in the system's limits configuration, but the daemon still reports the old value in /proc/<pid>/limits. Why?
    Resource limits are inherited at process creation and cannot be lowered or raised retroactively from the outside by editing a config file. The running process still carries the limits it was given when it was started, so it must be restarted by whatever supervises it — and if a service manager starts it, the limit has to be set in that service's own configuration rather than in the login-session limits file.
  • Why is /proc/<pid>/environ sometimes misleading during an investigation?
    It is the environment captured when the process was exec'd, not a live view. Anything the program changed internally with setenv, or any secret it read and then cleared, will not be reflected. It is evidence about how the process was launched — useful for spotting a missing or wrong variable at startup — not about its current configuration state.

saying these in an interview costs you the question

  • Quotes ulimit -n from their own shell as the daemon's limit
  • Thinks a limits config change applies to already-running processes
  • Reads a single fd count as proof of a leak
  • Assumes /proc/<pid>/environ updates as the process runs
  • Believes a deleted open file has already freed its disk space

context