Ders 02 / 15
Süreç Yönetimi
fork bir süreci ikiye ayırır ve o anki belleğin bir kopyasını çocuğa verir; kopya kurulduktan sonra iki taraf birbirinden bağımsızdır, hiçbir değişiklik öteki tarafa hiçbir yönde ulaşmaz.
Yayın Güncelleme
Bu derste
- fork sonrası iki sürecin aynı değişkeni bağımsız kopyalar olarak tuttuğunu, iki yönü de ölçerek gösterir
- fflush(stdout) çağrısının fork'tan önce zorunlu olduğu kuralı, tamponlanmış bir yapının kopyalanma riskini ölçerek açıklar
- exec ailesinin süreç görüntüsünü neden değiştirdiğini, kodu koşturmadan düzyazıyla anlatır
İçindekiler
Önceki ders dup ile türetilen bir tanıtıcının aynı açık dosya kaydını paylaştığını
gösterdi: biri konumu değiştirdiğinde öteki de gördü. fork bunun tam tersini yapar.
Çağrıldığı anda süreç belleğinin bir kopyasını çocuğa verir, ve o kopya kurulduktan
sonra iki taraf birbirinden tamamen bağımsızdır. Kurstaki dört sınıftan biri olan
kopya, tam olarak bunu tanımlıyor: değişiklik yalnız onu yapan tarafta kalır,
öteki taraf hiçbir zaman görmez. Bu ders bu bağımsızlığı iki yönde, iki farklı
fork çağrısında ve iki farklı veri türünde ölçüyor: önce iki düz tamsayı, sonra
çıkış durumunu okuyan makrolar üzerinden, en sonunda da standart çıktının
tamponunu taklit eden küçük bir yapı üzerinden.
İki Yönlü Kopya Ölçümü
Ölçüm birimleri değişmiyor: gozlem.h ve gozlem.c, önceki dersteki içerikleriyle
aynı. Bu dosyadaki yeni parça, iki fork çağrısını art arda kullanan
surec_olcumu.c: ilki iki düz tamsayıyı iki yönde ölçüyor, ikincisi standart
çıktının tamponunu taklit eden küçük bir yapıyı aynı şekilde ölçüyor.
// 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);
}
// surec_olcumu.c — iki ayri fork, iki ayri veri turu, ayni kopyalanma kurali
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "gozlem.h"
/* Model: gercek stdio tamponu degil, ayni kopyalanma kuralini izleyen kucuk bir yapi */
typedef struct {
char veri[32];
int uzunluk;
} ModelTampon;
static void model_yaz(ModelTampon *t, const char *s) {
int n = 0;
while (s[n] != '\0' && t->uzunluk < 31) {
t->veri[t->uzunluk] = s[n];
t->uzunluk++;
n++;
}
}
int main(void) {
int deger_a = 0;
int deger_b = 0;
fflush(stdout);
pid_t p = fork();
if (p == 0) {
deger_a = 1; /* cocugun kendi kopyasinda degisir */
_exit(deger_b); /* ebeveynin sonraki degisikligini gormeden cikar */
}
deger_b = 7; /* fork sonrasi yalniz ebeveynin kopyasinda degisir */
int durum = 0;
waitpid(p, &durum, 0);
gozlem("fork sonrasi cocugun degiskeni", KOPYA, deger_a == 1);
gozlem("fork sonrasi ebeveynin degiskeni", KOPYA, WEXITSTATUS(durum) == 7);
printf("cocugun normal yoldan cikisi (WIFEXITED): %s\n", WIFEXITED(durum) ? "evet" : "hayir");
/* ikinci fork: model tampon da ayni kurala uyuyor mu */
ModelTampon tampon;
memset(&tampon, 0, sizeof tampon);
model_yaz(&tampon, "onceki"); /* fork oncesi birikmis, henuz bosaltilmamis icerik modeli */
fflush(stdout);
pid_t p2 = fork();
if (p2 == 0) {
model_yaz(&tampon, "cocuk");
_exit(tampon.uzunluk);
}
model_yaz(&tampon, "ebeveyn");
int durum2 = 0;
waitpid(p2, &durum2, 0);
gozlem("cocugun tampona eklemesi ebeveyne ulasti mi", KOPYA,
WEXITSTATUS(durum2) == tampon.uzunluk);
gozlem_ozeti();
return 0;
}
fork sonrasi cocugun degiskeni gormedi kopya fork sonrasi ebeveynin degiskeni gormedi kopya cocugun normal yoldan cikisi (WIFEXITED): evet cocugun tampona eklemesi ebeveyne ulasti mi gormedi kopya siniflar: kopya 3, ortak 0, aktarim 0, gozlenmeyen 0 ulasan 0, ulasmayan 3
SA3. İlk iki satır da gormedi diyor. Çocuk deger_a’yı kendi kopyasında 1
yapıyor; ebeveyn waitpid’den sonra kendi deger_a’sına bakıyor ve hâlâ 0
buluyor — çocuğun yazması hiçbir zaman ebeveynin kopyasına ulaşmadı. Simetrik
yönde, ebeveyn fork döndükten sonra kendi deger_b’sini 7 yapıyor; çocuk bu
satırı hiç görmeden _exit(deger_b) ile çıkıyor, ve kendi kopyasındaki deger_b
hâlâ 0 olduğu için çıkış kodu da 0. Ebeveyn bunu WEXITSTATUS ile okuyup 7
değil 0 bulduğunda, kendi değişikliğinin çocuğa hiç ulaşmadığını doğruluyor.
İkisi de aynı sınıfa düşüyor: kopya. fork bir kere kopyalar, sonra iki taraf
birbirinden habersizdir — kim önce, kim sonra yazdığının burada hiçbir önemi yok,
çünkü paylaşılan bir kayıt yok, iki bağımsız kopya var.
fflush Kuralının Ölçümü
SA4. Üçüncü satır bu bağımsızlığı bir tamsayıdan çıkarıp çok satırlı bir
tampona taşıyor. ModelTampon, gerçek stdio tamponunun yerini tutan bir model:
forktan önce "onceki" metnini biriktiriyor, tıpkı boşaltılmamış bir çıktı
satırının terminale yazılmayı beklemesi gibi. fork çağrıldığında bu yapı da
belleğin geri kalanıyla birlikte kopyalanıyor — çocuk kendi kopyasına "cocuk"
ekliyor ve uzunluğunu çıkış koduyla bildiriyor, ebeveyn kendi kopyasına "ebeveyn"
ekliyor. "cocuk" beş, "ebeveyn" yedi karakter olduğu için iki tarafın son
uzunluğu hiçbir zaman eşit çıkmıyor, ve ölçüm bunu gormedi olarak okuyor: çocuğun
kendi tamponuna yaptığı ekleme ebeveynin tamponuna hiç ulaşmadı. Kurstaki her
derste geçerli olan kural tam olarak bu ölçümün gösterdiği şey: fork’tan önceki
fflush(stdout) çağrısı, gerçek stdio tamponunun içindeki boşaltılmamış içeriği
sıfırlar, böylece kopyalanacak bir şey kalmaz. Bu satır atlanırsa, "onceki" gibi
henüz yazılmamış bir içerik hem ebeveynin hem çocuğun kopyasında birlikte durur;
ikisi de kendi kapanışında kendi tamponunu boşaltırsa aynı satır iki kez
görünür — bir kez ebeveynden, bir kez çocuktan, çünkü artık ikisi de aynı sınıfa,
yani kopya’ya ait ayrı birer nüsha taşıyor. surec_olcumu.c bu riski iki kez
kapatıyor: her iki forktan hemen önce fflush(stdout) çağrılıyor, ve gozlem
çağrılarının kendisi yalnız waitpid sonrası ebeveyn tarafında koşuyor; çocuk
hiçbir zaman yazdırmıyor, _exit ile sessizce çıkıyor.
Çıkış Durumu Makroları
SA5. waitpid’in doldurduğu durum tek bir tamsayı ama içinde iki ayrı soru
kodlanmış: süreç nasıl bitti, ve hangi değerle. WEXITSTATUS(durum) yalnız ikinci
soruyu yanıtlıyor — çocuğun _exit’e ya da return’e verdiği değeri döndürüyor —
ama bu makro yalnız birinci soru olumluysa, yani çocuk gerçekten normal yoldan
çıktıysa anlamlı. WIFEXITED(durum) birinci soruyu yanıtlıyor: çocuğun bir
sinyalle değil, kendi isteğiyle sonlandığını doğruluyor. Bu ders WIFEXITED’i
her iki forktan sonra da doğru buluyor, çünkü iki çocuk da _exit ile çıkıyor;
gelecek dersin sinyal konusu bu makronun karşıtını, yani bir sinyalle sonlanan
bir süreç görene kadar bu ayrım soyut kalır. Buradaki nokta şu: WEXITSTATUS’a
WIFEXITED kontrolü olmadan güvenmek yanıltıcı olabilir, çünkü sinyalle
sonlanan bir sürecin durum alanı aynı bitleri başka bir amaçla taşır — bu ders
bunu koşturmuyor, ama makronun neden yalnız başına yeterli olmadığını burada
kaydediyor.
WEXITSTATUS’un döndürdüğü değer sekiz bitle sınırlıdır, çünkü POSIX çıkış
kodunu bu genişlikte taşımayı garanti eder; surec_olcumu.c içindeki iki ölçüm
de bu sınırın altında kalacak şekilde seçildi — deger_b yedi, model tamponun
uzunluğu otuz birin altında. Bir programın _exit’e üç haneli ya da negatif bir
değer verip beklentisiyle çıkış kodunu okuması, bu sekiz bitlik kesilme yüzünden
yanıltıcı sonuç üretebilir; bu ders o durumu ayrıca ölçmüyor, ama gozlem
çağrılarının küçük sabit değerler seçmesinin nedeni tam olarak bu sınırdan
kaçınmak.
WIFEXITED’in karşıtı WIFSIGNALED’dir: çocuk bir sinyal yüzünden sonlandıysa
bu makro doğru döner, ve o durumda WTERMSIG(durum) hangi sinyalin sonlandırdığını
okur. Bu ders sinyal numarası ya da adı yazdırmayı yasaklayan kural gereği bu
makroyu koşturmuyor — ne yazdırabileceği bir değer üretir ne de bu dersin ölçüm
eksenine bir şey katar, çünkü sinyalle sonlanan bir sürecin çıkış kodu diye bir
şey yoktur, yalnız sonlandıran sinyal vardır. Bir programın waitpid sonrası
hem WIFEXITED hem WIFSIGNALED’i kontrol etmeden WEXITSTATUS’a güvenmesi,
sinyalle sonlanan bir çocukta tanımsız bir değeri çıkış koduymuş gibi okumasına
yol açar; bu yüzden ikisi arasındaki ayrım yalnız bir stil tercihi değil, doğru
okumanın önkoşuludur.
exec Ailesi
SA6. forktan sonra çocuk sürecinde çoğunlukla ikinci bir adım gelir: exec
ailesinden bir çağrı. fork belleği kopyalarken program kodunu değiştirmez —
çocuk hâlâ aynı programın bir kopyasıdır. exec ise süreç kimliğini korur ama
süreç görüntüsünü baştan kurar: kod, veri ve yığıt yeni bir programınkiyle
değiştirilir, ve çağrı başarılı olduğunda geri dönmez. Bu ders exec’i
koşturmuyor, çünkü ölçtüğü soru — kim neyi görüyor — exec sonrasında anlamını
yitirir: eski sürecin değişkenleri, deger_a’sı, deger_b’si, model tamponu
artık yok, yerlerine yenisi geldi. fork ve exec’in birlikte kullanımı, bir
kabuğun yeni bir program başlatmasının temelidir: kabuk önce fork ile kendi
kopyasını üretir, çocuk süreç ortam değişkenlerini ve açık tanımlayıcıları kalıtım
yoluyla devralır, sonra exec ile o kopyanın üzerine istenen programı yükler.
Ebeveyn kabuk hiçbir zaman değişmez; yalnız çocuk exec çağırır, ve başarılı
olduğunda artık kabuk değil, çağrılan programdır.
exec ailesinin üyeleri birbirinden üç eksende ayrılır, hepsi düzyazıyla:
argümanları nasıl aldıkları (execl gibi değişken sayıda argümanla çağrılanlar,
execv gibi bir dizi alanlar), programı nasıl buldukları (execve yalnız tam
yolu kabul eder, execvp PATH ortam değişkeninde arama yapar) ve ortamı nasıl
devraldıkları (execve ve execvpe yeni bir ortam dizisi kabul eder, diğerleri
çağıran sürecin ortamını olduğu gibi kullanır). Hepsinin ortak noktası dönüş
değeri yoktur — çağrı başarılıysa geri dönmez, başarısızsa -1 döner ve çağıran
süreç kendi değişkenleriyle devam eder. fork olmadan çağrılan bir exec,
çağıran süreci geri dönülmez biçimde başka bir programa çevirir; bu yüzden bir
kabuğun kendi sürecini kaybetmeden yeni bir program başlatması, fork ile önce
bir kopya üretip execi o kopyada çağırmasına bağlıdır. Açık dosya tanımlayıcıları
varsayılan olarak exec sonrasında da açık kalır — FD_CLOEXEC bayrağı
işaretlenmediği sürece çocuğun forktan miras aldığı tanımlayıcılar yeni
programda da geçerlidir; bir kabuğun yönlendirme (>, <) sözdizimini
gerçeklemesi tam olarak buna dayanır: kabuk önce tanımlayıcıyı dup2 ile istenen
konuma taşır, sonra exec çağırır, ve yeni program o tanımlayıcının nereden
geldiğini hiç bilmeden kullanır.
forkun bellek kopyasını nasıl ürettiği bu dersin konusu değil; işletim sistemleri
kursunun bellek yönetimi bölümü bu soruyu zaten yanıtladı — çekirdek çoğu zaman
sayfaları gerçekten çoğaltmaz, yazma-üzerine-kopyala düzeniyle işaretler ve gerçek
kopya yalnız bir taraf yazdığında oluşur. Bu ders o iç işleyişe dokunmuyor; ölçtüğü
tek şey, kopyalama hangi düzenekle yapılırsa yapılsın iki tarafın sonuçta bağımsız
görünmesi — ister bir tamsayı, ister çok satırlı bir tampon olsun.
ModelTampon ölçümü gerçek bir stdio tamponunun dahili yapısını taklit
etmiyor; yalnız aynı kopyalanma kuralına uyan, kendi tanımladığımız küçük bir
yapı. Buradaki genellik önemli: gerçek FILE* tamponu içinde tutulan bayt
dizisi de, bu dersin başka yerinde ölçtüğü bir tamsayı değişkeni de, fork
anında aynı kopyalama kuralına tabidir — çocuk ebeveynin o anki durumunu alır,
sonrasında ikisi bağımsız değişir. ModelTampon bu genelliği somutlaştırıyor:
üç bayt uzunluğundaki bir sayaç ile otuz iki baytlık bir dizi, fork açısından
eşit muamele görür. Gerçek stdio’da fark, tamponun ne zaman boşaldığını
programcının değil kütüphanenin kararının belirlemesi; satır tamponlu bir
terminalde her satır sonunda, tamamen tamponlu bir dosyada yalnız tampon
dolduğunda ya da program kapanırken boşalır. fork bu boşalma zamanlamasını
değiştirmez, yalnız o ana kadar birikmiş içeriği iki bağımsız kopyaya böler;
kuralı ihlal eden fflush(stdout) çağrısının eksikliği değil, birikmiş
içeriğin iki kez boşalacağı beklentisidir.
Sınırlayıcı Ölçüm
Bu ölçümün sınırı zamanlamadan bağımsız olmasıdır, ve bu kasıtlı bir tasarım. Çocuğun
_exit çağrısı ile ebeveynin sonraki yazması hangi sırayla çalışırsa çalışsın
sonuç değişmez, çünkü ikisi aynı belleğe yazmıyor — her biri kendi kopyasına
yazıyor. Bu yüzden ölçüm bir sleep ya da bekleme gerektirmiyor; waitpid
çağrısının kendisi yeterli, çünkü sıralamayı değil kopyaların bağımsızlığını
doğruluyor. Model tampon ölçümü de aynı sınırı taşıyor: "cocuk" ile "ebeveyn"
metinlerinin farklı uzunlukta seçilmesi rastgele değil — sonucun her koşumda
aynı çıkması için kasıtlı, çünkü bu ders bir yarış koşulunu değil yalnız
kopyalanmanın varlığını ölçüyor. Kurstaki diğer sınıflar — özellikle aktarım —
sıralamaya duyarlı olabilir, ama kopya sınıfı için sıralama hiçbir zaman sonucu
değiştirmez.
Bu ölçümün kapsamadığı bir başka soru, kaç kez fork çağrıldığında sonucun nasıl
değiştiğidir. surec_olcumu.c iki forku art arda, ilkinin çocuğu tamamen
kapandıktan sonra çağırıyor — ikinci fork birinci çocuğun süreç tablosundan
düşmesini beklemiyor, çünkü waitpid zaten birincisini topladı. Bir program bunun
yerine iki çocuğu aynı anda, ikincisini birincisinin bitişini beklemeden
başlatsaydı, iki çocuk da aynı ebeveyn belleğinin ayrı birer kopyasını taşırdı;
kopya sınıfının garantisi çocuk sayısından bağımsızdır, çünkü her fork çağrısı
kendi başına yeni ve bağımsız bir kopya üretir, önceki çocuklardan habersiz. Bu
ders tek seferde birden çok çocuk üretmiyor, çünkü sonucun gösterdiği şey zaten
tek bir çift üzerinden tam olarak kanıtlanıyor; ekstra çocuklar aynı garantiyi
tekrarlardı, yeni bir sınıf ya da yeni bir sınır getirmezdi.
Özet
fork, çağrıldığı andaki bellek durumunun bir kopyasını çocuğa verir; kopya kurulduktan sonra iki taraf bağımsızdır.- Bu bağımsızlık iki yönlüdür: çocuğun yazdığı bir değişken ebeveyne ulaşmaz,
ebeveynin
forksonrası yazdığı bir değişken de çocuğa ulaşmaz. - Aynı bağımsızlık tek bir tamsayıyla sınırlı değil; çok satırlı bir tampon gibi daha büyük bir yapı da aynı kurala uyar.
fflush(stdout),fork’tan önce zorunludur; aksi hâlde tamponlanmış içerik kopyalanır ve aynı satır iki kez yazılabilir.WIFEXITEDveWEXITSTATUSbirlikte okunur: biri sürecin nasıl bittiğini, öteki hangi değerle bittiğini söyler; biri olmadan öteki yanıltıcı olabilir.execailesi süreç görüntüsünü baştan kurar ve geri dönmez; bu ders onu koşturmuyor, çünkü kopyalanan değişkenlerin sorusuexecsonrasında anlamsızlaşıyor.
Sonraki Adım
Bir sonraki ders forkun kopyaladığı değişkenlerden bambaşka bir taşıma biçimine
geçiyor: bir sinyal işleyicisinin kaldırdığı bir bayrak, ana akışa yalnız bir kez
ulaşan bir aktarım.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.