技术演进中的开发沉思-15 window编程系列:内存体系结构(下)

今天接着上个章节没讲完的内容继续,在我眼里Windows 内存体系结构就如同深埋在海底的神秘宝藏,承载着系统运行的关键密码,今天我们从其中的页面保护属性、实例分析与数据对齐说起。

一、 页面保护属性

在 Windows 的内存世界里,页面保护属性就像是一支训练有素的守护者联盟,它们日夜坚守岗位,守护着数据的安全与稳定,确保系统能够有条不紊地运行。每一种保护属性都有着独特的职责与使命,它们相互协作,共同构建起一道坚不可摧的防线。

1、 写时复制

初次接触写时复制(Copy - On - Write,COW)这个概念时,就像在纷繁复杂的代码迷宫中发现了一条隐秘的捷径。它是 Windows 内存管理中一位精打细算的资源管理者,用一种巧妙的方式平衡着效率与资源消耗。

想象我们经营着一家繁忙的图书馆,每天都有大量读者借阅书籍。如果每一位读者想要借阅某本书时,我们都立刻复制一本全新的书供其使用,那不仅会造成纸张、空间等资源的极大浪费,管理起来也会变得异常困难。写时复制就如同图书馆采用的一种智慧策略:当多位读者想要阅读同一本书时,他们最初拿到的其实是同一本实体书的 “借阅凭证”,共享同一本实体书的内容。只有当其中一位读者想要在书上做笔记、修改内容时,图书馆才会复制一本新的书,供这位读者在新的副本上进行修改,而其他读者依然可以继续共享原始的书籍内容。

在 Windows 系统中,当多个进程需要访问相同的数据时,系统并不会立即为每个进程复制一份数据副本,而是让这些进程共享同一份物理内存页面。只有当某个进程尝试对数据进行写入操作时,系统才会为该进程创建一个独立的数据副本,将原始页面复制一份,并将写入操作应用到新的副本上。这样一来,在大多数情况下,系统可以节省大量的内存空间,避免不必要的数据复制,就像图书馆减少了大量重复购书的成本,提高了资源的利用效率。

展示了一个简单场景下写时复制的应用模拟(实际 Windows 底层实现更为复杂,此处仅作示意):


#include 

#include 

int main() {

// 分配一块共享内存,初始设置为只读,模拟写时复制的初始状态

SIZE_T allocationSize = 1024;

LPVOID sharedMemory = VirtualAlloc(NULL, allocationSize, MEM_COMMIT, PAGE_READONLY);

if (sharedMemory == NULL) {

std::cerr << "内存分配失败" << std::endl;

return 1;

}

// 假设这里有两个进程共享这块内存,先读取数据

char* dataPtr = (char*)sharedMemory;

for (SIZE_T i = 0; i < allocationSize; ++i) {

dataPtr[i] = 'A'; // 这里只是初始化数据

}

// 模拟第二个进程读取数据

char readData[1024];

memcpy(readData, sharedMemory, allocationSize);

std::cout << "第二个进程读取到的数据: ";

for (SIZE_T i = 0; i < allocationSize; ++i) {

std::cout << readData[i];

}

std::cout << std::endl;

// 模拟第一个进程尝试写入数据,此时系统会触发写时复制

DWORD oldProtect;

if (!VirtualProtect(sharedMemory, allocationSize, PAGE_READWRITE, &oldProtect)) {

std::cerr << "修改内存保护属性失败" << std::endl;

VirtualFree(sharedMemory, 0, MEM_RELEASE);

return 1;

}

dataPtr[0] = 'B'; // 修改数据,此时系统会创建新的副本

// 再次让第二个进程读取数据,验证数据是否改变(因为是写时复制,第二个进程数据应不变)

memcpy(readData, sharedMemory, allocationSize);

std::cout << "第二个进程再次读取到的数据: ";

for (SIZE_T i = 0; i < allocationSize; ++i) {

std::cout << readData[i];

}

std::cout << std::endl;

VirtualFree(sharedMemory, 0, MEM_RELEASE);

return 0;

}

记得有一次,在开发一个多进程协作的项目时,项目中有多个进程需要读取大量相同的配置文件数据。起初担心频繁的数据复制会消耗大量内存,影响系统性能。后来采用了写时复制机制,在实际运行过程中,只有当某个进程真正需要修改配置数据时,才会产生数据复制操作,大部分时间里,多个进程都能高效地共享同一份数据,大大降低了内存的使用量,整个系统的运行也变得更加流畅。这种巧妙的设计,让我深刻体会到了 Windows 内存管理的精妙之处。

2、 一些特殊的访问保护属性标志

除了写时复制这位 “智慧管家”,Windows 内存体系中还有一群身怀绝技的特殊卫士 —— 特殊的访问保护属性标志。它们就像是武侠世界里的江湖高手,各自拥有独特的 “武功秘籍”,在内存保护的战场上发挥着不可替代的作用。

比如,PAGE_NOACCESS 属性标志就像是一位冷酷的守门人,它所守护的内存区域严禁任何进程进行访问。想象在一座神秘的城堡中,有一间被重重封印的密室,门口站着一位铁面无私的守卫,任何人胆敢靠近都会被毫不留情地驱逐。在 Windows 系统里,如果将某块内存区域设置为 PAGE_NOACCESS 属性,那么一旦有进程试图读取、写入或执行该区域的数据,系统就会立即触发异常,阻止进程的违规操作,就如同守卫坚决捍卫密室的秘密,确保系统内存的安全边界不被侵犯。

以下是使用 PAGE_NOACCESS 属性的 C++ 代码示例:


#include 

#include 

int main() {

// 分配一块内存,并设置为PAGE_NOACCESS

SIZE_T allocationSize = 1024;

LPVOID restrictedMemory = VirtualAlloc(NULL, allocationSize, MEM_COMMIT, PAGE_NOACCESS);

if (restrictedMemory == NULL) {

std::cerr << "内存分配失败" << std::endl;

return 1;

}

// 尝试读取内存,会触发异常

char* dataPtr = (char*)restrictedMemory;

char readData;

try {

readData = dataPtr[0]; // 这里会触发异常

} catch (...) {

std::cerr << "尝试访问受限制内存,触发异常" << std::endl;

}

VirtualFree(restrictedMemory, 0, MEM_RELEASE);

return 0;

}

而 PAGE_READONLY 属性标志则如同一位严谨的图书管理员,它所管理的内存页面只允许进程进行读取操作,禁止任何写入行为。就像图书馆里珍贵的古籍善本,读者只能在规定区域安静翻阅,不能在上面随意涂写修改。当我们将内存页面设置为 PAGE_READONLY 时,进程可以顺利读取其中的数据,但如果尝试进行写入操作,系统同样会抛出异常,防止数据被意外篡改,保障数据的完整性和准确性。

下面是 PAGE_READONLY 属性的代码示例:


#include 

#include 

int main() {

// 分配一块内存,并设置为PAGE_READONLY

SIZE_T allocationSize = 1024;

LPVOID readOnlyMemory = VirtualAlloc(NULL, allocationSize, MEM_COMMIT, PAGE_READONLY);

if (readOnlyMemory == NULL) {

std::cerr << "内存分配失败" << std::endl;

return 1;

}

// 初始化数据

char* dataPtr = (char*)readOnlyMemory;

for (SIZE_T i = 0; i < allocationSize; ++i) {

dataPtr[i] = 'C';

}

// 尝试写入数据,会触发异常

try {

dataPtr[0] = 'D'; // 这里会触发异常

} catch (...) {

std::cerr << "尝试写入只读内存,触发异常" << std::endl;

}

VirtualFree(readOnlyMemory, 0, MEM_RELEASE);

return 0;

}

这些特殊的访问保护属性标志,如同忠诚的卫士,时刻警惕着内存世界中的潜在威胁,用它们独特的能力,维护着 Windows 内存系统的秩序与稳定。每当在代码中设置这些属性标志时,都仿佛在与这些卫士进行一场无声的对话,将数据的安全托付给它们,心中充满了信任与安心。

二、 实例分析:内存世界的实战演练

在 Windows 内存体系的探索之旅中,仅仅了解理论知识就如同纸上谈兵,真正深入到实例分析中,才能领略到其中的奥秘与魅力。每一个实际的案例,都是一次与内存世界的亲密对话,让我们在解决问题的过程中,不断加深对 Windows 内存管理的理解。

曾经参与过一个大型的图形处理项目,在项目运行过程中,偶尔会出现程序崩溃的情况。经过一番排查,发现问题出在内存管理上。在图形渲染过程中,程序需要频繁地访问和修改大量的图像数据。由于对内存页面的保护属性设置不当,导致在多线程环境下,多个线程同时对同一内存区域进行写入操作,引发了数据冲突和内存错误,最终导致程序崩溃。

就像是一场热闹的绘画比赛,许多画家在同一张画布上作画,却没有任何规则和协调,最终画面变得混乱不堪。为了解决这个问题,我们重新审视了内存页面的保护属性设置。对于那些只需要读取图像数据的线程,将相关内存页面设置为 PAGE_READONLY 属性,禁止它们进行写入操作;而对于负责修改图像数据的线程,则采用写时复制机制,为每个线程创建独立的数据副本,避免数据冲突。经过这样的调整,就如同为绘画比赛制定了明确的规则,画家们各自在自己的画布上创作,互不干扰。程序崩溃的问题得到了有效解决,整个图形处理过程变得更加稳定和高效。

以下是一个简化的多线程图形处理示例,展示如何通过合理设置内存保护属性避免冲突(使用 C++ 和 Windows API):


#include 

#include 

#include 

#include 

// 模拟图像数据

const int imageSize = 1024;

char* sharedImageData;

// 读取图像数据的线程函数

void readImageData() {

// 假设这里将内存设置为只读

DWORD oldProtect;

if (!VirtualProtect(sharedImageData, imageSize, PAGE_READONLY, &oldProtect)) {

std::cerr << "设置内存为只读失败" << std::endl;

return;

}

char localData[imageSize];

memcpy(localData, sharedImageData, imageSize);

// 这里可以进行图像数据读取相关操作

std::cout << "线程读取图像数据成功" << std::endl;

// 恢复内存保护属性

if (!VirtualProtect(sharedImageData, imageSize, oldProtect, &oldProtect)) {

std::cerr << "恢复内存保护属性失败" << std::endl;

}

}

// 修改图像数据的线程函数

void modifyImageData() {

// 假设这里使用写时复制机制,先将内存设置为可读写

DWORD oldProtect;

if (!VirtualProtect(sharedImageData, imageSize, PAGE_READWRITE, &oldProtect)) {

std::cerr << "设置内存为可读写失败" << std::endl;

return;

}

// 修改图像数据

sharedImageData[0] = 'X';

std::cout << "线程修改图像数据成功" << std::endl;

// 恢复内存保护属性

if (!VirtualProtect(sharedImageData, imageSize, oldProtect, &oldProtect)) {

std::cerr << "恢复内存保护属性失败" << std::endl;

}

}

int main() {

// 分配共享内存

sharedImageData = (char*)VirtualAlloc(NULL, imageSize, MEM_COMMIT, PAGE_READWRITE);

if (sharedImageData == NULL) {

std::cerr << "内存分配失败" << std::endl;

return 1;

}

// 初始化图像数据

for (int i = 0; i < imageSize; ++i) {

sharedImageData[i] = 'Y';

}

// 创建读取线程和修改线程

std::vector threads;

threads.push_back(std::thread(readImageData));

threads.push_back(std::thread(modifyImageData));

// 等待线程完成

for (auto& th : threads) {

if (th.joinable()) {

th.join();

}

}

VirtualFree(sharedImageData, 0, MEM_RELEASE);

return 0;

}

还有一次,在开发一个实时数据监控系统时,系统需要实时接收和处理大量的传感器数据。由于数据量庞大,内存资源变得十分紧张。通过对内存使用情况进行深入分析,我们发现一些数据在内存中的存储方式不够合理,导致内存空间浪费严重。比如,某些数据结构在内存中的对齐方式不符合 Windows 系统的要求,使得内存访问效率低下,并且占用了不必要的内存空间。

我们借鉴了数据对齐的原理,对数据结构进行了优化调整。将数据按照合理的字节边界进行对齐,就像将杂乱无章的物品按照一定规律整齐摆放,不仅节省了空间,还提高了内存访问速度。经过优化后,系统在处理相同规模数据时,内存占用明显减少,运行效率大幅提升,能够更加稳定地实时监控和处理数据。这些实例分析,就像是一面镜子,让我们清晰地看到 Windows 内存体系在实际应用中的优势与不足,也让我们在不断解决问题的过程中,积累了宝贵的经验,提升了技术能力。

三、 数据对齐的重要性

在 Windows 内存体系的宏大画卷中,数据对齐或许是一幅容易被忽视的细节,但它却如同建筑物的基石,看似平凡,却支撑起整个系统高效运行的大厦。数据对齐,就像是给内存空间赋予了一种有序的美学,让数据在内存中的存储和访问更加高效、和谐。

想象我们居住的城市里,街道上的房屋如果毫无规划地随意建造,不仅会影响城市的美观,还会给居民的生活带来诸多不便,比如交通拥堵、水电供应困难等。而经过合理规划、整齐排列的房屋,则会让城市变得井然有序,居民的生活也会更加便捷舒适。在 Windows 内存世界里,数据对齐就如同城市规划师,它规定了数据在内存中存储的位置和方式,使数据能够按照一定的规则整齐排列。

计算机在访问内存数据时,就像快递员在城市中派送包裹。如果数据没有进行合理对齐,存储位置杂乱无章,那么计算机在读取数据时,就如同快递员在混乱的街道中寻找收件人,需要花费大量的时间和精力去定位数据,效率会大大降低。而当数据按照规定的字节边界进行对齐后,计算机就可以像熟悉城市道路的资深快递员一样,快速准确地找到数据的存储位置,高效地完成数据读取和写入操作。

在实际编程中,数据对齐的重要性也随处可见。比如,在定义结构体时,如果不考虑数据对齐,可能会导致结构体占用的内存空间比预期的要大。假设我们定义一个包含一个字节的字符变量和一个四字节的整数变量的结构体,如果不进行数据对齐,它们在内存中可能会按照顺序依次存储,这样结构体可能会占用 6 个字节的空间(为了满足整数变量的对齐要求,可能会在字符变量后面填充一个字节)。但如果按照数据对齐规则进行合理布局,我们可以将字符变量放在前面,然后填充三个字节,再存储整数变量,这样结构体只需要占用 8 个字节的空间,虽然空间占用看似没有减少,但在内存访问效率上却有了显著提升,因为计算机可以更高效地读取和处理对齐后的数据。

下面通过代码展示数据对齐的影响(使用 C++):


#include 

// 未对齐的结构体

struct UnalignedStruct {

char c;

int i;

};

// 对齐的结构体,使用#pragma pack指令设置对齐方式

#pragma pack(push, 1)

struct AlignedStruct {

char c;

int i;

};

#pragma pack(pop)

int main() {

std::cout << "未对齐结构体大小: " << sizeof(UnalignedStruct) << " 字节" << std::endl;

std::cout << "对齐结构体大小: " << sizeof(AlignedStruct) << " 字节" << std::endl;

return 0;

}

数据对齐不仅影响内存空间的使用效率,还与系统的性能密切相关。在一些对性能要求极高的应用场景,如游戏开发、实时数据处理等领域,合理的数据对齐能够大幅提升程序的运行速度,减少系统。

最后小结

我们可以理解,页面保护属性作为数据的守护者联盟,以写时复制为代表的策略,如同精打细算的资源管理者,巧妙地平衡了效率与资源消耗;而 PAGE_NOACCESS、PAGE_READONLY 等特殊访问保护属性标志,则像身怀绝技的卫士,时刻警惕着潜在威胁,守护着内存世界的秩序。通过实际的代码示例,我们直观地看到这些属性如何在程序中发挥作用,保障数据安全。​

实例分析环节,那些曾在项目中遭遇的 “内存困境”,无论是图形处理程序的崩溃,还是实时数据监控系统的内存紧张,都成为了我们深入理解内存管理的突破口。通过合理调整内存保护属性、优化数据对齐,问题迎刃而解,这不仅让我们积累了宝贵的实战经验,更深刻体会到 Windows 内存体系结构在实际应用中的强大生命力。​

数据对齐看似细微,却有着深远影响。它如同内存空间的有序美学,通过合理规划数据存储位置,大幅提升内存访问效率,减少空间浪费。从结构体的内存占用差异代码示例中,我们清晰地看到数据对齐对系统性能的重要意义。​

时间久矣,在现在日益强大的系统框架面前,我想很多程序员都忽视了或者都没了解过从前编程技术上的一些精髓。今天把它写出来放在这里,我相对于自己也是一次总结。未完待续...........

你可能感兴趣的:(熬之滴水穿石,windows)