淺析C#異步中的Overlapped是如何尋址的
一:背景
1. 講故事
前段時間訓練營里的一位朋友提了一個問題,我用ReadAsync做文件異步讀取時,我知道在Win32層面會傳 lpOverlapped 到內核層,那在內核層回頭時,它是如何通過這個 lpOverlapped 尋找到 ReadAsync 這個異步的Task的呢?
這是一個好問題,這需要回答人對異步完整的運轉流程有一個清晰的認識,即使有清晰的認識也不能很好的口頭表述出來,就算表述出來對方也不一定能聽懂,所以干脆開兩篇文章來嘗試解讀一下吧。
二:lpOverlapped 如何映射
1. 測試案例
為了能夠講清楚,我們先用 fileStream.ReadAsync 方法來寫一段異步讀取來產生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")} 獲取到結果:{content.Length}";
Console.WriteLine(query);
return query;
}
}很顯然上面的方法會調用 Win32 中的 ReadFile,接下來上一下它的簽名和 _OVERLAPPED 結構體。
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. 尋找映射的兩端
既然是映射嘛,肯定要找到兩個端口,即非托管層的 NativeOverlapped 和 托管層的 ThreadPoolBoundHandleOverlapped。
1.非托管 _OVERLAPPED
在 C# 中用 NativeOverlapped 結構體表示 Win32 的 _OVERLAPPED 結構,參考如下:
public struct NativeOverlapped
{
public nint InternalLow;
public nint InternalHigh;
public int OffsetLow;
public int OffsetHigh;
public nint EventHandle;
}
2.托管 ThreadPoolBoundHandleOverlapped
ReadAsync 所產生的 Task<int> 在底層是經過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);
}

最后就是兩端的映射關系了,先通過 malloc 分配了一塊私有內存,中間隔了一個refcount 的 8byte大小,模型圖如下:

3. 眼見為實
要想眼見為實,可以從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 方法分配了一塊私有內存,隨后還把 Overlapped 給 GCHandle.Alloc 住了,這是防止異步期間對象被移動,有了代碼接下來上windbg去眼見為實,在 Kernel32!ReadFile 中下斷點觀察方法的第五個參數。
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
仔細閱讀卦中的匯編代碼,通過這句 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
根據上面的模型圖,00000241ca2d4d90 保存的是引用計數,00000241c8761358 就是我們的 ThreadPoolBoundHandleOverlapped ,可以 !do 它一下便知。

最后用 dnspy 在 Overlapped.GetOverlappedFromNative 方法中下一個斷點,這個方法會在異步處理完成后,執(zhí)行NativeOverlapped尋址ThreadPoolBoundHandleOverlapped 的邏輯,截圖如下,那個 ReadAsync保存在內部的 _continuationState 字段里。

三:總結
C#的傳統(tǒng)做法大多都是采用傳參數的方式來建議映射關系,而本篇中用 malloc 開辟一塊私有區(qū)域來映射兩者的關系也真是獨一份,實屬無奈!
到此這篇關于淺析C#異步中的Overlapped是如何尋址的的文章就介紹到這了,更多相關C#異步Overlapped如何尋址內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C#中實現(xiàn)輸入漢字獲取其拼音(漢字轉拼音)的2種方法
這篇文章主要介紹了C#中實現(xiàn)輸入漢字獲取其拼音(漢字轉拼音)的2種方法,本文分別給出了使用微軟語言包、手動編碼實現(xiàn)兩種實現(xiàn)方式,需要的朋友可以參考下2015-01-01
C# FileStream實現(xiàn)多線程斷點續(xù)傳
這篇文章主要為大家詳細介紹了C# FileStream實現(xiàn)多線程斷點續(xù)傳,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-03-03

