أعماق ذاكرة Windows (الجزء 3) — كائنات القسم والنسخ عند الكتابة: حقيقة DLL وتعيين الملف

· · Windows, إدارة الذاكرة, الذاكرة المشتركة, تعيين الملف, النسخ عند الكتابة, DLL, Cache Manager

في المقال السابق، «أعماق ذاكرة Windows (الجزء 2) — حياة صفحة ماديّة»، تتبعنا كيف تنتقل صفحة ماديّة تغادر Working Set عبر Modified وStandby وFree وZeroed. ذلك Standby يحمل أيضاً صفحات DLL وEXE والملفات المعيَّنة وذاكرة الملف المؤقتة.

هنا يبرز سؤال. حين تستخدم 100 عمليّة نفس kernel32.dll، هل يضع Windows 100 مجموعة صفحات شفرة في RAM؟ حين تعيّن عمليّتان نفس الملف في الذاكرة، أيمكن للأخرى أن تستخدم صفحة قرأتها إحداهما؟

الجواب هو تعيين عناوين افتراضيّة متعدّدة على نفس الصفحة الماديّة. المركز الذي يمثّل وحدة المشاركة تلك هو كائن القسم، والآليّة التي تفصل المشاركة عند الكتابة فقط هي النسخ عند الكتابة (Copy-on-Write، CoW).

يتتبع هذا المقال كيف تتعلّق EXE وDLL وملفات البيانات والذاكرة المشتركة المسنودة بملف الصفحات وذاكرة الملف المؤقتة بنفس تدفّق الملف، وأين تتفرّع مسارات المعالجة. قراءة الأرقام نفسها تفترض مقال المقدّمة «What Does Windows’ “Memory Usage” Actually Mean?».

«أعماق ذاكرة Windows» — الأجزاء الثلاثة

  1. الجزء 1: العناوين الافتراضيّة وأخطاء الصفحات
    نتبع لحظة حصول صفحة افتراضيّة ملتزمة على RAM ماديّة.
  2. الجزء 2: حياة صفحة ماديّة
    نتبع انتقالات حالة صفحة تغادر Working Set.
  3. الجزء 3 (هذا المقال): كائنات القسم والنسخ عند الكتابة
    نتبع آليّة تشارك DLL وتعيين الملف والذاكرة المشتركة للصفحات الماديّة.

السؤال الذي يجيب عنه الجزء 3 واحد.

لماذا تستطيع عمليّات متعدّدة استخدام نفس DLL أو الملف كمجموعة واحدة من الصفحات الماديّة؟

القرّاء المقصودون مطوّرون ومشغّلون يريدون فهم مشاركة DLL وCreateFileMapping وMapViewOfFile والذاكرة المشتركة وCoW وعلاقتها بذاكرة الملف المؤقتة لا كاستخدام API فحسب بل من البنية الداخليّة. المتطلّبات Windows 10/11 أو Windows Server الحاليّ، والخلفية اللازمة أساسيّات العناوين الافتراضيّة وأخطاء الصفحات وWorking Set. الصعوبة متوسّطة؛ نستخدم أيضاً مصطلحات داخليّة مثل Control Area وPrototype PTE، لكن يمكن إعادة الرصد بـ VMMap وProcess Explorer وQueryWorkingSetEx.

1. الخلاصة أوّلاً

في البداية، هذه الصورة الكلّيّة.

  • كائن القسم يمثّل نطاقاً ذاكريّاً قابلاً للمشاركة.
    كلّ عمليّة تعيّن جزءاً من ذلك القسم في فضائها الافتراضيّ كـ «عرض».1
  • عروض نفس القسم لا يلزم أن تكون في نفس العنوان الافتراضيّ.
    0x000001... للعمليّة A و0x000002... للعمليّة B يمكن أن يشيرا إلى نفس إزاحة القسم ونفس الصفحة الماديّة.2
  • هناك أقسام مسنودة بملف وأقسام مسنودة بملف الصفحات.
    الأولى تستخدم ملفاً حقيقيّاً؛ والثانية تُستخدم للذاكرة المشتركة بلا ملف صريح ونحو ذلك.2
  • تحميل EXE/DLL قسم صورة؛ تعيين الملف العاديّ قسم بيانات.
    مع SEC_IMAGE تحدّد سمات القسم داخل PE حماية الصفحة.3
  • أثناء القراءة يمكن مشاركة نفس الصفحة الماديّة.
    حين يكتب أحد الجانبين في صفحة CoW تُنسَخ تلك الصفحة فقط ويُستبدَل PTE العمليّة الكاتبة.4
  • حتّى على نفس تدفّق الملف تتفرّع مسارات الذاكرة المؤقتة والبيانات والصورة.
    إدخال/إخراج Cache Manager المخزَّن يستخدم SharedCacheMap، وتعيين البيانات يستخدم DataSectionObject، وEXE/DLL يستخدمان ImageSectionObject. الثلاثة تتعلّق بنفس تدفّق الملف عبر SECTION_OBJECT_POINTERS، لكن خطأ الصورة لا يمرّ بـ Cache Manager.5
  • Private Bytes وحدها لا تؤكّد حدوث CoW.
    FILE_MAP_COPY يحمّل Commit للعرض بأكمله مسبقاً، احتياطاً لخصخصة كلّ صفحة لاحقاً. للفحص صفحة بصفحة استخدم بت Shared في QueryWorkingSetEx.67

في جملة واحدة: ما يُشارَك ليس العنوان الافتراضيّ، بل المحتوى داخل القسم والصفحة الماديّة التي تقابله في تلك اللحظة.

2. كائنات القسم والعروض

في تعريف Microsoft يمثّل كائن القسم منطقة ذاكرة قابلة للمشاركة وهو أيضاً الآليّة التي تعيّن ملفاً في فضاء عناوين العمليّة.1

حيلة الفهم فصل القسم نفسه عن العرض الذي تراه كلّ عمليّة.

المفهوم الدور
كائن القسم يمثّل المحتوى للمشاركة والحجم ومخزن الإسناد وسقف الحماية
العرض يُظهر جزءاً من القسم كنطاق عناوين افتراضيّة في عمليّة ما
PTE يربط كلّ صفحة افتراضيّة في العرض بالصفحة الماديّة الحاليّة أو بحالة غير متحقّقة
PFN يمثّل صفحة ماديّة موجودة فعلاً في RAM

في Win32 يعيد CreateFileMapping مقبضاً لكائن تعيين ملف، وMapViewOfFile ينشئ عرضاً في الفضاء الافتراضيّ للعمليّة.3

HANDLE mapping = CreateFileMappingW(
    file,
    nullptr,
    PAGE_READONLY,
    0,
    0,
    nullptr);

void* view = MapViewOfFile(
    mapping,
    FILE_MAP_READ,
    0,
    0,
    0);

استدعاء CreateFileMapping وحده لا يعطيك بعد عنواناً تستطيع العمليّة قراءته. ثمّ إنّ إنشاء العرض لا يضع كلّ صفحة في RAM فوراً. من أوّل صفحة تُلمَس يقرأ خطأ صفحة محتوى الملف ويربط صفحة ماديّة بـ PTE.8 أي إنّ الترحيل عند الطلب الذي رأيناه في الجزء 1 ينطبق على عروض القسم أيضاً.

2.1. نفس القسم، عناوين افتراضيّة مختلفة

حتّى حين تعيّن العمليّة A والعمليّة B نفس إزاحة نفس القسم، يمكن أن يختلف عنوان بداية العرض.

تعيين عناوين افتراضيّة مختلفة على نفس الصفحة الماديّةللعمليّة A والعمليّة B عرض كلّ منهما في عنوان افتراضيّ مختلف، لكنّهما تصلان إلى نفس الصفحة الماديّة PFN X عبر نفس إزاحة القسمالعمليّة A: 0x000001A00000 + 0x3000إزاحة القسم 0x3000العمليّة B: 0x000002700000 + 0x3000نفس الصفحة الماديّة PFN X

الشكل 1: ما يُشارَك هو المحتوى داخل القسم والصفحة الماديّة، لا العنوان الافتراضيّ.

لذلك لا تخزّن مؤشّراً خاماً في الذاكرة المشتركة. قيمة مؤشّر العمليّة A قد تكون عنواناً غير ذي صلة في العمليّة B.

في بنية مشتركة استخدم إزاحة من بداية العرض وأعداداً صحيحة بعرض ثابت وإصداراً ومحاذاة صريحين. توثيق Microsoft لـ MapViewOfFileEx يوصي أيضاً بتخزين إزاحة من القاعدة لا مؤشّراً، إذ لا ضمان أن نفس العنوان سيكون متاحاً لاحقاً.6

3. المسنود بملف والمسنود بملف الصفحات

تنقسم الأقسام إلى نوعين كبيرين حسب موضع استعادة المحتوى.

كيف يتفرّع المسنود بملف والمسنود بملف الصفحاتتمرير ملف حقيقيّ إلى CreateFileMapping ينتج قسماً مسنوداً بملف، ويمكن إعادة قراءة صفحة نظيفة من الملف الأصليّ. تمرير INVALID_HANDLE_VALUE ينتج قسماً مسنوداً بملف الصفحات؛ ملف الصفحات يسند المحتوى الذي يختفي عند تدمير الكائنتمرير مقبض ملف حقيقيّتمرير INVALID_HANDLE_VALUECreateFileMappingقسم مسنود بملفقسم مسنود بملف الصفحاتيمكن إعادة قراءة صفحة نظيفة من الملف الأصليّملف الصفحات يسند المحتوى الذي يختفي عند التدمير

الشكل 2: فرق مخزن الإسناد يقرّر من أين يُستعاد المحتوى وكم يعيش.

3.1. الأقسام المسنودة بملف

تمرير ملف حقيقيّ إلى CreateFileMapping ينتج قسماً مسنوداً بملف.

  • عرض للقراءة فقط يقرأ الصفحات اللازمة من الملف.
  • تغييرات عرض القراءة/الكتابة تُعامل كبيانات لذلك الملف.
  • تغييرات عرض CoW لا تُكتب إلى الملف الأصليّ؛ تصبح صفحات خاصّة.

إن كانت صفحة مسنودة بملف نظيفة، يمكن التخلّص من الصفحة الماديّة وإعادة قراءتها من الملف الأصليّ. تلك الخاصّيّة تدعم كفاءة Standby وذاكرة الملف المؤقتة التي رأيناها في الجزء 2.

3.2. الأقسام المسنودة بملف الصفحات

تمرير INVALID_HANDLE_VALUE كـ hFile لـ CreateFileMapping وتحديد حجم ينتج قسماً مسنوداً بملف الصفحات.

HANDLE mapping = CreateFileMappingW(
    INVALID_HANDLE_VALUE,
    nullptr,
    PAGE_READWRITE,
    0,
    64 * 1024,
    L"Local\\KomuraMemoryDemo");

هذا قسم بلا ملف بيانات صريح، مسنود بملف الصفحات. المحتوى الأوّليّ أصفار، ويمكن لعمليّات متعدّدة فتح نفس الكائن عبر اسم أو وراثة مقبض أو DuplicateHandle ونحو ذلك.93 التغييرات مرئيّة للعمليّات التي تعيّن نفس الصفحة المشتركة. في المقابل، عند تدمير كائن القسم لا يبقى المحتوى، لذا لا يصلح لترك ملف دائم.2

تنبيه: الذاكرة المشتركة لا تأتي تلقائيّاً بالاستبعاد المتبادل. تصمّم قفلاً أو إشارة أو حدثاً أو بروتوكولاً بلا أقفال أو ما شابه على حدة.9

4. تعيين الصورة وتعيين البيانات

حين نقول «EXE أو DLL تعيين ملف أيضاً» تبقى الفروق عن ملف بيانات عاديّ قائمة.

البند تعيين الصورة تعيين البيانات
الاستخدام الرئيس تحميل EXE أو DLL ملفات عاديّة، بيانات مشتركة
سمة الإنشاء SEC_IMAGE PAGE_READONLY وPAGE_READWRITE ونحوهما
حماية الصفحة سمات داخل صورة PE تقرّر التعيين ومواصفة العرض تقرّران
الكتابات يمكن تخصيصها عبر قسم قابل للكتابة أو CoW يمكن اختيار كتابة مشتركة أو CoW
Type لـ VirtualQuery MEM_IMAGE MEM_MAPPED

مع SEC_IMAGE تقرّر سمات قسم الصورة المنفَّذة نفسها حماية صفحة العرض أكثر من قيمة الحماية العاديّة الممرَّرة إلى CreateFileMapping.3

بهذه الآليّة يمكن لصفحات غير معدّلة مثل الشفرة أن تشارك نفس الصفحة الماديّة عبر عمليّات كثيرة، ولا تتفرّع عبر CoW سوى الصفحات التي تحتاج تغييراً خاصّاً بالعمليّة. مع ذلك ليست كلّ صفحة DLL مشتركة بالضرورة — بسبب إعادة تموضع ASLR وتصحيحات المحمّل والترقيع الساخن وسمات أقسام PE الفعليّة ونحو ذلك.

التصميم المهمّ هو مشاركة الصفحات القابلة للمشاركة أوّلاً، ونسخ الصفحات التي تحتاج تغييراً كسولاً فقط.

5. النسخ عند الكتابة من البداية إلى النهاية

نتبع من حالة عمليّتين تقرآن نفس صفحة CoW حتّى تكتب العمليّة A بايتاً واحداً.

5.1. قبل الكتابة

قبل حدوث كتابة يصل PTE العمليّتين مفهوميّاً إلى نفس الصفحة المشتركة، وتنجح القراءات كما هي.

الحالة المشتركة قبل النسخ عند الكتابةقبل حدوث كتابة يصل PTE العمليّة A وPTE العمليّة B كلاهما إلى نفس الصفحة المشتركة PFN X، وتنجح القراءات كما هيPTE العمليّة APFN X مشترك(قراءة / نسخ عند الكتابة)PTE العمليّة B

الشكل 3: قبل الكتابة يشير PTE العمليّتين إلى نفس الصفحة الماديّة.

5.2. خطأ حماية عند الكتابة

صفحة CoW ليست من البداية صفحة مشتركة قابلة للكتابة عاديّة. حين تحاول العمليّة A الكتابة يرفع المعالج خطأ حماية. مدير الذاكرة الذي يتلقّى التحكّم يحكم أنّ هذه ليست كتابة غير قانونيّة بل كتابة إلى سمة CoW.

5.3. إنشاء صفحة ماديّة جديدة

على ذلك الحكم يفعل Windows ما يلي.

  1. الحصول على صفحة ماديّة واحدة للعمليّة A.
  2. نسخ محتوى PFN X إلى الصفحة الجديدة PFN Y.
  3. استبدال PTE العمليّة A بـ PFN Y.
  4. تغيير حماية العمليّة A إلى قراءة/كتابة عاديّة.
  5. إعادة تنفيذ تعليمة الكتابة التي فشلت.
حالة الانقسام بعد النسخ عند الكتابةبعد كتابة العمليّة A يُستبدَل PTE العمليّة A فقط بالصفحة الخاصّة PFN Y التي تلقّت نسخة المحتوى، بينما يستمر PTE العمليّة B في الإشارة إلى الصفحة المشتركة الأصليّة PFN Xنُسِخ عند الكتابةPTE العمليّة APFN Y خاصّ(R/W، بعد الكتابة)PTE العمليّة BPFN X مشترك(أصليّ)

الشكل 4: يُستبدَل PTE العمليّة الكاتبة فقط بصفحة خاصّة جديدة؛ الجانب الآخر يستمرّ في قراءة المحتوى الأصليّ.

تستمرّ العمليّة B في قراءة المحتوى الأصليّ ولا ترى تغيير العمليّة A. ذلك هو النسخ عند الكتابة. مشاركة DLL وFILE_MAP_COPY يستخدمان نفس المبدأ: لا تنسخ حتّى تكتب.46

5.4. الفرق عن FILE_MAP_WRITE

الصفحة المكتوبة عبر كتابة مشتركة بـ FILE_MAP_WRITE مصمَّمة بحيث يظهر تغيير أحد الجانبين أيضاً من عرض آخر يستخدم نفس تعيين الملف. مع FILE_MAP_COPY، بالمقابل، تصبح الصفحات المكتوبة فقط خاصّة بالعمليّة؛ لا تُكتب التغييرات إلى الملف الأصليّ وتضيع عند فكّ تعيين العرض.6

أتريد «إيصال تحديث عبر الذاكرة المشتركة»، أم «أن تجري كلّ عمليّة تغييرات خاصّة من بيانات أوّليّة مشتركة»؟ الاختيار الصحيح عكس الآخر حسب الغرض.

6. بعد CoW يبقى MEM_MAPPED / MEM_IMAGE

الصفحة بعد CoW أصبحت Private ماديّاً. قد تتوقّع عندئذ أن يتغيّر Type لـ VirtualQuery إلى MEM_PRIVATE أيضاً، لكن عرض البيانات يبقى MEM_MAPPED والصورة القابلة للتنفيذ تبقى MEM_IMAGE. VirtualQuery يبلّغ من أيّ تخصيص أوّليّ جاءت المنطقة.7

لرؤية ما إن حدث CoW بالفعل صفحة بصفحة، استخدم الإجراء التالي.

  1. أوصل إلى الصفحة المستهدفة واجعلها مقيمة.
  2. احصل على معلومات Working Set للصفحة بـ QueryWorkingSetEx.
  3. انظر بت Shared.
  4. إن كان Shared == 0 فتلك الصفحة المقيمة Private.

عند التأكيد في VMMap أيضاً انظر لا إلى Type المنطقة فحسب بل إلى تفصيل Private/Shareable لـ Working Set.

6.1. قد لا تزيد Private Bytes

مع FILE_MAP_COPY قد تكتب العمليّة لاحقاً كلّ صفحة في العرض. لذا يأخذ Windows تحميلاً لـ Commit يعادل العرض بأكمله عند التعيين.6 نتيجة ذلك أنّ كتابة الصفحة الأولى لا تزيد بالضرورة Private Bytes بمقدار 4KiB في تلك اللحظة.

المقاييس المفضّلة عند رصد CoW هي التالية.

  • بت Shared من QueryWorkingSetEx
  • Private WS / Shareable WS في VMMap
  • معلومات الصفحات الماديّة في RAMMap
  • Private Bytes كمعلومة مكمّلة

إن اعتبرت «هل زادت Private Bytes لحظة الكتابة» اختبار نجاح/فشل فستفوت CoW يعمل بشكل صحيح.

7. نقطة التماس مع Cache Manager — فصل ثلاثة مسارات

القول إنّ «تحميل EXE/DLL وذاكرة الملف المؤقتة والذاكرة المشتركة كلّها أقسام» يعطي الصورة الكلّيّة، لكن لا يجوز طيّ التنفيذ في كائن واحد.

لتدفّق الملف SECTION_OBJECT_POINTERS يستخدمها مدير الذاكرة وCache Manager.

typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject;
    PVOID SharedCacheMap;
    PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
  • DataSectionObject: حالة القسم لملف بيانات
  • SharedCacheMap: عرض الذاكرة المؤقتة الذي يتتبعه Cache Manager
  • ImageSectionObject: حالة القسم لصورة قابلة للتنفيذ

تشرح وثائق Microsoft أنّ هذا الهيكل يربط كائن ملف بأقسام تدفّق الملف ويتتبع المحتوى في الذاكرة ومعلومات الذاكرة المؤقتة.5

هنا تلتقي سلسلة الإدخال/الإخراج وسلسلة الذاكرة. افهم المسارات الثلاثة منفصلة مع ذلك.

  • ReadFile / WriteFile المخزَّنان يستخدمان SharedCacheMap وعرض الذاكرة المؤقتة لدى Cache Manager.
  • خطأ تعيين على ملف بيانات يعالجه مدير الذاكرة من جانب DataSectionObject. يتعاون مع الإدخال/الإخراج المخزَّن على نفس تدفّق الملف لإبقاء المحتوى متّسقاً.
  • خطأ صورة على EXE/DLL يعالجه مدير الذاكرة بـ ImageSectionObject وإدخال/إخراج الترحيل. ليس مساراً يمرّ بـ SharedCacheMap لدى Cache Manager.
ثلاثة مسارات تتعلّق بنفس تدفّق الملفReadFile/WriteFile المخزَّنان يستخدمان SharedCacheMap، وخطأ تعيين البيانات يستخدم DataSectionObject، وخطأ صورة EXE/DLL يستخدم ImageSectionObject؛ الثلاثة تتعلّق بنفس تدفّق الملف عبر SECTION_OBJECT_POINTERSReadFile / WriteFile المخزَّنانSharedCacheMapخطأ تعيين البياناتDataSectionObjectخطأ صورة EXE/DLLImageSectionObjectنفس تدفّق الملف(SECTION_OBJECT_POINTERS)

الشكل 5: المسارات الثلاثة تُعالَج منفصلة، لكنّها تتعلّق بنفس تدفّق الملف.

المشترك بين الثلاثة ليس أنّ «كلّ شيء يدخل Cache Manager»، بل أنّ نفس تدفّق الملف يربط حالات منفصلة — ذاكرة مؤقتة وقسم بيانات وقسم صورة — عبر SECTION_OBJECT_POINTERS.5 قراءات وكتابات الذاكرة المؤقتة وLazy Writer وعلاقة Cc بـ Mm مغطّاة في «The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?».

لاحظ أنّك حين تمزج عرضاً معيَّناً في الذاكرة مع ReadFile/WriteFile لا تُضمَن رؤية محتوى نفس اللحظة دائماً. يحتاج التصميم مزامنة وتفريغاً ووضع مشاركة الملف.36

8. عمر الكائن والعرض

إغلاق مقبض CreateFileMapping وحده لا يدمّر عرضاً قائماً. العرض يمسك مرجعاً داخليّاً إلى القسم، وفقط بعد UnmapViewOfFile لكلّ عرض وCloseHandle لكلّ مقبض يصبح الكائن قابلاً للتدمير.3

UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);

فصل الأعمار هذا سبب ظاهرة «أغلقت الملف لكنّه ما زال مستخدماً». حتّى بعد إغلاق مقبض الملف، إن كان قسم صورة أو عرض بيانات ما زال يشير إلى تدفّق الملف، يأتي الإغلاق النهائيّ للملف لاحقاً.

العلاقة مع التنظيف/الإغلاق في جانب الإدخال/الإخراج في «The Depths of Windows I/O (Part 1)»، ومطبّات التنفيذ في «المزالق وأفضل الممارسات عند استخدام shared memory».

9. انظر بنفسك

9.1. النظر إلى نفس DLL من عمليّتين

أوّلاً أكّد مشاركة DLL بعمليّات قائمة.

  1. ابدأ Process Explorer كمسؤول.
  2. ابدأ عمليّتي cmd.exe.
  3. اختر View > Lower Pane View > DLLs.
  4. أكّد مسار وتعيين نفس DLL في العمليّتين.
  5. افتح كلّ cmd.exe في VMMap وقارن Images Working Set وPrivate وShareable.

رؤية نفس DLL في Process Explorer دليل أنّ كليهما عيّن نفس الصورة. ذلك وحده مع ذلك لا يثبت تطابق PFN كلّ صفحة. اجمع تفصيل Shareable في VMMap وRAMMap وQueryWorkingSetEx لتأكيد المشاركة صفحة بصفحة. Process Explorer وVMMap يوفّرهما Sysinternals.1011

9.2. رصد FILE_MAP_COPY من عمليّتين

البرنامج التالي يعيّن نفس الملف كعرض CoW ويعرض بت Shared من QueryWorkingSetEx. يُنشأ كائن تعيين الملف بـ PAGE_READONLY، لكنّ تلك الحماية متوافقة مع عرض FILE_MAP_COPY، وأوّل كتابة من جانب العرض تسبّب 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);
}

ابنِ وأعدّ.

cl /std:c++20 /EHsc /W4 cow_demo.cpp

$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))

ثمّ افتح نفس الملف من وحدتي تحكّم.

.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write

بعد بدء كليهما اضغط Enter أوّلاً في جانب القراءة ثمّ في جانب الكتابة، وأكّد أنّ Shared مضبوط في before لدى كليهما. اضغط Enter مرّة أخرى في جانب الكتابة فتصبح صفحة تلك العمليّة Shared 0. ثمّ اضغط Enter في جانب القراءة وتؤكّد أنّ القارئ يستمرّ في قراءة الصفحة الأصليّة. يبقى Type لـ VirtualQuery MEM_MAPPED بعد الكتابة أيضاً.

لاحظ أنّ PrintPage يلمس الصفحة المستهدفة مجدّداً قبيل الاستعلام، وإن كان Valid == 0 لا يفسّر Shared وShareCount ويعيد المحاولة حتّى ثلاث مرّات. إن بقيت الصفحة غير مقيمة لا يُنتج نتيجة ويبلّغ بذلك. يمكن أن يتغيّر ShareCount بالتوقيت وضغط الذاكرة، فانظر تغيّر بت Shared المؤكَّد عند Valid == 1 لا قيمة ثابتة.

10. خمس قراءات خاطئة تُتجنَّب في العمل

10.1. «الذاكرة المشتركة تنتهي في نفس العنوان الافتراضيّ»

ما يُشارَك هو القسم والصفحة الماديّة. عنوان العرض الافتراضيّ يمكن أن يختلف لكلّ عمليّة، فخزّن إزاحة لا مؤشّراً خاماً.

10.2. «إن كان نفس DLL فكلّ صفحة مشتركة بالضرورة»

صفحات الشفرة النظيفة سهلة المشاركة، بينما إعادة التموضع وقسم قابل للكتابة وCoW والإقامة لحظة القياس تنتج أيضاً صفحات Private.

10.3. «بعد CoW يصبح MEM_PRIVATE»

Type لـ VirtualQuery يبقى MEM_MAPPED أو MEM_IMAGE. أكّد حالة المشاركة الفعليّة بـ QueryWorkingSetEx.7

10.4. «إن لم تزد Private Bytes فلم يحدث CoW»

FILE_MAP_COPY يحمّل Commit للعرض بأكمله مسبقاً. فضّل Private WS وبت Shared.6

10.5. «إن كانت الصفحة مشتركة فالمزامنة غير لازمة»

رؤية نفس الصفحة الماديّة والقدرة على تحديثها بأمان من نوى معالج متعدّدة مشكلتان مختلفتان. صمّم الذرّيّة وترتيب الذاكرة والاستبعاد المتبادل والحالة الوسيطة عند الانهيار وتوافق الإصدار.

فكرة تتبّع المراجع والعمر منفصلين تنطبق أيضاً على بقاء عمليّة بعد توافق COM لـ Excel. انظر أيضاً «مشكلة بقاء EXCEL.EXE عند التعامل مع Excel من C# ── أنماط تحرير مراجع COM وقرار الاستبدال».

11. الخلاصة

  • كائن القسم يمثّل نطاقاً ذاكريّاً قابلاً للمشاركة، وكلّ عمليّة تعيّنه في فضائها الافتراضيّ كعرض.1
  • نفس إزاحة نفس القسم تُعيَّن من عناوين افتراضيّة مختلفة على نفس الصفحة الماديّة.2
  • القسم المسنود بملف يسند ملفاً حقيقيّاً؛ والقسم المسنود بملف الصفحات يسند ذاكرة مشتركة مسمّاة ونحوها.9
  • يُعامَل EXE/DLL كقسم صورة والملف العاديّ كقسم بيانات؛ الحماية ووجهة إعادة الكتابة تختلفان.3
  • CoW يشارك الصفحة الماديّة أثناء القراءة، وعند أوّل كتابة ينسخ تلك الصفحة فقط ويستبدل PTE.4
  • بعد CoW ما زال VirtualQuery يعيد MEM_MAPPED/MEM_IMAGE، فتؤكّد ببت Shared في QueryWorkingSetEx.7
  • مع FILE_MAP_COPY يُحمَّل Commit للعرض بأكمله أوّلاً، فلا يمكن الحكم على CoW من Private Bytes وحدها.6
  • إدخال/إخراج Cache Manager المخزَّن وتعيين البيانات وتعيين الصورة تتعلّق بنفس تدفّق الملف كمسارات منفصلة تستخدم SharedCacheMap وDataSectionObject وImageSectionObject على التوالي.5
  • تصبح الذاكرة المشتركة آمنة فقط حين تضمّن عمر العروض والمقابض والمزامنة وقوائم ACL وتصميم الإزاحات.

بذلك تكتمل الأجزاء الثلاثة من «أعماق ذاكرة Windows». تحجز/تلتزم عنواناً افتراضيّاً، وتحصل على صفحة ماديّة عبر خطأ صفحة، وتنقل الصفحة من Working Set إلى قائمة صفحات، وتشاركها عبر قسم، وتفصل عبر CoW الصفحات التي كتبتها فقط — إدارة ذاكرة Windows متّصلة كجريان واحد.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع تحقيقات عيوب الذاكرة المشتركة وتعيين الملفات وتحميل DLL وقفل الملفات والتواصل بين العمليّات واستخدام الذاكرة في تطبيقات Windows.

روابط مرجعيّة

  1. Microsoft Learn, Section Objects and Views. عن كائن قسم يمثّل نطاقاً ذاكريّاً قابلاً للمشاركة، وعن كلّ عمليّة تعيّن جزءاً من القسم كعرض.  2 3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections. عن الأقسام المسنودة بملف وبملف الصفحات وCoW وإمكان مشاركة نفس الذاكرة الماديّة من عناوين افتراضيّة لعمليّات مختلفة.  2 3 4

  3. Microsoft Learn, CreateFileMappingW function. عن كائن تعيين الملف والأقسام المسنودة بملف الصفحات وSEC_IMAGE وعمر العروض والمقابض واتّساق العروض التي تسند نفس الملف.  2 3 4 5 6 7 8

  4. Microsoft Learn, Memory Protection. عن تشارك عمليّات متعدّدة صفحات DLL الماديّة نفسها، وعن نسخ CoW إلى صفحة ماديّة جديدة وتحديث PTE حين يكتب أحد الجانبين.  2 3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. عن ربط DataSectionObject وSharedCacheMap وImageSectionObject تعيين تدفّق الملف ومعلومات الذاكرة المؤقتة بمدير الذاكرة / Cache Manager.  2 3 4

  6. Microsoft Learn, MapViewOfFileEx function. عن CoW مع FILE_MAP_COPY؛ والصفحات الخاصّة المسنودة بملف الصفحات؛ وتحميل Commit للعرض بأكمله؛ وتخزين إزاحة لا عنواناً افتراضيّاً.  2 3 4 5 6 7 8

  7. Microsoft Learn, VirtualQuery function. عن بقاء Type MEM_MAPPED/MEM_IMAGE بعد CoW، وإمكان تأكيد التخصيص ببت Shared في QueryWorkingSetEx 2 3 4

  8. Microsoft Learn, Managing Memory Sections. عن عدم تعيين ذاكرة ماديّة حتّى يُوصَل إلى العرض، وعن خطأ صفحة أوّل وصول الذي يقرأ محتوى الملف. 

  9. Microsoft Learn, Sharing Files and Memory. عن مشاركة نفس كائن تعيين الملف بالاسم أو المقبض؛ وإنشاء ذاكرة مشتركة مسنودة بملف الصفحات بـ INVALID_HANDLE_VALUE؛ وضرورة المزامنة على حدة.  2 3

  10. Microsoft Learn, Process Explorer - Sysinternals. عن قدرة Process Explorer على عرض مقابض العمليّة وDLL / الملفات المعيَّنة في الذاكرة المحمَّلة. 

  11. Microsoft Learn, VMMap - Sysinternals. عن تفصيل VMMap للذاكرة الافتراضيّة إلى Image وMapped File وPrivate ونحوها، وعرض تفصيل Private/Shareable لـ Working Set. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

هل يحصل كلّ عمليّة تستخدم نفس DLL على نسخة كاملة من ذلك DLL في RAM؟
عادة لا. الصفحات غير المعدّلة من نفس الصورة تُعيَّن من عناوين افتراضيّة مختلفة لكلّ عمليّة إلى نفس الصفحة الماديّة. الصفحات التي تحتاج كتابة فقط تصبح صفحات ماديّة خاصّة بالعمليّة، عبر النسخ عند الكتابة وما شابه.
هل يخصّص CreateFileMapping ذاكرة للعمليّة في تلك اللحظة؟
CreateFileMapping ينشئ كائن تعيين ملف، لكن MapViewOfFile هو الذي يجعله مرئيّاً في الفضاء الافتراضيّ للعمليّة. ثمّ إنّ الصفحات الماديّة للعرض تتجسّد عادة بخطأ صفحة بدءاً من أوّل صفحة يُوصَل إليها.
ما الفرق بين FILE_MAP_WRITE وFILE_MAP_COPY؟
التغيير عبر FILE_MAP_WRITE كتابة تنعكس على جانب بيانات الملف المشتركة. FILE_MAP_COPY يشارك الصفحات الأوّليّة، لكن الصفحات المكتوبة فقط تصبح نسخاً خاصّة بالعمليّة؛ لا تُكتب التغييرات إلى الملف الأصليّ وتضيع عند فكّ تعيين العرض.
بعد النسخ عند الكتابة، هل يعيد VirtualQuery القيمة MEM_PRIVATE؟
لا. عرض البيانات يبقى MEM_MAPPED وعرض الصورة يبقى MEM_IMAGE. لمعرفة إن خُصِّصت الصفحة فعلاً، اجعل الصفحة مقيمة وانظر بت Shared في QueryWorkingSetEx.
هل يجوز تخزين مؤشّر خام في الذاكرة المشتركة؟
عادة لا. حتّى مع نفس القسم لا ضمان أن عرض كلّ عمليّة يوضع في نفس العنوان الافتراضيّ. في بنية مشتركة استخدم إزاحات من القاعدة وأعداداً صحيحة بعرض ثابت وتخطيطاً ومخطّط مزامنة صريحين.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة