RustProgramlamaRust Developer

Yürütme sırasında bir `select!` dal iptali sırasında asenkron bir geleceğin bırakılmasının ortaya çıkardığı bellek güvenliği tehlikelerini açıklayın ve iptal sırasında kaynak tutarlılığını sağlamak için uygulanması gereken mimari desenleri -örneğin, drop-guard ifadesi- detaylandırın.

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

Cevap

Bir asenkron geleceğin, await noktasında askıya alındığında (örneğin, bir kardeş dalın tokio::select! içinde tamamlandığında) bırakılması durumunda, tutulan kaynakları yok etmek için Drop uygulaması senkron bir şekilde çalışır. Tehlike, geleceğin asenkron temizleme gerektiren kaynakları sahiplenmesi durumunda ortaya çıkar - örneğin bir TcpStream'i boşaltmak, bir protokol kapanış çerçevesi göndermek veya bir veritabanı işlemine taahhüt etmek gibi - çünkü Drop özelliği asenkron bağlam sağlamaz. Eğer gelecek, durumu kısmen değiştirdikten sonra (örneğin, bir dosya tamponunun yarısını yazarak) iptal edilirse, ama sonlandırmadan önce senkron Drop temizleme işlemlerinin tamamlanması için .await bekleyemez, bu da sistemi tutarsız bir durumda bırakabilir veya kaynak sızıntısına neden olabilir. Mimari çözüm, kaynakları saracak bir koruma yapısında drop-guard deseninin uygulanmasıdır; bu, Drop uygulamasının ya senkron bir yedek temizleme (bloklama risklerini kabul ederek) planlaması ya da kaynağı bir bağlantısız temizleme görevine dönüştürmesini sağlar, böylece kritik invariantın (örneğin, geçici dosya silinmesi) temizleme işlemleriyle birlikte asenkron kod gerektirmeden uygulanmasını sağlar.

Hayattan bir durum

Yüksek hızlı bir medya alım servisi geliştirdik; burada tokio::spawn eşzamanlı dosya yüklemelerini işledi. Her yükleme görevi geçici bir dosyaya parçalar yazdı, harici bir işlem aracılığıyla virüs taraması yaptı ve sonunda onaylanmış dosyayı atomik olarak kalıcı bir depolama alanına taşıdı. Gereklilik katıydı: eğer müşteri bağlantıyı keserse (virüs taraması ve atomik taşıma arasında select! aracılığıyla görev iptali tetiklerse), geçici dosyanın hemen silinmesi gerekiyordu, aksi takdirde disk alanı tükenebilirdi.

Çözüm 1: Drop'da senkron temizleme. std::fs::File ve yol dizesini saran bir TempFileGuard yapısı uyguladık. Drop uygulamasında, geçici dosyayı silmek için senkron olarak std::fs::remove_file çağrısını gerçekleştirdik. Artıları: Kod basit ve yığın çözülmesi veya iptali sırasında yürütülmesi garanti ediliyordu. Eksileri: std::fs::remove_file, bloklayıcı bir sistem çağrısıdır. Tokio çalışma zamanının işçi iş parçacıklarında çalışırken, bu, yüksek disk yükü altında iş parçacığını milisaniyelerce bloke etti, diğer görevler aç bırakıldı ve asenkron bloklama sözleşmesini ihlal etti. Ayrıca, eğer geçici dosya bir ağ dosya sistemi (NFS) üzerinde bulunuyorsa, blok süresi saniyelere uzanabilir ve felaket gecikme baloncuklarına yol açabilirdi.

Çözüm 2: Başlatılan temizleme görevi. Koruyucunun Drop'unda, yol dizesini yakaladık ve tokio::fs::remove_file'ı asenkron olarak çalıştırmak için ayrık bir tokio::task oluşturduk. Artıları: Bu, denetimi hemen çalışma zamanına geri verdi ve gecikmeyi korudu. Eksileri: Eğer çalışma zamanı zaten kapanıyorsa veya aşırı yük altındaysa, temizleme görevi hiç yürütülmeyebilir, bu da kaynak sızıntılarına yol açabilir. Ayrıca, bu desenin koruyucunun çalışma zamanı için bir Clone tutması gerekiyordu, bu da yapının ömrünü karmaşıklaştırıyordu ve eğer çalışma zamanı koruyucudan önce düşerse potansiyel olarak kullanımdan sonra serbest bırakma sorunları ortaya çıkarıyordu.

Çözüm 3: Senkron yedek ile açık iptal belirteci. tokio_util::sync::CancellationToken'ı kullandık ve yükleme mantığını atomik taşımadan önce iptal kontrolü için yapılandırdık. İptal durumunda, dosya belirli bir boyut eşiğinin altındaysa (hızlı silme) yalnızca senkron bir silme denemesi yapıldı, aksi takdirde, bir kanal aracılığıyla (yani std::thread kullanılarak oluşturulan) ayrı bir arka plan temizleme iş parçacığına sıraya alındı. Koruyucunun Drop'u yalnızca panik durumunun nadir bir kenar durumunu ele aldı ve senkron silmeyi son çare olarak kullandı. Seçilen çözüm: Seçimimiz 3 numaralı çözümdü. Küçük dosyalar için belirleyicilik (senkron yol) ve ölçeklenebilirlik (yavaş işlemler için arka plan iş parçacığı) arasında denge sağladı ve Tokio işçilerini bloke etmeyi önledi. Sonuç, 10,000 eşzamanlı iptalle yük testi sırasında sıfır sızan geçici dosya oldu ve p99 gecikmesi stabil kaldı çünkü arka plan iş parçacığı NFS gecikme cezasını Absorbe etti.

Adayların genellikle kaçırdığı şeyler


Neden Drop uygulaması içinde asenkron temizleme gerçekleştirmek için block_on çağrısının çoğu asenkron çalışma zamanında temelde sağlam olmadığı?

Drop içinde block_on çağrısı yapmak, yeniden giriş tehlikesi yaratır. Drop, yığın çözülmesi sırasında veya bir geleceğin iptal edilmesi durumunda senkron bir şekilde çağrılır. Eğer mevcut iş parçacığı Tokio (veya async-std) çalışma zamanının işçi iş parçacığıysa, block_on yeni geleceği tamamlamak için reaktörü çalıştırmaya çalışacaktır. Ancak, çalışma zamanı zaten mevcut görevin iptal edilen iş parçacığının serbest bırakılmasını bekliyor. Bu, bir felç durumuna yol açar: block_on, temizleme geleceğinin reaktörün anket yapmasını bekler, ancak reaktör ilerleme kaydedemez çünkü iş parçacığı block_on içinde bloke olmuştur. Ayrıca, Tokio gibi çalışma zamanları, bu durumu önlemek için iç içe geçmiş block_on çağrıları tespit edildiğinde açıkça panik yapar. Doğru yaklaşım, temizliği senkron olarak (ani ise) gerçekleştirmek veya bir kanalı kullanarak ayrık bir iş parçacığına devretmek ve bir yok edicide asenkron yürütücüyü bloke etmemektir.


Future::poll metodunun tasarımı, iptalin yalnızca await noktalarında gerçekleşmesini neden sınırlıyor ve bu, kritik bölüm tasarımı için neden önemlidir?

Future::poll metodu senkroniktir ve Poll::Ready veya Poll::Pending'i hemen döndürmelidir; bu, yürütme sırasında duramaz. Bir await noktası, poll Pending döndüğünde, derleyici tarafından oluşturulan durum makinesi arasında geçiş yapmak için sözdizimsel bir şekildir. Yürütücü (veya select! makrosu), geleceği aktif bir şekilde çalışırken bırakamaz; özel olarak, Pending döndüğünde ve kontrolü bıraktığında. Dolayısıyla, iptal, poll çağrılarıyla atomiktir. Bu, iki await noktası arasındaki herhangi bir kodun (bir "kritik bölüm") asenkron çalışma zamanı açısından tamamen ya da hiç yürütülmediğini garanti eder. Ancak, eğer bir gelecek, bir await üzerinde bir MutexGuard tutuyorsa (bu, standart Mutex için Rust tarafından yasaklanmıştır fakat tokio::sync::Mutex için izin verilmiştir), iptal paylaşılan verileri tutarsız bir durumda bırakabilir. Adaylar genellikle, her await noktasından önce veri yapısı invariantlarının geri kazandırılmasını sağlamaları gerektiğini unutur, yalnızca fonksiyonun sonunda değil, çünkü iptal, bu askıya alma noktasında tüm canlı değişkenler için Drop'u çalıştırır.


std::pin::Pin bağlamında, select! içinde kullanılan geleceklerin neden ya Unpin olması ya da açıkça sabitlenmiş olması gerektiği ve bunun kısmi bırakma sırasında bellek güvenliğini nasıl sağladığı?

select!, birden fazla geleceği rastgele anket yapar. Eğer bir gelecek !Unpin'se (örneğin, kendi kendine referans veren işaretçiler veya müdahil liste bağlantıları içeriyorsa), ilk poll'den sonra hareket ettirilmesi bu işaretçileri geçersiz kılar. Pin, geleceğin bellek konumunun sabit kalmasını garanti eder. select!, geleceklerin Unpin (hareketlere izin veren) olmasını veya belirli bir bellek konumuna (yığın veya yığın) yönelik olarak zaten Pin-lenmiş olmasını gerektirir. Bir dal tamamlandığında, select! diğer gelecekleri bırakır. Eğer gelecek Unpin ise, bırakma yapıştırıcısına taşınır. Eğer Pin-lenmişse, yerinde bırakılır. Bellek güvenliği garantisi, Pin'in geleceğin kendi bellek adresinde bırakıldığını sağlamasından kaynaklanır; bu, bir kendi kendine referans veren geleceğin, anketten sonra taşınmasının (hatta yok edilmesi için bile) neden olabileceği kullanım sonrası serbest bırakma veya sarkık işaretçi sorunlarını önler. Adaylar genellikle Pin'in yalnızca anketi değil, aynı zamanda iptal edilen geleceği yok etme anlamlarını da etkilediğini göz ardı eder.