أعماق ذاكرة Windows (الجزء 1) — اللحظة التي يصبح فيها العنوان الافتراضي ذاكرة RAM مادّيّة: خطأ الصفحة من البداية إلى النهاية
· Go Komura · Windows, إدارة الذاكرة, VirtualAlloc, خطأ الصفحة, VAD, مراقبة الأداء
تمرير MEM_COMMIT إلى VirtualAlloc يزيد Commit في تلك اللحظة. أمّا Working Set فلا يزيد بالضرورة بالمقدار نفسه. فأين الذاكرة التي ظننت أنّك خصّصتها؟
الجواب أنّ معظم الصفحات ليس لها بعد RAM مادّيّ مقابل. يؤجّل Windows إسناد صفحة مادّيّة إلى أن يلمس التطبيق الصفحة فعلاً. عندما يجعل أوّل وصول المعالج يرفع خطأ صفحة، يفحص مدير الذاكرة VAD وPTE وسمات الحماية والمخزن الخلفيّ، ويربط RAM صفحةً صفحة إن لزم.1
يتتبّع هذا المقال المسار الذي يسلكه «أوّل بايت تلمسه» حتّى يصل إلى RAM المادّيّ. إن أردت أوّلاً ترتيب معنى أرقام مثل Working Set وCommit، انظر المقال التمهيديّ «What Does Windows’ “Memory Usage” Actually Mean? — Correctly Reading Working Set, Private Bytes, Commit, and the Page File». لا تعيد هذه السلسلة تعريف المصطلحات المستخدمة هناك؛ بل تحفر من جانب الآليّة في «لماذا يخرج الرقم كذلك».
«أعماق ذاكرة Windows» — الأجزاء الثلاثة
- الجزء 1 (هذا المقال): العناوين الافتراضيّة وأخطاء الصفحة
نتتبّع متى تحصل منطقة خصّصهاVirtualAllocعلى RAM مادّيّ. - الجزء 2: حياة الصفحة المادّيّة
نتتبّع كيف تنتقل صفحة غادرت Working Set عبر Modified وStandby وFree وZeroed. - الجزء 3: كائنات القسم والنسخ عند الكتابة
نتتبّع لماذا تستطيع DLL وتعيينات الملفّات والذاكرة المشتركة مشاركة صفحات مادّيّة.
السؤال الذي يجيب عنه الجزء 1 واحد فقط.
في أيّ لحظة يصبح عنوان افتراضيّ ملتزَم RAM مادّيّاً؟
القرّاء المقصودون مطوّرون ومشغّلون يريدون فهم استخدام ذاكرة تطبيقات Windows وأخطاء الصفحة بعد التشغيل مباشرةً و0xC0000005 وأرقام VMMap وPerfMon من الآليّة. المتطلّبات Windows 10/11 أو إصدار حاليّ من Windows Server، والخلفية اللازمة المؤشّرات وأساسيّات VirtualAlloc؛ لا تحتاج خبرة تخطيط بتّات جداول الصفحات أو منقّح النواة. الصعوبة متوسّطة. نستخدم أسماء البنى الداخليّة، لكنّنا لا نفترض تخطيطات غير موثّقة تعتمد على بناء Windows معيّن.
1. الخلاصة أوّلاً
تدفّق الذاكرة الخاصّة العاديّ، في سطر واحد، هو هذا.
Reserve يحجز نطاق عناوين افتراضيّة، وCommit يحتسب حصة التزام (commit charge) لضمان مكان مستقبليّ لحفظ المحتوى، وخطأ الصفحة عند أوّل وصول يسند الصفحة المادّيّة.
بعبارة أخرى، MEM_COMMIT ليس أمراً يقول «خصّص RAM الآن». وثائق Microsoft لـ VirtualAlloc تضمن أيضاً أنّ المحتوى الأوّلي لصفحة ملتزَمة صفر، وتشرح أنّ الصفحة المادّيّة الفعليّة لا تُسنَد حتّى يُوصَل إلى العنوان الافتراضيّ.1
مع ذلك، من غير الدقيق أيضاً الادّعاء أنّ «Reserve/Commit يكتبان في VAD فحسب». في الممارسة ينشئ Reserve أساساً VAD يمثّل نطاق العناوين الافتراضيّة وسماته، ويزيد Commit قيمة Commit Total للنظام ويسجّل حالة الالتزام للنطاق. تُبنى مستويات جداول الصفحات الوسطى وPTE الفرديّة بتكاسل عند الحاجة، والربط النهائي بـ RAM المادّيّ يحدث عادةً عند أوّل وصول.
Commit ليس وعداً فارغاً؛ إنّه وعد على مستوى النظام بأنّ المحتوى يمكن حفظه مستقبلاً في RAM أو مخزن خلفيّ مناسب. نقطة الدخول التي تجسّد ذلك الوعد صفحةً صفحة هي خطأ الصفحة.
flowchart TB
accTitle: ما يحدث عند Reserve وCommit وأوّل وصول
accDescr: يسجّل MEM_RESERVE النطاق والسمات في VAD، ويستهلك MEM_COMMIT قيمة Commit Total ليعد بالتخزين، وخطأ الصفحة عند أوّل وصول يسند صفحة مادّيّة ويضيفها إلى Working Set
reserve["1. MEM_RESERVE"] --> commit["2. MEM_COMMIT"]
commit --> touch["3. أوّل وصول(Touch)"]
reserve -.-> vad["تسجيل النطاق والسمات في VAD"]
commit -.-> charge["استهلاك Commit Total(لا صفحة مادّيّة بعد)"]
touch --> fault["خطأ صفحة"]
fault --> zero["ربط صفحة مادّيّة مُصفَّرة بـ PTE"]
zero --> ws["إضافتها إلى Working Set وإعادة تنفيذ التعليمة"]
الشكل 1: Reserve وCommit وTouch أحداث منفصلة. لا يُربَط RAM المادّيّ إلا في الخطوة الأخيرة، أوّل وصول.
2. ثلاثة دفاتر تتتبّع صفحة افتراضيّة
لفهم المسار من عنوان افتراضيّ إلى RAM مادّيّ، تحتاج إلى تمييز أنواع الدفاتر الثلاثة التي يمسكها Windows.
| الدفتر | الوحدة | الدور |
|---|---|---|
| VAD | نطاق عناوين افتراضيّة | يدير ماهيّة المنطقة وReserve/Commit والحماية ومقابلة القسم |
| جدول الصفحات / PTE | صفحة افتراضيّة | يمثّل الترجمة الحاليّة إلى صفحة مادّيّة، أو حالة غير متحقّقة |
| قاعدة PFN | صفحة مادّيّة | يتتبّع الملكيّة والمراجع وحالة كلّ صفحة RAM |
يمسك VAD معلومات عن نطاق، وPTE عن صفحة افتراضيّة، وقاعدة PFN عن صفحة مادّيّة. يعبر معالج خطأ الصفحة هذه الدفاتر ليقرّر إن كان الوصول يمكن أن يستمرّ.
flowchart TB
accTitle: ثلاثة دفاتر من العنوان الافتراضيّ إلى RAM المادّيّ
accDescr: يدير VAD العنوان الافتراضيّ بحبيبيّة النطاق وPTE بحبيبيّة الصفحة الافتراضيّة، وتتتبّع قاعدة PFN الصفحة المادّيّة هدف ترجمة PTE بحبيبيّة الصفحة المادّيّة
va["عنوان افتراضيّ"] --> vad["VAD(دفتر النطاق)"]
va --> pte["PTE(دفتر الصفحة الافتراضيّة)"]
vad -.->|الحكم على Reserve/Commit والحماية| pte
pte -->|ترجمة صالحة| pfn["قاعدة PFN(دفتر الصفحة المادّيّة)"]
pfn --> ram["صفحة RAM مادّيّة"]
الشكل 2: ثلاثة دفاتر بحبيبيّات مختلفة. تعالج الأخطاء بمقابلة VAD وPTE ثمّ تعكس النتيجة على جانب PFN.
نجما هذا المقال هما VAD وPTE. سننظر إلى قاعدة PFN من جانب الصفحة المادّيّة في الجزء 2.
3. Reserve وCommit وTouch أحداث منفصلة
3.1. Reserve — المطالبة بعنوان
أوّلاً، احجز نطاقاً متّصلاً من 256 ميبيبايت من العناوين الافتراضيّة.
void* base = VirtualAlloc(
nullptr,
256ull * 1024 * 1024,
MEM_RESERVE,
PAGE_NOACCESS);
ما حدث في هذه النقطة هو فقط أنّ عنواناً وُضع جانباً في الفضاء الافتراضيّ للعمليّة كي لا تستخدم تخصيصات أخرى هذا النطاق. لا يسند MEM_RESERVE تخزيناً مادّيّاً في RAM ولا في ملفّ الصفحات.1
لأنّ عمليّة 64 بت لها فضاء افتراضيّ هائل، يصبح عمليّاً عمل Reserve لنطاق كبير أوّلاً ثمّ Commit للأجزاء التي تحتاجها لاحقاً فقط.
3.2. Commit — الوعد بأنّه يمكن حفظه
بعد ذلك، اعمل Commit للنطاق المحجوز.
void* committed = VirtualAlloc(
base,
256ull * 1024 * 1024,
MEM_COMMIT,
PAGE_READWRITE);
عند النجاح يزيد المقدار الموعود الذي ينعكس في Commit Total للنظام — وعادةً في Private Bytes للعمليّة. مع ذلك لا تصطفّ 256 ميبيبايت من الصفحات المادّيّة دفعة واحدة. تبقى الصفحات العاديّة غير مسندة مادّيّاً حتّى أوّل وصول.12
فما فائدة Commit؟ أنّه عندما لا يستطيع النظام تحمّل الوعد، يمكنه إرجاع فشل وقت Commit، لا في وسط استخدام الذاكرة.
3.3. Touch — متى تصبح الصفحة المادّيّة ضروريّة
أخيراً، التعيين التالي يكتب في الصفحة الأولى لأوّل مرّة.
static_cast<unsigned char*>(base)[0] = 1;
يحاول المعالج ترجمة العنوان الافتراضيّ إلى عنوان مادّيّ، لكن ليس لـ PTE بعد ترجمة صالحة إلى صفحة مادّيّة. يحدث خطأ صفحة هنا.
يحكم مدير الذاكرة الذي يتلقّى التحكّم أنّ هذا «أوّل وصول إلى صفحة خاصّة ملتزَمة وقابلة للكتابة»، فيحصل على صفحة مادّيّة مُصفَّرة ويربطها بـ PTE ويضيفها إلى Working Set. ثمّ يعيد تنفيذ تعليمة الكتابة التي فشلت.
من التطبيق يبدو مجرّد تعيين، لكن داخليّاً يدخل التحكّم النواة في وسط التعيين، وتُسنَد صفحة مادّيّة، ويعود التنفيذ إلى التعليمة نفسها.
4. VAD — دفتر نطاقات الفضاء الافتراضيّ
VAD اختصار Virtual Address Descriptor، ويدير Windows نطاقات العناوين المستخدمة لعمليّة كشجرة من VAD. بأمر !vad في WinDbg يمكنك فحص VPN البداية والنهاية وCommit وسمات الحماية وPrivate/Mapped وControl Area وغيرها.3
المعلومات التمثيليّة التي يسجّلها VAD تشمل ما يلي.
- بداية نطاق العناوين ونهايته
- النوع مثل Private أو Mapped أو Image
- حالة Reserve/Commit
- حماية مثل القراءة والكتابة والتنفيذ والنسخ عند الكتابة
- المقابلة لملفّ أو قسم
- سمات خاصّة مثل صفحات الحراسة
سبب الإدارة بالنطاق هو الكفاءة. 256 ميبيبايت هي 65,536 صفحة عند 4 كيبيبايت. بدل بناء بنية إدارة كاملة لكلّ صفحة مسبقاً، أقلّ هدراً أن تمسك «هذا النطاق المتّصل حجز واحد» في VAD وتجسّد الصفحات حين تصبح ضروريّة.
4.1. العثور على VAD لا يضمن الاستعادة
«إن كان في VAD يُحَلّ الخطأ؛ وإن لم يكن تحصل على انتهاك وصول» شرح مبتدئ مريح، لكنّه يبسّط أكثر ممّا ينبغي. حتّى عند العثور على VAD لا يمكن استمرار الوصول العاديّ في حالات مثل التالية.
- Reserve فقط، والصفحة المستهدفة غير ملتزَمة
PAGE_NOACCESS- كتابة إلى صفحة للقراءة فقط
- تنفيذ تعليمة من صفحة غير قابلة للتنفيذ
- أوّل لمس لصفحة حراسة
- لمس خارج النطاق الصالح لقسم
عكس ذلك، حتّى إن كانت PTE غير صالحة، إن أظهر الحالة البرمجيّة لـ VAD وPTE وصولاً مشروعاً، يمكن حلّه كـ demand-zero أو استعادة Transition أو جلب صفحة أو CoW. الأدقّ أنّ الجواب احكم على VAD وPTE وسمات الحماية ونوع الوصول معاً.
5. جداول الصفحات وTLB
المؤشّر الذي يمسكه التطبيق عنوان افتراضيّ. لكي يصل المعالج إلى RAM يجب أن يترجم رقم صفحة افتراضيّة إلى رقم صفحة مادّيّة. جدول الترجمة الهرميّ ذلك هو جدول الصفحات، والمدخل الورقيّ هو PTE (Page Table Entry).
تمسك PTE صالحة مفهوميّاً PFN وحماية القراءة/الكتابة/التنفيذ وإذن وضع المستخدم وAccessed/Dirty ومعلومات مشابهة. تخطيط البتّات الفعليّ يعتمد على المعالج وإصدار Windows.
مشي جدول الصفحات في كلّ مرّة سيكون بطيئاً جدّاً، لذا يخزّن المعالج الترجمات الحديثة في TLB (Translation Lookaside Buffer). تسير ترجمة العناوين بهذا الترتيب.
- إن كان في TLB ترجمة ووافق الوصول تلك الحماية، استُخدم ذلك الناتج.
- إن لم يكن في TLB ترجمة، يمشي المعالج جدول الصفحات.
- إن وُجدت PTE صالحة ووافقت الحماية أيضاً، سُجّلت في TLB واستمرّ التنفيذ.
- إن لم توجد ترجمة صالحة، أو وُجد انتهاك حماية، انتقل التحكّم إلى مدخل خطأ الصفحة. يُنفَّذ فحص الحماية أيضاً عندما جاءت الترجمة من TLB.
كما يُظهر هذا التدفّق، إخفاق TLB وخطأ الصفحة شيئان مختلفان. إن كانت المسألة الوحيدة أنّ TLB بلا ترجمة وPTE صالحة، يحدث مشي جدول الصفحات فحسب. عكس ذلك، حتّى إن كان في TLB ترجمة، فإنّ انتهاك حماية مثل كتابة إلى صفحة للقراءة فقط أو تنفيذ تعليمة على صفحة غير قابلة للتنفيذ ينتقل إلى مدخل خطأ الصفحة. لذلك يمكن لكتابة إلى صفحة CoW أن تخطئ حتّى عندما تكون الترجمة مخزّنة مسبقاً.
flowchart TB
accTitle: تدفّق ترجمة العناوين ومدخل خطأ الصفحة
accDescr: حتّى إن كان في TLB ترجمة فإنّ عدم مطابقة الحماية ينتقل إلى مدخل خطأ الصفحة. إن لم يكن في TLB ترجمة يُمشى جدول الصفحات؛ PTE صالحة توافق الحماية أيضاً تُسجَّل في TLB ويستمرّ التنفيذ، وترجمة غير صالحة أو انتهاك حماية ينتقلان إلى مدخل خطأ الصفحة
access["وصول إلى الذاكرة"] --> tlb{"هل لدى TLB ترجمة؟"}
tlb -->|نعم| perm{"هل يوافق الوصول الحماية؟"}
perm -->|موافق| go["الاستمرار بتلك الترجمة"]
perm -->|انتهاك حماية| entry["إلى مدخل خطأ الصفحة"]
tlb -->|لا| walk["مشي جدول الصفحات"]
walk --> valid{"PTE صالحة والحماية توافق أيضاً؟"}
valid -->|نعم| register["التسجيل في TLB والاستمرار(بلا خطأ)"]
valid -->|غير صالحة أو انتهاك حماية| entry
الشكل 3: يمكن حلّ إخفاق TLB بمشي جدول الصفحات. ينتقل التحكّم إلى خطأ صفحة عندما تكون الترجمة غير صالحة أو هناك انتهاك حماية، ويحدث انتهاك الحماية حتّى عند إصابة TLB.
5.1. PTE غير الصالحة ليست فراغاً فحسب
حتّى PTE غير الصالحة ليست فارغة. من الحالة البرمجيّة لـ PTE غير صالحة يميّز Windows حالات مثل التالية.
- صفحة demand-zero لم تُجسَّد قطّ
- صفحة Transition تبقى في RAM
- صفحة مشتركة تشير إلى Prototype PTE
- صفحة خاصّة محفوظة في ملفّ الصفحات
- انتهاك حماية أو منطقة غير صالحة
عمل المعالج هو فقط أن يقرّر «هذه ليست ترجمة صالحة عاديّة» ويسلّمها للنواة؛ مدير الذاكرة يوفّر المعنى من هناك.
6. خطأ الصفحة من البداية إلى النهاية
لنتتبّع أوّل كتابة إلى صفحة خاصّة ملتزَمة في ستّ مراحل.
- يحاول المعالج الكتابة.
يفحص TLB وجدول الصفحات، لكن ليس لـ PTE المستهدفة PFN صالح. - يرفع المعالج خطأ صفحة.
يمرّر العنوان الافتراضيّ الخاطئ ونوع القراءة/الكتابة/التنفيذ والمستخدم/النواة وما إذا كانت المسألة ترجمة ناقصة أو انتهاك حماية إلى النواة. - يفحص مدير الذاكرة VAD وPTE.
يقرّر إن كانت الصفحة ملتزَمة وإن وافقت الحماية وأيّ من demand-zero وTransition والمشترك وجلب الصفحة وCoW أو استثناء ينطبق. - إن كان demand-zero، تُحصَل صفحة مادّيّة مُصفَّرة.
يجب أن تكون الصفحة المسلَّمة حديثاً صفراً كي لا تتسرّب بيانات عمليّة أخرى. - تُحدَّث معلومات إدارة PTE وPFN.
يُضبَط PFN والحماية في PTE، وتُجعَل الصفحة المادّيّة Active، وتُضاف إلى Working Set للعمليّة. - تُعاد تنفيذ التعليمة الفاشلة.
لأنّ الخطأ حُلّ بشكل طبيعيّ لا يُسلَّم استثناء وضع مستخدم، ويستمرّ التطبيق في التعيين كالمعتاد.
تسجّل أحداث ETW لخطأ الصفحة أيضاً Transition وDemand Zero وCopy-on-Write وGuard Page وHard Page Fault وAccess Violation أنواعاً متمايزة.4
إذن خطأ الصفحة ليس كلمة تعني «شاذّ» من البداية. إنّه مدخل مشترك لطلب قرار نظام التشغيل عندما لم يستطع المعالج الترجمة على المسار العاديّ.
flowchart LR
accTitle: تفرّع حلول خطأ الصفحة
accDescr: يحكم مدير الذاكرة على VAD وPTE وسمات الحماية ونوع الوصول ويوزّع إلى demand-zero وإعادة ربط صفحة ما زالت في RAM وخطأ صلب من مخزن خلفيّ والنسخ عند الكتابة وإشعار صفحة حراسة أو استثناء
faultIn["حدوث خطأ صفحة"] --> judge["الحكم على VAD وPTE والحماية والنوع"]
judge -->|أوّل وصول| dz["Demand-zero(ليّن)"]
judge -->|ما زالت في RAM| soft["إعادة الربط من Standby(ليّن)"]
judge -->|يلزم قراءة قرص| hard["خطأ صلب(إدخال/إخراج قرص)"]
judge -->|كتابة CoW| cow["نسخ واستبدال PTE"]
judge -->|صفحة حراسة| guard["إزالة الحراسة والإشعار"]
judge -->|غير قابل للحلّ| av["استثناء(0xC0000005 إلخ)"]
الشكل 4: الأخطاء التي تدخل من المدخل نفسه تنقسم إلى ستّة أنواع من النتائج حسب الحكم. تفاصيل صفحات الحراسة في القسم 9.
7. Demand-zero — خطأ ليّن لا يقرأ القرص
Demand-zero هو الخطأ الليّن التمثيليّ الذي يحدث عند أوّل لمس لصفحة خاصّة ملتزَمة. تسرد وثائق Microsoft لـ Working Set أيضاً «تشير العمليّة إلى صفحة افتراضيّة مخصّصة لأوّل مرّة» مثالاً لخطأ ليّن.5
لـ demand-zero الخصائص التالية.
- لا حاجة لقراءة بيانات أصليّة من القرص
- المحتوى الأوّلي صفر
- تُربَط صفحة مادّيّة متاحة
- يزيد Working Set وPage Fault Count التراكميّ
- هذه المعالجة وحدها لا تزيد
Memory\\Pages Input/sec
لذلك قفزة Page Faults/sec بعد التشغيل مباشرةً لا تعني وحدها أنّ التخزين هو الاختناق.
جدير أيضاً ترتيب مقايضة التخصيص الكسول. إن عملت Commit لـ 256 ميبيبايت واستخدمت فعلاً 8 ميبيبايت فقط، فإنّ ترك الـ 248 ميبيبايت الباقية خارج RAM معقول. في المقابل يحمل أوّل وصول تكلفة معالجة الخطأ. للعمل الحسّاس للكمون يوجد تصميم يلمس كلّ صفحة قبل البدء لعمل prefault، لكنّها مقايضة تزيد إقامة RAM مسبقاً.
8. الأخطاء الليّنة والأخطاء الصلبة
8.1. الأخطاء الليّنة
الخطأ الليّن خطأ يمكن حلّه بلا إدخال/إخراج قراءة إلى مخزن خلفيّ. تشمل الأمثلة التمثيليّة ما يلي.
- Demand-zero
- إعادة ربط صفحة تبقى على Standby/Transition
- ربط صفحة مشتركة في Working Set لعمليّة أخرى
- ربط صفحة مسبقة الجلب
- نسخ عند الكتابة صفحته الأصليّة مقيمة
ما زالت هناك تكلفة معالج لانتقال النواة والأقفال وتحديثات PTE/PFN واتّساق TLB وما شابه، لكن لا انتظار تخزين.5
8.2. الأخطاء الصلبة
من الجهة الأخرى، عندما لا تكون الصفحة اللازمة في أيّ مكان في RAM ويجب قراءتها من مخزن خلفيّ، فذلك خطأ صلب. مصدر القراءة ليس ملفّ الصفحات فقط.
- صفحة خاصّة كُتبت إلى ملفّ الصفحات
- ملفّ معيَّن في الذاكرة
- صورة EXE أو DLL
- ملفّ بيانات يشير إليه ذاكرة الملفّات المؤقّتة
تتضمّن أحداث ETW HardFault FileObject وReadOffset وByteCount، لذا يمكنك تتبّع مصدر القراءة الفعليّ.6
لذلك Hard Fault = قراءة pagefile.sys غير صحيح.
عندما تلزم قراءة مخزن خلفيّ، يدخل الطلب مكدّس إدخال/إخراج Windows. تدفّق IRP والإصدار/الإكمال مشروح في «The Depths of Windows I/O (Part 1)»، والتقاطع مع ذاكرة الملفّات المؤقّتة في «The Depths of Windows I/O (Part 4)». إن كانت الصفحة في RAM يمكن لمدير الذاكرة أن يعود وحده؛ وإن لم تكن يصدر إدخالاً/إخراجاً وينتظر الخيط الخاطئ حتّى الإكمال.
9. الخطأ غير القابل للحلّ يصبح استثناءً
خطأ لا يمكن، بعد فحص VAD وPTE، حلّه كتخصيص مشروع أو جلب صفحة أو CoW يُسلَّم إلى وضع المستخدم كاستثناء.
الحالة التمثيليّة هي STATUS_ACCESS_VIOLATION، رمز الاستثناء 0xC0000005. يحدث عند قراءة أو كتابة أو تنفيذ عنوان غير صالح؛ يشير معامل الاستثناء الأوّل إلى نوع الوصول والثاني إلى العنوان المخالف.7
تشمل الأنماط النموذجيّة ما يلي.
- قراءة NULL أو عنوان محرَّر أو عنوان خارج مصفوفة
- كتابة إلى صفحة للقراءة فقط
- تنفيذ تعليمة من صفحة جعلها DEP/NX غير قابلة للتنفيذ
- لمس نطاق محجوز غير ملتزَم
لـ PAGE_GUARD معنى مختلف قليلاً. إنّه إشعار لمرّة واحدة بالوصول: يرفع STATUS_GUARD_PAGE_VIOLATION ويُستخدم لأمور مثل نموّ المكدّس.8
التخصيص الكسول العاديّ وجلب الصفحة وCoW وإشعار الحراسة وانتهاك الوصول النهائي تتجمّع، من وجهة نظر المعالج، عند مدخل خطأ الصفحة نفسه. ما يقرّر النتيجة هو تركيبة VAD وPTE وسمات الحماية ونوع الوصول.
10. انظر بنفسك
يمكنك ملاحظة التدفّق حتّى هنا على جهازك. برنامج C++ التالي يعمل Reserve لـ 256 ميبيبايت ثمّ Commit ويكتب بايتاً واحداً لكلّ صفحة وأخيراً Release. ينتظر Enter في كلّ مرحلة كي تلاحظ التغييرات في VMMap وPerfMon.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cstdlib>
#pragma comment(lib, "Psapi.lib")
constexpr SIZE_T kSize = 256ull * 1024 * 1024;
void PrintMemory(const char* stage)
{
PROCESS_MEMORY_COUNTERS_EX c{};
c.cb = sizeof(c);
if (!GetProcessMemoryInfo(
GetCurrentProcess(),
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&c),
sizeof(c))) {
std::printf("GetProcessMemoryInfo failed: %lu\n", GetLastError());
return;
}
std::printf(
"%-10s WS=%zu MiB Private=%zu MiB Faults=%lu\n",
stage,
c.WorkingSetSize / 1024 / 1024,
c.PrivateUsage / 1024 / 1024,
c.PageFaultCount);
}
void Pause(const char* message)
{
PrintMemory(message);
std::puts("Press Enter...");
(void)std::getchar();
}
int main()
{
SYSTEM_INFO si{};
GetSystemInfo(&si);
std::printf("PID=%lu, page=%lu bytes\n",
GetCurrentProcessId(), si.dwPageSize);
void* base = VirtualAlloc(nullptr, kSize, MEM_RESERVE, PAGE_NOACCESS);
if (!base) {
std::fprintf(stderr, "Reserve failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("reserved");
if (!VirtualAlloc(base, kSize, MEM_COMMIT, PAGE_READWRITE)) {
std::fprintf(stderr, "Commit failed: %lu\n", GetLastError());
VirtualFree(base, 0, MEM_RELEASE);
return EXIT_FAILURE;
}
Pause("committed");
auto* bytes = static_cast<volatile unsigned char*>(base);
for (SIZE_T offset = 0; offset < kSize; offset += si.dwPageSize) {
bytes[offset] = 1;
}
Pause("touched");
if (!VirtualFree(base, 0, MEM_RELEASE)) {
std::fprintf(stderr, "Release failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("released");
}
من موجه أوامر Native Tools x64 في Visual Studio يمكنك البناء بالأمر التالي.
cl /std:c++20 /EHsc /W4 memory_fault_demo.cpp
10.1. ما الذي تنظر إليه في VMMap
VMMap أداة تعرض الذاكرة الافتراضيّة المحجوزة وCommit وWorking Set وPrivate وShareable حسب النوع.9 التغييرات المتوقّعة في كلّ مرحلة كما يلي.
| المرحلة | التغيير المتوقّع |
|---|---|
| Reserve | يزيد Address Space Size، لكن Commit/WS لا يزيدان بالمقدار نفسه |
| Commit | يزيد Private Commit بنحو 256 ميبيبايت |
| Touch | يزيد Working Set وPrivate WS زيادة كبيرة، ويزيد Fault Count أيضاً |
| Release | يختفي النطاق المستهدف، وينخفض Commit وWS |
تختلف الأرقام الفعليّة مع بيئة التشغيل ومنتجات الأمن وضغط الذاكرة ووقت الملاحظة. انظر في أيّ اتجاه تحرّكت الأرقام بين المراحل، لا إلى ما إذا خرجت تماماً 256 ميبيبايت.
10.2. فصل الليّن والصلب في PerfMon
في PerfMon ضع العدّادات التالية على المحور الزمنيّ نفسه.
Process(<target>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<target>)\\Working Set - PrivateProcess(<target>)\\Private Bytes
يشمل Process\\Page Faults/sec الأخطاء الليّنة والصلبة معاً. أمّا Memory\\Pages Input/sec فهو عدد الصفحات المقروءة من القرص لحلّ أخطاء صلبة.10
في مرحلة Touch لهذا البرنامج ينبغي أن يقفز Page Faults/sec بينما لا يرتفع Pages Input/sec كثيراً. تُجسَّد الصفحات الملتزَمة حديثاً بـ demand-zero، لذا لا حاجة لقراءة بيانات أصليّة من القرص.
عندما تشترك عدّة عمليّات في الاسم نفسه، يمكن لأرقام PerfMon مثل process#1 أن تتغيّر عبر إعادة التشغيل. قابل مع عدّاد يعرض PID، أو عرّف بـ PID عبر Process V2 أو ETW/WPA.
11. ثلاث قراءات خاطئة تُتجنَّب في الممارسة
11.1. «ارتفع Commit، إذن هو تسرّب RAM»
Commit هو المقدار الموعود من المحتوى للحفظ؛ قد لا تكون الصفحات غير الملموسة مقيمة في RAM. للحكم على تسرّب انظر السلسلة الزمنيّة لـ Private Bytes وتفكيك التخصيصات وما إذا عاد الرقم إلى خطّ أساس بعد انتهاء المعالجة.
11.2. «Page Faults/sec مرتفع، إذن القرص بطيء»
الأخطاء الليّنة لا تتضمّن إدخال/إخراج قرص. افصل Page Faults/sec وPages Input/sec وانتظار التخزين، وإن لزم تتبّع ملفّ المصدر والمكدّس بأحداث ETW HardFault.
11.3. «إفراغ Working Set سيصلح التسرّب»
إزالة صفحة من Working Set لا تحرّر Commit ولا الملكيّة. تنتقل الصفحة إلى Standby أو Modified ثمّ تعود بخطأ لاحقاً. إصلاح التسرّب يتطلّب أن ينفّذ المخصّص VirtualFree أو تحرير كومة أو تدمير كائن وما شابه.
إلى أين تذهب تلك الصفحة المادّيّة المُزالة هو ما نتتبّعه في الجزء 2.
12. الخلاصة
- يحجز
MEM_RESERVEنطاق عناوين افتراضيّة لكنّه لا يسند منطقة مادّيّة في RAM أو ملفّ الصفحات.1 - يستهلك
MEM_COMMITCommit ويضمن أنّ المحتوى يمكن حفظه مستقبلاً، لكنّ صفحة مادّيّة عاديّة لا تُسنَد حتّى أوّل وصول.12 - VAD دفتر النطاقات، وPTE دفتر الصفحة الافتراضيّة، وقاعدة PFN دفتر الصفحة المادّيّة.
- إخفاق TLB ليس خطأ صفحة. إن كانت PTE صالحة، يكفي مشي جدول الصفحات لحلّه.
- Demand-zero واستعادة Transition وربط صفحة مشتركة أخطاء ليّنة تُحَلّ بلا إدخال/إخراج قرص.5
- إن لزم قراءة من ملفّ الصفحات أو DLL أو EXE أو ملفّ معيَّن، فهو خطأ صلب.6
- إن تعذّر فحص VAD وPTE وسمات الحماية حلّ الخطأ، حصلت على استثناء مثل
0xC0000005.7 - لحكم الأداء لا تنظر إلى
Page Faults/secوحده؛ انظرPages Input/secوAvailable وWorking Set وPrivate Bytes وانتظار التخزين على المحور الزمنيّ نفسه.
يتواصل في الجزء 2، «حياة الصفحة المادّيّة: خمس قوائم وحقيقة ملفّ الصفحات».
بعد تحويل وعد Commit إلى صفحة مادّيّة، نتتبّع إلى أين تذهب تلك الصفحة عندما تغادر Working Set، من قاعدة PFN وقوائم الصفحات.
مقالات ذات صلة
- What Does Windows’ “Memory Usage” Actually Mean? — Correctly Reading Working Set, Private Bytes, Commit, and the Page File
- The Depths of Windows I/O (Part 1) — Every Read and Write Becomes an IRP: The Big Picture of the I/O System
- The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
- قراءة ملفّات تفريغ الأعطال عبر WinDbg + SOS ── مدخل عمليّ للتحليل بعد الجمع
- جمع crash dumps لتطبيقات Windows: متى تبدأ بـ WER أو ProcDump أو WinDbg
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تحقيقات استخدام ذاكرة تطبيقات Windows وانتهاكات الوصول وتأخّر التشغيل والصفحات وعيوب الشيفرة الأصليّة.
- تطوير تطبيقات Windows
- التحقيق في الأخطاء وتحليل الأسباب الجذريّة
- ترحيل الأصول القائمة والاستفادة منها
- تواصل معنا
روابط مرجعيّة
-
Microsoft Learn, VirtualAlloc function. حول حجز
MEM_RESERVEنطاق عناوين افتراضيّة دون إسناد تخزين مادّيّ؛ واحتسابMEM_COMMITحصة التزام (commit charge) على ذاكرة النظام وملفّ الصفحات؛ وكون المحتوى الأوّلي لصفحة ملتزَمة صفراً؛ وعدم إسناد الصفحة المادّيّة الفعليّة حتّى يُوصَل إليها. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, PERFORMANCE_INFORMATION structure. حول كون
CommitTotalالعدد الحالي لصفحات Commit في النظام، وCommitLimitالحدّ الأعلى الذي يمكن التزامه دون توسيع ملفّ الصفحات. ↩ ↩2 -
Microsoft Learn, !vad (WinDbg). حول عرض
!vadشجرة VAD وتمكين فحص VPN البداية والنهاية وCommit وMapped/Private وسمات الحماية وControl Area وغيرها. ↩ -
Microsoft Learn, PageFault_TypeGroup1 class. حول تمييز ETW وتسجيله Transition Fault وDemand Zero Fault وCopy-on-Write وGuard Page Fault وHard Page Fault وAccess Violation. ↩
-
Microsoft Learn, Working Set. حول إمكان حلّ خطأ ليّن دون الوصول إلى مخزن خلفيّ، وحدوثه من Working Set لعمليّة أخرى وTransition وdemand-zero عند أوّل مرجع ونحو ذلك. ↩ ↩2 ↩3
-
Microsoft Learn, PageFault_HardFault class. حول تضمّن حدث HardFault FileObject وReadOffset وByteCount وVirtualAddress ومعرّف خيط، بحيث يمكن تتبّع مصدر القراءة. ↩ ↩2
-
Microsoft Learn, Access Violation C0000005. حول حدوث
0xC0000005عند قراءة أو كتابة أو تنفيذ عنوان ذاكرة غير صالح، وإشارة معاملات الاستثناء إلى نوع الوصول والعنوان المخالف. ↩ ↩2 -
Microsoft Learn, Creating Guard Pages. حول توفير
PAGE_GUARDإشعاراً لمرّة واحدة بوصول الصفحة ورفعهSTATUS_GUARD_PAGE_VIOLATION. ↩ -
Microsoft Learn, VMMap - Sysinternals. حول تفكيك VMMap الذاكرة الافتراضيّة الملتزَمة حسب النوع وعرض Working Set لكلّ نوع وخريطة عناوين مفصّلة. ↩
-
Microsoft Learn, Performance Analysis of Logs (PAL) Tool. حول كون
Memory\\Pages Input/secعدد الصفحات المقروءة من القرص لحلّ أخطاء صفحة صلبة. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
أعماق ذاكرة Windows (الجزء 2) — حياة الصفحة الفعلية: خمس قوائم وحقيقة ملف الصفحات
تربط هذه المقالة قاعدة PFN وStandby وModified وضغط الذاكرة وملف الصفحات لتشرح إلى أين تذهب الصفحة الفعلية بعد مغادرة Working Set.
أعماق ذاكرة Windows (الجزء 3) — كائنات القسم والنسخ عند الكتابة: حقيقة DLL وتعيين الملف
يربط المقال كائنات القسم وتعيين الصورة والبيانات والذاكرة المؤقتة المشتركة والنسخ عند الكتابة ليشرح كيف تتشارك DLL والذاكرة المشتركة الصف...
أعماق الافتراضيّة في Windows(الجزء 3)── آلات افتراضيّة تقلع في ثوانٍ: لماذا WSL2 وWindows Sandbox والحاويات خفيفة هكذا
لماذا يبدأ WSL2 وWindows Sandbox في ثوانٍ ويبدوان خفيفَين هكذا؟ يشرح هذا المقال الآليّات، من الصور الأساسيّة الديناميّة والـ direct map ع...
أعماق الافتراضيّة في Windows(الجزء 2)── ذاكرة لا تراها النواة: كيف يعمل VBS وHVCI وCredential Guard
عند تثبيت نظيف على عتاد متوافق، يُفعَّل VBS افتراضيّاً ويستخدم الـ hypervisor وSLAT لإنشاء عزل أقوى من النواة. يشرح هذا المقال بنية VTL و...
أعماق الافتراضيّة في Windows(الجزء 1)── أين يعمل Windows فعليّاً؟ الـ hypervisor والأقسام
عندما تفعِّل Hyper-V، يعمل Windows المضيف نفسه فوق الـ hypervisor بوصفه القسم الجذر. يشرح هذا المقال أسس الافتراضيّة عبر أدوار VT-x وSLAT...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل تمرير MEM_COMMIT إلى VirtualAlloc يخصّص RAM في تلك اللحظة؟
- في الذاكرة الخاصّة العاديّة يستهلك Commit هامش الالتزام في النظام، لكن الصفحة المادّيّة المقابلة لا تُسنَد حتّى أوّل وصول. الصفحة التي تُلمَس أوّل مرّة بكتابة تحصل على صفحتها المادّيّة أثناء معالجة خطأ demand-zero.
- هل يعني خطأ الصفحة أنّ هناك خللاً أو مشكلة أداء؟
- لا. الأخطاء الليّنة بلا إدخال/إخراج قرص — مثل demand-zero أو إعادة صفحة من Standby — تشغيل عاديّ. لحكم الأداء انظر لا إلى Page Faults/sec وحده بل أيضاً إلى Pages Input/sec وانتظار التخزين وAvailable MBytes.
- هل إخفاق TLB وخطأ الصفحة شيء واحد؟
- هما مختلفان. حتّى إن لم يكن في TLB ترجمة، إذا كانت PTE جدول الصفحات صالحة فإنّ المعالج يمشي الجدول ويعيد تسجيل الترجمة فحسب. ينتقل إلى مدخل خطأ الصفحة عندما تكون PTE غير صالحة أو هناك انتهاك حماية.
- إن كان نطاق العناوين في VAD، ألا يمكن أن يحدث انتهاك وصول؟
- ليس بالضرورة. فضلاً عن وجود VAD، يقيّم مدير الذاكرة Reserve مقابل Commit وحماية القراءة/الكتابة/التنفيذ وصفحات الحراسة وحالة PTE وغيرها. إن تعذّر حلّ الخطأ حصلت على استثناء مثل 0xC0000005.
- هل ارتفاع Page Faults/sec يعني أنّ النظام ينقصه RAM؟
- لا يمكن الجزم من ذلك وحده. Page Faults/sec يشمل أيضاً عدداً كبيراً من الأخطاء الليّنة. يلزم ربطه على المحور الزمنيّ نفسه مع Memory\Pages Input/sec وMemory\Page Reads/sec وAvailable MBytes وزمن انتظار القرص.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.