Freigeben über


Bewährte Methoden für Zuverlässigkeit

Die folgenden Zuverlässigkeitsregeln sind auf SQL Server ausgerichtet; Sie gelten jedoch auch für jede hostbasierte Serveranwendung. Es ist äußerst wichtig, dass Server wie SQL Server keine Ressourcen verlieren und nicht ausfallen. Dies kann jedoch nicht durch Schreiben von Backoutcode für jede Methode erfolgen, die den Status eines Objekts ändert. Das Ziel ist nicht, 100 Prozent zuverlässigen verwalteten Code zu schreiben, der mit Zurücksetzungscode nach Fehlern an einer beliebigen Stelle wiederhergestellt wird. Das wäre eine entmutigende Aufgabe mit wenig Erfolgschancen. Die Common Language Runtime (CLR) kann nicht einfach genug Garantien für verwalteten Code bieten, um das Schreiben von perfektem Code machbar zu machen. Beachten Sie, dass SQL Server im Gegensatz zu ASP.NET nur einen Prozess verwendet, der nicht wiederverwendet werden kann, ohne eine Datenbank für eine unannehmbar lange Zeit herunterzunehmen.

Mit diesen schwächeren Garantien und der Ausführung in einem einzigen Prozess basiert die Zuverlässigkeit auf dem Beenden von Threads oder dem Recycling von Anwendungsdomänen bei Bedarf und ergreifen Sie Vorsichtsmaßnahmen, um sicherzustellen, dass Betriebssystemressourcen wie Handles oder Arbeitsspeicher nicht verloren gehen. Auch bei dieser einfacheren Zuverlässigkeitsbeschränkung gibt es immer noch eine erhebliche Zuverlässigkeitsanforderung:

  • Betriebssystemressourcen dürfen niemals verloren gehen.

  • Alle verwalteten Sperren für die CLR müssen identifiziert werden.

  • Ein anwendungsdomänenübergreifender freigegebener Zustand darf niemals unterbrochen werden, damit das Wiederverwenden von AppDomain reibungslos funktioniert.

Obwohl es theoretisch möglich ist, verwalteten Code zu schreiben, um ThreadAbortException, StackOverflowException, und OutOfMemoryException-Ausnahmen zu verarbeiten, ist es unzumutbar zu erwarten, dass Entwickler solch robusten Code in einer gesamten Anwendung schreiben. Aus diesem Grund führen Out-of-band-Ausnahmen dazu, dass der ausführende Thread beendet wird. Wenn der beendete Thread einen Freigabezustand geändert hat, was dadurch ermittelt werden kann, ob im Thread eine Sperre enthalten ist, wird AppDomain nicht geladen. Wenn eine Methode, die den freigegebenen Zustand bearbeitet, beendet wird, ist der Zustand beschädigt, da es nicht möglich ist, zuverlässigen Back-Out-Code für Aktualisierungen im freigegebenen Zustand zu schreiben.

In .NET Framework, Version 2.0, ist der einzige Host, der Zuverlässigkeit erfordert, SQL Server. Wenn Ihre Assembly auf SQL Server ausgeführt wird, sollten Sie die Zuverlässigkeit für jeden Teil dieser Assembly sicherstellen, auch wenn bestimmte Funktionen deaktiviert sind, wenn sie in der Datenbank ausgeführt werden. Dies ist erforderlich, da das Codeanalysemodul Code auf Assemblyebene untersucht und deaktivierten Code nicht unterscheiden kann. Eine andere Überlegung bei der SQL Server-Programmierung ist, dass SQL Server alles in einem Prozess ausführt. Außerdem wird die AppDomain-Wiederverwendung zur Bereinigung aller Ressourcen wie Arbeitsspeicher und Betriebssystemhandles verwendet.

Sie können nicht von Finalizern oder Destruktoren oder try/finally Blöcken für Back-Out-Code abhängen. Diese können unterbrochen oder nicht aufgerufen werden.

Asynchrone Ausnahmen können an unerwarteten Stellen ausgelöst werden, möglicherweise an jeder Computeranweisung: ThreadAbortException, , StackOverflowExceptionund OutOfMemoryException.

Verwaltete Threads sind nicht unbedingt Win32-Threads in SQL; sie können Fasern sein.

Prozessweiter oder anwendungsübergreifender, veränderbarer, gemeinsam genutzter Zustand ist äußerst schwierig sicher zu ändern und sollte möglichst vermieden werden.

Speichermangelzustände sind bei SQL Server nicht selten.

Wenn Bibliotheken, die in SQL Server gehostet werden, ihren freigegebenen Zustand nicht ordnungsgemäß aktualisieren, besteht eine hohe Wahrscheinlichkeit, dass der Code erst wiederhergestellt wird, nachdem die Datenbank neu gestartet wurde. Darüber hinaus kann dies in einigen extremen Fällen dazu führen, dass der SQL Server-Prozess fehlschlägt, wodurch die Datenbank neu gestartet wird. Ein Neustart der Datenbank kann eine Website außer Betrieb setzen oder die Unternehmensabläufe beeinträchtigen, was sich negativ auf die Verfügbarkeit auswirkt. Ein langsamer Verlust von Betriebssystemressourcen wie Arbeitsspeicher oder Handles kann dazu führen, dass der Server die Zuweisung von Handles ohne Möglichkeit der Wiederherstellung fehlschlägt, oder der Server beeinträchtigt möglicherweise langsam die Leistung und verringert die Anwendungsverfügbarkeit eines Kunden. Wir wollen diese Szenarien klar vermeiden.

Bewährte Methodenregeln

In der Einführung wurde der Fokus darauf gelegt, auf welche Fehler im Code Review für den verwalteten Code, der auf dem Server ausgeführt wird, geprüft werden muss, um die Stabilität und Zuverlässigkeit des Frameworks zu verbessern. Alle diese Prüfungen sind allgemein bewährte Methoden und ein absoluter Muss auf dem Server.

Bei einem Deadlock oder einer Ressourceneinschränkung bricht SQL Server einen Thread ab oder entfernt eine AppDomain. In diesem Fall wird garantiert nur Back-Out-Code in einem eingeschränkten Ausführungsbereich (CER) ausgeführt.

Verwenden von SafeHandle, um Ressourcenlecks zu vermeiden

Im Falle eines AppDomain Entladens können Sie sich nicht darauf verlassen, dass finally Blöcke oder Finalizer ausgeführt werden. Daher ist es wichtig, alle Betriebssystemressourcenzugriffe über die SafeHandle Klasse zu abstrahieren, anstatt IntPtr, HandleRef oder ähnliche Klassen zu verwenden. Dies ermöglicht der CLR, die von Ihnen verwendeten Handles selbst im Fall des Entfernens von AppDomain nachzuverfolgen und zu schließen. SafeHandle verwendet ein Objekt der Klasse CriticalFinalizerObject als Finalizer, der von der CLR immer ausgeführt wird.

Das Betriebssystemhandle wird vom Moment seiner Erstellung in einem SafeHandle bis zu dem Moment gespeichert, in dem es freigegeben wird. Es gibt kein Zeitfenster, in dem eine ThreadAbortException den Verlust eines Handles verursachen kann. Darüber hinaus führt der Plattformaufruf eine Verweiszählung des Handles aus, was eine genaue Nachverfolgung der Lebenszeit des Handles ermöglicht. Hierdurch wird ein Sicherheitsproblem mit einer Racebedingung zwischen Dispose und einer Methode verhindert, die das Handle momentan verwendet.

Die meisten Klassen, die derzeit über einen Finalizer zum einfachen Bereinigen eines Betriebssystemhandles verfügen, werden den Finalizer nicht mehr benötigen. Stattdessen wird sich der Finalizer in der abgeleiteten SafeHandle-Klasse befinden.

Beachten Sie, dass SafeHandle kein Ersatz für IDisposable.Dispose ist. Es bestehen immer noch potenzielle Vorteile im Hinblick auf Ressourcenkonflikte und Leistung gegenüber dem expliziten Löschen von Betriebssystemressourcen. Beachten Sie einfach, dass finally Blöcke, die Ressourcen explizit freigeben, möglicherweise nicht vollständig ausgeführt werden.

SafeHandle ermöglicht es Ihnen, Ihre eigene ReleaseHandle-Methode zu implementieren, die die Arbeit zur Freigabe des Handles übernimmt, z. B. indem der Zustand an eine Betriebssystem-Handle-Freigabe-Routine übergeben oder eine Reihe von Handles in einer Schleife freigegeben wird. Die CLR garantiert, dass diese Methode ausgeführt wird. Es liegt in der Verantwortung des Erstellers der ReleaseHandle-Implementierung, sicherzustellen, dass das Handle in allen Fällen freigegeben wird. Wird dies nicht sichergestellt, geht das Handle verloren, was oft dazu führt, dass native Ressourcen verloren gehen, die dem Handle zugewiesen sind. Daher ist es wichtig, abgeleitete Klassen so zu strukturieren SafeHandle , dass die ReleaseHandle Implementierung keine Zuordnung von Ressourcen erfordert, die möglicherweise nicht zur Aufrufzeit verfügbar sind. Beachten Sie, dass es zulässig ist, Methoden aufzurufen, die innerhalb der Implementierung ReleaseHandle fehlschlagen können, sofern Ihr Code solche Fehler behandeln und den Vertrag abschließen kann, um das systemeigene Handle freizugeben. Für Debuggingzwecke hat ReleaseHandle einen Boolean Rückgabewert, der auf false gesetzt werden kann, falls ein katastrophaler Fehler auftritt, der die Freigabe der Ressource verhindert. Dadurch wird der releaseHandleFailed MDA aktiviert, wenn er eingeschaltet ist, um das Problem zu identifizieren. Er wirkt sich auf keine andere Weise auf die Laufzeit aus. ReleaseHandle wird nicht erneut für die dieselbe Ressource aufgerufen, und daher geht das Handle verloren.

SafeHandle ist in bestimmten Kontexten nicht geeignet. Da die ReleaseHandle Methode auf einem GC Finalizerthread ausgeführt werden kann, sollten alle Handles, die auf einem bestimmten Thread freigegeben werden müssen, nicht in einen SafeHandle umschlossen werden.

Runtime Callable Wrapper (RCWs) können durch die CLR ohne zusätzlichen Code bereinigt werden. Für Code, der Platform Invocation verwendet und ein COM-Objekt als IUnknown* oder als IntPtr behandelt, sollte der Code umgeschrieben werden, um ein RCW zu verwenden. SafeHandle ist für dieses Szenario möglicherweise nicht geeignet, da die Möglichkeit besteht, dass eine nicht verwaltete Freigabemethode einen Rückruf für den verwalteten Code ausführt.

Codeanalyseregel

Verwenden Sie SafeHandle, um Betriebssystemressourcen zu kapseln. Nicht verwenden HandleRef oder Felder vom Typ IntPtr.

Stellen Sie sicher, dass Finalizer nicht ausgeführt werden müssen, um das Verlust von Betriebssystemressourcen zu verhindern.

Überprüfen Sie Ihre Finalizer sorgfältig, um sicherzustellen, dass selbst wenn sie nicht ausgeführt werden, eine wichtige Betriebssystemressource nicht verloren geht. Im Gegensatz zu einem normalen AppDomain Entladen, wenn die Anwendung in einem stabilen Zustand ausgeführt wird oder wenn ein Server wie SQL Server heruntergefahren wird, werden Objekte während eines abrupten AppDomain Entladens nicht abgeschlossen. Stellen Sie sicher, dass Ressourcen im Falle eines abrupten Entladens nicht verloren gehen, da die Richtigkeit einer Anwendung nicht garantiert werden kann, aber die Integrität des Servers muss durch nicht auslaufende Ressourcen beibehalten werden. Verwenden Sie SafeHandle, um alle Betriebssystemressourcen freizugeben.

Stellen Sie sicher, dass finally-Klauseln nicht ausgeführt werden müssen, um Betriebssystemressourcen nicht zu verlieren.

finally Klauseln werden nicht garantiert außerhalb von CERs ausgeführt. Daher sollten Bibliotheksentwickler sich nicht darauf verlassen, dass Code innerhalb eines finally Blocks nicht verwaltete Ressourcen freisetzt. Die Verwendung SafeHandle ist die empfohlene Lösung.

Codeanalyseregel

Verwenden Sie SafeHandle zum Bereinigen von Betriebssystemressourcen anstelle von Finalize. Verwenden Sie IntPtrnicht ; verwenden Sie SafeHandle zum Kapseln von Ressourcen. Wenn die finally-Klausel ausgeführt werden muss, platzieren Sie diese in einer CER.

Alle Sperren sollten vorhandenen verwalteten Sperrcode durchlaufen

Die CLR muss wissen, wann der Code in einer Sperre ist, damit sie weiß, dass sie den AppDomain abräumen kann, anstatt nur den Thread abzubrechen. Das Abbrechen des Threads könnte gefährlich sein, da die vom Thread bearbeiteten Daten in einem inkonsistenten Zustand zurückgelassen werden könnten. Daher muss das Gesamte AppDomain wiederverwendet werden. Die Folgen, wenn eine Sperre nicht identifiziert wird, können entweder Blockaden oder falsche Ergebnisse sein. Verwenden Sie die Methoden BeginCriticalRegion, um Sperrbereiche zu identifizieren, und EndCriticalRegion. Es handelt sich um statische Methoden für die Thread Klasse, die nur auf den aktuellen Thread angewendet wird, um zu verhindern, dass ein Thread die Sperranzahl eines anderen Threads bearbeitet.

Enter und Exit haben diese CLR-Benachrichtigung eingebaut, daher wird ihre Verwendung sowie die Verwendung der Lock-Anweisung empfohlen, die diese Methoden nutzt.

Andere Sperrmechanismen wie Drehsperren und AutoResetEvent müssen diese Methoden aufrufen, um die CLR zu benachrichtigen, dass ein kritischer Abschnitt eingegeben wird. Diese Methoden verwenden keine Sperren; sie informieren die CLR darüber, dass Code in einem kritischen Abschnitt ausgeführt wird und das Abbrechen des Threads könnte den gemeinsamen Zustand inkonsistent machen. Wenn Sie Ihren eigenen Sperrtyp definiert haben, z. B. eine benutzerdefinierte ReaderWriterLock Klasse, verwenden Sie diese Methoden für die Sperranzahl.

Codeanalyseregel

Markieren und Identifizieren aller Sperren mit BeginCriticalRegion und EndCriticalRegion. Verwenden Sie CompareExchange, Increment und Decrement nicht in einer Schleife. Führen Sie keine Plattformaufrufe der Win32-Varianten dieser Methoden aus. Verwenden Sie Sleep nicht in einer Schleife. Verwenden Sie keine veränderliche Felder.

Bereinigungscode muss sich in einem finally- oder catch-Block befinden und darf nicht auf einen catch-Block folgen

Bereinigungscode sollte niemals einem catch Block folgen; er sollte sich in einem finally oder im catch Block selbst befinden. Dies sollte eine normale bewährte Methode sein. Ein finally Block wird im Allgemeinen bevorzugt, da er denselben Code ausführt, wenn eine Ausnahme ausgelöst wird und wenn das Ende des try Blocks normalerweise auftritt. Wenn eine unerwartete Ausnahme ausgelöst wird, z. B. ein ThreadAbortException, wird der Bereinigungscode nicht ausgeführt. Alle nicht verwalteten Ressourcen, die Sie in einem finally bereinigen würden, sollten idealerweise in eine SafeHandle umschlossen werden, um Lecks zu verhindern. Beachten Sie, dass das C#-Schlüsselwort using effektiv verwendet werden kann, um Objekte einschließlich Handles zu löschen.

Obwohl AppDomain das Recycling Ressourcen im Finalizerthread bereinigen kann, ist es dennoch wichtig, Bereinigungscode richtig zu platzieren. Wenn ein Thread eine asynchrone Ausnahme empfängt, ohne eine Sperre zu verwenden, sollten Sie beachten, dass die CLR versucht, den Thread selbst zu beenden, ohne AppDomain wiederherstellen zu müssen. Sicherzustellen, dass Ressourcen so bald wie möglich bereinigt werden, hilft, mehr Ressourcen verfügbar zu halten und die Lebensdauer besser zu verwalten. Wenn Sie einen Handle nicht explizit für eine Datei in einem Fehlercodepfad schließen, warten Sie darauf, dass der SafeHandle-Finalizer ihn bereinigt. Wenn Ihr Code das nächste Mal ausgeführt wird, tritt möglicherweise ein Fehler beim dem Versuch auf, auf dieselbe Datei zuzugreifen, falls der Finalizer nicht bereits ausgeführt wurde. Aus diesem Grund ist es wichtig sicherzustellen, dass Bereinigungscode vorhanden ist und ordnungsgemäß funktioniert, da dies dabei hilft, Fehler sauberer und schneller zu beheben, auch wenn es nicht unbedingt erforderlich ist.

Codeanalyseregel

Bereinigungscode nach catch muss sich in einem finally-Block befinden. Platzieren Sie Aufrufe zum Löschen in einem finally-Block. catch-Blöcke sollten abschließend Ausnahmen auslösen bzw. neu auslösen. Auch wenn es Ausnahmefälle geben wird, wie etwa bei Code, der feststellt, ob eine Netzwerkverbindung hergestellt werden kann, wobei zahlreiche Ausnahmen auftreten könnten, sollte jeder Code, der unter normalen Umständen das Abfangen mehrerer Ausnahmen erfordert, darauf hinweisen, dass es ratsam ist, ihn zu testen, um festzustellen, ob er erfolgreich ausgeführt wird.

Ein prozessweiter veränderbarer Freigabezustand zwischen Anwendungsdomänen sollte beseitigt werden, oder es sollte ein eingeschränkter Ausführungsbereich verwendet werden

Wie in der Einführung beschrieben, kann es sehr schwierig sein, verwalteten Code zu schreiben, der den prozessweiten gemeinsam genutzten Zustand über Anwendungsdomänen hinweg zuverlässig überwacht. Ein prozessübergreifender veränderbarer Freigabezustand ist jede Art von Datenstruktur zwischen Anwendungsdomänen, entweder in Win32-Code, innerhalb der CLR oder in verwaltetem Code, der Remoting verwendet. Jeder änderbare freigegebene Zustand ist sehr schwierig, ordnungsgemäß in verwaltetem Code zu schreiben, und jeder statische freigegebene Zustand kann nur mit großer Sorgfalt ausgeführt werden. Wenn Sie einen prozessweiten oder maschinenweiten gemeinsam genutzten Zustand haben, finden Sie eine Möglichkeit, ihn zu beseitigen oder den gemeinsamen Zustand mithilfe eines eingeschränkten Ausführungsbereichs (CER) zu schützen. Beachten Sie, dass jede Bibliothek mit Freigabezustand, der nicht identifiziert und korrigiert ist, dazu führen könnte, dass ein Host wie SQL Server abstürzt, der ein korrektes Entladen von AppDomain erfordert.

Wenn Code ein COM-Objekt verwendet, vermeiden Sie das Teilen dieses COM-Objekts zwischen Anwendungsdomänen.

Sperren funktionieren nicht prozessweit oder zwischen Anwendungsdomänen.

In der Vergangenheit wurden Enter und die Lock-Anweisung verwendet, um globale Prozesssperren zu erstellen. Dies ist beispielsweise beim Sperren von agilen AppDomain-Klassen wie Type-Instanzen von nicht freigegebenen Assemblys, Thread-Objekten, internalisierten Zeichenfolgen und einigen Zeichenfolgen der Fall, die mithilfe von Remoting zwischen Anwendungsdomänen freigegeben sind. Diese Sperren sind nicht mehr prozessübergreifend. Um das Vorhandensein einer prozessweiten Domänensperrung zu identifizieren, ermitteln Sie, ob der Code innerhalb der Sperre externe, persistente Ressourcen wie eine Datei auf dem Datenträger oder möglicherweise eine Datenbank verwendet.

Beachten Sie, dass das Erstellen einer Sperre innerhalb eines AppDomain Blocks zu Problemen führen kann, wenn der geschützte Code eine externe Ressource verwendet, da dieser Code gleichzeitig in mehreren Anwendungsdomänen ausgeführt wird. Dies kann ein Problem beim Schreiben in eine Protokolldatei oder bindung an einen Socket für den gesamten Prozess sein. Diese Änderungen bedeuten, dass es keine einfache Möglichkeit gibt, mit verwaltetem Code eine prozessglobale Sperre zu erhalten, außer durch die Verwendung einer benannten Mutex- oder Semaphore-Instanz. Erstellen Sie Code, der nicht in zwei Anwendungsdomänen gleichzeitig ausgeführt wird, oder verwenden Sie die Mutex oder Semaphore die Klassen. Wenn vorhandener Code nicht geändert werden kann, verwenden Sie keinen Win32 namens Mutex, um diese Synchronisierung zu erzielen, da die Ausführung im Fasermodus bedeutet, dass Sie nicht garantieren können, dass derselbe Betriebssystemthread einen Mutex erhält und freigibt. Sie müssen die verwaltete Mutex Klasse oder eine benannte ManualResetEvent, AutoResetEventoder eine Semaphore verwenden, um die Codesperre auf eine Weise zu synchronisieren, die von der CLR bekannt ist, anstatt die Sperre mit nicht verwaltetem Code zu synchronisieren.

Vermeiden Sie lock(typeof(MyType))

Private und öffentliche Type Objekte in freigegebenen Assemblys mit nur einer Kopie des Codes, der für alle Anwendungsdomänen freigegeben ist, stellen ebenfalls Probleme dar. Für freigegebene Assemblys ist nur eine Instanz einer Type-Klasse pro Prozess vorhanden. Dies bedeutet, dass mehrere Anwendungsdomänen genau dieselbe Type-Instanz freigeben. Wenn Sie eine Sperre für eine Type-Instanz nehmen, wirkt sich diese auf den gesamten Prozess aus, nicht nur auf die AppDomain. Wenn eine AppDomain ein Type-Objekt sperrt, wird der Thread unvermittelt unterbrochen, und die Sperre wird nicht aufgehoben. Diese Sperre kann dann dazu führen, dass andere Anwendungsdomänen deadlocken.

Eine gute Möglichkeit zum Übernehmen von Sperren in statischen Methoden ist das Hinzufügen eines statischen internen Synchronisierungsobjekts zum Code. Dies kann im Klassenkonstruktor initialisiert werden, wenn eins vorhanden ist, aber wenn nicht, kann es wie folgt initialisiert werden:

private static Object s_InternalSyncObject;
private static Object InternalSyncObject
{
    get
    {
        if (s_InternalSyncObject == null)
        {
            Object o = new Object();
            Interlocked.CompareExchange(
                ref s_InternalSyncObject, o, null);
        }
        return s_InternalSyncObject;
    }
}

Verwenden Sie dann beim Öffnen einer Sperre die InternalSyncObject Eigenschaft, um ein Objekt abzurufen, das gesperrt werden soll. Sie müssen die Eigenschaft nicht verwenden, wenn Sie das interne Synchronisierungsobjekt im Klassenkonstruktor initialisiert haben. Der Code für die Doppelte Überprüfung der Sperrinitialisierung sollte wie in diesem Beispiel aussehen:

public static MyClass SingletonProperty
{
    get
    {
        if (s_SingletonProperty == null)
        {
            lock(InternalSyncObject)
            {
                // Do not use lock(typeof(MyClass))
                if (s_SingletonProperty == null)
                {
                    MyClass tmp = new MyClass(…);
                    // Do all initialization before publishing
                    s_SingletonProperty = tmp;
                }
            }
        }
        return s_SingletonProperty;
    }
}

Eine Notiz zu lock(this)

Es ist im Allgemeinen akzeptabel, eine Sperre für ein einzelnes Objekt zu übernehmen, das öffentlich zugänglich ist. Wenn es sich bei dem Objekt jedoch um ein Singleton-Objekt handelt, das dazu führen könnte, dass ein gesamtes Subsystem in einem Deadlock endet, sollten Sie das oben genannte Design-Muster ebenfalls in Betracht ziehen. Eine Sperre auf dem SecurityManager Objekt könnte zu einem Deadlock innerhalb des AppDomain führen, wodurch das gesamte AppDomain unbrauchbar wird. Es wird daher empfohlen, ein als öffentlich deklariertes Objekt dieses Typs nicht zu sperren. Eine Sperre für eine einzelne Sammlung oder ein Array sollte jedoch in der Regel kein Problem darstellen.

Codeanalyseregel

Verwenden Sie keine Sperren bei Typen, die möglicherweise in verschiedenen Anwendungsdomänen genutzt werden, oder die keinen starken Identitätssinn haben. Rufen Sie Enter nicht bei einem Type, MethodInfo, PropertyInfo, String, ValueType, Thread oder einem Objekt auf, das von MarshalByRefObject erbt.

Entfernen Sie GC. KeepAlive-Anrufe

Ein erheblicher Teil des vorhandenen Codes verwendet entweder KeepAlive nicht, wenn es angebracht wäre, oder verwendet es, wenn es nicht geeignet ist. Nach der Konvertierung zu SafeHandle müssen Klassen KeepAlive nicht aufrufen, vorausgesetzt, sie verfügen nicht über einen Finalizer, sondern verlassen sich darauf, dass SafeHandle die Betriebssystemhandles finalisiert. Während die Leistungskosten für die Beibehaltung eines Anrufs bei KeepAlive vernachlässigbar sein können, erschwert die Wahrnehmung, dass ein Aufruf bei KeepAlive entweder erforderlich oder ausreichend ist, um ein Problem mit der Lebensdauer zu lösen, das möglicherweise nicht mehr vorhanden ist, die Wartung des Codes. Beim Verwenden der Runtime Callable Wrapper (RCWs) von COM-Interop muss KeepAlive allerdings dennoch im Code vorhanden sein.

Codeanalyseregel

Entfernen Sie KeepAlive.

Verwenden des HostProtection-Attributs

Die HostProtectionAttribute (HPA) ermöglicht den Einsatz deklarativer Sicherheitsaktionen, um die Anforderungen an den Hostschutz zu bestimmen, sodass der Host verhindern kann, dass auch vollständig vertrauenswürdiger Code bestimmte Methoden aufruft, die für den angegebenen Host ungeeignet sind, wie z. B. Exit oder Show für SQL Server.

Die HPA betrifft nur nicht verwaltete Anwendungen, die die Common Language Runtime hosten und Hostschutz implementieren, z. B. SQL Server. Die Anwendung der Sicherheitsaktion führt basierend auf den Hostressourcen, die von der Klasse oder Methode verfügbar gemacht werden, zur Erstellung eines Linkaufrufs. Wenn der Code in einer Clientanwendung oder auf einem Server ausgeführt wird, der nicht hostgeschützt ist, wird das Attribut "verdampft"; sie wird nicht erkannt und daher nicht angewendet.

Von Bedeutung

Der Zweck dieses Attributs besteht darin, hostspezifische Programmiermodellrichtlinien und nicht das Sicherheitsverhalten zu erzwingen. Obwohl eine Linkanforderung verwendet wird, um die Einhaltung der Programmiermodellanforderungen zu überprüfen, ist dies HostProtectionAttribute keine Sicherheitsberechtigung.

Wenn der Host keine Programmiermodellanforderungen hat, treten die Verknüpfungsanforderungen nicht auf.

Dieses Attribut identifiziert Folgendes:

  • Methoden oder Klassen, die nicht zum Hostprogrammiermodell passen, sind aber ansonsten gutartig.

  • Methoden oder Klassen, die nicht zum Hostprogrammiermodell passen und zu einer Destabilisierung von serververwaltetem Benutzercode führen können.

  • Methoden oder Klassen, die nicht zum Hostprogrammiermodell passen und zu einer Destabilisierung des Serverprozesses selbst führen können.

Hinweis

Wenn Sie eine Klassenbibliothek erstellen, die von Anwendungen aufgerufen werden soll, die in einer hostgeschützten Umgebung ausgeführt werden können, sollten Sie dieses Attribut auf Member anwenden, die Ressourcenkategorien verfügbar machen HostProtectionResource . Die .NET Framework-Klassenbibliotheksmitglieder mit diesem Attribut führen nur dazu, dass der sofortige Aufrufer überprüft wird. Auch der Bibliotheksmember muss auf gleiche Weise seine aufrufende Methode überprüfen.

Weitere Informationen zu HPA finden Sie in HostProtectionAttribute.

Codeanalyseregel

Für SQL Server müssen alle Methoden, die zum Einführen von Synchronisierung oder Threading verwendet werden, mit der HPA identifiziert werden. Dazu gehören Methoden, die den Zustand gemeinsam nutzen, synchronisiert sind oder externe Prozesse verwalten. Die HostProtectionResource Werte, die sich auf SQL Server auswirken, sind SharedState, Synchronizationund ExternalProcessMgmt. Jede Methode, die ein HostProtectionResource offenlegt, sollte jedoch durch eine HPA identifiziert werden, nicht nur jene mit Ressourcen, die SQL beeinflussen.

Verwenden Sie Blockierungen in nicht verwaltetem Code nicht unbegrenzt lange

Das Blockieren in nicht verwaltetem Code anstelle von verwaltetem Code kann zu einem Denial-of-Service-Angriff führen, da der CLR den Thread nicht abbrechen kann. Durch einen blockierten Thread wird – zumindest bei Verzicht auf unsichere Vorgänge – die CLR daran gehindert, die AppDomain zu entladen. Das Blockieren mit einem Windows-Synchronisierungsprimitiv ist ein klares Beispiel für etwas, das wir nicht zulassen dürfen. Eine Blockierung in einem Aufruf von ReadFile auf einem Socket sollte nach Möglichkeit vermieden werden. Im Idealfall sollte die Windows-API einen Mechanismus für einen solchen Vorgang zur Verfügung stellen, sodass ein Timeout ausgelöst werden kann.

Jede Methode, die in nativem Code aufgerufen wird, sollte idealerweise einen Win32-Aufruf mit einem angemessenen, zeitlich begrenzten Timeout verwenden. Wenn der Benutzer das Timeout angeben darf, darf der Benutzer kein unendliches Timeout ohne bestimmte Sicherheitsberechtigung angeben. Wenn eine Methode länger als ca. 10 Sekunden blockiert wird, benötigen Sie entweder eine Version, die Timeouts unterstützt, oder zusätzliche CLR-Unterstützung.

Hier sind einige Beispiele für problematische APIs. Pipes (sowohl anonym als auch benannt) können mit einem Timeout erstellt werden; der Code muss jedoch sicherstellen, dass er niemals CreateNamedPipe oder WaitNamedPipe mit NMPWAIT_WAIT_FOREVER aufruft. Darüber hinaus kann es zu unerwartetem Blockieren kommen, auch wenn ein Timeout angegeben ist. Das Aufrufen WriteFile einer anonymen Pipe wird blockiert, bis alle Bytes geschrieben wurden, d. h., wenn der Puffer ungelesene Daten enthält, wird der WriteFile Aufruf blockiert, bis der Leser Platz im Puffer der Pipe freigegeben hat. Sockets sollten immer eine API verwenden, die einen Timeoutmechanismus berücksichtigt.

Codeanalyseregel

Das Blockieren ohne Timeout im nicht verwalteten Code ist ein Denial-of-Service-Angriff. Führen Sie keine Plattformaufrufe an WaitForSingleObject, WaitForSingleObjectEx, WaitForMultipleObjects, MsgWaitForMultipleObjects und MsgWaitForMultipleObjectsEx durch. Verwenden Sie NMPWAIT_WAIT_FOREVER nicht.

Identifizieren Sie STA-abhängige Features

Identifizieren Sie Code, der COM-Single-Threaded-Apartments (STAs) verwendet. STAs sind im SQL Server-Prozess deaktiviert. Funktionen, die von CoInitialize abhängig sind, wie Leistungsindikatoren oder die Zwischenablage, müssen in SQL Server deaktiviert werden.

Stellen Sie sicher, dass Finalizer frei von Synchronisierungsproblemen sind

In zukünftigen Versionen von .NET Framework können mehrere Finalizer-Threads vorhanden sein, was bedeutet, dass die Finalizer für verschiedene Instanzen desselben Typs gleichzeitig ausgeführt werden. Sie müssen nicht vollständig threadsicher sein; Der Garbage Collector garantiert, dass nur ein Thread den Finalizer für eine bestimmte Objektinstanz ausführt. Die Finalizer müssen jedoch codiert werden, um Rennbedingungen und Deadlocks zu vermeiden, wenn sie gleichzeitig auf mehreren verschiedenen Objektinstanzen ausgeführt werden. Wenn ein externer Zustand genutzt wird, etwa beim Schreiben in eine Protokolldatei, müssen in einem Finalizer Threading-Probleme behandelt werden. Verlassen Sie sich nicht auf die Fertigstellung, um Threadsicherheit bereitzustellen. Verwenden Sie keinen lokalen Threadspeicher, verwaltet oder systemeigenen, um den Zustand im Finalizer-Thread zu speichern.

Codeanalyseregel

Finalisierer müssen frei von Synchronisierungsproblemen sein. Verwenden Sie keinen statischen veränderbaren Zustand in einem Finalizer.

Vermeiden Sie möglichst nicht verwalteten Arbeitsspeicher.

Nicht verwalteter Speicher kann ebenso wie ein Betriebssystemhandle verloren gehen. Verwenden Sie daher falls möglich mit stackalloc Arbeitsspeicher im Stapel, ein fixiertes verwaltetes Objekt wie die fixed-Anweisung oder ein GCHandle unter Verwendung von „byte[]“. Der GC gibt diese Ressourcen abschließend frei. Wenn Sie jedoch nicht verwalteten Speicher zuweisen müssen, sollten Sie eine Klasse verwenden, die von SafeHandle abgeleitet ist, um die Speicherzuweisung zu kapseln.

Beachten Sie, dass es mindestens einen Fall gibt, in dem SafeHandle nicht angemessen ist. Für COM-Methodenaufrufe, die Arbeitsspeicher zuweisen oder freigeben, ist es üblich, dass eine DLL Speicher über CoTaskMemAlloc zuweist und eine andere DLL denselben Speicher mit CoTaskMemFree freigibt. Die Verwendung von SafeHandle an diesen Stellen wäre unangemessen, da versucht wird, die Lebensdauer des nicht verwalteten Speichers an die Lebensdauer von SafeHandle zu binden, anstatt der anderen DLL die Kontrolle über die Lebensdauer des Speichers zu überlassen.

Überprüfen Sie alle Vorkommnisse von catch(Exception)

Catch-Blöcke, die alle Ausnahmen anstelle einer spezifischen Ausnahme abfangen, werden jetzt auch asynchrone Ausnahmen abfangen. Überprüfen Sie jeden Catch(Exception)-Block, und suchen Sie nach einem Mangel an wichtigem Ressourcenfreigabe- oder Rücksetzcode, der übersprungen werden könnte, sowie nach potenziell falschem Verhalten innerhalb des Catch-Blocks selbst bei der Behandlung eines ThreadAbortException, StackOverflowException oder OutOfMemoryException. Beachten Sie, dass dieser Code möglicherweise protokolliert oder einige Annahmen vornimmt, dass er nur bestimmte Ausnahmen sehen kann oder dass bei einer Ausnahme ein Fehler aus genau einem bestimmten Grund auftritt. Diese Annahmen müssen möglicherweise aktualisiert werden, einschließlich ThreadAbortException.

Sie sollten alle Stellen, an denen Ausnahmen jeden Typs abgefangen werden, so ändern, dass ein erwarteter Ausnahmetyp abgefangen wird. Ein Beispiel ist eine FormatException, die bei einer Methode zur Formatierung von Zeichenfolgen auftritt. Dadurch wird verhindert, dass der Catch-Block bei unerwarteten Ausnahmen ausgeführt wird, und stellt sicher, dass der Code Fehler nicht ausblendet, indem unerwartete Ausnahmen erfasst werden. Im Allgemeinen behandeln Sie niemals eine Ausnahme im Bibliothekscode (Code, der eine Ausnahme abfangen muss, kann auf einen Entwurfsfehler im Code hinweisen, den Sie aufrufen). In einigen Fällen möchten Sie möglicherweise eine Ausnahme abfangen und einen anderen Ausnahmetyp auslösen, um weitere Daten bereitzustellen. Verwenden Sie geschachtelte Ausnahmen in diesem Fall, und speichern Sie die tatsächliche Ursache des Fehlers in der InnerException Eigenschaft der neuen Ausnahme.

Codeanalyseregel

Überprüfen Sie alle Catch-Blöcke in verwaltetem Code, die alle Objekte abfangen oder alle Ausnahmen abfangen. In C# bedeutet dies, dass sowohl catch{} als auch catch(Exception){} markiert werden müssen. Erwägen Sie, den Ausnahmetyp sehr spezifisch zu machen, oder überprüfen Sie den Code, um sicherzustellen, dass er nicht schlecht funktioniert, wenn er einen unerwarteten Ausnahmetyp erfasst.

Gehen Sie nicht davon aus, dass ein verwalteter Thread ein Win32-Thread ist – Es handelt sich um eine Faser

Die Verwendung des lokalen Speichers mit verwaltetem Thread funktioniert zwar, Sie können jedoch keinen nicht verwalteten lokalen Threadspeicher verwenden oder davon ausgehen, dass der Code erneut im aktuellen Betriebssystemthread ausgeführt wird. Ändern Sie keine Einstellungen wie das Gebietsschema des Threads. Verwenden Sie zum Abruf der Methoden InitializeCriticalSection oder CreateMutex keinen Plattformaufruf, da diese darauf angewiesen sind, dass der gesperrte Betriebssystemthread wieder entsperrt wird. Da dies bei der Verwendung von Fibern nicht der Fall ist, können kritische Abschnitte in Win32 und Mutex-Verfahren nicht direkt in SQL verwendet werden. Beachten Sie, dass die verwaltete Mutex Klasse diese Threadaffinitätsprobleme nicht behandelt.

Sie können den Zustand eines verwalteten Thread-Objekts größtenteils sicher nutzen und dabei beispielsweise verwalteten threadlokalen Speicher und die aktuelle Benutzeroberflächenkultur des Threads verwenden. Sie können auch das ThreadStaticAttribute verwenden, wodurch nur der aktuell verwaltete Thread auf den Wert einer vorhandenen statischen Variable zugreifen kann. Dies ist eine alternative Methode zur Verwendung threadlokalen Speichers in einer Fiber in der CLR. Aus Programmiermodellgründen können Sie die aktuelle Kultur eines Threads nicht ändern, wenn sie in SQL ausgeführt wird.

Codeanalyseregel

SQL Server wird im Fasermodus ausgeführt; Verwenden Sie keinen lokalen Threadspeicher. Führen Sie außerdem keine Plattformaufrufe von TlsAlloc, TlsFree, TlsGetValue und TlsSetValue. aus.

Überlassen Sie den Identitätswechsel SQL Server

Da der Identitätswechsel auf Threadebene erfolgt und SQL im Fasermodus ausgeführt werden kann, sollte verwalteter Code keine Benutzer imitieren und RevertToSelf nicht aufrufen.

Codeanalyseregel

Überlassen Sie den Identitätswechsel SQL Server. Verwenden Sie RevertToSelf, ImpersonateAnonymousToken, DdeImpersonateClient, ImpersonateDdeClientWindow, ImpersonateLoggedOnUser, ImpersonateNamedPipeClient, ImpersonateSelf, RpcImpersonateClient, RpcRevertToSelf, RpcRevertToSelfEx oder SetThreadToken nicht.

Rufen Sie nicht Thread::Suspend auf

Die Möglichkeit zum Anhalten eines Threads kann ein einfacher Vorgang sein, kann jedoch zu Deadlocks führen. Wenn ein Thread, der eine Sperre hält, von einem zweiten Thread angehalten wird und der zweite Thread versucht, dieselbe Sperre zu übernehmen, tritt ein Deadlock auf. Suspend kann aktuell die Sicherheit, das Laden von Klassen, Remoting und Reflektion beeinträchtigen.

Codeanalyseregel

Rufen Sie nicht Suspend auf. Erwägen Sie stattdessen die Verwendung eines echten Synchronisierungsprimitivs, wie einer Semaphore oder eines ManualResetEvent.

Schützen kritischer Vorgänge mit eingeschränkten Ausführungsregionen und Zuverlässigkeitsverträgen

Stellen Sie bei der Ausführung eines komplexen Vorgangs, der einen freigegebenen Zustand aktualisiert oder der deterministisch entweder vollständig gelingt oder fehlschlägt, sicher, dass dieser von einem eingeschränkten Ausführungsbereich (CER) geschützt ist. Auf diese Weise wird garantiert, dass der Code immer und sogar dann ausgeführt wird, wenn ein Thread unerwartet abgebrochen oder AppDomain unvorhergesehen entladen wird.

Ein CER ist ein besonderer try/finally-Block, dem ein Aufruf der Methode PrepareConstrainedRegions vorangestellt wird.

Dadurch wird der Just-in-Time-Compiler angewiesen, den gesamten Code im letzten Block vorzubereiten, bevor der try Block ausgeführt wird. Dadurch wird sichergestellt, dass der Code im endgültigen Block erstellt und in allen Fällen ausgeführt wird. In einer CER ist es nicht ungewöhnlich, einen leeren try Block zu haben. Die Verwendung einer CER schützt vor asynchronen Threadabbrüchen und Out-of-Memory-Ausnahmen. Unter ExecuteCodeWithGuaranteedCleanup finden Sie einen CER, der zusätzlich Stapelüberläufe in überaus komplexem Code behandelt.

Siehe auch