聊一聊 C#异步中的Overlapped是如何寻址的

打印 上一主题 下一主题

主题 860|帖子 860|积分 2580

一:背景

1. 讲故事

前段时间训练营里的一位朋友提了一个问题,我用ReadAsync做文件异步读取时,我知道在Win32层面会传 lpOverlapped 到内核层,那在内核层回头时,它是如何通过这个 lpOverlapped 寻找到 ReadAsync 这个异步的Task的呢?
这是一个好问题,这需要答复人对异步完整的运转流程有一个清楚的认识,纵然有清楚的认识也不能很好的口头表述出来,就算表述出来对方也不一定能听懂,以是干脆开两篇文章来尝试解读一下吧。
二:lpOverlapped 如何映射

1. 测试案例

为了能够讲清楚,我们先用 fileStream.ReadAsync 方法来写一段异步读取来产生Overlapped,参考代码如下:
  1.         static void Main(string[] args)
  2.         {
  3.             UseAwaitAsync();
  4.             Console.ReadLine();
  5.         }
  6.         static async Task<string> UseAwaitAsync()
  7.         {
  8.             string filePath = "D:\\dumps\\trace-1\\GenHome.DMP";
  9.             Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} 请求发起...");
  10.             FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 16, useAsync: true);
  11.             {
  12.                 byte[] buffer = new byte[fileStream.Length];
  13.                 int bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length);
  14.                 string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
  15.                 var query = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} 获取到结果:{content.Length}";
  16.                 Console.WriteLine(query);
  17.                 return query;
  18.             }
  19.         }
复制代码
很显然上面的方法会调用 Win32 中的 ReadFile,接下来上一下它的签名和 _OVERLAPPED 结构体。
  1. BOOL ReadFile(
  2.   [in]                HANDLE       hFile,
  3.   [out]               LPVOID       lpBuffer,
  4.   [in]                DWORD        nNumberOfBytesToRead,
  5.   [out, optional]     LPDWORD      lpNumberOfBytesRead,
  6.   [in, out, optional] LPOVERLAPPED lpOverlapped
  7. );
  8. typedef struct _OVERLAPPED {
  9.   ULONG_PTR Internal;
  10.   ULONG_PTR InternalHigh;
  11.   union {
  12.     struct {
  13.       DWORD Offset;
  14.       DWORD OffsetHigh;
  15.     } DUMMYSTRUCTNAME;
  16.     PVOID Pointer;
  17.   } DUMMYUNIONNAME;
  18.   HANDLE    hEvent;
  19. } OVERLAPPED, *LPOVERLAPPED;
复制代码
2. 寻找映射的两端

既然是映射嘛,肯定要找到两个端口,即非托管层的 NativeOverlapped 和 托管层的 ThreadPoolBoundHandleOverlapped。

  • 非托管 _OVERLAPPED
在 C# 中用 NativeOverlapped 结构体表示 Win32 的 _OVERLAPPED 结构,参考如下:
  1. public struct NativeOverlapped
  2. {
  3.         public nint InternalLow;
  4.         public nint InternalHigh;
  5.         public int OffsetLow;
  6.         public int OffsetHigh;
  7.         public nint EventHandle;
  8. }
复制代码

  • 托管 ThreadPoolBoundHandleOverlapped
ReadAsync 所产生的 Task 在底层是经过ValueTask, OverlappedValueTaskSource 一阵痉挛后弄出来的,最后会藏匿在 Overlapped 子类的 ThreadPoolBoundHandleOverlapped 中,参考代码和模子图如下:
  1.         public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
  2.         {
  3.             ValueTask<int> valueTask = this.ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken);
  4.             if (!valueTask.IsCompletedSuccessfully)
  5.             {
  6.                 return valueTask.AsTask();
  7.             }
  8.             return this._lastSyncCompletedReadTask.GetTask(valueTask.Result);
  9.         }
  10.         private unsafe static ValueTuple<SafeFileHandle.OverlappedValueTaskSource, int> QueueAsyncReadFile(SafeFileHandle handle, Memory<byte> buffer, long fileOffset, CancellationToken cancellationToken, OSFileStreamStrategy strategy)
  11.         {
  12.             SafeFileHandle.OverlappedValueTaskSource overlappedValueTaskSource = handle.GetOverlappedValueTaskSource();
  13.             
  14.             NativeOverlapped* ptr = overlappedValueTaskSource.PrepareForOperation(buffer, fileOffset, strategy);
  15.             if (Interop.Kernel32.ReadFile(handle, (byte*)overlappedValueTaskSource._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, ptr) == 0)
  16.             {
  17.                 overlappedValueTaskSource.RegisterForCancellation(cancellationToken);
  18.             }
  19.             overlappedValueTaskSource.FinishedScheduling();
  20.             return new ValueTuple<SafeFileHandle.OverlappedValueTaskSource, int>(overlappedValueTaskSource, -1);
  21.         }
复制代码

最后就是两端的映射关系了,先通过 malloc 分配了一块私有内存,中间隔了一个refcount 的 8byte大小,模子图如下:

3. 眼见为实

要想眼见为实,可以从C#源码中的Overlapped.AllocateNativeOverlapped方法寻找答案。
  1.     public unsafe class Overlapped
  2.     {
  3.         private NativeOverlapped* AllocateNativeOverlapped(object? userData)
  4.         {
  5.             NativeOverlapped* pNativeOverlapped = null;
  6.             nuint handleCount = 1;
  7.             pNativeOverlapped = (NativeOverlapped*)NativeMemory.Alloc((nuint)(sizeof(NativeOverlapped) + sizeof(nuint)) + handleCount * (nuint)sizeof(GCHandle));
  8.             GCHandleCountRef(pNativeOverlapped) = 0;
  9.             pNativeOverlapped->InternalLow = default;
  10.             pNativeOverlapped->InternalHigh = default;
  11.             pNativeOverlapped->OffsetLow = _offsetLow;
  12.             pNativeOverlapped->OffsetHigh = _offsetHigh;
  13.             pNativeOverlapped->EventHandle = _eventHandle;
  14.             GCHandleRef(pNativeOverlapped, 0) = GCHandle.Alloc(this);
  15.             GCHandleCountRef(pNativeOverlapped)++;
  16.             return pRet;
  17.         }
  18.         private static ref nuint GCHandleCountRef(NativeOverlapped* pNativeOverlapped)
  19.                                => ref *(nuint*)(pNativeOverlapped + 1);
  20.         private static ref GCHandle GCHandleRef(NativeOverlapped* pNativeOverlapped, nuint index)
  21.                               => ref *((GCHandle*)((nuint*)(pNativeOverlapped + 1) + 1) + index);
  22.     }
复制代码
卦中代码先用 NativeMemory.Alloc 方法分配了一块私有内存,随后还把 Overlapped 给 GCHandle.Alloc 住了,这是防止异步期间对象被移动,有了代码接下来上windbg去眼见为实,在 Kernel32!ReadFile 中下断点观察方法的第五个参数。
  1. 0:000> bp Kernel32!ReadFile
  2. 0:000> g
  3. Breakpoint 0 hit
  4. KERNEL32!ReadFile:
  5. 00007ffd`fa2f56a0 ff25caca0500    jmp     qword ptr [KERNEL32!_imp_ReadFile (00007ffd`fa352170)] ds:00007ffd`fa352170={KERNELBASE!ReadFile (00007ffd`f85c5520)}
  6. 0:000> k 5
  7. # Child-SP          RetAddr               Call Site
  8. 00 000000ff`8837e1c8 00007ffd`96229ce3     KERNEL32!ReadFile
  9. 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]
  10. 02 000000ff`8837e2d0 00007ffd`96411942     System_Private_CoreLib!System.IO.RandomAccess.QueueAsyncReadFile+0x8a
  11. 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]
  12. 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]
  13. 0:000> uf 00007ffd`96229ce3
  14. ...
  15. 6797 00007ffd`96229c98 4c8b7d30        mov     r15,qword ptr [rbp+30h]
  16. 6797 00007ffd`96229c9c 4c897c2420      mov     qword ptr [rsp+20h],r15
  17. 6797 00007ffd`96229ca1 498bce          mov     rcx,r14
  18. 6797 00007ffd`96229ca4 48894dac        mov     qword ptr [rbp-54h],rcx
  19. 6797 00007ffd`96229ca8 488bd3          mov     rdx,rbx
  20. 6797 00007ffd`96229cab 488955a4        mov     qword ptr [rbp-5Ch],rdx
  21. 6797 00007ffd`96229caf 448bc6          mov     r8d,esi
  22. 6797 00007ffd`96229cb2 448945b4        mov     dword ptr [rbp-4Ch],r8d
  23. 6797 00007ffd`96229cb6 4c8bcf          mov     r9,rdi
  24. 6797 00007ffd`96229cb9 4c894d9c        mov     qword ptr [rbp-64h],r9
  25. 6797 00007ffd`96229cbd 488d8d40ffffff  lea     rcx,[rbp-0C0h]
  26. 6797 00007ffd`96229cc4 ff159e909e00    call    qword ptr [System_Private_CoreLib!Interop.CallStringMethod+0x5ab9c8 (00007ffd`96c12d68)]
  27. 6797 00007ffd`96229cca 488b055708a100  mov     rax,qword ptr [System_Private_CoreLib!Interop.CallStringMethod+0x5d3188 (00007ffd`96c3a528)]
  28. 6797 00007ffd`96229cd1 488b4dac        mov     rcx,qword ptr [rbp-54h]
  29. 6797 00007ffd`96229cd5 488b55a4        mov     rdx,qword ptr [rbp-5Ch]
  30. 6797 00007ffd`96229cd9 448b45b4        mov     r8d,dword ptr [rbp-4Ch]
  31. 6797 00007ffd`96229cdd 4c8b4d9c        mov     r9,qword ptr [rbp-64h]
  32. 6797 00007ffd`96229ce1 ff10            call    qword ptr [rax]
  33. 6797 00007ffd`96229ce3 8bd8            mov     ebx,eax
复制代码
仔细阅读卦中的汇编代码,通过这句 r15,qword ptr [rbp+30h] 可知 pNativeOverlapped 是生存在 r15 寄存器中。
  1. 0:000> r r15
  2. r15=00000241ca2d4d70
  3. 0:000> dp 00000241ca2d4d70
  4. 00000241`ca2d4d70  00000000`00000000 00000000`00000000
  5. 00000241`ca2d4d80  00000000`00000000 00000000`00000000
  6. 00000241`ca2d4d90  00000000`00000001 00000241`c8761358
复制代码
根据上面的模子图,00000241ca2d4d90 生存的是引用计数,00000241c8761358 就是我们的 ThreadPoolBoundHandleOverlapped ,可以 !do 它一下便知。

最后用 dnspy 在 Overlapped.GetOverlappedFromNative 方法中下一个断点,这个方法会在异步处理完成后,实行NativeOverlapped寻址ThreadPoolBoundHandleOverlapped 的逻辑,截图如下,那个 ReadAsync生存在内部的 _continuationState 字段里。

三:总结

C#的传统做法大多都是接纳传参数的方式来建议映射关系,而本篇中用 malloc 开发一块私有区域来映射两者的关系也真是独一份,实属无奈!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宝塔山

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表