RustProgramlamaRust Sistem Geliştiricisi

**GlobalAlloc** ve **Allocator** ile ilgili temel güvenlik ikiliğini, ilki için **unsafe** uygulamaları zorunlu kıldığı nedenleri detaylandırarak açıkla ve ham bellek tahsisi sırasında yanlış **Layout** yönetiminin neden olduğu belirli tanımsız davranış risklerini tanımla.

Hintsage yapay zeka asistanı ile mülakatları geçin

Cevap

Tarih: Rust'ın bellek yönetimi, bir global tahsisatçı arayüzünden (GlobalAlloc, Rust 1.28'de stabil hale geldi) daha esnek, tür farkında bir sisteme (Allocator, şu anda kararsız ancak std::alloc içinde mevcut) evrildi. GlobalAlloc, işletim sisteminin bellek ilkelere (örneğin, malloc, VirtualAlloc) düşük seviyeli köprü olarak hizmet eder; yalnızca ham işaretçiler ve byte boyutları üzerinde tür bilgisi olmadan çalışır.

Sorun şu ki, GlobalAlloc ham bellek manipülasyonunu ifşa eder ve derleyici bunu doğrulayamaz. Uygulayıcılar, kritik invariatları manuel olarak sağlamalıdır: hizalama garantileri, tahsisat/de-tahsisat eşleştirmesi ve çift serbest bırakmaların yasaklanması. GlobalAlloc, Box, Vec ve Rc ile desteklendiğinden, herhangi bir ihlal tanımsız davranışı programın tamamına yayar ve bu güvenlik sözleşmelerinde programcının sorumluluk aldığını belirtmek için unsafe impl işareti gerektirir.

Çözüm, Layout sözleşmesine sıkı sıkıya bağlı kalmaktır. alloc yöntemi, Layout::align() ile uyumlu bir işaretçi döndürmelidir ve dealloc yalnızca tahsisat için kullanılan aynı düzen ile çağrılmalıdır. Ayrıca, tahsisatçı, belleğin, güvenli soyutlamalar tarafından hala referans alındığı sürede geri kazanılmadığını sağlamalıdır. Allocator özelliği, içsel olarak Layout hesaplamalarını yöneterek bu riskleri hafifletir ve tehlikeli işlemleri altında yatan GlobalAlloc uygulamalarına delege eder.

use std::alloc::{GlobalAlloc, Layout, System}; use std::sync::atomic::{AtomicUsize, Ordering}; struct CountingAllocator { bytes_allocated: AtomicUsize, } unsafe impl GlobalAlloc for CountingAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let ptr = System.alloc(layout); if !ptr.is_null() { self.bytes_allocated.fetch_add(layout.size(), Ordering::SeqCst); } ptr } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { System.dealloc(ptr, layout); self.bytes_allocated.fetch_sub(layout.size(), Ordering::SeqCst); } } #[global_allocator] static GLOBAL: CountingAllocator = CountingAllocator { bytes_allocated: AtomicUsize::new(0), };

Gerçek Hayattaki Durum

Yüksek frekanslı ticaret motoru geliştiren bir ekip, standart kütüphanenin tahsisatçısının global heap'teki kilitlenmeler nedeniyle kabul edilemez gecikmeli dalgalanmalar oluşturduğunu gözlemledi. NUMA-yerli, deterministik bellek erişimini sağlamak için dev bir sayfadan önceden tahsis edilmiş özel bir bump tahsisatçısına ihtiyaçları vardı.

Birçok çözüm değerlendirildi. İlk yaklaşım, sistem tahsisatçısını bir mutex ile korunan havuz ile sarmayı düşündü, ancak bu yalnızca rekabeti kaydırdı ve gecikme gereksinimlerini ihlal etti. İkinci yaklaşım, kararsız Allocator API'sini kullanarak belirli sipariş yapıları için türlendirilmiş bir arenayı oluşturmayı içeriyordu; ancak bu, kod tabanında Vec ve Box kullanımlarının kapsamlı bir şekilde yeniden yapılandırılmasını gerektiriyordu ve üretim dağıtımı için kararlılık endişeleriyle karşılaştı.

Sonunda seçilen üçüncü çözüm, ticaret ipliği içindeki tüm dinamik tahsisatı kesmek için GlobalAlloc'u uygulamak oldu ve bunları mmap bölgeleri ile desteklenen iplik yerel bir bump tahsisatçısına yönlendirdi. Bu uygulama, bump tahsisatçısının ham işaretçileri yönetmesi ve döndürülen işaretçilerin 64-byte önbellek sınırı hizalamalarını korumasını garanti etmesi gerektiğinden unsafe impl gerektirdi. Ekip, mevcut koleksiyon türlerini değiştirmeden sistem çapında müdahale sağladığı için bu yolu seçti; ancak, her zaman dealloc'a geçen Layout'un orijinal tahsisat ile eşleştiğini doğrulamak için Miri ile titiz testler yapılmasını zorunlu kıldı. Sonuç olarak p99 gecikmesinde %40'lık bir azalma elde edildi, ancak ekip olağanüstü piyasa dalgalanmaları sırasında bellek sızıntılarını önlemek için unsafe kod blokları için sıkı bir denetim protokolü sürdürdü.

Adayların Sıklıkla Gözden Kaçırdığı Noktalar

dealloc'ya geçirilen Layout'un neden tam olarak alloc'a verilenle eşleşmesi gerektiğini ve eğer boyut farklıysa, hizalamanın doğru olmasının ne olacağını kaçıranlar genellikle ne olur? GlobalAlloc sözleşmesi, tahsisat ve de-tahsisat için kullanılan Layout arasında bit düzeyinde kimlik gerektirir, çünkü birçok tahsisatçı (örneğin jemalloc veya dlmalloc) tahsis edilen blok içinde metadata gömer veya boyut sınıfı ayrılmış listeleri sürdürür. Farklı bir boyut geçmek—hatta daha küçük bir boyut—tahsisatçının yanlış bir bölümde aramasına veya birleşme için yanlış bir ofset hesaplamasına yol açarak yığın bozulmasına veya çift serbest bırakma zafiyetlerine neden olur. Bu, C'deki free ile farklıdır, bu genellikle yalnızca işaretçiyi gerektirir, bu da Rust'ın gereksinimlerini daha sıkı ama tahsisatçıdan bağımsız olmak için gerekli hale getirir.

GlobalAlloc, Box::new ile etkileşime nasıl geçer ve kutu daha sonra bırakıldığında, tahsisatçı için Drop'ı uygulamak neden sorunludur?**

Box::new çağrıldığında, #[global_allocator] statik aracılığıyla GlobalAlloc::alloc çağrılır. Box bırakıldığında, derleyici türün Layout'ı otomatik olarak hesaplanarak GlobalAlloc::dealloc çağrısını ekler. Adaylar genellikle GlobalAlloc uygulamasının kendisinin 'static ve iplik güvenli (Sync'i uygulayan) olması gerektiğini ancak yönettiği tahsis edilen belleği referans alan bir durum tutmaması gerektiğini gözden kaçırır, çünkü bu, tahsisatçının bırakılmasını gerektiren bir dairesel bağımlılık oluşturur; programın kapanması sırasında kullanımdan sonra serbest bırakma durumuna neden olabilir.

alloc_zeroed ile alloc'un güvenlik gereksinimlerini ne ayırır ve neden uygulama bunun yerine alloc'u çağırıp ardından std::ptr::write_bytes kullanamaz?**

alloc_zeroed, teorik olarak alloc artı sıfırlama olarak uygulanabilse de, standart kütüphane, tahsisatçıların OS'ye özgü sıfırlanmış sayfa optimizasyonlarından yararlanmasına izin vermek için onu ayrı bir yöntem olarak sağlar (örneğin, MAP_ANONYMOUS Linux'ta sıfırlanmış sayfalar döndürür). Güvenlik açısından, alloc_zeroed döndürülen belleğin sıfır byte içerdiğini garanti etmelidir; bu, alloc (başlangıçta başlatılmamış bellek döndüren) ile karşılaştırıldığında daha güçlü bir sonucudur. Eğer bir uygulama sıfırlama iddiasında bulunursa ama çöp döndürürse, sıfır ile başlatma varsayımında bulunan güvenli kod (güvenlik duyarlı bağlamlar için kritik) başlangıçta başlatılmamış verileri okuyarak Rust'ın güvenlik garantilerini ihlal edecektir.