تشرح هذه المقالة كيف يمكن لنواة نظام التشغيل الوصول إلى إطارات الذاكرة الفعلية. سوف ندرس وظيفة تحويل العناوين الافتراضية إلى عناوين فعلية. سنكتشف أيضًا كيفية إنشاء تعيينات جديدة في جداول الصفحات.
تم نشر هذه المدونة على
جيثب . إذا كان لديك أي أسئلة أو مشاكل ، افتح التذكرة المقابلة هناك. جميع مصادر المقال
هنا .
مقدمة
من
المقالة الأخيرة ، تعلمنا حول مبادئ ذاكرة الترحيل وكيف تعمل جداول الصفحات ذات المستوى الرابع على x86_64. لقد وجدنا أيضًا أن المُحمل قد قام بالفعل بإعداد التسلسل الهرمي لجدول الصفحات للنواة لدينا ، وبالتالي فإن النواة تعمل على عناوين افتراضية. هذا يحسن الأمان ، لكن المشكلة تبرز: كيفية الوصول إلى العناوين الفعلية الحقيقية المخزنة في إدخالات جدول الصفحة أو
CR3
؟
في القسم الأول من المقالة ، سنناقش المشكلة والمناهج المختلفة لحلها. بعد ذلك ، نقوم بتنفيذ دالة تتسلل عبر التسلسل الهرمي لجداول الصفحات لتحويل العناوين الافتراضية إلى عناوين فعلية. أخيرًا ، تعلم كيفية إنشاء تعيينات جديدة في جداول الصفحات والعثور على إطارات الذاكرة غير المستخدمة لإنشاء جداول جديدة.
تحديثات التبعية
للعمل ، تحتاج إلى الإصدار
x86_64
0.4.0 أو الأحدث. تحديث التبعية في
Cargo.toml
لدينا:
[dependencies] x86_64 = "0.4.0" # or later
الوصول إلى جداول الصفحات
ليس من السهل الوصول إلى جداول الصفحات من النواة. لفهم المشكلة ، ألق نظرة أخرى على التسلسل الهرمي للجدول ذي المستوى الرابع من المقالة السابقة:
الشيء المهم هو أن كل إدخال صفحة يخزن العنوان
الفعلي للجدول التالي. هذا يتجنب ترجمة هذه العناوين ، مما يقلل من الأداء ويؤدي بسهولة إلى حلقات لا نهاية لها.
المشكلة هي أنه لا يمكننا الوصول مباشرة إلى العناوين الفعلية من النواة ، لأنها تعمل أيضًا على العناوين الافتراضية. على سبيل المثال ، عندما نذهب إلى العنوان
4 KiB
، يمكننا الوصول إلى العنوان
الافتراضي 4 KiB
، وليس إلى العنوان
الفعلي حيث يتم تخزين جدول صفحات المستوى الرابع. إذا كنا نريد الوصول إلى العنوان الفعلي لـ
4 KiB
، فإننا نحتاج إلى استخدام بعض العناوين الافتراضية ، والتي يتم ترجمتها إليها.
لذلك ، للوصول إلى إطارات جداول الصفحات ، تحتاج إلى تعيين بعض الصفحات الافتراضية لهذه الإطارات. هناك طرق مختلفة لإنشاء مثل هذه التعيينات.
1. الحل البسيط هو
العرض المتطابق لجميع جداول الصفحات .
في هذا المثال ، نرى العرض المتماثل للإطارات. العناوين الفعلية لجداول الصفحات هي في الوقت نفسه عناوين افتراضية صالحة ، حتى نتمكن من الوصول بسهولة إلى جداول الصفحات بجميع المستويات ، بدءًا من التسجيل CR3.
ومع ذلك ، فإن هذا النهج يكتنف مساحة العنوان الافتراضية ويجعل من الصعب العثور على مناطق كبيرة متجاورة من الذاكرة الخالية. لنفترض أننا نريد إنشاء مساحة ذاكرة افتراضية 1000 كيلوبايت في الشكل أعلاه ، على سبيل المثال ،
لعرض ملف في الذاكرة . لا يمكننا أن نبدأ بمنطقة
28 KiB
، لأنها تقع على صفحة مشغولة بالفعل في
1004 KiB
. لذلك ، سيتعين عليك البحث أكثر حتى نجد شظية كبيرة مناسبة ، على سبيل المثال ، مع
1008 KiB
. هناك نفس مشكلة التجزئة كما في الذاكرة المقسمة.
بالإضافة إلى ذلك ، يعد إنشاء جداول صفحات جديدة أكثر تعقيدًا ، حيث نحتاج إلى العثور على إطارات مادية لم يتم استخدام صفحاتها المناظرة بعد. على سبيل المثال ، بالنسبة لملفنا ، قمنا بحجز مساحة قدرها 1000 كيلوبايت من الذاكرة
الظاهرية ، بدءًا من العنوان
1008 KiB
. الآن لم يعد بإمكاننا استخدام أي إطار بعنوان فعلي يتراوح ما بين
1000 KiB
و
2008 KiB
، لأنه لا يمكن عرضه بشكل مماثل.
2. هناك خيار آخر وهو
بث جداول الصفحات مؤقتًا فقط عندما تحتاج إلى الوصول إليها. بالنسبة إلى المقارنات المؤقتة ، يلزم تقديم عرض مماثل لجدول المستوى الأول فقط:
في هذا الشكل ، يدير جدول المستوى الأول أول 2 ميجابايت من مساحة العنوان الافتراضية. هذا ممكن لأن الوصول يتم من سجل CR3 من خلال إدخالات فارغة في جداول المستويات 4 و 3 و 2. يترجم السجل مع الفهرس
8 الصفحة الافتراضية عند
32 KiB
إلى إطار فعلي عند
32 KiB
، وبالتالي تحديد الجدول المستوى 1 نفسه. في الشكل يظهر هذا من خلال سهم أفقي.
من خلال الكتابة إلى جدول المستوى الأول المعين بشكل مماثل ، يمكن لـ kernel إنشاء ما يصل إلى 511 مقارنات زمنية (512 ناقص السجل اللازم لتعيين الهوية). في المثال أعلاه ، تطابق النواة السجل الخالي لجدول المستوى 1 مع إطار عند
24 KiB
. أدى هذا إلى إنشاء تعيين مؤقت للصفحة الافتراضية عند
0 KiB
إلى الإطار الفعلي لجدول مستوى الصفحة 2 المشار إليه بواسطة السهم المنقط. يمكن للنواة الآن الوصول إلى جدول المستوى 2 عن طريق الكتابة إلى صفحة تبدأ من
0 KiB
.
وبالتالي ، فإن الوصول إلى إطار تعسفي لجدول الصفحة مع تعيينات مؤقتة يتكون من الإجراءات التالية:
- ابحث عن إدخال مجاني في جدول المستوى 1 المعروض بشكل مماثل.
- قم بتعيين هذا الإدخال على الإطار الفعلي لجدول الصفحة الذي نريد الوصول إليه.
- قم بالوصول إلى هذا الإطار من خلال الصفحة الافتراضية المرتبطة بالإدخال.
- أعد السجل إلى غير مستخدم ، وبالتالي قم بإزالة التعيين المؤقت.
مع هذا الأسلوب ، تظل مساحة العنوان الافتراضية نظيفة ، حيث يتم استخدام نفس الصفحات الظاهرية 512 باستمرار. العيب هو بعض التعقيد ، خاصة وأن المقارنة الجديدة قد تتطلب تغيير عدة مستويات من الجدول ، أي أننا نحتاج إلى تكرار العملية الموضحة عدة مرات.
3. على الرغم من أن كلا النهجين أعلاه يعملان ، إلا أن هناك طريقة ثالثة:
جداول الصفحات العودية . فهو يجمع بين مزايا كلتا الطريقتين: فهو يقارن باستمرار جميع إطارات جداول الصفحات دون الحاجة إلى مقارنات مؤقتة ، كما يبقي الصفحات معيّنة جنبًا إلى جنب ، مع تجنب تجزئة مساحة العنوان الافتراضية. هذه هي الطريقة التي سوف نستخدمها.
الجداول الصفحة العودية
الفكرة هي ترجمة بعض السجلات من جدول المستوى الرابع إلى نفسه. وبالتالي ، فإننا نحتفظ فعليًا بجزء من مساحة العنوان الافتراضية ونعين جميع إطارات الجدول الحالية والمستقبلية لهذه المساحة.
دعونا نلقي نظرة على مثال لفهم كيف يعمل كل هذا:
الاختلاف الوحيد من المثال في بداية المقالة هو سجل إضافي مع الفهرس
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 بت بوضوح على مستويات مختلفة. هذا غير ممكن في النظام السداسي عشر ، حيث يمثل كل حرف أربعة بتات.
التنفيذ
بعد كل هذه النظرية ، يمكننا المضي قدمًا في التنفيذ. ملائمًا ، لم يولِّد المُحمل جداول للصفحات فحسب ، ولكن أيضًا عرض متكرر في السجل الأخير من جدول المستوى 4. لقد فعل المُحمل ذلك لأنه ستكون هناك مشكلة في الدجاج أو البيضة: نحتاج إلى الوصول إلى جدول المستوى 4 لإنشاء خريطة متكررة ولكن لا يمكننا الوصول إليه دون أي عرض.
لقد استخدمنا بالفعل هذا التعيين العودية في نهاية المقالة السابقة للوصول إلى جدول المستوى 4 من خلال العنوان الثابت المرمز
0xffff_ffff_ffff_f000
. إذا قمنا بتحويل هذا العنوان إلى ثماني ومقارنته بالجدول أعلاه ،
0o777
أنه يتوافق تمامًا مع بنية السجل في جدول المستوى 4 مع
RRR
=
0o777
و
AAAA
=
0
0o777
امتداد الإشارة
1
:
هيكل: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
العنوان: 0o_177777_777_777_777_777_0000
بفضل معرفة الجداول العودية ، يمكننا الآن إنشاء عناوين افتراضية للوصول إلى جميع الجداول النشطة. وجعل وظيفة البث.
ترجمة العناوين
كخطوة أولى ، قم بإنشاء دالة تحول عنوانًا ظاهريًا إلى عنوان فعلي ، عبر التسلسل الهرمي لجداول الصفحات:
أولاً ، نقدم متغيرات الفهرس العودية (511 =
0o777
)
0o777
تمديد الإشارة (كل واحدة 1). ثم نقوم بحساب فهارس جداول الصفحات والإزاحة من خلال عمليات bitwise ، كما هو موضح في الرسم التوضيحي:
الخطوة التالية هي حساب العناوين الافتراضية لجداول الصفحات الأربعة ، كما هو موضح في القسم السابق. بعد ذلك ، في الوظيفة ، نقوم بتحويل كل من هذه العناوين إلى روابط
PageTable
. هذه عمليات غير آمنة لأن المترجم لا يمكنه معرفة أن هذه العناوين صالحة.
بعد حساب العنوان ، نستخدم عامل الفهرسة لعرض السجل في جدول المستوى 4. إذا كان هذا السجل صفرًا ، فلا يوجد جدول من المستوى 3 لسجل المستوى 4. وهذا يعني أن
addr
لم
addr
تعيينه إلى أي ذاكرة فعلية. لذلك نحن نعود
None
. خلاف ذلك ، نحن نعلم أن جدول المستوى 3 موجود. ثم نكرر الإجراء ، كما في المستوى السابق.
بعد التحقق من ثلاث صفحات من المستوى الأعلى ، يمكننا أخيرًا قراءة سجل جدول المستوى 1 ، والذي يخبرنا بالإطار الفعلي الذي تم تعيين العنوان به. كخطوة أخيرة ، أضف إزاحة الصفحة إليه - وأرجع العنوان.
إذا علمنا على وجه اليقين أنه تم تعيين العنوان ، فيمكننا الوصول مباشرةً إلى جدول المستوى الأول دون النظر إلى صفحات المستوى الأعلى. ولكن نظرًا لأننا لا نعرف ذلك ، نحتاج أولاً إلى التحقق من وجود جدول من المستوى الأول ، وإلا فإن وظيفتنا ستُرجع صفحة مفقودة خطأ للعناوين غير المتطابقة.
جرب
دعونا نحاول استخدام وظيفة الترجمة للعناوين الافتراضية في دالة
_start
لدينا:
بعد البدء ، نرى النتيجة التالية:
كما هو متوقع ، يترجم العنوان 0xb8000 المرتبط بالمعرف إلى نفس العنوان الفعلي. يتم تحويل صفحة الرموز وصفحة المكدس إلى بعض العناوين الفعلية التعسفية ، والتي تعتمد على كيفية إنشاء المحمل التعيين الأولي للنواة الخاصة بنا.
RecursivePageTable
يوفر x86_64 نوع
RecursivePageTable
الذي ينفذ التجريدات الآمنة لمختلف عمليات جدول الصفحات. باستخدام هذا النوع ، يمكنك تنفيذ وظيفة
translate_addr
بإيجاز شديد:
RecursivePageTable
نوع
RecursivePageTable
بالكامل تتبع ارتباطات غير آمنة لجداول الصفحات ، وبالتالي لم تعد هناك حاجة إلى الشفرة
unsafe
في وظيفة
translate_addr
. تظل الدالة
init
غير آمنة بسبب الحاجة إلى ضمان صحة المستوى الذي تم تمريره
level_4_table_addr
.
يجب تحديث وظيفة
_start
بنا لإعادة تسجيل الوظيفة كما يلي:
الآن ، بدلاً من تمرير
LEVEL_4_TABLE_ADDR
إلى
translate_addr
والوصول إلى جداول الصفحات من خلال مؤشرات خام غير آمنة ، نقوم بتمرير مراجع إلى نوع
RecursivePageTable
. وبالتالي ، لدينا الآن تجريد آمن ودلالات واضحة للملكية. هذا يضمن أننا لن نتمكن من تغيير جدول الصفحات عن طريق الخطأ في الوصول المشترك ، لأن تغييره يتطلب امتلاك حصري لـ
RecursivePageTable
.
تعطي هذه الوظيفة نفس نتيجة وظيفة الترجمة الأصلية المكتوبة يدويًا.
جعل ميزات غير آمنة أكثر أمانًا
memory::init
هي وظيفة غير آمنة: إنها تتطلب كتلة للاتصال بها unsafe
، لأن المتصل يجب أن يضمن تلبية بعض المتطلبات. في حالتنا ، يكون الشرط هو أن العنوان المنقول يتم تعيينه بدقة إلى الإطار الفعلي لجدول صفحة المستوى 4.يتم وضع النص unsafe
الكامل للوظيفة غير الآمنة في الكتلة بحيث يتم تنفيذ جميع أنواع العمليات دون إنشاء كتل إضافية unsafe
. لذلك ، لا نحتاج إلى كتلة غير آمنة لإلغاء التسجيل level_4_table_ptr
: pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr;
المشكلة هي أننا لا نرى على الفور الأجزاء غير الآمنة. على سبيل المثال ، دون النظر إلى تعريف الوظيفة ، RecursivePageTable::new
لا يمكننا القول ما إذا كانت آمنة أم لا. لذلك من السهل جدًا تخطي بعض الرموز غير الآمنة عن طريق الخطأ.لتجنب هذه المشكلة ، يمكنك إضافة وظيفة مضمنة آمنة:
والآن ، unsafe
يُطلب من المجموعة مرة أخرى إلغاء التسجيل level_4_table_ptr
، ونرى على الفور أن هذه هي العمليات غير الآمنة الوحيدة. يحتوي Rust حالياً على RFC مفتوح لتغيير هذه الخاصية غير الناجحة للوظائف غير الآمنة.إنشاء تعيين جديد
عندما نقرأ جداول الصفحات وأنشأنا وظيفة التحويل ، فإن الخطوة التالية هي إنشاء تعيين جديد في التسلسل الهرمي لجدول الصفحة.يعتمد تعقيد هذه العملية على الصفحة الافتراضية التي نريد عرضها. في أبسط الحالات ، يوجد بالفعل جدول لصفحات المستوى 1 لهذه الصفحة ، وعلينا فقط إدخال إدخال واحد. في أصعب الحالات ، توجد الصفحة في منطقة الذاكرة التي لا يوجد لها مستوى 3 بعد ، لذلك تحتاج أولاً إلى إنشاء جداول جديدة من المستوى 3 والمستوى 2 والمستوى 1.لنبدأ بحالة بسيطة عندما لا تحتاج إلى إنشاء جداول جديدة. يتم تحميل أداة التحميل في أول ميغا بايت من مساحة العنوان الافتراضية ، لذلك نعلم أنه يوجد في هذه المنطقة جدول مستوى 1. صالح ، على سبيل المثال ، يمكننا تحديد أي صفحة غير مستخدمة في منطقة الذاكرة هذه ، على سبيل المثال ، الصفحة الموجودة على العنوان 0x1000
. نستخدم 0xb8000
إطار المخزن المؤقت للنص VGA كإطار مطلوب . من السهل للغاية التحقق من كيفية عمل ترجمة عناويننا.ننفذه في وظيفة جديدة create_maping
في الوحدة memory
:
تقبل الدالة مرجعًا قابلاً للتغيير RecursivePageTable
(سيتم تغييره) FrameAllocator
، وهو موضح أدناه. ثم يتم تطبيق الوظيفة map_to
في الدرج Mapper
لتعيين الصفحة على العنوان 0x1000
مع الإطار الفعلي على العنوان 0xb8000
. الوظيفة غير آمنة ، لأنه من الممكن انتهاك أمان الذاكرة باستخدام وسائط غير صالحة.بالإضافة إلى الوسائط page
و frame
، map_to
تأخذ الدالة وسيطين آخرين. الوسيطة الثالثة هي مجموعة العلامات لجدول الصفحة. وضعنا العلامة PRESENT
اللازمة لجميع الإدخالات الصالحة WRITABLE
وعلامة الكتابة.يجب أن تكون الوسيطة الرابعة عبارة عن بنية تنفذ السمة FrameAllocator
. هذه الحجة مطلوبة بواسطة الطريقة.map_to
لأن إنشاء جداول صفحات جديدة قد يتطلب إطارات غير مستخدمة. يتطلب تنفيذ هذه الصفة حجة Size4KiB
، وأنواع Page
و PhysFrame
هي عالمية للسمة PageSize
، والعمل مع 4 صفحات كيلوبايت القياسية ومع صفحات هائلة 2 ميجابت / 1 بنك الخليج الدولي. قد تفشلوظيفة map_to
، لذلك يعود Result
. نظرًا لأن هذا مجرد مثال على الكود الذي يجب ألا يكون موثوقًا به ، فإننا ببساطة نستخدمه في expect
حالة من الذعر عند حدوث خطأ. إذا نجحت ، تُرجع الدالة نوعًا MapperFlush
يوفر طريقة سهلة لمسح الصفحة المطابقة مؤخرًا من طريقة المخزن المؤقت للترجمة الترابطية (TLB) flush
. مثلResult
، يستخدم النوع السمة #[must_use]
ويصدر تحذيرًا إذا نسينا تطبيقه عن طريق الخطأ.نظرًا لأننا نعلم أن العنوان 0x1000
لا يتطلب جداول صفحات جديدة ، فيمكنه FrameAllocator
دائمًا العودة None
. لاختبار الوظيفة ، قم بإنشاء هذا EmptyFrameAllocator
:
(إذا allocate_frame
ظهرت رسالة الخطأ "الطريقة ليست عضوًا في الصفة FrameAllocator
" ، فستحتاج إلى الترقية x86_64
إلى الإصدار 0.4.0.)الآن يمكننا اختبار وظيفة الترجمة الجديدة:
أولاً ، نقوم بإنشاء مناظرة للصفحة على العنوان 0x1000
، create_example_mapping
مع استدعاء الوظيفة بواسطة ارتباط قابل للتغيير للمثيل RecursivePageTable
. هذا يترجم الصفحة 0x1000
إلى مخزن مؤقت لنص VGA ، لذلك سنرى بعض النتائج على الشاشة.ثم نكتب قيمة في هذه الصفحة 0xf021f077f065f04e
، والتي تتوافق مع السطر "جديد!" على خلفية بيضاء. لا تحتاج فقط إلى كتابة هذه القيمة مباشرةً في أعلى الصفحة 0x1000
، لأن السطر العلوي سينتقل بعد ذلك من الشاشة println
، ثم يكتبها في الإزاحة 0x900
، الموجودة تقريبًا في منتصف الشاشة. كما نعلم من المقالة "وضع نص VGA" ، يجب أن تكون الكتابة إلى المخزن المؤقت VGA متقلبة ، لذلك نحن نستخدم هذه الطريقة write_volatile
.عندما نقوم بتشغيله في QEMU ، نرى هذا:النقش على الشاشة.تم عمل الكود لأنه كان هناك بالفعل جدول المستوى الأول لعرض الصفحة 0x1000
. إذا حاولنا ترجمة صفحة لا يوجد لها مثل هذا الجدول بعد ، فستُرجع الدالة map_to
خطأً ، لأنها ستحاول تحديد إطارات منها لإنشاء جداول صفحات جديدة EmptyFrameAllocator
. سنرى هذا إذا حاولنا ترجمة الصفحة 0xdeadbeaf000
بدلاً من 0x1000
:
عند البدء ، يبدأ الذعر برسالة الخطأ التالية: ذعر في 'map_to فشل: FrameAllocationFailed' ، /.../result.rs:999 ربما
لعرض الصفحات التي لا تحتوي على جدول مستوى الصفحة 1 ، تحتاج إلى إنشاء الصحيح FrameAllocator
. لكن كيف يمكنك معرفة الإطارات المجانية ومقدار الذاكرة الفعلية المتوفرة؟معلومات التمهيد
تحتوي أجهزة الكمبيوتر المختلفة على كميات مختلفة من الذاكرة الفعلية وتختلف المساحات المختلفة المحجوزة بواسطة أجهزة مثل VGA. فقط BIOS أو UEFI الثابتة تعرف بالضبط مناطق الذاكرة التي يمكن استخدامها والمحجوزة. يوفر كل من معايير البرنامج الثابت وظائف للحصول على بطاقة تخصيص الذاكرة ، ولكن لا يمكن استدعاؤها إلا في بداية التنزيل. لذلك ، طلب أداة تحميل التشغيل الخاصة بنا بالفعل هذه المعلومات (وغيرها) من BIOS.لتمرير المعلومات إلى kernel الخاص بنظام التشغيل ، فإن المُحمل كوسيطة عند استدعاء الوظيفة _start
يعطي رابطًا إلى بنية معلومات التمهيد. أضف هذه الحجة إلى وظيفتنا:
BootInfo
لا تزال البنية قيد الانتهاء ، لذا لا تفاجأ عند تعطلها عند الترقية إلى الإصدارات المستقبلية من محمل الإقلاع غير المتوافق مع semver . لديه حاليا ثلاثة مجالات p4_table_addr
، memory_map
و package
:p4_table_addr
يحتوي الحقل على عنوان افتراضي متكرر لجدول صفحات المستوى 4. وبفضل هذا ، ليس من الضروري تسجيل العنوان بجد 0o_177777_777_777_777_777_0000
.
memory_map
يحظى الحقل بأهمية كبيرة ، لأنه يحتوي على قائمة بجميع مناطق الذاكرة وأنواعها (غير المستخدمة أو المحجوزة أو غيرها).
- الحقل
package
هو الوظيفة الحالية لربط البيانات الإضافية بالجرافة. لم يكتمل التنفيذ ، حتى نتمكن من تجاهله الآن.
قبل استخدام الحقل memory_map
لإنشاء الحقل الصحيح FrameAllocator
، نريد ضمان نوع الوسيطة الصحيح boot_info
.ماكرو entry_point
لأنه _start
يتم استدعاء خارجيا ، لم يتم التحقق من توقيع الوظيفة. هذا يعني أن الوسائط التعسفية لن تؤدي إلى أخطاء في الترجمة ، ولكنها قد تسبب تعطلًا أو سلوكًا غير معروف في وقت التشغيل.للتحقق من التوقيع ، bootloader
يستخدم الصندوق لتحديد وظيفة الصدأ كنقطة إدخال ماكرو entry_point
مع أنواع التحقق من صحتها. نعيد كتابة وظيفتنا لهذا الماكرو:
بالنسبة لنقطة الإدخال ، لم تعد بحاجة إلى استخدام extern "C"
أو no_mangle
، بما أن الماكرو يعين نقطة الدخول الحقيقية ذات المستوى المنخفض _start
. أصبحت الوظيفة kernel_main
الآن وظيفة Rust طبيعية تمامًا ، حتى نتمكن من اختيار اسم اعتباطي لها. من المهم أن يتم كتابتها بالفعل ، بحيث يحدث خطأ في الترجمة إذا قمت بتغيير توقيع الوظيفة ، على سبيل المثال ، عن طريق إضافة وسيطة أو تغيير نوعها.لاحظ أننا الآن نرسل إلى memory::init
عنوان مشفر ، لكن boot_info.p4_table_addr
. وبالتالي ، سيعمل الرمز حتى إذا كان الإصدار المستقبلي من أداة تحميل التشغيل يختار إدخالًا آخر في جدول جدول مستوى الصفحة 4 للعرض المتكرر.اختيار الإطار
الآن ، بفضل المعلومات من BIOS ، لدينا حق الوصول إلى بطاقة تخصيص الذاكرة ، بحيث يمكنك إنشاء موزع إطار عادي. لنبدأ مع الهيكل العظمي العام:
تتم frames
تهيئة الحقل بواسطة تكرار الإطار التعسفي . هذا يتيح لك ببساطة تفويض المكالمات alloc
إلى Iterator :: الطريقة التالية . تتمالتهيئة BootInfoFrameAllocator
في وظيفة جديدة init_frame_allocator
:
تقوم هذه الوظيفة ، باستخدام أداة التجميع ، بتحويل خريطة تخصيص الذاكرة الأصلية إلى مُكرّر للإطارات المادية المستخدمة:iter
MemoryRegion
. filter
, . , , (, ) , InUse
. , , - .
map
range Rust .
- الخطوة الثالثة هي الأكثر صعوبة: نقوم بتحويل كل نطاق إلى مكرر باستخدام الطريقة
into_iter
، ثم حدد كل عنوان 4096 به step_by
. نظرًا لأن حجم الصفحة 4096 بايت (4 كيلوبايت) ، نحصل على عنوان بداية كل إطار. محاذاة الصفحة محاذاة جميع مناطق الذاكرة المستخدمة ، لذلك نحن لسنا بحاجة إلى رمز محاذاة أو تقريب. استبدال map
بـ flat_map
، نحصل عليه Iterator<Item = u64>
بدلاً من ذلك Iterator<Item = Iterator<Item = u64>>
.
- في المرحلة النهائية ، سنحول عناوين البدء إلى أنواع من
PhysFrame
أجل بناء العنوان المطلوب Iterator<Item = PhysFrame>
. ثم استخدم هذا التكرار لإنشاء وإرجاع واحدة جديدة BootInfoFrameAllocator
.
الآن يمكننا تغيير وظيفتنا kernel_main
بحيث تتجاوز المثيل BootInfoFrameAllocator
بدلاً من ذلك EmptyFrameAllocator
:
أصبحت ترجمة العنوان الآن ناجحة - ونرى مرة أخرى الرسالة بالأبيض والأسود "جديد!" على الشاشة .
خلف الكواليس ، تقوم الطريقة map_to
بإنشاء جداول صفحات مفقودة كما يلي:- مقتطفات غير مستخدمة من الإطار
frame_allocator
.
- يطابق إدخال جدول المستوى الأعلى مع هذا الإطار. يمكن الوصول إلى الإطار الآن من خلال جدول صفحات متكرر.
- أصفار الإطار لإنشاء جدول صفحات فارغ وجديد.
- ينتقل إلى جدول المستوى التالي.
على الرغم من أن وظيفتنا create_maping
هي مجرد مثال ، يمكننا الآن إنشاء تعيينات جديدة للصفحات التعسفية. هذا مفيد جدًا عند تخصيص الذاكرة وتطبيق تعدد العمليات في المقالات المستقبلية.ملخص
في هذه المقالة ، تعلمت كيفية استخدام جدول عودي من المستوى 4 لترجمة جميع الإطارات إلى عناوين افتراضية محسوبة. استخدمنا هذه الطريقة لتطبيق وظيفة ترجمة العناوين وإنشاء مناظرة جديدة في جداول الصفحات.رأينا أن إنشاء تعيينات جديدة يتطلب إطارات غير مستخدمة للجداول الجديدة. يمكن تطبيق موزع الإطارات هذا استنادًا إلى معلومات من BIOS والتي يقوم محمل الإقلاع بتمريرها إلى kernel لدينا.ما التالي
في المقالة التالية ، سنقوم بإنشاء منطقة ذاكرة كومة للنواة الخاصة بنا ، والتي سوف تسمح لنا بتخصيص الذاكرة واستخدام أنواع مختلفة من المجموعات .