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], °er, 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], °er, 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_SHAREDeş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
0bayt 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_ANONYMOUSile 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.socketpairiki 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_olcumubunu 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.