Çağrı yığını neden statik maksimum boyuta sahip?


46

Birkaç programlama diliyle çalıştım, her zaman iş parçacığının neden gerekli şekilde otomatik olarak genişlemek yerine önceden tanımlanmış bir maksimum boyuta sahip olduğunu merak ettim. 

Buna karşılık, çoğu programlama dilinde bulunan bazı çok ortak bazı üst düzey yapılar (listeler, haritalar, vb.), Yeni elemanlar eklenirken, sadece mevcut hafıza ile veya hesaplama limitleriyle boyut olarak sınırlandırıldığında, gerektiği gibi büyümek üzere tasarlanmıştır. örneğin 32 bit adresleme).

Maksimum yığın boyutunun bazı varsayılan veya derleyici seçenekleriyle önceden sınırlandırılmadığı herhangi bir programlama dili veya çalışma zamanı ortamı olduğunu bilmiyorum. Bu nedenle, çok fazla özyineleme, bir işlem için kullanılabilir belleğin yalnızca en az bir yüzdesi yığın için kullanıldığında bile, her yerde bulunan yığın taşması hatası / istisnasıyla çok hızlı sonuçlanmasına neden olur.

Neden çoğu (tümü değilse) çalışma zamanı ortamlarının bir yığının çalışma zamanında büyütebileceği boyut için maksimum bir sınır koyduğu durumda?


13
Bu tür bir yığın, sahne arkasında sessizce hareket ettirilemeyen sürekli adres alanıdır. Adres alanı 32 bit sistemlerde değerlidir.
CodesInChaos

7
Akademi'den sızan özyineleme ve gerçek dünyadaki kod okunabilirliğinin azalması ve toplam sahip olma maliyetinin artması gibi sorunların ortaya çıkması gibi fildişi-kule fikirlerinin oluşumunu azaltmak için;)
Brad Thomas

6
@BradThomas Bu kuyruk çağrısı optimizasyonu bunun için var.
JAB

3
@JohnWu: Şimdi yaptığı aynı şey, bir süre sonra: yetersiz bellek.
Jörg W Mittag

1
Durumda belli değil, bir nedeni bellek tükeniyor yığınının azalıyor daha kötüdür, bu sadece neden yığının azalıyor (bir tuzak sayfa var varsayarak) 'dir sizin başarısız olmasına süreci. Hafızanın tükenmesi, herhangi birinin başarısız olmasına neden olabilir , daha sonra kim hafıza ayırmaya çalışırsa. Sonra yine, bir tuzak sayfası olmayan bir sistemde veya istifin bitmediğini tespit etmek için başka yollar, istifin bitmesi felaket olabilir ve sizi tanımsız davranışa sokar. Böyle bir sistemde boş hafıza alanınız tükenir ve sınırsız özyinelemeyle kod yazamazsınız.
Steve Jessop

Yanıtlar:


13

Adres alanında bitişik olmak için yığın gerektirmeyen bir işletim sistemi yazmak mümkündür. Temel olarak, aşağıdakileri sağlamak için çağrı sözleşmesinde fazladan karışıklığa ihtiyacınız var:

  1. aradığınız işlev için geçerli yığın boyutunda yeterli alan yoksa, yeni bir yığın boyutu oluşturur ve arama işaretçisinin başlangıcına işaret etmek için yığın işaretçisini hareket ettirirsiniz.

  2. Bu çağrıdan geri döndüğünüzde, orijinal yığın boyutuna geri dönersiniz. Büyük olasılıkla (1) 'te yaratılanı, aynı iş parçacığı tarafından ileride kullanmak üzere saklarsınız. Prensipte onu serbest bırakabilirsiniz, ancak bu şekilde bir döngü içinde sınır boyunca ileri geri durmaya devam ettiğiniz ve verimsiz durumlarda yatar ve her çağrı hafıza tahsisini gerektirir.

  3. setjmpve longjmp, işletim sisteminizin yerel olmayan kontrol transferi için sahip olduğu eşdeğeri, hareket halindedir ve gerektiğinde eski yığına kadar doğru şekilde geri hareket edebilir.

Ben "çağrı yapma sözleşmesi" diyorum - belirli olmak gerekirse, arayanlar tarafından değil, işlev prologlarında en iyi şekilde yapıldığını düşünüyorum, ancak bununla ilgili hafızam bulanık.

Bir kaç dilin bir iş parçacığı için sabit bir yığın boyutu belirlemesinin nedeni, bunu yapmayan işletim sistemlerinde yerel yığını kullanarak çalışmak istemeleridir . Herkesin cevaplarının söylediği gibi, her yığının adres alanında bitişik olması gerektiği ve taşınamayacağı varsayımı altında, her bir iş parçacığı tarafından kullanılmak üzere belirli bir adres aralığı ayırmanız gerekir. Bu, büyüklükte bir beden seçmek demektir. Adres alanınız çok büyük olsa ve seçtiğiniz boyut gerçekten büyük olsa bile, iki iş parçacığınız olduğu anda onu seçmelisiniz.

“Aha” diyorsunuz, “bitişik olmayan yığınları kullanan bu sözde işletim sistemleri nelerdir? Bahse girerim, bana yararı olmayan bazı karanlık akademik sistemler!”. Bu, neyse ki çoktan sorulan ve cevaplanan başka bir soru .


36

Bu veri yapıları tipik olarak işletim sistemi yığınının sahip olmadığı özelliklere sahiptir:

  • Bağlantılı listeler, bitişik adres alanı gerektirmez. Böylece büyürken istedikleri yerden bir parça hafıza ekleyebilirler.

  • C ++ 'ın vektörü gibi bitişik depolamaya ihtiyaç duyan koleksiyonlar bile işletim sistemi yığınlarına göre bir avantaja sahiptir: Büyüyünce tüm işaretçileri / yineleyicileri geçersiz ilan edebilir. Öte yandan, işletim sistemi yığınının işaretçileri, hedefin karesine ait olan fonksiyon geri dönene kadar geçerli olan yığında tutması gerekir.

Bir programlama dili veya çalışma zamanı, işletim sistemi yığınlarının sınırlandırılmasını önlemek için bitişik olmayan veya hareketli olan kendi yığınlarını uygulamayı seçebilir. Golang, bu gibi özel yığınları, başlangıçta bitişik olmayan bellek olarak uygulanan ve şimdi imleç takibi sayesinde hareketli yığınlar aracılığıyla uygulanan çok sayıda ortak rutini desteklemek için kullanır (hobb'un yorumuna bakın). Yığınsız python, Lua ve Erlang da özel yığınlar kullanabilir, ancak bunu onaylamadım.

64 bit sistemlerde nispeten büyük yığınları nispeten düşük maliyetle yapılandırabilirsiniz, çünkü adres alanı çok fazladır ve fiziksel bellek yalnızca gerçekten kullandığınızda tahsis edilir.


1
Bu iyi bir cevap ve ben sizin anlamınızı takip ediyorum, ancak her bir bellek biriminin kendine özgü bir adresi olduğundan, "sürekli" yerine "bitişik" bir bellek bloğu değil mi?
DanK

2
"Çağrı yığını sınırlı olmak zorunda değil" için +1 genellikle basitlik ve performans için bu şekilde uygulanır, ancak olması gerekmez.
Paul Draper,

Go konusunda oldukça haklısın. Aslında benim anlayışım, eski sürümlerin bitişik olmayan yığınlara sahip olduğu ve yeni sürümlerin hareketli yığınlara sahip olduğu. Her iki durumda da, çok sayıda goroutine izin verilmesi bir zorunluluktur. Yığın için goroutine birkaç megabayt önceden dağıtmak, amaçlarına uygun bir şekilde hizmet etmeleri için fazla pahalı olmasını sağlar.
Ocaklar

@ hobbs: Evet, Go büyüyebilir yığınlarla başladı, ancak onları hızlı hale getirmek zordu. Go kesin bir Çöp Toplayıcı kazandığında, hareketli yığınları uygulamak için üzerine destek verdi: yığın hareket ettiğinde, işaretçileri önceki yığına güncellemek için tam tip harita kullanılır.
Matthieu M.

26

Uygulamada, yığını büyütmek zor (ve bazen imkansız). Neden biraz sanal bellek anlamayı gerektirdiğini anlamak için.

Tek iş parçacıklı uygulamalar ve bitişik hafızanın Ye Olde Günleri'nde üç, bir işlem adres alanının üç bileşeniydi: kod, yığın ve yığın. Bu üçünün nasıl kullanıldığının işletim sistemine bağlı olmasına rağmen, genellikle kod ilk önce belleğin altından başlayarak, öbek sıraya geldi ve yukarı doğru büyüdü ve yığın hafızanın en üstünde başladı ve aşağı doğru büyüdü. İşletim sistemi için ayrılan bir bellek de vardı, ancak bunu görmezden gelebiliriz. O günlerde programlar biraz daha dramatik yığın taşmalarına sahipti: yığın yığına çarpacak ve hangisinin önce güncellendiğine bağlı olarak ya kötü verilerle çalışacak ya da bir alt yordamdan hafızanın keyfi bir bölümüne geri dönecekti.

Bellek yönetimi bu modeli biraz değiştirdi: programın bakış açısına göre hala bir süreç bellek haritasının üç bileşenine sahiptiniz ve genel olarak aynı şekilde düzenlenmişlerdi, ancak şimdi bileşenlerin her biri bağımsız bir bölüm olarak yönetildi ve MMU, İşletim sistemi, programın bir segment dışındaki belleğe erişmeye çalıştığı durumlarda. Sanal hafızası vardı sonra, orada gerek vardı ya arzu onun tüm adres alanına bir program erişimi vermek için. Böylece segmentlere sabit sınırlar verildi.

Öyleyse neden bir programa tam adres alanına erişim vermek istenmiyor? Çünkü bu hafıza takas aleyhinde bir "taahhüt yükü" teşkil eder; herhangi bir zamanda bir programın hafızasının bir kısmı veya tamamı başka bir programın hafızasına yer açmak için değiştirilebilir. Eğer her program potansiyel olarak 2GB takas alabilirse, ya tüm programlarınız için yeterli takas sağlamanız ya da iki programın alabileceğinden daha fazla ihtiyaç duyma şansını yakalamanız gerekir.

Bu noktada, yeterli sanal adres alanı varsaysak, olabilir gerekirse bu segmentleri artırma ve veri segmenti (yığın) aslında zaman içinde büyümek demek: Eğer küçük bir veri segmenti ile başlar ve bellek ayırıcısı fazla yer zaman istediğinde gerekli. Bu noktada, tek bir yığınla yığın bölümünü genişletmek fiziksel olarak mümkün olurdu: İşletim sistemi bölüm dışına bir şey itme ve daha fazla bellek ekleme girişimini yakalayabilir. Ancak bu da özellikle istenmez.

Çoklu iş parçacığı girin. Bu durumda, her iş parçacığının bağımsız bir yığın kesimi vardır, yine sabit boyutludur. Ancak şimdi sanal adres alanında bölümler birbiri ardına sıralanmıştır, bu nedenle bir bölümü diğerini hareket ettirmeden genişletmenin bir yolu yoktur - çünkü bunu yapmadan önce programın yığında yaşayan hafızayı işaret edecek potansiyele sahip olması muhtemeldir. Alternatif olarak bölümler arasında bir miktar boşluk bırakabilirsiniz, ancak bu alan neredeyse her durumda boşa gider. Uygulama geliştiricisinin yükünü koymak daha iyi bir yaklaşımdı: gerçekten derin yığınlara ihtiyacınız varsa, iş parçacığını oluştururken bunu belirtebilirsiniz.

Bugün, 64 bitlik bir sanal adres alanıyla, sonsuz sayıda iplik için etkili bir şekilde sonsuz yığınlar oluşturabildik. Fakat yine de, bu özellikle istenen bir durum değil: neredeyse her durumda, bir yığın taşması kodunuzla ilgili bir hata olduğunu gösterir. Size 1 GB'lık bir yığın sağlamak, yalnızca bu hatanın keşfedilmesini önler.


3
Mevcut x86-64 CPU'lar sadece 48 bit adres alanına
sahipler

Afaik, linux yapar dinamik yığın büyümek: Bir süreç çalışır şu anda ayrılmış yığını altında alan hakkına erişmek istediğinde, kesilme yalnızca yığın bellek ek bir sayfasını haritalama yerine sürecini segfaulting tarafından gerçekleştirilmektedir.
cmaster

2
@cmaster: true, ancak kdgregory'nin "yığını büyüt" ile kastettiği şey değil. Şu anda yığın olarak kullanılmak üzere belirlenmiş bir adres aralığı var. Kademeli olarak daha fazla fiziksel belleği bu adres aralığına haritalandırmaktan bahsediyorsunuz. kdgregory, menzili arttırmanın zor veya imkansız olduğunu söylüyor.
Steve Jessop

x86 tek mimari değil ve 48 bit hala etkili bir şekilde sonsuz
kdgregory

1
BTW, x86 ile çalıştığım günlerin, bölümleme ile uğraşma gereği nedeniyle çok eğlenceli olmadığını hatırlıyorum. Ben MC68k platformlarında projeleri tercih ettim ;-)
kdgregory

4

Sabit bir maksimum boyuta sahip olan yığın her yerde bulunmaz.

Doğru olması da zordur: yığın derinlikleri bir Güç Yasası Dağıtımını takip eder, bu da yığın boyutunu ne kadar küçük yaparsanız yapın, daha küçük yığınlara sahip fonksiyonların önemli bir bölümünü hala tutar. ve ne kadar büyüttüğünüz önemli değil, yine de daha büyük yığınlara sahip işlevler olacaktır (bu nedenle, hatasız işlevler için bir yığın taşması hatası zorlarsınız). Başka bir deyişle: ne boyutta olursanız olun, aynı anda her zaman hem küçük hem de büyük olacaktır.

İlk sorunu, yığınların küçülmesine ve dinamik olarak büyümesine izin vererek düzeltebilirsiniz, ancak daha sonra ikinci sorunu yaşarsınız. Yığının yine de dinamik olarak büyümesine izin verirseniz, neden üzerinde isteğe bağlı bir sınır koymalısınız?

Örneğin yığınların dinamik olarak büyüyebildiği ve maksimum boyuta sahip olmadığı sistemler var: örneğin Erlang, Go, Smalltalk ve Scheme. Böyle bir şeyi uygulamak için birçok yol vardır:

  • hareketli yığınlar: bitişik yığın artık büyüyemediğinde, başka bir şey olduğu için, bellekte başka bir yere, daha fazla boş alan olacak şekilde hareket edin
  • bitişik olmayan yığınlar: tüm yığını tek bir bitişik bellek alanında ayırmak yerine, bunu birden fazla bellek alanında ayırın
  • yığın tahsisli yığınlar: yığın ve yığın için ayrı bellek alanlarına sahip olmak yerine, yığını yığında tahsis et; fark ettiğiniz gibi, yığınla ayrılan veri yapılarının gerektiği gibi büyümekte ve küçülmekte herhangi bir problemi yoktur.
  • hiç istif kullanmayın: bu aynı zamanda bir seçenektir, örneğin fonksiyon durumunu bir yığında takip etmek yerine, fonksiyonun callee'ye devam etmesini sağlayın

Güçlü yerel olmayan kontrol akışlı yapılara sahip olduğunuzda, tek bir bitişik yığın fikri, yine de pencereden dışarı çıkar: yeniden başlatılabilir istisnalar ve süreklilikler, örneğin yığını "çatallar", böylece aslında bir ağ ile sonuçlanırsınız yığınları (örneğin bir spagetti yığını ile uygulanır). Ayrıca, Smalltalk gibi birinci sınıf değiştirilebilir yığınlara sahip sistemler, hemen hemen spagetti yığınları veya benzer bir şey gerektirir.


1

Bir yığın talep edildiğinde işletim sisteminin bitişik bir blok vermesi gerekir. Bunu yapabilmenin tek yolu maksimum boyut belirtilmiş olmasıdır.

Örneğin, istek sırasında hafızanın bu şekilde göründüğünü varsayalım (Xs kullanılmış, Os kullanılmamış):

XOOOXOOXOOOOOX

Yığın boyutu 6 olan bir istekte, 6'dan fazla olsa bile, OS cevabı hayır olarak cevap verecektir. 3 boyutunda bir yığın için bir talep varsa, OS cevabı üst üste 3 boş alandan (Os) alanlardan biri olacaktır.

Ayrıca, bir sonraki bitişik yuva işgal edildiğinde, büyümeye izin vermenin zorluğunu görebilir.

Bahsedilen diğer nesneler (Listeler, vb.) Yığına gitmezler, bitişik olmayan veya parçalanmış alanlarda yığına çıkarlar, yani büyüdüklerinde sadece yer kaplarlar, oldukları gibi bitişik olmaları gerekmez farklı yönetildi.

Çoğu sistem yığın boyutu için makul bir değer belirler, daha büyük bir boyut gerekiyorsa iplik oluşturulduğunda geçersiz kılabilirsiniz.


1

Linux'ta bu, yalnızca kaynağın zararlı miktarlarını tüketmeden önce kaçak işlemleri öldürmek için var olan bir kaynak limitidir. Debian sistemimde aşağıdaki kod

#include <sys/resource.h>
#include <stdio.h>

int main() {
    struct rlimit limits;
    getrlimit(RLIMIT_STACK, &limits);
    printf("   soft limit = 0x%016lx\n", limits.rlim_cur);
    printf("   hard limit = 0x%016lx\n", limits.rlim_max);
    printf("RLIM_INFINITY = 0x%016lx\n", RLIM_INFINITY);
}

çıktı üretir

   soft limit = 0x0000000000800000
   hard limit = 0xffffffffffffffff
RLIM_INFINITY = 0xffffffffffffffff

Sabit sınırın şu değere ayarlandığını unutmayın RLIM_INFINITY: İşlemin yumuşak sınırını herhangi bir miktarda yükseltmesine izin verilir . Bununla birlikte, programcının programın olağandışı miktarlarda yığın belleğe ihtiyacı olduğuna inanması için bir neden olmadığı sürece, işlem sekiz mebibit yığın büyüklüğünü aştığında öldürülür.

Bu sınırlama nedeniyle, kaçak bir işlem (istenmeyen sonsuz yinelenme), sistemin değişmeye başlaması için çok fazla miktarda bellek tüketmeye başlamadan çok önce öldürülür. Bu, düşen bir işlem ile düşen bir sunucu arasındaki farkı yaratabilir. Bununla birlikte, büyük bir yığına meşru bir ihtiyaç duyan programları sınırlamaz, sadece yumuşak sınırı uygun bir değere ayarlamaları gerekir.


Teknik olarak, yığınlar dinamik olarak büyür: Yumuşak sınır sekiz mebibite ayarlandığında, bu bellek miktarının henüz eşlendiği anlamına gelmez. Çoğu program kendi yumuşak sınırlarının yakınında hiçbir zaman yer alamadığından, bu oldukça israf olacaktır. Aksine, çekirdek yığının altındaki erişimleri algılar ve gerektiğinde yalnızca bellek sayfalarında eşler. Bu nedenle, yığın boyutundaki tek gerçek sınırlama 64 bit sistemlerdeki bellektir (adres alanı parçalanması, 16 zebibit adres alanı boyutuyla oldukça teoriktir).


2
Bu sadece ilk iş parçacığı için yığın. Yeni dişlilerin yeni yığınlar tahsis etmesi gerekir ve diğer nesnelere çarpacakları için sınırlıdır.
Zan Lynx

0

Maksimum olmasıdır, çünkü yığın boyutu statik tanımı "fazla" . Herhangi bir şey için maksimum değer, sabit ve kararlaştırılmış sınırlayıcı bir rakamdır. Kendiliğinden hareket eden bir hedef gibi davranırsa, maksimum değildir.

Sanal bellek işletim sistemindeki yığınlar gerçekte , maksimuma kadar dinamik bir şekilde büyür .

Bundan bahsetmek, statik olmak zorunda değildir. Aksine, işlem başına veya iş parçacığı bazında bile yapılandırılabilir.

Sorunuz, "neden olduğu (genelde çok daha az kullanılabilir bellek daha yapay olarak empoze bir) maksimum yığın boyutu var"?

Bunun bir nedeni, çoğu algoritmanın çok büyük miktarda yığın alanı gerektirmemesidir. Büyük bir yığın olası bir kaçak özyinelemenin bir göstergesidir . Kullanılabilir tüm belleği ayırmadan önce kaçak tekrarlama işlemini durdurmak iyidir. Kaçak özyinelemeye benzeyen bir sorun, belki de beklenmeyen bir test durumu tarafından tetiklenen dejenere yığın kullanımıdır. Örneğin, bir ikili ayrıştırıcı için ayrıştırıcı varsayalım, infix operatörü sağ operandda tekrarlayarak çalışır: ilk operand'ı ayır, scan operatörü, ifadenin geri kalanını ayrıştır. Yığın derinliği ifade uzunluğu ile orantılı olduğu bu şekilde gerçekleşir: a op b op c op d .... Bu formun büyük bir test durumu çok büyük bir yığın gerektirir. Makul bir yığın limitine ulaştığında programın iptal edilmesi bunu yakalayacaktır.

Sabit bir maksimum yığın boyutunun diğer bir nedeni, o yığın için sanal alanın özel bir eşleme yoluyla ayrılabilmesi ve böylece garanti edilebilmesidir. Garantili, alanın, limite ulaşmadan önce yığının kendisiyle çarpışacağı başka bir tahsise verilmeyeceği anlamına gelir. Bu eşlemeyi istemek için maksimum yığın boyutu parametresi gereklidir.

İplikler buna benzer bir nedenden dolayı maksimum yığın boyutuna ihtiyaç duyar. Yığınları dinamik olarak oluşturulur ve bir şeyle çarpışırlarsa taşınamazlar; sanal alanın önceden ayrılmış olması gerekir ve bu tahsis için bir boyut gerekir.


@Lynn Maksimum boyutun neden statik olduğunu sormadı, neden önceden tanımlandığını sordu.
Calderwood
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.