مزالق shared memory وأفضل الممارسات في العمل

· آخر تحديث: · · Shared Memory, IPC, Concurrency, C++, C#, تطوير Windows

سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240862)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621414)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). مزالق shared memory وأفضل الممارسات في العمل. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621414 https://comcomponent.com/ar/blog/2026/03/18/000-shared-memory-pitfalls-best-practices/

DOI (أحدث نسخة)
10.5281/zenodo.21621414
DOI (هذه النسخة)
10.5281/zenodo.22279836

إطارات صور، نتائج فحص، سجلات زمنية، بيانات لوحة، buffers ضخمة. عندما نريد تبادل بيانات كبيرة بزمن استجابة منخفض داخل الجهاز نفسه، يبدو shared memory جذاباً جداً.

غير أن ما هو خطر قليلاً هنا أن shared memory يقترب بوجه «IPC سريع». في الواقع، shared memory هو «IPC يتيح تقليل النسخ، وفي المقابل يدفع مسؤولية الاتساق إلى جهة التطبيق».

  • سريع
  • مرن
  • لكن الـ protocol من صنعك أنت
  • وحين تقع الأعطال تكون الأعراض صارخة

عادة تأتي هذه الأربع معاً.

الوجهان لـ shared memoryمخطّط يبيّن أن shared memory يقترب بوجه IPC سريع، بينما هو في الواقع IPC يتيح تقليل النسخ ويدفع مسؤولية الاتساق إلى جهة التطبيق.وجه «IPC سريع»الشكل الفعلييمكن تقليل النسخمسؤولية الاتساق على التطبيقالـ protocol من صنعك، والأعراض صارخة عند العطل

الشكل 1: يقترب shared memory بوجه «IPC سريع»، ويدفع مسؤولية الاتساق إلى جهة التطبيق.

في هذه المقالة نرتّب، مع أخذ file mapping في Windows و shm_open / mmap في POSIX في الحسبان، نقاط الاحتكاك عند استخدام shared memory في العمل، والتصميم الذي يخفض معدل الأعطال. سواء في C/C++ أو في MemoryMappedFile على C#، الجوهر تقريباً واحد.1

القارئ المستهدف والافتراضات

المقالة موجّهة إلى مطوّر يحسم الآن تصميماً لتمرير بيانات كبيرة بين عمليات على الجهاز نفسه. الافتراض الأساسي من يلمس file mapping في Windows أو shm_open في POSIX مباشرة بـ C / C++، لكن من يدخل من MemoryMappedFile في C# يطأ المزالق نفسها. فصول المزالق ومبادئ التصميم (الفصل 5 والفصل 6) لا تعتمد على اللغة.

عيّنات تعمل موجودة في 6.9، ونعرض C (Windows / MSVC) و C# كليهما. جهة POSIX نبيّن فقط مقابل أسماء API في جدول الفصل 7.

مصطلحات نثبّتها مسبقاً

في المتن تظهر بعض المصطلحات بالإنجليزية كما هي. نجمعها هنا حتى لا تتعثر عند أول ظهور.

المصطلح المعنى
IPC (Inter-Process Communication) اتصال بين العمليات. الآليات عامة لتبادل بيانات أو إشارات مع عملية أخرى. يشمل pipe و socket و named pipe و shared memory وغيرها
coherent أن عدة views تشير إلى الكيان نفسه تبدو بالمحتوى نفسه في اللحظة نفسها. ليس بمعنى «يستطيع القارئ دائماً قراءة سجل محدّث ومتسق»
ABI (Application Binary Interface) اتفاق على مستوى ثنائي تلتزم به الملفات التنفيذية، لا الشيفرة المصدرية. يشمل حجم النوع و alignment و padding وترتيب الحقول في البنية
SPSC / MPSC / SPMC / MPMC اختصارات لعدد المنتجين والمستهلكين. S = single، M = multi، P = producer، C = consumer. SPSC يعني كاتباً واحداً وقارئاً واحداً. نوسّعها في 4.2
lock-free بناء يتقدم بعمليات atomic فقط دون أخذ قفل. يشير إلى ضمان تقدّم «خيط واحد على الأقل يستطيع التقدم دائماً»، وهي صفة غير «السرعة»
sentinel قيمة خاصة محجوزة لتعني «غير صالح» أو «نهاية». مثل الاتفاق أن UINT64_MAX في offset تعني غير صالح
NUMA (Non-Uniform Memory Access) تكوين تختلف فيه مسافة الذاكرة كما يراها المعالج. لمس ذاكرة عقدة بعيدة يبطئ الكود نفسه بشكل ملحوظ

1. الخلاصة أولاً (بجملة)

بصيغة خشنة جداً لكنها مفيدة في العمل، الأمر هكذا.

  • shared memory آلية تعرض سلسلة البايتات نفسها على عدة عمليات، وليست المزامنة نفسها23
  • السرعة تظهر مع البيانات الكبيرة عند تبادلها داخل الجهاز نفسه. أما الرسائل الصغيرة للتحكم فقط، فغالباً ما يكون pipe / socket / named pipe / queue أسهل بكثير
  • في shared memory، أن يكون مرئياً و أن يُقرأ بأمان مسألتان منفصلتان
  • لا تجعل volatile أساس التصميم. الذرية، والترتيب، والانتظار تُعالج كلٌّ على حدة45
  • إذا وضعت مؤشراً خاماً، أو HANDLE، أو file descriptor، أو std::string، أو std::vector، أو std::mutex كما هي، فستندم لاحقاً غالباً
  • البيانات التي توضع في shared memory أسلم أن تُمال نحو أعداد صحيحة بعرض ثابت + تخطيط صريح + ترويسة بإصدار
  • مجرد وضع magic / version / size / state / generation / heartbeat في الترويسة الأمامية يغيّر كثيراً سهولة التحقيق في الأعطال
  • صعوبات shared memory ليست السرعة، بل التهيئة، والعمر، والاستعادة، والأذونات، وABI
  • في Windows الهيكل هو CreateFileMapping / OpenFileMapping / MapViewOfFile، وفي POSIX shm_open / ftruncate / mmap63
  • الأقل عرضة للأعطال أن تبدأ بـ SPSC (single-producer single-consumer) ring buffer أو double buffer

باختصار، shared memory سريع، لكن استخدامه بخشونة يصيب صاحبه بـ «وهم أن المزامنة تحدث وحدها». تجنّب هذا هو رهان البداية.

الفرق بين أن يكون مرئياً وأن يُقرأ بأمانمخطّط يبيّن أن shared memory يعرض سلسلة البايتات نفسها على عدة عمليات وليس المزامنة نفسها، وأن الرؤية والقراءة الآمنة مسألتان منفصلتان.آلية تعرض سلسلة البايتات نفسهاليست المزامنة نفسهاأن يكون مرئياًأن يُقرأ بأمانيُصمَّمان كمسألتين منفصلتين

الشكل 2: في shared memory تصبح «الرؤية» و«القراءة الآمنة» مسألتين منفصلتين.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 24، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. ماذا يشارك shared memory، وماذا لا يشارك

shared memory، بصورة عامة، آلية تربط الصفحة الفيزيائية نفسها إلى فضاءات العناوين الافتراضية لعدة عمليات. في Windows نستخدم file mapping object و view، وفي POSIX نجري mmap على shared memory object.273

المهم هنا نقطتان.

  1. ما يُشارك هو سلسلة بايتات المحتوى، وليس العنوان الافتراضي نفسه
  2. أن يكون coherent و أن يكون متزامناً مسألتان مختلفتان
الصفحة الفيزيائية نفسها تُرى عبر views مختلفةمخطّط يبيّن أن shared memory يربط الصفحة الفيزيائية نفسها إلى فضاءات العناوين الافتراضية لعدة عمليات، وأن المشارَك هو سلسلة البايتات لا العنوان الافتراضي نفسه.view العملية Aالصفحة الفيزيائية نفسهاview العملية Bالمشارَك سلسلة البايتات، لا العنوان الافتراضي

الشكل 3: المشارَك سلسلة بايتات الصفحة الفيزيائية نفسها، لا العنوان الافتراضي نفسه.

في وثائق Windows أيضاً، الـ views المشتقة من file mapping object نفسه تكون coherent في اللحظة نفسها. لكن ذلك لا يعني أن القارئ يستطيع دائماً قراءة سجل محدّث ومتسق.8

مثلاً،

  • يقصد الكاتب كتابة length
  • ثم payload
  • ثم ready flag

بهذا الترتيب. لكن القارئ إن قرأ بلا أي مزامنة، فقد يرى length جديداً مع payload قديماً مركّبين. shared memory لا يصلح هذا تلقائياً.

أي أن ما يشاركه shared memory هو البايتات. وما لا يشاركه هو المعنى، والترتيب، وإشعار الاكتمال، وسياسة الاستعادة. هذه كلها تحتاج تصميماً من جهتنا.

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

الشكل 4: البايتات تُشارك، أما المعنى والترتيب وإشعار الاكتمال وسياسة الاستعادة فنصمّمها بأنفسنا.

3. المشاهد التي يناسبها shared memory / والتي لا يناسبه

المشهد يناسب / لا يناسب السبب
تمرير إطارات أو buffers كبيرة داخل الجهاز نفسه يناسب يسهل تقليل عدد النسخ
قيم حسّاس عالية التكرار، صور، صوت، بيانات لوحة يناسب يسهل استهداف زمن استجابة منخفض ومعدل نقل عالٍ
تبادل أوامر وردود صغيرة فقط لا يناسب كثيراً تكلفة المزامنة للتحكم تصبح ثقيلة نسبياً
التبادل مع جهاز آخر لا يناسب shared memory يفترض أساساً المضيف نفسه
تعايش طويل للغات وإصدارات مختلفة صعب يلزم تصميم ABI وإصدارات
يلزم أيضاً الاستدامة حسب الغرض file-backed mapping خيار قوي، لكن يسهل اختلاط مسؤوليات الاستدامة و IPC

في العمل، فصل التحكم عبر الرسائل، والجسم عبر shared memory قوي جداً. مثلاً،

  • إشعار من عملية UI إلى عملية worker بـ «استخدم الإطار التالي» يتم عبر event / pipe / socket
  • جسم الإطار الفعلي عبر shared memory

هذا التكوين. وهذا غالباً أكثر سلاماً.

التحكم عبر الرسائل، والجسم عبر shared memoryمخطّط يبيّن تكويناً يفصل إشعار «استخدم الإطار التالي» من عملية UI إلى عملية worker عبر event أو pipe أو socket، ويمرّر جسم الإطار عبر shared memory.إشعار (event / pipe / socket)كتابة جسم الإطارقراءة جسم الإطارعملية UIعملية workershared memory

الشكل 5: تكوين يرسل الإشعار عبر الرسائل ويضع جسم الإطار فقط في shared memory.

4. أربعة أمور يجب حسمها أولاً

عند تصميم shared memory، ما يجب حسمه أولاً هذه الأربعة.

أربعة أمور تُحسم أولاًمخطّط يبيّن أربعة بنود تُحسم أولاً عند تصميم shared memory: فصل الـ planes، ونموذج التزامن، والمالك والعمر، وABI والإصدار.تصميم shared memoryفصل الـ planesنموذج التزامنالمالك والعمرABI والإصدار

الشكل 6: في بداية التصميم نحدد الفصل ونموذج التزامن والمالك والعمر وABI.

4.1 افصل control plane عن data plane

احسم أولاً ماذا يوضع في shared memory.

  • data plane: صور، صوت، صفوف سجلات، بيانات بالجملة
  • control plane: بدء، إيقاف، خطأ، إعادة اتصال، إعادة تهيئة، إشعار

فصل هذين وحدهما يبسّط تصميم جهة shared memory كثيراً.

4.2 ضيّق نموذج التزامن

  • SPSC: منتج واحد / مستهلك واحد
  • MPSC: عدة كتّاب / مستهلك واحد
  • SPMC: كاتب واحد / عدة قرّاء
  • MPMC: عدة كتّاب / عدة قرّاء

الصعوبة ترتفع تقريباً بهذا الترتيب. لا يُنصح بالذهاب إلى MPMC من البداية. تصبح مطالباً بالتعامل مع الإقصاء المتبادل بين الكتّاب وترتيب الذاكرة في الوقت نفسه، فتظهر لاحقاً أعطال يصعب تكرارها في الاختبار.

صعوبة نماذج التزامنمخطّط يبيّن أن الصعوبة ترتفع تقريباً من SPSC إلى MPSC ثم SPMC ثم MPMC، وأن الذهاب إلى MPMC من البداية يعني مواجهة الإقصاء المتبادل وترتيب الذاكرة معاً.SPSC (كتابة واحدة وقراءة واحدة)MPSC (كتابات متعددة وقراءة واحدة)SPMC (كتابة واحدة وقراءات متعددة)MPMC (كتابات متعددة وقراءات متعددة)مواجهة الإقصاء المتبادل وترتيب الذاكرة معاً

الشكل 7: صعوبة نموذج التزامن ترتفع تقريباً بهذا الترتيب من SPSC إلى MPMC.

4.3 حدّد المالك والعمر

  • من ينشئ
  • من يهيّئ
  • من يحذف
  • إذا سقط مشارك في الوسط، من يستعيد

إذا غامت هذه، يتغير السلوك مع ترتيب الإقلاع وإعادة التشغيل، ويصعب فصل السبب.

4.4 حدّد ABI والإصدار

  • التخطيط
  • حجم النوع
  • alignment
  • منطقة reserved
  • version / feature flags
  • وجود التوافق من عدمه

shared memory ليس حديث API بل حديث ABI (binary interface). إذا عولج هذا بخشونة، يقع عطل مزعج: توافق مصدري موجود بينما ينكسر وقت التشغيل فقط.

shared memory حديث ABIمخطّط يبيّن أن shared memory اتفاق ثنائي لا API، فإذا عولج بخشونة ينكسر وقت التشغيل رغم التوافق المصدري.shared memoryاتفاق ABI لا APIإذا عولج بخشونةينكسر وقت التشغيل رغم التوافق المصدري

الشكل 8: shared memory اتفاق ABI، ومعالجته بخشونة تكسره وقت التشغيل حتى مع توافق مصدري.

5. مزالق شائعة

5.1 عدم المزامنة

هذا الأكثر شيوعاً.

«نرى الذاكرة نفسها، إذن ما يُكتب يُقرأ»

قد يُقرأ. لكن ذلك لا يعني القراءة في التوقيت الصحيح، وبالوحدة الصحيحة، وبالترتيب الصحيح.

في Windows و POSIX على السواء، يُفترض أن يُجمع الوصول إلى shared memory مع وسيلة مزامنة أخرى. شرح Windows أيضاً يطلب التنسيق على الوصول إلى view مشترك بـ mutex / semaphore / event وغيرها.2 وشرح POSIX أيضاً يقول إن الوصول إلى shared memory يحتاج مزامنة.9

مزلق «ما يُكتب يُقرأ»مخطّط يبيّن أنه حتى لو أمكنت القراءة لأن الذاكرة نفسها مرئية، فضمان التوقيت والوحدة والترتيب الصحيح مسألة أخرى، وأن الوصول إلى shared memory يفترض الجمع مع وسيلة مزامنة.«ما يُكتب يُقرأ»قد يُقرأالتوقيت والوحدة والترتيب الصحيح شيء آخريفترض الجمع مع وسيلة مزامنةmutex / semaphore / event وغيرها

الشكل 9: إمكان القراءة شيء، والقراءة بالوحدة والترتيب الصحيحين شيء آخر، ووسيلة المزامنة مفترضة.

5.2 محاولة تدبير الأمر بـ volatile

volatile ليس سحراً ينقذ تصميم shared memory. على الأقل atomicity و mutual exclusion مسألتان منفصلتان.45

مثلاً تصميم يضع volatile bool ready; ويراقب بـ busy loop:

  • يهدر CPU
  • يغيم ضمان ترتيب payload و ready
  • غير قابل للنقل
  • يسهل التقاط حالة وسط الكتابة

تقريباً لا شيء جيد فيه.

علاوة على ذلك، WaitOnAddress في Windows موجّه إلى threads داخل العملية نفسها. لا يُعتمد كآلية انتظار بين العمليات.10

مشكلات التصميم الذي يراقب بـ volatileمخطّط يبيّن أن مراقبة volatile bool بـ busy loop تهدر CPU وتغيم ضمان الترتيب وتسهل التقاط حالة وسط الكتابة، لذا لا تُجعل أساس التصميم.مراقبة volatile bool بـ busy loopإهدار CPUضمان الترتيب غائميسهل التقاط حالة وسط الكتابةلا تُجعل أساس التصميمWaitOnAddress أيضاً موجّه إلى thread داخل العملية

الشكل 10: busy loop على volatile غير مواتٍ في ثلاث نقاط: CPU والترتيب وحالة وسط الكتابة.

5.3 جعل القارئ يقرأ حالة وسط الكتابة

مظهر العطل في shared memory عادي جداً.

  • الترويسة وحدها جديدة
  • الـ payload وحده قديم
  • الطول وحده محدّث
  • زوج حقلين مكسور

إذا رُسم، فطريقة وقوع العطل بسيطة. يدخل القارئ في الفجوة قبل أن ينتهي الكاتب من كتابة length و payload.

عملية القارئshared memoryعملية الكاتبعملية القارئshared memoryعملية الكاتبlength جديد والـ payload لا يزال قديماً«الترويسة وحدها جديدة» يمسك حالة وسط الطريقكتابة 1024 في lengthقراءة length1024قراءة payload بطول 1024 بايتمحتوى الجيل السابقكتابة payloadرفع ready flag

الشكل 11: يدخل القارئ في الفجوة قبل أن ينتهي الكاتب من payload، فيمسك حالة وسط الكتابة.

هذه الفجوة موجودة حتماً ما دامت كتابة length و payload «ليست عملية واحدة غير قابلة للتجزئة». تحديث scalar واحد بشكل atomic أبسط نسبياً، أما نشر سجل من عدة حقول فيحتاج إجراء commit.

نموذجياً أحد التالي.

  • الحماية كلها بـ mutex
  • double buffer ثم تبديل «رقم الـ buffer الساري الآن» في النهاية
  • ring buffer مع state / sequence لكل slot
  • إن كان كاتباً واحداً وعدة قرّاء، أخذ snapshot بـ sequence counter

حتى «رفع ready flag في النهاية» وحده لا يكفي كتصميم ما لم تُحدد بأي ترتيب ذاكرة يُكتب / يُقرأ ذلك الـ flag. في shared memory، لحظة النشر نفسها هي الـ protocol.

5.4 وضع مؤشرات أو كائنات معقّدة كما هي

هذا أيضاً نمط متكرر.

  • مؤشر خام
  • HANDLE
  • file descriptor
  • std::string
  • std::vector
  • std::unordered_map
  • std::mutex
  • CRITICAL_SECTION

نمط يضع هذه في shared memory كما هي ويحاول استخدامها من عملية أخرى. في عملية القارئ، شبه المؤكد انتهاك وصول أو قيمة بلا معنى.

السبب بسيط: العنوان الافتراضي وموارد process-local لا معنى لها إلا في سياق تلك العملية. وحتى view في Windows، إذا رُبط الـ mapping نفسه في عملية أخرى، ليس مضموناً تطابق العنوان الافتراضي.711

لذا إن احتجت إشارة، الأساس حملها كـ offset من العنوان الأساسي.

typedef struct ShmRef {
    uint64_t offset;   // セグメント先頭からの相対位置
    uint32_t length;
    uint32_t kind;
} ShmRef;

عندها تستطيع كل عملية التحويل إلى عنوانها بـ base + offset.

الإشارة بـ offset لا بالمؤشرمخطّط يبيّن أن العنوان الافتراضي وموارد process-local لا معنى لها إلا في سياق تلك العملية، لذا تُحمل الإشارة كـ offset من العنوان الأساسي وتحلّها كل عملية بـ base+offset.بدلاً من ذلكوضع مؤشر خام أو HANDLEقيمة بلا معنى في عملية أخرىالحمل كـ offsetكل عملية تحلّ بـ base+offset

الشكل 12: الإشارة ليست مؤشراً خاماً بل offset من العنوان الأساسي.

5.5 انكسار ABI

shared memory اتفاق ثنائي لا شيفرة مصدرية. أي أن كل الفروق التالية تؤثّر.

  • حجم int / long
  • تمثيل bool
  • underlying type لـ enum
  • حجم wchar_t
  • فرق 32bit / 64bit
  • #pragma pack
  • فرق المترجم / اللغة
  • alignment / padding
  • little-endian / big-endian

داخل المضيف نفسه غالباً ما تتوافق endianness، لكن مجرد دخول دعم ARM64 أو mixed toolchain يكفي لإزاحة شائعة جداً.

لذا نوصي بقوة للبنى الموضوعة في shared memory بما يلي.

  • أعداد صحيحة بعرض ثابت مثل uint32_t / uint64_t
  • padding / reserved صريح
  • في الترويسة version, header_size, record_size, total_size
  • إن لزم static_assert(sizeof(...))
  • عدم وضع كائن غير trivial
الفروق التي تكسر ABI وطريقة المنعمخطّط يبيّن أن فروق حجم النوع و alignment و pack و padding و 32bit/64bit تكسر الاتفاق الثنائي، فتُمنع بالميل إلى أعداد بعرض ثابت وتخطيط صريح وترويسة بإصدار.فرق حجم النوعينكسر الاتفاق الثنائيفرق pack أو paddingفرق 32bit / 64bitأعداد بعرض ثابت + تخطيط صريحوضع version و size في الترويسة

الشكل 13: فروق حجم النوع و padding تكسر ABI، فنميل إلى أعداد بعرض ثابت وتخطيط صريح.

5.6 سباق التهيئة

shared memory ينكسر بسهولة بوهم «الجانب الذي أنشأ هوّأ حتماً».

في Windows، إذا صادف CreateFileMapping اسماً موجوداً يعيد الكائن القائم، ويُعرف ذلك بـ GetLastError() = ERROR_ALREADY_EXISTS. الصفحات الأولى لـ mapping مدعوم بملف الصفحة تبدأ بأصفار.8 في POSIX، shared memory object الجديد طوله في البداية 0، ويُعطى حجماً بـ ftruncate. البايتات المخصصة حديثاً تُهيّأ بأصفار. الإنشاء بـ O_CREAT | O_EXCL ذري.3

من دون معرفة هذا الفرق،

  • الاستخدام فور open
  • بلا علامة اكتمال التهيئة
  • مشاركون يهيّئون في الوقت نفسه
  • عدم النظر إلى version mismatch

ينكسر حسب ترتيب الإقلاع.

كحد أدنى، ضع هذه الحالات في الترويسة الأمامية.

  • INITIALIZING
  • READY
  • BROKEN

ثم المنشئ وحده يهيّئ، والمنضم ينتظر READY. هذه الممارسة وحدها تهدّئ العالم كثيراً.

كيف نتجنّب سباق التهيئةمخطّط يبيّن تجنّب سباق التهيئة بوضع حالات INITIALIZING و READY و BROKEN في الترويسة، وتهيئة المنشئ وحده ورفع READY، وانتظار المنضم READY قبل الاستخدام.المنشئ ينشئالمنشئ وحده يهيّئيجعل الحالة READYالمنضم لا يستخدم فور openينتظر READYيبدأ الاستخدام

الشكل 14: المنشئ وحده يهيّئ، والمنضم ينتظر READY في الترويسة ثم يستخدم.

5.7 عدم التفكير في استعادة الانهيار

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

mutex في Windows، إذا انتهى خيط المالك دون release، يصبح abandoned، ويستلم جانب الانتظار WAIT_ABANDONED. المعنى: المورد المشترك قد يكون في حالة غير محددة.12 ومع robust mutex في POSIX أيضاً، عند موت المالك يعود EOWNERDEAD، وبعد الإصلاح يُستدعى pthread_mutex_consistent().1314

المهم ألا «نواصل مؤقتاً» هنا. للاستعادة يلزم أحد التالي على الأقل.

  • رقم generation
  • sequence آخر commit مكتمل
  • heartbeat
  • dirty / clean flag
  • commit على مرحلتين يشبه journal
  • إجراء إعادة تهيئة كاملة عند التلف
عندما يسقط الكاتب أثناء التحديثمخطّط يبيّن أنه إذا انتهى المالك دون release يعود WAIT_ABANDONED في Windows و EOWNERDEAD في robust mutex على POSIX، فالمورد المشترك قد يكون غير محدد، فلا نواصل مؤقتاً بل نتجه إلى وسيلة الاستعادة.يسقط الكاتب أثناء التحديثWAIT_ABANDONED (Windows)EOWNERDEAD (POSIX robust)المورد المشترك قد يكون في حالة غير محددةلا نواصل مؤقتاًgeneration / heartbeat / commit على مرحلتين / إعادة تهيئة

الشكل 15: إذا سقط المالك نشتبه بحالة غير محددة ونتجه إلى إجراء الاستعادة دون مواصلة.

5.8 false sharing وتنافس سطر الـ cache

يُقال غالباً إن shared memory سريع. لكن إذا اكتظت عدّادات hot في سطر cache واحد، يتنقل السطر بين المعالجات فيبطئ بما لا يُتجاهل.

المثال النموذجي:

  • المنتِج يحدّث write_index
  • المستهلِك يحدّث read_index
  • كلاهما على سطر cache نفسه

هذا النوع.

في هذه الحالة يكفي:

  • فصل الحقول hot إلى سطر cache آخر
  • فصل الحقول عالية التحديث عن منخفضة التحديث
  • الوعي بأن كاتباً واحداً لسطر cache واحد

ليتغيّر الأمر كثيراً. كثيراً ما يُذكر المحاذاة على 64 بايت، لكن انظر إليها بروح أن 64 بايت قيمة شائعة في كثير من المعالجات وليست قانوناً مطلقاً.

المثال النموذجي لـ false sharing والعلاجمخطّط يبيّن أن write_index الذي يحدّثه المنتِج و read_index الذي يحدّثه المستهلِك إذا وقعا على سطر cache واحد يتنقل السطر بين المعالجات فيبطئ، لذا تُفصل الحقول hot إلى سطر آخر.المنتِج يحدّث write_indexسطر cache نفسهالمستهلِك يحدّث read_indexالسطر يتنقل بين المعالجات فيبطئفصل الحقول hot إلى سطر cache آخر

الشكل 16: عدّادات hot على سطر cache واحد تبطئ، فنفرّقها إلى أسطر أخرى.

5.9 الاستخفاف بالاسم والأذونات والأمان

named shared memory مريح، لكن خشونة الاسم والأذونات تسبب أعطالاً.

في Windows،

  • توجد مساحتا أسماء Global\ و Local\
  • لإنشاء file mapping في Global\ من خارج session 0 إنشاءً جديداً يلزم SeCreateGlobalPrivilege
  • اسم الكائن يشترك في مساحة الأسماء مع event / semaphore / mutex / waitable timer / job

هذه العادات موجودة.1582

أي أن مشاهد من قبيل:

  • تظن أن "Global\\MyApp" يكفي للمشاركة بين خدمة وتطبيق سطح مكتب
  • لكن يفشل بسبب الأذونات
  • بل إن mutex بالاسم نفسه أُنشئ أولاً فيصير ERROR_INVALID_HANDLE

طين شديد الـ Windows-ية يظهر.

طين مساحة الأسماء Globalمخطّط يبيّن أن استخدام اسم Global للمشاركة بين خدمة وتطبيق سطح مكتب قد يفشل بسبب غياب SeCreateGlobalPrivilege، أو يصطدم بـ mutex بالاسم نفسه موجود مسبقاً في مساحة الأسماء المشتركة.نريد المشاركة باسم Globalفشل بسبب الأذوناتاصطدام بـ mutex بالاسم نفسهالإنشاء الجديد يحتاج SeCreateGlobalPrivilegeيصير ERROR_INVALID_HANDLE

الشكل 17: مساحة الأسماء Global سهلة الوقوع في نوعين من الطين: الأذونات واصطدام الأسماء.

وفي جهة POSIX أيضاً، الاستخفاف بـ mode أو umask في shm_open يجعله مرئياً أوسع مما يلزم، أو بالعكس يتعذر فتحه.3

shared memory ليس آمناً لمجرد أنه ذاكرة. من عملية تملك صلاحية القراءة يظهر المحتوى بصراحة. إن وضعت معلومات سرية، فكّر في سياق paging / swap / dump / الأذونات كما تفعل مع الذاكرة العادية.

5.10 خشونة تغيير الحجم والترقية

«نريد توسيعه قليلاً لاحقاً» طلب خطر نسبياً في shared memory.

  • لكائن mapping في Windows حجم عند الإنشاء8
  • وفي POSIX أيضاً إن لم تُراعَ مواءمة ftruncate و mmap، لا يتوافق طول الـ map لدى المشاركين316

في العمل أسلم أن يكون الحجم ثابتاً داخل ذلك الجيل. إن لزم التوسيع،

  1. إنشاء segment بإصدار / اسم / generation جديد
  2. تبديل المشاركين
  3. إغلاق الـ segment القديم

يخفض معدل الأعطال أكثر.

توسيع الحجم بتبديل الجيلمخطّط يبيّن أن حجم shared memory يُثبَّت داخل الجيل، وإن لزم التوسيع يُنشأ segment لجيل جديد ويُبدَّل المشاركون ثم يُغلق القديم، فهذا يخفض معدل الأعطال أكثر من resize in place.نريد التوسيع لاحقاًإنشاء segment لجيل جديدتبديل المشاركينإغلاق الـ segment القديمresize in place خطر

الشكل 18: الحجم ثابت داخل الجيل، والتوسيع يتم بتبديل إلى segment جيل جديد.

5.11 حشر الإشعار أيضاً كله في shared memory

الشائع:

  • ready = 1 في shared memory
  • الطرف الآخر while (!ready) Sleep(1);

هذا يعمل في البداية. لكن لاحقاً يعود بالشكل التالي.

  • إهدار CPU
  • تذبذب زمن الاستجابة بسبب Sleep(1)
  • صعوبة ملاحظة الفوات
  • صعوبة كتابة مهلة أو إشعار إنهاء بشكل نظيف

الأفضل إمالة shared memory إلى سطح البيانات، وتوجيه الإشعار إلى primitive يمكن انتظاره.

  • Windows: event / semaphore / mutex / named pipe وغيرها217
  • POSIX: semaphore / mutex مشترك بين العمليات + condvar وغيرها1819
توجيه الإشعار إلى primitive يمكن انتظارهمخطّط يبيّن أن مراقبة علم في shared memory بـ Sleep تهدر CPU وتذبذب زمن الاستجابة وتفوّت أحداثاً، لذا يُمال shared memory إلى سطح البيانات ويُوجَّه الإشعار إلى primitive يمكن انتظاره مثل event أو semaphore.مراقبة ready flag بـ Sleepإهدار CPU وتذبذب زمن الاستجابةصعوبة ملاحظة الفواتالإشعار إلى primitive يمكن انتظارهفي Windows event / semaphore وغيرهافي POSIX semaphore / condvar وغيرها

الشكل 19: لا نراقب علماً، بل نستلم الإشعار بـ primitive يمكن انتظاره.

5.12 الظن أن «بهذا نشارك مع جهاز آخر أيضاً»

تأتي لحظة تظن فيها أن ربط ملف مشترك عبر الشبكة بـ file-backed mapping يكفي لعمل يشبه shared memory مع جهاز آخر.

هذا خطر.

حتى شرح CreateFileMapping في Windows يقول إن coherence غير مضمون تجاه remote file. إذا ربط جهازان الصفحة نفسها للكتابة، لا يرى كل منهما إلا كتابته، ولا يُدمَج عند تحديث القرص.8

shared memory أساساً آلية داخل المضيف نفسه. إذا عبرت الأجهزة، فاختيار socket / RPC / message broker بصراحة أبقى على الرشد.

لا نشارك مع جهاز آخرمخطّط يبيّن أن ربط ملف مشترك عبر الشبكة لا يضمن coherence لـ remote file، وأن shared memory آلية داخل المضيف نفسه، فإن عبرت الأجهزة نختار socket أو RPC أو message broker.نريد المشاركة بربط remote filecoherence غير مضمونshared memory آلية داخل المضيف نفسهإلى socket / RPC / message broker

الشكل 20: shared memory آلية داخل المضيف نفسه، فإن عبرت الأجهزة نختار عائلة الرسائل.

6. أفضل الممارسات

6.1 افصل control plane عن data plane

إذا أنزلنا الفصل المحسوم في 4.1 إلى تخصيص على مستوى التنفيذ يصبح كالتالي (للشكل الفاشل راجع 5.11).

  • shared memory: frame, sample, batch, snapshot
  • event / semaphore / pipe / socket: ready, consumed, stop, error, reconnect

هذا الفصل يحسّن وضوح التصميم قبل الأداء.

6.2 ضع ترويسة ثابتة في المقدمة

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

typedef struct SharedHeader {
    uint32_t magic;
    uint16_t abi_version;
    uint16_t header_size;

    uint32_t state;          // 0=initializing, 1=ready, 2=broken
    uint32_t flags;

    uint64_t total_size;
    uint64_t generation;
    uint64_t heartbeat_ns;

    uint64_t payload_offset;
    uint64_t payload_size;

    uint64_t write_seq;
    uint64_t read_seq;

    uint8_t  reserved[64];
} SharedHeader;

النقاط هي:

  • magic يستبعد شيئاً آخر أو غير مهيّأ
  • abi_version و header_size يستبعدان فرق التخطيط
  • state يستبعد وسط التهيئة
  • generation يكشف إعادة الإنشاء
  • heartbeat يرى الحياة
  • reserved يفتح مهرباً لتوسيع مستقبلي

.

ما يشق في shared memory هو «صعوبة رؤية ما يحدث». لذلك نحمل metadata للرصد من البداية.

دور كل حقل في الترويسة الثابتةمخطّط يبيّن توزيع الأدوار: magic يستبعد شيئاً آخر أو غير مهيّأ، و abi_version مع header_size يستبعدان فرق التخطيط، و state يستبعد وسط التهيئة، و generation يكشف إعادة الإنشاء، و heartbeat يرى الحياة.ترويسة ثابتة في المقدمةmagic يستبعد شيئاً آخرversion يستبعد الفرقstate يستبعد الوسطتصير metadata للرصدgeneration يكشف إعادة الإنشاءheartbeat يرى الحياة

الشكل 21: كل حقل في الترويسة الثابتة يستبعد على حدة: شيئاً آخر، أو فرقاً، أو وسط تهيئة.

6.3 اجعل الإشارة بـ offset

الإشارة تُحمل كـ offset لا كمؤشر.

  • تُحل بـ base + offset
  • يُدخل فحص نطاق offset + length
  • يُحدد sentinel للقيمة غير الصالحة

هذا وحده يقلل كثيراً أعطال عائلة عدم تطابق العنوان.

6.4 ضيّق نموذج التزامن

من النماذج الأربعة في 4.2، ما ينبغي اختياره أولاً أحدهما.

  • SPSC ring buffer
  • snapshot بكاتب واحد وعدة قرّاء

SPSC ring buffer بنية تكتب فيها المنتِج عند موضع write_seq في مصفوفة slots ثابتة الطول، ويقرأ المستهلِك من موضع read_seq. الكاتب قارئ واحد لكل منهما، فاتجاه التقدم أحادي.

ring buffer بـ 8 slotsعند النهاية يعود إلى slot 0slot 0 (انتهت قراءته)slot 1 (انتهت قراءته)slot 2 (لم يُقرأ)slot 3 (لم يُقرأ)slot 4 (قيد الكتابة)slot 5 (فارغ)slot 6 (فارغ)slot 7 (فارغ)المستهلِك read_seq = 2 يتقدم بعد انتهاء القراءةالمنتِج write_seq = 4 يتقدم بعد انتهاء الكتابة

الشكل 22: بنية SPSC ring buffer. المنتِج والمستهلِك يتقدمان بفهارس منفصلة في اتجاه واحد.

النقطة ألا تُكسر ترتيب «التقدم بالفهرس بعد انتهاء الكتابة» و «التقدم بالفهرس بعد انتهاء القراءة». وwrite_seq و read_seq كما في 5.8 يوضعان على سطري cache مختلفين.

إن لزم عدة كتّاب،

  • الـ enqueue وحده lock-free / atomic
  • تحديث البيانات الفعلية يُجمع إلى مستهلك واحد

مثل هذا، تقليل نقاط مسؤولية الاتساق ينجح غالباً.

6.5 صرّح ببروتوكول commit

تصميم لا تستطيع شرح «من أي لحظة يجوز القراءة» بجملة خطر.

مثلاً في double buffer:

  1. الكتابة إلى الـ buffer غير المنشور
  2. تثبيت المجموع الاختباري أو الطول
  3. تبديل فهرس الـ buffer الساري مع release
  4. القارئ يقرأ الفهرس الساري مع acquire
  5. بعد انتهاء القراءة يتحقق أن الفهرس لم يتغير

هكذا تُحدد طقوس النشر. إذا رُسم، يتضح أن لحظة التبديل موضع واحد فقط.

القارئالـ buffer Bactive indexالـ buffer Aالكاتبالقارئالـ buffer Bactive indexالـ buffer Aالكاتبactive index هو Aactive index هو Bيُهمل المحتوى المقروء ويُعاد من Bالقراءة مع acquireAقراءة الـ buffer Aالكتابة إلى B غير المنشورتثبيت الطول والمجموع الاختباريالتبديل إلى B مع releaseبعد القراءة إعادة التحقق من الفهرستغيّر إلى B

الشكل 23: طقوس نشر double buffer. القارئ يعيد التحقق من active index بعد انتهاء القراءة.

إذا أُسقط إجراء «إعادة التحقق من الفهرس بعد انتهاء القراءة»، يعيد الكاتب استخدام الـ buffer نفسه للكتابة التالية بينما القارئ لا يزال يقرأ، فتصير الحالة الوسط نفسها التي في 5.3. وبما أن الوجهين اثنان فقط، قد يحدث تبديل آخر أثناء إعادة القراءة، فإن كان التحديث سريعاً تُزاد عدد الأوجه أو يُمال إلى أسلوب sequence counter المذكور في 5.3.

6.6 ثبّت الحجم لكل جيل

بدلاً من resize in place،

  • name = MyShm.v3
  • abi_version = 3
  • generation = 42

قطع الأجيال أسهل في الصيانة.

shared memory لا يفحص الأنواع عند الاستدعاء كما يفعل API. لذلك عدم كسر ABI حُسم مرة مهم.

6.7 أدخل قابلية الرصد

كحد أدنى، وجود ما يلي يساعد.

  • وقت آخر تحديث
  • آخر sequence ناجح
  • عدد drop / overwrite
  • عدد version mismatch
  • عدد attach / detach
  • last error code
  • heartbeat

عندما ينكسر shared memory، تكون السجلات عادة رقيقة. وضع counters بنفسك يسهّل التعامل مع الأعطال كثيراً.

6.8 اصنع اختبارات المسار غير الطبيعي أولاً

المسار الطبيعي وحده لا يكفي. انظر على الأقل إلى التالي.

  • إنهاء قسري أثناء تحديث الكاتب
  • امتلاء الحلقة بسبب تأخير القارئ
  • اتصال مع version mismatch
  • اختلاط 32bit / 64bit
  • open عبر الجلسات
  • نقص الأذونات
  • إعادة تشغيل بينما عملية سابقة لا تزال تحمل جيلاً قديماً
  • أثر cache miss / NUMA عند نقل بيانات ضخمة متواصل

في shared memory قيمة اختبار طرق الكسر أكبر من المسار الطبيعي.

6.9 أصغر عيّنة ذهاب وإياب

نُنزل الممارسات حتى هنا إلى أصغر تكوين يعمل. تكوين واحد: كتلة بتخطيط ثابت على file mapping مدعوم بملف الصفحة في Windows، والإشعار بحدثين auto-reset.

الاتفاقات المشتركة هذه الأربعة.

  • الكتلة أعداد صحيحة بعرض ثابت ومصفوفات ثابتة الطول فقط. لا مؤشر ولا HANDLE
  • في المقدمة magic / abi_version / block_size / state
  • تثبيت الطول بعد انتهاء كتابة الجسم ثم رفع event
  • اسم الـ event مختلف عن file mapping (في Windows تشترك event / semaphore / mutex / waitable timer / job / file mapping في مساحة الأسماء)815
ذهاب وإياب العيّنة الصغرىمخطّط يبيّن ذهاباً وإياباً ينهي فيه المرسل التهيئة ثم يكتب الجسم ويثبّت الطول ويرفع event، وينتظر المستقبل الـ event ثم يتحقق من ABI والحالة ويفحص نطاق الطول ويقرأ، ويرد بالترتيب نفسه.المستقبلالكتلة المشتركةالمرسلالمستقبلالكتلة المشتركةالمرسلالتهيئة وجعل الحالة READYتثبيت الطول بعد انتهاء كتابة الجسمرفع event الطلبالتحقق من magic / version / stateفحص نطاق الطول ثم القراءةالرد أيضاً يثبّت الطول بعد كتابة الجسمرفع event الرد

الشكل 24: ذهاب وإياب العيّنة الصغرى. يُثبَّت الطول بعد انتهاء كتابة الجسم ثم يُشعَر.

أولاً المرسل.

/* shm_writer.c : المرسل. شغِّل هذا أوّلاً.
 *   cl /W4 /nologo shm_writer.c        (kernel32.lib يُربَط افتراضياً) */
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define SHM_NAME  L"Local\\KsShmDemo.v1.Block"
#define EVT_REQ   L"Local\\KsShmDemo.v1.Request"
#define EVT_REP   L"Local\\KsShmDemo.v1.Reply"
#define SHM_MAGIC 0x314D4853u   /* قيمة 'S','H','M','1' مرتَّبة little-endian */
#define SHM_ABI   1u
#define STATE_INITIALIZING 0u
#define STATE_READY        1u

#pragma pack(push, 8)
typedef struct DemoBlock {
    uint32_t magic;
    uint32_t abi_version;
    uint32_t block_size;
    uint32_t state;
    uint32_t request_len;
    uint32_t reply_len;
    char     request[256];
    char     reply[256];
} DemoBlock;          /* 24 + 256 + 256 = 536 بايت */
#pragma pack(pop)

int main(void)
{
    HANDLE hMap = NULL, hReq = NULL, hRep = NULL;
    DemoBlock *blk = NULL;
    uint32_t len = 0;
    DWORD waited = 0;
    int rc = 1;

    /* 1. أنشئ mapping مدعوماً بـ pagefile. الصفحات الابتدائيّة تبدأ بأصفار.
     *    إن وُجد الاسم نفسه، CreateFileMappingW «ينجح ويعيد الموجود»،
     *    ففحص NULL وحده لا يرفض مرسلاً ثانياً. إن مضيت إلى memset أدناه
     *    مسحت الكتلة المشتركة للطرف العامل.
     *    GetLastError() يُضبَط حتى عند النجاح، فاقرأه فوراً. */
    hMap = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
                              0, (DWORD)sizeof(DemoBlock), SHM_NAME);
    if (hMap == NULL) {
        printf("CreateFileMapping failed: %lu\n", GetLastError());
        goto cleanup;
    }
    if (GetLastError() == ERROR_ALREADY_EXISTS) {
        printf("%ls مستخدَم سلفاً. المرسل واحد في الوقت نفسه فقط\n", SHM_NAME);
        goto cleanup;
    }

    blk = (DemoBlock *)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(DemoBlock));
    if (blk == NULL) {
        printf("MapViewOfFile failed: %lu\n", GetLastError());
        goto cleanup;
    }

    /* 2. event للإشعار. اسم مختلف عن mapping */
    hReq = CreateEventW(NULL, FALSE, FALSE, EVT_REQ);   /* auto-reset / غير مُشار */
    hRep = CreateEventW(NULL, FALSE, FALSE, EVT_REP);
    if (hReq == NULL || hRep == NULL) {
        printf("CreateEvent failed: %lu\n", GetLastError());
        goto cleanup;
    }

    /* 3. من أنشأ فقط يُهيِّئ. state يُرفَع أخيراً */
    memset(blk, 0, sizeof(*blk));
    blk->magic       = SHM_MAGIC;
    blk->abi_version = SHM_ABI;
    blk->block_size  = (uint32_t)sizeof(DemoBlock);
    blk->state       = STATE_INITIALIZING;
    MemoryBarrier();
    blk->state = STATE_READY;

    /* 4. لا تُخِل بترتيب الجسم → الحاجز → الطول → الإشعار */
    strcpy_s(blk->request, sizeof(blk->request), "ping from writer");
    MemoryBarrier();
    blk->request_len = (uint32_t)strlen(blk->request);
    if (!SetEvent(hReq)) {
        printf("SetEvent failed: %lu\n", GetLastError());
        goto cleanup;
    }

    /* 5. انتظر الردّ. لا تنتظر بلا نهاية */
    waited = WaitForSingleObject(hRep, 5000);
    if (waited == WAIT_TIMEOUT) {
        printf("لا ردّ من reader\n");
        goto cleanup;
    }
    if (waited != WAIT_OBJECT_0) {
        printf("WaitForSingleObject failed: %lu\n", GetLastError());
        goto cleanup;
    }

    /* 6. افحص نطاق الطول قبل القراءة */
    len = blk->reply_len;
    if (len > sizeof(blk->reply)) {
        printf("reply_len خارج النطاق: %u\n", len);
        goto cleanup;
    }
    printf("reply: %.*s\n", (int)len, blk->reply);
    rc = 0;

cleanup:
    /* إغلاق كلّ view وhandle يُزيل الاسم أيضاً. لا تسقط قبل reader */
    if (hRep != NULL) CloseHandle(hRep);
    if (hReq != NULL) CloseHandle(hReq);
    if (blk  != NULL) UnmapViewOfFile(blk);
    if (hMap != NULL) CloseHandle(hMap);
    return rc;
}

المستقبل يستبدل main فقط بعد جعل التعريفات من SHM_NAME حتى DemoBlock مطابقة تماماً للمرسل. في العمل يُقطع هذا الجزء المشترك إلى ملف ترويسة.

/* shm_reader.c : المستقبل. تعريفات الثوابت وDemoBlock مطابقة لـ shm_writer.c.
 *   cl /W4 /nologo shm_reader.c */
int main(void)
{
    HANDLE hMap = NULL, hReq = NULL, hRep = NULL;
    DemoBlock *blk = NULL;
    uint32_t len = 0;
    int rc = 1;

    hMap = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, SHM_NAME);
    if (hMap == NULL) {
        printf("OpenFileMapping failed: %lu / هل شغَّلت writer؟\n", GetLastError());
        goto cleanup;
    }

    blk = (DemoBlock *)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(DemoBlock));
    if (blk == NULL) {
        printf("MapViewOfFile failed: %lu\n", GetLastError());
        goto cleanup;
    }

    hReq = CreateEventW(NULL, FALSE, FALSE, EVT_REQ);
    hRep = CreateEventW(NULL, FALSE, FALSE, EVT_REP);
    if (hReq == NULL || hRep == NULL) {
        printf("CreateEvent failed: %lu\n", GetLastError());
        goto cleanup;
    }

    /* 1. انتظر الإشعار أوّلاً. writer لا يرفع إلّا بعد انتهاء التهيئة */
    if (WaitForSingleObject(hReq, 5000) != WAIT_OBJECT_0) {
        printf("لم يأتِ request\n");
        goto cleanup;
    }

    /* 2. تحقّق من ABI وstate قبل اللمس */
    if (blk->magic != SHM_MAGIC || blk->abi_version != SHM_ABI ||
        blk->block_size != (uint32_t)sizeof(DemoBlock)) {
        printf("ABI غير متطابق: magic=%08X abi=%u size=%u\n",
               blk->magic, blk->abi_version, blk->block_size);
        goto cleanup;
    }
    if (blk->state != STATE_READY) {
        printf("التهيئة لم تنتهِ بعد: state=%u\n", blk->state);
        goto cleanup;
    }

    /* 3. افحص نطاق الطول ثم اقرأ */
    len = blk->request_len;
    if (len > sizeof(blk->request)) {
        printf("request_len خارج النطاق: %u\n", len);
        goto cleanup;
    }
    printf("request: %.*s\n", (int)len, blk->request);

    /* 4. الجسم → الحاجز → الطول → الإشعار بنفس ترتيب writer */
    strcpy_s(blk->reply, sizeof(blk->reply), "pong from reader");
    MemoryBarrier();
    blk->reply_len = (uint32_t)strlen(blk->reply);
    if (!SetEvent(hRep)) {
        printf("SetEvent failed: %lu\n", GetLastError());
        goto cleanup;
    }
    rc = 0;

cleanup:
    if (hRep != NULL) CloseHandle(hRep);
    if (hReq != NULL) CloseHandle(hReq);
    if (blk  != NULL) UnmapViewOfFile(blk);
    if (hMap != NULL) CloseHandle(hMap);
    return rc;
}

MemoryBarrier موضوع لمنع إعادة ترتيب تجعل الطول أو العلم يظهران قبل انتهاء كتابة الجسم.5 إن أردت إسقاط انحراف التخطيط وقت البناء، أضف /std:c11 إلى MSVC وثبّت sizeof(DemoBlock) بـ static_assert من <assert.h>.

التعامل مع الكتلة نفسها من جهة C# يكون كالتالي. النقطة التصريح عن الـ offsets كثوابت، مكتوبة حتى لا تنزاح بايتاً واحداً عن بنية جهة C. MemoryMappedFile و EventWaitHandle المسماة خاصة بـ Windows.1

// .NET 8 / Windows. المرسل: dotnet run -- write، والمستقبل: dotnet run -- read
using System.IO.MemoryMappedFiles;
using System.Text;

const string MapName = "Local\\KsShmDemo.v1.Block";
const string ReqName = "Local\\KsShmDemo.v1.Request";
const string RepName = "Local\\KsShmDemo.v1.Reply";
const uint Magic = 0x314D4853;   // 'S','H','M','1'
const uint Abi = 1;
const int BlockSize = 536;
const int MaxBody = 256;

// ثبِّت التخطيط نفسه لـ DemoBlock جهة C بثوابت الإزاحة
const int OffMagic = 0, OffAbi = 4, OffBlockSize = 8, OffState = 12;
const int OffRequestLen = 16, OffReplyLen = 20, OffRequest = 24, OffReply = 280;

bool isWriter = args.Length > 0 && args[0] == "write";

// المرسل يستخدم CreateNew. CreateOrOpen يفتح الكتلة التي يعمل عليها مرسل ثانٍ
// كما هي، ويهيِّئها أدناه فيكسر تبادل الطرف.
// CreateNew يرمي IOException إن وُجد الاسم، فتتنبّه هناك
// (نفس قصد الحكم على ERROR_ALREADY_EXISTS جهة C)
using var mmf = isWriter
    ? MemoryMappedFile.CreateNew(MapName, BlockSize)
    : MemoryMappedFile.OpenExisting(MapName);
using var view = mmf.CreateViewAccessor(0, BlockSize);
using var reqEvent = new EventWaitHandle(false, EventResetMode.AutoReset, ReqName);
using var repEvent = new EventWaitHandle(false, EventResetMode.AutoReset, RepName);

if (isWriter)
{
    view.Write(OffMagic, Magic);
    view.Write(OffAbi, Abi);
    view.Write(OffBlockSize, (uint)BlockSize);
    Thread.MemoryBarrier();
    view.Write(OffState, 1u);              // READY

    byte[] request = Encoding.UTF8.GetBytes("ping from C#");
    view.WriteArray(OffRequest, request, 0, request.Length);
    Thread.MemoryBarrier();
    view.Write(OffRequestLen, (uint)request.Length);
    reqEvent.Set();

    if (!repEvent.WaitOne(TimeSpan.FromSeconds(5)))
    {
        Console.WriteLine("لا ردّ من reader");
        return 1;
    }
    return PrintBody(OffReplyLen, OffReply, "reply");
}

if (!reqEvent.WaitOne(TimeSpan.FromSeconds(5)))
{
    Console.WriteLine("لم يأتِ request");
    return 1;
}
if (view.ReadUInt32(OffMagic) != Magic || view.ReadUInt32(OffAbi) != Abi
    || view.ReadUInt32(OffBlockSize) != BlockSize || view.ReadUInt32(OffState) != 1u)
{
    Console.WriteLine("ABI غير متطابق، أو التهيئة لم تنتهِ بعد");
    return 1;
}
if (PrintBody(OffRequestLen, OffRequest, "request") != 0)
{
    return 1;
}

byte[] reply = Encoding.UTF8.GetBytes("pong from C#");
view.WriteArray(OffReply, reply, 0, reply.Length);
Thread.MemoryBarrier();
view.Write(OffReplyLen, (uint)reply.Length);
repEvent.Set();
return 0;

int PrintBody(int lenOffset, int bodyOffset, string label)
{
    uint length = view.ReadUInt32(lenOffset);
    if (length > MaxBody)
    {
        Console.WriteLine($"{label} طوله خارج النطاق: {length}");
        return 1;
    }
    byte[] body = new byte[length];
    view.ReadArray(bodyOffset, body, 0, body.Length);
    Console.WriteLine($"{label}: {Encoding.UTF8.GetString(body)}");
    return 0;
}

إصدارا C و C# يحملان الاسم نفسه والتخطيط نفسه، فيتم الذهاب والإياب حتى لو كان أحدهما مرسلاً والآخر مستقبلاً. هذا معنى تثبيت ABI.

هذه العيّنة عمداً ذهاب وإياب واحد فقط. للنقل المتواصل انتقل إلى ring buffer في 6.4، ولتحمل الإنهاء غير الطبيعي للكاتب انتقل إلى generation و heartbeat في 5.7.

7. نقاط ننظر إليها في Windows و POSIX

الزاوية Windows POSIX
الإنشاء / open CreateFileMapping / OpenFileMapping / MapViewOfFile6 shm_open / ftruncate / mmap3
مشاركة غير مرتبطة بالقرص mapping مدعوم بملف الصفحة مع تحديد INVALID_HANDLE_VALUE68 POSIX shared memory object + mmap3
القيمة الأولية صفحات مدعومة بملف الصفحة تُهيّأ بأصفار8 الكائن الجديد طوله 0. البايتات المخصصة حديثاً تُهيّأ بأصفار3
المزامنة mutex / semaphore / event / interlocked وغيرها25 mutex / condvar / semaphore مشتركة بين العمليات2018
ما لا يُستخدم عبر العمليات CRITICAL_SECTION, WaitOnAddress2110 mutex / condvar التي بقيت PTHREAD_PROCESS_PRIVATE2019
موت المالك WAIT_ABANDONED12 robust mutex + EOWNERDEAD / pthread_mutex_consistent()1314
حذف الاسم يختفي بتحرير آخر handle / view28 حذف الاسم بـ shm_unlink. إن بقيت مراجع يبقى الكيان حتى النهاية2223
مساحة الأسماء / الأذونات Global\ / Local\، ACL، SeCreateGlobalPrivilege1524 mode, umask, مساحة الأسماء، O_CREAT|O_EXCL3

MemoryMappedFile في C# أيضاً في الجوهر غلاف لـ file mapping في Windows. لذا الأساسيات لا تتغير:

  • الفتح بالاسم نفسه
  • استخدام mutex / event على حدة
  • القراءة بتخطيط صريح على الـ view
  • عدم وضع مرجع كائن كما هو

.1

MemoryMappedFile يحمل الأساسيات نفسهامخطّط يبيّن أن MemoryMappedFile في C# في الجوهر غلاف لـ file mapping في Windows، لذا لا تتغير أساسيات الفتح بالاسم نفسه واستخدام mutex أو event على حدة والقراءة بتخطيط صريح وعدم وضع مرجع كائن.MemoryMappedFile في C#غلاف لـ file mappingالفتح بالاسم نفسهالمزامنة بـ mutex / event على حدةالقراءة بتخطيط صريحلا يُوضع مرجع كائن كما هو

الشكل 25: حتى مع MemoryMappedFile في C# تسري أساسيات file mapping كما هي.

8. قائمة تحقق ننظر إليها أولاً

  • هل shared memory لازم حقاً. هل هي بيانات كبيرة على المضيف نفسه
  • هل فُصل control plane عن data plane
  • هل يمكن تضييق نموذج التزامن إلى SPSC / كاتب واحد وعدة قرّاء
  • هل في الترويسة الأمامية magic / version / size / state / generation / heartbeat
  • هل لم تُوضع pointer / HANDLE / fd / كائن STL / std::mutex
  • هل يوجد بروتوكول commit يمنع القارئ من رؤية حالة وسط الكتابة
  • هل المهيّئ محدد بشخص واحد
  • هل يوجد إجراء استعادة عند الإنهاء غير الطبيعي
  • هل الاسم والأذونات مصرَّح بهما
  • هل Global\ لازم حقاً
  • هل لم يُفترض resize in place
  • هل جُرّب قتل الكاتب / توقف القارئ / version mismatch / نقص الأذونات

9. الخلاصة

shared memory قوي جداً إذا أُحسن استخدامه. خصوصاً في

  • الصور
  • الصوت
  • صفوف حسّاسات
  • دفعات كبيرة
  • snapshot عالية التكرار

أي بيانات كبيرة داخل الجهاز نفسه، يؤثّر حقاً.

لكن جوهر shared memory ليس «السرعة» بل نقل المسؤولية. بدلاً من تقليل النسخ والمراسلة عبر النواة، نتولى نحن:

  • المزامنة
  • الرؤية
  • التهيئة
  • ABI
  • الاستعادة
  • الأذونات
  • قابلية الرصد

.

جوهر shared memory نقل المسؤوليةمخطّط يبيّن نقل المسؤولية: تقليل النسخ والمراسلة عبر النواة مقابل تولّي التطبيق للمزامنة والرؤية والتهيئة وABI والاستعادة والأذونات وقابلية الرصد.تقليل النسخ والمراسلةما نتولاه بدلاً من ذلكالمزامنة والرؤية والتهيئةABI والاستعادة والأذوناتقابلية الرصد

الشكل 26: جوهر shared memory ليس «السرعة» بل نقل مسؤولية الاتساق إلينا.

لذا الأمان في الخيط الأول هكذا.

  • SPSC ring buffer أو double buffer
  • ترويسة ثابتة في المقدمة
  • إشارة بـ offset
  • إشعار عبر قناة أخرى
  • مع version / generation / heartbeat
  • مع اختبارات المسار غير الطبيعي

البدء بهذا الشكل يجعل shared memory أداة صريحة جداً. بالمقابل، معاملته فوراً كـ «ذاكرة مشتركة سريعة يوضع فيها أي شيء» يحوّل الأمر تدريجياً من تطبيق إلى علم آثار.

10. روابط مرجعية

  • Windows: أساسيات file mapping و named shared memory682
  • Windows: مساحة الأسماء / الأمان / المزامنة1524512
  • POSIX: shm_open, shm_unlink, mmap, مزامنة process-shared / robust322162013
  • .NET: نظرة عامة على MemoryMappedFile1
  1. Microsoft Learn, Memory-mapped files / Microsoft Learn, MemoryMappedFile Class ↩ ↩2 ↩3 ↩4

  2. Microsoft Learn, Sharing Files and Memory ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  3. man7.org, shm_open(3) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11

  4. Microsoft Learn, /volatile (volatile Keyword Interpretation) / Microsoft Learn, volatile (C++) ↩ ↩2

  5. Microsoft Learn, Interlocked Variable Access / Microsoft Learn, MemoryBarrier function ↩ ↩2 ↩3 ↩4 ↩5

  6. Microsoft Learn, Creating Named Shared Memory ↩ ↩2 ↩3 ↩4

  7. Microsoft Learn, Scope of Allocated Memory ↩ ↩2

  8. Microsoft Learn, CreateFileMappingA function ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10

  9. man7.org, POSIX Shared Memory training slides ↩

  10. Microsoft Learn, WaitOnAddress function ↩ ↩2

  11. Microsoft Learn, MapViewOfFileEx function / Microsoft Learn, MapViewOfFile function ↩

  12. Microsoft Learn, Mutex Objects ↩ ↩2 ↩3

  13. man7.org, pthread_mutex_lock(3p) / man7.org, pthread_mutexattr_setrobust(3) ↩ ↩2 ↩3

  14. man7.org, pthread_mutex_consistent(3) / man7.org, pthread_mutex_consistent(3p) ↩ ↩2

  15. Microsoft Learn, Kernel object namespaces ↩ ↩2 ↩3 ↩4

  16. man7.org, mmap(2) ↩ ↩2

  17. Microsoft Learn, Using Mutex Objects ↩

  18. man7.org, sem_init(3) / man7.org, sem_init(3p) ↩ ↩2

  19. man7.org, pthread_condattr_setpshared(3p) / man7.org, pthread_condattr_getpshared(3p) ↩ ↩2

  20. man7.org, pthread_mutexattr_getpshared(3) / man7.org, pthread_mutexattr_getpshared(3p) ↩ ↩2 ↩3

  21. Microsoft Learn, Critical Section Objects ↩

  22. Microsoft Learn, File Mapping Security and Access Rights ↩ ↩2

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

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

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

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

إذا كتبت قيمة في shared memory، هل يقرأها عملية أخرى فوراً وبشكل صحيح؟
أن تكون مرئية شيء، وأن تُقرأ بأمان شيء آخر. shared memory آلية تعرض سلسلة البايتات نفسها على عدة عمليات، وليست المزامنة نفسها. حتى لو قصد الكاتب كتابة length ثم payload ثم ready flag بهذا الترتيب، قد يرى القارئ بلا أي مزامنة length جديداً مع payload قديماً مركّبين. في Windows و POSIX على السواء، يُفترض أن يُجمع الوصول إلى shared memory مع وسيلة مزامنة مثل mutex أو semaphore أو event.
هل يجوز وضع مؤشر أو std::string أو HANDLE في shared memory؟
الأفضل ألا تضعها. العنوان الافتراضي وموارد process-local لا معنى لها إلا في سياق تلك العملية، وحتى لو رُبط الـ mapping نفسه في عملية أخرى فليس مضموناً أن يتطابق العنوان الافتراضي. الأمر نفسه ينطبق على std::vector و std::mutex و CRITICAL_SECTION. إن احتجت إشارة فاحملها كـ offset من العنوان الأساسي، وأملِ البيانات في shared memory نحو أعداد صحيحة بعرض ثابت + تخطيط صريح + ترويسة بإصدار.
هل يغني استخدام volatile عن مزامنة shared memory؟
لا. volatile ليس سحراً ينقذ تصميم shared memory، وatomicity وmutual exclusion مسألتان منفصلتان على الأقل. تصميم يراقب volatile bool بـ busy loop يهدر CPU، ويغيم ضمان ترتيب payload و ready flag، ويسهل التقاط حالة وسط الكتابة. كذلك WaitOnAddress في Windows موجّه إلى threads داخل العملية نفسها، فلا يُعتمد كآلية انتظار بين العمليات. الإشعار يُوجَّه إلى primitive يمكن انتظاره مثل event أو semaphore.
ما الذي يجب حسمه أولاً عند تصميم shared memory؟
أربعة أمور. فصل control plane عن data plane بحيث يكون التحكم (بدء، إيقاف، إشعار) عبر الرسائل والجسم عبر shared memory، وتضييق نموذج التزامن (في البداية SPSC ring buffer أو double buffer أقل عرضة للأعطال)، والمالك والعمر: من ينشئ ويهيّئ ويحذف ويستعيد، وتصميم ABI بما فيه التخطيط والإصدار. وضع magic و version و size و state و generation و heartbeat في الترويسة الأمامية وحده يغيّر كثيراً سهولة التحقيق في الأعطال.

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

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

غو كومورا

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

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

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