淺聊一下C#中內(nèi)存映射文件的玩法
一:背景
1. 講故事
前段時(shí)間訓(xùn)練營(yíng)里有朋友問(wèn) 內(nèi)存映射文件 是怎么玩的?說(shuō)實(shí)話這東西理論我相信很多朋友都知道,就是將文件映射到進(jìn)程的虛擬地址,說(shuō)起來(lái)很容易,那如何讓大家眼見(jiàn)為實(shí)呢?可能會(huì)難倒很多人,所以這篇我以自己的認(rèn)知嘗試讓大家眼見(jiàn)為實(shí)。
二:如何眼見(jiàn)為實(shí)
1. 我想象的文件映射
在任何討論之前,內(nèi)存文件映射大概像下面這樣,多個(gè)進(jìn)程可以完全View一個(gè)文件,也可以 View 文件的一部分到進(jìn)程的虛擬地址中,畫個(gè)圖大概像下面這樣。

但仔細(xì)一想,這里還有很多的小細(xì)節(jié),比如:
疑問(wèn)1:到底是映射文件還是映射磁盤的物理地址 ?
疑問(wèn)2:既然是后備存儲(chǔ),那是不是每次修改虛擬地址都要刷硬盤 ?
疑問(wèn)3:內(nèi)存頁(yè)是4k為一個(gè)單位,文件大小不是 4k 整數(shù)倍怎么辦 ?
這三個(gè)疑問(wèn)我相信很多朋友或多或少都會(huì)遇到,這里我簡(jiǎn)單解答一下,后面再用 windbg 驗(yàn)證。
- 嚴(yán)格來(lái)說(shuō)是
硬盤物理地址。 - 文件所處的硬盤地址為后備存儲(chǔ)這個(gè)不假,但這里有個(gè)小細(xì)節(jié),對(duì)虛擬地址的讀寫涉及到
內(nèi)存頁(yè)概念,如果訪問(wèn)的虛擬地址所在的物理地址不在物理內(nèi)存中,就會(huì)引發(fā)缺頁(yè)中斷,操作系統(tǒng)會(huì)將 磁盤上的 4k 頁(yè)粒度灌入到物理內(nèi)存中,同樣的道理,如果修改了虛擬地址,那么物理內(nèi)存頁(yè)就是臟數(shù)據(jù),會(huì)在后續(xù)的某個(gè)時(shí)刻刷新到硬盤上,產(chǎn)生磁盤 IO。
總的來(lái)說(shuō):從磁盤到物理內(nèi)存(內(nèi)存條) 之間的內(nèi)存頁(yè)的換入換出都是一種按需的 懶加載懶寫入行為,稍后我們用 windbg 驗(yàn)證下。
內(nèi)存的管理采用的是內(nèi)存頁(yè)的方式,如果 View 大于 文件Size,那么文件會(huì)擴(kuò)容到 4k 對(duì)齊,這樣方便對(duì)文件追加寫入。
綜合上面的三點(diǎn)信息,圖就可以畫的再詳細(xì)一點(diǎn)了,比如下面這樣:

熟悉內(nèi)存管理的朋友應(yīng)該知道,我們程序的 exe 和 dll 就是用 內(nèi)存映射文件 的方式加載到虛擬地址中的,所以就拿它開(kāi)刀吧。
2. 一段測(cè)試代碼
為了方便演示,上一段簡(jiǎn)單的的測(cè)試代碼,觀察 ConsoleApp1.exe 的映射方式。
static void Main(string[] args)
{
Console.WriteLine($"當(dāng)前時(shí)間:{DateTime.Now}, 程序啟動(dòng)!");
Console.ReadLine();
}接下來(lái)用 windbg 啟動(dòng) ConsoleApp1.exe 兩次,結(jié)合詳細(xì)分解圖,我們觀察下這兩個(gè)進(jìn)程的虛擬地址所映射的內(nèi)存條物理地址是否一致?
1.實(shí)例1
ModLoad: 00007ff6`bfe00000 00007ff6`bfe2a000 apphost.exe
ModLoad: 00007ff9`b1450000 00007ff9`b1648000 ntdll.dll
...
0:008> lmvm apphost
Browse full module list
start end module name
00007ff6`bfe00000 00007ff6`bfe2a000 apphost C (private pdb symbols) c:\mysymbols\apphost.pdb\1643A9EB126F4FE184548E9CC1B740B71\apphost.pdb
Loaded symbol image file: D:\net7\ConsoleApplication1\ConsoleApp1\bin\Debug\net6.0\ConsoleApp1.exe
Image path: apphost.exe
Image name: apphost.exe
...
0:008> ~
0 Id: 232c.4abc Suspend: 1 Teb: 0000000e`7b1a5000 Unfrozen2.實(shí)例2
ModLoad: 00007ff6`bfe00000 00007ff6`bfe2a000 apphost.exe ModLoad: 00007ff9`b1450000 00007ff9`b1648000 ntdll.dll ... 0:008> ~ 0 Id: 60e8.3e3c Suspend: 1 Teb: 000000da`ab498000 Unfrozen 1 Id: 60e8.53b0 Suspend: 1 Teb: 000000da`ab49a000 Unfrozen
這里要提醒一下的是在 Windows 平臺(tái)上 ConsoleApp1.exe 已經(jīng)成了一個(gè)引導(dǎo)程序,通過(guò) lmvm 可以看到它其實(shí)是 apphost.exe。
兩個(gè)實(shí)例都開(kāi)起來(lái)后,可以看到 apphost.exe 在各自進(jìn)程的虛擬地址都一樣,那他們的物理地址是否也一樣呢? 要尋找答案,接下來(lái)我們到 Windows 內(nèi)核態(tài)去挖一挖。
lkd> !process 0 0 ConsoleApp1.exe
PROCESS ffff838bd84c9080
SessionId: 8 Cid: 232c Peb: e7b1a4000 ParentCid: 0b14
FreezeCount 2
DirBase: 3468cf000 ObjectTable: ffff938feae02900 HandleCount: 172.
Image: ConsoleApp1.exe
PROCESS ffff838bef157080
SessionId: 8 Cid: 60e8 Peb: daab497000 ParentCid: 4804
FreezeCount 2
DirBase: 3552f3000 ObjectTable: ffff938fe8f7ec40 HandleCount: 166.
Image: ConsoleApp1.exe從卦中看,Cid: 232c 是我們的實(shí)例1, Cid: 60e8 是我們的實(shí)例2,接下來(lái)用 windbg 提供的 !vtop 命令觀察 apphost.exe 的首地址對(duì)應(yīng)的物理地址。
// ---- 實(shí)例1 ----- lkd> !vtop 3468cf000 00007ff6bfe00000 Amd64VtoP: Virt 00007ff6bfe00000, pagedir 00000003468cf000 Amd64VtoP: PML4E 00000003468cf7f8 Amd64VtoP: PDPE 00000001138dbed0 Amd64VtoP: PDE 00000002153dcff8 Amd64VtoP: PTE 000000024dadd000 Amd64VtoP: Mapped phys 00000002271c2000 Virtual address 7ff6bfe00000 translates to physical address 2271c2000. //---- 實(shí)例2 ----- lkd> !vtop 3552f3000 00007ff6bfe00000 Amd64VtoP: Virt 00007ff6bfe00000, pagedir 00000003552f3000 Amd64VtoP: PML4E 00000003552f37f8 Amd64VtoP: PDPE 00000002db7ffed0 Amd64VtoP: PDE 0000000208100ff8 Amd64VtoP: PTE 000000033de01000 Amd64VtoP: Mapped phys 00000002271c2000 Virtual address 7ff6bfe00000 translates to physical address 2271c2000.
從卦中看,物理地址上有一段 This program cannot be run in DOS mode,這不就是經(jīng)典的 PE 文件哈,如果不相信可以用 WinHex 打開(kāi) ConsoleApp1.exe 即可,截圖如下:

最后就是內(nèi)核中的 內(nèi)存管理器 會(huì)將 物理地址 與 磁盤地址 進(jìn)行打通,實(shí)現(xiàn)懶加載和懶寫入。
3. 如何自定義實(shí)現(xiàn)
Image 雖然是一個(gè)快捷的觀察內(nèi)存文件映射方式,那如果自己能實(shí)現(xiàn)一個(gè)就更有意思了,比如下面對(duì) 1.txt 進(jìn)行文件映射,在 C# 中有一個(gè)快捷類 MemoryMappedFile 實(shí)現(xiàn)了 win32api 的封裝,參考代碼如下:
internal class Program
{
static void Main(string[] args)
{
int capaticy = 1024; //1k
using (var mmf = MemoryMappedFile.CreateFromFile(@"C:\1.txt", FileMode.OpenOrCreate,
"testmapfile",
capaticy,
MemoryMappedFileAccess.ReadWrite))
{
var viewAccessor = mmf.CreateViewAccessor(0, capaticy);
while (true)
{
Console.WriteLine("請(qǐng)輸入你要寫入的內(nèi)容: ");
string input = Console.ReadLine();
viewAccessor.WriteArray(0, input.ToArray(), 0, input.Length);
}
}
}
}接下來(lái)用 windbg 附加一下,觀察 1.txt 是不是被 MappedFile 上了,同時(shí)做的修改有沒(méi)有更新到物理磁盤上。
0:006> !address BaseAddr EndAddr+1 RgnSize Type State Protect Usage ----------------------------------------------------------------------------------------------- ... + 31a0000 31a1000 1000 MEM_MAPPED MEM_COMMIT PAGE_READWRITE MappedFile "\Device\HarddiskVolume3\1.txt" ... 0:006> du 31a0000 031a0000 "helloworld!"

從卦中可以看到,雖然 1.txt 最大的 View 區(qū)間是 1k,但提交的內(nèi)存頁(yè)還是按照最小粒度 4k 給的。
到此這篇關(guān)于淺聊一下C#中內(nèi)存映射文件的玩法的文章就介紹到這了,更多相關(guān)C#內(nèi)存映射文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#在PDF中繪制不同風(fēng)格類型的文本方法實(shí)例
這篇文章主要給大家介紹了關(guān)于C#在PDF中繪制不同風(fēng)格類型的文本的相關(guān)資料,文中通過(guò)圖文以及示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
C#使用時(shí)序數(shù)據(jù)庫(kù)InfluxDB的教程詳解
InfluxDB是一個(gè)開(kāi)源的時(shí)序數(shù)據(jù)庫(kù),可以自動(dòng)處理時(shí)間序列數(shù)據(jù),這篇文章主要為大家詳細(xì)介紹了C#如何使用InfluxDB,感興趣的小伙伴可以跟隨小編一起了解下2023-11-11
C# 如何實(shí)現(xiàn)一個(gè)帶通知的List<T>
這篇文章主要介紹了C# 如何實(shí)現(xiàn)一個(gè)帶通知的List<T>,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-02-02
C#實(shí)現(xiàn)簡(jiǎn)易計(jì)算器功能(2)(窗體應(yīng)用)
這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)簡(jiǎn)易計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01

