淺析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-01
C#結(jié)束Excel進(jìn)程的步驟教學(xué)
在本篇文章里小編給大家分享了關(guān)于C#結(jié)束Excel進(jìn)程的步驟教學(xué)內(nèi)容,有興趣的朋友們學(xué)習(xí)下。2019-01-01
C#如何讀取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-08
C#中實(shí)現(xiàn)輸入漢字獲取其拼音(漢字轉(zhuǎn)拼音)的2種方法
這篇文章主要介紹了C#中實(shí)現(xiàn)輸入漢字獲取其拼音(漢字轉(zhuǎn)拼音)的2種方法,本文分別給出了使用微軟語言包、手動編碼實(shí)現(xiàn)兩種實(shí)現(xiàn)方式,需要的朋友可以參考下2015-01-01
C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
這篇文章主要為大家詳細(xì)介紹了C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03

