في هذه المقالة ، سنعرف كيفية تنفيذ دعم ذاكرة الصفحة في جوهرنا. أولاً ، سنقوم بدراسة الطرق المختلفة حتى تصبح إطارات جدول الصفحات الفعلية متاحة للنواة ومناقشة مزاياها وعيوبها. ثم ننفذ وظيفة ترجمة العنوان ووظيفة إنشاء تعيين جديد.
هذه السلسلة من المقالات المنشورة على
جيثب . إذا كان لديك أي أسئلة أو مشاكل ، افتح التذكرة المقابلة هناك. جميع مصادر المقال
في هذا الموضوع .
مقال آخر حول الترحيل؟
إذا تابعت هذه الدورة ، فقد رأيت المقالة "ذاكرة الصفحة: المستوى المتقدم" في نهاية شهر يناير. لكنني تعرضت لانتقادات لجداول الصفحات العودية. لذلك ، قررت إعادة كتابة المقالة ، باستخدام طريقة مختلفة للوصول إلى الإطارات.هنا خيار جديد. لا يزال المقال يوضح كيفية عمل جداول الصفحات العودية ، لكننا نستخدم تطبيقًا أبسط وأكثر قوة. لن نقوم بحذف المقالة السابقة ، لكننا سنضع علامة عليها على أنها قديمة ولن نقوم بتحديثها.
أتمنى أن تستمتع بالخيار الجديد!محتوى
مقدمة
من
المقالة الأخيرة ، تعلمنا حول مبادئ ذاكرة الترحيل وكيف تعمل جداول الصفحات ذات المستوى الرابع على
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:
يفترض هذا الرمز تعيين عودي لسجل المستوى 4 الأخير مع الفهرس
0o777
(511) بشكل متكرر. هذا ليس هو الحال حاليًا ، لذلك لن يعمل الرمز بعد. انظر أدناه لمعرفة كيفية إخبار المحمل بإعداد تعيين متكرر.
كبديل لتنفيذ عمليات bitwise يدويًا ، يمكنك استخدام
RecursivePageTable
نوع
x86_64
قفص ، والذي يوفر تجريدات آمنة لعمليات الجدول المختلفة. على سبيل المثال ، توضح الشفرة أدناه كيفية تحويل عنوان افتراضي إلى العنوان الفعلي المقابل له:
مرة أخرى ، يتطلب هذا الرمز تعيين عودية الصحيح. باستخدام هذا التعيين ،
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
. أضفه:
من المهم تحديد نوع الوسيطة الصحيح ، لأن المترجم لا يعرف نوع التوقيع الصحيح لوظيفة نقطة الدخول الخاصة بنا.ماكرو نقطة الدخول
نظرًا _start
لأنه يتم استدعاء الوظيفة خارجيًا من المُحمل ، لا يتم التحقق من توقيع الوظيفة. هذا يعني أنه يمكننا السماح لها بقبول الوسائط التعسفية دون أخطاء الترجمة ، ولكن هذا سوف يتلف أو يتسبب في سلوك غير محدد وقت التشغيل.لضمان أن وظيفة نقطة الدخول تحتوي دائمًا على التوقيع الصحيح ، bootloader
يوفر الصندوق ماكروًا entry_point
. نعيد كتابة وظيفتنا باستخدام هذا الماكرو:
لم تعد بحاجة إلى استخدام نقطة الإدخال extern "C"
أو no_mangle
، نظرًا لأن الماكرو يحدد لنا نقطة الدخول الحقيقية للمستوى الأدنى _start
. أصبحت الوظيفة kernel_main
الآن وظيفة Rust طبيعية تمامًا ، حتى نتمكن من اختيار اسم اعتباطي لها. الشيء المهم هو أنه يتم فحصه حسب النوع ، لذلك إذا استخدمت التوقيع الخاطئ ، على سبيل المثال ، عن طريق إضافة وسيطة أو تغيير نوعه ، فسيحدث خطأ في الترجمةتطبيق
الآن لدينا إمكانية الوصول إلى الذاكرة الفعلية ، ويمكننا أخيرًا البدء في تنفيذ النظام. أولاً ، ضع في الاعتبار جداول الصفحات النشطة الحالية التي يعمل عليها kernel. في الخطوة الثانية ، قم بإنشاء دالة ترجمة تقوم بإرجاع العنوان الفعلي الذي تم تعيين هذا العنوان الظاهري إليه. في الخطوة الأخيرة ، سنحاول تعديل جداول الصفحات لإنشاء تعيين جديد.أولاً ، قم بإنشاء وحدة نمطية جديدة في الكود memory
:
بالنسبة للوحدة النمطية ، قم بإنشاء ملف فارغ src/memory.rs
.الوصول إلى جداول الصفحات
في نهاية المقالة السابقة ، حاولنا إلقاء نظرة على جدول الصفحات التي يعمل عليها النواة ، لكننا لم نتمكن من الوصول إلى الإطار المادي الذي يشير إليه السجل CR3
. يمكننا الآن مواصلة العمل من هذا المكان: active_level_4_table
ستُرجع الدالة رابطًا إلى جدول صفحات نشط من المستوى الرابع:
أولاً ، نقرأ الإطار الفعلي للجدول النشط للمستوى الرابع من السجل CR3
. ثم نأخذ عنوان البداية الفعلي ونحوله إلى عنوان افتراضي عن طريق الإضافة physical_memory_offset
. أخيرًا ، قم بتحويل العنوان إلى مؤشر أولي من *mut PageTable
خلال الطريقة as_mut_ptr
، ثم قم بإنشاء رابط منه بطريقة غير آمنة &mut PageTable
. نقوم بإنشاء الرابط &mut
بدلاً من ذلك &
، لأننا في وقت لاحق من المقالة سنقوم بتعديل جداول الصفحات هذه.ليست هناك حاجة لإدراج كتلة غير آمنة هنا ، لأن الصدأ يعتبر الجسم كله unsafe fn
كتلة واحدة غير آمنة. هذا يزيد من المخاطر ، لأنه من الممكن إدخال عملية غير آمنة بطريق الخطأ في الأسطر السابقة. كما أنه يجعل من الصعب اكتشاف العمليات غير الآمنة. تم بالفعل إنشاء RFC لتعديل هذا السلوك من Rust.الآن يمكننا استخدام هذه الوظيفة لإخراج سجلات جدول المستوى الرابع:
كما physical_memory_offset
peredaom المقابلة الهيكل الميداني BootInfo
. ثم نستخدم دالة iter
للتكرار من خلال إدخالات جدول الصفحة ومجمع enumerate
لإضافة فهرس i
إلى كل عنصر. يتم عرض الإدخالات غير الفارغة فقط ، لأن جميع الإدخالات 512 لن يتم وضعها على الشاشة.عندما نقوم بتشغيل التعليمات البرمجية ، نرى هذه النتيجة:
نرى عدة سجلات غير فارغة يتم تعيينها إلى جداول المستوى الثالث المختلفة. يتم استخدام العديد من مناطق الذاكرة لأن هناك حاجة إلى مناطق منفصلة لرمز kernel ، ومكدس kernel ، وترجمة الذاكرة الفعلية ، ومعلومات التمهيد.لتصفح جداول الصفحات وإلقاء نظرة على جدول المستوى الثالث ، يمكننا مرة أخرى تحويل الإطار المعروض إلى عنوان افتراضي:
لعرض جداول المستويين الثاني والأول ، كرر هذه العملية ، على التوالي ، لسجلات المستويين الثالث والثاني. كما يمكنك أن تتخيل ، ينمو مقدار الكود بسرعة كبيرة ، لذلك لن ننشر القائمة الكاملة.يعد اجتياز الجداول يدويًا أمرًا مثيرًا للاهتمام لأنه يساعد على فهم كيفية ترجمة المعالج للعناوين. لكننا عادة ما نهتم فقط بعرض عنوان فعلي واحد لعنوان افتراضي محدد ، لذلك دعونا ننشئ وظيفة لهذا الغرض.ترجمة العناوين
لترجمة عنوان افتراضي إلى عنوان فعلي ، يجب علينا الانتقال إلى جدول صفحات من أربعة مستويات حتى نصل إلى الإطار المعين. لنقم بإنشاء وظيفة تؤدي ترجمة العنوان هذه:
نشير إلى وظيفة آمنة translate_addr_inner
للحد من كمية التعليمات البرمجية غير الآمنة. كما هو مذكور أعلاه ، يعتبر الصدأ الجسم بأكمله unsafe fn
ككتلة كبيرة غير آمنة. من خلال استدعاء وظيفة واحدة آمنة ، نجعل مرة أخرى كل عملية واضحة unsafe
.وظيفة داخلية خاصة لها وظيفة حقيقية:
بدلاً من إعادة استخدام الوظيفة ، active_level_4_table
نعيد قراءة إطار المستوى الرابع من السجل CR3
، لأن هذا يبسط تنفيذ النموذج الأولي. لا تقلق ، سوف نحسن الحل قريبًا.توفر البنية VirtAddr
بالفعل طرقًا لحساب الفهارس في جداول الصفحات ذات المستويات الأربعة. نقوم بتخزين هذه الفهارس في صفيف صغير ، لأنه يسمح لك بالحلقة بين جميع الجداول for
. خارج الحلقة ، نتذكر الإطار الأخير الذي تمت زيارته لحساب العنوان الفعلي لاحقًا. frame
يشير إلى إطارات جدول الصفحة أثناء التكرار وإلى الإطار المقترن بعد التكرار الأخير ، أي بعد اجتياز سجل المستوى 1.داخل الحلقة ، نطبق مرة أخرىphysical_memory_offset
لتحويل إطار إلى ارتباط جدول صفحة. ثم نقرأ سجل جدول الصفحة الحالي ونستخدم الدالة PageTableEntry::frame
لاسترداد الإطار المطابق. إذا لم يتم تعيين السجل إلى إطار ، فارجع None
. إذا كان السجل يعرض صفحة ضخمة من 2 ميغا بايت أو 1 جيجا بايت ، فسيصيبنا الذعر حتى الآن.لذلك ، دعونا نتحقق من وظيفة الترجمة في بعض العناوين:
عندما نقوم بتشغيل الشفرة ، نحصل على النتيجة التالية:
كما هو متوقع ، مع تعيين متطابق ، يتم 0xb8000
تحويل العنوان إلى نفس العنوان الفعلي. يتم تحويل صفحة الرموز وصفحة المكدس إلى عناوين فعلية تعسفية ، والتي تعتمد على كيفية إنشاء المحمل التعيين الأولي للنواة الخاصة بنا. physical_memory_offset
يجب أن يشير التعيين إلى العنوان الفعلي 0
، لكنه يفشل ، لأن الترجمة تستخدم صفحات ضخمة لتحقيق الكفاءة. قد يطبق إصدار مستقبلي من أداة تحميل التشغيل نفس التحسين لصفحات kernel و stack.باستخدام MappedPageTable
تعتبر ترجمة العناوين الافتراضية إلى عناوين فعلية مهمة نموذجية لنظام التشغيل OS ، وبالتالي فإن الصندوق x86_64
يوفر ميزة تجريدية لها. إنه يدعم بالفعل الصفحات الضخمة والعديد من الوظائف الأخرى ، باستثناء translate_addr
، لذلك نستخدمها بدلاً من إضافة دعم للصفحات الكبيرة إلى تطبيقنا.أساس التجريد هو سمتان تحددان وظائف الترجمة المختلفة لجدول الصفحة:تحدد السمات الواجهة فقط ، لكن لا توفر أي تطبيق. الآن x86_64
يوفر الصندوق نوعين يطبقان السمات: MappedPageTable
و RecursivePageTable
. الأول يتطلب أن يتم عرض كل إطار من جدول الصفحة في مكان ما (على سبيل المثال ، مع إزاحة). يمكن استخدام النوع الثاني إذا تم عرض جدول المستوى الرابع بشكل متكرر.لدينا كل الذاكرة الفعلية المعينة physical_memory_offset
، بحيث يمكنك استخدام نوع MappedPageTable. لتهيئتها ، قم بإنشاء وظيفة جديدة init
في الوحدة النمطية memory
: use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr;
لا يمكننا العودة مباشرة 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
:
بعد البدء ، نرى نفس نتائج الترجمة كما كان من قبل ، ولكن الصفحات الضخمة تعمل الآن أيضًا:
كما هو متوقع ، يتم 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
تبدو كالتالي:
بالإضافة إلى الصفحة 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
وظيفة العرض هذه للاختبار:
تحتاج الآن إلى العثور على صفحة يمكن عرضها دون إنشاء جداول صفحات جديدة. يتم تحميل المُحمل في أول ميغا بايت من مساحة العنوان الافتراضية ، لذلك نعلم أنه يوجد في هذه المنطقة جدول مستوى صالح 1. على سبيل المثال ، يمكننا تحديد أي صفحة غير مستخدمة في منطقة الذاكرة هذه ، على سبيل المثال ، الصفحة الموجودة على العنوان 0x1000
.لاختبار الوظيفة ، نعرض الصفحة أولاً 0x1000
، ثم نعرض محتويات الذاكرة:
أولاً ، نقوم بإنشاء مناظرة للصفحة في 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
:
في حالة بدء هذا الأمر ، يحدث ذعر برسالة الخطأ التالية: panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5
لعرض الصفحات التي لا تحتوي على جدول مستوى الصفحة 1 ، تحتاج إلى إنشاء الصحيح FrameAllocator
. لكن كيف يمكنك معرفة الإطارات المجانية ومقدار الذاكرة الفعلية المتوفرة؟اختيار الإطار
لجداول الصفحات الجديدة ، تحتاج إلى إنشاء موزع إطارات صحيح. لنبدأ مع الهيكل العظمي العام:
frames
يمكن تهيئة الحقل باستخدام مكرر إطار تعسفي. هذا يسمح لك ببساطة تفويض المكالمات إلى alloc
الأسلوب Iterator::next
.للتهيئة ، BootInfoFrameAllocator
نستخدم بطاقة الذاكرة memory_map
التي ينقلها محمل الإقلاع كجزء من الهيكل BootInfo
. كما هو موضح في قسم معلومات التمهيد ، يتم توفير بطاقة الذاكرة بواسطة BIOS / UEFI الثابتة. يمكن طلب ذلك فقط في بداية عملية التمهيد ، لذا فإن أداة تحميل التشغيل قد استدعت بالفعل الوظائف الضرورية.تتكون بطاقة الذاكرة من قائمة بنيات MemoryRegion
تحتوي على عنوان البداية والطول والنوع (على سبيل المثال ، غير مستخدمة أو محجوزة ، إلخ) لكل منطقة ذاكرة. من خلال إنشاء مكرر ينتج إطارات من مناطق غير مستخدمة ، يمكننا إنشاء إطار صالح BootInfoFrameAllocator
.BootInfoFrameAllocator
تتم التهيئة في وظيفة جديدة init_frame_allocator
:
تستخدم هذه الوظيفة أداة دمج لتحويل الخريطة الأولية 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
:
هذه المرة نجحت عملية تعيين العناوين ونرى مرة أخرى اللونين الأبيض والأسود "جديد!" .
خلف الكواليس ، تقوم الطريقة map_to
بإنشاء جداول صفحات مفقودة كما يلي:- حدد إطارًا غير مستخدم من الإطار المرسل
frame_allocator
.
- إطار الصفر لإنشاء جدول صفحة فارغ جديد.
- قم بتعيين مدخل جدول بمستوى أعلى لهذا الإطار.
- انتقل إلى المستوى التالي من الجدول.
على الرغم من أن وظيفتنا create_example_mapping
هي مجرد نموذج شفرة ، إلا أنه يمكننا الآن إنشاء تعيينات جديدة للصفحات التعسفية. سيكون هذا ضروريًا لتخصيص الذاكرة وتطبيق تعدد العمليات في المقالات المستقبلية.ملخص
في هذه المقالة ، علمنا بالطرق المختلفة للوصول إلى الإطارات الفعلية لجداول الصفحات ، بما في ذلك تعيين الهوية ، وتعيين الذاكرة الفعلية الكاملة ، والتعيين المؤقت ، وجداول الصفحات العودية. اخترنا عرض الذاكرة الفعلية الكاملة كوسيلة بسيطة وقوية.لا يمكننا تعيين الذاكرة الفعلية من النواة دون الوصول إلى جدول الصفحة ، لذلك يلزم دعم أداة تحميل الإقلاع. bootloader
ينشئ الحامل التعيينات الضرورية من خلال وظائف الشحن الإضافية. يقوم بتمرير المعلومات الضرورية إلى kernel كوسيطة &BootInfo
لدالة نقطة الدخول.من أجل تطبيقنا ، مررنا يدويًا أولاً بجداول الصفحات ، وقمنا بوظيفة الترجمة ، ثم استخدمنا نوع MappedPageTable
الصندوقx86_64
. لقد تعلمنا أيضًا كيفية إنشاء تعيينات جديدة في جدول الصفحات وكيفية إعدادها FrameAllocator
على بطاقة ذاكرة مرسلة بواسطة أداة تحميل التشغيل.ما التالي؟
في المقالة التالية ، سنقوم بإنشاء منطقة ذاكرة كومة للنواة الخاصة بنا ، والتي سوف تسمح لنا بتخصيص الذاكرة واستخدام أنواع مختلفة من المجموعات .