İçeriğe geç
academia.sh

Ders 21 / 24

Tuzaklar ve Temizlik

Sinyal kavramı, tuzak kurma, çıkışta çalışan temizlik kancası, geçici dosyanın güvenli yaratımı ve eşzamanlı çalışmayı engelleyen kilit.

İçindekiler

Katı kip betiği hata anında durduruyor. Durduğu yerde ne bıraktığı ise ayrı bir sorudur: yarım yazılmış çıktı dosyası, temizlenmemiş geçici dizin, başka bir sürecin beklediği bir kilit. Betiğin doğru sonlanması, doğru çalışması kadar önemlidir.

Bilgisayarlar Nasıl Çalışır kursunda kesme, donanımın işlemciye “işini böl” demesiydi. Sinyal (signal), aynı fikrin süreçler düzeyindeki karşılığıdır: çekirdeğin ya da başka bir sürecin, çalışan bir sürece gönderdiği asenkron bildirim. Bu ders sinyalleri yakalamayı ve çıkışta yapılacak temizliği kurar.

Sinyaller

Bir süreç sinyal aldığında öntanımlı davranışı uygular — çoğu sinyal için bu, sonlanmaktır. Betiklerde ilgilenilen sinyaller azdır:

Sinyal Numara Kaynak
SIGHUP 1 denetleyen terminal kapandı
SIGINT 2 klavyeden kesme
SIGTERM 15 düzgün sonlandırma isteği
SIGKILL 9 koşulsuz sonlandırma
SIGPIPE 13 okuma ucu kapalı boruya yazma

Çıkış Kodları dersinde sinyalle sonlanan sürecin 128 + sinyal numarası kodu döndürdüğü gösterilmişti; SIGTERM için 143, SIGINT için 130.

SIGKILL ve SIGSTOP yakalanamaz, yok sayılamaz ve yeniden tanımlanamaz. Bu, işletim sisteminin süreç üzerinde son sözü söylemesini güvence altına alır. Dolayısıyla hiçbir temizlik kancası, SIGKILL ile sonlandırılan bir betikte çalışmaz — temizliğin tek dayanağı olamaz.

Tuzak Kurmak

trap 'komut' sinyal... biçimi, sinyal geldiğinde çalışacak komutu belirler. Kabuk ayrıca üç sözde sinyal tanır: EXIT, ERR ve DEBUG.

EXIT en önemlisidir: kabuk hangi yolla sonlanırsa sonlansın — normal bitiş, exit çağrısı, katı kipin erken çıkışı, yakalanan bir sinyalin işleyicisinden sonra — çalışır.

#!/usr/bin/env bash
set -Eeuo pipefail

gecici=$(mktemp -d)
temizle() { rm -rf "$gecici"; }
trap temizle EXIT

echo "veri" > "$gecici/ara.txt"
echo "gecici dizin icerigi: $(ls "$gecici")"
gecici dizin icerigi: ara.txt

Betik bittiğinde dizin silinmiş olur. Hata yolunda da aynısı geçerlidir:

#!/usr/bin/env bash
set -Eeuo pipefail
gecici=$(mktemp -d)
trap 'rm -rf "$gecici"' EXIT
echo "islem basladi"
false
echo "BU SATIR YAZILMAZ"
islem basladi

Katı kip false satırında betiği durdurdu; EXIT tuzağı yine de çalıştı ve geçici dizin kalmadı.

Tuzak, kurulduğu andan itibaren geçerlidir. Bu yüzden kaynak yaratılır yaratılmaz kurulur; iki satır arasında bir hata oluşursa temizlik yapılmamış olur.

Çıkış Kodunu Korumak

EXIT tuzağının gövdesi, betiğin çıkış kodunu değiştirmemelidir. Kodun okunması için gövdenin ilk komutu $? okumalıdır:

#!/usr/bin/env bash
temizle() { local kod=$?; echo "temizlik, korunan kod=$kod"; }
trap temizle EXIT
exit 7
temizlik, korunan kod=7
echo $?
7

Tuzak gövdesi kendisi bir exit çağırırsa betiğin kodu o olur; çağırmazsa özgün kod korunur. Temizlik başarısızlığının betiğin sonucunu değiştirmesi istenmiyorsa, gövdedeki komutların kodları bastırılmalıdır.

Sinyali Yeniden Göndermek

Bir sinyali yakalayıp temizlik yapmak, sürecin o sinyalle sonlandığı bilgisini yok eder: betik normal biterse çıkış kodu 0 olur ve çağıran, sürecin kesildiğini anlayamaz.

Doğru desen üç adımlıdır: temizle, tuzağı kaldır, sinyali kendine yeniden gönder.

#!/usr/bin/env bash
temizle() { echo "temizlik calisti (kaynak: $1)"; }
trap 'temizle EXIT' EXIT
trap 'temizle TERM; trap - TERM; kill -TERM $$' TERM
echo "hazir"
sleep 30

Betik arka planda başlatılıp SIGTERM gönderildiğinde:

hazir
temizlik calisti (kaynak: TERM)
temizlik calisti (kaynak: EXIT)
echo $?
143

Kod 143; yani 128 + 15. Süreci yöneten bir servis yöneticisi ya da zamanlayıcı, bu kodu görerek betiğin hata vermediğini, sonlandırıldığını ayırt edebilir.

trap - SINYAL yazımı tuzağı kaldırıp öntanımlı davranışı geri getirir. Bu adım atlanırsa kill çağrısı yine kendi tuzağını tetikler ve sonsuz döngü oluşur.

EXIT tuzağının da çalıştığına dikkat edin: temizlik iki kez yürüdü. Bu yüzden temizlik fonksiyonu yinelemeye dayanıklı (idempotent) yazılmalıdır — rm -rf ve rmdir çağrıları zaten yok olan bir yolu ikinci kez silmeye çalıştığında zarar vermez, ama sayaç artıran ya da ileti gönderen bir temizlik iki kez çalışırsa yanlış olur.

Geçici Dosyaları Güvenle Yaratmak

Geçici dosya adını elle üretmek — /tmp/rapor.$$ gibi — üç açıdan yanlıştır.

Öngörülebilir. Süreç numaraları dar bir aralıktadır ve yeniden kullanılır; bir saldırgan adı önceden tahmin edip o yola bir sembolik bağ koyabilir. Betik dosyayı açtığında bağın gösterdiği yere yazar.

Yarış içerir. “Dosya var mı” sınaması ile yaratma arasındaki aralık, araya girmeye açıktır.

Çakışabilir. Aynı betiğin iki örneği aynı süreç numarasını farklı zamanlarda alabilir.

mktemp, adı öngörülemez biçimde üretir ve dosyayı atomik olarak yaratır; ad çakışırsa yeniden dener. -d seçeneği dizin yaratır ve birden çok geçici dosya gerektiğinde tek bir temizlik noktası sağlar.

gecici=$(mktemp -d)
trap 'rm -rf "$gecici"' EXIT

mktemp yarattığı dosya ve dizinleri yalnızca sahibinin erişebileceği izinlerle açar; elle touch edilen bir dosyada bu güvence yoktur.

rm -rf "$gecici" satırındaki tırnak zorunludur. Değişken tanımsız kalırsa — set -u olmadan — komut rm -rf hâline gelir; tırnak, boşluk içeren yollarda ikinci bir korumadır. Katı kipin set -u seçeneği bu satırın en önemli koruyucusudur.

Eşzamanlı Çalışmayı Engellemek

Zamanlanmış bir rapor betiği, önceki çalışma bitmeden yeniden başlatılabilir. İki örnek aynı geçici dosyalara yazarsa sonuç bozulur.

Kilit almanın taşınabilir yolu dizin yaratmaktır: mkdir, hedef zaten varsa başarısız olan atomik bir işlemdir. Dosya yaratmak aynı güvenceyi vermez, çünkü “var mı” sınaması ile yaratma iki ayrı adımdır.

#!/usr/bin/env bash
set -Eeuo pipefail
KILIT="rapor.kilit"

if ! mkdir "$KILIT" 2>/dev/null; then
  echo "baska bir calisma suruyor, cikiliyor" >&2
  exit 75
fi
trap 'rmdir "$KILIT"' EXIT

echo "kilit alindi, is yapiliyor"
sleep 1

Bir örnek çalışırken ikincisi başlatıldığında:

kilit alindi, is yapiliyor
baska bir calisma suruyor, cikiliyor
ikinci calisma kodu=75

Tuzağın mkdir başarılı olduktan sonra kurulması zorunludur. Önce kurulsaydı, kilidi alamayan ikinci örnek çıkarken başkasının kilidini silerdi.

75 kodu, “geçici hata, sonra yeniden dene” anlamında yaygın bir sözleşmedir; zamanlayıcı bu kodu gerçek bir hatadan ayırt edebilir.

Bu desenin bilinen zayıflığı, betik SIGKILL ile sonlandırıldığında ya da makine kapandığında kilidin kalmasıdır. Uzun ömürlü sistemlerde kilit dizinine süreç numarası yazılır ve kilit alınamadığında o sürecin hâlâ yaşayıp yaşamadığı sınanır.

Betiğe Uygulama

rapor.sh katı kip başlığını ve temizlik altyapısını alır:

#!/usr/bin/env bash
# rapor.sh — erisim kutugunden ozet rapor uretir.
set -Eeuo pipefail
export LC_ALL=C

BETIK_ADI="rapor.sh"
gecici=""

temizle() {
  local kod=$?
  [ -n "$gecici" ] && rm -rf "$gecici"
  return "$kod"
}
trap temizle EXIT
trap 'temizle; trap - INT;  kill -INT  $$' INT
trap 'temizle; trap - TERM; kill -TERM $$' TERM

gecici=$(mktemp -d)

gecici değişkeninin tuzak kurulmadan önce boş dizgiyle ilklendirilmesi, set -u altında gereklidir: tuzak, mktemp çağrısından önce bir hata oluşursa da çalışır ve o anda değişken tanımsız olurdu.

Önceki dersteki karsilastir fonksiyonu artık geçici dosya bırakmıyor; $gecici altındaki her şey çıkışta siliniyor.

Özet

  • Sinyal, çalışan bir sürece gönderilen asenkron bildirimdir; SIGKILL ve SIGSTOP yakalanamaz, bu yüzden temizlik tek dayanak olamaz.
  • trap ... EXIT, kabuğun hangi yolla sonlandığından bağımsız olarak çalışır ve kaynak yaratılır yaratılmaz kurulur.
  • EXIT tuzağının ilk komutu $? okumalıdır; yoksa betiğin çıkış kodu kaybolur.
  • Sinyal yakalandığında temizlenip tuzak kaldırılır ve sinyal kendine yeniden gönderilir; böylece çıkış kodu 128 + sinyal olarak korunur.
  • Geçici dosya mktemp ile yaratılır: ad öngörülemez, yaratım atomiktir ve izinler dardır.
  • mkdir atomik olduğu için taşınabilir kilit mekanizması olarak kullanılır; tuzak kilidi aldıktan sonra kurulur.

Sonraki Adım

Betik artık düzgün duruyor ve arkasında iz bırakmıyor. Yanlış sonuç üretiyorsa bunu nasıl bulacağımız ise başka bir soru. Sonraki ders hata ayıklama araçlarını ele alır: her komutu çalıştırmadan önce yazan izleme kipi, izleme çıktısını okunur kılan biçim değişkeni, çalıştırmadan yapılan sözdizim denetimi ve izlemenin betiğin yalnızca bir bölümüne uygulanması.

İ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