Blog
Mert Özen'in yazılım geliştirme, backend, frontend ve DevOps üzerine blog yazıları.
İşletim Sistemi Ne Yapar? Kernel Mode, User Mode ve System Call
İşletim sistemi CPU'yu ve belleği sanallaştırır, kaynakları process'ler arasında paylaştırıp korur ve veriyi kalıcı hale getirir; uygulaman ondan bir şey istediğinde system call ile user mode'dan kernel mode'a geçer. File.ReadAllText'in yaptığı yedi syscall'ı strace ile izliyor, .NET'in System.Native katmanını görüyor ve her satırı ayrı açan log yazımının buffered yazımdan 38 kat yavaş olduğunu ölçüyoruz.
Process Nedir? Address Space, PID, Exit Code ve .NET'te Child Process Çalıştırmak
Process, çalışan bir programın kernel'deki karşılığıdır: kendi address space'i, file descriptor tablosu, environment'ı ve en az bir thread'i olan, PID ile tanınan bir birim. /proc ile bir .NET process'inin içine bakıyor, fork/exec ve zombie'yi strace ve ps ile görüyor, Process.Start'ta stdout ve stderr'i ayrı ayrı okumanın 64 KB pipe buffer'ında nasıl deadlock ürettiğini ve timeout, exit code, shell injection ile birlikte nasıl düzeltildiğini gösteriyoruz.
Thread Nedir? Kernel Thread, Stack, Thread-Local Storage ve .NET Thread'inin OS Karşılığı
Thread, bir process'in address space'ini paylaşan ama kendi stack'i, register'ları ve thread-local storage'ı olan, scheduler'ın CPU'ya koyduğu birimdir; .NET'te her managed thread bire bir bir OS thread'idir. ps -T ile bir console app'in on iki thread'ini tanıyor, 1000 thread'in 12,7 GB virtual ama 20 MB resident memory tuttuğunu ve 100.000 bekleyen task'ın yedi thread'le çalıştığını ölçüyor, ThreadStatic ile AsyncLocal'ın await sonrası farkını gösteriyoruz.
Scheduling: OS Hangi Thread'i Ne Zaman Çalıştırır? Context Switch, Priority ve Linux EEVDF
Scheduler, core'lardan fazla olan thread'leri kısa dilimlerle çalıştırır ve her geçişte bir context switch yapar; Linux bunu 6.6'dan beri EEVDF ile, nice ağırlıklarına göre adil paylaştırarak yapar. Context switch'leri /proc ile sayıyor, maliyetini ölçüyor, nice 10'un CPU'nun onda birini aldığını, Linux'ta Process.PriorityClass'ın yalnız ana thread'i etkilediğini ve --cpus=0.5 container'ında 31 periyodun 30'unda throttling olduğunu gösteriyoruz.
Virtual Memory: Page, Page Table, TLB ve Page Fault
Virtual memory, her process'e kendi adres alanını verir ve adresleri sayfa sayfa fiziksel belleğe çevirir; bir aralığı ayırmak ile ona RAM vermek ayrı işlerdir ve RAM ancak sayfaya dokunulunca gelir. 512 MB'lık allocation'ın resident memory'yi 3 MB, dokununca 513 MB artırdığını ve 131 bin minor fault ürettiğini ölçüyor, .NET'in 17 GB'lık VIRT'inin GC reserve'ü olduğunu ve 256 MB'lık container'ın 1 GB allocation'ı kabul edip OOM killer ile öldüğünü gösteriyoruz.
Memory'yi Process Gözüyle Okumak: mmap, Copy-on-Write, Shared Memory ve Container Memory Limit
Bir process belleği OS'tan mmap ve VirtualAlloc ile alır; dosyaya bağlı sayfalar process'ler arasında paylaşılır, yazıldığında copy-on-write ile kopyalanır ve container'da .NET GC, cgroup limitinin yüzde 75'ini heap sınırı yapar. 256 MB'lık container'da managed allocation'ın OOMKilled yerine 189. MB'ta yakalanabilir bir OutOfMemoryException verdiğini, paylaşılan bir dosyanın iki process'in RSS'inde iki kez sayıldığını ve DATAS'ın Server GC'nin belleğini 593 MB'tan 167 MB'a indirdiğini ölçüyoruz.
File System Nasıl Çalışır: Inode, File Descriptor, Page Cache ve fsync
Bir dosyaya yazmak veriyi önce kernel'in page cache'ine koyar; diske ancak fsync (.NET'te Flush(true)) ile ya da kernel'in arka plandaki yazmasıyla iner. Inode, hard link ve file descriptor'ı gösteriyor, 1000 siparişte fsync'in yazmayı yaklaşık 60 kat yavaşlattığını, Linux'ta .NET'in FileShare.None lock'unun advisory olduğunu ve temp dosyası + rename ile atomic write'ı strace ile ölçüyoruz.
I/O Modelleri ve IPC: Blocking, Non-Blocking, epoll/kqueue/IOCP, Pipe ve Signal
await sırasında bekleyen bir thread yoktur: .NET, Linux'ta socket'leri epoll'a kaydeder ve tek bir .NET Sockets thread'i hepsini izler, Windows'ta aynı işi IOCP yapar. 1000 boşta bağlantının async'te 14, blocking'de 1014 OS thread'i tuttuğunu strace ile gösteriyor; Unix domain socket ve named pipe'ı deniyor ve shell ile başlatılan bir container'ın SIGTERM'i neden hiç almayıp 10 saniye sonra 137 ile öldüğünü ölçüyoruz.
Container'ı İşletim Sistemi Gözüyle Anlamak: Namespace, cgroup v2 ve "Container Sadece Bir Process'tir"
Container, namespace'lerle neyi göreceği ve cgroup'larla ne kadar kullanacağı sınırlanmış sıradan bir process'tir; host'un kernel'ini paylaşır. Container'da PID 1 olan dotnet'in host'ta 300861 numaralı bir process olduğunu, user namespace'in paylaşıldığını, .NET'in CPU ve bellek limitini cgroup'tan okuduğunu ve pids.max'a çarpan bir thread açılışının bellek boşken "Out of memory." ile çöktüğünü gösteriyoruz.