waitid ve waitpid Sistem Çağrıları ile Süreç Tamamlanma Durumunu İzleme ve Yönetme

Süreç yönetimi deyince akla ilk gelen şeyler genellikle ps, top, ya da kill gibi araçlar olur. Ama işin mutfağında, yani çekirdek seviyesinde, süreçlerin nasıl başladığı ve nasıl bittiği meselesi çok daha derin bir konu. waitid ve waitpid sistem çağrıları, bu derinliğin tam ortasında duruyor. Özellikle daemon yazıyorsanız, özel bir süreç yöneticisi geliştiriyorsanız ya da zombie süreç sorunlarıyla boğuşuyorsanız, bu iki sistem çağrısını kavramak hem sorunlarınızı anlamamızı hem de çözümleri doğru inşa etmenizi sağlıyor.

Temel Kavramlar: Neden wait() Gerekli?

Linux’ta bir süreç fork() ile çocuk süreç oluşturduğunda, çocuk süreç işini bitirip exit() çağırdıktan sonra hemen yok olmaz. Çekirdek, o sürecin çıkış durumunu (exit status) ebeveyn süreç tarafından okunana kadar process table’da tutar. Bu aşamaya “zombie” denir. Ebeveyn süreç bu durumu okumadıkça, o giriş orada kalır.

İşte wait ailesi tam burada devreye giriyor. waitpid ve waitid, ebeveyn sürecin çocuklarının durumunu sorgulamasını, beklenmesini ve zombie temizlenmesini sağlıyor.

Bunu bir örnekle görelim. Aşağıdaki C kodu kasıtlı olarak wait çağrısı yapmadan çocuk süreç oluşturuyor:

# Zombie sürecin nasıl oluştuğunu görmek için basit bir test
cat > /tmp/zombie_test.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // Cocuk surecin hemen cikmasi
        exit(42);
    }
    // Ebeveyn 30 saniye bekliyor ama wait() cagirmiyor
    printf("Ebeveyn PID: %d, Cocuk PID: %dn", getpid(), pid);
    sleep(30);
    return 0;
}
EOF

gcc -o /tmp/zombie_test /tmp/zombie_test.c
/tmp/zombie_test &

# Baska bir terminalde zombie'yi gormek icin:
ps aux | grep -E 'Z|zombie'

Bu testi çalıştırdığınızda, çocuk sürecin Z (zombie) durumunda göründüğünü fark edeceksiniz. Production ortamlarında binlerce zombie biriken servislerde ben bunu çok yaşadım, özellikle eski CGI tabanlı uygulamalarda.

waitpid: Kontrollü Süreç Bekleme

waitpid, wait() fonksiyonunun daha gelişmiş versiyonu. Hangi çocuk süreci beklediğinizi, nasıl beklediğinizi kontrol altına almanızı sağlıyor.

Fonksiyon imzası şöyle:

# strace ile waitpid sistem cagrisini gormek
strace -e trace=wait4 ls /tmp 2>&1 | head -20

# Genel kullanim formatini anlamak icin man sayfasi:
man 2 waitpid

waitpid‘in pid parametresi birkaç farklı değer alabilir:

  • pid > 0: Tam olarak o PID’e sahip çocuk süreci bekle
  • pid == -1: Herhangi bir çocuk süreci bekle (klasik wait() davranışı)
  • pid == 0: Çağıran süreçle aynı process group’taki herhangi bir çocuğu bekle
  • pid < -1: Mutlak değeri verilen process group ID’sindeki herhangi bir çocuğu bekle

waitpid‘in options parametresi ise davranışı şekillendiriyor:

  • WNOHANG: Beklenecek çocuk hazır değilse hemen dön, bloklanma
  • WUNTRACED: Durdurulmuş (stopped) çocukları da raporla
  • WCONTINUED: SIGCONT ile devam ettirilen çocukları da raporla

Şimdi bunu pratik bir senaryo ile kullanalım. Diyelim ki birden fazla paralel iş başlatıp hepsinin tamamlanmasını beklemeniz gerekiyor:

# Bash'te waitpid benzeri davranis gosteren bir script
# Paralel isler baslatip hepsini takip etme

#!/bin/bash
declare -a PIDS=()
declare -a JOB_NAMES=()

# 5 paralel is baslatiyoruz
for i in $(seq 1 5); do
    sleep $((RANDOM % 5 + 1)) &
    PIDS+=($!)
    JOB_NAMES+=("Job-$i")
    echo "Baslatildi: Job-$i (PID: $!)"
done

# Her süreci ayrı ayrı bekle ve durumunu kontrol et
for idx in "${!PIDS[@]}"; do
    pid=${PIDS[$idx]}
    name=${JOB_NAMES[$idx]}
    
    wait $pid
    exit_code=$?
    
    if [ $exit_code -eq 0 ]; then
        echo "BASARILI: $name (PID: $pid) tamamlandi"
    else
        echo "HATA: $name (PID: $pid) hata kodu: $exit_code"
    fi
done

echo "Tum isler tamamlandi"

waitid: Daha Hassas Kontrol Mekanizması

waitid, waitpid‘den daha yeni ve daha esnek bir arayüz sunuyor. POSIX.1-2001 ile geldi. Temel fark, döndürdüğü bilginin çok daha zengin olması.

waitid sistem çağrısının parametreleri:

  • idtype: Ne tür bir ID kullandığımızı belirtiyor

P_PID: Belirli bir süreç ID’si – P_PGID: Belirli bir process group ID’si – P_ALL: Herhangi bir çocuk süreç

  • id: idtype’a göre değişen ID değeri
  • infop: siginfo_t yapısı, sonuç buraya yazılıyor
  • options: Davranış bayrakları

waitid‘in options değerleri:

  • WEXITED: Normal çıkış yapan süreçleri bekle
  • WSTOPPED: Durdurulmuş süreçleri bekle
  • WCONTINUED: Devam eden süreçleri bekle
  • WNOHANG: Bloklanmadan dön
  • WNOWAIT: Süreci zombie durumundan çıkarma, sadece bilgiyi al

siginfo_t yapısı sayesinde çok daha fazla bilgiye erişebilirsiniz. Örneğin hangi sinyalle öldürüldüğünü, hangi exit kodunu döndürdüğünü, hatta bazı sistemlerde CPU kullanımını bile öğrenebilirsiniz.

# waitid kullanan C programini test etme
cat > /tmp/waitid_demo.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>

void check_child(pid_t child_pid) {
    siginfo_t info;
    memset(&info, 0, sizeof(info));
    
    int ret = waitid(P_PID, child_pid, &info, WEXITED | WSTOPPED);
    
    if (ret == -1) {
        perror("waitid hatasi");
        return;
    }
    
    printf("Cocuk PID: %dn", info.si_pid);
    printf("Kullanici ID: %dn", info.si_uid);
    
    switch(info.si_code) {
        case CLD_EXITED:
            printf("Normal cikis, kod: %dn", info.si_status);
            break;
        case CLD_KILLED:
            printf("Sinyal ile olduruldu: %d (%s)n", 
                   info.si_status, strsignal(info.si_status));
            break;
        case CLD_STOPPED:
            printf("Durduruldu, sinyal: %dn", info.si_status);
            break;
        default:
            printf("Bilinmeyen durum: %dn", info.si_code);
    }
}

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        sleep(1);
        exit(37);  // Ozel cikis kodu
    }
    
    printf("Cocuk (PID: %d) bekleniyor...n", pid);
    check_child(pid);
    
    return 0;
}
EOF

gcc -o /tmp/waitid_demo /tmp/waitid_demo.c
/tmp/waitid_demo

Gerçek Dünya Senaryosu: Basit Bir Process Pool

Sistem yöneticileri olarak zaman zaman bash script’leri içinde paralel iş yönetimi yapmamız gerekiyor. Aşağıdaki örnek, waitpid mantığını bash’te uygulayan ve başarısız işleri yeniden deneyen bir yapı kuruyor:

#!/bin/bash
# process_pool.sh - Paralel is yonetimi ve hata takibi

MAX_PARALLEL=4
RETRY_COUNT=2
declare -A PID_TO_JOB
declare -A JOB_RETRY_COUNT
FAILED_JOBS=()

run_job() {
    local job_id=$1
    local command=$2
    
    # Islemi arka planda calistir
    eval "$command" &
    local pid=$!
    PID_TO_JOB[$pid]="$job_id:$command"
    echo "[$(date '+%H:%M:%S')] Baslatildi: Job-$job_id (PID: $pid)"
    echo $pid
}

wait_any_job() {
    # Herhangi bir cocuk surecin bitmesini bekle
    local pid
    wait -n 2>/dev/null
    local exit_code=$?
    
    # Hangi PID bittigini bul
    for pid in "${!PID_TO_JOB[@]}"; do
        if ! kill -0 $pid 2>/dev/null; then
            echo "$pid:$exit_code"
            unset PID_TO_JOB[$pid]
            return
        fi
    done
}

# Ornek isler listesi (gercekte farkli komutlar olabilir)
JOBS=(
    "sleep 2 && echo 'Job 1 tamam'"
    "sleep 1 && exit 1"  # Basarisiz olacak
    "sleep 3 && echo 'Job 3 tamam'"
    "sleep 1 && echo 'Job 4 tamam'"
    "sleep 2 && echo 'Job 5 tamam'"
)

active_count=0
job_index=0

# Maksimum paralel sayisi kadar is baslatiyoruz
while [ $job_index -lt ${#JOBS[@]} ] || [ $active_count -gt 0 ]; do
    # Slot bos ve is varsa yeni is baslat
    while [ $active_count -lt $MAX_PARALLEL ] && [ $job_index -lt ${#JOBS[@]} ]; do
        run_job $job_index "${JOBS[$job_index]}" > /dev/null
        ((active_count++))
        ((job_index++))
    done
    
    # Bir isin bitmesini bekle
    if [ $active_count -gt 0 ]; then
        wait -n
        ((active_count--))
    fi
done

echo "Process pool tamamlandi"

WNOHANG ile Non-Blocking Kontrol

Production sistemlerde en kritik pattern’lerden biri, bloklanmadan çocuk süreç durumunu kontrol etmek. Özellikle event loop’lar veya daemon’lar yazarken bu şart oluyor:

# Non-blocking wait ornegi - C ile
cat > /tmp/nonblocking_wait.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

int main() {
    pid_t child_pid = fork();
    
    if (child_pid == 0) {
        // Cocuk surecin 3 saniye calismasi
        sleep(3);
        exit(0);
    }
    
    // Ebeveyn event loop'u simule ediyor
    int loop_count = 0;
    while (1) {
        int status;
        // WNOHANG: hemen don, bloklama
        pid_t result = waitpid(child_pid, &status, WNOHANG);
        
        if (result == 0) {
            // Cocuk hala calisiyor
            printf("Loop %d: Cocuk hala calisiyor, baska isler yapiliyor...n", 
                   ++loop_count);
            sleep(1);
        } else if (result == child_pid) {
            // Cocuk bitti
            if (WIFEXITED(status)) {
                printf("Cocuk normal cikis yapti, kod: %dn", 
                       WEXITSTATUS(status));
            } else if (WIFSIGNALED(status)) {
                printf("Cocuk sinyal ile oldu: %dn", WTERMSIG(status));
            }
            break;
        } else {
            // Hata
            perror("waitpid hatasi");
            break;
        }
    }
    
    return 0;
}
EOF

gcc -o /tmp/nonblocking_wait /tmp/nonblocking_wait.c
/tmp/nonblocking_wait

Macro’larla Exit Status Analizi

waitpid döndürdüğü status değerini doğrudan yorumlamak yerine, standart macro’lar kullanmanız gerekiyor. Bu macro’lar taşınabilirlik sağlıyor ve okunabilirliği artırıyor:

  • WIFEXITED(status): Süreç normal çıkış yaptıysa true döner
  • WEXITSTATUS(status): Normal çıkışta exit kodunu verir (0-255 arası)
  • WIFSIGNALED(status): Süreç bir sinyal tarafından öldürüldüyse true döner
  • WTERMSIG(status): Öldüren sinyalin numarasını verir
  • WIFSTOPPED(status): Süreç durdurulduysa true döner
  • WSTOPSIG(status): Durduran sinyali verir
  • WIFCONTINUED(status): SIGCONT ile devam ettirildiyse true döner
# Bash'te exit status analizi - gercek bir deployment scripti ornegi
#!/bin/bash

analyze_exit() {
    local pid=$1
    local job_name=$2
    
    wait $pid
    local status=$?
    
    # Bash'te 128+N formatinda sinyal kayiplarini tespit etme
    if [ $status -eq 0 ]; then
        echo "OK: $job_name basariyla tamamlandi"
    elif [ $status -gt 128 ]; then
        local signal=$((status - 128))
        echo "SINYAL: $job_name sinyal $signal ile sonlandi"
        # Kritik sinyal kontrolu
        case $signal in
            9)  echo "   SIGKILL - zorla sonlandirildi (OOM Killer?)" ;;
            11) echo "   SIGSEGV - segmentation fault!" ;;
            15) echo "   SIGTERM - nazik kapatma istegi" ;;
        esac
    else
        echo "HATA: $job_name hata kodu $status ile cikti"
    fi
}

# Test senaryosu
echo "Deployment kontrol testi basliyor..."

# Basarili is
sleep 1 &
analyze_exit $! "Veritabani baglanti kontrolu"

# Basarisiz is (exit code 1)
(exit 1) &
analyze_exit $! "API endpoint sagligi kontrolu"

# Sinyal ile sonlandirilan is
sleep 100 &
bg_pid=$!
kill -TERM $bg_pid
analyze_exit $bg_pid "Uzun sureli background gorevi"

strace ile Sistem Çağrılarını Gözlemleme

Teorik bilgi güzel ama sistemin gerçekte ne yaptığını görmek çok daha öğretici oluyor. strace ile hem waitpid hem de waitid çağrılarını canlı izleyebilirsiniz:

# Bash'in kendi wait mekanizmasini strace ile izleme
strace -e trace=wait4,waitid -f bash -c 'sleep 1 & wait' 2>&1

# Daha okunakli cikti icin timestamp ekleyerek
strace -T -e trace=wait4,waitid -f bash -c 
    'for i in 1 2 3; do sleep 0.5 & done; wait' 2>&1 | 
    grep -E 'wait|exit'

# Var olan bir sureci izlemek icin (örnek: nginx worker'lari)
# Oncelikle nginx master PID'ini bul
NGINX_PID=$(pgrep nginx | head -1)
strace -p $NGINX_PID -e trace=wait4 2>&1 | head -50

strace çıktısında dikkat etmeniz gereken şey, Linux çekirdeğinin waitpid çağrısını aslında wait4 olarak uyguladığıdır. wait4, waitpid‘in rusage (resource usage) desteği eklenmiş halidir.

Zombie Süreç Temizleme: Pratik Yaklaşımlar

Gerçek sistemlerde zombie süreçlerle karşılaştığınızda ne yapacağınızı bilmek kritik önem taşıyor:

# Sistemdeki zombie surecleri tespit etme
ps aux | awk '$8 == "Z" {print $0}'

# Zombie sayisini izleme - threshold alarmı icin
zombie_count=$(ps aux | awk '$8 == "Z"' | wc -l)
if [ $zombie_count -gt 10 ]; then
    echo "UYARI: $zombie_count zombie surecler tespit edildi!"
    echo "Ebeveyn PID'leri:"
    ps aux | awk '$8 == "Z" {print $2}' | while read zpid; do
        ppid=$(cat /proc/$zpid/status 2>/dev/null | grep PPid | awk '{print $2}')
        echo "  Zombie PID: $zpid -> Ebeveyn PID: $ppid"
        if [ -n "$ppid" ]; then
            echo "  Ebeveyn komutu: $(cat /proc/$ppid/cmdline 2>/dev/null | tr '' ' ')"
        fi
    done
fi

# Ebeveyn sürece SIGCHLD gondererek wait cagrisi yapmaya zorla
# DIKKAT: Bu her zaman islemez, uygulamanin SIGCHLD handle etmesi gerekir
kill -CHLD $PARENT_PID

# Eğer ebeveyn sürec yanit vermiyorsa ve yeniden baslatilabiliyorsa:
# systemctl restart uygulama_adi

# init/systemd'in benimseyecegi orphan surecleri icin:
# Ebeveyn oldugunda init otomatik olarak wait() cagirarak zombieleri temizler

SIGCHLD ile Asenkron Süreç Yönetimi

Profesyonel daemon geliştirmede waitpid‘i doğrudan çağırmak yerine, SIGCHLD sinyal işleyicisi içinde çağırmak yaygın bir pattern. Bu yaklaşım, event-driven mimarilerle çok daha uyumlu:

# SIGCHLD handler ile otomatik zombie temizleme
cat > /tmp/sigchld_demo.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <string.h>

volatile int children_done = 0;

void sigchld_handler(int signo) {
    int status;
    pid_t pid;
    
    // Tum bitmis cocuklari temizle (WNOHANG ile loop)
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            // Sinyal guvensiz oldugu icin sadece sayaci artiriyoruz
            children_done++;
        }
    }
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigchld_handler;
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);
    
    // 5 cocuk surecin baslatilmasi
    for (int i = 0; i < 5; i++) {
        if (fork() == 0) {
            sleep(1 + (i % 3));
            exit(0);
        }
    }
    
    // Ana dongu - gercek uygulamada baska isler yapilir
    while (children_done < 5) {
        printf("Calisiyor... Biten cocuk sayisi: %d/5n", children_done);
        sleep(1);
    }
    
    printf("Tum cocuklar tamamlandi!n");
    return 0;
}
EOF

gcc -o /tmp/sigchld_demo /tmp/sigchld_demo.c
/tmp/sigchld_demo

Bir detay önemli: SIGCHLD handler içinde waitpid‘i WNOHANG ile döngüye almak zorunlu. Birden fazla çocuk eş zamanlı biterse, Linux birden fazla sinyal biriktirmeyebilir, tek sinyal gönderir. Döngüsüz yazarsanız bazı zombie’leri kaçırırsınız.

Sonuç

waitpid ve waitid sistem çağrıları, Linux süreç modelin en temel taşlarından ikisi. Sysadmin olarak bu çağrıları doğrudan her gün yazmıyor olabilirsiniz, ama ne zaman ve neden çalıştıklarını anlamak, zombie süreç sorunlarını çözmekten OOM Killer vakalarını analiz etmeye kadar pek çok durumda sizi doğru yönlendiriyor.

Özet olarak şunu söyleyebilirim: waitpid, belirli bir çocuğu veya herhangi bir çocuğu beklemek için hızlı ve pratik bir araç. waitid ise çok daha ayrıntılı bilgi istediğinizde, özellikle sinyal kaynaklı çıkışları ayırt etmeniz gerektiğinde tercih edilmesi gereken daha modern alternatif. WNOHANG flag’i ise her ikisinin de event-driven sistemlerde kullanılabilmesini sağlayan kritik parametre.

Bash script yazıyorsanız, yerleşik wait komutunun arkasında bu mekanizmaların yattığını bilmek, neden bazı durumlarda beklenmedik davranışlar gördüğünüzü açıklıyor. strace ile bu çağrıları kendi sistemlerinizde gözlemlemek, soyut kavramları somutlaştırmanın en etkili yolu.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir