Chiều sâu bộ nhớ Windows (phần 3) — Đối tượng section và copy-on-write: DLL và ánh xạ tệp thực sự là gì
· Go Komura · Windows, Quản lý bộ nhớ, Bộ nhớ dùng chung, Ánh xạ tệp, Copy-on-Write, DLL, Cache Manager
Trong bài trước, “Chiều sâu bộ nhớ Windows (phần 2) — Đời sống của một trang vật lý”, chúng ta theo dõi cách một trang vật lý rời Working Set di chuyển qua Modified, Standby, Free và Zeroed. Standby đó cũng giữ các trang từ DLL, EXE, tệp được ánh xạ và cache tệp.
Một câu hỏi xuất hiện. Khi 100 tiến trình dùng cùng kernel32.dll, Windows có đặt 100 bộ trang mã trong RAM không? Khi hai tiến trình ánh xạ cùng một tệp vào bộ nhớ, phía kia có dùng được trang mà một bên đã đọc không?
Câu trả lời là ánh xạ nhiều địa chỉ ảo lên cùng một trang vật lý. Trung tâm biểu diễn đơn vị chia sẻ đó là đối tượng section, và cơ chế chỉ tách chia sẻ lúc ghi là copy-on-write (CoW).
Bài này theo dõi cách EXE và DLL, tệp dữ liệu, bộ nhớ dùng chung dựa trên page file, và cache tệp bám vào cùng một luồng tệp, cũng như chỗ các đường xử lý tách ra. Cách đọc các con số lấy bài mở đầu “What Does Windows’ "Memory Usage" Actually Mean?” làm tiền đề.
“Chiều sâu bộ nhớ Windows” — cả 3 phần
- Phần 1: Địa chỉ ảo và page fault
Theo dõi khoảnh khắc một trang ảo đã Commit nhận RAM vật lý. - Phần 2: Đời sống của một trang vật lý
Theo dõi chuyển trạng thái của trang rời Working Set. - Phần 3 (bài này): Đối tượng section và copy-on-write
Theo dõi cơ chế DLL, ánh xạ tệp và bộ nhớ dùng chung chia sẻ trang vật lý.
Câu hỏi phần 3 trả lời chỉ có một.
Vì sao nhiều tiến trình có thể dùng cùng một DLL hay tệp như một bộ trang vật lý?
Độc giả dự kiến là nhà phát triển và vận hành muốn hiểu chia sẻ DLL, CreateFileMapping, MapViewOfFile, bộ nhớ dùng chung, CoW và quan hệ với cache tệp không chỉ như cách dùng API mà từ cấu trúc nội bộ. Môi trường tiên quyết là Windows 10/11 hoặc Windows Server hiện hành, và nền tảng cần có là địa chỉ ảo, page fault và Working Set. Độ khó trung cấp; chúng ta cũng dùng thuật ngữ nội bộ như Control Area và Prototype PTE, nhưng quan sát có thể tái hiện bằng VMMap, Process Explorer và QueryWorkingSetEx.
1. Kết luận trước
Trước hết, đây là bức tranh tổng thể.
- Đối tượng section biểu diễn một khoảng bộ nhớ có thể chia sẻ.
Mỗi tiến trình ánh xạ một phần section đó vào không gian ảo của mình như một “view”.1 - View của cùng một section không cần cùng địa chỉ ảo.
0x000001...của tiến trình A và0x000002...của tiến trình B có thể trỏ cùng offset section và cùng trang vật lý.2 - Có section dựa trên tệp và section dựa trên page file.
Loại trước dùng tệp thật; loại sau dùng cho bộ nhớ dùng chung không có tệp tường minh, và tương tự.2 - Nạp EXE/DLL là section image; ánh xạ tệp thông thường là section dữ liệu.
VớiSEC_IMAGE, thuộc tính section bên trong PE quyết định bảo vệ trang.3 - Khi đọc, cùng một trang vật lý có thể được chia sẻ.
Khi một bên ghi vào trang CoW, chỉ trang đó được sao chép và PTE của tiến trình ghi bị hoán.4 - Dù trên cùng luồng tệp, đường cache, dữ liệu và image vẫn tách.
I/O cache của Cache Manager dùngSharedCacheMap, ánh xạ dữ liệu dùngDataSectionObject, EXE/DLL dùngImageSectionObject. Cả ba bám cùng luồng tệp quaSECTION_OBJECT_POINTERS, nhưng fault image không đi qua Cache Manager.5 - Riêng Private Bytes không đủ để xác nhận đã xảy ra CoW.
FILE_MAP_COPYtính Commit cho cả view ngay lúc ánh xạ, phòng trường hợp mọi trang sau này bị tư nhân hóa. Để kiểm tra từng trang, dùng bit Shared củaQueryWorkingSetEx.67
Một câu: cái được chia sẻ không phải địa chỉ ảo, mà là nội dung trong section và trang vật lý tương ứng lúc đó.
2. Đối tượng section và view
Theo định nghĩa của Microsoft, đối tượng section biểu diễn một vùng bộ nhớ có thể chia sẻ và cũng là cơ chế ánh xạ tệp vào không gian địa chỉ tiến trình.1
Mẹo để hiểu là tách section khỏi view mà mỗi tiến trình thấy.
| Khái niệm | Vai trò |
|---|---|
| Đối tượng section | Biểu diễn nội dung cần chia sẻ, kích thước, backing store và trần bảo vệ |
| View | Hiện một phần section như một khoảng địa chỉ ảo trong một tiến trình |
| PTE | Gắn mỗi trang ảo trong view với trang vật lý hiện tại hoặc trạng thái chưa hiện thực |
| PFN | Biểu diễn một trang vật lý thực sự tồn tại trong RAM |
Trong Win32, CreateFileMapping trả về handle tới đối tượng ánh xạ tệp, và MapViewOfFile tạo view trong không gian ảo tiến trình.3
HANDLE mapping = CreateFileMappingW(
file,
nullptr,
PAGE_READONLY,
0,
0,
nullptr);
void* view = MapViewOfFile(
mapping,
FILE_MAP_READ,
0,
0,
0);
Chỉ gọi CreateFileMapping chưa cho bạn địa chỉ tiến trình có thể đọc. Hơn nữa, tạo view không đưa ngay mọi trang vào RAM. Từ trang đầu tiên bị chạm, page fault đọc nội dung tệp và gắn trang vật lý vào PTE.8 Nói cách khác, demand paging ở phần 1 cũng áp dụng cho view section.
2.1. Cùng section, khác địa chỉ ảo
Dù tiến trình A và tiến trình B ánh xạ cùng offset của cùng section, địa chỉ bắt đầu của view vẫn có thể khác.
flowchart LR
accTitle: Ánh xạ các địa chỉ ảo khác nhau lên cùng một trang vật lý
accDescr: Tiến trình A và tiến trình B mỗi bên có view ở địa chỉ ảo khác nhau, nhưng đều tới cùng trang vật lý PFN X qua cùng offset section
viewA["Tiến trình A: 0x000001A00000 + 0x3000"] --> offset["Offset section 0x3000"]
viewB["Tiến trình B: 0x000002700000 + 0x3000"] --> offset
offset --> pfnX["Cùng trang vật lý PFN X"]
Hình 1: Cái được chia sẻ là nội dung trong section và trang vật lý, không phải địa chỉ ảo.
Đó là lý do không được lưu con trỏ thô trong bộ nhớ dùng chung. Giá trị con trỏ của tiến trình A có thể là địa chỉ không liên quan ở tiến trình B.
Trong cấu trúc dùng chung, hãy dùng offset từ đầu view, số nguyên độ rộng cố định, cùng phiên bản và căn chỉnh tường minh. Tài liệu MapViewOfFileEx của Microsoft cũng khuyên lưu offset từ gốc thay vì con trỏ, vì không đảm bảo cùng địa chỉ sẽ còn dùng được sau này.6
3. Dựa trên tệp và dựa trên page file
Section tách thành hai loại lớn tùy nơi có thể khôi phục nội dung.
flowchart TB
accTitle: File-backed và pagefile-backed tách nhau thế nào
accDescr: Truyền tệp thật cho CreateFileMapping tạo section dựa trên tệp, và trang sạch có thể đọc lại từ tệp gốc. Truyền INVALID_HANDLE_VALUE tạo section dựa trên page file; page file chống nội dung, nội dung biến mất khi hủy đối tượng
create["CreateFileMapping"] -->|Truyền handle tệp thật| fileBacked["Section dựa trên tệp"]
create -->|Truyền INVALID_HANDLE_VALUE| pfBacked["Section dựa trên page file"]
fileBacked --> restore1["Trang sạch có thể đọc lại từ tệp gốc"]
pfBacked --> restore2["Page file chống nội dung, biến mất khi hủy"]
Hình 2: Khác biệt backing store quyết định nội dung khôi phục từ đâu và sống bao lâu.
3.1. Section dựa trên tệp
Truyền tệp thật cho CreateFileMapping tạo section dựa trên tệp.
- View chỉ đọc đọc các trang cần thiết từ tệp.
- Thay đổi trên view đọc/ghi được coi là dữ liệu của tệp đó.
- Thay đổi trên view CoW không ghi vào tệp gốc; chúng trở thành trang riêng.
Nếu trang dựa trên tệp sạch, trang vật lý có thể bị bỏ và đọc lại từ tệp gốc. Tính chất đó chống đỡ hiệu quả Standby và cache tệp ở phần 2.
3.2. Section dựa trên page file
Truyền INVALID_HANDLE_VALUE làm hFile của CreateFileMapping và chỉ định kích thước tạo section dựa trên page file.
HANDLE mapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
0,
64 * 1024,
L"Local\\KomuraMemoryDemo");
Đây là section không có tệp dữ liệu tường minh, dựa trên page file. Nội dung ban đầu là không, và nhiều tiến trình có thể mở cùng đối tượng qua tên, kế thừa handle, DuplicateHandle và tương tự.93 Thay đổi hiện với các tiến trình ánh xạ cùng trang dùng chung. Ngược lại, khi đối tượng section bị hủy nội dung không còn, nên không hợp để để lại tệp bền vững.2
Lưu ý: bộ nhớ dùng chung không tự kèm loại trừ lẫn nhau. Mutex, semaphore, event, giao thức lock-free hay tương tự bạn thiết kế riêng.9
4. Ánh xạ image và ánh xạ dữ liệu
Khi nói “EXE hay DLL cũng là ánh xạ tệp”, vẫn phải giữ khác biệt với tệp dữ liệu thông thường.
| Mục | Ánh xạ image | Ánh xạ dữ liệu |
|---|---|---|
| Công dụng chính | Nạp EXE hoặc DLL | Tệp thông thường, dữ liệu dùng chung |
| Thuộc tính tạo | SEC_IMAGE |
PAGE_READONLY, PAGE_READWRITE và tương tự |
| Bảo vệ trang | Thuộc tính trong image PE quyết định | Đặc tả ánh xạ và view quyết định |
| Ghi | Có thể tư nhân hóa qua section ghi được hoặc CoW | Có thể chọn ghi dùng chung hoặc CoW |
Type VirtualQuery |
MEM_IMAGE |
MEM_MAPPED |
Với SEC_IMAGE, thuộc tính section của chính image thực thi quyết định bảo vệ trang của view nhiều hơn giá trị bảo vệ thông thường truyền cho CreateFileMapping.3
Nhờ cơ chế này, các trang chưa sửa như mã có thể chia cùng trang vật lý giữa nhiều tiến trình, và chỉ những trang cần thay đổi riêng tiến trình mới rẽ qua CoW. Tuy vậy, không phải mọi trang DLL đều chắc chắn được chia — vì relocation ASLR, sửa loader, hotpatch, thuộc tính section PE thực tế, v.v.
Thiết kế quan trọng là chia trước các trang có thể chia, và chỉ sao chép muộn những trang cần thay đổi.
5. Copy-on-write từ đầu đến cuối
Hãy theo dõi từ trạng thái hai tiến trình đang đọc cùng trang CoW đến khi tiến trình A ghi một byte.
5.1. Trước khi ghi
Trước khi có ghi, PTE của cả hai tiến trình về khái niệm đều tới cùng trang dùng chung, và đọc thành công như vậy.
flowchart LR
accTitle: Trạng thái dùng chung trước copy-on-write
accDescr: Trước khi có ghi, PTE tiến trình A và PTE tiến trình B đều tới cùng trang dùng chung PFN X, và đọc thành công như vậy
pteA["PTE tiến trình A"] --> pfnX["PFN X dùng chung(đọc / copy-on-write)"]
pteB["PTE tiến trình B"] --> pfnX
Hình 3: Trước khi ghi, PTE của cả hai tiến trình trỏ cùng trang vật lý.
5.2. Protection fault khi ghi
Trang CoW ngay từ đầu không phải trang dùng chung ghi được thông thường. Khi tiến trình A cố ghi, CPU phát protection fault. Bộ quản lý bộ nhớ nhận quyền điều khiển đánh giá đây không phải ghi trái phép mà là ghi vào thuộc tính CoW.
5.3. Tạo trang vật lý mới
Với đánh giá đó, Windows làm như sau.
- Lấy một trang vật lý cho tiến trình A.
- Sao nội dung PFN X sang trang mới PFN Y.
- Hoán PTE của tiến trình A sang PFN Y.
- Đổi bảo vệ của tiến trình A thành đọc/ghi thông thường.
- Thực thi lại lệnh ghi đã thất bại.
flowchart LR
accTitle: Trạng thái tách sau copy-on-write
accDescr: Sau khi tiến trình A ghi, chỉ PTE tiến trình A được hoán sang trang riêng PFN Y nhận bản sao nội dung, trong khi PTE tiến trình B vẫn trỏ trang dùng chung gốc PFN X
pteA2["PTE tiến trình A"] --> pfnY["PFN Y riêng(R/W, sau khi ghi)"]
pteB2["PTE tiến trình B"] --> pfnX2["PFN X dùng chung(gốc)"]
pfnX2 -.->|Sao lúc ghi| pfnY
Hình 4: Chỉ PTE của tiến trình ghi được hoán sang trang riêng mới; phía kia tiếp tục đọc nội dung gốc.
Tiến trình B tiếp tục đọc nội dung gốc và không thấy thay đổi của tiến trình A. Đó là Copy-on-Write. Chia sẻ DLL và FILE_MAP_COPY cùng nguyên lý: đừng sao cho đến khi bạn ghi.46
5.4. Khác biệt với FILE_MAP_WRITE
Trang ghi qua ghi dùng chung FILE_MAP_WRITE được thiết kế để thay đổi của một bên cũng hiện từ view khác dùng cùng ánh xạ tệp. Với FILE_MAP_COPY, ngược lại, chỉ các trang bị ghi trở thành riêng tiến trình; thay đổi không ghi lại tệp gốc và mất khi view bị unmap.6
Bạn muốn “truyền cập nhật qua bộ nhớ dùng chung”, hay “mỗi tiến trình tự thay đổi riêng từ dữ liệu ban đầu chung”? Lựa chọn đúng đảo ngược tùy mục đích.
6. Sau CoW vẫn là MEM_MAPPED / MEM_IMAGE
Trang sau CoW đã trở thành Private về vật lý. Bạn có thể nghĩ Type của VirtualQuery cũng đổi thành MEM_PRIVATE, nhưng thực tế view dữ liệu vẫn MEM_MAPPED và image thực thi vẫn MEM_IMAGE. VirtualQuery báo vùng đó đến từ kiểu cấp phát ban đầu nào.7
Để xem từng trang CoW đã xảy ra chưa, dùng quy trình sau.
- Truy cập trang đích và làm nó resident.
- Lấy thông tin Working Set của trang bằng
QueryWorkingSetEx. - Xem bit
Shared. - Nếu
Shared == 0, trang resident đó là Private.
Khi xác nhận trên VMMap, đừng chỉ nhìn Type của vùng mà còn nhìn phân tách Private/Shareable của Working Set.
6.1. Private Bytes có thể không tăng
Với FILE_MAP_COPY, tiến trình sau này có thể ghi mọi trang trong view. Vì vậy Windows lấy khoản Commit tương đương cả view ngay lúc ánh xạ.6 Kết quả là ghi trang đầu không nhất thiết tăng Private Bytes thêm 4KiB ngay lúc đó.
Các chỉ số nên ưu tiên khi quan sát CoW như sau.
- Bit Shared của
QueryWorkingSetEx - Private WS / Shareable WS của VMMap
- Thông tin trang vật lý của RAMMap
- Private Bytes như thông tin bổ trợ
Nếu bạn coi “Private Bytes có tăng lúc ghi không” là bài kiểm tra đạt/không đạt, bạn sẽ bỏ sót CoW đang hoạt động đúng.
7. Điểm tiếp xúc với Cache Manager — tách ba đường
Nói “nạp EXE/DLL, cache tệp và bộ nhớ dùng chung đều là section” cho bức tranh tổng thể, nhưng không được dồn triển khai thành một đối tượng.
Luồng tệp có SECTION_OBJECT_POINTERS, được bộ quản lý bộ nhớ và Cache Manager dùng.
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
DataSectionObject: trạng thái section cho tệp dữ liệuSharedCacheMap: view cache mà Cache Manager theo dõiImageSectionObject: trạng thái section cho image thực thi
Tài liệu Microsoft giải thích cấu trúc này gắn đối tượng tệp với các section của luồng tệp và theo dõi nội dung trong bộ nhớ cùng thông tin cache.5
Ở đây chuỗi I/O và chuỗi bộ nhớ gặp nhau. Dù vậy hãy hiểu ba đường tách nhau.
ReadFile/WriteFilecó cache dùngSharedCacheMapvà view cache của Cache Manager.- Fault ánh xạ trên tệp dữ liệu do bộ quản lý bộ nhớ xử lý phía
DataSectionObject. Nó phối hợp với I/O cache trên cùng luồng tệp để giữ nội dung nhất quán. - Fault image trên EXE/DLL do bộ quản lý bộ nhớ xử lý bằng
ImageSectionObjectvà I/O phân trang. Đó không phải đường đi quaSharedCacheMapcủa Cache Manager.
flowchart TB
accTitle: Ba đường bám cùng một luồng tệp
accDescr: ReadFile/WriteFile có cache dùng SharedCacheMap, fault ánh xạ dữ liệu dùng DataSectionObject, fault image EXE/DLL dùng ImageSectionObject; cả ba bám cùng luồng tệp qua SECTION_OBJECT_POINTERS
cached["ReadFile / WriteFile có cache"] --> scm["SharedCacheMap"]
dataFault["Fault ánh xạ dữ liệu"] --> dso["DataSectionObject"]
imageFault["Fault image EXE/DLL"] --> iso["ImageSectionObject"]
scm --> stream["Cùng luồng tệp(SECTION_OBJECT_POINTERS)"]
dso --> stream
iso --> stream
Hình 5: Ba đường được xử lý riêng, nhưng bám trên cùng một luồng tệp.
Điểm chung của ba đường không phải “mọi thứ vào Cache Manager”, mà là cùng một luồng tệp gắn các trạng thái tách — cache, section dữ liệu và section image — qua SECTION_OBJECT_POINTERS.5 Đọc ghi cache, Lazy Writer và quan hệ Cc với Mm được trình bày trong “The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?”.
Lưu ý khi trộn view ánh xạ bộ nhớ với ReadFile/WriteFile, bạn không được đảm bảo luôn thấy nội dung cùng một khoảnh khắc. Thiết kế cần gồm đồng bộ, flush và chế độ chia sẻ tệp.36
8. Vòng đời đối tượng và view
Chỉ đóng handle CreateFileMapping không hủy view đã tồn tại. View giữ tham chiếu nội bộ tới section, và chỉ sau khi mọi view đã UnmapViewOfFile và mọi handle đã CloseHandle đối tượng mới có thể bị hủy.3
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
Tách vòng đời này là nguyên nhân hiện tượng “tôi đã đóng tệp nhưng vẫn đang được dùng”. Ngay cả sau khi đóng handle tệp, nếu section image hoặc view dữ liệu vẫn tham chiếu luồng tệp, lần đóng cuối của tệp đến muộn hơn.
Quan hệ với cleanup/close phía I/O nằm trong “The Depths of Windows I/O (Part 1)”, và các bẫy triển khai trong “Shared Memory Pitfalls and Practical Best Practices”.
9. Tự nhìn lấy
9.1. Nhìn cùng DLL từ hai tiến trình
Trước hết, xác nhận chia sẻ DLL với các tiến trình sẵn có.
- Chạy Process Explorer với quyền quản trị.
- Khởi hai tiến trình
cmd.exe. - Chọn View > Lower Pane View > DLLs.
- Xác nhận đường dẫn và ánh xạ cùng DLL ở cả hai tiến trình.
- Mở từng
cmd.exetrong VMMap và so Images Working Set, Private và Shareable.
Thấy cùng DLL trong Process Explorer là bằng chứng cả hai đã ánh xạ cùng image. Tuy nhiên chỉ thế chưa chứng minh PFN từng trang khớp. Kết hợp phân tách Shareable của VMMap, RAMMap và QueryWorkingSetEx để xác nhận chia sẻ từng trang. Process Explorer và VMMap do Sysinternals cung cấp.1011
9.2. Quan sát FILE_MAP_COPY từ hai tiến trình
Chương trình sau ánh xạ cùng tệp thành view CoW và hiện bit Shared của QueryWorkingSetEx. Đối tượng ánh xạ tệp được tạo với PAGE_READONLY, nhưng bảo vệ đó tương thích với view FILE_MAP_COPY, và lần ghi đầu phía view gây CoW.3
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cwchar>
#pragma comment(lib, "Psapi.lib")
void PrintPage(const char* stage, void* address)
{
MEMORY_BASIC_INFORMATION mbi{};
if (!VirtualQuery(address, &mbi, sizeof(mbi))) {
std::printf("VirtualQuery failed: %lu\n", GetLastError());
return;
}
for (int attempt = 0; attempt < 3; ++attempt) {
// The page may have been trimmed while the user was waiting.
// Touch it immediately before querying the working-set attributes.
volatile unsigned char resident =
*static_cast<volatile unsigned char*>(address);
(void)resident;
PSAPI_WORKING_SET_EX_INFORMATION ws{};
ws.VirtualAddress = address;
if (!QueryWorkingSetEx(GetCurrentProcess(), &ws, sizeof(ws))) {
std::printf("QueryWorkingSetEx failed: %lu\n", GetLastError());
return;
}
if (!ws.VirtualAttributes.Valid) {
Sleep(0);
continue;
}
std::printf(
"%s: Type=0x%lx Valid=1 Shared=%llu ShareCount=%llu\n",
stage,
static_cast<unsigned long>(mbi.Type),
static_cast<unsigned long long>(ws.VirtualAttributes.Shared),
static_cast<unsigned long long>(ws.VirtualAttributes.ShareCount));
return;
}
std::printf(
"%s: page is not resident; Shared/ShareCount were not interpreted\n",
stage);
}
int wmain(int argc, wchar_t** argv)
{
if (argc != 3) {
std::fwprintf(stderr, L"usage: cow_demo <file> <read|write>\n");
return 2;
}
HANDLE file = CreateFileW(
argv[1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return 3;
HANDLE mapping = CreateFileMappingW(
file, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!mapping) {
CloseHandle(file);
return 4;
}
auto* view = static_cast<unsigned char*>(
MapViewOfFile(mapping, FILE_MAP_COPY, 0, 0, 0));
if (!view) {
CloseHandle(mapping);
CloseHandle(file);
return 5;
}
volatile unsigned char value = view[0];
(void)value;
std::puts("Start the other process. When both are waiting, press Enter...");
(void)std::getchar();
PrintPage("before", view);
if (std::wcscmp(argv[2], L"write") == 0) {
std::puts("Press Enter to trigger copy-on-write...");
(void)std::getchar();
view[0] ^= 0x5a;
PrintPage("after write", view);
} else {
std::puts("After the writer changes its page, press Enter...");
(void)std::getchar();
PrintPage("reader after peer write", view);
}
std::puts("Press Enter to exit...");
(void)std::getchar();
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
}
Biên dịch và chuẩn bị.
cl /std:c++20 /EHsc /W4 cow_demo.cpp
$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))
Rồi mở cùng tệp từ hai console.
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write
Sau khi khởi cả hai, nhấn Enter trước ở phía đọc rồi phía ghi, và xác nhận Shared được đặt trong before ở cả hai. Nhấn Enter thêm một lần ở phía ghi và trang của tiến trình đó thành Shared 0. Rồi nhấn Enter ở phía đọc và bạn xác nhận phía đọc vẫn đọc trang gốc. Type của VirtualQuery vẫn MEM_MAPPED sau khi ghi.
Lưu ý PrintPage chạm lại trang đích ngay trước khi truy vấn, và nếu Valid == 0 nó không diễn giải Shared cùng ShareCount rồi thử lại tối đa ba lần. Nếu trang vẫn chưa resident, nó không đưa kết quả và báo điều đó. ShareCount có thể đổi theo thời điểm và áp lực bộ nhớ, nên hãy nhìn thay đổi bit Shared được xác nhận khi Valid == 1, không phải một giá trị cố định.
10. Năm cách đọc sai cần tránh trong thực tế
10.1. “Bộ nhớ dùng chung kết thúc ở cùng địa chỉ ảo”
Cái được chia sẻ là section và trang vật lý. Địa chỉ ảo của view có thể khác theo tiến trình, nên hãy lưu offset chứ không phải con trỏ thô.
10.2. “Cùng DLL thì mọi trang chắc chắn được chia”
Trang mã sạch dễ chia, trong khi relocation, section ghi được, CoW và trạng thái resident lúc đo cũng sinh trang Private.
10.3. “Sau CoW nó thành MEM_PRIVATE”
Type của VirtualQuery vẫn MEM_MAPPED hoặc MEM_IMAGE. Xác nhận trạng thái chia sẻ thực bằng QueryWorkingSetEx.7
10.4. “Nếu Private Bytes không tăng thì CoW không xảy ra”
FILE_MAP_COPY tính Commit cả view từ trước. Ưu tiên Private WS và bit Shared.6
10.5. “Nếu trang được chia thì không cần đồng bộ”
Nhìn cùng trang vật lý và cập nhật an toàn từ nhiều nhân CPU là hai bài toán khác. Hãy thiết kế tính nguyên tử, thứ tự bộ nhớ, loại trừ lẫn nhau, trạng thái trung gian khi crash, và tương thích phiên bản.
Ý tưởng theo dõi tham chiếu và vòng đời tách nhau cũng áp dụng cho vấn đề tiến trình còn lại sau interop COM Excel. Xem thêm “Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision”.
11. Tóm tắt
- Đối tượng section biểu diễn khoảng bộ nhớ có thể chia sẻ, và mỗi tiến trình ánh xạ nó vào không gian ảo của mình như một view.1
- Cùng offset của cùng section được ánh xạ từ các địa chỉ ảo khác nhau lên cùng trang vật lý.2
- Section dựa trên tệp chống tệp thật; section dựa trên page file chống bộ nhớ dùng chung có tên và tương tự.9
- EXE/DLL được coi là section image và tệp thông thường là section dữ liệu; bảo vệ và đích ghi lại khác nhau.3
- CoW chia trang vật lý khi đọc và, ở lần ghi đầu, chỉ sao trang đó rồi hoán PTE.4
- Sau CoW,
VirtualQueryvẫn trảMEM_MAPPED/MEM_IMAGE, nên bạn xác nhận bằng bit Shared củaQueryWorkingSetEx.7 - Với
FILE_MAP_COPY, Commit cả view được tính trước, nên không thể phán CoW chỉ từ Private Bytes.6 - I/O cache của Cache Manager, ánh xạ dữ liệu và ánh xạ image bám cùng luồng tệp như các đường tách dùng lần lượt
SharedCacheMap,DataSectionObjectvàImageSectionObject.5 - Bộ nhớ dùng chung chỉ an toàn khi bạn gồm vòng đời view và handle, đồng bộ, ACL và thiết kế offset.
Thế là đủ cả ba phần “Chiều sâu bộ nhớ Windows”. Bạn Reserve/Commit địa chỉ ảo, lấy trang vật lý qua page fault, chuyển trang từ Working Set sang danh sách trang, chia qua section, và chỉ tách những trang đã ghi bằng CoW — quản lý bộ nhớ Windows nối thành một dòng chảy này.
Bài viết liên quan
- Chiều sâu bộ nhớ Windows (phần 1) — Khoảnh khắc địa chỉ ảo thành RAM vật lý: page fault từ đầu đến cuối
- Chiều sâu bộ nhớ Windows (phần 2) — Đời sống của một trang vật lý: năm danh sách và sự thật về page file
- The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
- Shared Memory Pitfalls and Practical Best Practices
- Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision
- Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and “File in Use” from the State Right Now
Lĩnh vực tư vấn liên quan
KomuraSoft LLC đảm nhận điều tra lỗi bộ nhớ dùng chung, ánh xạ tệp, nạp DLL, khóa tệp, giao tiếp liên tiến trình và mức dùng bộ nhớ của ứng dụng Windows.
- Phát triển ứng dụng Windows
- Điều tra lỗi và phân tích nguyên nhân gốc rễ
- Di chuyển tài sản kế thừa
- Liên hệ với chúng tôi
Liên kết tham khảo
-
Microsoft Learn, Section Objects and Views. Về đối tượng section biểu diễn khoảng bộ nhớ có thể chia sẻ, và mỗi tiến trình ánh xạ một phần section như một view. ↩ ↩2 ↩3
-
Microsoft Learn, File-Backed and Page-File-Backed Sections. Về section dựa trên tệp và page file, CoW, và khả năng chia cùng bộ nhớ vật lý từ địa chỉ ảo của các tiến trình khác nhau. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateFileMappingW function. Về đối tượng ánh xạ tệp, section dựa trên page file,
SEC_IMAGE, vòng đời view và handle, và tính nhất quán giữa các view chống cùng một tệp. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, Memory Protection. Về nhiều tiến trình chia trang vật lý của cùng DLL, và CoW sao sang trang vật lý mới rồi cập nhật PTE khi một bên ghi. ↩ ↩2 ↩3
-
Microsoft Learn, SECTION_OBJECT_POINTERS structure. Về DataSectionObject, SharedCacheMap và ImageSectionObject gắn ánh xạ luồng tệp và thông tin cache với bộ quản lý bộ nhớ / Cache Manager. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MapViewOfFileEx function. Về CoW với
FILE_MAP_COPY; trang riêng dựa trên page file; tính Commit cho cả view; và lưu offset thay vì địa chỉ ảo. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, VirtualQuery function. Về Type vẫn
MEM_MAPPED/MEM_IMAGEsau CoW, và khả năng xác nhận tư nhân hóa bằng bit Shared củaQueryWorkingSetEx. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Managing Memory Sections. Về việc bộ nhớ vật lý chưa được gán cho đến khi truy cập view, và page fault lần truy cập đầu đọc nội dung tệp. ↩
-
Microsoft Learn, Sharing Files and Memory. Về chia cùng đối tượng ánh xạ tệp bằng tên hoặc handle; tạo bộ nhớ dùng chung dựa trên page file với
INVALID_HANDLE_VALUE; và đồng bộ cần thiết kế riêng. ↩ ↩2 ↩3 -
Microsoft Learn, Process Explorer - Sysinternals. Về Process Explorer có thể hiện handle của tiến trình cùng DLL / tệp ánh xạ bộ nhớ đã nạp. ↩
-
Microsoft Learn, VMMap - Sysinternals. Về VMMap tách bộ nhớ ảo tiến trình thành Image, Mapped File, Private và tương tự, đồng thời hiện phân tách Private/Shareable của Working Set. ↩
Bài viết liên quan
Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.
DllMain và Loader Lock — Lý do thật sự bạn bị bảo "đừng làm gì trong khởi tạo DLL"
Vì sao bạn không được gọi LoadLibrary hoặc đồng bộ với luồng khác từ DllMain. Dựa trên nguồn gốc, bài viết giải thích cách loader lock tu...
Chiều sâu bộ nhớ Windows (phần 2) — Đời sống của một trang vật lý: năm danh sách và sự thật về tệp trang
Bài viết nối cơ sở PFN, Standby, Modified, nén bộ nhớ và tệp trang để giải thích trang vật lý đi đâu sau khi rời Working Set.
Chiều sâu bộ nhớ Windows (phần 1) — Khoảnh khắc địa chỉ ảo trở thành RAM vật lý: page fault từ đầu đến cuối
Bài viết nối VirtualAlloc, VAD, bảng trang, TLB, demand-zero và hard fault để giải thích khoảnh khắc một địa chỉ ảo được gán RAM vật lý.
Bên trong ảo hóa Windows (Phần 3) — Máy ảo khởi động trong vài giây: Vì sao WSL2, Windows Sandbox và container lại nhẹ
Vì sao WSL2 và Windows Sandbox khởi động trong vài giây và cảm giác nhẹ đến vậy? Bài viết này giải thích các cơ chế, từ ảnh cơ sở động và...
Bên trong ảo hóa Windows (Phần 2) — Bộ nhớ ngay cả kernel cũng không thấy: VBS, HVCI và Credential Guard
Khi cài sạch trên phần cứng tương thích, VBS được bật mặc định và dùng hypervisor cùng SLAT để tạo cô lập mạnh hơn kernel. Bài viết này g...
Chủ đề liên quan
Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.
Chủ đề kỹ thuật Windows
Cổng vào phát triển Windows, điều tra lỗi và khai thác tài sản hiện có.
Dịch vụ liên quan đến chủ đề này
Bài viết liên quan trực tiếp đến các dịch vụ sau.
Phát triển ứng dụng Windows
Ứng dụng nghiệp vụ, tích hợp thiết bị và công cụ liên lạc, từ yêu cầu đến phát triển.
Câu hỏi thường gặp
Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.
- Mỗi tiến trình dùng cùng một DLL có nhận một bản sao đầy đủ DLL đó trong RAM không?
- Thường thì không. Các trang chưa sửa của cùng một image được ánh xạ từ các địa chỉ ảo khác nhau của mỗi tiến trình lên cùng một trang vật lý. Chỉ những trang cần ghi mới trở thành trang vật lý riêng của tiến trình, nhờ copy-on-write và tương tự.
- CreateFileMapping có cấp phát bộ nhớ cho tiến trình ngay lúc đó không?
- CreateFileMapping tạo đối tượng ánh xạ tệp, nhưng chính MapViewOfFile mới làm nó hiện trong không gian ảo của tiến trình. Hơn nữa, các trang vật lý của view thường được hiện thực hóa bằng page fault từ trang đầu tiên được truy cập.
- FILE_MAP_WRITE và FILE_MAP_COPY khác nhau thế nào?
- Thay đổi qua FILE_MAP_WRITE là ghi phản ánh sang phía dữ liệu tệp dùng chung. FILE_MAP_COPY chia sẻ các trang ban đầu, nhưng chỉ các trang bị ghi mới trở thành bản sao riêng của tiến trình; thay đổi không được ghi lại tệp gốc và mất khi view bị unmap.
- Sau copy-on-write, VirtualQuery có trả về MEM_PRIVATE không?
- Không. View dữ liệu vẫn là MEM_MAPPED và view image vẫn là MEM_IMAGE. Để biết trang đã thực sự bị tư nhân hóa chưa, hãy làm trang resident rồi xem bit Shared của QueryWorkingSetEx.
- Có được lưu con trỏ thô trong bộ nhớ dùng chung không?
- Thường thì không nên. Dù cùng một section, không có đảm bảo view của mỗi tiến trình được đặt ở cùng địa chỉ ảo. Trong cấu trúc dùng chung, hãy dùng offset từ gốc, số nguyên độ rộng cố định, và bố cục cùng cơ chế đồng bộ tường minh.