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;
SIGKILLveSIGSTOPyakalanamaz, 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.EXITtuzağı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 + sinyalolarak korunur. - Geçici dosya
mktempile yaratılır: ad öngörülemez, yaratım atomiktir ve izinler dardır. mkdiratomik 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.