İçeriğe geç
academia.sh

Ders 04 / 15

Süreçler Arası İletişim

Bir boru baytları bir kez tasir ve okunan her bayt akistan duser; mmap ile MAP_SHARED kurulan bir eslem ise ayni sayfayi iki surece birden gosterir ve bu ikisi ayni derste iki farkli sinifa duser.

Yayın Güncelleme

Bu derste

  • boruya yazılan bir değerin ebeveyne bir kez ulaştığını ve ikinci okumanın 0 baytlık dosya sonuyla bittiğini gösterir
  • MAP_SHARED ile kurulan bir eşlemin aynı sayfayı iki sürece birden gösterdiğini, borudaki aktarımla karşılaştırarak ayırt eder
  • bir borunun ileti sınırı taşımadığını, kendi uzunluk önekiyle kurulan bir çözümü çalıştırarak ölçer
İçindekiler

Önceki ders bir sinyalin bıraktığı izi ölçtü: bir bayrak, ana akışa bir kez ulaşan bir aktarım. Bu ders aynı sınıfı iki süreç arasında ölçüyor, ve karşısına kasıtlı olarak farklı bir sınıf koyuyor. Bir boru, çocuğun yazdığı baytları ebeveyne taşır — okunan her bayt akıştan düşer, bu aktarımdır. Bir mmap paylaşımı ise aynı bellek sayfasını iki sürece birden gösterir — biri yazdığında öteki aynı anda görür, bu ortaktır. Aynı derste iki sınıfın yan yana durması, aralarındaki farkı görünür kılıyor.

Boruya Yazılan Değer

gozlem.h ve gozlem.c değişmiyor. Bu dersin kendi dosyası iletisim_olcumu.c, dört ölçümü sırayla koşturuyor: boru, paylaşılan eşlem, iki yönlü bir kanal, ve uzunluk önekiyle ayrılan iki ileti.

// gozlem.h — bir degisikligin kime ulastigini siniflayan kayit
#ifndef GOZLEM_H
#define GOZLEM_H

enum Sinif { KOPYA, ORTAK, AKTARIM, GOZLENMEYEN };

/* Bir degisikligi kaydeder: gordu, degisikligi yapmayan tarafin onu
   okuyup okuyamadigidir (1 ya da 0). */
void gozlem(const char *degisiklik, enum Sinif sinif, int gordu);

/* Sinif basina sayilari ve ulasan / ulasmayan toplamini basar. */
void gozlem_ozeti(void);

#endif
// gozlem.c — sayilar yalniz bu birimde tutulur
#include <stdio.h>
#include "gozlem.h"

static const char *const adlar[] = { "kopya", "ortak", "aktarim", "gozlenmeyen" };
static int sayilar[4];
static int ulasan = 0;
static int ulasmayan = 0;

void gozlem(const char *degisiklik, enum Sinif sinif, int gordu) {
    /* ortak ve aktarim ulasir, kopya ve gozlenmeyen ulasmaz */
    int beklenen = (sinif == ORTAK || sinif == AKTARIM);
    sayilar[sinif]++;
    if (gordu) {
        ulasan++;
    } else {
        ulasmayan++;
    }
    printf("%-34s %-8s %s%s\n", degisiklik, gordu ? "gordu" : "gormedi",
           adlar[sinif], gordu == beklenen ? "" : "  TUTARSIZ");
}

void gozlem_ozeti(void) {
    printf("siniflar: kopya %d, ortak %d, aktarim %d, gozlenmeyen %d\n",
           sayilar[KOPYA], sayilar[ORTAK], sayilar[AKTARIM], sayilar[GOZLENMEYEN]);
    printf("ulasan %d, ulasmayan %d\n", ulasan, ulasmayan);
}
// iletisim_olcumu.c — boru, paylasilan eslem ve iki yonlu kanal
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include "gozlem.h"

/* 1. Boru: cocugun yazdigi deger ebeveyne bir kez ulasir, ikinci okuma dosya sonu verir */
static void boru_olcumu(void) {
    int uc[2];
    if (pipe(uc) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        int deger = 42;
        close(uc[0]);
        ssize_t n = write(uc[1], &deger, sizeof deger);
        close(uc[1]);
        _exit(n == (ssize_t)sizeof deger ? 0 : 1);
    }
    close(uc[1]);
    int okunan = 0;
    ssize_t birinci = read(uc[0], &okunan, sizeof okunan);
    waitpid(p, NULL, 0);
    char kalan;
    ssize_t ikinci = read(uc[0], &kalan, 1);
    close(uc[0]);
    gozlem("boruya yazilan deger", AKTARIM,
           birinci == (ssize_t)sizeof okunan && okunan == 42 && ikinci == 0);
}

/* 2. Paylasilan eslem: cocugun yazdigi sayfayi ebeveyn ayni anda gorur */
static void eslem_olcumu(void) {
    int *pay = mmap(NULL, sizeof *pay, PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (pay == MAP_FAILED) {
        return;
    }
    *pay = 0;
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        *pay = 99;
        _exit(0);
    }
    waitpid(p, NULL, 0);
    gozlem("cocugun yazdigi paylasilan deger", ORTAK, *pay == 99);
    munmap(pay, sizeof *pay);
}

/* 3. Iki yonlu kanal: bir uctan yazilan deger oteki uctan okunur */
static void kanal_olcumu(void) {
    int uc[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, uc) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        close(uc[0]);
        int deger = 7;
        write(uc[1], &deger, sizeof deger);
        int yanit = 0;
        read(uc[1], &yanit, sizeof yanit);
        _exit(yanit == 14 ? 0 : 1);
    }
    close(uc[1]);
    int gelen = 0;
    read(uc[0], &gelen, sizeof gelen);
    int yanit = gelen * 2;
    write(uc[0], &yanit, sizeof yanit);
    int durum = 0;
    waitpid(p, &durum, 0);
    close(uc[0]);
    gozlem("kanaldan gelen yanit", AKTARIM,
           gelen == 7 && WIFEXITED(durum) && WEXITSTATUS(durum) == 0);
}

/* 4. Uzunluk onekiyle ileti siniri: bir borudan gelen iki mesaj birbirine karismadan ayrilir */
static ssize_t tam_oku(int fd, void *arabellek, size_t sayi) {
    size_t okunan = 0;
    while (okunan < sayi) {
        ssize_t n = read(fd, (char *)arabellek + okunan, sayi - okunan);
        if (n <= 0) {
            return n;
        }
        okunan += (size_t)n;
    }
    return (ssize_t)okunan;
}

static void onekli_yaz(int fd, const char *metin) {
    unsigned char uzunluk = (unsigned char)strlen(metin);
    write(fd, &uzunluk, 1);
    write(fd, metin, uzunluk);
}

static void onek_olcumu(void) {
    int uc[2];
    if (pipe(uc) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        close(uc[0]);
        onekli_yaz(uc[1], "ilk");
        onekli_yaz(uc[1], "ikincimesaj");
        close(uc[1]);
        _exit(0);
    }
    close(uc[1]);

    unsigned char uzunluk1 = 0;
    tam_oku(uc[0], &uzunluk1, 1);
    char birinci[32];
    tam_oku(uc[0], birinci, uzunluk1);
    birinci[uzunluk1] = '\0';

    unsigned char uzunluk2 = 0;
    tam_oku(uc[0], &uzunluk2, 1);
    char ikinci[32];
    tam_oku(uc[0], ikinci, uzunluk2);
    ikinci[uzunluk2] = '\0';

    waitpid(p, NULL, 0);
    close(uc[0]);
    gozlem("onekli iki mesajin dogru ayrilmasi", AKTARIM,
           strcmp(birinci, "ilk") == 0 && strcmp(ikinci, "ikincimesaj") == 0);
}

int main(void) {
    boru_olcumu();
    eslem_olcumu();
    kanal_olcumu();
    onek_olcumu();
    gozlem_ozeti();
    return 0;
}
boruya yazilan deger               gordu    aktarim
cocugun yazdigi paylasilan deger   gordu    ortak
kanaldan gelen yanit               gordu    aktarim
onekli iki mesajin dogru ayrilmasi gordu    aktarim
siniflar: kopya 0, ortak 1, aktarim 3, gozlenmeyen 0
ulasan 4, ulasmayan 0

SA12. Dört satır da gordu diyor, ama ikinci satırın sınıfı ötekilerden farklı. Çocuk süreç boruya 42 yazıyor, ebeveyn read ile bunu okuyor — bu bir aktarım: bayt akıştan düşüyor, ikinci read çağrısı artık okunacak bir şey kalmadığı için 0 dönüyor, yani dosya sonu. Bu 0 dönüşü bir hata değil, bir durumdur: readin dönüş değeri normalde okunan bayt sayısıdır, negatifse hata, sıfırsa “yazma ucu kapandı ve arabellekte artık okunacak veri yok” demektir. boru_olcumu çocuk tarafında close(uc[1]) çağırıyor, sonra _exit ediyor — yazma ucunun son kopyası da kapanınca çekirdek okuma ucuna “artık yeni veri gelmeyecek” bilgisini işte bu 0 dönüşüyle taşıyor. Eğer çocuk yazma ucunu kapatmasaydı, ikinci read veri gelene kadar bloke olurdu — 0 dönüşü veri yokluğunun değil, tüm yazıcıların ayrıldığının işareti.

Ama eslem_olcumu farklı çalışıyor: mmap(..., MAP_SHARED | MAP_ANONYMOUS, ...) çağrısı forktan önce bir bellek sayfası ayırıyor ve bu sayfayı paylaşılan olarak işaretliyor. fork süreç belleğini kopyalarken bu özel eşlemi kopyalamıyor — ebeveyn ve çocuk, aynı fiziksel sayfayı gösteren iki ayrı işaretçiye sahip oluyor. Çocuk *pay = 99 yazdığında, ebeveyn waitpidden sonra kendi *payına baktığında da 99 görüyor — çünkü ikisi de aynı sayfaya bakıyor. Bu ortak sınıfın tanımı: bir kayıt gibi bir kez taşınmıyor, sürekli paylaşılıyor. Burada “dosya sonu” kavramının hiçbir karşılığı yok: bir bellek sayfasının “tükenmesi” diye bir şey yok, sayfa süreçlerden biri munmap edene ya da süreç sonlanana kadar orada duruyor.

Boru mu, Paylaşılan Bellek mi

SA13. Bu iki düzeneğin görünürlüğü aynı kelimeyle anlatılabilir — “çocuğun yazdığını ebeveyn görüyor” — ama altındaki mekanizma tamamen farklı. Boru, çekirdeğin tuttuğu bir arabellektir; yazma ve okuma birer sistem çağrısı, ve okunan veri arabellekten silinir. İki süreç asla aynı belleğe bakmaz, çekirdek aradaki taşıyıcıdır. MAP_SHARED ile kurulan eşlem ise gerçekten aynı fiziksel sayfayı gösterir; okuma ve yazma sıradan bellek erişimidir, sistem çağrısı gerekmez, ve veri hiçbir zaman “tükenmez” — sayfa her okunduğunda orada kalır. Bu yüzden borunun ikinci okuması dosya sonu verirken, paylaşılan sayfanın ikinci okuması hâlâ aynı değeri verir; eslem_olcumu bunu denemiyor çünkü fark zaten birinci okumada görünüyor.

Bu karşıtlığın pratik sonucu, hangi düzeneğin ne zaman seçileceğidir. Boru sıralı, tek yönlü bir akış taşıdığı ve çekirdek tarafından senkronize edildiği için “kimin ne zaman ne gönderdiği” sorusuna doğal bir cevap verir — okuyan taraf veri gelene kadar bloke olur, yarışa girmez. MAP_SHARED ise senkronizasyonu vermez: iki süreç aynı sayfaya aynı anda yazarsa sonuç hangi yazmanın önce göründüğüne bağlıdır ve bu ders böyle bir çakışmayı kurmuyor, yalnız tek yönlü bir yazma-sonra-oku sırası kullanıyor. Paylaşılan bellek, sırayı değil yalnız görünürlüğü veriyor — kilitleme ayrı bir sorumluluk, bu konunun son dersinde iş parçacıkları için ayrıca ele alınıyor.

kanal_olcumu, socketpair ile kurulan iki yönlü bir kanalı bir kez gösteriyor. Bu kanal da bir boru gibi çekirdek üzerinden akan, tükenen bir taşıyıcı — aradaki fark tek yönlü değil çift yönlü olması: aynı iki uçtan hem yazılabiliyor hem okunabiliyor. Ders burada ağ soketi açmıyor; AF_UNIX yerel bir uç noktadır, adres ya da bağlantı noktası hiç yazdırılmıyor. Çocuk bir değer gönderiyor, ebeveyn onu ikiyle çarpıp geri yolluyor, çocuk yanıtı okuyup çıkış koduna yazıyor — bu da aktarım, çünkü kanal üzerinden akan her değer bir kez okunup tüketiliyor. pipe ile socketpair arasındaki fark yön sayısıdır, sınıf değil: pipe tek yönlü bir dosya tanıtıcı çifti verir, socketpair her iki tanıtıcıdan da hem okunup hem yazılabilen tam çift yönlü bir kanal verir — ikisi de aynı çekirdek arabellekleme kuralına, aynı tükenme davranışına tabidir.

Bu tabiiyetin somut bir sonucu var: socketpairın iki ucu birbirinden bağımsız iki tanıtıcı gibi görünse de, her iki tanıtıcının da yazma tarafı kapanmadan öteki uç dosya sonu görmez — tıpkı pipeda olduğu gibi. Fark yalnızca kimin kime yazabildiği: pipeda bir uç yalnız yazabilir, öteki yalnız okuyabilir; socketpairda her iki uç da her iki yönde kullanılabilir. Bu yüzden kanal_olcumu çocuğun kanalı hangi sırayla kapattığı konusunda dikkatli olmalı — kendi tanıtıcısını yanıtı gönderdikten sonra kapatmazsa, ebeveyn tarafındaki okuma sonsuza kadar bekleyebilirdi, çünkü çekirdek hâlâ açık bir yazma ucu görür ve dosya sonu üretmez. onek_olcumudaki onekli_yaz çağrılarının ardından close(uc[1]) çağrılması da aynı kuralın bir başka görünümü: ebeveyn tarafı iki iletiyi okuduktan sonra beklemeye devam etmiyor, çünkü çocuk kendi yazma ucunu açıkça kapatıyor.

Bu üç düzenek arasındaki seçim, veri akışının şekline bağlı. Tek yönlü ve sıralı bir akış için pipe en basit araç; hem gönderme hem alma yönü aynı iki uçtan gerekiyorsa socketpair fazladan bir kanal açmayı önlüyor; büyük ya da sık paylaşılan veri için kopyalama maliyetini tamamen ortadan kaldırmak gerekiyorsa MAP_SHARED devreye giriyor, ama o zaman senkronizasyon sorumluluğu tasarımcıya kalıyor.

Sınırlayıcı Ölçüm

SA14. Bir boru bir akıştır, ileti sınırı taşımaz. Önceki üç ölçümdeki her write çağrısı tek bir read ile karşılandı, ama bu garanti genel değil: iki ayrı write çağrısı bir tek read ile birlikte gelebilir, ya da bir writeın verisi birden çok read çağrısına bölünebilir. Boru, gönderilen baytları bir kuyrukta tutar ve nereye bir iletinin bittiğini, ötekinin başladığını bilmez — bu bilgiyi taşıyan hiçbir şey yoktur.

SA15. onek_olcumu bu sınırı gerçekten kuruyor ve gerçekten sınıyor. Çocuk süreç aynı boruya art arda iki ileti yazıyor — "ilk" ve "ikincimesaj" — ama ham metni değil, onekli_yaz her iletinin önüne tek baytlık bir uzunluk alanı koyuyor. Ebeveyn tarafı bu iki iletiyi tek bir read çağrısıyla değil, tam_oku yardımcı işleviyle okuyor: önce uzunluk baytını, sonra tam olarak o kadar veri baytını. tam_okunun kendisi de bir garantiyi telafi ediyor — tek bir read çağrısı istenen baytların hepsini getirmeyebilir, bu yüzden işlev istenen sayıya ulaşana kadar döngüde okumaya devam ediyor. İki iletinin baytları aynı boruda art arda, aralarında hiçbir ayraç olmadan akıyor; okuyan tarafı doğru yerden kesen tek şey, her iletinin kendi taşıdığı uzunluk bilgisi. Ölçüm "ilk" ve "ikincimesaj"ın karışmadan, doğru sınırlarla geri kurulduğunu doğruluyor — bu ders üç iletiyi tek bir okumada gösterdiği için kendi başına bir ileti sınırı çizmiyordu, ama onek_olcumu artık çiziyor: basılan yalnız yeniden kurulmuş iletiler, boruya ham baytlar olarak değil.

onekli_yaz uzunluğu unsigned char olarak taşıyor, yani tek baytlık bir alan — en çok 255 baytlık bir iletiyi tanımlayabiliyor. Bu ölçüm için yeterli, çünkü "ikincimesaj" bu sınırın çok altında; ama gerçek bir protokolde ileti büyüklüğü öngörülemezse tek bayt yetmez, iki ya da dört baytlık bir alan gerekir ve o zaman bayt sırası (endianness) sorusu araya girer — gönderen ve alan makine aynı sırayla mı yazıp okuyor. Bu ders o soruyu açmıyor, çünkü boru her iki ucu da aynı makinede çalıştırıyor; ağ üzerinden konuşan bir protokolde uzunluk alanının sırası açıkça sabitlenmeli, aksi hâlde bir uçta doğru okunan sayı öteki uçta yanlış çıkar. Önemli olan kural değişmiyor: uzunluk alanı ne kadar büyük olursa olsun, onu taşıyan düzenek aynı — önce ne kadar veri geleceğini oku, sonra tam o kadarını oku.

tam_okunun döngüsü kozmetik değil, boru semantiğinin doğrudan sonucu. Çekirdek bir write çağrısındaki baytları tek bir birim olarak teslim etmeyi garanti etmez; okuyan taraf read çağırdığında çekirdek o an boru arabelleğinde ne kadar bayt birikmişse onu döndürür, istenenden azını döndürebilir. boru_olcumu ve eslem_olcumu ölçümlerinde bu hiç görünmedi, çünkü yazılan değerler birkaç bayt uzunluğundaydı ve tek read çağrısı hep yetti; ama genel kural bu değil. tam_oku istenen sayıya ulaşılana kadar okumaya devam ederek bu garantisizliği bir kez, tek bir yerde telafi ediyor — çağıran taraf artık “kaç kez read gerekti” sorusuyla uğraşmıyor, tam_okunun kendisi kısa okumaları görünmez kılıyor. n <= 0 dönen anı ayrıca kontrol ediyor, çünkü 0 boru kapandığını, negatif değer ise bir hatayı gösteriyor; ikisi de “tam sayıda bayt bekleniyordu ama gelmedi” durumudur ve döngü bu durumda sonsuza kadar dönmek yerine hemen çıkıyor.

Özet

  • Bu ders iki farklı sınıfı yan yana koyuyor: boru aktarımdır, MAP_SHARED eşlemi ortaktır — ikisi de “çocuğun yazdığını ebeveyn görür” cümlesiyle anlatılsa da mekanizmaları ayrı.
  • Boru çekirdek üzerinden akan, okunduğunda tükenen bir taşıyıcıdır; ikinci okuma 0 bayt döner, çünkü tüm yazma uçları kapanmıştır — bu veri yokluğu değil, yazıcıların ayrıldığının işaretidir.
  • MAP_SHARED | MAP_ANONYMOUS ile kurulan bir eşlem aynı fiziksel sayfayı iki sürece birden gösterir; bir taraf yazdığında öteki taraf aynı sayfayı okur, veri tükenmez — ama sıralamayı değil yalnız görünürlüğü verir, kilitleme ayrı bir sorumluluktur.
  • socketpair iki yönlü bir kanal kurar; bu da bir aktarımdır, çünkü akan her değer bir kez okunup tüketilir. Ağ soketi açılmaz, adres ya da bağlantı noktası basılmaz.
  • Bir boru ileti sınırı taşımaz; onek_olcumu bunu uzunluk önekiyle gerçekten kurup iki iletiyi karıştırmadan geri kurarak gösteriyor.

Sonraki Adım

Bir sonraki ders — bu konunun sonuncusu — aynı süreç içinde kalıyor ama süreç yerine iş parçacığına geçiyor: kilit altında yazılan bir sayaç, join sonrasında ana iş parçacığına ulaşan bir ortak paylaşım.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat