Debugging Linux: Menelusuri Proses dengan strace, lsof, dan /proc
Saat program tidak merespons, ps hanya memberi satu potret. Artikel ini melangkah lebih jauh: membaca syscall yang benar-benar dipanggil, melihat berkas mana yang dipegang, lalu mengaitkannya dengan isi /proc. Semua output berasal dari uji nyata di Ubuntu 26.04.1 dengan kernel 7.0, strace 6.19, dan lsof 4.99.4, termasuk kasus yang gagal.
Ringkasan
stracemenunjukkan syscall yang dipanggil program, termasuk yang tidak pernah selesai.lsofmemetakan deskriptor ke berkas, direktori, pipe, socket, atau device./proc/<pid>/fdmemberi symlink yang bisa dibaca denganreadlink.-o,-e,-f,-c,-yy,-s, dan--syscall-limitmenjaga trace tetap terbaca.strace -ppada Ubuntu sering ditolak Yamaptrace_scope=1; jalankan program sebagai wrapper agar pasti berhasil.lsof +L1menemukan berkas yang sudah dihapus tapi masih dipegang, penyebab umum disk penuh yang membingungkan.wchansering tetap terbaca meski/proc/<pid>/syscalldanstackditolak.
Lingkungan Pengujian
$ uname -srmo
Linux 7.0.0-31-generic x86_64 GNU/Linux
$ . /etc/os-release && echo "$PRETTY_NAME"
Ubuntu 26.04.1 LTS
$ strace --version
strace -- version 6.19
$ lsof -v 2>&1 | grep -i revision
revision: 4.99.4
$ id -u
1000
$ cat /proc/sys/kernel/yama/ptrace_scope
1Semua dijalankan sebagai user biasa tanpa sudo. Bagian yang membutuhkan root ditandai eksplisit karena tidak bisa diverifikasi di mesin ini.
Trace Dasar dan Pemisahan Output Program
Tanpa -o, trace ditulis ke stderr sedangkan stdout program tetap utuh. strace(1) menyebut opsi ini membuat keluaran diagnostik dicampur dengan stderr program. Mengarahkan 2>/dev/null hanya membuang trace-nya, sedangkan stdout program tetap tampil.
$ strace /bin/true
execve("/bin/true", ["/bin/true"], 0x7fffca686978 /* 56 vars */) = 0
+++ exited with 0 +++Untuk analisis serius, pakai -o supaya trace terpisah rapi dan output program tidak tercemar. strace(1) menjelaskan bahwa nilai balik ditulis setelah tanda =, dan +++ exited with N +++ menandai kode keluar program.
$ strace -e trace=openat -o basic.log /bin/ls .
$ wc -l < basic.log
36
$ tail -2 basic.log
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
+++ exited with 0 +++ENOENT di /etc/ld.so.preload muncul di hampir setiap program Linux dan sama sekali tidak bermasalah. Yang menentukan sehat atau tidak adalah exited code, bukan keberadaan ENOENT.
strace -e trace=openat -o /tmp/trace.log /bin/ls /tmpSelektor %file, %network, %process, %memory, dan %desc tersedia untuk mempersempit ke kelompok syscall tertentu. Persentasenya dihitung dari tabel syscall kernel, jadi nama selektornya ikut berubah antar-versi kernel. Kalau butuh kepastian, sebut syscall secara eksplisit.
Rekap dengan -c untuk Melihat Polanya
Membaca ratusan baris trace melelahkan. -c mengubahnya menjadi tabel.
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
30.55 0.000505 505 1 execve
15.18 0.000251 9 27 mmap
11.31 0.000187 23 8 mprotect
9.56 0.000158 11 14 read
6.72 0.000111 10 11 1 openat
2.90 0.000048 24 2 2 statfs
0.73 0.000012 6 2 2 access
0.00 0.000000 0 7 7 ioctl
------ ----------- ----------- --------- --------- ------------------
100.00 0.001653 11 138 12 totalKolom errors jauh lebih berguna daripada calls. ioctl gagal 7 dari 7 pemanggilan, tapi itu normal karena konfigurasi terminal. Sebaliknya, openat gagal 1 dari 11, statfs dan access gagal seluruhnya; pada program lain pola seperti itu bisa berarti path yang salah.
Perhatikan juga kolom % time: execve memakai 30 persen waktu meski hanya dipanggil sekali. Untuk program yang lambat, ini cara cepat menemukan syscall yang jadi bottleneck tanpa membaca seluruh trace.
Mengikuti Proses Anak dan Memperkecil Log
Saat program melakukan fork atau membuat thread, syscall-nya menyebar ke banyak proses. -f mengikuti semuanya dan memberi awalan PID di tiap baris.
$ strace -f -o f2.log /bin/sh -c '/bin/echo satu > /dev/null; /bin/echo dua > /dev/null'
$ grep -E '^[0-9]+ execve' f2.log
54535 execve("/bin/sh", ["/bin/sh", "-c", "/bin/echo satu > /dev/null; /bin/echo dua > /dev/null"...], 0x7fffca686978 /* 56 vars */) = 0
54536 execve("/bin/echo", ["/bin/echo", "satu"], 0x59a004356e98 /* 56 vars */ <unfinished ...>
54537 execve("/bin/echo", ["/bin/echo", "dua"], 0x59a004356e98 /* 56 vars */ <unfinished ...>Tiga PID muncul sekaligus dan tiap baris diawali PID masing-masing. <unfinished ...> muncul karena dua execve berjalan bersamaan; sisanya diselesaikan terpisah dengan resumed.
Semua PID masuk ke satu berkas bila -o dipakai. Tanpa -o, strace menulis ke pola <file>.<pid> per proses.
Untuk string panjang, -s menentukan berapa karakter yang ditampilkan sebelum dipangkas. Tanpa itu, argumen berisi path panjang atau nilai konfigurasi akan terpotong dan sulit dibaca.
strace -f -s 200 -e trace=openat -o /tmp/trace.log /path/ke/programPagar lain adalah --syscall-limit, yang menghentikan perekaman setelah N syscall. Ini mencegah log membengkak untuk program yang melakukan ribuan syscall per detik.
$ strace --syscall-limit=5 -e trace=openat -o limit.log /bin/ls .
$ wc -l < limit.log
5-yy Membuat Trace Mudah Dibaca
Tanpa -yy, Anda hanya melihat nomor deskriptor dan harus menebak path-nya. Dengan -yy, nama berkas disisipkan di dalam kurung sudut.
$ strace -e trace=write /bin/echo halo
write(1, "halo\n", 5) = 5
$ strace -yy -e trace=write,openat /bin/echo halo
write(1</tmp/opencode/dbgval/out5.log>, "halo\n", 5) = 5AT_FDCWD</tmp/opencode/dbgval> memberi tahu perintah itu dijalankan dari direktori mana. Untuk kasus umum, satu perintah strace sudah cukup tanpa lsof terpisah.
Menempel strace ke Proses yang Sudah Jalan
strace -p adalah cara menempel ke proses yang sudah berjalan. Kenyataannya di Ubuntu, perintah ini sering ditolak. Dokumentasi Yama di kernel menyebut ptrace_scope=1 sebagai restricted ptrace: sebuah proses hanya boleh melakukan PTRACE_ATTACH ke target jika punya hubungan tertentu, dan hubungan bawaannya adalah hanya descendant. Dokumen yang sama menyebut langsung bahwa mode ini tetap membiarkan gdb EXE dan strace EXE bekerja, sementara strace -p hanya bekerja dengan CAP_SYS_PTRACE.
$ python3 -c "import time; time.sleep(20)" &
$ sleep 0.5
$ strace -p $PID -e trace=read
strace: attach: ptrace(PTRACE_SEIZE, 54562): Operation not permittedIni gagal bahkan ketika target adalah anak dari shell yang sama, karena strace dan target adalah dua anak yang setara, bukan hubungan ancestor-descendant. Cara yang pasti berhasil adalah menjalankan program sebagai wrapper, sehingga strace menjadi induk langsung.
| Skenario | Hasil |
|---|---|
strace ./program (program jadi anak strace) | Berhasil |
strace -p ke anak langsung milik shell yang sama | Gagal, Operation not permitted |
strace -p ke proses milik user lain | Gagal, Operation not permitted |
strace sebagai wrapper lalu timeout mengirim SIGTERM | Berhasil, sinyal tercatat |
Naikkan ptrace_scope ke 0, jalankan dengan sudo strace -p, atau beri capability CAP_SYS_PTRACE adalah opsi lain, tetapi ketiganya butuh akses yang lebih tinggi dan tidak diuji di sini. Pemakaian sudo yang berlebihan untuk debugging harian tidak disarankan.
Membaca Output lsof
lsof(8) memetakan deskriptor ke sumberdayanya. Kolom-kolomnya perlu dibaca berurutan.
$ lsof -p 56422
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 56422 su cwd DIR 0,44 980 3115 /tmp/opencode/dbgval
python3 56422 su rtd DIR 259,3 4096 2 /
python3 56422 su txt REG 259,3 7477160 6554575 /usr/bin/python3.14
python3 56422 su mem REG 259,3 3064432 6553731 /usr/lib/locale/locale-archive
python3 56422 su 0r CHR 1,3 0t0 5 /dev/null
python3 56422 su 1u unix 0x0000000000000000 0t0 209520 type=STREAM (CONNECTED)
python3 56422 su 2u unix 0x0000000000000000 0t0 209522 type=STREAM (CONNECTED)Kolom FD punya dua jenis. Flag mode r read, w write, u dua-duanya. Label non-bilangan punya arti khusus: cwd direktori kerja, rtd root direktori, txt executable, mem pustaka yang dipetakan ke memori. Kolom TYPE membedakan REG (berkas), DIR (direktori), CHR (device), unix dan IPv4 (socket). Untuk socket Unix seperti 1u dan 2u, kolom NAME justru tidak ada karena socket memang tidak punya nama path, hanya inode.
Untuk user lain, beberapa entri seperti cwd, rtd, dan exe akan menampilkan Permission denied karena /proc milik proses tersebut tidak bisa dibaca.
Membaca Symlink /proc/PID/fd secara Langsung
proc_pid_fd(5) menjelaskan bahwa direktori /proc/<pid>/fd berisi satu symlink per deskriptor terbuka. readlink pada masing-masing symlink memberi tahu berkas yang sebenarnya.
$ ls -l /proc/56422/fd
lr-x------ 1 su su 64 0 -> /dev/null
lrwx------+ 1 su su 64 1 -> socket:[209520]
lrwx------+ 1 su su 64 2 -> socket:[209522]Socket ditampilkan sebagai socket:[INODE], di mana nomor tersebut adalah inode filesystem, bukan nomor port. Untuk itu Anda perlu lsof -i agar alamat socketnya terbaca. Deskriptor anonim seperti yang dipakai event loop juga tidak muncul karena tidak punya inode.
Mencari Berkas yang Dihapus tapi Masih Dipegang
Salah satu penyebab paling umum ruang disk tidak kembali penuh adalah proses yang masih memegang berkas yang sudah dihapus. proc_pid_fd(5) mencatat bahwa target symlink diberi akhiran (deleted).
$ printf 'isi\n' > held.txt
$ python3 -c "
f=open('held.txt'); open('held.txt','w').close(); import time; time.sleep(30)
" &
$ rm held.txt
$ lsof -p $PID | grep held
python3 56707 su 3r REG 0,44 4 3243 /tmp/opencode/dbgval/held.txt (deleted)
$ readlink /proc/$PID/fd/3
/tmp/opencode/dbgval/held.txt (deleted)lsof +L1 menemukan semua berkas yang sudah di-unlink tapi masih dipegang proses mana pun. Pada mesin uji, perintah ini menemukan 106 unlinked open files sekaligus. Ini pola yang perlu diwaspadai: banyak proses yang tidak pernah menutup handle-nya dengan benar.
Socket: Membaca Alamat yang Tersembunyi di /proc/net/tcp
Nomor port socket tidak pernah muncul di /proc/PID/fd karena yang tampil hanya inode. Alamat sebenarnya ada di /proc/net/tcp, dalam format heksadesimal little-endian.
$ python3 -c "
import socket
s=socket.socket(); s.bind(('127.0.0.1',0)); s.listen(1)
print(s.getsockname()[1])
" &
56505
$ lsof -iTCP -sTCP:LISTEN -P -n | grep 56505
python3 56404 su 3u IPv4 73396 0t0 TCP 127.0.0.1:56505 (LISTEN)
$ grep -i '0100007F:DCB9' /proc/net/tcp
0: 0100007F:DCB9 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 29388 1-P -n membuat lsof menampilkan nomor port tanpa resolusi nama, dan -sTCP:LISTEN menyaring hanya socket yang listen. Format heksanya perlu diterjemahkan satu per satu, tapi lsof -i jauh lebih cepat dibaca.
Kasus Utama: Proses yang Menggantung di Dalam openat
Kasus yang paling khas adalah proses yang terjebak bukan karena menunggu disk, tapi karena menunggu rekan yang tidak pernah datang. Kita membuat FIFO lalu membukanya untuk baca; open() pada FIFO read-only memblokir sampai ada penulis.
Proses yang diuji:
$ python3 -c "
import os
fd=os.open('/tmp/opencode/dbgval/fifoN', os.O_RDONLY)
os.read(fd,1)
" &
56422Bukti dari strace
$ timeout 4 strace -f -e trace=openat,read,close -o hang.log python3 -c "
import os
fd=os.open('fifoN', os.O_RDONLY)
os.read(fd,1)
"
54884 openat(AT_FDCWD, "/tmp/opencode/dbgval/fifoN", O_RDONLY|O_CLOEXEC) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
54884 --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=54880, si_uid=1000} ---
54884 +++ killed by SIGTERM +++Baris terakhir sebelum SIGTERM adalah openat yang tidak pernah mengembalikan nilai. Tidak ada syscall read sama sekali. Ini bukti langsung bahwa macetnya di dalam openat, bukan di read. Perhatikan juga = ? ERESTARTSYS: syscall terhenti karena sinyal, bukan return value asli. Sinyal SIGTERM yang dikirim timeout juga tercatat lengkap dengan PID pengirimnya.
Bukti tanpa ptrace
$ awk '{print $3}' /proc/56422/stat
S
$ cat /proc/56422/wchan
wait_for_partner
$ readlink /proc/56422/exe
/usr/bin/python3.14
$ tr '\0' ' ' < /proc/56422/cmdline
python3 -c
import os
fd=os.open('/tmp/opencode/dbgval/fifoN', os.O_RDONLY)
os.read(fd,1)proc_pid_wchan(5) menjelaskan bahwa wchan menampilkan nama simbolis fungsi kernel tempat task menunggu. wait_for_partner persis sesuai dokumentasi: nama fungsi yang menunggu pasangan pada FIFO. State S (sleep) memastikan proses bukan D (uninterruptible), jadi ini bukan masalah disk.
Yang paling menarik: FIFO tidak muncul di /proc/56422/fd maupun di lsof. Karena openat belum selesai, deskriptornya belum pernah dibuat. Ini berbeda dari kasus menahan FIFO yang sudah terbuka, di mana fd-nya akan terlihat.
Batas yang harus diakui
$ cat /proc/56422/syscall
cat: /proc/56422/syscall: Operation not permitted
$ cat /proc/56422/stack
cat: /proc/56422/stack: Permission deniedproc_pid_status(5) menyediakan State, Threads, dan capability yang dipegang proses, tapi /proc/<pid>/syscall yang memberi nomor syscall dan register tidak terbaca pada user biasa. wchan tetap terbaca. /proc/<pid>/stack selalu butuh root.
Yang tidak bisa diverifikasi di sini: sudo strace -p, ptrace_scope=0, dan CAP_SYS_PTRACE. Ketiganya butuh akses lebih tinggi yang tidak tersedia di mesin uji.
Urutan Triase yang Disarankan
Sebelum menyentuh strace yang mahal, lima perintah ini sering sudah menjawab pertanyaan utama.
H=56422
tr '\0' ' ' < /proc/$H/cmdline; echo
readlink /proc/$H/exe
awk '{print $3}' /proc/$H/stat
cat /proc/$H/wchan
ls -l /proc/$H/fd
lsof -p $HUrutannya: cmdline memberi tahu program apa yang jalan, exe memberi tahu binary mana, stat memberi tahu state, wchan memberi tahu sedang menunggu apa, fd dan lsof memberi tahu berkas apa yang dipegang. Naikkan ke sudo hanya kalau prosesnya milik user lain, dan pakai strace hanya kalau lima perintah ini belum cukup. ptrace(2) mendokumentasikan mekanisme permission yang mendasari semua batas ini.
Naik ke strace -f -e trace=%file sebagai langkah berikutnya, lalu -c jika perlu pola, lalu -p hanya kalau benar-benar tidak ada cara lain. Pembatas timeout dan --syscall-limit jangan lupa dipasang, supaya debugging tidak ikut menggantung.
