淺析C#異步中的Overlapped是如何尋址的
一:背景
1. 講故事
前段時(shí)間訓(xùn)練營里的一位朋友提了一個(gè)問題,我用ReadAsync做文件異步讀取時(shí),我知道在Win32層面會傳 lpOverlapped 到內(nèi)核層,那在內(nèi)核層回頭時(shí),它是如何通過這個(gè) lpOverlapped 尋找到 ReadAsync 這個(gè)異步的Task的呢?
這是一個(gè)好問題,這需要回答人對異步完整的運(yùn)轉(zhuǎn)流程有一個(gè)清晰的認(rèn)識,即使有清晰的認(rèn)識也不能很好的口頭表述出來,就算表述出來對方也不一定能聽懂,所以干脆開兩篇文章來嘗試解讀一下吧。
二:lpOverlapped 如何映射
1. 測試案例
為了能夠講清楚,我們先用 fileStream.ReadAsync
方法來寫一段異步讀取來產(chǎn)生Overlapped,參考代碼如下:
static void Main(string[] args) { UseAwaitAsync(); Console.ReadLine(); } static async Task<string> UseAwaitAsync() { string filePath = "D:\\dumps\\trace-1\\GenHome.DMP"; Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} 請求發(fā)起..."); FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 16, useAsync: true); { byte[] buffer = new byte[fileStream.Length]; int bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length); string content = Encoding.UTF8.GetString(buffer, 0, bytesRead); var query = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} 獲取到結(jié)果:{content.Length}"; Console.WriteLine(query); return query; } }
很顯然上面的方法會調(diào)用 Win32 中的 ReadFile,接下來上一下它的簽名和 _OVERLAPPED 結(jié)構(gòu)體。
BOOL ReadFile( [in] HANDLE hFile, [out] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [out, optional] LPDWORD lpNumberOfBytesRead, [in, out, optional] LPOVERLAPPED lpOverlapped ); typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; } DUMMYSTRUCTNAME; PVOID Pointer; } DUMMYUNIONNAME; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;
2. 尋找映射的兩端
既然是映射嘛,肯定要找到兩個(gè)端口,即非托管層的 NativeOverlapped 和 托管層的 ThreadPoolBoundHandleOverlapped。
1.非托管 _OVERLAPPED
在 C# 中用 NativeOverlapped 結(jié)構(gòu)體表示 Win32 的 _OVERLAPPED 結(jié)構(gòu),參考如下:
public struct NativeOverlapped { public nint InternalLow; public nint InternalHigh; public int OffsetLow; public int OffsetHigh; public nint EventHandle; }
2.托管 ThreadPoolBoundHandleOverlapped
ReadAsync 所產(chǎn)生的 Task<int>
在底層是經(jīng)過ValueTask, OverlappedValueTaskSource 一陣痙攣后弄出來的,最后會藏匿在 Overlapped 子類的 ThreadPoolBoundHandleOverlapped 中,參考代碼和模型圖如下:
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValueTask<int> valueTask = this.ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken); if (!valueTask.IsCompletedSuccessfully) { return valueTask.AsTask(); } return this._lastSyncCompletedReadTask.GetTask(valueTask.Result); } private unsafe static ValueTuple<SafeFileHandle.OverlappedValueTaskSource, int> QueueAsyncReadFile(SafeFileHandle handle, Memory<byte> buffer, long fileOffset, CancellationToken cancellationToken, OSFileStreamStrategy strategy) { SafeFileHandle.OverlappedValueTaskSource overlappedValueTaskSource = handle.GetOverlappedValueTaskSource(); NativeOverlapped* ptr = overlappedValueTaskSource.PrepareForOperation(buffer, fileOffset, strategy); if (Interop.Kernel32.ReadFile(handle, (byte*)overlappedValueTaskSource._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, ptr) == 0) { overlappedValueTaskSource.RegisterForCancellation(cancellationToken); } overlappedValueTaskSource.FinishedScheduling(); return new ValueTuple<SafeFileHandle.OverlappedValueTaskSource, int>(overlappedValueTaskSource, -1); }
最后就是兩端的映射關(guān)系了,先通過 malloc 分配了一塊私有內(nèi)存,中間隔了一個(gè)refcount 的 8byte大小,模型圖如下:
3. 眼見為實(shí)
要想眼見為實(shí),可以從C#源碼中的Overlapped.AllocateNativeOverlapped
方法尋找答案。
public unsafe class Overlapped { private NativeOverlapped* AllocateNativeOverlapped(object? userData) { NativeOverlapped* pNativeOverlapped = null; nuint handleCount = 1; pNativeOverlapped = (NativeOverlapped*)NativeMemory.Alloc((nuint)(sizeof(NativeOverlapped) + sizeof(nuint)) + handleCount * (nuint)sizeof(GCHandle)); GCHandleCountRef(pNativeOverlapped) = 0; pNativeOverlapped->InternalLow = default; pNativeOverlapped->InternalHigh = default; pNativeOverlapped->OffsetLow = _offsetLow; pNativeOverlapped->OffsetHigh = _offsetHigh; pNativeOverlapped->EventHandle = _eventHandle; GCHandleRef(pNativeOverlapped, 0) = GCHandle.Alloc(this); GCHandleCountRef(pNativeOverlapped)++; return pRet; } private static ref nuint GCHandleCountRef(NativeOverlapped* pNativeOverlapped) => ref *(nuint*)(pNativeOverlapped + 1); private static ref GCHandle GCHandleRef(NativeOverlapped* pNativeOverlapped, nuint index) => ref *((GCHandle*)((nuint*)(pNativeOverlapped + 1) + 1) + index); }
卦中代碼先用 NativeMemory.Alloc
方法分配了一塊私有內(nèi)存,隨后還把 Overlapped 給 GCHandle.Alloc 住了,這是防止異步期間對象被移動,有了代碼接下來上windbg去眼見為實(shí),在 Kernel32!ReadFile
中下斷點(diǎn)觀察方法的第五個(gè)參數(shù)。
0:000> bp Kernel32!ReadFile
0:000> g
Breakpoint 0 hit
KERNEL32!ReadFile:
00007ffd`fa2f56a0 ff25caca0500 jmp qword ptr [KERNEL32!_imp_ReadFile (00007ffd`fa352170)] ds:00007ffd`fa352170={KERNELBASE!ReadFile (00007ffd`f85c5520)}
0:000> k 5
# Child-SP RetAddr Call Site
00 000000ff`8837e1c8 00007ffd`96229ce3 KERNEL32!ReadFile
01 000000ff`8837e1d0 00007ffd`96411a4a System_Private_CoreLib!Interop.Kernel32.ReadFile+0xa3 [/_/src/coreclr/System.Private.CoreLib/Microsoft.Interop.LibraryImportGenerator/Microsoft.Interop.LibraryImportGenerator/LibraryImports.g.cs @ 6797]
02 000000ff`8837e2d0 00007ffd`96411942 System_Private_CoreLib!System.IO.RandomAccess.QueueAsyncReadFile+0x8a
03 000000ff`8837e350 00007ffd`96433677 System_Private_CoreLib!System.IO.RandomAccess.ReadAtOffsetAsync+0x112 [/_/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs @ 238]
04 000000ff`8837e3f0 00007ffd`9642d5f8 System_Private_CoreLib!System.IO.Strategies.OSFileStreamStrategy.ReadAsync+0xb7 [/_/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @ 290]
0:000> uf 00007ffd`96229ce3
...
6797 00007ffd`96229c98 4c8b7d30 mov r15,qword ptr [rbp+30h]
6797 00007ffd`96229c9c 4c897c2420 mov qword ptr [rsp+20h],r15
6797 00007ffd`96229ca1 498bce mov rcx,r14
6797 00007ffd`96229ca4 48894dac mov qword ptr [rbp-54h],rcx
6797 00007ffd`96229ca8 488bd3 mov rdx,rbx
6797 00007ffd`96229cab 488955a4 mov qword ptr [rbp-5Ch],rdx
6797 00007ffd`96229caf 448bc6 mov r8d,esi
6797 00007ffd`96229cb2 448945b4 mov dword ptr [rbp-4Ch],r8d
6797 00007ffd`96229cb6 4c8bcf mov r9,rdi
6797 00007ffd`96229cb9 4c894d9c mov qword ptr [rbp-64h],r9
6797 00007ffd`96229cbd 488d8d40ffffff lea rcx,[rbp-0C0h]
6797 00007ffd`96229cc4 ff159e909e00 call qword ptr [System_Private_CoreLib!Interop.CallStringMethod+0x5ab9c8 (00007ffd`96c12d68)]
6797 00007ffd`96229cca 488b055708a100 mov rax,qword ptr [System_Private_CoreLib!Interop.CallStringMethod+0x5d3188 (00007ffd`96c3a528)]
6797 00007ffd`96229cd1 488b4dac mov rcx,qword ptr [rbp-54h]
6797 00007ffd`96229cd5 488b55a4 mov rdx,qword ptr [rbp-5Ch]
6797 00007ffd`96229cd9 448b45b4 mov r8d,dword ptr [rbp-4Ch]
6797 00007ffd`96229cdd 4c8b4d9c mov r9,qword ptr [rbp-64h]
6797 00007ffd`96229ce1 ff10 call qword ptr [rax]
6797 00007ffd`96229ce3 8bd8 mov ebx,eax
仔細(xì)閱讀卦中的匯編代碼,通過這句 r15,qword ptr [rbp+30h]
可知 pNativeOverlapped 是保存在 r15
寄存器中。
0:000> r r15
r15=00000241ca2d4d70
0:000> dp 00000241ca2d4d70
00000241`ca2d4d70 00000000`00000000 00000000`00000000
00000241`ca2d4d80 00000000`00000000 00000000`00000000
00000241`ca2d4d90 00000000`00000001 00000241`c8761358
根據(jù)上面的模型圖,00000241ca2d4d90
保存的是引用計(jì)數(shù),00000241c8761358
就是我們的 ThreadPoolBoundHandleOverlapped
,可以 !do 它一下便知。
最后用 dnspy 在 Overlapped.GetOverlappedFromNative
方法中下一個(gè)斷點(diǎn),這個(gè)方法會在異步處理完成后,執(zhí)行NativeOverlapped尋址ThreadPoolBoundHandleOverlapped 的邏輯,截圖如下,那個(gè) ReadAsync保存在內(nèi)部的 _continuationState 字段里。
三:總結(jié)
C#的傳統(tǒng)做法大多都是采用傳參數(shù)的方式來建議映射關(guān)系,而本篇中用 malloc 開辟一塊私有區(qū)域來映射兩者的關(guān)系也真是獨(dú)一份,實(shí)屬無奈!
到此這篇關(guān)于淺析C#異步中的Overlapped是如何尋址的的文章就介紹到這了,更多相關(guān)C#異步Overlapped如何尋址內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
c#讀取圖像保存到數(shù)據(jù)庫中(數(shù)據(jù)庫保存圖片)
這篇文章主要介紹了使用c#讀取圖像保存到數(shù)據(jù)庫中的方法,大家參考使用吧2014-01-01C#結(jié)束Excel進(jìn)程的步驟教學(xué)
在本篇文章里小編給大家分享了關(guān)于C#結(jié)束Excel進(jìn)程的步驟教學(xué)內(nèi)容,有興趣的朋友們學(xué)習(xí)下。2019-01-01C#如何讀取Txt大數(shù)據(jù)并更新到數(shù)據(jù)庫詳解
這篇文章主要給大家介紹了關(guān)于C#如何讀取Txt大數(shù)據(jù)并更新到數(shù)據(jù)庫的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用C#具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08C#中實(shí)現(xiàn)輸入漢字獲取其拼音(漢字轉(zhuǎn)拼音)的2種方法
這篇文章主要介紹了C#中實(shí)現(xiàn)輸入漢字獲取其拼音(漢字轉(zhuǎn)拼音)的2種方法,本文分別給出了使用微軟語言包、手動編碼實(shí)現(xiàn)兩種實(shí)現(xiàn)方式,需要的朋友可以參考下2015-01-01C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
這篇文章主要為大家詳細(xì)介紹了C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03