2 puan yazan GN⁺ 3 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • spawn templates, aynı yürütülebilir dosyayı tekrar tekrar çalıştıran uygulamalarda çekirdeğin yürütülebilir dosya bilgisini önbelleğe alarak sonraki süreç başlatmalarını hızlandırmasını amaçlayan Linux çekirdeği için bir süreç oluşturma önerisi
  • fork(), çocuk süreç için bellek dahil tüm süreç durumunu kopyalamak zorundadır; hemen ardından gelen exec() ise çoğu zaman bu belleği atar ve bu da mevcut kalıpta verimsizlik yaratır
  • spawn_template_create(), yürütülebilir dosyayı execfd ya da mutlak yol filename ile belirtip bir şablon dosya tanımlayıcısı döndürür; çekirdek de hızlı yürütme için gerekli bilgileri önbelleğe alır
  • spawn_template_spawn(), genel fork()/exec() yoluna yakın bir şekilde çalışır, yeni dosya yürütülürken uygulanan kontrolleri korur ve kapak yazısındaki benchmark yaklaşık %2 iyileşme gösterir {p:2}
  • pidfd tabanlı boş süreç oluşturma ve pidfd_config() ile yapılandırma daha iyi bir yaklaşım olarak değerlendiriliyor; amaç kullanıcı alanındaki posix_spawn() uygulamasını desteklemek

Unix süreç oluşturma modelinin sınırları

  • Unix'in ilk günlerinden beri fork(), ebeveynin kopyası olarak bir çocuk süreç oluşturur; exec() ise mevcut sürecin yerinde yeni bir program çalıştıran temel süreç odaklı sistem çağrısıdır
  • Linux çekirdeğinde aynı temel işlevsellik daha çok clone() ve execve() ile bilinir
  • Bu süreç oluşturma modelinin hem zarif hem de sorunlu yanları vardır; Li Chen'in spawn templates önerisi mevcut haliyle Linux çekirdeğine alınmayacak olsa da gelecekte yeni süreç oluşturma ilkel işlemlerine yol açabilir
  • fork(), çocuk süreç oluşturmak için bellek dahil tüm süreç durumunu kopyalaması gereken, görece pahalı bir sistem çağrısıdır
  • Yıllar içinde çeşitli optimizasyonlar yapılmış olsa da fork() özünde maliyetli bir işlemdir
  • fork() çağrısını çoğu zaman hemen exec() izler ve exec(), çocuk için kopyalanan belleğin tamamını atar
  • vfork() gibi optimizasyon denemeleri oldu, ancak fork() ardından exec() kalıbı hâlâ mümkün olandan daha pahalı bir yapı

Spawn templates

  • Li Chen'in yama seti, fork() ve exec() kalıbını optimize etmek için aynı yürütülebilir dosyayı tekrar tekrar çalıştıran uygulamalara odaklanıyor
  • Örnek olarak, depo içeriği bilgisi almak için Git'i tekrar tekrar çalıştırmak zorunda olan bir program bu duruma giriyor
  • Böyle durumlarda program, kurulum maliyetini birden fazla çalıştırmaya yaymak için bir şablon oluşturabilir ve çağrıları bu şablon üzerinden hızlandırabilir
  • Şablon oluşturma, spawn_template_create() sistem çağrısı ile yapılır
    • İmza biçimi: int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);
  • Bu çağrı, yürütülebilir dosya şablonunu temsil eden bir dosya tanımlayıcısı döndürür
  • Yürütülebilir dosya, dosya tanımlayıcısı execfd ya da mutlak yol filename ile belirtilmelidir; ikisi aynı anda kullanılamaz
  • Çekirdek belirtilen dosyayı açar ve daha sonra onu daha hızlı çalıştırmak için gereken çeşitli bilgileri önbelleğe alır
  • Her çalıştırma farklı argümanlara, ortam değişkenlerine, dosya tanımlayıcı değişikliklerine ve sinyal işleme değişikliklerine sahip olabilir
  • Somut çalıştırma bilgileri spawn_template_spawn_args yapısına yerleştirilir
    • argv, programa iletilecek argüman listesini gösteren işaretçidir
    • envp, program ortamını gösteren işaretçidir
    • actions, dosya tanımlayıcı ve sinyal işleme değişikliklerini ileten spawn_template_action dizisine işaret eder
    Reklam
  • spawn_template_action, type, flags, fd, newfd, arg alanlarından oluşur
    • Çocukta dosya tanımlayıcısı 4 kapatılacaksa type SPAWN_TEMPLATE_ACTION_CLOSE, fd ise 4 olarak ayarlanır
    • Diğer eylemler dosya tanımlayıcısı çoğaltma, dosya açma, çalışma dizini değiştirme ve sinyal işleme değiştirmeyi destekler
  • Çalıştırma bilgileri doldurulduktan sonra yeni süreç spawn_template_spawn() ile başlatılır
    • İmza biçimi: int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);
  • İç işleyiş, olağan fork()/exec() yoluna yakın bir biçimdedir
  • Yeni bir dosya çalıştırılırken uygulanan genel kontrollerin tümü olduğu gibi korunur
  • Şablonda önbelleğe alınan bilgiler tüm oluşturma akışını hızlandırır
  • Kapak yazısındaki benchmark sonuçları yaklaşık %2 iyileşme gösteriyor; beklenen kalıba uyan uygulamalar için bu anlamlı bir fark olabilir {p:2}

posix_spawn() yolunda

  • Mateusz Guzik, “tam fork + exec deyimi korkunç ve ortadan kaldırılmalı” değerlendirmesini yapıyor
  • Yama setindeki garip nokta, fork() kısmını olduğu gibi bırakması; çünkü maliyetin büyük kısmının orada olduğu düşünülüyor
  • Optimizasyonun, mevcut sürecin kopyasını çıkarmayı bırakıp “temiz (pristine) bir süreç” oluşturacak biçimde olması gerektiği savunuluyor
  • Christian Brauner, exec için bir builder API fikrinin “o kadar da tuhaf olmadığını” düşünüyor
  • Ancak yeni API'nin mevcut pidfd soyutlaması üzerine kurulması yaklaşımı tercih ediliyor
  • Somut ayrıntılar verilmedi, ancak pidfd_open() içine boş bir süreç oluşturma seçeneği eklemek doğru yaklaşım olarak görülüyor
  • Ardından yeni pidfd_config() sistem çağrısı birden çok kez çağrılarak yeni sürece ortam, çalıştırılacak imaj ve istenen diğer ayarlar uygulanabilir
  • pidfd_config(), fsconfig() ile benzer bir rol üstlenir
  • Yeni arayüzün önemli hedeflerinden biri, kullanıcı alanında posix_spawn() uygulamasını desteklemektir
  • posix_spawn(), fork()/exec() kalıbına uygun bir alternatif olarak görülüyor
  • Mevcut uygulama içeride fork() ve exec() işlemlerini gizliyor; yerel bir uygulama ise bu yapıdan farklı olacak
  • Li Chen de Brauner'ın geniş çerçevede tarif ettiği API'nin daha iyi göründüğü konusunda hemfikir oldu ve sonraki çalışmaları o yöne taşımayı planlıyor
  • Linux çekirdeğine spawn templates girmeyecek, ancak sonraki çalışmalar sonuç verirse Linux uygun bir posix_spawn() uygulamasına kavuşabilir

1 yorum

 
GN⁺ 3 시간 전
Hacker News yorumları
  • İlgili bir tartışma olarak A fork() in the road makalesi var: https://www.microsoft.com/en-us/research/wp-content/uploads/...
    Özetinde, Unix’in fork()+exec() birleşiminin ilham verici bir tasarım olduğu yönündeki yaygın kanının aksine, bunun 1970’lerin makineleri ve programları için zekice bir hack olduğu, ancak bugün modern programcılar için kötü bir soyutlama olduğu ve işletim sistemi uygulamasını da kısıtladığı savunuluyor
    Bunun işletim sisteminin birinci sınıf ilkel işlevi olarak kalmasındansa tarihsel bir kalıntı olarak öğretilmesi ve öğrencilerin ilk öğrendiği süreç oluşturma yöntemi olmaması gerektiği görüşü dile getiriliyor

    • fork()+exec() kombinasyonunun böyle olmasının nedeni, ebeveyn programla birlikte belleğe sığmayacak kadar büyük programları çalıştırabilmekti
      İlk uygulamada fork() çağrıldığında çatallanan program diske swap-out ediliyor, denetim geri dönmeden önce süreç tablosu girdisi kopyalanıp ayarlanıyor ve böylece bellekteki süreç ile swap-out edilmiş süreç oluşuyordu; bellekte kalan taraf denetimi alıp exec() çağırabiliyordu
      Bu yaklaşım sayesinde küçük PDP-11 makinelerde bile büyük programlar çalıştırılabiliyordu ve belleğin çok pahalı olduğu bir dönemde bu gerekliydi
      İlginç şekilde QNX’te program yükleme işletim sisteminin içinde değil, bir kütüphanede yer alıyor. Yürütülebilir dosya başlığını okuyup belleği ayırıyor, programı yükleyip çalışmaya hazırlıyor ve ardından başlatan .soya bağlanıyor; program yükleyici ayrıcalıksız kullanıcı alanında çalışıyor. Muhtemelen doğru yaklaşıma daha yakın olan da bu
    • fork() kullanmayan en yaygın “büyük” işletim sistemi olan Windows’ta süreç oluşturmanın çok yavaş olması ilginç
      fork() dışında bir ilkel işlev olması gerektiğine katılıyorum, ama performansın en güçlü argüman olup olmadığından emin değilim
    • Bu makale de iyi, ayrıca kaynakça [29] da fork() dahil ölçeklenebilir arayüzlerin ince noktalarını ele aldığı için özellikle iyiydi: The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors https://people.csail.mit.edu/nickolai/papers/clements-sc.pdf
    • O zamanki tartışma burada: https://news.ycombinator.com/item?id=19621799 - A fork() in the road (2019-04-10, 178 comments)
    • fork() zygote deseni için harika
      Bu kadar verimli ve zarif başka bir optimizasyon düşünmek zor
  • Yakın zamanda, fork edilmiş bir süreçte daha fazla dosya tanımlayıcısı kapatılması gerektiği için ortaya çıkan belirsiz bir bug yaşadım
    Benim deneyimimde “mevcut sürecin bir kopyasını istiyorum”dan çok “tamamen yeni bir süreç istiyorum” daha yaygın; ama ikincisini doğrudan ifade etmenin bir yolu olmayıp sadece kopyalayıp sonradan düzeltmeye çalışmak tuhaf geliyor

    • Genelde o süreçle iletişim kurmak istersiniz; dolayısıyla örneğin dosya tanımlayıcıları gibi şeyleri ayarlamanız ve ebeveyn süreçten bilgi aktarmanız gerekir
    • Bu O_CLOEXEC ile çözülmüyor mu?
    • “İkincisini doğrudan ifade etmenin yolu” derken, bunun amacı tam olarak posix_spawn değil mi?
    • “Tamamen yeni bir süreç” tam olarak ne demek?
  • fork() görece pahalı bir sistem çağrısıdır ve çocuk süreç için bellek dahil tüm süreç durumunu kopyalamak zorundadır. Yıllar içinde birçok optimizasyon yapıldı, ancak temelde bu maliyetli bir iştir. Daha da kötüsü, çoğu zaman fork() çağrısını hemen exec() izler ve çocuk için özenle kopyalanan belleğin tamamı çöpe gider” denirken copy-on-write (yazma anında kopyalama) mekanizmasından hiç söz edilmemesi garip
    Sonuçta tüm belleğin gerçekten kopyalanmamasını sağlayan optimizasyon bu

    • Yazıda bu ima ediliyordu, ama burada süreç durumu kopyasından kasıt bellek yönetimi yapıları. Esas olarak sayfa tabloları ve VMA’ler
      Gerçek sayfaların işaret ettiği bellek paylaşılsa bile, bu yapıların kopyalarını tutmak için yeni sayfalar ayırmak gerekiyor. Ayrıca bu yapıların tamamını dolaşıp kopyalamak da hâlâ pahalı
    • Redis, bu maliyetin ciddi önem taşıdığı süreç türlerinden biri. fork() belleğin kendisini kopyalamasa da sayfa tablolarını yine de kopyalamak zorunda
      Onlarca GB RAM kullanan bir süreç için fork() uzun sürebilir ve bu, Redis .rdb dosyası dump ettiğinde ya da ikili günlükleme AOF’yi yeniden yazdığında her seferinde olur
      2012’de bile bu işin yüksek maliyetini gösteren bir yazı vardı: https://redis.io/blog/testing-fork-time-on-awsxen-infrastruc...
      Yaklaşık 25GB RAM kullanan bir m2.xlarge üzerinde fork() 5,67 saniye sürmüş. Redis istemcilerinin çoğu işte tipik olarak tek haneli milisaniye gecikme beklediği düşünülürse bu uzun bir duraklama. Üstelik bu sadece sayfa tablolarını kopyalama süresi
      Huge page’den hiç bahsedilmemesi şaşırtıcı; burada temel bir etken gibi görünüyor. 14 yıl sonra donanım hızlanmış olabilir, ama Redis örnekleri de muhtemelen daha fazla RAM kullanıyordur; bu benchmark’ı yeniden yapmak ilginç olabilir
    • Bu tür makalelerin hedef kitlesi için copy-on-write muhtemelen temel bilgi sayıldığı için atlanmıştır
    • Copy-on-write olsa bile fork() bunun kurulum maliyetini ödemek zorunda. Ebeveyn süreçte yoğun çalışan çok sayıda thread varsa, örneğin Java’da, exec() çalışmadan önce gereksiz çok sayıda copy-on-write tetiklenebilir
    • Metinde “durum” deniyordu. Copy-on-write olsa bile içerik kopyalanmasa da sayfa tablosu girdilerinin sayısıyla orantılı bir maliyet kalır
      Büyük sanal bellek boyutuna sahip programları fork etmenin yavaş olduğu iyi bilinen bir sorun
  • fork()+exec() modelinin zarafeti, fork() sonrasında her türlü yapılandırmayı yapmak için genel API’leri aynen kullanabilmesinde yatıyor
    Şimdiye kadar gördüğümüz birleşik çağrı biçimindeki alternatifler temelde yetersiz görünüyordu; çünkü tüm yapılandırma seçeneklerini çağrı parametrelerine eklemek ve bunu sonradan genişletilebilir olurken aynı zamanda keşmekeşe dönüşmeyecek şekilde tasarlamak gerekiyor

    • Biraz katılmıyorum ama faydasını görüyorum. fork()/exec() bazı durumlarda yararlı olabilir, ancak API’ler pidfd parametresi alsa oldukça iyi olabilir gibi görünüyor. 0, mevcut süreci ifade edecek şekilde kullanılabilir
      Sorun muhtemelen setuid/setgid ikilileri olurdu; bu durumda özel işlemenin exec içinde yapılması daha iyi olabilir
      Örneğin pidfd_t ps = spawn(); ile durdurulmuş bir süreç oluşturup, bunu setuid(ps, 33);, capset(ps, ...);, socket(ps, ...);, mmap(ps, ...);, process_vm_writev(ps, ...);, exec(ps, ...);, signal(ps, SIGCONT); şeklinde yapılandırabilirsiniz
      Bu aynı zamanda olağan sistem çağrısı API’lerinin “Erişim yetkim olan başka bir süreç üzerinde bu işlemi yapmak istersem ne olacak?” sorusunu yeterince dikkate almadığı yönünde bir eleştiri. Bu şekilde fork() tarafında thread güvenliği de bir ölçüde sağlanabilir
      Yine de kullanıcı alanı API’si olarak CreateProcess gibi çok sayıda parametre alan bir yaklaşımın harika olmadığına katılıyorum
    • Buna tamamen karşıyım. UNIX tarzı modelin en büyük hatası, süreç oluşturulurken çok fazla durumun korunması
      Örneğin bir nesnenin dosya tanımlayıcı numarasının 4 olmasını sağlayan API’ler var ve sonra bir program çalıştırıp o programın bu nesneyi 4 numaralı tanımlayıcıda bulmasını sağlayabiliyorsunuz. Bu tuhaf
      Windows, sayısız kusuruna rağmen fork()+exec() kullanmıyor; bunun yerine çoğunlukla süreç oluşturma yöntemine dair seçenekler sunuyor. Zarif değildi ama yön doğruydu
    • Buna zarif demek, fork()+exec() tarihindeki yol bağımlılığından kaynaklanıyor
      fork()+exec() olmayan başka bir dünyada, bu tür “genel API”lerin çoğunda başka bir sürecin ayarlarını değiştirebilmek için açık bir pid parametresi olurdu. Fuchsia kabaca böyle çalışıyor
      Bu dünyanın birçok avantajı var. En belirgini, yapılandırma hatalarını bildirmek için ayrı bir IPC mekanizmasını sihirli biçimde icat etmek gerekmemesi; ayrıca çocuğun özelliklerini ayarlayan bir yönetici süreç bulundurabilmek de oldukça kullanışlı. Özellikle hata ayıklayıcılar bunu severdi
    • fork()u ortadan kaldırmanın doğru yolu, süreç durumunu değiştiren genel API’lerin açık bir süreç tanıtıcısı almasını sağlamak
      Böylece aynı API ile boş bir süreci yapılandırabilir, bunu IPC veya hata ayıklama gibi başka yöntemlerle de birleştirebilirsiniz
    • Sıralama spawn, configure, exec olmalı
      Süreç ptrace bağlı durumda ve threadsiz başlarsa, yapılandırma aşamasında sistem çağrılarını zorla yaptırabilirsiniz. Linux’ta “threadsiz süreç” diye bir kavram bile olmadığından muhtemelen sahte bir thread gerekir
  • fork()un ucuz olduğu yanılgısı şaşırtıcı derecede yaygın, oysa süreç boyutuna göre O(N) ve her zaman da öyleydi
    Doğru, copy-on-write var. Ama süreç boyutu ile bunu temsil etmek için gereken sayfa tablosu girdilerinin sayısı arasında doğrusal bir ilişki var

  • Chen’in yamasının reddedilmesi şaşırtıcı değil. Kullanım senaryosu fazla özel, bu yüzden desteklemeye değmez
    Kabuk geliştiricisi bakış açısından, “geliştiricilerin mevcut uygulamadaki gibi içeride fork() ve exec()i gizlemeyen yerel bir uygulamayı memnuniyetle karşılayabileceği” sonucuna katılıyorum

    • Belirli bir uygulamadan çok, kavramın kendisine ilgi var gibi görünüyor
  • fork() ilk öğrendiğim andan beri kavramsal olarak korkunç görünüyordu. Tek bir iş, yani bir süreç başlatmak istiyorsanız, bununla ilgisiz başka bir iş olan mevcut süreci fork etme gibi gizemli bir büyüden geçmeniz gerekmemeli
    Yazıdaki örnekte olduğu gibi bir sürecin çok sayıda git alt süreci başlattığı durumu en iyi nasıl ele alacağımızı merak ediyorum. Uzun süren bir ebeveyn işi sırasında giti tekrar tekrar sıfırdan başlatmak mantıklı görünmüyor; aynı sonucu veren düşük maliyetli soyutlama ne olabilir?

    • fork() kavramsal olarak basit. Başka katmanları işin içine katmazsanız, bir süreci kesin olarak var olduğunu bildiğiniz tek şeyden, yani kendinizden başlatıyorsunuz
      Aksi halde bir süreç oluşturmak, onu çalıştırılacak bir şeyle doldurmak ve çalışacak şekilde yerleştirmek için birden fazla adım gerekir. Ya da Win32’de olduğu gibi dosya sistemi, nesne yükleyici, bağlayıcı gibi başka katmanlarla kalıcı biçimde ezilip birleştirilmesi gerekir
    • Windows’tan gelmiş biri olarak fork()+exec() modeli bana hiç mantıklı gelmemişti. Artık bunun sadece tarihsel bir tuhaflık olduğunu biliyorum ama hâlâ fork()+exec()in gerçekten iyi bir şeymiş gibi davranan insanlar var
    • libgit2 var. Boru ya da soket üzerinden bir gitd ile iletişim kurulan bir yöntem hayal edilebilir ama bunun neden iyi bir fikir olacağını bilmiyorum. Bunun dışında süreç başlatmanız gerekir
  • exec/fork yerine geçecek bir şeyi tasarlamanın zor olmasının nedeni, yeni sürecin genellikle yapılandırılmak zorunda olması. Örneğin sinyal işleyici ayarları, dosya tanımlayıcıların kapatılması veya açılması, namespace değiştirme, seccomp ayarı, yetki düzenlemesi gerekebilir
    Ama bunlar için kullanılan sistem çağrıları yalnızca mevcut sürece uygulanıyor, dolayısıyla bir alternatif gerekiyor. Yazıdaki öneri, bunun için yeni bir API oluşturmaktı
    Bence spawn gibi yeni bir sistem çağrısı boş bir süreç oluşturabilir, içine hafif bir yükleyici yerleştirip rastgele yapılandırma verileri aktarılmasına izin verebilir. Yükleyici süreci yapılandırır ve ana programı exec() eder
    Bu şekilde belleği fork etmeden mevcut API korunabilir, ancak dosya tanımlayıcıları ve diğer şeylerin yine de çoğaltılması gerekir

    • Neyse ki biri zaman makinesine binip bu yazıyı görmüş ve bunu POSIX.1-2001’e eklemiş gibi görünüyor :)
      Şaka yapmıyorsan kusura bakma ama posix_spawn() zaten var ve glibc’de fork, sadece clone() için bir takma ad
      Orijinal öneriyle tam olarak aynı olmasa da fork()/exec() gerçekten legacy’ye oldukça yakın
  • fork ve exec, copy-on-write niteliğinin ötesine geçip kalıcı ve cebirsel davranışlar sergileyebilseydi, sadece daha kullanışlı olmakla kalmaz, kullanımı da daha ilginç olurdu. Örneğin tembel değerlendirme için kullanılabilirdi

  • Bu eski API hakkında Hacker News'te çok sayıda tartışma yapıldı; örneğin https://news.ycombinator.com/item?id=31739794