TEKNİK YAZI / Backend Sistemleri

Sistem Programlama Nedir?

Sistem programlamanın süreç, bellek, dosya tanıtıcısı ve işletim sistemi API'leriyle ilişkisini; güvenli bir C başlangıcı üzerinden öğrenin.

Sistem programlama, uygulama ile işletim sistemi veya donanım arasındaki sınırda çalışan yazılımları geliştirir. Süreçler, bellek, dosya sistemleri, ağ soketleri, aygıtlar ve eşzamanlılık bu alanın temel konularıdır. C sık kullanılan dillerden biridir; fakat sistem programlama yalnızca C sözdizimi öğrenmekten ibaret değildir.

Uygulama programlamadan farkı

Yüksek seviyeli bir uygulamada çalışma zamanı (runtime) birçok ayrıntıyı yönetebilir. Sistem katmanında ise şu sorular doğrudan görünür hâle gelir:

Bu nedenle doğruluk kadar hata kodu, kaynak yaşam döngüsü ve sınır kontrolü de tasarımın parçasıdır.

C geliştirme ortamı

Debian/Ubuntu tabanlı bir ortamda derleyici ve temel araçlar şu paketle kurulabilir. Komutlar sudo ile yönetici yetkisi alır ve sistem paketlerini değiştirir; kendi geliştirme makinenizde ya da bir sanal makinede çalıştırın:

sudo apt update
sudo apt install build-essential
gcc --version

İlk program:

#include <stdio.h>

int main(void) {
    puts("Merhaba, sistem programlama");
    return 0;
}

Uyarıları açarak derleyin:

gcc -std=c17 -Wall -Wextra -Wpedantic -O2 main.c -o main
./main

Uyarıları kapatmak yerine nedenini anlayın. Derleyici uyarısı her zaman hata değildir; ancak tür dönüşümü, başlatılmamış değer veya yanlış format belirteci gibi gerçek kusurları erken gösterebilir. Kaynak dosyası sayısı arttığında bu derleme komutlarını elle yazmak yerine bir Makefile ile yalnız değişen dosyaları yeniden derlemek pratik olur.

Sistem çağrısı ve kütüphane fonksiyonu

C standart kütüphanesi fopen ve printf gibi taşınabilir yüzeyler sunar. POSIX sistem çağrıları ve onları saran ince kütüphane fonksiyonları ise open, read, write, fork ve exec ailesi gibi arayüzlerle işletim sistemi kaynaklarına daha doğrudan erişir. exec tek bir fonksiyon değildir: Linux’taki sistem çağrısı execve’dir; execl, execlp, execv ve execvp gibi fonksiyonlar onun üzerine kurulu kütüphane arayüzleridir ve çalışan sürecin programını yenisiyle değiştirir.

Basit bir dosya okuma örneği:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void) {
    char buffer[4096];
    int fd = open("example.txt", O_RDONLY);

    if (fd == -1) {
        fprintf(stderr, "open: %s\n", strerror(errno));
        return 1;
    }

    ssize_t count = read(fd, buffer, sizeof buffer);
    if (count == -1) {
        fprintf(stderr, "read: %s\n", strerror(errno));
        close(fd);
        return 1;
    }

    if (write(STDOUT_FILENO, buffer, (size_t)count) == -1) {
        fprintf(stderr, "write: %s\n", strerror(errno));
        close(fd);
        return 1;
    }

    if (close(fd) == -1) {
        fprintf(stderr, "close: %s\n", strerror(errno));
        return 1;
    }

    return 0;
}

Bu kısa örnekte bile her çağrının başarısız olabileceği, dosya tanıtıcısının kapatılması ve byte sayısının doğru türle taşınması gerekir. Gerçek uygulamada read ve write kısmi sonuç döndürebileceğinden döngü ve EINTR davranışı ayrıca ele alınır.

Bellek ve sınırlar

Dinamik bellek kullanırken tahsis sonucu ve boyut hesabı kontrol edilmelidir:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int *allocate_values(size_t count) {
    // count * sizeof(int) taşarsa malloc'a beklenenden küçük bir boyut gider
    if (count > SIZE_MAX / sizeof(int)) {
        return NULL;
    }

    return malloc(count * sizeof(int));
}

int main(void) {
    int *values = allocate_values(100);
    if (values == NULL) {
        fputs("allocation failed\n", stderr);
        return 1;
    }

    values[0] = 42;
    free(values);
    return 0;
}

malloc boyutu tek bir sayı olarak alır; çarpımı kod hesapladığı için taşma kontrolü de koda düşer. Kontrol yapılmazsa çok büyük bir count değerinde count * sizeof(int) çarpımı taşarak küçük bir sayıya döner, küçük bir alan ayrılır ve sonraki yazmalar sınır dışına çıkar. calloc(count, sizeof(int)) bu çarpımı kendisi denetler ve taşma durumunda hata döndürür; ayrıca ayırdığı belleği sıfırlar. Var olan bir diziyi büyütürken de aynı denetimi yapan reallocarray (glibc 2.26 ve sonrası, POSIX.1-2024) kullanılabilir. AddressSanitizer ve UndefinedBehaviorSanitizer gibi araçlar geliştirme sırasında bu hata sınıflarını görünür kılmaya yardımcı olur:

gcc -std=c17 -Wall -Wextra -fsanitize=address,undefined -g main.c -o main

Öğrenme sırası

Sistem programlama için yararlı bir sıra:

  1. C türleri, pointer, dizi ve kaynak yaşam döngüsü
  2. dosya tanıtıcıları ve POSIX I/O
  3. süreçler, sinyaller, fork ve exec ailesi
  4. sanal bellek ve bellek tahsisi
  5. iş parçacıkları, mutex ve yarış koşulları
  6. TCP/UDP soketleri
  7. hata ayıklama, profiler ve sanitizer araçları

Her adımda yalnızca başarılı yolu değil, sistem çağrısının kısmi veya hatalı sonucunu da test edin.

Sonuç

Sistem programlama, makineye yakın kod yazmaktan önce kaynakların ve hata durumlarının açık sahipliğini kurma disiplinidir. Küçük programlarla başlayın, tüm dönüş değerlerini kontrol edin, uyarıları açık tutun ve bellek hatalarını araçlarla doğrulayın. Bu temel, daemon, ağ servisi, veritabanı motoru veya gömülü yazılım gibi daha büyük sistemleri anlamayı kolaylaştırır.

Birincil kaynaklar