İçeriğe geç
academia.sh

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 fork sonrası 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.
  • WIFEXITED ve WEXITSTATUS birlikte okunur: biri sürecin nasıl bittiğini, öteki hangi değerle bittiğini söyler; biri olmadan öteki yanıltıcı olabilir.
  • exec ailesi süreç görüntüsünü baştan kurar ve geri dönmez; bu ders onu koşturmuyor, çünkü kopyalanan değişkenlerin sorusu exec sonrası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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat