Szerelvény eltávolíthatóságának használata és hibakeresése a .NET-ben
A .NET (Core) lehetővé teszi a szerelvények betöltését és későbbi eltávolítását. A .NET-keretrendszer egyéni alkalmazástartományokat használtak erre a célra, de a .NET (Core) csak egyetlen alapértelmezett alkalmazástartományt támogat.
A kiüríthetőséget a . AssemblyLoadContexttámogatja. A szerelvényeket be lehet tölteni egy gyűjthetőbe AssemblyLoadContext
, metódusokat hajthat végre bennük, vagy megvizsgálhatja őket visszatükrözéssel, és végül eltávolíthatja a AssemblyLoadContext
. Ez eltávolítja a berakott szerelvényeket a AssemblyLoadContext
.
Van egy figyelemre méltó különbség az AppDomains használatával AssemblyLoadContext
és használatával történő kirakodás között. Az AppDomains használatával a kiürítés kényszerítve van. A kiürítéskor a cél AppDomainben futó összes szál megszakad, a cél AppDomainben létrehozott felügyelt COM-objektumok el lesznek pusztítva, és így tovább. Ezzel AssemblyLoadContext
a "kooperatív" a kipakolás. A metódus meghívása AssemblyLoadContext.Unload csak a kiürítést kezdeményezi. A kiürítés a következő után fejeződik be:
- Egyetlen szál sem rendelkezik metódusokkal a hívásverembe
AssemblyLoadContext
betöltött szerelvényekből. - Az ilyen típusú szerelvényekbe betöltött
AssemblyLoadContext
szerelvények egyik típusára sem, és maguk a szerelvényekre a következők hivatkoznak:- Külső hivatkozások, kivéve a
AssemblyLoadContext
gyenge hivatkozásokat (WeakReference vagy WeakReference<T>). - Erős szemétgyűjtő (GC) fogópontok (GCHandleType.Normal vagy GCHandleType.Pinned) belülről
AssemblyLoadContext
és kívülről is.
- Külső hivatkozások, kivéve a
Collectible AssemblyLoadContext használata
Ez a szakasz részletes részletes oktatóanyagot tartalmaz, amely bemutatja, hogyan tölthet be egy .NET-alkalmazást (Core-alkalmazást) egy gyűjthetőbe, végrehajthatja AssemblyLoadContext
a belépési pontját, majd eltávolíthatja azt. A teljes minta a következő helyen https://github.com/dotnet/samples/tree/main/core/tutorials/Unloadingtalálható: .
Gyűjthető AssemblyLoadContext létrehozása
Származtathatja az osztályt a AssemblyLoadContext metódusból, és felülbírálhatja annak metódusát AssemblyLoadContext.Load . Ez a módszer feloldja az összes olyan szerelvényre mutató hivatkozást, amely az ebbe AssemblyLoadContext
betöltött szerelvények függőségei.
Az alábbi kód a legegyszerűbb egyéni AssemblyLoadContext
példa:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Mint látható, a Load
metódus ad vissza null
. Ez azt jelenti, hogy az összes függőségi szerelvény betöltődik az alapértelmezett környezetbe, és az új környezet csak a kifejezetten betöltött szerelvényeket tartalmazza.
Ha a függőségek egy részét vagy egészét is be AssemblyLoadContext
szeretné tölteni, a metódusban használhatja.AssemblyDependencyResolver
Load
A AssemblyDependencyResolver
szerelvénynevek feloldása abszolút szerelvényfájl-elérési utakra. A feloldó a környezetbe betöltött főszerelvény könyvtárában található .deps.json fájl- és szerelvényfájlokat használja.
using System.Reflection;
using System.Runtime.Loader;
namespace complex
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
}
protected override Assembly? Load(AssemblyName name)
{
string? assemblyPath = _resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
}
Egyéni gyűjthető AssemblyLoadContext használata
Ez a szakasz feltételezi, hogy a rendszer a TestAssemblyLoadContext
használt egyszerűbb verziót használja.
Létrehozhatja az egyéni AssemblyLoadContext
példányt, és az alábbiak szerint tölthet be egy szerelvényt:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
A rendszer meghívja a metódust a betöltött szerelvény által hivatkozott szerelvények mindegyikére, TestAssemblyLoadContext.Load
hogy eldönthesse TestAssemblyLoadContext
, honnan szerezze be a szerelvényt. Ebben az esetben a rendszer azt null
jelzi, hogy az alapértelmezett környezetbe kell betölteni olyan helyekről, amelyeket a futtatókörnyezet alapértelmezés szerint a szerelvények betöltésére használ.
Most, hogy egy szerelvény be lett töltve, végrehajthat belőle egy metódust. Futtassa a metódust Main
:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
A metódus visszatérése után kezdeményezheti a Main
kiürítést, ha meghívja a Unload
metódust az egyénire AssemblyLoadContext
, vagy eltávolítja a következőre AssemblyLoadContext
mutató hivatkozást:
alc.Unload();
Ez elegendő a tesztszerelvény eltávolításához. Ezután mindezt egy külön, nem elválasztható metódusba helyezi, amely biztosítja, hogy a TestAssemblyLoadContext
, Assembly
és MethodInfo
(a Assembly.EntryPoint
) nem tartható életben veremhelyhivatkozásokkal (valós vagy JIT által bevezetett helyiek). Ez életben tarthatja az TestAssemblyLoadContext
embert, és megakadályozhatja a kirakodást.
Emellett adjon vissza egy gyenge hivatkozást a AssemblyLoadContext
fájlra, hogy később használhassa a kipakolás befejezésének észleléséhez.
[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
alcWeakRef = new WeakReference(alc, trackResurrection: true);
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
alc.Unload();
}
Most futtathatja ezt a függvényt a szerelvény betöltéséhez, végrehajtásához és eltávolításához.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
A kirakodás azonban nem fejeződik be azonnal. Mint korábban említettük, a szemétgyűjtőre támaszkodik, hogy összegyűjtse az összes objektumot a tesztszerelvényből. Sok esetben nem szükséges megvárni a kiürítés befejezését. Vannak azonban olyan esetek, amikor hasznos tudni, hogy a kirakodás befejeződött. Előfordulhat például, hogy törölni szeretné a lemezről az egyénibe AssemblyLoadContext
betöltött szerelvényfájlt. Ilyen esetben a következő kódrészlet használható. Elindítja a szemétgyűjtést, és várakozik a függőben lévő véglegesítőkre egy hurokban, amíg az egyénire AssemblyLoadContext
mutató gyenge hivatkozás be nem van állítva null
, jelezve, hogy a célobjektum összegyűjtve lett. A legtöbb esetben csak egy áthaladás szükséges a hurokon. Összetettebb esetekben azonban, amikor a kód AssemblyLoadContext
által létrehozott objektumok véglegesítőkkel rendelkeznek, több passzra lehet szükség.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Korlátozások
A gyűjthető szerelvényekre vonatkozó általános korlátozásoknak meg kell felelniük a gyűjthető AssemblyLoadContext
szerelvényekre vonatkozó általános korlátozásoknak. A következő korlátozások is érvényesek:
- A C++/CLI nyelven írt szerelvények nem támogatottak.
- A ReadyToRun által létrehozott kód figyelmen kívül lesz hagyva.
A kiürítési esemény
Bizonyos esetekben szükség lehet arra, hogy a kód be legyen töltve egy egyéni AssemblyLoadContext
fájlba a törlés indításakor. Előfordulhat például, hogy le kell állítania a szálakat, vagy törölnie kell az erős GC-fogantyúkat. Ilyen Unloading
esetekben az esemény használható. Csatlakoztathat egy kezelőt, amely elvégzi a szükséges tisztítást az eseményhez.
Eltávolíthatósági problémák elhárítása
A kirakodás kooperatív jellege miatt könnyű elfelejteni azokat a hivatkozásokat, amelyek életben tartják a cuccot egy gyűjtőben AssemblyLoadContext
, és megakadályozzák a kirakodást. Íme az entitások összegzése (amelyek némelyike nem látható), amelyek tartalmazhatják a hivatkozásokat:
- A gyűjtőn
AssemblyLoadContext
kívülről tárolt rendszeres hivatkozások, amelyek egy veremhelyen vagy egy processzorregisztrálóban vannak tárolva (a metódus helyi beállításai, amelyeket explicit módon a felhasználói kód hoz létre, vagy implicit módon a just-in-time (JIT) fordító), egy statikus változó vagy egy erős (rögzítési) GC-leíró, és tranzitív módon a következőkre mutat:- A gyűjtőbe
AssemblyLoadContext
betöltött szerelvény. - Egy ilyen szerelvény típusa.
- Egy ilyen szerelvényből származó típuspéldány.
- A gyűjtőbe
- A gyűjtőbe
AssemblyLoadContext
betöltött szerelvényből kódot futtató szálak. - A gyűjteményben
AssemblyLoadContext
létrehozott egyéni, nem értelmezhetőAssemblyLoadContext
típusok példányai. - Függőben lévő RegisteredWaitHandle példányok, amelyben a visszahívások az egyéni
AssemblyLoadContext
metódusokra van beállítva.
Tipp.
A veremhelyeken vagy a processzorregisztrálókban tárolt objektumhivatkozások, amelyek megakadályozhatják a törlést AssemblyLoadContext
, a következő helyzetekben fordulhatnak elő:
- Ha a függvényhívás eredményei közvetlenül egy másik függvénynek lesznek átadva, annak ellenére, hogy nincs felhasználó által létrehozott helyi változó.
- Amikor a JIT-fordító egy olyan objektumra mutat, amely egy metódus egy bizonyos pontján elérhető volt.
Törlési problémák hibakeresése
A kiürítéssel kapcsolatos hibakeresési problémák fárasztóak lehetnek. Olyan helyzetekbe kerülhet, amikor nem tudja, mi tartható AssemblyLoadContext
életben, de a kirakodás meghiúsul. A legjobb eszköz, hogy segítsen, hogy a WinDbg (vagy LLDB a Unix) az SOS beépülő modul. Meg kell találnia, hogy mi tart egy LoaderAllocator
adott élőhöz AssemblyLoadContext
tartozót. Az SOS beépülő modul segítségével áttekintheti a GC halomobjektumokat, azok hierarchiáit és gyökereit.
Az SOS beépülő modul hibakeresőbe való betöltéséhez írja be az alábbi parancsok egyikét a hibakereső parancssorába.
WinDbg-ben (ha még nincs betöltve):
.loadby sos coreclr
AZ LLDB-ben:
plugin load /path/to/libsosplugin.so
Most egy olyan példaprogramot fog hibakeresésre végezni, amely problémákat tapasztal a kiürítéssel kapcsolatban. A forráskód a Példa forráskód szakaszban érhető el. Amikor a WinDbg alatt futtatja, a program közvetlenül azután tör be a hibakeresőbe, hogy megkísérelte ellenőrizni a törlés sikerességét. Ezután elkezdheti keresni a bűnösöket.
Tipp.
Ha az LLDB használatával hibakeresést hajt végre a Unixon, az alábbi példák SOS-parancsai nem eléjük vannak !
beállítva.
!dumpheap -type LoaderAllocator
Ez a parancs a GC-halomtárban található típusnévvel rendelkező LoaderAllocator
összes objektumot kiírja. Példa:
Address MT Size
000002b78000ce40 00007ffadc93a288 48
000002b78000ceb0 00007ffadc93a218 24
Statistics:
MT Count TotalSize Class Name
00007ffadc93a218 1 24 System.Reflection.LoaderAllocatorScout
00007ffadc93a288 1 48 System.Reflection.LoaderAllocator
Total 2 objects
A "Statisztika:" részben ellenőrizze a MT
MethodTable
() objektumot, amely a System.Reflection.LoaderAllocator
fontos objektumhoz tartozik. Ezután az elején található listában keresse meg azt a bejegyzést, amely MT
megfelel az adottnak, és kérje le magának az objektumnak a címét. Ebben az esetben ez "000002b78000ce40".
Most, hogy megismerte az LoaderAllocator
objektum címét, egy másik paranccsal megkeresheti annak GC-gyökerét:
!gcroot 0x000002b78000ce40
Ez a parancs a példányhoz vezető objektumhivatkozások láncolatát LoaderAllocator
dobja ki. A lista a gyökérrel kezdődik, amely az az entitás, amely életben tartja a LoaderAllocator
problémát, és így a probléma magja. A gyökér lehet veremhely, processzorregisztrálás, GC-leíró vagy statikus változó.
Íme egy példa a parancs kimenetére gcroot
:
Thread 4ac:
000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
rbp-20: 000000cf9499dd90
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
-> 000002b78000d1d0 System.RuntimeType
-> 000002b78000ce40 System.Reflection.LoaderAllocator
HandleTable:
000002b7f8a81198 (strong handle)
-> 000002b78000d948 test.Test
-> 000002b78000ce40 System.Reflection.LoaderAllocator
000002b7f8a815f8 (pinned handle)
-> 000002b790001038 System.Object[]
-> 000002b78000d390 example.TestInfo
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
-> 000002b78000d1d0 System.RuntimeType
-> 000002b78000ce40 System.Reflection.LoaderAllocator
Found 3 roots.
A következő lépés annak kiderítése, hogy hol található a gyökér, hogy kijavíthassa. A legegyszerűbb eset az, ha a gyökér egy veremhely vagy egy processzorregisztrálás. Ebben az esetben a gcroot
függvény neve jelenik meg, amelynek kerete tartalmazza a függvényt végrehajtó gyökér- és szálat. A nehéz eset az, ha a gyökér statikus változó vagy GC-leíró.
Az előző példában az első gyökér a címben található függvény example.Program.Main(System.String[])
keretében tárolt helyi típusú System.Reflection.RuntimeMethodInfo
(rbp
a processzorregisztrálásrbp
, a -20 pedig a regiszter hexadecimális eltolása).rbp-20
A második gyökér egy normál (erős), GCHandle
amely az osztály egy példányára test.Test
mutató hivatkozást tartalmaz.
A harmadik gyökér egy rögzített GCHandle
. Ez valójában egy statikus változó, de sajnos nem lehet megmondani. A referenciatípusok statikus elemeit egy felügyelt objektumtömb tárolja belső futtatókörnyezeti struktúrákban.
Egy másik eset, amely megakadályozhatja a kipakolást, AssemblyLoadContext
ha egy szál egy metóduskerettel rendelkezik a verembe AssemblyLoadContext
betöltött szerelvényből. Ezt az összes szál felügyelt hívásveremeinek memóriaképével ellenőrizheti:
~*e !clrstack
A parancs azt jelenti, hogy "a parancs minden szálára !clrstack
érvényes". A példában a következő parancs kimenete látható. Sajnos a Unix LLDB-jének nincs módja arra, hogy parancsot alkalmazzon az összes szálra, ezért manuálisan kell váltania a szálakat, és meg kell ismételnie a clrstack
parancsot. Hagyja figyelmen kívül azokat a szálakat, ahol a hibakereső a következőhöz hasonló szöveget írja: "Nem lehet a felügyelt vermet járni".
OS Thread Id: 0x6ba8 (0)
Child SP IP Call Site
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
OS Thread Id: 0x2ae4 (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x61a4 (2)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x7fdc (3)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5390 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5ec8 (5)
Child SP IP Call Site
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
OS Thread Id: 0x4624 (6)
Child SP IP Call Site
GetFrameContext failed: 1
0000000000000000 0000000000000000
OS Thread Id: 0x60bc (7)
Child SP IP Call Site
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]
Mint látható, az utolsó szál van test.Program.ThreadProc()
. Ez egy függvény a szerelvény berakottAssemblyLoadContext
, és így tartja életben.AssemblyLoadContext
Példa forráskódra
Az előző hibakeresési példában a következő, eltávolíthatósági problémákat tartalmazó kódot használjuk.
Fő tesztelési program
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace example
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
class TestInfo
{
public TestInfo(MethodInfo? mi)
{
_entryPoint = mi;
}
MethodInfo? _entryPoint;
}
class Program
{
static TestInfo? entryPoint;
[MethodImpl(MethodImplOptions.NoInlining)]
static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo? testEntryPoint)
{
var alc = new TestAssemblyLoadContext();
testAlcWeakRef = new WeakReference(alc);
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
if (a == null)
{
testEntryPoint = null;
Console.WriteLine("Loading the test assembly failed");
return -1;
}
var args = new object[1] {new string[] {"Hello"}};
// Issue preventing unloading #1 - we keep MethodInfo of a method
// for an assembly loaded into the TestAssemblyLoadContext in a static variable.
entryPoint = new TestInfo(a.EntryPoint);
testEntryPoint = a.EntryPoint;
var oResult = a.EntryPoint?.Invoke(null, args);
alc.Unload();
return (oResult is int result) ? result : -1;
}
static void Main(string[] args)
{
WeakReference testAlcWeakRef;
// Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
MethodInfo? testEntryPoint;
int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
System.Diagnostics.Debugger.Break();
Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
}
}
}
A TestAssemblyLoadContextbe betöltött program
Az alábbi kód a fő tesztelési programban a metódusnak ExecuteAndUnload
átadott test.dll jelöli.
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace test
{
class Test
{
}
class Program
{
public static void ThreadProc()
{
// Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
Thread.Sleep(Timeout.Infinite);
}
static GCHandle handle;
static int Main(string[] args)
{
// Issue preventing unloading #3 - normal GC handle
handle = GCHandle.Alloc(new Test());
Thread t = new Thread(new ThreadStart(ThreadProc));
t.IsBackground = true;
t.Start();
Console.WriteLine($"Hello from the test: args[0] = {args[0]}");
return 1;
}
}
}