RustProgramlamaRust Sistem Geliştirici

UnsafeCell'in içsel değişkenliği nasıl sağladığını tanımlayın ve ham işaretçisini dereferans ederken güvenli bellek ilkesi neyi korumalıdır.

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

Cevap

Soru Tarihçesi

Rust'ın başlangıcında, tasarımcılar kritik bir çıkmaza girdi: döngüsel grafikler ve zamanlı ödünç kontrolü olan kapsayıcılar gibi temel veri yapıları, paylaşılan referanslar aracılığıyla değişiklik gerektiriyordu; ancak bu durum, dilin özel değişken erişim ilkesine doğrudan aykırıydı. Bunu sıfır maliyetli soyutlama ilkesini ihlal etmeden çözmek için, UnsafeCell paylaşılan referanslarla ilişkili değişmezlik garantisini devreden çıkaran tek ilke olarak tanıtıldı ve tüm güvenli içsel değişkenlik soyutlamalarının temeli haline geldi.

Problem

Rust derleyicisi, referansın ömrü boyunca altında yatan belleğin değişmeyeceğini varsayarak, değer önbellekleme ve talimat yeniden sıralama gibi agresif optimizasyonlar gerçekleştirmek için &T'nin değişmezliğini kullanır. UnsafeCell, içeriğinin paylaşılan bir referans aracılığıyla bile değişebileceğini derleyiciye bildiren bir işaretçi olarak kullanılır ve böylece kapsayıcı veriler için bu optimizasyonların devre dışı bırakılmasını sağlar. Ancak, bu devre dışı bırakma, UnsafeCell::get() üzerinden elde edilen ham işaretçiden türetilen referansları kapsamamaktadır; bu işaretçi &mut T'ye dönüştürüldüğünde, standart paylaşımlı kurallar yeniden kesin bir şekilde uygulanır.

Çözüm

Çözüm, programcının UnsafeCell'in ham işaretçisinden üretilen her &mut T değişken referansının, o belleğe erişim için tek aktif yol olması gerekliliğini sağlamasıdır. Bu ayrıcalık, değişken referansın varlığı sırasında başka bir işaretçi, referans veya sonraki get() çağrısının üzerinden eşzamanlı okuma veya yazmaya izin vermez. UnsafeCell, ödünç alıcıyı devre dışı bırakmaz; yalnızca zamansal ayrıcalığı garanti etme ve veri yarışlarını önleme sorumluluğunu derleyiciden geliştiriciye aktarır.

Gerçek Hayat Durumu

Problem Tanımı

Düşük gecikmeli bir ticaret sistemi için yüksek verimli bir metrik toplayıcı tasarlıyorduk, burada çoklu iş parçacıkları belirli finansal enstrümanlara bağlı sayaçları güncelleyordu. Paylaşılan harita başlatmadan sonra değişmezdi, ancak metrik değerleri sık sık artırılmak zorundaydı. Mutex<u64> kullanmak kabul edilemez bir rekabet yarattı, AtomicU64 ise karmaşık bileşik metrik türleri için yetersiz kaldı. Biz, runtime ödünç alma kontrolleri olmadan Arc işaretçileri ardındaki yapıları kilitsiz, sıfır tahsilli güncellemelere ihtiyaç duyuyorduk.

Düşünülen Farklı Çözümler

Çözüm 1: Sharded Mutex'ler

Her metriği bir Mutex içine sarmayı ve bunları 256 parçaya dağıtmayı değerlendirdik. Bu yaklaşım basit bir güvenlik ve kolay, sürdürülebilir bir kod sundu. Ancak, profil oluşturma, çatışmasız Mutex işlemlerinin futex sistem çağrıları ve önbellek tutarlılık protokolleri nedeniyle yüzlerce nanosaniye harcadığını, bu durumun ise katı alt mikrosaniye gecikme bütçemizi ihlal ettiğini ortaya koydu.

Çözüm 2: AtomicPtr ile Kutulu Değerler

Başka bir yaklaşım, değerleri AtomicPtr<Metric> olarak saklamayı ve güncellemeler için karşılaştırma ve değişim döngüleri kullanmayı içeriyordu. Bu, engellemeleri ortadan kaldırdı ancak her artırma için yeni Box örneklerinin tahsis edilmesini zorunlu kıldı; bu da ciddi bellek baskısı ve tahsisat rekabetine yol açtı. Ayrıca, bellek geri kazanımını karmaşıklaştırdı ve önemli derecede kod karmaşıklığı ve denetim alanı artıran tehlike işaretçileri veya dönem tabanlı çöp toplama gerektirdi.

Çözüm 3: Cache-Line Hizalaması ile UnsafeCell

Metrikleri, farklı parçaları yazan iş parçacıkları tarafından asla paylaşılamayan önbellek satırı hizalı yapılar içerisinde UnsafeCell<Metric> içinde saklamayı seçtik. Her iş parçacığı, güncelleme sırasında bir ham işaretçi elde eder, bunu güncelleme sırasında &mut Metric'ye dönüştürür ve parçalama mantığımız, başka bir iş parçacığının o belirli yuvaya erişemeyeceğini garanti ettiği için bu işlemi güvenli hale getirir ve değişikliği gerçekleştirir. Bu, unsafe bloklar gerektirdi ve tutarlı hash'imizin eşzamanlı erişimler sırasında çakışma sağlamadığını resmi bir kanıt sağlar.

Hangi çözüm seçildi ve neden

Çözüm 3'ü seçtik çünkü bu, ham bellek üzerinde sıfır maliyetli soyutlama sağlarken, agresif gecikme gereksinimlerini de karşıladı. Parçalama garantisi, özel erişimin manuel bir kanıtı olarak hareket etti ve bize UnsafeCell'i çalışma zamanı senkronizasyon maliyeti olmadan kullanma olanağı sağladı. MIRI ve loom eşzamanlılık model kontrolcüsünü kullanarak güvenliği doğruladık ve tüm olası iş parçacığı ara kesmelerinde herhangi bir paylaşımlı ihlalinin gerçekleşmediğinden emin olduk.

Sonuç

Uygulama, sıcak yol boyunca sıfır tahsisat ile 100 nanosaniye altı güncelleme gecikmeleri elde etti. Ancak, bir sonraki yeniden yapılandırma sırasında bakım görevi yanlışlıkla tüm parçaları dolandırarak, örtük parça kilidini almayı unutarak aynı metrik için iki değişken referans oluşturdu. MIRI bunu hemen tanıdı ve CI sırasında tanımsız davranış olarak işaretledi, bu da UnsafeCell'in disiplin gerektirdiğini pekiştirdi.

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

Neden aynı anda bir UnsafeCell'den türetilen iki değişken referansa sahip olmak tanımsız bir davranıştır, oysa UnsafeCell açıkça standart ödünç alma kurallarından muaf olduğunu belirtiyor?

UnsafeCell, tür seviyesi açısından paylaşılan referanslar için değişmezlik garantisini devreden çıkarmakta; ancak bu, &mut T türünün temel ilkesini gevşetmez. Get() çağrısı yaptığınızda, hayat süresi veya paylaşımlı kısıtlamalar taşımayan bir ham işaretçi *mut T alırsınız. Ancak bu işaretçiyi bir &mut T'ye dereferans ettiğiniz an, bu referansın özel olduğunu derleyiciye beyan edersiniz. Aynı UnsafeCell'den gelen, örtüşen belleğe iki böyle referans oluşturarak Rust'ın bellek modelini temel alan paylaşım XOR değişiklik kuralını ihlal etmiş olursunuz; bu, referansların nasıl oluşturulduğundan bağımsız olarak hemen tanımsız bir davranışa yol açar.

MIRI, UnsafeCell ilkesinin ihlallerini nasıl tespit eder ve neden kod üretim testlerinden geçerken MIRI altında başarısız olabilir?

MIRI, bellek erişim izinlerini soyut "etiketler" aracılığıyla takip eden Stacked Borrows (veya isteğe bağlı olarak Tree Borrows) paylaşımlı modelini uygular. UnsafeCell'den bir referans oluşturduğunuzda, MIRI eşsiz bir etiket atar. İlk referans aktifken aynı belleğe erişmek için farklı bir etiketi kullanmaya çalışmak bir ihlal teşkil eder. Kod genellikle standart testleri geçer çünkü donanım bellek modelleri affedicidir ve masum veri yarışları pratikte çökme yaratmaz. MIRI, teorik modeli titizlikle uygular ve uygun senkronizasyon olmadan aynı UnsafeCell'den bir paylaşılan referans oluşturarak bir değişken referansını geçersiz kılmanın gibi ihlalleri yakalar, bu, mevcut CPU mimarisi üzerinde çalışsa bile.

Cell<T> neden değişiklik için unsafe bloklar gerektirmezken UnsafeCell<T> neden gerektirir ve bu ayrımı sağlayan belirli güvenlik garantisi nedir?

Cell<T>, iç verilerine referansları asla ortaya çıkarmayarak içsel değişkenliği unsafe olmadan sağlar; yalnızca Copy uygulayan türler için değerlerin içeri alınmasına (set) veya dışarı alınmasına (get) ya da Copy olmayan türler için taşınmasına (replace) izin verir. Cell, asla içindeki değere bir &T veya &mut T vermediği için paylaşım kurallarını ihlal etmek imkânsızdır; alias'e mevcut referans yoktur. UnsafeCell ise, referansların oluşturulmasına izin veren bir ham işaretçi *mut T döndüren get() sağlar. Bu esneklik karmaşık yerinde değişiklikler için gerekli olsa da, özel erişimi sağlama ve veri yarışlarını önleme yükümlülüğünü tam olarak programcının üzerine aktarır ve unsafe blokları gerektirir.