2 of 4

Application DebuggingLesson 2 of 4

Open files, listening ports and resource limits

Find what a process holds open, who owns a port, and which limit or memory event stopped it.

  • lsof
  • lsof -i
  • lsof -p
  • ss -tulpn
  • fuser
  • ls /proc/PID/fd
  • /proc/PID/limits
  • ulimit
  • systemctl show -p LimitNOFILE
  • journalctl -k
  • dmesg
  • oom_score

45 min3 exercises

After this lesson you can

  • List the files, sockets and libraries a process has open, and the process that owns a given port or file.
  • Read a process's limits from /proc and set a limit for a service with a systemd drop-in.
  • Recognise an out-of-memory kill in the kernel log and explain what the kernel chose to kill and why.

The idea

A process can fail because of what is around it rather than what it does. It cannot open its file because another program owns it, cannot bind its port because another process already has it, runs out of descriptors, or is killed because the machine ran out of memory. Each of these is visible in the kernel’s records of the process.

Everything is a file descriptor

A Linux process refers to open files, directories, sockets, pipes and devices by small integers called file descriptors. The kernel exposes them under /proc/PID/fd, and lsof (“list open files”) reads and formats that.

ls -l /proc/PID/fd            # descriptors and what they point to
lsof -p PID                   # same, with type, device, size and name
lsof /var/log/messages        # which processes have this file open?
lsof -nP -iTCP -sTCP:LISTEN   # every TCP listener (numeric)
lsof +D /mnt/data             # who is using this mounted directory? (why umount says "busy")
fuser -vm /mnt/data           # shorter form of the same question
ss -tulpn                     # listening sockets with owning processes
QuestionCommand
Which process listens on port 443?ss -tlnp 'sport = :443' or lsof -nP -iTCP:443 -sTCP:LISTEN
Why can’t I unmount /mnt/data?lsof +f -- /mnt/data or fuser -vm /mnt/data
Which process holds a deleted file open?ls -l /proc/*/fd 2>/dev/null | grep deleted or lsof +L1
What is this process connected to?lsof -nP -a -p PID -i
Which libraries are loaded?lsof -p PID | grep mem or cat /proc/PID/maps

Limits

Every process has soft and hard limits for several resources, readable in /proc/PID/limits:

grep -E 'open files|processes|core file|address space' /proc/PID/limits
ulimit -n        # the soft limit on open files in the current shell

Where they come from depends on how the process started:

Started byLimits come from
An interactive login (ssh, console)pam_limits and /etc/security/limits.conf, limits.d/*.conf
A systemd serviceThe unit: LimitNOFILE=, LimitNPROC=, LimitCORE=… (defaults in /etc/systemd/system.conf)
A cron jobpam_limits for the cron session

This is the cause of a very common surprise: you raise nofile in limits.conf, log in, and ulimit -n shows the new value, but the service still fails because it never went through PAM. The fix is a drop-in for the unit.

Memory and the OOM killer

When the system, or a cgroup such as a systemd service with MemoryMax=, cannot satisfy a memory request, the kernel’s OOM killer selects a process and kills it. The evidence is in the kernel log, not in the application’s own log (it was killed and had no chance to write anything):

journalctl -k -b | grep -iE 'out of memory|oom-kill|Killed process'
systemctl status UNIT          # "Result: oom-kill" for a service in its own cgroup
cat /proc/PID/oom_score        # how attractive this process is as a victim (higher = more likely)

The score favours big processes; OOMScoreAdjust=-900 in a unit, or writing -900 to /proc/PID/oom_score_adj, protects an essential process, and MemoryMax= contains a likely offender before it hurts others.

Try it

Who owns a port

Real output: one process listening, then a second one trying to bind the same address and port.

python3 -c "..." &                       # listener on 127.0.0.1:8099
python3 -c "import socket; s=socket.socket(); s.bind(('127.0.0.1',8099))"
lsof -nP -iTCP:8099 -sTCP:LISTEN
OSError: [Errno 98] Address already in use
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3  377 root    3u  IPv4   2153      0t0  TCP 127.0.0.1:8099 (LISTEN)

The error is EADDRINUSE, and lsof names the owner. On RHEL, sudo ss -tlnp 'sport = :8099' gives the same answer in one line, and it is installed by default (package iproute). If you need to start a new service on that port, either stop the other process or change the port. For a web service on a non-default port, remember SELinux (semanage port -l) and the firewall are separate from “address in use”.

What a process has open

sleep 100 &
ls /proc/$!/fd | wc -l
lsof -p $! | head -5
3
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
sleep   366 root  cwd    DIR  254,0     4096 1204669 /tmp/ad
sleep   366 root  rtd    DIR  254,0     4096       2 /
sleep   366 root  txt    REG  254,0    35336  321702 /usr/bin/sleep
sleep   366 root  mem    REG  254,0  2129424  322137 /usr/lib/x86_64-linux-gnu/libc.so.6

cwd is the working directory, rtd the root, txt the program, mem a memory-mapped library; numbered descriptors (0u, 1u, 3r) follow. The cwd line explains many “device is busy” errors: a shell sitting in a mount point holds it open.

Hitting a limit

ulimit -n 20
python3 -c "
fs=[]
try:
  for i in range(100): fs.append(open('/etc/hostname'))
except OSError as e: print(len(fs),'open:',e)"
17 open: [Errno 24] Too many open files: '/etc/hostname'

With a limit of 20, three descriptors were already in use (standard input, output, error), so 17 more opened and the 18th failed with EMFILE. In a real service the descriptors are sockets: a server with 1,024 clients and a limit of 1,024 fails the same way. Read the limit of the live process:

grep 'open files' /proc/$(pidof -s fdhog)/limits
systemctl show fdhog -p LimitNOFILE

Raise it with a drop-in:

[Service]
LimitNOFILE=1024

Then systemctl daemon-reload && systemctl restart fdhog. Check /proc/PID/limits again: only a process started after the change has the new limit.

The OOM killer’s trail

journalctl -k -b | grep -iE 'out of memory|oom-kill|Killed process'
kernel: Memory cgroup out of memory: Killed process 4412 (python3) total-vm:211540kB, anon-rss:102308kB, file-rss:2820kB, shmem-rss:0kB, UID:0 pgtables:268kB oom_score_adj:0
systemd[1]: memhog.service: A process of this unit has been killed by the OOM killer.
systemd[1]: memhog.service: Failed with result 'oom-kill'.

Memory cgroup out of memory says the group exceeded its own limit (here MemoryMax=100M), not that the machine was out of memory. A global event reads Out of memory: Killed process ... and is preceded by a Mem-Info block describing free memory and swap. The numbers anon-rss tell you how large the process was when it died, which is the starting point for the question “is this a leak or an undersized limit?”.

Going further

Raising limits in the right place. Use LimitNOFILE= for a service, limits.d/*.conf for interactive users. The system-wide ceiling is fs.file-max and the per-process ceiling fs.nr_open (sysctl fs.nr_open); a limit above nr_open is refused. Beyond the exam: MemoryHigh= throttles a cgroup before MemoryMax= kills it, systemd-oomd can act on memory pressure before the kernel does, and earlyoom is a user-space alternative. The monitoring lesson shows how to see memory pressure and swap before an OOM event happens.

Practice

0 of 3 done

Do these on your own system. They get harder. Check each result, then tick it off.

  1. Warm-up

    Who has port 8099?

    Start any listener on port 8099 (python3 -m http.server 8099 --bind 127.0.0.1 &). With ss and lsof, show the PID and program that own the port, the user it runs as, and the other files it has open. Then start a second listener on the same port and read the error.

    Check your result
    • sudo ss -tlnp 'sport = :8099' shows users:(("python3",pid=...,fd=3)).
    • sudo lsof -nP -iTCP:8099 -sTCP:LISTEN shows the same PID.
    • The second start fails with OSError: [Errno 98] Address already in use.
    Hint

    ss -tlnp lists TCP listeners with process names (root is needed to see other users' processes). lsof -i :PORT filters by port. -n -P avoid name lookups and show numbers.

    Solution
    python3 -m http.server 8099 --bind 127.0.0.1 &
    sudo ss -tlnp 'sport = :8099'
    sudo lsof -nP -iTCP:8099 -sTCP:LISTEN
    sudo lsof -p $! | head
    python3 -m http.server 8099 --bind 127.0.0.1
    kill %1
  2. Practice

    Too many open files

    A service fdhog.service opens many files and dies with Too many open files. Setup:

    sudo tee /etc/systemd/system/fdhog.service <<'EOT'
    [Service]
    LimitNOFILE=64
    ExecStart=/usr/bin/python3 -c "import time; fs=[open('/etc/hostname') for _ in range(200)]; time.sleep(3600)"
    EOT
    sudo systemctl daemon-reload && sudo systemctl start fdhog

    Show the failure in the journal, the limit the process had (/proc/PID/limits or systemctl show), raise the limit with a drop-in (not by editing the original file) to 1024, and prove the service now stays up.

    Check your result
    • journalctl -u fdhog contains OSError: [Errno 24] Too many open files.
    • systemctl show fdhog -p LimitNOFILE prints LimitNOFILE=1024 after the change.
    • systemctl is-active fdhog prints active.
    Hint

    systemctl edit fdhog creates /etc/systemd/system/fdhog.service.d/override.conf. Put [Service] and LimitNOFILE=1024 there. Limits in /etc/security/limits.conf apply to PAM login sessions, not to services.

    Solution
    journalctl -u fdhog --no-pager | tail -5
    sudo systemctl edit fdhog      # add:  [Service]  LimitNOFILE=1024
    # or without an editor:
    sudo mkdir -p /etc/systemd/system/fdhog.service.d
    printf '[Service]\nLimitNOFILE=1024\n' | sudo tee /etc/systemd/system/fdhog.service.d/override.conf
    sudo systemctl daemon-reload && sudo systemctl restart fdhog
    systemctl show fdhog -p LimitNOFILE; systemctl is-active fdhog
    grep 'open files' /proc/$(systemctl show fdhog -p MainPID --value)/limits
  3. Challenge

    Who killed my process?

    Setup (lab VM with swap off or small memory, in a cgroup so the host survives):

    sudo systemd-run --unit=memhog --property=MemoryMax=100M --property=MemorySwapMax=0 \
      python3 -c "x=[bytearray(1024*1024) for _ in range(500)]; import time; time.sleep(60)"

    The unit ends with oom-kill. From the journal and the kernel log alone, prove that the kernel (not the program) ended it, state how much memory the cgroup was allowed, and name the process chosen. Then explain the difference between this cgroup OOM kill and a system-wide OOM kill, and give two ways to stop a different important service from being the victim.

    Check your result
    • systemctl status memhog shows Result: oom-kill.
    • journalctl -k -b | grep -i 'out of memory\|oom' shows Memory cgroup out of memory: Killed process ... (python3).
    • Your answer mentions OOMScoreAdjust= in the unit (or oom_score_adj), and MemoryMax=/MemoryHigh= limits.
    Hint

    A cgroup OOM kill happens when a group exceeds its own limit, regardless of free system memory. A global OOM kill happens when the whole system is out, and the kernel picks the process with the highest oom_score. Protect a service with OOMScoreAdjust=-900 or limit the likely offender.

    Solution
    systemctl status memhog --no-pager | sed -n 1,10p
    journalctl -k -b --no-pager | grep -iE 'out of memory|oom-kill|Killed process' | tail
    systemctl show memhog -p MemoryMax,Result
    cat /proc/$(pidof -s sshd)/oom_score
    # protect sshd and constrain a hog
    sudo systemctl edit sshd        # [Service]  OOMScoreAdjust=-900
    sudo systemctl set-property --runtime memhog.service MemoryMax=200M