نكتب نظام التشغيل على الصدأ. تطبيق ذاكرة الصفحة (جديد)

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

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

مقال آخر حول الترحيل؟
إذا تابعت هذه الدورة ، فقد رأيت المقالة "ذاكرة الصفحة: المستوى المتقدم" في نهاية شهر يناير. لكنني تعرضت لانتقادات لجداول الصفحات العودية. لذلك ، قررت إعادة كتابة المقالة ، باستخدام طريقة مختلفة للوصول إلى الإطارات.

هنا خيار جديد. لا يزال المقال يوضح كيفية عمل جداول الصفحات العودية ، لكننا نستخدم تطبيقًا أبسط وأكثر قوة. لن نقوم بحذف المقالة السابقة ، لكننا سنضع علامة عليها على أنها قديمة ولن نقوم بتحديثها.

أتمنى أن تستمتع بالخيار الجديد!

محتوى



مقدمة


من المقالة الأخيرة ، تعلمنا حول مبادئ ذاكرة الترحيل وكيف تعمل جداول الصفحات ذات المستوى الرابع على x86_64 . لقد وجدنا أيضًا أن المُحمل قد قام بالفعل بإعداد التسلسل الهرمي لجدول الصفحات للنواة لدينا ، وبالتالي فإن النواة تعمل على عناوين افتراضية. هذا يزيد الأمان لأن الوصول غير المصرح به إلى الذاكرة يؤدي إلى خطأ صفحة بدلاً من تغيير الذاكرة الفعلية بشكل عشوائي.

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

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

تحديثات التبعية


تتطلب هذه المقالة منك تسجيل إصدار أداة تحميل التشغيل 0.4.0 أو أعلى و x86_64 الإصدار 0.5.2 أو أعلى في التبعيات. يمكنك تحديث التبعيات في Cargo.toml :

 [dependencies] bootloader = "0.4.0" x86_64 = "0.5.2" 

لمعرفة التغييرات في هذه الإصدارات ، راجع سجل أداة تحميل التشغيل وسجل x86_64 .

الوصول إلى جداول الصفحات


ليس من السهل الوصول إلى جداول الصفحات من النواة. لفهم المشكلة ، ألق نظرة أخرى على التسلسل الهرمي للجدول ذي المستوى الرابع من المقالة السابقة:



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

المشكلة هي أنه لا يمكننا الوصول مباشرة إلى العناوين الفعلية من النواة ، لأنها تعمل أيضًا على العناوين الافتراضية. على سبيل المثال ، عندما نذهب إلى العنوان 4 KiB ، يمكننا الوصول إلى العنوان الافتراضي 4 KiB ، وليس إلى العنوان الفعلي حيث يتم تخزين جدول صفحات المستوى الرابع. إذا كنا نريد الوصول إلى العنوان الفعلي لـ 4 KiB ، فإننا نحتاج إلى استخدام بعض العناوين الافتراضية ، والتي يتم ترجمتها إليها.

لذلك ، للوصول إلى إطارات جداول الصفحات ، تحتاج إلى تعيين بعض الصفحات الافتراضية لهذه الإطارات. هناك طرق مختلفة لإنشاء مثل هذه التعيينات.

رسم الخرائط الهوية


الحل البسيط هو العرض المتطابق لجميع جداول الصفحات .



في هذا المثال ، نرى العرض المتماثل للإطارات. العناوين الفعلية لجداول الصفحات هي في الوقت نفسه عناوين افتراضية صالحة ، حتى نتمكن من الوصول بسهولة إلى جداول الصفحات بجميع المستويات ، بدءًا من التسجيل CR3.

ومع ذلك ، فإن هذا النهج يكتنف مساحة العنوان الافتراضية ويجعل من الصعب العثور على مناطق كبيرة متجاورة من الذاكرة الخالية. لنفترض أننا نريد إنشاء مساحة ذاكرة افتراضية 1000 كيلوبايت في الشكل أعلاه ، على سبيل المثال ، لعرض ملف في الذاكرة . لا يمكننا أن نبدأ بمنطقة 28 KiB ، لأنها تقع على صفحة مشغولة بالفعل في 1004 KiB . لذلك ، سيتعين عليك البحث أكثر حتى نجد شظية كبيرة مناسبة ، على سبيل المثال ، مع 1008 KiB . هناك نفس مشكلة التجزئة كما في الذاكرة المقسمة.

بالإضافة إلى ذلك ، يعد إنشاء جداول صفحات جديدة أكثر تعقيدًا ، حيث نحتاج إلى العثور على إطارات مادية لم يتم استخدام صفحاتها المناظرة بعد. على سبيل المثال ، بالنسبة لملفنا ، قمنا بحجز مساحة قدرها 1000 كيلوبايت من الذاكرة الظاهرية ، بدءًا من العنوان 1008 KiB . الآن لم يعد بإمكاننا استخدام أي إطار بعنوان فعلي يتراوح ما بين 1000 KiB و 2008 KiB ، لأنه لا يمكن عرضه بشكل مماثل.

خريطة الإزاحة الثابتة


لتجنب تشوش مساحة العنوان الافتراضية ، يمكنك عرض جداول الصفحات في منطقة ذاكرة منفصلة . لذلك ، بدلاً من تحديد التعيين ، نقوم بتعيين الإطارات بإزاحة ثابتة في مساحة العنوان الافتراضية. على سبيل المثال ، يمكن أن يكون الإزاحة 10 تيرابايت:



من خلال تخصيص هذا النطاق من الذاكرة الافتراضية فقط لعرض جداول الصفحات ، نتجنب مشاكل العرض المتطابق. لا يمكن حجز مساحة كبيرة من مساحة العنوان الافتراضية إلا إذا كانت مساحة العنوان الافتراضية أكبر بكثير من حجم الذاكرة الفعلية. في x86_64 هذه ليست مشكلة لأن مساحة العنوان 48 بت هي 256 تيرابايت.

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

خرائط الذاكرة الفعلية الكاملة


يمكننا حل هذه المشكلات عن طريق عرض جميع الذاكرة الفعلية ، وليس فقط إطارات جدول الصفحات:



يتيح هذا النهج لل kernel الوصول إلى الذاكرة الفعلية التعسفية ، بما في ذلك إطارات جدول الصفحات من مساحات العناوين الأخرى. يتم حجز مجموعة من الذاكرة الظاهرية بنفس الحجم كما كان من قبل ، ولكن فقط لا توجد صفحات غير مسبوقة بها.

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

ومع ذلك ، في x86_64 ، يمكننا استخدام صفحتين كبيرتين من MiB لعرضهما بدلاً من الحجم الافتراضي البالغ 4 كيلوبايت. وبالتالي ، لعرض 32 جيجابايت من الذاكرة الفعلية ، لا يلزم سوى 132 كيلوبايت لكل جدول صفحة: جدول واحد فقط من المستوى الثالث و 32 من جداول المستوى الثاني. يتم تخزين الصفحات الضخمة في ذاكرة التخزين المؤقت بشكل أكثر فاعلية لأنها تستخدم إدخالات أقل في مخزن الترجمة الديناميكي (TLB).

عرض مؤقت


بالنسبة للأجهزة التي تحتوي على القليل من الذاكرة الفعلية ، يمكنك فقط عرض جداول الصفحات مؤقتًا عندما تحتاج إلى الوصول إليها. بالنسبة إلى المقارنات المؤقتة ، يلزم تقديم عرض مماثل لجدول المستوى الأول فقط:



في هذا الشكل ، يدير جدول المستوى الأول أول 2 ميجابايت من مساحة العنوان الافتراضية. هذا ممكن لأن الوصول يتم من سجل CR3 من خلال إدخالات فارغة في جداول المستويات 4 و 3 و 2. يترجم السجل مع الفهرس 8 الصفحة الافتراضية عند 32 KiB إلى إطار فعلي عند 32 KiB ، وبالتالي تحديد الجدول المستوى 1 نفسه. في الشكل يظهر هذا من خلال سهم أفقي.

من خلال الكتابة إلى جدول المستوى الأول المعين بشكل مماثل ، يمكن لـ kernel إنشاء ما يصل إلى 511 مقارنات زمنية (512 ناقص السجل اللازم لتعيين الهوية). في المثال أعلاه ، يقوم kernel بإنشاء مقارنات زمنية مرتين:

  • تعيين إدخال فارغ في جدول المستوى 1 إلى إطار عند 24 KiB . يؤدي هذا إلى إنشاء تعيين مؤقت للصفحة الافتراضية عند 0 KiB للإطار الفعلي لجدول مستوى الصفحة 2 المشار إليه بواسطة السهم المنقط.
  • تطابق السجل التاسع لجدول المستوى 1 بإطار عند 4 KiB . يؤدي هذا إلى إنشاء تعيين مؤقت للصفحة الافتراضية عند 36 KiB إلى الإطار الفعلي لجدول مستوى الصفحة 4 المشار إليه بواسطة السهم المنقط.

يمكن للنواة الآن الوصول إلى جدول المستوى 2 عن طريق الكتابة إلى صفحة تبدأ من 0 KiB وجدول المستوى 4 بالكتابة إلى صفحة تبدأ من 33 KiB .

وبالتالي ، فإن الوصول إلى إطار تعسفي لجدول الصفحة مع تعيينات مؤقتة يتكون من الإجراءات التالية:

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

مع هذا الأسلوب ، تظل مساحة العنوان الافتراضية نظيفة ، حيث يتم استخدام نفس الصفحات الظاهرية 512 باستمرار. العيب هو بعض التعقيد ، خاصة وأن المقارنة الجديدة قد تتطلب تغيير عدة مستويات من الجدول ، أي أننا نحتاج إلى تكرار العملية الموضحة عدة مرات.

الجداول الصفحة العودية


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

الفكرة هي ترجمة بعض السجلات من جدول المستوى الرابع إلى نفسه. وبالتالي ، فإننا نحتفظ فعليًا بجزء من مساحة العنوان الافتراضية ونعين جميع إطارات الجدول الحالية والمستقبلية لهذه المساحة.

دعونا نلقي نظرة على مثال لفهم كيف يعمل كل هذا:



الاختلاف الوحيد من المثال في بداية المقالة هو سجل إضافي مع الفهرس 511 في جدول المستوى 4 ، والذي تم تعيينه إلى الإطار الفعلي 4 KiB ، الموجود في هذا الجدول نفسه.

عندما يتم تشغيل وحدة المعالجة المركزية في هذا السجل ، فإنها لا تشير إلى جدول المستوى 3. ولكنها تشير مرة أخرى إلى جدول المستوى 4. وهذا مشابه لوظيفة متكررة تستدعي نفسها. من المهم أن يفترض المعالج أن كل سجل في جدول المستوى 4 يشير إلى جدول المستوى 3 ، لذلك يتعامل الآن مع جدول المستوى 4. كجدول من المستوى 3. وهذا يعمل لأن جداول جميع المستويات في x86_64 لها نفس البنية.

باتباع سجل متكرر مرة أو أكثر قبل بدء التحويل الفعلي ، يمكننا تقليل عدد المستويات التي يمر بها المعالج بشكل فعال. على سبيل المثال ، إذا اتبعنا السجل العودي مرة واحدة ، ثم انتقلنا إلى جدول المستوى 3 ، يعتقد المعالج أن جدول المستوى 3. هو جدول المستوى 2. أثناء الانتقال ، يعتبر جدول المستوى 2 بمثابة جدول المستوى الأول ، وجدول المستوى 1 كما تم تعيينه الإطار في الذاكرة الفعلية. هذا يعني أنه يمكننا الآن القراءة والكتابة إلى جدول مستوى الصفحة 1 لأن المعالج يعتقد أن هذا إطار معين. يوضح الشكل أدناه الخطوات الخمس لمثل هذه الترجمة:



وبالمثل ، يمكننا متابعة إدخال متكرر مرتين قبل بدء التحويل لتقليل عدد المستويات التي تم تمريرها إلى مستويين:



دعنا نذهب من خلال هذا الإجراء خطوة بخطوة. أولاً ، تتبع وحدة المعالجة المركزية إدخالًا متكررًا في جدول المستوى 4 وتعتقد أنها وصلت إلى مستوى المستوى 3. ثم تتبع السجل المتكرر مرة أخرى وتعتقد أنها وصلت إلى المستوى 2. ولكن في الواقع لا تزال في المستوى 4. ثم تنتقل وحدة المعالجة المركزية إلى العنوان الجديد ويدخل في جدول المستوى 3. ولكنه يعتقد أنه موجود بالفعل في جدول المستوى 1. أخيرًا ، عند نقطة الإدخال التالية في جدول المستوى 2. يعتقد المعالج أنه قد وصل إلى إطار الذاكرة الفعلية. هذا يتيح لنا القراءة والكتابة إلى جدول المستوى 2.

يتم الوصول أيضًا إلى جداول المستويات 3 و 4. للوصول إلى جدول المستوى 3 ، نتبع سجلًا متكررًا ثلاث مرات: يعتقد المعالج أنه موجود بالفعل في جدول المستوى 1 ، وفي الخطوة التالية نصل إلى المستوى 3 ، والذي تعتبره وحدة المعالجة المركزية كإطار معين. للوصول إلى جدول المستوى 4 نفسه ، نحن ببساطة نتبع السجل العودي أربع مرات حتى المعالج يعالج جدول المستوى 4 نفسه كإطار معين (باللون الأزرق في الشكل أدناه).



من الصعب فهم هذا المفهوم في البداية ، ولكنه في الواقع يعمل بشكل جيد.

حساب العنوان


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



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



للوصول إلى جدول المستوى 2 من هذه الصفحة ، نقوم بنقل جميع كتل الفهرس إلى كتلتين إلى اليمين وتعيين الفهرس العودية إلى مكان كلتا الكتلتين المصدرتين: المستوى 4 والمستوى 3:



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



أخيرًا ، للوصول إلى جدول المستوى 4 ، انقل كل شيء أربع كتل إلى اليمين.



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

يوضح الجدول أدناه بنية العناوين للوصول إلى أنواع مختلفة من الإطارات:

العنوان الافتراضي لهيكل العنوان ( ثماني )
صفحة0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
الدخول في المستوى 1 الجدول0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
الدخول في المستوى 2 الجدول0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
الدخول في المستوى 3 الجدول0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
الدخول في المستوى 4 الجدول0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

هنا هو مؤشر المستوى 4 ، هو المستوى 3 ، هو المستوى 2 ، و DDD هو مؤشر المستوى 1 للإطار المعروض ، EEEE هو EEEE . RRR هو فهرس السجل العودية. يتم تحويل الفهرس (ثلاثة أرقام) إلى إزاحة (أربعة أرقام) بضرب 8 (حجم إدخال جدول الصفحة). باستخدام هذا الإزاحة ، يشير العنوان الناتج مباشرةً إلى إدخال جدول الصفحة المقابل.

SSSS هي بتات توسيع للأرقام الموقعة ، أي أنها كلها نسخ من البتة 47. هذا مطلب خاص للعناوين الصالحة في بنية x86_64 ، التي ناقشناها في مقال سابق .

العناوين هي ثماني ، لأن كل حرف ثماني يمثل ثلاثة بتات ، والذي يسمح لك بفصل فهارس الجداول 9 بت بوضوح على مستويات مختلفة. هذا غير ممكن في النظام السداسي عشر ، حيث يمثل كل حرف أربعة بتات.

كود الصدأ


يمكنك إنشاء مثل هذه العناوين في كود Rust باستخدام عمليات bitwise:

 // the virtual address whose corresponding page tables you want to access let addr: usize = […]; let r = 0o777; // recursive index let sign = 0o177777 << 48; // sign extension // retrieve the page table indices of the address that we want to translate let l4_idx = (addr >> 39) & 0o777; // level 4 index let l3_idx = (addr >> 30) & 0o777; // level 3 index let l2_idx = (addr >> 21) & 0o777; // level 2 index let l1_idx = (addr >> 12) & 0o777; // level 1 index let page_offset = addr & 0o7777; // calculate the table addresses let level_4_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); let level_3_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); let level_2_table_addr = sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); 

يفترض هذا الرمز تعيين عودي لسجل المستوى 4 الأخير مع الفهرس 0o777 (511) بشكل متكرر. هذا ليس هو الحال حاليًا ، لذلك لن يعمل الرمز بعد. انظر أدناه لمعرفة كيفية إخبار المحمل بإعداد تعيين متكرر.

كبديل لتنفيذ عمليات bitwise يدويًا ، يمكنك استخدام RecursivePageTable نوع x86_64 قفص ، والذي يوفر تجريدات آمنة لعمليات الجدول المختلفة. على سبيل المثال ، توضح الشفرة أدناه كيفية تحويل عنوان افتراضي إلى العنوان الفعلي المقابل له:

 // in src/memory.rs use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; /// Creates a RecursivePageTable instance from the level 4 address. let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap(); } /// Retrieve the physical address for the given virtual address let addr: u64 = […] let addr = VirtAddr::new(addr); let page: Page = Page::containing_address(addr); // perform the translation let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) 

مرة أخرى ، يتطلب هذا الرمز تعيين عودية الصحيح. باستخدام هذا التعيين ، level_4_table_addr حساب level_4_table_addr المفقود level_4_table_addr كما في المثال الأول للرمز.



يعد التعيين المتكرر طريقة شيقة توضح مدى قوة المطابقة من خلال جدول واحد. إنه سهل التنفيذ نسبيًا ولا يتطلب سوى إعداد بسيط (إدخال واحد متكرر فقط) ، لذلك يعد هذا اختيارًا جيدًا للتجارب الأولى.

ولكن لديها بعض العيوب:

  • كمية كبيرة من الذاكرة الظاهرية (512 جيجابايت). هذه ليست مشكلة في مساحة عنوان 48 بت كبيرة ، ولكن قد يؤدي إلى سلوك ذاكرة التخزين المؤقت دون المستوى الأمثل.
  • يتيح الوصول بسهولة فقط إلى مساحة العنوان النشطة حاليًا. لا يزال الوصول إلى مساحات العناوين الأخرى ممكنًا عن طريق تغيير الإدخال العودي ، ولكن يلزم إجراء مطابقة مؤقتة للتبديل. وصفنا كيفية القيام بذلك في مقالة سابقة (قديمة).
  • يعتمد بشكل كبير على تنسيق جدول صفحة x86 وقد لا يعمل على بنيات أخرى.

دعم بووتلوأدر


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

لذلك ، أنا بحاجة إلى مساعدة من محمل الإقلاع. لديه حق الوصول إلى جداول الصفحات ، حتى يتمكن من إنشاء أي عروض نحتاجها. في تطبيقه الحالي ، يدعم صندوق أداة تحميل التشغيل النهجين المذكورين أعلاه باستخدام وظائف الشحن :

  • تقوم دالة map_physical_memory بتعيين الذاكرة الفعلية الكاملة في مكان ما في مساحة العنوان الافتراضية. وبالتالي ، يمكن للنواة الوصول إلى جميع الذاكرة الفعلية ويمكن تطبيق نهج مع عرض الذاكرة الفعلية الكاملة .
  • باستخدام الدالة recursive_page_table ، يعرض المُحمل بشكل متكرر إدخال جدول صفحة من المستوى الرابع. يسمح هذا للنواة بالعمل وفقًا للطريقة الموضحة في قسم "جداول الصفحات العودية" .

بالنسبة إلى النواة الخاصة بنا ، نختار الخيار الأول ، لأنه أسلوب بسيط ومستقل عن المنصة وأكثر قوة (يتيح أيضًا الوصول إلى إطارات أخرى ، وليس فقط جداول الصفحات). للحصول على الدعم من أداة تحميل التشغيل ، أضف الوظيفة إلى تبعياتها map_physical_memory:

 [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]} 

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

معلومات التمهيد


قفص bootloaderيحدد هيكل BootInfo مع جميع المعلومات التي أحالتها الأساسية. لا تزال البنية قيد الانتهاء ، لذلك قد تكون هناك بعض الإخفاقات عند الترقية إلى الإصدارات المستقبلية غير المتوافقة مع semver . حاليا، حقلين في هيكل: memory_mapو physical_memory_offset:

  • memory_mapيوفر الحقل نظرة عامة على الذاكرة الفعلية المتوفرة. يخبر النواة مقدار الذاكرة الفعلية المتوفرة على النظام وأي مناطق الذاكرة محجوزة لأجهزة مثل VGA. يمكن طلب بطاقة ذاكرة من BIOS أو UEFI الثابتة ، ولكن فقط في بداية عملية التمهيد. لهذا السبب ، يجب على المُحمل توفيره ، لأنه بعد ذلك لن تتمكن النواة من تلقي هذه المعلومات. ستصبح بطاقة الذاكرة في متناول اليد لاحقًا في هذه المقالة.
  • physical_memory_offsetتقارير عنوان البدء الظاهري لتعيين الذاكرة الفعلية. عند إضافة هذا الإزاحة إلى العنوان الفعلي ، نحصل على العنوان الظاهري المقابل. هذا يتيح الوصول من النواة إلى الذاكرة المادية التعسفية.

يقوم المُحمل بتمرير البنية BootInfoإلى kernel كوسيطة &'static BootInfoللدالة _start. أضفه:

 // in src/main.rs use bootloader::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] } 

من المهم تحديد نوع الوسيطة الصحيح ، لأن المترجم لا يعرف نوع التوقيع الصحيح لوظيفة نقطة الدخول الخاصة بنا.

ماكرو نقطة الدخول


نظرًا _startلأنه يتم استدعاء الوظيفة خارجيًا من المُحمل ، لا يتم التحقق من توقيع الوظيفة. هذا يعني أنه يمكننا السماح لها بقبول الوسائط التعسفية دون أخطاء الترجمة ، ولكن هذا سوف يتلف أو يتسبب في سلوك غير محدد وقت التشغيل.

لضمان أن وظيفة نقطة الدخول تحتوي دائمًا على التوقيع الصحيح ، bootloaderيوفر الصندوق ماكروًا entry_point. نعيد كتابة وظيفتنا باستخدام هذا الماكرو:

 // in src/main.rs use bootloader::{BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] } 

لم تعد بحاجة إلى استخدام نقطة الإدخال extern "C"أو no_mangle، نظرًا لأن الماكرو يحدد لنا نقطة الدخول الحقيقية للمستوى الأدنى _start. أصبحت الوظيفة kernel_mainالآن وظيفة Rust طبيعية تمامًا ، حتى نتمكن من اختيار اسم اعتباطي لها. الشيء المهم هو أنه يتم فحصه حسب النوع ، لذلك إذا استخدمت التوقيع الخاطئ ، على سبيل المثال ، عن طريق إضافة وسيطة أو تغيير نوعه ، فسيحدث خطأ في الترجمة

تطبيق


الآن لدينا إمكانية الوصول إلى الذاكرة الفعلية ، ويمكننا أخيرًا البدء في تنفيذ النظام. أولاً ، ضع في الاعتبار جداول الصفحات النشطة الحالية التي يعمل عليها kernel. في الخطوة الثانية ، قم بإنشاء دالة ترجمة تقوم بإرجاع العنوان الفعلي الذي تم تعيين هذا العنوان الظاهري إليه. في الخطوة الأخيرة ، سنحاول تعديل جداول الصفحات لإنشاء تعيين جديد.

أولاً ، قم بإنشاء وحدة نمطية جديدة في الكود memory:

 // in src/lib.rs pub mod memory; 

بالنسبة للوحدة النمطية ، قم بإنشاء ملف فارغ src/memory.rs.

الوصول إلى جداول الصفحات


في نهاية المقالة السابقة ، حاولنا إلقاء نظرة على جدول الصفحات التي يعمل عليها النواة ، لكننا لم نتمكن من الوصول إلى الإطار المادي الذي يشير إليه السجل CR3. يمكننا الآن مواصلة العمل من هذا المكان: active_level_4_tableستُرجع الدالة رابطًا إلى جدول صفحات نشط من المستوى الرابع:

 // in src/memory.rs use x86_64::structures::paging::PageTable; /// Returns a mutable reference to the active level 4 table. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable { use x86_64::{registers::control::Cr3, VirtAddr}; let (level_4_table_frame, _) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = VirtAddr::new(phys.as_u64() + physical_memory_offset); let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); &mut *page_table_ptr // unsafe } 

أولاً ، نقرأ الإطار الفعلي للجدول النشط للمستوى الرابع من السجل CR3. ثم نأخذ عنوان البداية الفعلي ونحوله إلى عنوان افتراضي عن طريق الإضافة physical_memory_offset. أخيرًا ، قم بتحويل العنوان إلى مؤشر أولي من *mut PageTableخلال الطريقة as_mut_ptr، ثم قم بإنشاء رابط منه بطريقة غير آمنة &mut PageTable. نقوم بإنشاء الرابط &mutبدلاً من ذلك &، لأننا في وقت لاحق من المقالة سنقوم بتعديل جداول الصفحات هذه.

ليست هناك حاجة لإدراج كتلة غير آمنة هنا ، لأن الصدأ يعتبر الجسم كله unsafe fnكتلة واحدة غير آمنة. هذا يزيد من المخاطر ، لأنه من الممكن إدخال عملية غير آمنة بطريق الخطأ في الأسطر السابقة. كما أنه يجعل من الصعب اكتشاف العمليات غير الآمنة. تم بالفعل إنشاء RFC لتعديل هذا السلوك من Rust.

الآن يمكننا استخدام هذه الوظيفة لإخراج سجلات جدول المستوى الرابع:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::active_level_4_table; let l4_table = unsafe { active_level_4_table(boot_info.physical_memory_offset) }; for (i, entry) in l4_table.iter().enumerate() { if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); } } println!("It did not crash!"); blog_os::hlt_loop(); } 

كما physical_memory_offsetperedaom المقابلة الهيكل الميداني BootInfo. ثم نستخدم دالة iterللتكرار من خلال إدخالات جدول الصفحة ومجمع enumerateلإضافة فهرس iإلى كل عنصر. يتم عرض الإدخالات غير الفارغة فقط ، لأن جميع الإدخالات 512 لن يتم وضعها على الشاشة.

عندما نقوم بتشغيل التعليمات البرمجية ، نرى هذه النتيجة:



نرى عدة سجلات غير فارغة يتم تعيينها إلى جداول المستوى الثالث المختلفة. يتم استخدام العديد من مناطق الذاكرة لأن هناك حاجة إلى مناطق منفصلة لرمز kernel ، ومكدس kernel ، وترجمة الذاكرة الفعلية ، ومعلومات التمهيد.

لتصفح جداول الصفحات وإلقاء نظرة على جدول المستوى الثالث ، يمكننا مرة أخرى تحويل الإطار المعروض إلى عنوان افتراضي:

 // in the for loop in src/main.rs use x86_64::{structures::paging::PageTable, VirtAddr}; if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); // get the physical address from the entry and convert it let phys = entry.frame().unwrap().start_address(); let virt = phys.as_u64() + boot_info.physical_memory_offset; let ptr = VirtAddr::new(virt).as_mut_ptr(); let l3_table: &PageTable = unsafe { &*ptr }; // print non-empty entries of the level 3 table for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); } } } 

لعرض جداول المستويين الثاني والأول ، كرر هذه العملية ، على التوالي ، لسجلات المستويين الثالث والثاني. كما يمكنك أن تتخيل ، ينمو مقدار الكود بسرعة كبيرة ، لذلك لن ننشر القائمة الكاملة.

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

ترجمة العناوين


لترجمة عنوان افتراضي إلى عنوان فعلي ، يجب علينا الانتقال إلى جدول صفحات من أربعة مستويات حتى نصل إلى الإطار المعين. لنقم بإنشاء وظيفة تؤدي ترجمة العنوان هذه:

 // in src/memory.rs use x86_64::{PhysAddr, VirtAddr}; /// Translates the given virtual address to the mapped physical address, or /// `None` if the address is not mapped. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { translate_addr_inner(addr, physical_memory_offset) } 

نشير إلى وظيفة آمنة translate_addr_innerللحد من كمية التعليمات البرمجية غير الآمنة. كما هو مذكور أعلاه ، يعتبر الصدأ الجسم بأكمله unsafe fnككتلة كبيرة غير آمنة. من خلال استدعاء وظيفة واحدة آمنة ، نجعل مرة أخرى كل عملية واضحة unsafe.

وظيفة داخلية خاصة لها وظيفة حقيقية:

 // in src/memory.rs /// Private function that is called by `translate_addr`. /// /// This function is safe to limit the scope of `unsafe` because Rust treats /// the whole body of unsafe functions as an unsafe block. This function must /// only be reachable through `unsafe fn` from outside of this module. fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; // read the active level 4 frame from the CR3 register let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() ]; let mut frame = level_4_table_frame; // traverse the multi-level page table for &index in &table_indexes { // convert the frame into a page table reference let virt = frame.start_address().as_u64() + physical_memory_offset; let table_ptr: *const PageTable = VirtAddr::new(virt).as_ptr(); let table = unsafe {&*table_ptr}; // read the page table entry and update `frame` let entry = &table[index]; frame = match entry.frame() { Ok(frame) => frame, Err(FrameError::FrameNotPresent) => return None, Err(FrameError::HugeFrame) => panic!("huge pages not supported"), }; } // calculate the physical address by adding the page offset Some(frame.start_address() + u64::from(addr.page_offset())) } 

بدلاً من إعادة استخدام الوظيفة ، active_level_4_tableنعيد قراءة إطار المستوى الرابع من السجل CR3، لأن هذا يبسط تنفيذ النموذج الأولي. لا تقلق ، سوف نحسن الحل قريبًا.

توفر البنية VirtAddrبالفعل طرقًا لحساب الفهارس في جداول الصفحات ذات المستويات الأربعة. نقوم بتخزين هذه الفهارس في صفيف صغير ، لأنه يسمح لك بالحلقة بين جميع الجداول for. خارج الحلقة ، نتذكر الإطار الأخير الذي تمت زيارته لحساب العنوان الفعلي لاحقًا. frameيشير إلى إطارات جدول الصفحة أثناء التكرار وإلى الإطار المقترن بعد التكرار الأخير ، أي بعد اجتياز سجل المستوى 1.

داخل الحلقة ، نطبق مرة أخرىphysical_memory_offsetلتحويل إطار إلى ارتباط جدول صفحة. ثم نقرأ سجل جدول الصفحة الحالي ونستخدم الدالة PageTableEntry::frameلاسترداد الإطار المطابق. إذا لم يتم تعيين السجل إلى إطار ، فارجع None. إذا كان السجل يعرض صفحة ضخمة من 2 ميغا بايت أو 1 جيجا بايت ، فسيصيبنا الذعر حتى الآن.

لذلك ، دعونا نتحقق من وظيفة الترجمة في بعض العناوين:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; use x86_64::VirtAddr; let addresses = [ // the identity-mapped vga buffer page 0xb8000, // some code page 0x20010a, // some stack page 0x57ac_001f_fe48, // virtual address mapped to physical address 0 boot_info.physical_memory_offset, ]; for &address in &addresses { let virt = VirtAddr::new(address); let phys = unsafe { translate_addr(virt, boot_info.physical_memory_offset) }; println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

عندما نقوم بتشغيل الشفرة ، نحصل على النتيجة التالية:



كما هو متوقع ، مع تعيين متطابق ، يتم 0xb8000تحويل العنوان إلى نفس العنوان الفعلي. يتم تحويل صفحة الرموز وصفحة المكدس إلى عناوين فعلية تعسفية ، والتي تعتمد على كيفية إنشاء المحمل التعيين الأولي للنواة الخاصة بنا. physical_memory_offsetيجب أن يشير التعيين إلى العنوان الفعلي 0، لكنه يفشل ، لأن الترجمة تستخدم صفحات ضخمة لتحقيق الكفاءة. قد يطبق إصدار مستقبلي من أداة تحميل التشغيل نفس التحسين لصفحات kernel و stack.

باستخدام MappedPageTable


تعتبر ترجمة العناوين الافتراضية إلى عناوين فعلية مهمة نموذجية لنظام التشغيل OS ، وبالتالي فإن الصندوق x86_64يوفر ميزة تجريدية لها. إنه يدعم بالفعل الصفحات الضخمة والعديد من الوظائف الأخرى ، باستثناء translate_addr، لذلك نستخدمها بدلاً من إضافة دعم للصفحات الكبيرة إلى تطبيقنا.

أساس التجريد هو سمتان تحددان وظائف الترجمة المختلفة لجدول الصفحة:

  • Mapperتوفر السمة وظائف تعمل على الصفحات. على سبيل المثال ، translate_pageلترجمة هذه الصفحة إلى إطار بنفس الحجم ، وكذلك map_toلإنشاء تعيين جديد في الجدول.
  • السمة MapperAllSizesتعني تطبيق Mapperلجميع أحجام الصفحات. بالإضافة إلى ذلك ، يوفر وظائف تعمل مع صفحات بأحجام مختلفة ، بما في ذلك translate_addrأو عامة translate.

تحدد السمات الواجهة فقط ، لكن لا توفر أي تطبيق. الآن x86_64يوفر الصندوق نوعين يطبقان السمات: MappedPageTableو RecursivePageTable. الأول يتطلب أن يتم عرض كل إطار من جدول الصفحة في مكان ما (على سبيل المثال ، مع إزاحة). يمكن استخدام النوع الثاني إذا تم عرض جدول المستوى الرابع بشكل متكرر.

لدينا كل الذاكرة الفعلية المعينة physical_memory_offset، بحيث يمكنك استخدام نوع MappedPageTable. لتهيئتها ، قم بإنشاء وظيفة جديدة initفي الوحدة النمطية memory:

 use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr; /// Initialize a new MappedPageTable. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn init(physical_memory_offset: u64) -> impl MapperAllSizes { let level_4_table = active_level_4_table(physical_memory_offset); let phys_to_virt = move |frame: PhysFrame| -> *mut PageTable { let phys = frame.start_address().as_u64(); let virt = VirtAddr::new(phys + physical_memory_offset); virt.as_mut_ptr() }; MappedPageTable::new(level_4_table, phys_to_virt) } // make private unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable {…} 

لا يمكننا العودة مباشرة MappedPageTableمن وظيفة لأنها شائعة في نوع الإغلاق. سنتغلب على هذه المشكلة باستخدام بناء جملة impl Trait. ميزة إضافية هي أنه يمكنك عندئذ التبديل إلى kernel RecursivePageTableدون تغيير توقيع الوظيفة. تتوقع

الوظيفة MappedPageTable::newمعلمتين: ارتباط قابل للتغيير لجدول صفحة المستوى 4 وإغلاق phys_to_virtيحول الإطار الفعلي إلى مؤشر جدول صفحة *mut PageTable. بالنسبة للمعلمة الأولى ، يمكننا إعادة استخدام الوظيفة active_level_4_table. للمرة الثانية ، نقوم بإنشاء إغلاق يستخدم physical_memory_offsetلإجراء التحويل.

نحن نجعلها أيضًا active_level_4_tableوظيفة خاصة ، لأنه من الآن فصاعدًا لن يتم استدعاؤها إلا من init.

لاستخدام هذه الطريقةMapperAllSizes::translate_addrبدلاً من وظيفتنا الخاصة memory::translate_addr، نحتاج إلى تغيير بضعة أسطر في kernel_main:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS // new: different imports use blog_os::memory; use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; // new: initialize a mapper let mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let addresses = […]; // same as before for &address in &addresses { let virt = VirtAddr::new(address); // new: use the `mapper.translate_addr` method let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

بعد البدء ، نرى نفس نتائج الترجمة كما كان من قبل ، ولكن الصفحات الضخمة تعمل الآن أيضًا:



كما هو متوقع ، يتم physical_memory_offsetتحويل العنوان الافتراضي إلى عنوان فعلي 0x0. باستخدام وظيفة الترجمة للنوع MappedPageTable، نلغي الحاجة إلى تنفيذ دعم للصفحات الضخمة. لدينا أيضًا إمكانية الوصول إلى وظائف الصفحة الأخرى ، مثل map_toالتي سنستخدمها في القسم التالي. في هذه المرحلة ، لم نعد بحاجة إلى الوظيفة memory::translate_addr، يمكنك حذفها إذا كنت تريد ذلك.

إنشاء تعيين جديد


حتى الآن ، نظرنا فقط في جداول الصفحات ، لكننا لم نغير أي شيء. لنقم بإنشاء مناظرة جديدة لصفحة لم يتم عرضها من قبل.

سوف نستخدم الوظيفة map_toمن الصفة Mapper، لذلك سننظر أولاً في هذه الوظيفة. تشير الوثائق إلى أنها تتطلب أربع وسائط: الصفحة التي نريد عرضها ؛ الإطار الذي يجب تعيين الصفحة إليه. مجموعة من الأعلام لكتابة جدول الصفحة وموزع الإطار frame_allocator. يعد تخصيص الإطارات ضروريًا لأن تعيين هذه الصفحة قد يتطلب إنشاء جداول إضافية تحتاج إلى إطارات غير مستخدمة كتخزين نسخ احتياطي.

وظيفة create_example_mapping


تتمثل الخطوة الأولى في تطبيقنا في إنشاء وظيفة جديدة create_example_mappingتقوم بتعيين هذه الصفحة على 0xb8000الإطار الفعلي لمخزن النصوص VGA. نختار هذا الإطار لأنه يجعل من السهل التحقق مما إذا كان قد تم إنشاء العرض بشكل صحيح: نحتاج فقط إلى الكتابة إلى الصفحة المعروضة مؤخرًا ومعرفة ما إذا كانت تظهر على الشاشة.

الوظيفة create_example_mappingتبدو كالتالي:

 // in src/memory.rs use x86_64::structures::paging::{Page, Size4KiB, Mapper, FrameAllocator}; /// Creates an example mapping for the given page to frame `0xb8000`. pub fn create_example_mapping( page: Page, mapper: &mut impl Mapper<Size4KiB>, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } 

بالإضافة إلى الصفحة pageالتي تريد تعيينها ، تتوقع الدالة مثيلًا mapperو frame_allocator. mapperينفذ النوع السمة Mapper<Size4KiB>التي توفرها الطريقة map_to. المعلمة المشتركة Size4KiBضرورية لأن سمة Mapperغير مشتركة للسمة PageSize، والعمل مع 4 صفحات كيلوبايت القياسية ومع صفحات هائلة 2 و ميب 1 بنك الخليج الدولي. نريد إنشاء 4 صفحات فقط لـ KiB ، حتى نتمكن من استخدامها Mapper<Size4KiB>بدلاً من المتطلبات MapperAllSizes.

للمقارنة ، اضبط العلامة PRESENT، نظرًا لأنه ضروري لجميع الإدخالات الصالحة ، وجعل العلامة WRITABLEقابلة للكتابة في الصفحة المعروضة. دعوةmap_toغير آمن: يمكنك انتهاك أمان الذاكرة باستخدام الوسائط غير الصالحة ، لذلك يجب عليك استخدام كتلة unsafe. للحصول على قائمة بجميع العلامات الممكنة ، راجع قسم "تنسيق جدول الصفحة" في المقالة السابقة . قد تفشل

وظيفة map_to، لذلك يعود Result. نظرًا لأن هذا مجرد مثال على الكود الذي يجب ألا يكون موثوقًا به ، فنحن ببساطة نستخدمه expectللذعر في حالة حدوث خطأ. إذا نجحت ، تُرجع الدالة نوعًا MapperFlushيوفر طريقة سهلة لمسح الصفحة المعروضة مؤخرًا من مخزن الترجمة الديناميكي (TLB) باستخدام الطريقة flush. مثل Result، ينطبق هذا النوع على [ #[must_use]] السمةإصدار تحذير إذا نسينا بطريق الخطأ استخدامه .

زائف FrameAllocator


للاتصال create_example_mapping، يجب عليك أولاً إنشاء FrameAllocator. كما ذكر أعلاه ، يعتمد تعقيد إنشاء عرض جديد على الصفحة الافتراضية التي نريد عرضها. في أبسط الحالات ، يوجد بالفعل جدول المستوى 1 للصفحة ، ونحن بحاجة فقط إلى إنشاء سجل واحد. في أصعب الحالات ، توجد الصفحة في منطقة ذاكرة لم يتم إنشاء المستوى 3 لها بعد ، لذلك يجب عليك أولاً إنشاء جداول صفحات من المستويات 3 و 2 و 1.

لنبدأ بحالة بسيطة ونفترض أنك لا تحتاج إلى إنشاء جداول صفحات جديدة. موزع الإطار الذي يعود دائمًا يكفي لذلك None. نقوم بإنشاء EmptyFrameAllocatorوظيفة العرض هذه للاختبار:

 // in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } } 

تحتاج الآن إلى العثور على صفحة يمكن عرضها دون إنشاء جداول صفحات جديدة. يتم تحميل المُحمل في أول ميغا بايت من مساحة العنوان الافتراضية ، لذلك نعلم أنه يوجد في هذه المنطقة جدول مستوى صالح 1. على سبيل المثال ، يمكننا تحديد أي صفحة غير مستخدمة في منطقة الذاكرة هذه ، على سبيل المثال ، الصفحة الموجودة على العنوان 0x1000.

لاختبار الوظيفة ، نعرض الصفحة أولاً 0x1000، ثم نعرض محتويات الذاكرة:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory; use x86_64::{structures::paging::Page, VirtAddr}; let mut mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; // map a previously unmapped page let page = Page::containing_address(VirtAddr::new(0x1000)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); // write the string `New!` to the screen through the new mapping let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

أولاً ، نقوم بإنشاء مناظرة للصفحة في 0x1000، استدعاء دالة create_example_mappingمع ارتباط قابل للتغيير إلى المثيلات mapperو frame_allocator. هذا يعيّن الصفحة 0x1000إلى إطار المخزن المؤقت للنص VGA ، لذلك يجب أن نرى ما هو مكتوب هناك على الشاشة.

ثم قم بتحويل الصفحة إلى مؤشر أولي واكتب القيمة إلى الإزاحة 400. نحن لا نكتب إلى أعلى الصفحة لأن السطر العلوي من المخزن المؤقت VGA يتم نقله مباشرة من الشاشة كما يلي println. اكتب القيمة 0x_f021_f077_f065_f04eالتي تتوافق مع السلسلة "جديد!" على خلفية بيضاء. كما تعلمنا في المقالة "وضع نص VGA" ، يجب أن تكون الكتابة إلى المخزن المؤقت VGA متقلبة ، لذلك نحن نستخدم هذه الطريقة write_volatile.

عندما نقوم بتشغيل الكود في QEMU ، نرى النتيجة التالية:



بعد الكتابة على الصفحة 0x1000، نقش "جديد!" .لذلك ، قمنا بإنشاء مناظرة جديدة في جداول الصفحات بنجاح.

نجح هذا الترتيب لأنه كان هناك بالفعل جدول المستوى 1 للترتيب 0x1000. عندما نحاول تعيين صفحة لا يوجد لها جدول من المستوى 1 بعد ، map_toتفشل الوظيفة لأنها تحاول تخصيص إطارات منها EmptyFrameAllocatorلإنشاء جداول جديدة. نرى أن هذا يحدث عندما نحاول عرض الصفحة 0xdeadbeaf000بدلاً من 0x1000:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] } 

في حالة بدء هذا الأمر ، يحدث ذعر برسالة الخطأ التالية:

 panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 

لعرض الصفحات التي لا تحتوي على جدول مستوى الصفحة 1 ، تحتاج إلى إنشاء الصحيح FrameAllocator. لكن كيف يمكنك معرفة الإطارات المجانية ومقدار الذاكرة الفعلية المتوفرة؟

اختيار الإطار


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

 // in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } } 

framesيمكن تهيئة الحقل باستخدام مكرر إطار تعسفي. هذا يسمح لك ببساطة تفويض المكالمات إلى allocالأسلوب Iterator::next.

للتهيئة ، BootInfoFrameAllocatorنستخدم بطاقة الذاكرة memory_mapالتي ينقلها محمل الإقلاع كجزء من الهيكل BootInfo. كما هو موضح في قسم معلومات التمهيد ، يتم توفير بطاقة الذاكرة بواسطة BIOS / UEFI الثابتة. يمكن طلب ذلك فقط في بداية عملية التمهيد ، لذا فإن أداة تحميل التشغيل قد استدعت بالفعل الوظائف الضرورية.

تتكون بطاقة الذاكرة من قائمة بنيات MemoryRegionتحتوي على عنوان البداية والطول والنوع (على سبيل المثال ، غير مستخدمة أو محجوزة ، إلخ) لكل منطقة ذاكرة. من خلال إنشاء مكرر ينتج إطارات من مناطق غير مستخدمة ، يمكننا إنشاء إطار صالح BootInfoFrameAllocator.

BootInfoFrameAllocatorتتم التهيئة في وظيفة جديدة init_frame_allocator:

 // in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } } 

تستخدم هذه الوظيفة أداة دمج لتحويل الخريطة الأولية MemoryMapإلى تكرار للإطارات المادية المستخدمة:

  • أولاً ، نحن نسمي طريقة iterتحويل بطاقة الذاكرة إلى مكرر MemoryRegion. ثم نستخدم الطريقة filterلتخطي المناطق المحجوزة أو التي يتعذر الوصول إليها. يقوم المُحمل بتحديث بطاقة الذاكرة لجميع التعيينات التي يقوم بإنشائها ، وبالتالي فإن الإطارات المستخدمة من قبل النواة (رمز ، بيانات أو رصة) أو لتخزين معلومات حول الإقلاع قد تم وضع علامة عليها بالفعل InUseأو بشكل مشابه. وبالتالي ، يمكننا التأكد من Usableعدم استخدام الإطارات في مكان آخر .
  • map range Rust .
  • : into_iter , 4096- step_by . 4096 (= 4 ) — , . , . flat_map map , Iterator<Item = u64> Iterator<Item = Iterator<Item = u64>> .
  • PhysFrame , Iterator<Item = PhysFrame> . BootInfoFrameAllocator .

الآن يمكننا تغيير وظيفتنا kernel_mainلتمرير مثيل BootInfoFrameAllocatorبدلاً من ذلك EmptyFrameAllocator:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); […] } 

هذه المرة نجحت عملية تعيين العناوين ونرى مرة أخرى اللونين الأبيض والأسود "جديد!" .خلف الكواليس ، تقوم الطريقة map_toبإنشاء جداول صفحات مفقودة كما يلي:

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

على الرغم من أن وظيفتنا create_example_mappingهي مجرد نموذج شفرة ، إلا أنه يمكننا الآن إنشاء تعيينات جديدة للصفحات التعسفية. سيكون هذا ضروريًا لتخصيص الذاكرة وتطبيق تعدد العمليات في المقالات المستقبلية.

ملخص


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

لا يمكننا تعيين الذاكرة الفعلية من النواة دون الوصول إلى جدول الصفحة ، لذلك يلزم دعم أداة تحميل الإقلاع. bootloaderينشئ الحامل التعيينات الضرورية من خلال وظائف الشحن الإضافية. يقوم بتمرير المعلومات الضرورية إلى kernel كوسيطة &BootInfoلدالة نقطة الدخول.

من أجل تطبيقنا ، مررنا يدويًا أولاً بجداول الصفحات ، وقمنا بوظيفة الترجمة ، ثم استخدمنا نوع MappedPageTableالصندوقx86_64. لقد تعلمنا أيضًا كيفية إنشاء تعيينات جديدة في جدول الصفحات وكيفية إعدادها FrameAllocatorعلى بطاقة ذاكرة مرسلة بواسطة أداة تحميل التشغيل.

ما التالي؟


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

Source: https://habr.com/ru/post/ar445618/


All Articles