Mert Özen Her Satırda Daha İleri
Bilgisayar Bilimi Temelleri İşletim Sistemi Temelleri — #01

İşletim Sistemi Ne Yapar? Kernel Mode, User Mode ve System Call

Mert Özen 14 Kas 2026 14 dk 1 görüntülenme
İş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.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; testler xUnit v3 4.0.3, ölçümler BenchmarkDotNet 0.15.8 ile alındı. Linux gözlemleri Ubuntu 24.04 tabanlı mcr.microsoft.com/dotnet/sdk:10.0 container'ında (Linux 6.x kernel) strace 6.8 ile yapıldı. macOS ve Windows araçları resmi dokümanlarına dayanarak anlatılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) açılırken kataloğu küçük bir JSON dosyasından okuyor: File.ReadAllText("data/catalog.json"). Dosya 460 byte, içinde dört kitap var. Bu tek satır işletim sistemine kaç kez başvurur: bir mi, üç mü, yedi mi? Ve bu satırı çalıştıran dotnet process'i, açılıştan kapanışa kadar toplam kaç kez kernel'e gider: yüz mü, bin mi?

İkinci soru: sipariş log'unu her satır için File.AppendAllText çağırarak yazan kodla aynı satırları bir StreamWriter üzerinden yazan kod arasında kaç kat fark vardır? Cevapları gerçek strace çıktısı ve BenchmarkDotNet ölçümüyle vereceğiz. Seri boyunca sorumuz hep aynı: uygulamam neden böyle davranıyor ve bunun işletim sistemi seviyesindeki karşılığı ne?

İşletim Sistemi Bir Uygulama İçin Ne Yapar?

Bookstore API'yi çalıştırdığında kodun makinede yalnız çalışıyormuş gibi davranır: kendine ait bir CPU'su, belleği ve diski varmış gibi. Oysa aynı makinede SQL Server, bir reverse proxy ve belki birkaç yüz process daha çalışıyor. Bu görüntüyü sağlayan işletim sistemi. Bir uygulama geliştiricisi gözüyle üç iş yapar.

Birincisi sanallaştırma. Az sayıdaki CPU core'unu yüzlerce thread arasında çok kısa dilimlerle paylaştırır ve her birine sürekli çalışıyormuş hissi verir; bunun nasıl yapıldığı serinin dördüncü yazısı olan scheduling'in konusu. Fiziksel belleği de her process'e kendi özel address space'i varmış gibi gösterir; bu da beşinci yazıdaki virtual memory'nin konusu. İkincisi paylaştırma ve koruma. CPU zamanı, bellek, dosyalar ve network bağlantıları process'ler arasında bölünür ve bir process'in ötekinin belleğine ya da dosyalarına izinsiz dokunması engellenir. Donanıma doğrudan erişim tek bir yerde, kernel'de toplanır. Üçüncüsü kalıcılık. Elektrik kesilse bile kalması gereken veriyi file system üzerinden diske yazar ve Dune'un fiyatını içeren dosyanın yarın sabah da orada olmasını sağlar.

Bunların hepsini soyutlamalarla sunar: disk sektörü yerine dosya, network kartı yerine socket, çalışan kod yerine process.

Kernel'in iç yapısı sistemden sisteme değişir. Linux monolithic bir kernel'dir: scheduler, memory yönetimi, file system'ler, network stack ve driver'lar aynı address space'te, kernel mode'da çalışır. Microkernel yaklaşımında ise kernel küçük tutulur, file system ve driver gibi servisler ayrı process'ler olarak çalışıp mesajlaşır. Windows NT (bugünkü Windows sürümlerinin dayandığı kernel ailesi) genellikle hybrid olarak sınıflandırılır: microkernel fikirlerinden etkilenmiştir ama executive ve driver'lar kernel mode'dadır. macOS'un kernel'i XNU (Apple'ın macOS ve iOS'ta kullandığı kernel) da hybrid'dir; Apple'ın dokümanına göre Mach, BSD (Berkeley Software Distribution; Unix'in bir kolu) ve I/O Kit bileşenlerini bir arada barındırır. Uygulama geliştirici için fark küçük: üçünde de uygulaman kernel'den bir şey istemek için system call yapar.

User Mode, Kernel Mode ve Privilege Ring

Korumanın temeli donanımda. CPU en az iki ayrıcalık seviyesi destekler ve o an hangisinde çalıştığını bir register'da tutar. x86 ailesinde bu seviyelere ring denir ve 0'dan 3'e dört ring vardır; Linux ve Windows yalnız ikisini kullanır: kernel için ring 0, uygulamalar için ring 3. ARM (telefonlarda ve birçok sunucuda kullanılan işlemci mimarisi ailesi) tarafında karşılığı exception level'lardır; uygulamalar EL0 (en düşük ayrıcalıklı exception level) seviyesinde, kernel EL1 (bir üstteki, kernel'e ayrılmış seviye) seviyesinde çalışır.

User mode'da çalışan kod bazı instruction'ları çalıştıramaz: interrupt'ları kapatmak, page table'ı değiştirmek, I/O port'larına doğrudan yazmak gibi. Denerse CPU bir exception üretir ve kontrol kernel'e geçer. Bellekte de aynı ayrım var: kernel'e ait sayfalar user mode'dan okunamaz. Microsoft'un driver dokümanına göre her user mode uygulaması kendi özel virtual address space'ini ve handle tablosunu alır, kernel mode'daki bütün kod ise tek bir address space'i paylaşır. Bu yüzden Bookstore'daki bir NullReferenceException yalnız process'i kapatır; hatalı bir driver ise bütün sistemi çökertebilir.

System Call: Kernel'den Bir Şey İstemenin Tek Yolu

Ring 3'teki kod diske yazamıyorsa File.ReadAllText nasıl çalışıyor? Cevap system call (kısaca syscall; uygulamanın kernel'den bir iş istemesi). Uygulama istediği işin numarasını ve argümanlarını register'lara koyar ve özel bir instruction çalıştırır: x86-64'te syscall, ARM64 (ARM'ın 64 bit mimarisi) üzerinde svc. CPU bu noktada ring 0'a geçer ve kernel'in açılışta belirlediği sabit bir giriş noktasına atlar. Uygulama kernel'in içinde istediği adrese zıplayamaz; yalnız bu kapıdan girebilir.

01-os-layers-tr.png

Kapıdan girdikten sonra kernel uygulamaya güvenmez. Microsoft'un native system service dokümanına göre user mode'dan gelen parametreler güvenilmez kabul edilir ve kullanılmadan önce ayrıntılı kontrol edilir; verilen buffer'ın gerçekten o process'in belleğinde olup olmadığı da buna dahildir. Linux'ta da aynı: file descriptor geçerli mi, pointer user space'te mi, bu process'in bu dosyayı okumaya yetkisi var mı? İş bitince kernel sonucu bir register'a yazar ve ring 3'e döner. Linux'ta hata, negatif bir hata koduyla döner; glibc (Linux'taki standart C kütüphanesi) bunu -1 ve errno'ya çevirir, .NET de sonunda bir IOException'a.

Bu geçişin bir maliyeti var ve normal bir method çağrısıyla karşılaştırılamaz. Register'lar saklanır, kernel giriş kodu çalışır, dönüşte her şey geri yüklenir. 2018'de ortaya çıkan Meltdown açığından sonra eklenen önlemler bunu daha da pahalı yaptı: Linux kernel dokümanı, PTI'ın (Page Table Isolation; kernel ve user sayfa tablolarını ayıran önlem) her kernel giriş ve çıkışında page table değiştirdiğini ve bunun tek başına yaklaşık yüz cycle tuttuğunu söylüyor. Üstüne kernel'in yaptığı asıl iş ve CPU cache'lerinin bozulması geliyor. Kısacası bir syscall, boş bir method çağrısından çok daha pahalıdır; ne kadar pahalı olduğunu aşağıda kendi ölçümümüzle göreceğiz.

File.ReadAllText'i strace ile İzlemek

Teori yeter, bakalım. Linux'ta bir process'in yaptığı syscall'ları görmenin en kolay yolu strace: kernel'in ptrace mekanizmasıyla hedef process'i her syscall'da durdurur ve çağrıyı argümanlarıyla birlikte yazar. Windows'ta Docker Desktop ya da WSL (Windows Subsystem for Linux; Windows içinde gerçek bir Linux kernel'i çalıştıran katman) ile, macOS'ta Docker ile aynı ortamı şöyle kurarsın (PowerShell'de çalışır; --cap-add=SYS_PTRACE strace'in ptrace kullanabilmesi için):

docker run --rm -it --cap-add=SYS_PTRACE -v ${PWD}:/src -w /src mcr.microsoft.com/dotnet/sdk:10.0 bash
apt-get update && apt-get install -y strace
dotnet publish src/Bookstore.OsLab -c Release -o /app
cd /app

Deney uygulaması Bookstore.OsLab adında küçük bir console app; seri boyunca her yazı ona bir komut ekleyecek. Bu yazının ilk komutu kataloğu okuyor:

using Bookstore.OsLab;

string command = args.Length > 0 ? args[0] : "help";

switch (command)
{
    case "read-catalog":
        string json = File.ReadAllText("data/catalog.json");
        Console.WriteLine($"catalog.json: {json.Length} chars");
        break;

    case "log-naive":
        OrderLog.WriteNaive("orders.log", int.Parse(args[1]));
        break;

    case "log-buffered":
        OrderLog.WriteBuffered("orders.log", int.Parse(args[1]));
        break;

    default:
        Console.WriteLine("usage: read-catalog | log-naive N | log-buffered N");
        break;
}

Şimdi yalnız catalog.json'a dokunan syscall'ları izleyelim. -f child process ve thread'leri de takip eder, -y file descriptor numarasının yanına dosya yolunu yazar, -P çıktıyı tek bir dosyaya filtreler:

strace -f -y -P /app/data/catalog.json dotnet Bookstore.OsLab.dll read-catalog
[pid   714] openat(AT_FDCWD</app>, "/app/data/catalog.json", O_RDONLY|O_CLOEXEC) = 22</app/data/catalog.json>
[pid   714] fstat(22</app/data/catalog.json>, {st_mode=S_IFREG|0644, st_size=460, ...}) = 0
[pid   714] flock(22</app/data/catalog.json>, LOCK_SH|LOCK_NB) = 0
[pid   714] pread64(22</app/data/catalog.json>, "[\n  { \"isbn\": \"9780441172719\", \""..., 4096, 0) = 460
[pid   714] pread64(22</app/data/catalog.json>, "", 4096, 460) = 0
[pid   714] flock(22</app/data/catalog.json>, LOCK_UN) = 0
[pid   714] close(22</app/data/catalog.json>) = 0
catalog.json: 460 chars

İlk sorunun cevabı yedi. openat dosyayı açar ve kernel'in bu process için tuttuğu tabloda bir numara döner: file descriptor 22 (0, 1 ve 2 stdin, stdout ve stderr'in; arası runtime'ın kendi dosyaları). O_CLOEXEC bayrağı, bu process ileride bir child process başlatırsa descriptor'ın ona miras kalmamasını sağlar; bir sonraki yazıda bu bayrağın neden önemli olduğunu göreceğiz. fstat dosyanın türünü ve boyutunu sorar; .NET bununla açılan şeyin bir directory olmadığını da kontrol eder.

flock satırı çoğu kişiyi şaşırtır. Unix'te open'ın Windows'taki gibi bir paylaşım modu yoktur. .NET, FileShare davranışını taklit etmek için dosyayı açtıktan sonra advisory bir lock alır: FileShare.None için exclusive, diğer değerler için shared lock. Advisory olduğu için yalnız aynı mekanizmayı kullanan process'leri etkiler. Ardından iki pread64 geliyor: ilki 4096 byte'lık buffer'a 460 byte okuyor, ikincisi 0 dönüyor; 0, dosyanın sonuna gelindiği anlamına gelir. Son iki satır lock'u bırakıp dosyayı kapatıyor.

Peki process'in toplamı? -c çıktıyı tek tek yazmak yerine özet tablo üretir:

strace -f -c -o summary.txt dotnet Bookstore.OsLab.dll read-catalog
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- -----------------------
 41.31    0.008971        4485         2           ppoll
 26.25    0.005701         167        34         3 futex
  4.94    0.001072           6       157           mmap
  3.77    0.000818           2       340           mprotect
  3.16    0.000687           9        76           read
  2.89    0.000628           8        78        18 openat
  2.82    0.000612           6        93        89 readlinkat
  1.79    0.000389           7        54           munmap
  1.76    0.000383           6        60           close
  1.47    0.000320           5        63           fstat
  1.33    0.000288          41         7           clone
  0.64    0.000139           4        31           madvise
...
------ ----------- ----------- --------- --------- -----------------------
100.00    0.021714          16      1278       126 total

Toplam 1278 syscall; bizim satırımızınki yalnız yedi. Geri kalanı runtime'ın açılışı: assembly'leri ve native kütüphaneleri belleğe map etmek (mmap), reserve ettiği bellek bölgelerinden sayfa sayfa kullanıma açmak (mprotect), yedi thread başlatmak (clone) ve thread'ler arası bekleşme (futex). 126 hatalı çağrı da var ve çoğu normal: runtime, LTTng (Linux'ta bir tracing altyapısı) kütüphanesi kurulu mu diye birkaç yola bakıyor ve ENOENT alıyor. strace'te hata görmek her zaman sorun anlamına gelmez.

strace -f dotnet run da çalışır ama dotnet run önce MSBuild ile derleme yapar ve -f o child process'leri de izler; çıktı derleme satırlarıyla dolar. Derlenmiş dll'i doğrudan çalıştırmak daha temiz.

.NET Runtime OS ile Nerede Konuşuyor?

Figürdeki katmanların .NET tarafına bakalım. .NET'in OS ile konuştuğu iki ayrı yer var. Birincisi runtime'ın kendisi: CoreCLR'ın GC'si (garbage collector; kullanılmayan belleği otomatik toplayan bileşen), JIT'i (Just-In-Time; kodu çalışma anında makine koduna çeviren derleyici) ve thread yönetimi C++ ile yazılmış ve tarihsel olarak Win32 API'sine göre tasarlanmış. JIT'in girdisi IL (Intermediate Language; .NET'in işlemciden bağımsız ara dili), çıktısı o makinenin instruction'ları. Linux ve macOS'ta runtime'ın ihtiyaç duyduğu Win32 fonksiyonlarını PAL (Platform Adaptation Layer; Win32 çağrılarını Unix karşılıklarıyla taklit eden katman) sağlar; bu karşılıklar POSIX (Unix benzeri sistemlerin ortak API standardı) fonksiyonlarıdır.

İkincisi senin kullandığın BCL (Base Class Library; .NET'in temel class kütüphanesi). FileStream, Socket ve Process gibi class'lar Unix'te libSystem.Native.so gibi küçük native kütüphaneleri çağırır. dotnet/runtime deposundaki interop rehberi bu shim'lerin neden var olduğunu açıklıyor: Unix sistemleri kaynak kod seviyesinde aynı API'yi sunsa da binary seviyesinde farklıdır; struct alanlarının sırası, sabitlerin sayısal değerleri ve sembol adları Linux ile macOS arasında, hatta işlemci mimarileri arasında değişebilir. Tek bir P/Invoke tanımı her yerde çalışmaz; shim her platformda aynı imzayı sunan ince bir C katmanıdır. Örneğin SystemNative_Open, flag'leri platformun değerlerine çevirip libc'nin open() fonksiyonunu çağırır; glibc de bunu openat syscall'ına çevirir. strace'te kütüphanenin yüklendiği anı görebilirsin:

strace -f -e trace=openat dotnet Bookstore.OsLab.dll read-catalog 2>&1 | grep -E "System.Native|catalog"
[pid  1621] openat(AT_FDCWD, "/usr/share/dotnet/shared/Microsoft.NETCore.App/10.0.15/libSystem.Native.so", O_RDONLY|O_CLOEXEC) = 14
[pid  1621] openat(AT_FDCWD, "/app/data/catalog.json", O_RDONLY|O_CLOEXEC) = 22
catalog.json: 460 chars

Windows'ta ara shim yok: BCL doğrudan kernel32.dll'deki CreateFileW ve ReadFile'ı çağırır, bunlar ntdll.dll'deki NtCreateFile ve NtReadFile stub'larına iner ve syscall orada yapılır. Bu çağrıların altındaki P/Invoke mekanizmasını C# İleri Seviye serisindeki "C#'ta Interop ve unsafe Kod: P/Invoke, Pointer'lar ve fixed" yazısında anlatmıştık; burada önemli olan, her .NET I/O çağrısının sonunda bu katmanlardan geçip bir syscall'a dönüştüğü.

macOS ve Windows'ta Aynı Gözlem

Bu bölümdeki araçlar bu yazı için çalıştırılmadı; resmi dokümanlarına göre anlatıyorum. macOS'ta strace yok. Klasik karşılığı DTrace tabanlı dtruss, ancak SIP (System Integrity Protection; macOS'un sistem dosyalarını ve kernel'i koruyan mekanizması) açıkken DTrace kısıtlıdır ve SIP'i kapatmak önerilmez. Onun yerine fs_usage var: man sayfasına göre file system ile ilgili syscall'ları ve page fault'ları canlı raporlar, root yetkisi ister ve process adıyla filtrelenebilir (sudo fs_usage -w -f filesys dotnet). Daha ayrıntılı görüntü için Instruments'ın System Trace template'i syscall'ları, thread scheduling'i ve virtual memory işlemlerini kaydeder. Pratik önerim: Mac'te bu serideki gözlemleri Docker ile Linux container'ında yap.

Windows'ta karşılık Sysinternals'ın Process Monitor'ü. Dokümanına göre file system, Registry ve process/thread etkinliğini gerçek zamanlı gösterir ve her işlem için thread stack'ini yakalar. Process Name filtresini dotnet.exe yapıp catalog.json'u okuyan uygulamayı çalıştırdığında CreateFile, ReadFile ve CloseFile gibi işlemleri görürsün. Bir farkı akılda tut: Process Monitor syscall'ların kendisini değil, kernel içindeki file system ve Registry işlemlerini gösterir; bu yüzden strace çıktısıyla satır satır eşleşmez.

Syscall Ucuz Değil: Buffering Neden Var?

Şimdi ikinci soruya geçelim. Bookstore'un sipariş log'unu yazan iki versiyon:

namespace Bookstore.OsLab;

public static class OrderLog
{
    public static string FormatLine(int orderNo) =>
        $"order={orderNo:D6} isbn=9780441172719 qty=1 total=420.00 TRY";

    // Her satır için dosya açılır, yazılır ve kapatılır
    public static void WriteNaive(string path, int count)
    {
        for (int i = 1; i <= count; i++)
        {
            File.AppendAllText(path, FormatLine(i) + "\n");
        }
    }

    // Dosya bir kez açılır; satırlar StreamWriter'ın buffer'ında birikir
    public static void WriteBuffered(string path, int count)
    {
        using StreamWriter writer = new(path, append: true);
        for (int i = 1; i <= count; i++)
        {
            writer.Write(FormatLine(i));
            writer.Write('\n');
        }
    }
}

İkisinin ürettiği dosyanın byte byte aynı olduğunu bir xUnit testi doğruluyor (snapshot'taki OrderLogTests). Kernel tarafında ise iki farklı dünya var. Bin satır için strace -c çıktılarından ilgili satırlar:

strace -f -c -o naive.txt dotnet Bookstore.OsLab.dll log-naive 1000
strace -f -c -o buffered.txt dotnet Bookstore.OsLab.dll log-buffered 1000
# log-naive 1000
  2.83    0.005745           2      2000           flock
  2.04    0.004142           3      1160        82 openat
  1.68    0.003416           3      1000           pwrite64
  1.62    0.003287           3      1077           fstat
  1.62    0.003285           3      1081           close
  1.57    0.003176           3      1000           fstatfs
  1.35    0.002728           2      1000           lseek
100.00    0.202783          19     10570       177 total

# log-buffered 1000
  3.70    0.000657           4       160        82 openat
  1.54    0.000274           3        81           close
  1.31    0.000232           2        78           fstat
  0.00    0.000000           0        14           pwrite64
100.00    0.017775          12      1397       173 total

File.AppendAllText her çağrıda dosyayı baştan açıyor: openat, file system bilgisini soran fstatfs, flock, konumu soran lseek, boyutu soran fstat, asıl yazma pwrite64, lock'u bırakan ikinci flock ve close. Satır başına sekiz syscall, bin satırda 10.570. Buffered versiyonda dosya bir kez açılıyor; 55.000 byte'lık içerik 4096 byte'lık buffer dolduğunda yazılıyor ve toplamda 14 pwrite64 yetiyor. Süre farkını BenchmarkDotNet ile, aynı container'da 10.000 satır için ölçtüm:

| Method   | Lines | Mean        | Error       | StdDev      | Median      | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------- |------ |------------:|------------:|------------:|------------:|------:|--------:|----------:|------------:|
| Naive    | 10000 | 22,638.6 us | 1,235.39 us | 3,360.96 us | 21,546.4 us |  1.02 |    0.20 |   3.66 MB |        1.00 |
| Buffered | 10000 |    595.5 us |    11.14 us |    15.98 us |    595.1 us |  0.03 |    0.00 |   1.31 MB |        0.36 |

Yaklaşık 38 kat. Naive versiyon satır başına ortalama 2,3 mikrosaniye harcıyor, buffered versiyon 0,06; farkın büyük kısmı o sekiz kernel ziyaretinden geliyor. Kabaca syscall başına birkaç yüz nanosaniye ediyor ve bunun içinde mode switch, kernel'in kontrolleri ve asıl iş var. Buffering'in var olma sebebi bu: kernel'e gitmek pahalıysa, gitmeden önce iş biriktir. StreamWriter, FileStream ve glibc'nin fwrite'ı aynı fikre dayanır. Bedeli de var: buffer'daki veri henüz kernel'e verilmemiştir; process tam o anda çökerse son satırlar kaybolur. Kernel'e verilen verinin diske ulaşıp ulaşmadığı ise ayrı bir soru; onu file system yazısında fsync ile konuşacağız.

Üretimde Dikkat

strace bedava değil: hedef process'i her syscall'da durdurur ve syscall yoğun bir process belirgin şekilde yavaşlar. Yukarıdaki tablolarda bu yüzden süreler değil, sayılar güvenilir. Üretimdeki bir Bookstore API'ye bağlanman gerekiyorsa strace -f -c -p <PID> ile kısa bir süre özet al, -e trace=openat,connect gibi bir filtreyle yalnız ilgilendiğin çağrıları izle ve işin bitince ayrıl. Daha düşük maliyetli gözlem için eBPF tabanlı araçlar ve perf trace var.

Container'da strace çalıştırmak ptrace izni ister. Kendi container'ında --cap-add=SYS_PTRACE yeterli; Kubernetes'te çalışan bir pod'a ise doğrudan değil, ephemeral debug container ile girilir. Teşhisin .NET araçlarıyla yapılan tarafı Debugging ve Problem Çözme serisinin dump ve hang yazılarında.

strace'in en çok işe yaradığı yer "dosya yok" ve "yetki yok" hataları: strace -f -e trace=%file -Z yalnız başarısız dosya çağrılarını gösterir, process'in bir configuration dosyasını tam olarak hangi yolda aradığını görürsün.

Sık Düşülen Tuzaklar

Syscall'ı method çağrısı kadar ucuz sanmak. File.AppendAllText, File.ReadAllText ve File.WriteAllText kolaylık method'larıdır; her çağrıda dosyayı açıp kapatırlar. Bir döngünün içinde kullanıldıklarında yukarıdaki 38 katlık fark ortaya çıkar. Döngüde yazıyorsan dosyayı bir kez aç.

Console'a sık yazmak. Console.Out her yazmadan sonra flush eder. Aynı container'da for (int i = 0; i < 1000; i++) Console.WriteLine(i); döngüsü 2006 write syscall'ı üretti; int alan overload değeri ve satır sonunu ayrı ayrı yazdığı için çağrı başına iki tane. Sıcak bir döngüde log'u Console'a basmak bu yüzden pahalıdır; logging framework'lerinin kendi buffer'ları ve arka plan yazıcıları bunun için var.

Buffer'ı unutmak. Buffered yazının verisi Dispose ya da Flush çağrılana kadar process'in belleğindedir. using olmadan açılmış bir StreamWriter ile process kapanırsa son satırlar dosyaya hiç ulaşmaz.

Küçük Bir Deneme

★☆☆ WriteBuffered'daki StreamWriter'ı new FileStreamOptions { Mode = FileMode.Append, Access = FileAccess.Write, BufferSize = 65536 } ile oluşturduğun bir FileStream üzerine kur. strace -c ile bin satırda kaç pwrite64 kaldığını say ve 14 ile karşılaştır.

★★☆ read-catalog komutunun yanına bufferSize: 1 ile açılmış bir FileStream üzerinden ReadByte döngüsüyle okuyan bir komut ekle. strace -y -P /app/data/catalog.json çıktısında kaç read ya da pread64 gördüğünü yaz, sonra aynı işi BenchmarkDotNet ile ölç.

★★★ Bir ASP.NET Core Minimal API'yi aynı container'da çalıştır ve strace -f -e trace=%network -p <PID> ile bağlan. Tek bir curl isteği sırasında hangi network syscall'larının çağrıldığını ve isteği hangi thread'in karşıladığını not et. Bu çıktıyı I/O modelleri yazısında yeniden okuyacağız.

Bu yazıda strace çıktısının her satırının başında bir pid vardı ve clone ile yedi yeni thread başladı. Bir sonraki yazıda bu numaranın arkasındaki kavrama bakıyoruz: process. Address space'in içinde neler var, /proc/<pid>/maps bir .NET process'i hakkında ne söyler ve Bookstore harici bir aracı Process.Start ile çağırırken neden kilitlenebilir?