
SafeHandle / CriticalHandle / SafeBuffer / abgeleitete Typen
Ich habe das Gefühl, ich werde die Büchse der Pandora für dich öffnen. Lassen Sie uns über spezielle Typen sprechen: SafeHandle, CriticalHandle und ihre abgeleiteten Typen.
Dies ist das Letzte am Muster eines Typs, der Zugriff auf eine nicht verwaltete Ressource gewährt. Aber zuerst wollen wir alles auflisten, was wir normalerweise aus einer nicht verwalteten Welt bekommen:
Das erste und naheliegende sind Griffe. Dies mag für einen .NET-Entwickler ein bedeutungsloses Wort sein, ist jedoch eine sehr wichtige Komponente der Betriebssystemwelt. Ein Handle ist von Natur aus eine 32- oder 64-Bit-Zahl. Es bezeichnet eine geöffnete Interaktionssitzung mit einem Betriebssystem. Wenn Sie beispielsweise eine Datei öffnen, erhalten Sie ein Handle von der WinApi-Funktion. Dann können Sie damit arbeiten und Such- , Lese- oder Schreibvorgänge ausführen. Sie können auch einen Socket für den Netzwerkzugriff öffnen. Wieder gibt Ihnen ein Betriebssystem ein Handle. In .NET werden Handles als IntPtr- Typ gespeichert.
Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.
Wenn Sie sich bei uns bedanken möchten, können Sie dies am besten tun, indem Sie uns einen Stern auf Github geben oder das Repository teilen
github / sidristij / dotnetbook .
- Das zweite sind Datenarrays. Sie können mit nicht verwalteten Arrays entweder über unsicheren Code arbeiten (unsicher ist hier ein Schlüsselwort) oder SafeBuffer verwenden, um einen Datenpuffer in eine geeignete .NET-Klasse zu verpacken. Beachten Sie, dass der erste Weg schneller ist (z. B. können Sie Schleifen stark optimieren), der zweite jedoch viel sicherer ist, da er auf SafeHandle basiert.
- Dann gehen Sie Saiten. Zeichenfolgen sind einfach, da wir das Format und die Codierung der von uns erfassten Zeichenfolge bestimmen müssen. Es wird dann für uns kopiert (ein String ist eine unveränderliche Klasse) und wir machen uns darüber keine Sorgen mehr.
- Das Letzte sind ValueTypes, die nur kopiert werden, damit wir überhaupt nicht darüber nachdenken müssen.
SafeHandle ist eine spezielle .NET CLR-Klasse, die CriticalFinalizerObject erbt und die Handles eines Betriebssystems auf die sicherste und bequemste Weise umschließen sollte.
[SecurityCritical, SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)] public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { protected IntPtr handle; // The handle from OS private int _state; // State (validity, the reference counter) private bool _ownsHandle; // The flag for the possibility to release the handle. // It may happen that we wrap somebody else's handle // have no right to release. private bool _fullyInitialized; // The initialized instance [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) { } // The finalizer calls Dispose(false) with a pattern [SecuritySafeCritical] ~SafeHandle() { Dispose(false); } // You can set a handle manually or automatically with p/invoke Marshal [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected void SetHandle(IntPtr handle) { this.handle = handle; } // This method is necessary to work with IntPtr directly. It is used to // determine if a handle was created by comparing it with one of the previously // determined known values. Pay attention that this method is dangerous because: // // – if a handle is marked as invalid by SetHandleasInvalid, DangerousGetHandle // it will anyway return the original value of the handle. // – you can reuse the returned handle at any place. This can at least // mean, that it will stop work without a feedback. In the worst case if // IntPtr is passed directly to another place, it can go to an unsafe code and become // a vector for application attack by resource substitution in one IntPtr [ResourceExposure(ResourceScope.None), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public IntPtr DangerousGetHandle() { return handle; } // The resource is closed (no more available for work) public bool IsClosed { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get { return (_state & 1) == 1; } } // The resource is not available for work. You can override the property by changing the logic. public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get; } // Closing the resource through Close() pattern [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Close() { Dispose(true); } // Closing the resource through Dispose() pattern [SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Dispose() { Dispose(true); } [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected virtual void Dispose(bool disposing) { // ... } // You should call this method every time when you understand that a handle is not operational anymore. // If you don't do it, you can get a leak. [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void SetHandleAsInvalid(); // Override this method to point how to release // the resource. You should code carefully, as you cannot // call uncompiled methods, create new objects or produce exceptions from it. // A returned value shows if the resource was releases successfully. // If a returned value = false, SafeHandleCriticalFailure will occur // that will enter a breakpoint if SafeHandleCriticalFailure // Managed Debugger Assistant is activated. [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected abstract bool ReleaseHandle(); // Working with the reference counter. To be explained further. [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void DangerousAddRef(ref bool success); public extern void DangerousRelease(); }
Um die Nützlichkeit der von SafeHandle abgeleiteten Klassen zu verstehen, müssen Sie sich daran erinnern, warum .NET-Typen so großartig sind: GC kann ihre Instanzen automatisch erfassen. Während SafeHandle verwaltet wird, erbt die nicht verwaltete Ressource, die es umschließt, alle Merkmale der verwalteten Welt. Es enthält auch einen internen Zähler für externe Referenzen, die für CLR nicht verfügbar sind. Ich meine Referenzen von unsicherem Code. Sie müssen einen Zähler überhaupt nicht manuell erhöhen oder verringern. Wenn Sie einen von SafeHandle abgeleiteten Typ als Parameter einer unsicheren Methode deklarieren, erhöht sich der Zähler bei der Eingabe dieser Methode oder verringert sich nach dem Beenden. Der Grund dafür ist, dass Sie, wenn Sie zu einem unsicheren Code wechseln, indem Sie dort ein Handle übergeben, dieses SafeHandle möglicherweise von GC sammeln lassen, indem Sie den Verweis auf dieses Handle in einem anderen Thread zurücksetzen (wenn Sie mit einem Handle aus mehreren Threads arbeiten). Mit einem Referenzzähler funktioniert es noch einfacher: SafeHandle wird erst erstellt, wenn der Zähler auf Null gesetzt ist. Deshalb müssen Sie den Zähler nicht manuell ändern. Oder Sie sollten es sehr sorgfältig tun, indem Sie es nach Möglichkeit zurücksenden.
Der zweite Zweck eines Referenzzählers besteht darin, die Reihenfolge der Finalisierung von CriticalFinalizerObject
festzulegen, die aufeinander verweisen. Wenn ein SafeHandle-basierter Typ auf einen anderen verweist, müssen Sie im Konstruktor des referenzierenden Typs zusätzlich einen Referenzzähler erhöhen und den Zähler in der ReleaseHandle-Methode verringern. Somit bleibt Ihr Objekt bestehen, bis das Objekt, auf das sich Ihr Objekt bezieht, nicht zerstört wird. Es ist jedoch besser, solche Rätsel zu vermeiden. Lassen Sie uns das Wissen über SafeHandler nutzen und die letzte Variante unserer Klasse schreiben:
public class FileWrapper : IDisposable { SafeFileHandle _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; _handle.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern SafeFileHandle CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); /// other methods }
Wie ist es anders? Wenn Sie einen SafeHandle-basierten Typ (einschließlich Ihres eigenen) als Rückgabewert in der DllImport-Methode festlegen, erstellt und initialisiert Marshal diesen Typ korrekt und setzt einen Zähler auf 1. Da wir dies wissen, legen wir den SafeFileHandle-Typ als Rückgabetyp für fest die CreateFile-Kernelfunktion. Wenn wir es erhalten, werden wir es genau zum Aufrufen von ReadFile und WriteFile verwenden (da ein Zählerwert beim Aufrufen erhöht und beim Beenden verringert wird, dass das Handle während des Lesens aus und Schreiben in eine Datei weiterhin vorhanden ist). Dies ist ein korrekt gestalteter Typ, der ein Dateihandle zuverlässig schließt, wenn ein Thread abgebrochen wird. Dies bedeutet, dass wir nicht unseren eigenen Finalizer und alles, was damit verbunden ist, implementieren müssen. Der ganze Typ ist vereinfacht.
Die Ausführung eines Finalizers, wenn Instanzmethoden funktionieren
Bei der Speicherbereinigung wird eine Optimierungstechnik verwendet, mit der mehr Objekte in kürzerer Zeit erfasst werden können. Schauen wir uns den folgenden Code an:
public void SampleMethod() { var obj = new object(); obj.ToString(); // ... // If GC runs at this point, it may collect obj // as it is not used anymore // ... Console.ReadLine(); }
Einerseits sieht der Code sicher aus, und es ist nicht sofort klar, warum wir uns darum kümmern sollten. Wenn Sie sich jedoch daran erinnern, dass es Klassen gibt, die nicht verwaltete Ressourcen umschließen, werden Sie verstehen, dass eine falsch gestaltete Klasse eine Ausnahme von der nicht verwalteten Welt verursachen kann. Diese Ausnahme meldet, dass ein zuvor erhaltenes Handle nicht aktiv ist:
// The example of an absolutely incorrect implementation void Main() { var inst = new SampleClass(); inst.ReadData(); // inst is not used further } public sealed class SampleClass : CriticalFinalizerObject, IDisposable { private IntPtr _handle; public SampleClass() { _handle = CreateFile("test.txt", 0, 0, IntPtr.Zero, 0, 0, IntPtr.Zero); } public void Dispose() { if (_handle != IntPtr.Zero) { CloseHandle(_handle); _handle = IntPtr.Zero; } } ~SampleClass() { Console.WriteLine("Finalizing instance."); Dispose(); } public unsafe void ReadData() { Console.WriteLine("Calling GC.Collect..."); // I redirected it to the local variable not to // use this after GC.Collect(); var handle = _handle; // The imitation of full GC.Collect GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine("Finished doing something."); var overlapped = new NativeOverlapped(); // it is not important what we do ReadFileEx(handle, new byte[] { }, 0, ref overlapped, (a, b, c) => {;}); } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)] static extern IntPtr CreateFile(String lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr securityAttrs, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadFileEx(IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead, [In] ref NativeOverlapped lpOverlapped, IOCompletionCallback lpCompletionRoutine); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr hObject); }
Geben Sie zu, dass dieser Code mehr oder weniger anständig aussieht. Wie auch immer, es sieht nicht so aus, als gäbe es ein Problem. In der Tat gibt es ein ernstes Problem. Ein Klassenfinalisierer versucht möglicherweise, eine Datei beim Lesen zu schließen, was fast zwangsläufig zu einem Fehler führt. Da in diesem Fall der Fehler explizit zurückgegeben wird ( IntPtr == -1
), wird dies nicht IntPtr == -1
. Das _handle
wird auf Null gesetzt, das folgende Dispose
kann die Datei nicht schließen und die Ressource leckt. Um dieses Problem zu lösen, sollten Sie SafeHandle
, CriticalHandle
, SafeBuffer
und deren abgeleitete Klassen verwenden. Abgesehen davon, dass diese Klassen Verwendungszähler in nicht verwaltetem Code haben, erhöhen sich diese Zähler automatisch, wenn sie mit den Parametern der Methoden an die nicht verwaltete Welt übergeben werden, und verringern sich, wenn sie ihn verlassen.
Dieser Charper wurde von professionellen Übersetzern aus dem Russischen wie aus der Sprache des Autors übersetzt . Sie können uns bei der Erstellung einer übersetzten Version dieses Textes in eine andere Sprache, einschließlich Chinesisch oder Deutsch, unter Verwendung der russischen und englischen Textversion als Quelle helfen.
Wenn Sie "Danke" sagen möchten, können Sie uns am besten einen Stern auf Github oder Forking Repository geben
https://github.com/sidristij/dotnetbook