File Descriptors

Focused on sockets

In my day to day I mostly deal with file descriptors that point to sockets. As a result, the examples below will be biased towards this use case.

Everything in linux is a file, and file descriptors are like pointers to a file. Therefore, file descriptors can point to all kinds of things.

They’re pretty opaque pointers though - they’re just integers. To even begin to make sense of them you need the FD table.

FD Mapping

Generally the relationship between FDs and the things they describe goes like this:

File Descriptor Tables

At a very high level, this is how FDs are mapped to the resources they describe:

  1. The FD table is per-process (meaning integers in one process probably don’t point to the same thing in another). Each integer value maps to a pointer (memory address) to a file struct
  2. That file struct points eventually to an inode struct
  3. The inode describes the contents and metadata of the file

As it pertains to sockets, things get a little interesting. /proc is where a bunch of pseudo-filesystems are located for getting information from the kernel. It is one of the places where the “everything is a file” becomes really apparent.

Two subdirectories are of particular interest as it pertains to file descriptors and how they apply to networking:

  • /proc/<pid>/fd is te file descriptor table for a given process. Maps FD to inode numbers
  • /proc/net 1 2 is part of a pseudo-filesystem containing all kinds of interesting details from the kernel.

Inspect a process’ open FDs. You’ll notice that this output is very helpful for identifying sockets.

 1mierdin@t-bug:~ $ ls -lha /proc/$(pgrep -f "python3 sock_stream_server.py")/fd
 2total 0
 3dr-x------ 2 mierdin mierdin  5 Aug  4 09:52 .
 4dr-xr-xr-x 9 mierdin mierdin  0 Aug  4 09:09 ..
 5lrwx------ 1 mierdin mierdin 64 Aug  4 09:52 0 -> /dev/pts/7
 6lrwx------ 1 mierdin mierdin 64 Aug  4 09:52 1 -> /dev/pts/7
 7lrwx------ 1 mierdin mierdin 64 Aug  4 09:52 2 -> /dev/pts/7
 8lrwx------ 1 mierdin mierdin 64 Aug  4 09:52 3 -> 'socket:[231891]'
 9lrwx------ 1 mierdin mierdin 64 Aug  4 09:52 4 -> 'socket:[231892]'
10
11mierdin@t-bug:~ $ stat /proc/$(pgrep -f "python3 sock_stream_server.py")/fd/3
12  File: /proc/100464/fd/3 -> socket:[231891]
13  Size: 64              Blocks: 0          IO Block: 1024   symbolic link
14Device: 0,19    Inode: 284542      Links: 1
15Access: (0700/lrwx------)  Uid: ( 1000/ mierdin)   Gid: ( 1000/ mierdin)
16Access: 2025-08-04 09:53:05.421363362 -0400
17Modify: 2025-08-04 09:52:34.793398433 -0400
18Change: 2025-08-04 09:52:34.793398433 -0400
19 Birth: -

The inode in the previous example (231891) can be used to look up the socket in /proc/net/tcp:

1mierdin@t-bug:~ $ cat /proc/net/tcp | sed -n '1p; /231891/p'
2  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
3   6: 0100007F:1FBB 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 231891 1 00000000d37434e9 100 0 0 10 0

Focused on sockets

The above examples are just emphasizing the file-like nature of these FDs even though they point to sockets.

A far better way of inspecting file descriptors is with lsof

Syscalls

Some syscalls create new FDs:

Others operate on an already-opened FD and therefore require it as a parameter:

Resources