Program Neden Bekliyor? Blocking, Timeouts ve Görünmeyen Bekleyişler
Programınız kilitlendi mi? CPU %0 ama yanıt yok mu? select, poll ve epoll_wait dünyasına girerek, programların neden ve nasıl beklediğini, 'Blocking I/O' kavramını ve zamanı okumayı öğreniyoruz.
Bir sistem yöneticisinin, bir DevOps mühendisinin veya bir SRE’nin (Site Reliability Engineer) meslek hayatında en sık karşılaştığı, ama en zor teşhis koyduğu, en sinir bozucu senaryo şudur:
“Program çalışmıyor. Ama çökmedi de.”
Terminali açarsınız. Titreyen ellerle top komutunu çalıştırırsınız. Beklentiniz yüksek bir CPU kullanımı (Load Average) görmektir. Böylece “Ha, işlemci yetmiyor, sunucuyu büyütelim” diyebilirsiniz.
Ama hayır.
İşlemci (CPU) kullanımı %0.
Bellek (RAM) kullanımı sabit, şişme yok.
Disk aktivitesi (I/O Wait) yok.
Hata loglarında (error.log) tek bir “Error” veya “Exception” satırı bile yok.
Program, sanki derin bir komaya girmiş, sanki zaman donmuş gibi öylece duruyor.
Servisi yeniden başlatıyorsunuz (systemctl restart), düzeliyor. Her şey harika.
10 dakika sonra? Yine aynı ölümcül sessizlik.
Yazılımcıya soruyorsunuz: “Hocam kodda sonsuz döngü (infinite loop) falan mı var?” Yazılımcı haklı olarak savunmaya geçiyor: “İmkansız. Sonsuz döngü olsa CPU %100 olurdu, fanlar uçak gibi öterdi. Bu program bir şey bekliyor.”
Peki ama, ne bekliyor? Sevdiğinden gelecek bir mektubu mu? Veritabanından (MySQL/PostgreSQL) gelecek bir cevabı mı? Yoksa Asgard’dan gelecek bir kahramanı mı?
Bu yazıda, Linux’un en sessiz ama en ölümcül katmanına, Blocking I/O (Bloklayan Giriş/Çıkış) ve Event Multiplexing (Olay Çoklama) dünyasına ineceğiz. select, poll ve modern dünyanın süper kahramanı epoll sistem çağrılarını inceleyerek, programların neden ve nasıl beklediğini, bu bekleyişin ne zaman “sağlıklı bir dinlenme” ne zaman “ölümcül bir koma” olduğunu anlayacağız.
⏳ 1. Beklemenin Anatomisi: Blocking vs Non-Blocking
Programlar, insanlara çok benzer. İş hayatında zamanlarının çoğunu aktif olarak çalışarak (CPU cycle harcayarak, kod çalıştırarak) değil, bir şeylerin olmasını veya birilerinin cevap vermesini bekleyerek geçirirler.
Bir web sunucusunun (nginx, apache, node.js) hayat döngüsüne bakalım:
- Diske “index.html dosyasını oku” der ve diskin dönüp veriyi getirmesini bekler (Milisaniyeler).
- Ağa “api.google.com’a bağlan” der ve karşı tarafın telefonu açmasını bekler (Hatta saniyeler).
- Veritabanına “SELECT * FROM users” der ve sonucun gelmesini bekler.
Linux çekirdeğinde (Kernel) bu bekleme eylemi, programın “Uyku Moduna” (Sleep State - S veya D) geçmesi demektir. Process durumunda R (Running) yerine S (Sleeping) görürsünüz. Bu kötü bir şey değildir; CPU’nun boşa dönmesini engeller. Enerji tasarrufu sağlar.
Klasik Yöntem: read() ve Bloklanma
En ilkel ve anlaşılır senaryoyu düşünelim. Tek bir istemciyle konuşan bir C programımız var.
1
2
3
4
5
6
7
8
9
// Basit, bloklayan okuma
char buffer[1024];
// Program burada Kernel'a "Bana veri ver" diyor.
ssize_t n = read(socket_fd, buffer, 1024);
// -- BURADA DÜNYA DURUR --
// Veri gelene kadar (1 saniye, 1 dakika, 1 yıl) alt satıra geçilmez.
process_request(buffer);
Kod read satırına geldiğinde, eğer ağ kablosundan içeri henüz bir bayt veri girmemişse, Kernel acımasız davranır: Programı CPU’dan atar (Schedule Out). “Sen git uyu, veri gelince ben seni dürter uyandırırım” der.
Bu, “Ahmet Amca’nın Bakkalı” gibi tek kişi çalışan bir script için mükemmeldir. CPU harcamaz. Ama ya bu bir Web Sunucusuysa ve kapıda bekleyen 10.000 müşteri varsa? Ahmet Amca (Process) ilk müşterinin (Socket A) siparişini vermesini (Veri göndermesini) beklerken uykuya dalarsa, sıradaki diğer 9.999 müşteri (Socket B, C, D…) sinirden çatlar. Hiçbiri hizmet alamaz.
Çözüm ne? Her müşteri için ayrı bir Ahmet Amca (Thread/Process) yaratmak mı? 10.000 Thread demek, 10.000 ayrı uyuyan process, devasa bir RAM kullanımı ve işlemcinin sürekli bağlam değiştirmekten (Context Switch) başının dönmesi demektir. Bu yöntem ölçeklenmez (C10K Problemi).
İşte burada devreye “I/O Multiplexing” (Giriş/Çıkış Çoklama) girer.
🚦 2. Trafik Polisi Metaforu: select, poll ve epoll
10.000 masası olan dev bir restoran düşünün. Her masada bir müşteri (Socket) var. Garson (Program) tek başına. Müşteriler rastgele zamanlarda sipariş vermek (Veri göndermek) istiyor. Garson siparişleri en verimli şekilde nasıl toplayacak?
A. select() ve poll(): “Herkesi Tek Tek Kontrol Et”
Linux’un ilk zamanlarında (80’ler ve 90’larda) icat edilen yöntem şudur:
Garson eline bir kağıt kalem (File Descriptor List) alır.
- Masaya gider: “Bir isteğin var mı?” Müşteri: “Yok.”
- Masaya gider. “Yok.”
- Masaya gider. “Yok.” … 10.000. Masaya gider. “Yok.”
Sonra tekrar koşarak 1. Masaya döner. Bu döngü sonsuza kadar sürer.
Teknik dilde select veya poll şudur: Program, Kernel’a 10.000 dosya numarasının listesini verir ve “Bunlardan hangisinde hareket var, bana söyle” der. Kernel, listeyi baştan sona (Lineer) tarar. Eğer 10.000. sokette veri varsa, Kernel 9.999 tanesini boşuna kontrol etmiştir.
Bu işlem O(N) karmaşıklığındadır. Yani müşteri sayısı arttıkça, tarama süresi uzar, sistem hantallaşır. CPU, sadece “Kimse bir şey istiyor mu?” diye sormaktan yorulur (%100 System CPU kullanımı).
B. epoll(): “Zili Çalanı Bana Söyle”
Modern yöntem (Linux 2.6 ile gelen devrim) şöyledir. Nginx, Node.js, Go, Redis gibi yüksek performanslı araçların gücü buradan gelir.
Garson mutfakta bir sandalye çeker ve oturur (Wait). Her masada bir elektronik buton vardır. Müşteri sipariş vermek istediğinde butona basar. Mutfaktaki ekrana “Masa 4532 Butona Bastı” bildirimi düşer. Garson yerinden kalkar, sadece Masa 4532’ye gider, siparişi alır ve tekrar yerine oturur.
Teknik dilde epoll (Event Poll) şudur:
epoll_create: Bir “Bildirim Listesi” oluşturulur.epoll_ctl: Kernel’a “Şu soketi bu listeye ekle, veri gelince haber ver” denir (Sadece bir kere yapılır).epoll_wait: Program uykuya yatar. Veri geldiğinde, Kernel doğrudan hangi soketin aktif olduğunu söyler. Listeyi taramaz. Bu işlem O(1) karmaşıklığındadır. İster 10 müşteri olsun, ister 1 milyon müşteri; Kernel’ın tepki süresi (neredeyse) değişmez.
İşte bu yüzden top çıktısında %0 CPU gören ama strace ile baktığınızda sürekli epoll_wait içinde bekleyen bir Nginx, aslında gayet sağlıklıdır. İş yoktur, dinleniyordur.
🕵️♂️ 3. Vaka Analizi: “White Screen” (Beyaz Ekran) Sendromu
Bir e-ticaret sitesindesiniz. Sepeti onayladınız. “Ödeme Yap” butonuna bastınız. Ekran beyazladı. Tarayıcının sekmesindeki o yuvarlak simge dönüyor… dönüyor… Sunucu tarafında her şey sakin. CPU kullanımı düşük. Ama işlem bitmiyor. Müşteri 30 saniye sonra “Pes” edip sekmeyi kapatıyor.
Hadi strace masamıza geçelim ve backend sürecine (örneğin PHP-FPM, Python Gunicorn veya Java Tomcat) canlı bir röntgen çekelim.
1
2
3
# -T: Her syscall'ın ne kadar sürdüğünü göster (Time spent)
# -p: Çalışan sürecin PID'sine bağlan
strace -T -p 12345
Çıktıda şunun gibi satırlar akar:
1
2
3
4
5
# 1. Veritabanına sorgu atılıyor (Send)
sendto(5, "SELECT sleep(50) FROM users...", 30, 0, NULL, 0) = 30 <0.000050>
# 2. Cevap bekleniyor (Receive) - KRİTİK AN
recvfrom(5, 0x7fff..., 8192, 0, NULL, NULL) ...
İmleç tam bu recvfrom satırında durur. Terminal donar.
1 saniye… 3 saniye… 10 saniye…
Siz ekrana bakarsınız, ekran size bakar.
Sonunda tamamlanır:
1
recvfrom(5, "...", 8192, ...) = 150 <50.00231>
Satırın sonundaki, < > içindeki <50.00231> ifadesi, bu sistem çağrısının tam 50 saniye sürdüğünü gösterir.
Program 50 saniye boyunca donmamış, çökmemiş, Bloke Olmuştur.
Veritabanı (MySQL/PostgreSQL) o kadar yavaştır ki (belki “Slow Query”, belki kilitli bir tablo, belki Deadlock), cevap verememiştir.
Siz Web Sunucusuna bakıp “CPU kullanmıyor, memory düşük, demek ki sorun yok” dersiniz. Oysa sunucu, veritabanının kapısında ağaç olmuştur.
💡 Hayat Kurtaran Ders: Düşük CPU kullanımı her zaman “performans sorunu yok” demek değildir. Çoğu zaman “Program çalışamıyor çünkü bir I/O bekliyor” demektir. Bunu anlamanın tek yolu
strace -Tile beklenen süreyi ölçmek veya APM (Application Performance Monitoring) araçlarıdır.
⏱️ 4. Gizemli Sayılar: 60 Saniye Efsanesi
Bazen program tam olarak 60. saniyede (veya 30’da) hata verir. Ne 59. saniye, ne 61. saniye. Tam 60.000 saniye. Doğada hiçbir süreç bu kadar “tam” ve simetrik olamaz. Bu, insan yapımı bir sınırdır: Timeout.
poll veya epoll_wait çağrılarını izlerken, son parametreye mikroskobunuzla bakın:
1
2
# poll(dosya_listesi, adet, milisaniye_cinsinden_timeout)
poll([{fd=4, events=POLLIN}], 1, 60000)
Veya:
1
epoll_wait(6, [], 1024, 30000)
Buradaki 60000 (60 saniye) veya 30000 (30 saniye), programcının (veya Nginx proxy_read_timeout, PHP max_execution_time configlerinin) Kernel’a verdiği kesin bir emirdir:
“En fazla 60 saniye bekle. Eğer o zamana kadar cevap gelmezse, beni uyandır. Daha fazla beklemeyeceğim, ‘Gateway Timeout’ hatasını basıp gideceğim.”
Eğer loglarda “504 Gateway Timeout” görüyorsanız ve strace çıktısında poll çağrısı tam olarak verilen süre sonunda 0 (Timeout) dönüyorsa, sorunu kodun içinde değil, karşı tarafta (Upstream Server, Database, 3rd Party API) arayın. Sizin programınız masumdur; sadece çok sabırlıdır ve sabrı taşmıştır.
🧵 5. Futex: Kilitlenip Kalmak (Deadlock)
Bazen I/O beklemesi yoktur. Ağ trafiği yoktur. Karşı sunucu çok hızlıdır. Ama program yine de ilerlemez.
strace çıktısı tek bir satıra sıkışır ve asla değişmez. Akan bir şey yoktur.
1
futex(0x7f8a3c..., FUTEX_WAIT_PRIVATE, 0, NULL)
futex (Fast Userspace Mutex): Linux’ta “Kilit” (Lock) mekanizmasının taşıyıcı kolonudur. Multithread programlarda (Java, Go, C++, Rust), iki thread aynı veriye (değişkene) erişmeye çalıştığında biri kilidi alır, diğeri usulca sırasını bekler.
Eğer strace ekranında saatlerce bu futex satırını görüyorsanız ve yanında <unfinished ...> yazıyorsa, geçmiş olsun: Bir Deadlock (Ölümcül Kilitlenme) yakaladınız.
Senaryo:
- Thread A: Elinde “Kalem” var, yazı yazmak için “Kağıt” bekliyor.
- Thread B: Elinde “Kağıt” var, yazı yazmak için “Kalem” bekliyor.
- İkisi de elindekini bırakmıyor. İkisi de sonsuza kadar birbirini bekliyor.
- Kernel buna müdahale etmez. CPU kullanımı %0’dır. Ama program aslında bitkisel hayattadır.
Çözüm: Bu noktada strace yetersiz kalır (sadece beklediğini söyler). pstack <PID> veya gdb kullanarak hangi fonksiyonun kilidi tuttuğunu (“Stack Trace”) görmeniz gerekir.
⚙️ 6. Case Study: “Nginx Neden Yanıt Vermiyor?”
Gerçek bir vaka. Nginx sunucunuz yanıt vermeyi kesti. Restart attınız düzeldi. Ama neden oldu?
O anın strace kaydına bakalım:
1
2
3
4
epoll_wait(6, [], 1024, 1000) = 0
epoll_wait(6, [], 1024, 1000) = 0
write(3, "Log verisi...", 50) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(6, [], 1024, 1000) = 0
Nginx sürekli epoll_wait çağırıyor ve 0 dönüyor (Timeout). Yani kimse ona istek göndermiyor mu?
Hayır. Biraz daha dikkatli baktığımızda arada başarısız bir write görüyoruz veya disk I/O’sunda takılan bir write görüyoruz.
Nginx asenkron (Non-blocking) bir sunucudur, ağ işlemlerinde asla bloklanmaz. AMA Linux’ta Disk I/O (Standart Buffered I/O) hala bloklayıcı olabilir.
Eğer diskiniz ölüyse, IOPS limitine vurduysanız veya NFS mount’unuz koptuysa; Nginx o minicik access.log satırını diske yazmak için write() çağrısında takılır kalır.
Ana süreç (Master/Worker) diskte takıldığı için, yeni gelen ağ isteklerini (accept) karşılayamaz.
Olay anında iostat -xz 1 ile diske bakmak, %100 %util görmek hayat kurtarır. Sorun Nginx’te değil, disktedir.
🛠️ Debugging Kontrol Listesi
Programınız “Donmuş” (Idle/Blocked) görünüyorsa, şu adımları izleyin:
- Zamanı Ölç (strace -T):
strace -T -p <PID>Hangi satırın sonunda< >içindeki süre uzun?readmi,connectmi,pollmu?read/recv: Veri gelmiyor. (Karşı taraf yavaş)connect: Bağlantı kurulamıyor. (Network/Firewall)write/send: Buffer dolu veya disk yavaş.
- Özet Tablo Çıkar (strace -c):
strace -c -p <PID>Program zamanını en çok nerede harcıyor?futex: %90 ise -> Kilitlenme (Deadlock/Race Condition).epoll_wait: %90 ise -> İş yok, boşta bekliyor (Normal).read: %90 ise -> I/O darboğazı.
-
Process Stack Analizi (pstack): Eğer
futextepedeyse,pstack <PID>(veya gdb) ile Thread’lerin kodun hangi satırında (hangi fonksiyonda) kilitlendiğine bakın. - Network vs Disk Ayrımı:
Soket (
recv) bekliyorsa sorun Ağ/DB/API tarafındadır. Dosya (write/fsync) bekliyorsa sorun Disk/Storage tarafındadır.
👋 Son Söz: Sabır Erdemdir, Ama Timeout Hayattır
Programcılıkta beklemek, kaçınılmaz bir gerçektir. Her sistem, başka bir sisteme muhtaçtır. Önemli olan, bu beklemenin kontrollü (Timeout’lu) ve Görünür (Trace edilebilir) olmasıdır.
“Program çalışmıyor” demek yerine “Program X sunucusundan 80 portuna yaptığı bağlantının cevabını 5 saniyedir bekliyor ve gelmediği için bloklanıyor” diyebildiğiniz gün, SRE (Site Reliability Engineer) unvanını ve maaşını hak ettiğiniz gündür.
Bu blog serisinde Kernel’ın derinliklerine kadar indik. Dosya yetkilerini aştık, ağ sorunlarını çözdük ve şimdi de zamanın donduğu anları analiz ettik. Artık Linux terminaline baktığınızda sadece komutları değil, arkada dönen o muazzam dişlileri, bekleyen garsonları ve çalan telefonları da görüyorsunuz.
Bir sonraki durağımız (belki de en korkutucusu ve en kanlısı): Kernel Panic, OOM Killer ve Crash Dump Analizi. Sistem tamamen çöktüğünde, Kernel “Beni öldürdüler” diyerek son bir çığlık attığında (Panic), geride kalan kanıtları (Kdump) nasıl okuruz? Hafıza (RAM) dolduğunda Kernel kimi, neden kurban seçer?
O zamana kadar, syscall’larınız hızlı, timeout’larınız makul, connectionlarınız keep-alive olsun.
