首页 资讯频道 互联频道 智能频道 网络 数据频道 安全频道 服务器频道 存储频道

Span统一C#进程中的三大块内存访问:栈内存、托管堆内存、非托管堆内存

2020-10-28 11:42:03 来源 : 一线码农聊技术

一:背景

1. 讲故事

前段时间写了几篇 C# 漫文,评论留言中有很多朋友多次提到 Span,周末抽空看了下,确实是一个非常的新结构,让我想到了当年的WCF,它统一了.NET下各种零散的分布式技术,包括:.NET Remoteing,WebService,NamedPipe,MSMQ,而这里的 Span 统一了 C# 进程中的三大块内存访问,包括:栈内存, 托管堆内存, 非托管堆内存,画个图如下:

接下来就和大家具体聊聊这三大块的内存统一访问。

二:进程中的三大块内存解析

1. 栈内存

大家应该知道方法内的局部变量是存放在栈上的,而且每一个线程默认会被分配 1M 的内存空间,我举个例子:

staticvoidMain(string[]args)

{

inti=10;

longj=20;

Listlist=newList();

}

上面 i,j 的值都是存于栈上,list的堆上内存地址也是存于栈上,为了看个究竟,可以用 windbg 验证一下:

0:000>!clrstack-l

OSThreadId:0x2708(0)

ChildSPIPCallSite

00000072E47CE55800007ff89cf7c184[InlinedCallFrame:00000072e47ce558]Interop+Kernel32.ReadFile(IntPtr,Byte*,Int32,Int32ByRef,IntPtr)

00000072E47CE55800007ff7c7c03fd8[InlinedCallFrame:00000072e47ce558]Interop+Kernel32.ReadFile(IntPtr,Byte*,Int32,Int32ByRef,IntPtr)

00000072E47CE52000007FF7C7C03FD8ILStubClass.IL_STUB_PInvoke(IntPtr,Byte*,Int32,Int32ByRef,IntPtr)

00000072E47CE7B000007FF8541E530DSystem.Console.ReadLine()

00000072E47CE7E000007FF7C7C0101EDataStruct.Program.Main(System.String[])[E:\net5\ConsoleApp2\ConsoleApp1\[email protected]]

LOCALS:

0x00000072E47CE82C=0x000000000000000a

0x00000072E47CE820=0x0000000000000014

0x00000072E47CE818=0x0000018015aeab10

通过 clrstack -l 查看线程栈,最后三行可以明显的看到 0a -> 10, 14 -> 20 , 0xxxxxxb10 => list堆地址,除了这些简单类型,还可以在栈上分配复杂类型,这里就要用到 stackalloc 关键词, 如下代码:

int*ptr=stackallocint[3]{10,11,12};

问题就在这里,指针类型虽然灵活,但是做任何事情都比较繁琐,比如说:

查找某一个数是否在 int[] 中

反转 int[]

剔除尾部的某一个数字(比如 12)

就拿第一个问题来说,操作指针的代码如下:

//指针接收

int*ptr=stackallocint[3]{10,11,12};

//包含判断

for(inti=0;i<3;i++)

{

if(*ptr++==11)

{

Console.WriteLine("11存在数组中");

}

}

后面的两个问题就更加复杂了,既然 Span 是统一访问,就应该用 Span 来接 stackalloc,代码如下:

Spanspan=stackallocint[3]{10,11,12};

//1.是否包含

varhasNum=span.Contains(11);

//2.反转

span.Reverse();

//3.剔除尾部

span.Trim(12);

这就很了,你既不需要接触指针,又能完成指针的大部分操作,而且还特别便捷,佩服,最后来验证一下 int[] 是否真的在 线程栈 上。

0:000>!clrstack-l

000000ED7737E4B000007FF7C4EA16ADDataStruct.Program.Main(System.String[])[E:\net5\ConsoleApp2\ConsoleApp1\[email protected]]

LOCALS:

0x000000ED7737E570=0x000000ed7737e4d0

0x000000ED7737E56C=0x0000000000000001

0x000000ED7737E558=0x000000ed7737e4d0

0:000>dp0x000000ed7737e4d0

000000ed`7737e4d00000000b`0000000c00000000`0000000a

从 Locals 处的 0x000000ED7737E570 = 0x000000ed7737e4d0 可以看到 key / value 是非常相近的,说明在栈上无疑。

从最后一行 a,b,c 可看出对应的就是数组中的 10,11,12。

2. 非托管堆内存

说到非托管内存,让我想起了当年 C# 调用 C++ 的场景,代码到处充斥着类似下面的语句:

privateboolSendMessage(intmessageType,stringip,stringport,intlength,byte[]messageBytes)

{

boolresult=false;

if(windowHandle!=0)

{

varbytes=newbyte[Const.MaxLengthOfBuffer];

Array.Copy(messageBytes,bytes,messageBytes.Length);

intsizeOfType=Marshal.SizeOf(typeof(StClientData));

StClientDatastData=newStClientData

{

Ip=GlobalConvert.IpAddressToUInt32(IPAddress.Parse(ip)),

Port=Convert.ToInt16(port),

Length=Convert.ToUInt32(length),

Buffer=bytes

};

intsizeOfStData=Marshal.SizeOf(stData);

IntPtrpointer=Marshal.AllocHGlobal(sizeOfStData);

Marshal.StructureToPtr(stData,pointer,true);

CopyDatacopyData=newCopyData

{

DwData=(IntPtr)messageType,

CbData=Marshal.SizeOf(sizeOfType),

LpData=pointer

};

SendMessage(windowHandle,WmCopydata,0,refcopyData);

Marshal.FreeHGlobal(pointer);

stringdata=GlobalConvert.ByteArrayToHexString(messageBytes);

CommunicationManager.Instance.SendDebugInfo(newDataSendEventArgs(){Data=data});

result=true;

}

returnresult;

}

上面代码中的: IntPtr pointer = Marshal.AllocHGlobal(sizeOfStData); 和 Marshal.FreeHGlobal(pointer) 就用到了非托管内存,从现在开始你就可以用 Span 来接 Marshal.AllocHGlobal 分配的非托管内存啦!,如下代码所示:

classProgram

{

staticunsafevoidMain(string[]args)

{

varptr=Marshal.AllocHGlobal(3);

//将ptr转换为span

varspan=newSpan((byte*)ptr,3){[0]=10,[1]=11,[2]=12};

//然后在span中可以进行各种操作了。。。

Marshal.FreeHGlobal(ptr);

}

}

这里我也用 windbg 给大家看一下 未托管内存 在内存中是个什么样子。

0:000>!clrstack-l

OSThreadId:0x3b10(0)

ChildSPIPCallSite

000000A51777E75800007ff89cf7c184[InlinedCallFrame:000000a51777e758]Interop+Kernel32.ReadFile(IntPtr,Byte*,Int32,Int32ByRef,IntPtr)

000000A51777E75800007ff7c4654dd8[InlinedCallFrame:000000a51777e758]Interop+Kernel32.ReadFile(IntPtr,Byte*,Int32,Int32ByRef,IntPtr)

000000A51777E72000007FF7C4654DD8ILStubClass.IL_STUB_PInvoke(IntPtr,Byte*,Int32,Int32ByRef,IntPtr)

000000A51777E9E000007FF7C46511D0DataStruct.Program.Main(System.String[])[E:\net5\ConsoleApp2\ConsoleApp1\[email protected]]

LOCALS:

0x000000A51777EA58=0x0000027490144760

0x000000A51777EA48=0x0000027490144760

0x000000A51777EA38=0x0000027490144760

0:000>dp0x0000027490144760

00000274`90144760abababab`ab0c0b0aabababab`abababab

最后一行的 0c0b0a 这就是低位到高位的 10,11,12 三个数,接下来从 Locals 处 0x000000A51777EA58 = 0x0000027490144760 可以看出,这个key,value 相隔十万八千里,说明肯定不在栈内存中,继续用 windbg 鉴别一下 0x0000027490144760 是否是托管堆上,可以用 !eeheap -gc 查看托管堆地址范围,如下代码:

0:000>!eeheap-gc

NumberofGCHeaps:1

generation0startsat0x00000274901B1030

generation1startsat0x00000274901B1018

generation2startsat0x00000274901B1000

ephemeralsegmentallocationcontext:none

segmentbeginallocatedsize

00000274901B000000000274901B100000000274901C53700x14370(82800)

Largeobjectheapstartsat0x00000274A01B1000

segmentbeginallocatedsize

00000274A01B000000000274A01B100000000274A01B54800x4480(17536)

TotalSize:Size:0x187f0(100336)bytes.

------------------------------

GCHeapSize:Size:0x187f0(100336)bytes.

从上面信息可以看到,0x0000027490144760 明显不在:3代堆:00000274901B1000 ~ 00000274901C5370 和 大对象堆:00000274A01B1000 ~ 00000274A01B5480 区间范围内。

3. 托管堆内存

用 Span 统一托管内存访问那是相当简单了,如下代码所示:

Spanspan = new byte[3] { 10, 11, 12 };

同样,你有了Span,你就可以使用 Span 自带的各种方法,这里就不多介绍了,大家有兴趣可以实操一下。

三:总结

总的来说,这一篇主要是从思想上带大家一起认识 Span,以及如何用 Span 对接 三大区域内存,关于 Span 的好处以及源码解析,后面上专门的文章吧!

相关文章

最近更新