See What a Process Has Open via /proc/pid/fd
When a service will not shut down or a disk fills for no visible reason, /proc/<pid>/fd is where you find out what it is still holding open. It lists every descriptor a process owns, including pipes and sockets that never show up in a normal file listing.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a shell and a process you are allowed to inspect. The examples use the local proc_pid_fd(5) reference from the manpages package, version 6.7-2.
- The first checks are read-only. Ordinary, unprivileged commands against your own process.
- sudo has limits. It does not make a descriptor readable when the process or its underlying inode denies access.
1. Confirm the reference and your own descriptors
Start with the installed manual page, then list the descriptors belonging to the shell running the command:
$ man 5 proc_pid_fd
$ printf 'shell pid: %s\n' "$$"
shell pid: 24680
$ ls -l /proc/self/fd
total 0
lrwx------ 1 user user 64 Sep 26 12:00 0 -> /dev/pts/3
lrwx------ 1 user user 64 Sep 26 12:00 1 -> /dev/pts/3
lrwx------ 1 user user 64 Sep 26 12:00 2 -> /dev/pts/3
lr-x------ 1 user user 64 Sep 26 12:00 3 -> /proc/24680/fd
The names are descriptor numbers, not ordinary file names. Descriptors 0, 1 and 2 are standard input, standard output and standard error. The final entry is an example: shells and commands can open extra descriptors, and the exact list will vary. /proc/self means the process making the lookup, which is useful inside scripts and avoids hard-coding your current PID.
Checkpoint
ls -l /proc/self/fd should return a list, not a missing-directory error. If it does not, check that you are on Linux and that /proc is mounted; do not create a replacement directory, it would not provide process descriptors.
2. Read the target of a descriptor
Use readlink when you need the link target without the visual formatting ls adds:
$ readlink /proc/self/fd/0
/dev/pts/3
$ readlink /proc/self/fd/1
/dev/pts/3
These results describe the terminal attached to this shell. A redirected command has different targets. Make a short-lived shell with an extra descriptor and inspect it from inside that process:
$ bash -c 'exec 3<<<"descriptor demo"; printf "fd 3: "; readlink /proc/self/fd/3; IFS= read -r line <&3; printf "read: %s\n" "$line"'
fd 3: pipe:[123456789]
read: descriptor demo
The inode number in the pipe target is illustrative and changes on every run. The descriptor is a live pipe endpoint, not a regular file. The shell can read from it through descriptor 3 because it already owns the open descriptor.
3. Inspect a separate process
Start a harmless sleep child so you have something alive long enough to examine, and save its PID:
$ sleep 60 &
[1] 24701
$ pid=$!
$ ls -l "/proc/$pid/fd"
total 0
lrwx------ 1 user user 64 Sep 26 12:01 0 -> /dev/pts/3
lrwx------ 1 user user 64 Sep 26 12:01 1 -> /dev/pts/3
lrwx------ 1 user user 64 Sep 26 12:01 2 -> /dev/pts/3
$ readlink "/proc/$pid/fd/0"
/dev/pts/3
List the directory before reading individual entries, because the process can exit between those two steps. Treat a disappearing /proc/$pid path as a normal race, not proof the descriptor never existed.
Checkpoint
Confirm the process is still the one you started, then clean it up.
$ kill "$pid"
$ wait "$pid" 2>/dev/null || true
[1]+ Terminated sleep 60
Warning
This is the only state-changing example in the guide, and it should only ever touch the temporary child. Never substitute a production PID into kill without checking it first. If you terminate a service by mistake, use that service's normal recovery procedure rather than repeatedly sending signals.
4. Recognise pipes, sockets and anonymous descriptors
Not every target names a path. Pipe and socket links take the form type:[inode]. An entry such as socket:[2248868] identifies a socket inode; the number can be correlated with files under /proc/net when you are diagnosing a network process.
Kernel objects without a normal inode use an anon_inode: target instead: epoll, eventfd, inotify, timerfd and signalfd descriptors all show up this way.
$ readlink "/proc/$TARGET_PID/fd/$FD_NUMBER"
anon_inode:[eventpoll]
Replace both placeholders after checking the directory listing. The descriptor number and object type are host-specific. Do not infer that every anonymous descriptor is an epoll instance, and do not treat a link target as permission to manipulate the object.
5. Feed a stream into a command as a file
A program can often be handed an existing standard stream through /proc/self/fd/N. This helps when a tool insists on a file argument but your data is already on standard input or output. Try it with a command that treats its argument as an input file:
$ printf 'one\ntwo\n' | cat /proc/self/fd/0
one
two
cat opens its own /proc/self/fd/0, which refers to the pipe it inherited. This is roughly the same idea as /dev/fd/0 on systems that provide that name, but it is not universal: a program that seeks may fail on a pipe, and a tool may treat a descriptor path differently from a regular file.
6. Understand the permission boundary
Reading a link under another process's fd directory is subject to the kernel's ptrace access check. Opening the object the link names has a separate boundary: the underlying inode keeps its own owner and mode. Together these explain a common surprise: a process can read from its own already-open standard input while a different, unprivileged process cannot open /proc/<pid>/fd/0 to get a new reference to that same pipe.
When an inspection fails, note which step failed. Cannot read the link points at process inspection permissions. Permission denied while a command opens the resolved path may point at the pipe, socket or file itself. Test against a process you own before reaching for elevated privileges, and get approval before reading a service's descriptors or putting their targets in a log.
7. Account for a multithreaded process
On a multithreaded process, the fd directory can stop being available once the main thread has terminated, commonly after pthread_exit. If a long-running process looks alive but its descriptor directory is unavailable, check its thread and process state before assuming /proc is broken. A supervisor restart changes every PID and descriptor, so capture your evidence before restarting a service.
Done means
- Listed your own descriptors with
/proc/self/fdand can explain entries 0, 1 and 2. - Used readlink to tell paths, pipes, sockets and
anon_inodeobjects apart. - Tested a temporary child and cleaned it up without touching a real service.
- Separated the two permission checks: reading a link versus opening what it points to.
- Checked for seek requirements before handing a
/proc/self/fdpath to another program.