fork() + exec()'in ötesinde
(lwn.net)- 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ı
execfdya da mutlak yolfilenameile 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ındakiposix_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 hemenexec()izler veexec(), çocuk için kopyalanan belleğin tamamını atar- vfork() gibi optimizasyon denemeleri oldu, ancak
fork()ardındanexec()kalıbı hâlâ mümkün olandan daha pahalı bir yapı
Spawn templates
- Li Chen'in yama seti,
fork()veexec()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);
- İmza biçimi:
- 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ı
execfdya da mutlak yolfilenameile 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_argsyapısına yerleştirilirargv, programa iletilecek argüman listesini gösteren işaretçidirenvp, program ortamını gösteren işaretçidiractions, dosya tanımlayıcı ve sinyal işleme değişikliklerini iletenspawn_template_actiondizisine işaret eder
spawn_template_action,type,flags,fd,newfd,argalanlarından oluşur- Çocukta dosya tanımlayıcısı 4 kapatılacaksa
typeSPAWN_TEMPLATE_ACTION_CLOSE,fdise 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
- Çocukta dosya tanımlayıcısı 4 kapatılacaksa
- Ç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);
- İmza biçimi:
- İç 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 + execdeyimi 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,
execiç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()veexec()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
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ığı savunuluyorBunun 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ıpexec()çağırabiliyorduBu 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 bufork()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ğilimfork()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.pdffork()zygote deseni için harikaBu 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
O_CLOEXECile çözülmüyor mu?posix_spawndeğil mi?“
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 zamanfork()çağrısını hemenexec()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 garipSonuçta tüm belleğin gerçekten kopyalanmamasını sağlayan optimizasyon bu
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ı
fork()belleğin kendisini kopyalamasa da sayfa tablolarını yine de kopyalamak zorundaOnlarca GB RAM kullanan bir süreç için
fork()uzun sürebilir ve bu, Redis.rdbdosyası dump ettiğinde ya da ikili günlükleme AOF’yi yeniden yazdığında her seferinde olur2012’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üzerindefork()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üresiHuge 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
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 tetiklenebilirBü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
fork()/exec()bazı durumlarda yararlı olabilir, ancak API’lerpidfdparametresi alsa oldukça iyi olabilir gibi görünüyor. 0, mevcut süreci ifade edecek şekilde kullanılabilirSorun muhtemelen
setuid/setgidikilileri olurdu; bu durumda özel işlemeninexeciçinde yapılması daha iyi olabilirÖrneğin
pidfd_t ps = spawn();ile durdurulmuş bir süreç oluşturup, bunusetuid(ps, 33);,capset(ps, ...);,socket(ps, ...);,mmap(ps, ...);,process_vm_writev(ps, ...);,exec(ps, ...);,signal(ps, SIGCONT);şeklinde yapılandırabilirsinizBu 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ğlanabilirYine de kullanıcı alanı API’si olarak
CreateProcessgibi çok sayıda parametre alan bir yaklaşımın harika olmadığına katılıyorumÖ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ğruydufork()+exec()tarihindeki yol bağımlılığından kaynaklanıyorfork()+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 birpidparametresi olurdu. Fuchsia kabaca böyle çalışıyorBu 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ğlamakBö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üreç
ptracebağ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 gerekirfork()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 öyleydiDoğ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()veexec()i gizlemeyen yerel bir uygulamayı memnuniyetle karşılayabileceği” sonucuna katılıyorumfork()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 gerekmemeliYazıdaki örnekte olduğu gibi bir sürecin çok sayıda
gitalt süreci başlattığı durumu en iyi nasıl ele alacağımızı merak ediyorum. Uzun süren bir ebeveyn işi sırasındagiti 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ıyorsunuzAksi 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
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 varlibgit2var. Boru ya da soket üzerinden birgitdile 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 gerekirexec/forkyerine 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,seccompayarı, yetki düzenlemesi gerekebilirAma 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
spawngibi 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()ederBu şekilde belleği fork etmeden mevcut API korunabilir, ancak dosya tanımlayıcıları ve diğer şeylerin yine de çoğaltılması gerekir
Şaka yapmıyorsan kusura bakma ama
posix_spawn()zaten var ve glibc’defork, sadececlone()için bir takma adOrijinal öneriyle tam olarak aynı olmasa da
fork()/exec()gerçekten legacy’ye oldukça yakınforkveexec, 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ılabilirdiBu eski API hakkında Hacker News'te çok sayıda tartışma yapıldı; örneğin https://news.ycombinator.com/item?id=31739794