أخطاء الصفحات
5. أخطاء الصفحات
ترفع وحدة المعالجة المركزيّة (CPU) في RISC-V استثناءَ خطأ الصفحة عندما يُستخدم عنوانٌ افتراضيّ لا توجد له مقابلةٌ في جدول الصفحات، أو كانت له مقابلةٌ راية PTE_V فيها ممسوحة، أو كانت له مقابلةٌ تمنع رايات الأذونات فيها (PTE_R، PTE_W، PTE_X، PTE_U) العمليّة التي تُحاول القيام بها. ويميّز RISC-V بين ثلاثة أنواعٍ من أخطاء الصفحات: أخطاء صفحات التحميل (والتي تسبّبها تعليمات التحميل)، وأخطاء صفحات التخزين (والتي تسبّبها تعليمات التخزين)، وأخطاء صفحات التعليمات (والتي تسبّبها عمليّات جلب التعليمات التي سيُنفَّذها). ويُشير سجلّ scause إلى نوع خطأ الصفحة، ويحتوي سجلّ stval على العنوان الذي لم يمكن ترجمته.
إنّ مجموع جداول الصفحات وأخطاء الصفحات أداةٌ قويّة. فجداول الصفحات تمنح النواة مستوىً من اللا مباشرة بين العناوين الافتراضيّة والفيزيائيّة، بحيث تستطيع النواة التحكّم في بنيّة محتويات فضاءات العناوين. وتسمح أخطاء الصفحات للنواة باعتراض عمليّات التحميل والتخزين، وبتحديد البيانات التي تشير إليها تلك المراجع أثناء العمل، وذلك بتعديل جدول الصفحات. ويمكن للنواة أن تستخدم هذه القدرات لزيادة الكفاءة: فمثلًا، يتيح الاستنساخ بالكتابة عند الحاجة (copy-on-write) للنواة مشاركة الذاكرة بين الأب والابن شفّافًا، ممّا يجنّبها تكلفة نسخ الصفحات التي لا يكتب فيها أيٌّ منهما. ويستطيع مبرمجو التطبيقات الاستفادة أيضًا. وإحدى الإمكانات هي الملفّات المخطّطة في الذاكرة، حيث تستخدم النواة صفحاتٍ لتجعل محتوى ملفٍّ ما يظهر في فضاء عناوين تطبيقٍ، عبر قراءة قطع الملفّ استجابةً لأخطاء الصفحات شفّافًا. وإمكانٌ آخر هو التخصيص الكسول للذاكرة، الذي يسمح لبرنامجٍ ما بطلب فضاء عناوين افتراضيّ ضخم، دون أن يدفع تكلفة تخصيص ذاكرةٍ فيزيائيّة إلّا للصفحات التي يقرؤها البرنامج ويكتب فيها فعلًا. ولا يستخدم xv6 أخطاء الصفحات إلّا لغرضٍ واحد: التخصيص الكسول.
قبل المضي، يُرجى قراءة الدوالّ sys_sbrk() في kernel/sysproc.c، وvmfault في kernel/vm.c. كما يُرجى البحث عن نداءات vmfault في kernel/trap.c وkernel/vm.c.
5.1 التخصيص الكسول
إنّ التخصيص الكسول في xv6 له جزءان. أوّلًا، حين يطلب تطبيقٌ ذاكرةً بمناداة sbrk بالراية SBRK_LAZY، تلاحظ النواة ازدياد الحجم، لكنّها لا تخصّص ذاكرةً فيزيائيّةً ولا تنشئ مدخلات جدول صفحات للمدى الجديد من العناوين الافتراضيّة. وثانيًا، عند وقوع خطأ صفحةٍ على أحد تلك العناوين الجديدة، تخصّص النواة صفحةً من الذاكرة الفيزيائيّة وتخطّطها في جدول الصفحات. وتنفّذ النواة التخصيص الكسول شفّافًا بالنسبة للتطبيقات: فلا حاجة إلى أيّ تعديلٍ على التطبيقات لتستفيد منه.
التخصيص الكسول مريح للتطبيقات لأنّها لا تضطرّ إلى التنبّؤ الدقيق بمدى الذاكرة التي ستحتاج إليها. فمثلًا، قد يعالج تطبيقٌ إدخالًا، لكنّه لا يعرف مسبقًا ما سيكون حجم ذلك الإدخال. وبالتخصيصات الكسولة، يستطيع تطبيقٌ ما طلب ذاكرةٍ لأسوأ الحالات، دون أن يضطرّ إلى الدفع مقابل أسوأ الحالات تلك: إذ لا يتعيّن على النواة أداء أيّ عملٍ من أجل الصفحات التي لا يستخدمها التطبيق أبدًا، وتستطيع تطبيقاتٌ أخرى تخصيص الصفحات غير المستخدمة.
علاوةً على ذلك، إذا كان التطبيق يطلب تنمية فضاء العناوين تنميةً كبيرة، فإنّ sbrk دون تخصيصٍ كسولٍ مكلف: فإذا طلب تطبيقٌ غيغابايتًا من الذاكرة، فعلى النواة أن تخصّص 262,144 صفحة فيزيائيّةً بحجم 4096 بايت وأن تصفّرها. ويتيح التخصيص الكسول توزيع هذه التكلفة على الزمن. ومن الجهة الأخرى، يتكبّد التخصيص الكسول كلفةً إضافيّةً من أخطاء الصفحات، وهي أخطاءٌ تتضمّن انتقالًا بين المستخدم والنواة. وتستطيع أنظمة التشغيل تخفيض هذه التكلفة بتخصيص دفعةٍ من الصفحات المتتالية لكلّ خطأ صفحةٍ بدلًا من صفحةٍ واحدة، وبهيئة شيفرة الدخول إلى النواة/الخروج منها لهذه الأخطاء تحديدًا (وإنّ xv6 لا يفعل أيًّا منهما).
ومن الجهة الأخرى، عند وقوع خطأ صفحةٍ من أجل صفحةٍ مخصّصةٍ كسولًا، قد تجد النواة أنّ لا ذاكرة حرّة لديها لتخصيصها. وفي هذه الحالة، ليست لدى النواة طريقةٌ سهلةٌ لإعادة خطأ نفاد الذاكرة إلى التطبيق، وإنّما تقتل التطبيق بدلًا من ذلك. أمّا بالنسبة للتطبيقات التي تفضّل خطأً عند التخصيص الفاشل، فإنّ xv6 يسمح لتطبيقٍ بتخصيص الذاكرة تحمّسًا بمناداة sbrk بالراية SBRK_EAGER.
5.2 الشيفرة
ينمو نداء النظام sbrk(n) (أو يتقلّص إذا كان n سالبًا) حجم ذاكرة العمليّة بمقدار n بايت، ويعيد بداية المنطقة المخصّصة حديثًا (أي الحجم القديم). أمّا تنفيذه في النواة فهو sys_sbrk.
إذا حدّد التطبيق SBRK_EAGER، فإنّ نداء النظام ينفّذ بواسطة الدالّة growproc. وتنادي growproc الدالّة uvmalloc. وتخصّص uvmalloc ذاكرةً فيزيائيّةً بـkalloc، وتصفّر الذاكرة المخصّصة، وتضيف مدخلات جدول صفحات إلى جدول صفحات المستخدم باستخدام mappages.
إذا خصّصت التطبيقات الذاكرة كسولًا، فإنّ sys_sbrk تزيد حجم العمليّة (myproc()->sz) بمقدار n فحسب وتعيد الحجم القديم؛ وهي لا تخصّص ذاكرةً فيزيائيّةً ولا تضيف مدخلات جدول صفحات إلى جدول صفحات العمليّة.
وعندما تُحمّل عمليّةٌ ما أو تخزّن إلى عنوانٍ افتراضيّ يفتقر إلى مقابلةٍ صالحةٍ في جدول الصفحات، ترفع وحدة المعالجة المركزيّة استثناء خطأ الصفحة. وتفحص usertrap هذه الحالة (trap.c:74) وتنادي vmfault للتعامل مع خطأ الصفحة. وتتحقّق vmfault من أنّ العنوان المُخطئ واقعٌ في المنطقة التي منحها sbrk سابقًا، ثمّ تخصّص صفحةً من الذاكرة الفيزيائيّة بـkalloc، وتصفّر الصفحة المخصّصة، وتضيف مدخلة جدول صفحات إلى جدول صفحات المستخدم باستخدام mappages. ويضبط xv6 رايات PTE_W وPTE_R وPTE_U وPTE_V في مدخلة جدول الصفحات الخاصّة بالصفحة الجديدة. ثمّ تستأنف usertrap العمليّة عند التعليمة التي سبّبت الخطأ. ولأنّ مدخلة جدول الصفحات صالحةٌ الآن، فستُنفَّذ تعليمة التحميل أو التخزين المعاد تنفيذها دون خطأ.
وإذا حرّر تطبيقٌ ذاكرةً بمناداة sbrk بـn سالب، فإنّ sys_sbrk تنادي growproc، التي تنادي uvmdealloc. أمّا العمل الحقيقيّ فتؤدّيه uvmunmap، التي تستخدم walk للعثور على مدخلات جدول الصفحات. وبما أنّ بعض الصفحات قد لا تكون قد استُخدمت من قِبل العمليّة أبدًا، وبالتالي لم تخصّصها vmfault أبدًا، فإنّ uvmunmap تتخطّى مدخلات جدول الصفحات التي لا راية PTE_V فيها. فإن كانت مقابلة مدخلة جدول صفحاتٍ صالحة، نادت uvmunmap الدالّة kfree لتحرير الذاكرة الفيزيائيّة التي تُشير إليها.
ولاحظ أنّ xv6 يستخدم جدول صفحات العمليّة لا لإخبار العتاد بكيفيّة تعيين العناوين الافتراضيّة للمستخدم فحسب، بل أيضًا بصفته السجلّ الوحيد للصفحات الفيزيائيّة المخصّصة لتلك العمليّة. ولذلك فإنّ تحرير ذاكرة المستخدم (في uvmunmap) يستلزم فحص جدول صفحات المستخدم.
5.3 عالم الواقع: الاستنساخ بالكتابة عند الحاجة (COW)
تستخدم كثيرٌ من الأنوية (وإن لم يكن xv6 بينها) أخطاء الصفحات لتنفيذ الاستنساخ بالكتابة عند الحاجة (COW). ويَعِدُّ نداء النظام fork بأنّ يرى الابن ذاكرةً محتواها الأوّليّ هو نفسه محتوى ذاكرة الأب لحظة الاستنساخ. وإحدى طرق تنفيذ ذلك هي نسخ ذاكرة الأب بالكامل إلى ذاكرةٍ فيزيائيّةٍ مخصّصةٍ حديثًا من أجل الابن؛ وهذا ما يفعله xv6. وقد يكون النسخ بطيئًا، وكان سيكون أكفأَ لو أمكن للابن مشاركة الذاكرة الفيزيائيّة للأب. غير أنّ التنفيذ المباشر لهذا لن ينجح، لأنّه سيؤدّي إلى تعطيل الأب والابن لتنفيذ كلٍّ منهما بكتاباتهما على المكدّس والكومة المشتركين.
يجعل الاستنساخ بالكتابة عند الحاجة الأب والابن يتشاركان الذاكرة الفيزيائيّة بأمان، وذلك باستخدامٍ ملائمٍ لأذونات جدول الصفحات وأخطاء الصفحات. والخطة الأساسيّة هي أن يتشارك الأب والابن جميع الصفحات الفيزيائيّة ابتداءً، لكن على كلٍّ منهما أن يخطّطها للقراءة فقط (مع مسح راية PTE_W). وحينئذٍ يستطيع الأب والابن القراءة من الذاكرة الفيزيائيّة المشتركة. فإن كتب أيٌّ منهما في صفحةٍ مشتركة، ترفع وحدة المعالجة المركزيّة في RISC-V استثناء خطأ الصفحة. وكانت نواةٌ تدعم COW لتستجيب بتخصيص صفحةٍ جديدةٍ من الذاكرة الفيزيائيّة وبنسخ الصفحة المشتركة في تلك الصفحة الجديدة. ثمّ لتغيّر النواة مدخلة جدول الصفحات المعنيّة في جدول صفحات العمليّة المُخطئة لتُشير إلى النسخة ولتسمح بالكتابة كما تسمح بالقراءة، ثمّ لتستأنف العمليّة المُخطئة عند التعليمة التي سبّبت الخطأ. ولأنّ مدخلة جدول الصفحات تسمح بالكتابة الآن، فستُنفَّذ تعليمة التخزين المعاد تنفيذها دون خطأ، وستعدّل نسخةً خاصّةً من الصفحة لا الصفحة المشتركة.
يتطلّب الاستنساخ بالكتابة عند الحاجة مسك دفاترًا للمساعدة في تحديد متى يمكن تحرير الصفحات الفيزيائيّة، إذ إنّ كلّ صفحةٍ يمكن أن تُشار إليها من عددٍ متغيّرٍ من جداول الصفحات بحسب تاريخ عمليّات الاستنساخ وأخطاء الصفحات وعمليّات exec وعمليّات الخروج. وهذا مسك الدفاتر يسمح بتحسينٍ مهم: فإذا تكبّدت عمليّةٌ ما خطأ صفحة تخزينٍ وكانت الصفحة الفيزيائيّة مُشارًا إليها من جدول صفحات تلك العمليّة وحده، فلا حاجة إلى أيّ نسخ.
يجعل الاستنساخ بالكتابة عند الحاجة fork أسرع، إذ ليس على fork أن ينسخ الذاكرة. وبعض الذاكرة سيكون لا بدّ من نسخه لاحقًا، عند كتابته، لكنّه كثيرًا ما يكون أكثر الذاكرة لا يحتاج إلى النسخ أبدًا. ومثالٌ شائعٌ هو fork متبوّعٌ بـexec: فقد تُكتب صفحات قليلة بعد fork، لكنّ exec الخاصّ بالابن يحرّر بعد ذلك معظم الذاكرة الموروثة من الأب. ويُلغي الاستنساخ بالكتابة عند الحاجة fork الحاجة إلى نسخ هذه الذاكرة أصلًا. فضلًا عن ذلك، فإنّ الاستنساخ بـCOW شفّاف: فلا حاجة إلى أيّ تعديلٍ على التطبيقات لتستفيد منه.
5.4 عالم الواقع: الصفحات حسب الطلب
هناك ميزةٌ أخرى واسعة الانتشار تستثمر أخطاء الصفحات، وهي الصفحات حسب الطلب (demand paging). ففي نداء النظام exec، يحمّل xv6 جميع النصّ والبيانات الخاصّين بتطبيقٍ ما إلى الذاكرة قبل بدء تشغيل التطبيق. وبما أنّ التطبيقات قد تكون كبيرةً، وبما أنّ القراءة من القرص تستغرق وقتًا، فإنّ تكلفة البدء هذه قد تكون ملحوظةً للمستخدمين. ولتقليل زمن البدء، لا تحمّل النواة الحديثة الملفّ التنفيذيّ إلى الذاكرة ابتداءً، وإنّما تنشئ جدول صفحات المستخدم بجميع مدخلات جدول صفحاته مُعلَّمةً بأنّها غير صالحة. ثمّ تبدأ النواة تشغيل البرنامج؛ وفي كلّ مرّةٍ يستخدم فيها البرنامج صفحةً للمرّة الأولى، يقع خطأ صفحةٍ، فتقرأ النواة استجابةً لذلك محتوى الصفحة من القرص وتخطّطه في فضاء عناويات المستخدم. ومثل الاستنساخ بالكتابة عند الحاجة والتخصيص الكسول، تستطيع النواة تنفيذ هذه الميزة شفّافًا بالنسبة للتطبيقات.
قد تحتاج البرامج التي تعمل على حاسوبٍ إلى ذاكرةً أكبر من الذاكرة الرئيسيّة التي يملكها الحاسوب. وللتغلّب على ذلك بسلاسة، قد ينفّذ نظام التشغيل الاستبدال إلى القرص. والفكرة هي تخزين جزءٍ فقط من صفحات المستخدم في الذاكرة الرئيسيّة، وتخزين الباقي على القرص في منطقة الاستبدال. وتُعلّم النواة مدخلات جدول الصفحات التي تقابل ذاكرةً مخزّنةً في منطقة الاستبدال (وهي بذلك ليست في الذاكرة الرئيسيّة) بأنّها غير صالحة. فإن حاول تطبيقٌ استخدام إحدى الصفحات التي أُخرجت إلى القرص، فسيتكبّد خطأ صفحةٍ، ولا بدّ من إدخال الصفحة: أي أنّ معالج مصائد النواة سيخصّص صفحةً من الذاكرة الرئيسيّة الفيزيائيّة، ويقرأ الصفحة من القرص إلى الذاكرة الرئيسيّة، ويعدّل مدخلة جدول الصفحات المعنيّة لتُشير إلى الذاكرة الرئيسيّة.
وماذا يحدث إذا كان لا بدّ من إدخال صفحةٍ، لكن لم تكن هناك ذاكرةٌ فيزيائيّة حِرّة؟ في هذه الحالة، يجب على النواة أن تحرّر صفحةً فيزيائيّةً أوّلًا بإخراجها إلى منطقة الاستبدال على القرص أو إزاحتها إليها، وبأن تُعلَّم مدخلات جدول الصفحات التي تُشير إلى تلك الصفحة الفيزيائيّة بأنّها غير صالحة. والإزاحة مكلفة، لذا يؤدّي الاستبدال أداءً أفضل إذا كان نادر الحدوث: أي إذا استخدمت التطبيقات مجموعةً فرعيّةً فقط من صفحات ذاكرتها وكان اتّحاد تلك المجموعات الفرعيّة يتّسع في الذاكرة الرئيسيّة. وتُشار إلى هذه الخاصّة غالبًا بأنّ لها المحليّة الجيّدة للمراجع. ومثل كثيرٍ من تقنيّات الذاكرة الافتراضيّة، تنفّذ الأنوية الاستبدال إلى القرص عادةً بطريقةٍ شفّافةٍ بالنسبة للتطبيقات.
كثيرًا ما تعمل الحواسيب بذاكرةٍ فيزيائيّةٍ حِرّةٍ قليلةٍ أو بلا ذاكرةٍ حرةٍ على الإطلاق، أيًّا كان مقدار الذاكرة الرئيسيّة التي يوفّرها العتاد. فمثلًا، يُضاعف مزوّدوا الحوسبة السحابيّة عملاء كثيرين على آلةٍ واحدةٍ لاستخدام عتادهم بكفاءةٍ من حيث التكلفة. ومثالٌ آخر هو أنّ المستخدمين يشغّلون تطبيقات كثيرةً على الهواتف الذكيّة في مقدارٍ صغيرٍ من الذاكرة الفيزيائيّة. وفي مثل هذه الأوضاع، قد يتطلّب تخصيص صفحةٍ إزاحة صفحةٍ قائمةٍ أوّلًا. وبالتالي، حين تكون الذاكرة الفيزيائيّة الحُرّة شحيحة، يكون التخصيص مكلفًا.
إنّ التخصيص الكسول والصفحات حسب الطلب مفيدان بصفةٍ خاصّةٍ حين تكون الذاكرة الحُرّة شحيحةً وحين تستخدم البرامج بنشاطٍ جزءًا فقط من ذاكرتها المخصّصة. وتساعد هذه التقنيّات أيضًا على تجنّب العمل الضائع حين تُخصَّص صفحةٌ أو تُحمَّل لكنّها لا تُستخدم إمّا أبدًا أو تُزاح قبل أن يمكن استخدامها.
5.5 عالم الواقع: الملفّات المخطّطة في الذاكرة
ومن الميزات الأخرى التي تجمع بين الاستبدال واستثناءات أخطاء الصفحات والمكدّسات المتنامية آليًّا والملفّات المخطّطة في الذاكرة، وهي ملفّاتٍ يخطّطها برنامجٌ ما في فضاء عناوياته باستخدام نداء النظام mmap، بحيث يستطيع البرنامج قراءتها والكتابة فيها باستخدام تعليمات التحميل والتخزين.
5.6 تمارين
- اكتب برنامجَ مستخدمٍ ينمّي فضاء عناوياته بمقدار بايتٍ واحدٍ بمناداة
sbrk(1). ثم شغّل البرنامج وافحص جدول صفحات البرنامج قبل نداءsbrkوبعد نداءsbrk. فكم مقدار المساحة التي خصّصتها النواة؟ وما الذي يحتويه مدخلة جدول الصفحات الخاصّة بالذاكرة الجديدة؟ - نفّذ الاستنساخ بالكتابة عند الحاجة (COW).
- نفّذ
mmap.