On a POSIX filesystem, rm does not delete a file. It calls unlink(), which removes a name from a directory and decrements the inode’s link count. The inode — and every block it owns — is freed only when two counters reach zero:

  1. the link count (how many names point at it), and
  2. the open descriptor count (how many processes currently hold it open).

A file with no name and one open descriptor is a perfectly normal, fully functional file. The process holding it can keep reading and writing it forever. Nothing can open it again, because there is no longer a name to open — but it still occupies its blocks.

This is the entire explanation for df and du disagreeing:

askssees unlinked-but-open files
duwalks directory entries — namesno
dfasks the filesystem for free blocksyes

So du reporting 2 GB while df reports 190 GB used is not a bug in either tool. They are answering different questions, and the gap between them is the size of what has been deleted but not closed — the shape behind a disk that fills up with nothing on it.

lsof +L1                       # every open file with a link count below 1
ls -l /proc/<pid>/fd | grep deleted

Two ways out, and they are not equivalent: closing or reopening the descriptor (a signal, a reload, a restart) frees the blocks properly, while truncating through the descriptor — : > /proc/<pid>/fd/N — reclaims the space instantly without touching the process, at the cost of discarding whatever it contained. The first is the fix; the second is what you do at 3am when the volume is at 100% and the process must not be restarted.

The same mechanic is why a temporary file created and immediately unlinked is the standard way to get private scratch space that cannot leak: it disappears the moment the process exits, however it exits.

du against df is one instance of a reading error worth recognising in general: two measurements that are each correct, taken by tools answering different questions, where the gap between them is the whole diagnosis. Java heap usage against process RSS is the same comparison one layer up, and it goes unmade for the same reason — the first number is the one on the dashboard, and it looks fine.