Home / Linux, Systems, and Infrastructure / Debugging Linux: …

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

  • strace menunjukkan syscall yang dipanggil program, termasuk yang tidak pernah selesai.
  • lsof memetakan deskriptor ke berkas, direktori, pipe, socket, atau device.
  • /proc/<pid>/fd memberi symlink yang bisa dibaca dengan readlink.
  • -o, -e, -f, -c, -yy, -s, dan --syscall-limit menjaga trace tetap terbaca.
  • strace -p pada Ubuntu sering ditolak Yama ptrace_scope=1; jalankan program sebagai wrapper agar pasti berhasil.
  • lsof +L1 menemukan berkas yang sudah dihapus tapi masih dipegang, penyebab umum disk penuh yang membingungkan.
  • wchan sering tetap terbaca meski /proc/<pid>/syscall dan stack ditolak.

Lingkungan Pengujian

text
$ 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
1

Semua 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.

text
$ 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.

text
$ 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.

bash
strace -e trace=openat -o /tmp/trace.log /bin/ls /tmp

Selektor %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.

text
% 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 total

Kolom 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.

text
$ 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.

bash
strace -f -s 200 -e trace=openat -o /tmp/trace.log /path/ke/program

Pagar lain adalah --syscall-limit, yang menghentikan perekaman setelah N syscall. Ini mencegah log membengkak untuk program yang melakukan ribuan syscall per detik.

text
$ 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.

text
$ 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) = 5

AT_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.

text
$ python3 -c "import time; time.sleep(20)" &
$ sleep 0.5
$ strace -p $PID -e trace=read
strace: attach: ptrace(PTRACE_SEIZE, 54562): Operation not permitted

Ini 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.

SkenarioHasil
strace ./program (program jadi anak strace)Berhasil
strace -p ke anak langsung milik shell yang samaGagal, Operation not permitted
strace -p ke proses milik user lainGagal, Operation not permitted
strace sebagai wrapper lalu timeout mengirim SIGTERMBerhasil, 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.

text
$ 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.

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.

text
$ 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).

text
$ 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.

text
$ 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:

text
$ python3 -c "
import os
fd=os.open('/tmp/opencode/dbgval/fifoN', os.O_RDONLY)
os.read(fd,1)
" &
56422

Bukti dari strace

text
$ 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

text
$ 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

text
$ cat /proc/56422/syscall
cat: /proc/56422/syscall: Operation not permitted
$ cat /proc/56422/stack
cat: /proc/56422/stack: Permission denied

proc_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.

bash
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 $H

Urutannya: 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.