SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

df đầy nhưng du không thấy: tìm file bị xóa

VPS báo đầy dù du không thấy dung lượng? Tìm file đã xóa nhưng process vẫn mở bằng lsof, xử lý không cần reboot và kiểm tra inode, mount point, block reserve.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 15, 2026.

Vì sao df báo đầy nhưng du lại cho kết quả khác

df báo disk đã đầy, còn du không tìm thấy phần dung lượng đó vì một process vẫn đang giữ một file đã bị xóa. Xóa file chỉ xóa tên file khỏi directory. Các data block chỉ được giải phóng khi file descriptor đang mở cuối cùng trỏ đến inode đó được đóng. du duyệt các tên file nên không đếm được file này. df hỏi filesystem có bao nhiêu block đã được cấp phát nên vẫn tính cả file không còn tên.

Guide này mô phỏng tình huống đó trên một Ubuntu VPS thông thường bằng các tool đã được cài sẵn, tìm process đang giữ file thông qua /proc và giải phóng dung lượng mà không cần reboot. Các nguyên nhân khác gây ra cùng triệu chứng cũng được trình bày ở phần sau: inode table không còn entry trống, file bị che dưới mount point và các block được reserve cho root.

Chạy từng command và đọc output trên máy của bạn. Các giá trị phụ thuộc vào disk của bạn. Vì vậy, hãy so sánh kết quả trước và sau trên chính máy của bạn thay vì so với một con số được in trong guide.

df tính gì và du tính gì

df (disk free) hỏi từng filesystem đã mount để lấy số liệu riêng: có bao nhiêu block, bao nhiêu block đã được cấp phát và bao nhiêu block còn trống. Lệnh này không bao giờ mở directory. Kết quả bao gồm mọi block đã được cấp phát, kể cả block thuộc về file không còn directory entry nào trỏ tới.

du (disk usage) làm ngược lại. Lệnh bắt đầu từ path bạn cung cấp, đọc các directory, lấy thông tin của mọi entry tìm thấy rồi cộng tổng số block. File không có tên sẽ không được tính. Directory mà lệnh không có quyền đọc cũng vậy. Vì thế, user thông thường thường nhận được tổng nhỏ hơn root. Hãy chạy du dưới quyền sudo trước khi rút ra kết luận từ việc so sánh.

Mỗi lần so sánh hai lệnh này, có 2 option cần chú ý.

  • -x giữ du trong một filesystem. Nếu không dùng option này, du / sẽ đi vào mọi filesystem được mount bên dưới / và tạo ra một tổng mà df / vốn không đo.
  • -s in một dòng tổng hợp cho mỗi argument thay vì in một dòng cho mỗi directory.

Như vậy, đây là cặp lệnh cần chạy song song trên filesystem bạn muốn kiểm tra.

df -h /
sudo du -xhs / 2>/dev/null

df trả kết quả ngay lập tức. du có thể mất vài phút trên filesystem lớn vì phải lấy thông tin của từng file trên đường đi. Khi hai tổng chênh lệch nhiều, và du đã chạy dưới quyền root cùng với -x, phần dung lượng bị thiếu là các block đã được cấp phát nhưng không còn tên.

Cố ý tái tạo trạng thái không khớp

Thực hiện trên một VPS test. Toàn bộ nội dung bên dưới dùng bash và coreutils, nên không cần cài thêm gì.

Ghi lại trạng thái ban đầu của filesystem chứa /var/tmp.

cd /var/tmp
df -h .
df --output=used -B1 .

Lệnh thứ hai in ra số byte đã dùng mà không làm tròn, nên phép kiểm tra ở cuối sẽ chính xác.

Bây giờ tạo một file. Kích thước file lấy theo dung lượng trống mà chính máy báo cáo, nên phần minh họa này phù hợp với mọi dung lượng disk.

free=$(df --output=avail -B1 . | tail -n 1)
fallocate -l $((free / 10)) ghost.bin
ls -l ghost.bin
df -h .

$(...) là command substitution: shell chạy command bên trong, rồi dùng output làm giá trị của free. Nếu cú pháp này còn mới với bạn, command substitution trong bash giải thích đầy đủ. fallocate cấp phát các block thực mà không ghi dữ liệu, nên lệnh hoàn tất ngay lập tức. Trên filesystem không hỗ trợ tính năng này, command sẽ fail; head -c $((free / 10)) /dev/zero > ghost.bin thực hiện cùng nhiệm vụ bằng cách ghi các byte ra disk.

So sánh df -h . này với kết quả bạn đã ghi lại. Cột used đã tăng và cột available đã giảm.

Bây giờ giữ file mở từ một process khác, rồi xóa file đó.

sleep infinity < ghost.bin &
holder=$!
rm ghost.bin
ls -l ghost.bin
df -h .
sudo du -xhs . 2>/dev/null

Phần redirection là toàn bộ mấu chốt. sleep infinity < ghost.bin & khởi chạy một background process với standard input trỏ đến file đó, nên shell mở file và chuyển file descriptor cho sleep, process này giữ descriptor mở. $! lưu process ID của background job đó. rm sau đó xóa tên file trong khi descriptor vẫn đang mở.

Đọc output. ls không tìm thấy file vì tên file đã bị xóa. du gần trở lại giá trị ban đầu vì nó duyệt theo các tên file. df không thay đổi vì các block vẫn đang được cấp phát. Filesystem và directory tree lúc này không còn khớp nhau; phần chênh lệch giữa chúng chính là file bạn vừa xóa.

Tìm tiến trình đang giữ file đã bị xóa

Mọi file descriptor đang mở đều xuất hiện dưới /proc/<pid>/fd/ dưới dạng symbolic link trỏ đến file mà nó tham chiếu. Khi file đã bị unlink, kernel đánh dấu đích của link đó là đã bị xóa. Vì vậy, để tìm tiến trình đang giữ file, hãy tìm link có đích chứa marker này.

sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

-lname khớp với đích của symbolic link thay vì tên của link, %p in đường dẫn của descriptor, còn %l in nội dung mà descriptor trỏ đến. PID là phần tử thứ hai trong đường dẫn được in ra. Hãy chạy lệnh với sudo, vì nếu không bạn chỉ có thể đọc /proc/<pid>/fd của các tiến trình do chính bạn chạy. Redirect stderr sẽ loại bỏ các thông báo nhiễu từ những tiến trình đã thoát trong khi find đang duyệt.

Một server bận có thể giữ nhiều file đã bị xóa cùng lúc, nhưng phần lớn chúng nhỏ và không đáng lo. Hãy sắp xếp theo kích thước để chỉ các file đáng chú ý nằm ở đầu danh sách.

sudo bash -c 'for fd in /proc/[0-9]*/fd/*; do
  target=$(readlink "$fd" 2>/dev/null) || continue
  case "$target" in
    *"(deleted)") echo "$(stat -Lc %s "$fd" 2>/dev/null) $fd $target" ;;
  esac
done' | sort -rn | head

stat -L lần theo link đến chính inode, vì vậy %s báo kích thước của file không còn tên. Sắp xếp theo số đó sẽ đưa file lớn nhất lên đầu.

Tiếp theo, xác định process đứng sau descriptor đó. Đường dẫn ở đầu danh sách chứa cả 2 số cần dùng, vì vậy hãy gán chúng vào biến trước. Thay PID và N bằng các giá trị mà listing của bạn in ra.

pid=PID
n=N
ps -o pid,user,etime,args -p "$pid"
sudo stat -L "/proc/$pid/fd/$n"

ps cho biết tên chương trình và thời gian chương trình đã chạy. stat -L in kích thước và số block đã cấp phát của inode đã bị xóa. Hai thông tin này trả lời câu hỏi quan trọng: service nào đang giữ file này tồn tại.

Nếu máy đã có lsof, sudo lsof +L1 sẽ liệt kê các file đang mở có link count giảm về 0 và hiển thị kích thước trong cùng một bảng. Gói này không có sẵn trên image Ubuntu tối giản. Việc cài package trên filesystem không còn dung lượng trống cũng có thể tự fail, vì vậy cách duyệt bằng /proc là phiên bản luôn hoạt động.

Giải phóng dung lượng mà không reboot

Reboot đúng là sẽ xử lý được, nhưng đó là lựa chọn đầu tiên không phù hợp: nó làm service ngừng chạy và xóa mất bằng chứng. Có 4 cách nhẹ nhàng hơn, hãy thử theo thứ tự dưới đây.

Trước tiên, hãy sao chép dữ liệu ra nơi khác nếu bạn vẫn cần giữ nó. Đọc đường dẫn descriptor sẽ đọc inode đang hoạt động.

sudo cp /proc/<pid>/fd/<n> /root/recovered.log

Đây là trường hợp duy nhất mà file đã bị xóa vẫn dễ khôi phục. Vì vậy, khôi phục file bị xóa bằng rm -rf bắt đầu bằng việc kiểm tra xem process có còn giữ file mở hay không. Khi descriptor cuối cùng đóng lại, cách này không còn dùng được.

Thứ hai, làm rỗng file thông qua descriptor. Đường dẫn /proc trỏ đến cùng inode, nên truncate file sẽ giải phóng các block trong khi process vẫn tiếp tục chạy.

sudo truncate -s 0 "/proc/$pid/fd/$n"
df -h /

Cách này hoạt động đúng khi writer mở file ở chế độ append, vì mỗi lần ghi sẽ đi đến cuối file hiện tại. Nếu không, process vẫn giữ write offset cũ. Vì vậy, lần ghi tiếp theo sẽ nằm rất xa trong file và tạo lại file với một hole ở phần đầu. Hole không được cấp phát block, nên các block vẫn còn trống và df vẫn giữ nguyên dung lượng vừa được giải phóng. Chỉ kích thước file quay lại: chạy lại sudo stat -L "/proc/$pid/fd/$n" sau khi process ghi dữ liệu, bạn sẽ thấy kích thước cũ nằm cạnh số block không còn khớp với kích thước đó. Hãy restart process nếu muốn kích thước cũng bắt đầu lại từ 0.

Thứ ba, yêu cầu service mở lại các log. Một daemon có file log bị xóa trong khi vẫn đang mở là tình huống thực tế phổ biến của vấn đề này. Nhiều daemon mở lại file log khi nhận signal: nginx dùng SIGUSR1, còn rsyslog dùng SIGHUP. Hãy xem tài liệu của daemon đang chạy thay vì đoán, vì gửi sai signal đến sai daemon có thể làm daemon đó dừng.

sudo systemctl kill -s USR1 nginx

Lệnh này gửi signal đến process mà systemd ghi nhận là process chính của unit. Vì vậy, nếu unit khai báo Type= không đúng với cách daemon thực sự khởi động, signal có thể được gửi đến process chưa từng giữ file bị xóa, và dung lượng vẫn không được giải phóng.

Thứ tư, restart unit. sudo systemctl restart <unit> đóng mọi descriptor mà process cũ đang giữ, nên chắc chắn các block sẽ được trả lại. Trong ví dụ trên, process giữ file là một sleep do bạn tự khởi chạy, nên chỉ cần kết thúc process đó.

kill $holder
df -h .
df --output=used -B1 .
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

So sánh số byte đã sử dụng với giá trị bạn đã ghi lại trước khi tạo file. Hai giá trị lại khớp nhau, và find không còn hiển thị descriptor của bạn. Xác minh bằng chính lệnh đã phát hiện vấn đề là một thói quen nên duy trì.

Theo dõi giá trị đó thay đổi sẽ dễ hơn việc chạy df thủ công nhiều lần. watch lặp lại một lệnh theo khoảng thời gian cố định và in lại output tại chỗ, nên watch df -h / hiển thị cột dung lượng đã sử dụng thay đổi khi dung lượng được trả lại.

Khi các tổng khớp nhau nhưng disk vẫn đầy

Nếu df và du -x của root khớp nhau, thì không có file đã xóa nào liên quan. Các nguyên nhân còn lại thuộc nhóm khác và mỗi nguyên nhân có một cách kiểm tra riêng.

Hết inode, không phải hết block

Một inode lưu metadata của một file. ext4 tạo số lượng inode cố định khi tạo filesystem, vì vậy filesystem có thể hết inode dù vẫn còn block trống. Khi đó, việc tạo file mới sẽ thất bại dù df -h vẫn cho thấy còn dung lượng.

df -h /
df -i /

Lệnh đầu đếm block, còn lệnh thứ hai đếm inode. So sánh cột use của mỗi lệnh. Nếu mức sử dụng block còn thấp nhưng mức sử dụng inode đã chạm giới hạn, nguyên nhân là có quá nhiều file rất nhỏ.

df loại trừ -i và --output trong cùng một lần gọi, vì vậy khi cần đọc các số liệu thô hoặc truyền chúng cho lệnh khác, hãy chọn các trường inode theo tên và không dùng -i.

df --output=itotal,iused,iavail,ipcent /

Các cột đó chứa cùng số liệu accounting mà df -i in ra, nhưng ở dạng có thể tách và phân tích.

Hãy tìm file bằng cách đếm số entry thay vì số byte.

sudo du --inodes -x -d 1 / 2>/dev/null | sort -rn | head

Lặp lại cùng lệnh với độ sâu thấp hơn một cấp trên thư mục đứng đầu, cho đến khi tìm được cây thư mục tạo ra các file. Nếu du không hỗ trợ --inodes, thì sudo find /var -xdev -type f | wc -l sẽ đếm subtree theo cách chậm hơn.

Cách xử lý là xóa hoặc di chuyển các file đó. Bạn không thể thêm inode vào một filesystem ext4 hiện có, vì số lượng inode được cố định tại thời điểm mkfs. Muốn tăng số lượng này, bạn phải tạo lại filesystem rồi khôi phục từ backup. XFS cấp phát inode theo nhu cầu, nên không gặp giới hạn cố định theo cùng cách đó. Máy chạy container thường chạm cả hai giới hạn sớm hơn, vì các image layer chứa nhiều file nhỏ. Trên máy đó, dọn dung lượng Docker trên VPS là cách xử lý cụ thể và thu hồi được nhiều dung lượng hơn đáng kể so với việc quét dọn filesystem nói chung.

Dung lượng bị ẩn dưới mount point

Một directory có thể chứa file trước khi có bất kỳ filesystem nào được mount lên đó. Mount một filesystem lên directory này không làm các file bên dưới thay đổi vị trí: chúng vẫn chiếm dung lượng, vẫn được tính bởi df, nhưng không còn truy cập được bằng tên. du không thể nhìn thấy chúng vì mount đã che khuất chúng.

Hãy minh họa bằng tmpfs, không cần disk trống. Phần này cần một máy mà bạn được phép mount, nên có thể thực hiện trên KVM VPS.

sudo mkdir -p /srv/covered
sudo cp /etc/services /srv/covered/
ls /srv/covered
sudo mount -t tmpfs tmpfs /srv/covered
ls /srv/covered
sudo umount /srv/covered
ls /srv/covered

Phần ls ở giữa hiển thị một directory rỗng. Bản sao không đi đâu cả: nó vẫn nằm trên root filesystem và sẽ xuất hiện lại ngay khi bạn unmount. Bây giờ hãy hình dung một service đã ghi log vào path đó trong một tháng trước khi ai đó mount một volume lên đó.

Để tìm dữ liệu thật trên server đang chạy, hãy mount root filesystem lần thứ hai vào một vị trí khác. Bind mount hiển thị một filesystem mà không hiển thị các filesystem đã được mount bên trong nó.

sudo mkdir -p /mnt/rootcheck
sudo mount --bind / /mnt/rootcheck
sudo du -xhs /mnt/rootcheck/* 2>/dev/null | sort -h
sudo umount /mnt/rootcheck

Mọi thứ xuất hiện trong listing đó nhưng không xuất hiện dưới path thông thường đều đang bị chôn bên dưới một mount point. Hãy unmount bind mount khi hoàn tất, nếu không một du tiếp theo không có -x sẽ tính cùng các file hai lần.

Các block dành riêng cho root

ext4 dành riêng một phần block cho user root, để khi filesystem đầy, root vẫn có thể đăng nhập và sửa máy. Process chạy dưới user thông thường sẽ chạm giới hạn này trước, trong khi df vẫn hiển thị còn trống một ít. Hãy đọc cấu hình trên filesystem của bạn thay vì mặc định rằng nó đang dùng giá trị mặc định.

dev=$(df --output=source / | tail -n 1)
sudo tune2fs -l "$dev" | grep -i 'block count'

Lệnh này in tổng số block và số block dành riêng theo cùng một đơn vị, nên có thể tính trực tiếp tỷ lệ giữa hai giá trị. df báo cột available là dung lượng mà user thông thường vẫn có thể sử dụng. Vì vậy, used cộng available sẽ nhỏ hơn size. Phần chênh lệch là dung lượng dự phòng.

Thay đổi giá trị bằng sudo tune2fs -m <percent> "$dev". Thay đổi có hiệu lực ngay và không cần remount. Giảm dung lượng dự phòng trên filesystem dữ liệu riêng là hợp lý. Với root filesystem, hãy giữ lại đủ dung lượng để root vẫn ghi được dữ liệu, vì root filesystem hoàn toàn không còn chỗ trống sẽ khó sửa hơn nhiều. Dung lượng này cũng giúp bạn không bị khóa ngoài hệ thống: nếu filesystem đã hết chỗ, một key được nối thêm vào authorized_keys có thể bị ghi thiếu hoặc không được ghi, và lần đăng nhập tiếp theo sẽ trả về Permission denied (publickey) dù nguyên nhân không liên quan đến bản thân key. tune2fs hoạt động trên ext2, ext3 và ext4. XFS không có cấu hình tương đương.

Trường hợp du cho kết quả sai lệch khi chạy độc lập

Bốn thói quen của du có thể tạo ra các tổng dung lượng trông không đúng.

  • Hard link: du chỉ đếm một lần cho mỗi inode, ngay cả khi có nhiều tên cùng trỏ đến inode đó. Vì vậy, một cây thư mục chứa nhiều hard link có tổng nhỏ hơn tổng kích thước các file.
  • Sparse file: du báo số block thực tế đã được cấp phát, còn ls -l báo kích thước biểu kiến. Thêm --apparent-size để xem giá trị còn lại.
  • Quyền truy cập: khi chạy dưới tài khoản người dùng thông thường, du bỏ qua những mục không thể đọc và báo thiếu dung lượng. Các lỗi mà lệnh này in ra thường bị chuyển hướng vào /dev/null, rồi người dùng không đọc tiếp.
  • Ranh giới filesystem: nếu không có -x, du / sẽ đếm mọi filesystem được mount bên dưới /. Vì vậy, tổng của nó có thể lớn hơn giá trị df / báo cáo.

df cũng có một đặc điểm đáng biết. Lệnh này báo cáo từng filesystem riêng biệt, vì vậy hãy chạy nó trên đúng path mà thao tác ghi bị lỗi nhắm đến. Một /boot riêng có thể đầy theo lịch riêng khi các kernel package tích lũy. Xóa kernel cũ trên Ubuntu là công việc khác với việc giải phóng dung lượng trên /.

Thứ tự xử lý khi có incident thực tế

  1. Chạy df -h <path> và df -i <path> trên filesystem mà thao tác ghi bị lỗi đang nhắm tới, thay vì theo thói quen chạy trên /.
  2. Chạy sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h, sau đó đi sâu vào directory lớn nhất.
  3. Nếu du không giải thích được phần dung lượng mà df báo là đã sử dụng, hãy tìm trong /proc các file đã bị xóa nhưng vẫn đang được mở.
  4. Nếu hai kết quả khớp nhau, hãy bind mount filesystem vào một vị trí khác rồi tìm các file nằm bên dưới một mount point.
  5. Nếu inode use chạm giới hạn, hãy đếm số file thay vì số byte.

Mỗi bước đều có một command cho output mà bạn có thể đọc và kiểm tra. Đó là điểm khác biệt giữa việc xử lý đúng lỗi này và chỉ đoán nguyên nhân.

FAQ

Vì sao df hiển thị ổ đĩa đầy trong khi du tìm thấy ít hơn nhiều?

Nguyên nhân thường gặp là một file đã bị xóa nhưng vẫn đang được một process mở. Xóa file sẽ xóa directory entry của nó, nên du không còn tên để duyệt và dừng đếm file đó. Inode và các block của file vẫn được cấp phát cho đến khi descriptor cuối cùng đóng lại, còn df đếm các block đã cấp phát. Tìm trong /proc/<pid>/fd các symbolic link có target được đánh dấu là đã xóa để tìm cả file lẫn process đang giữ file đó. Trước khi tin vào kết quả so sánh, hãy xác nhận bạn đã chạy du với quyền root và có -x, vì user thông thường sẽ âm thầm bỏ qua các directory mà họ không thể đọc.

Làm thế nào để tìm một file đã xóa nhưng vẫn đang được mở mà không dùng lsof?

Hãy dùng thông tin do kernel lưu về các descriptor đang mở. sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null liệt kê mọi descriptor trỏ đến một file không còn tên, và process ID nằm trong path mà lệnh in ra. sudo stat -Lc %s trên một trong các path descriptor đó sẽ báo kích thước của file, để bạn có thể sắp xếp và chọn file cần xử lý. Cách này không cần cài thêm package, điều này quan trọng vì cài package trên filesystem không còn dung lượng trống có thể thất bại.

Có thể giải phóng dung lượng mà không dừng process không?

Đôi khi có thể. sudo truncate -s 0 /proc/<pid>/fd/<n> truy cập cùng inode thông qua descriptor và giải phóng các block của file trong khi process vẫn tiếp tục chạy. Cách này phù hợp nhất khi process đã mở file ở chế độ append, vì mọi lần ghi đều đi đến cuối file hiện tại. Nếu không, write offset vẫn giữ nguyên vị trí cũ và lần ghi tiếp theo sẽ tạo lại file với một hole ở đầu, khiến kích thước được báo tăng trở lại trong khi các block nằm dưới hole vẫn trống. Restart unit hoặc gửi signal yêu cầu process mở lại log theo signal được nêu trong tài liệu của nó là cách xử lý không để lại sparse file.

df hiển thị dung lượng trống nhưng vẫn ghi thất bại. Còn nguyên nhân nào khác?

Kiểm tra inode bằng df -i trên cùng path, vì filesystem còn free block nhưng không còn free inode sẽ từ chối tạo file mới. Kiểm tra việc ghi có chạy dưới user không phải root trên filesystem ext4 chỉ còn reserved block hay không; sudo tune2fs -l trên device sẽ hiển thị thông tin này. Kiểm tra bạn có đang đọc đúng filesystem mà thao tác ghi thực sự nhắm đến hay không, vì một /boot hoặc /var riêng có thể đầy độc lập với /.

Vì sao du báo tổng dung lượng lớn hơn df?

du nếu không có -x sẽ đi vào mọi filesystem được mount bên dưới path bạn cung cấp, nên nó cộng nhiều filesystem trong khi df chỉ mô tả một filesystem. Bind mount làm tình trạng này nghiêm trọng hơn, vì cùng một file sẽ được đếm một lần dưới mỗi path mà nó xuất hiện. Thêm -x để giữ du trong một filesystem duy nhất, và cung cấp cho df cùng path để cả hai lệnh mô tả cùng một phạm vi.