تدبير الذاكرة الافتراضية

 

 

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

 

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

 

1- مفهوم الذاكرة الافتراضية

 

ما هي الذاكرة الافتراضية؟ 

في الدرس الثاني عن الوضع المحمي، أدخلنا مفهوم فضاء العنونة، وقلنا أنه عبارة عن مجال تشكله جميع القيم التي يمكن للمعالج إرسالها على ناقلة البيانات. قلنا أيضا أن عدد الخطوط المرصودة لهذه الناقلة يحدد بصفة مباشرة هذا الفضاء. في معالجات x86 عدد الخطوط هو 32 (إذا استثنينا معالجات  Pentium  التي تتوفر على 36 خط لكن استعمالها يتطلب إعدادات خاصة). هذا يعني أن فضاء عنونة هذه المعالجات يمتد من 0 إلى 232 = 4 جيغ (64 جيغ في بنتيوم).

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

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

 

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

 

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

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

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

 

طريقة ترجمة العناوين الافتراضية قد تختلف في التفاصيل بين مختلف المعالجات لكن القاعدة (في الغالب) واحدة. يتم استعمال جدول للترجمة و هذا الجدول يحدد للمعالج كيف يترجم كل عنوان افتراضي إلى نظيره الحقيقي. كل عنوان؟ كم يلزمنا إذن من مدخل في هذا الجدول لتخزين قيم فضاء عنونة من حجم 4جيغ؟ الجواب 4294967296 مدخل.

هذا طبعا غير ممكن عمليا. في الواقع يتم تقسيم فضاء العنونة الافتراضي إلى قطع متساوية من حجم معين تسمى صفحات. في x86 تكون هذه الصفحات عادة بحجم 4كيلب (قد تكون أيضا بحجم 4ميغ أو 2ميغ لكن دائما بإعدادات خاصة). هذا إذن يقلص عدد مداخل الجدول إلى 4كيلب/4جيغ = 1048576 مدخل. بفرض أن طول كل مدخل هو 32 بيت فإن الجدول سيأخذ 4ميغ و هذا مقبول بفرض أننا نتوفر على 4جيغ من الذاكرة الحقيقية. لكن إن كنا نتوفر فقط على 16ميغ فما العمل؟

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

 

 

 

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

يمكننا أيضا أن نمضي طبعا أبعد من ذلك و نستعمل ثلاث مستويات للتبويب (تستعمل في بعض المعالجات مثل إنتل 64 بيت). لكن ذلك لن يكون مفيدا إلا إذا كان حجم الفضاء الافتراضي كبيرا ويتطلب ذلك. معالجات x86 تستعمل مستويين للتبويب. الجدول الأساسي يسمى مجلد الصفحات و الجدول الثانوي يسمى جدول الصفحات.

 

فيما يلي سنتحدث عن كيفية إعداد الذاكرة الافتراضية و جداول الترجمة في هذه المعالجات.

 

الذاكرة الافتراضية في x86

 

في معالجات x86 نميز بين ثلاث أنواع من العناوين:

1-      العنوان المنطقي Logical Adress: و هي النوع الذي تستعمل من طرف البرامج. يكتب على شكل محددالقسم:البعد داخل القسم. في حال عدم تحديد القسم علنا، يتم استعمال المسجل cs لعناوين الشفرة و ds لعناوين البيانات.

2-      العنوان الخطي Linear Adress: يستنتج بعد حساب عنوان بداية محدد القسم و إضافة البعد عليه. مما يعطينا عنوانا واحدا من 32 بيت.

3-      العنوان الحقيقي Physical Adress: يستنتج بعد ترجمة العنوان الخطي إثر مروره على وحدة تدبير الذاكرة. كما رأينا في الفقرة السابقة.

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

رأينا في الفقرة السابقة أن العادة في ترجمة العناوين الافتراضية تعتمد على:

1-      تقسيم الذاكرة إلى مجموعة قطع متساوية تسمى صفحات.

2-      استعمال جدول أو أكثر لترجمة عناوين الصفحات من الفضاء الافتراضي إلى الفضاء الحقيقي (أو إعلام نظام التشغيل في حال ما كان العنوان الافتراضي ليس له مكافئ على الذاكرة الحقيقية).

معالجات x86 لا تشذ عن هذه القاعدة. فهي تقسم الفضاء الافتراضي إلى صفحات من حجم 4كيلب أو 2ميغ أو 4ميغ - تبعا للبيانات الموجودة في جدول الترجمة و بعض المسجلات – في درسنا سنقتصر على الحجم الأكثر شيوعا و هو 4كيلب.

في x86 نستعمل الفهرسة (Paging أي الترجمة عبر الجداول تماما كما نفعل مع الكتب) على مستويين:

- في المستوى الأول نجد مجلد الصفحات Page Directory. يتكون هذا الجدول من 1024 مدخل كل واحد بسعة 32 بيت. و كل مدخل يستعمل كمؤشر على جدول ثانوي و يتعلق الأمر ب

- جدول الصفحات Page Table: أيضا يتكون من 1024 مدخل كل واحد بسعة 32 بيت. كل مدخل يستعمل كمؤشر على صفحة حقيقية في الذاكرة (= صورة صفحة Page Frame).

باستعمال هذا التصميم يمكن إذن لمجلد الصفحات أن يعنون على الأكثر 1024 جدول للصفحات. مع كل جدول يستطيع أن يعنون على الأكثر 1024 صفحة (ما مجموعه 4ميغ). فالفضاء الذي يمكن تغطيته هو إذن

1024x1024 x 4كيلب = 4جيغ

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

الشكل التالي يعطينا بنية مدخل في مجلد الصفحات مع شرح البيانات التي تضمها:

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

13

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

P

R

/

W

U

/

S

P

W

T

P

C

D

A

0

P

S

-

غير مستعملة

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

 و فيما يلي بنية مدخل في جدول الصفحات

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

13

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

P

R

/

W

U

/

S

P

W

T

P

C

D

A

D

P

A

T

G

غير مستعملة

العنوان الحقيقي لصورة الصفحة

 

 

P (Present)

1= صورة الصفحة موجودة في الذاكرة الحقيقية. 0=غير موجودة

R/W (Read/Write)

1= مسموح بالكتابة و القراءة في الصفحة – 0= مسموح بالقراءة فقط

U/S (USER/SUPERVISOR)

1= مسموح في أي درجة أفضلية 0: مسموح فقط عند أفضلية 0

PWT (Page Write Through)

يتحكم في طريقة تخزين الصفحة في المخزونات Caches

 PCD (Page Cache Disbale)

1= تمنع تخزين هذه الصفحة في المخزونات

A (Accessed)

عندما يتم الولوج لصفحة "جديدة" (مثلا حُمّلَت من القرص  الصلب) فإن المعالج يقوم بوضع 1 في هذا البيت.

D (Dirty)

عندما تتم الكتابة في صفحة "جديدة" فإن المعالج يقوم بوضع 1 هذا البيت.

PS (Page Size)

تستعمل في مجلد الصفحات. 1 تعني حجم 4 ميغ (بفرض عدم استعمال عناوين 36 بيت). في هذه الحالة لا نحتاج لجدول الصفحات والبيتات من 22 إلى 31 تشير مباشرة إلى عنوان صورة صفحة من حجم 4 ميغ.

PAT (Page Attribute Table)

متوفر بدءا من بنتيوم 3. يستعمل مع PWT و  PCD

G (Global)

متوفر بدءا من بنتيوم برو Pentium pro.القيمة 1 تعني عدم إخراج الصفحة من TLB (سنرى TLB لاحقا) عند تغيير قيمة CR3. مفيد لصفحات النواة التي لا تتغير بين مختلف البرامج. يستعمل فقط في جداول الصفحات.

 

الآن رأينا كيف يتم تخزين عناوين الصفحات الحقيقية في الجداول. لكن السؤال ما زال قائما: كيف يتم ترجمة العناوين الافتراضية إلى العناوين الحقيقية؟

في بيئة x86 العناوين تكون من حجم 32بيت. لاستخلاص العنوان المكافئ في الذاكرة الحقيقية يتم اتباع المراحل التالية:

1-      يتم استخلاص العشر البيتات الأقوى من العنوان الافتراضي (من 22 إلى 31). و اعتبار العدد الناتج كمؤشر على مجلد الصفحات. مثلا إذا كانت قيمة البيتات العشرة تساوي 45 فإنها تشير إلى المدخل 45 في مجلد الصفحات. بعد العثور على المدخل في مجلد الصفحات يستخلص العنوان الحقيقي لجدول الصفحات المؤشر عليه في هذا المدخل و يحتفظ به للاستعمال في المرحلة الموالية.

2-      يتم استخلاص البيتات العشرة المجاورة (في الوسط من 12 إلى 21). الآن نعتبرها كمؤشر على جدول الصفحات الذي عثر على عنوانه في المرحلة الأولى. مثلا إذا كانت قيمة البيتات العشرة تساوي 32 فإننا نستعمل المدخل 32 في جدول الصفحات المستخلص من المرحلة الأولى. من هذا المدخل تستخلص عنوان صورة صفحة في الذاكرة الحقيقية.

3-      الآن نستخلص ال 12 بيت الأضعف من العنوان (من 0 إلى 11) و نستعمله كبعد داخل صورة الصفحة المستخلصة من المرحلة السابقة. فنحصل في النهاية على العنوان الحقيقي في الذاكرة.

للانطلاق لا بد طبعا من تخزين العنوان الحقيقي لمجلد الصفحات في مكان معروف للمعالج. في x86 يستعمل المسجل CR3 لهذا الغرض.

الشكل التالي يعطي صورة أوضح عن هذه العملية

 

 

 

عندما نقوم بمقابلة صفحة في الفضاء الافتراضي بصورة لها في الفضاء الحقيقي فإننا نسمي هذه العملية ترسيم Mapping صورة الصفحة الحقيقية في صفحة افتراضية.

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

عندما يقلع الحاسب و نمر إلى الوضع المحمي فإن الفهرسة Paging تكون معطلة مسبقا. و قبل تنشيطها يجب علينا أولا صنع جداول الترجمة و ملئها بما يناسبنا. أي علينا صنع الفضاء الافتراضي الذي سنشتغل عليه. بعد ذلك نحمل المسجل CR3 بالعنوان الحقيقي للصفحة التي تحتوي على مجلد الصفحات. لهذا الغرض نحن مضطرون لاستعمال تعليمات الأسمبلي. مثلا إذا كان المتغير pgdir يؤشر على العنوان الحقيقي لمجلد الصفحات فإننا نستعمل التعليمات الآتية

 

mov eax, pgdir

mov CR3, eax

 

 آنذاك فقط يمكننا تنشيط الفهرسة بوضع العلم PG flag – البيت 31 في المسجل CR0 – مما يعطي

 

mov eax, 0x80000000

or CR0, eax

 

هناك طبعا سؤال يفرض نفسه في هذه المرحلة. نحن الآن نشتغل في الفضاء الافتراضي مما يعني أن العناوين الحقيقية لم تعد صالحة – ابتداء من اللحظة التي تم فيها تنشيط الفهرسة -. كيف يمكن للنواة أن تواصل عملها من دون خطأ بعد تنشيط الذاكرة الافتراضية؟

 

هناك عدة أجوبة وبالتالي عدة حلول. أسهل هذه الحلول هو استعمال الترسيم المثلي Identity Mapping لكل المجال الذي يشغله برنامج النواة – شفرة و بيانات – أي ملئ جداول الترجمة بحيث أن كل ترجمة للعناوين الافتراضية تعطي تماما نفس القيمة في الذاكرة الحقيقية – لكل الفضاء الذي تشغله النواة في الذاكرة -. بهذه الطريقة لن يتأثر سير التنفيذ.

 

هناك أيضا الحل المستعمل في لينوكس. عندما تتم ترجمة شفرة لينوكس إلى ملف تنفيذي. فإن مخطوط محرر الروابط يخبر هذا الأخير بإنتاج العناوين كما لو أن النواة ستنفذ في العنوان 0xC0010000. لكن هذا لا يمنع مشكل أن تحميل النواة يتم في العنوان 0x10000 وأنه عند التحميل الفهرسة معطلة. من أجل ذلك فإن لينوكس يقوم في البداية بتنفيذ شفرة مكتوبة بالأسمبلي. هذه الأخيرة تستعمل العناوين بحذر و تطرح عند استعمال أية بيانات القيمة 0xC0000000. ما يعطي القيمة الصحيحة. هذا فقط حالما تقوم بإعداد جداول الفهرسة بحيث أن كل عنوان افتراضي في المساحة التي تشغلها النواة يكافئ العنوان الحقيقي+0xC0000000 و تنشيط هذه الأخيرة. مثلا عنوان بداية النواة الافتراضي 0xC0010000 يكافئه العنوان الحقيقي 0x10000.

 

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

 

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

 

سؤال آخر لا يقل أهمية: كيف نلج إلى جداول الترجمة في الذاكرة الافتراضية؟

 

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

1-      استخلاص البيتات العشرة الأقوى من العنوان الافتراضي و استعمالها كمدخل في مجلد الصفحات.

2-      في حال كان جدول الصفحات موجودا (البيت Present = 1) نمر إلى المرحلة التالية مباشرة. في الحالة العكسية، علينا تخصيص صورة صفحة لجدول الصفحات و تغيير المدخل السابق في مجلد الصفحات ليؤشر على هذه الصورة.

3-      الآن نستعمل العشر البيتات الوسطى كمدخل في جدول الصفحات. ثم نكتب العنوان الحقيقي لصورة الصفحة في هذا المدخل.

 

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

 

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

 

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

 

لهذا قمت بوضع خط تحت بعض الجمل في مراحل الترسيم التي فوق. هذه الأخيرة توضح العمليات التي تلج إلى جداول الترجمة (مجلد الصفحات+جداول الصفحات). و التي ينبغي أن نوفر لها طرقا ملائمة للولوج إليها في الفضاء الافتراضي.

 

كما عي العادة هناك عدة حلول. سأستعرض بعضا منها هنا.

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

 

حل آخر – المستعمل في لينوكس – يعتمد على إمكانية صنع صفحات بحجم 4 ميغ. إذا رجعت إلى الشكل الذي يوضح بنية مجلد الصفحات ستلاحظ أن البيت PS يمكننا من صنع صفحات تمتد على 4 ميغ وأنه في هذه الحالة العنوان في مجلد الصفحات لا يشير إلى جدول للصفحات بل مباشرة إلى صفحة في الذاكرة من حجم 4ميغ. باستعمال هذه التقنية يمكننا أن نصنع ترسيما مثليا لكل الذاكرة  الحقيقية – في حدود فضاء النواة – بحيث يمكن للنواة أن تلج لكل موقع في الذاكرة الحقيقية بما فيها المواقع التي تضم جداول الترجمة. مثلا إذا كنا نتوفر على ذاكرة حقيقية بحجم 64ميغ. سنقوم بترسيم مثلي لهذه الذاكرة كلية في فضاء النواة الافتراضي. ميزة هذه الطريق أنها تسهل كثيرا برمجة باقي مكونات النواة حيث تتيح استخداما مباشرا لصور الصفحات التي تؤخذ من المخصص الحقيقي. كما أن استخدام مداخل من حجم 4ميغ يجنبنا تضييع المساحات التي تأخذها جداول الصفحات.

بالمقابل، فإن استعمال هذه التقنية ممكن فقط في حدود فضاء النواة. مثلا إذا كانت النواة تستعمل فقط 1جيغ من ال4جيغ المشكلة للفضاء الافتراضي فإن أقصى حجم للذاكرة يمكن للنواة ترسيمه بهذه الطريقة هو 1جيغ (إذا أرادت النواة أن ترسم 4جيغ كلها بهذه الطريقة عليها استعمال كل مداخل مجلد الصفحات مما لا يبقي شيئا لبرامج المستعمل). أي أنه للولوج إلى الذاكرة العليا High Memory (الذاكرة التي لا تستطيع النواة استعمالها مباشرة) يجب استعمال تقنيات أخرى (لهذا تجد في نواة لينوكس دالة تدعى kmalloc تخصص فقط في فضاء النواة و دالة أخرى vmalloc تتسطيع أن تخصص في الذاكرة العليا).

 

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

 

مثلا نأخذ آخر 4ميغ في الفضاء الافتراضي (أي ال4ميغ العليا في الذاكرة الافتراضية) لنسمي هذه المنطقة "منطقة الترسيم". نحن نعرف أن كل جدول للصفحات يستطيع عنونة 4 ميغ في الذاكرة الافتراضية. "سنحجز" إذن 4 كيلب الأولى من منطقة الترسيم للجدول الذي يعنون الفضاء الافتراضي من0 الى 4ميغ، ثم نحجز ال4كيلب الثانية للجدول الذي يعنون من 4 إلى 8 و هكذا. ليس ضروريا أن نقوم بتخصيص صور حقيقة لهذه الجداول في الحين، يكفي أن نعرف أن جداول الصفحات ستحتل هذه المواقع المحجوزة لها، وعندما نريد أن نلج لجدول غير موجود في الذاكرة الحقيقية سنرسمه في هذا المكان المحجوز له بالضبط.

 

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

هناك ميزة أساسية لهذه الطريقة، عندما تقوم بتغيير مدخل في مجلد الصفحات فإننا نضرب عصفورين بحجر واحد : تذكر عندما نريد ترسيم صفحة عادية فإننا نحتاج لتغيير مدخلين: مدخل في مجلد الصفحات و آخر في جدول الصفحات. لكن بما أن مجلد الصفحات يشغل الوظيفتين معا بالنسبة لجداول الصفحات (1- مجلد الصفحات لكل الذاكرة + 2- جدول الصفحات لمنطقة الترسيم التي تضم جداول الصفحات). فإنه لترسيم جدول للصفحات في منطقة الترسيم يكفي تغيير المدخل المكافئ في مجلد الصفحات فنتمم العمليتين بضربة واحدة.

 

يمكننا تلخيص هذه التقنية – التي سنسميها الترسيم الذاتي – فيما يلي:

        

-          نضع (بمعنى حجز عناوين افتراضية وليس بالضرورة صور صفحات حقيقية) مجلد الصفحات و جداول في منطقة مسطرة على 4ميغ – 0 أو 4ميغ او 8ميغ...- و من حجم 4ميغ.

-          داخل هذه المنطقة سنرتب جداول الصفحات بحسب ال4ميغ التي تعنونها في الذاكرة: جدول الصفحات 0 يوضع في أول صفحة في منطقة الترسيم، جدول الصفحات رقم 1 يوضع في ثاني صفحة  وهكذا.

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

 

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

        

-          نريد أن نلج إلى مجلد الصفحات نستعمل الصيغة التالية

 

عنوان منطقة الترسيم + (المدخل الذاتي  * حجم الصفحة)

 

-          نريد أن نلج إلى جدول الصفحات رقم "س" نستعمل الصيغة التالية

 

عنوان منطقة الترسيم + (س  * حجم الصفحة)

 

هذا يعني أننا نحتاج فقط لتذكر قيمة المدخل الذاتي لأن عنوان منطقة الترسيم يتحدد تلقائيا بهذه القيمة (المدخل الذاتي * 4ميغ).

 

أعرف أن استيعاب هذه الطريقة أمر صعب في البداية (أذكر أنها سببت لي صداعا عندما قرأتها أول مرة). لكنها تتطلب فقط وقتا للتآلف معها. أما تنجيزها فيظل أمرا سهلا بالمقارنة.

 

 

مخزون الترجمة Translatin lookaside buffers TLB

 

كما رأينا فإن مجرد الولوج إلى موقع في الذاكرة الافتراضية أصبح يتطلب عدة عمليات ولوج وسيطة لمجلد الصفحات و جداول الصفحات. عملية الولوج إلى الذاكرة تعتبر مكلفة بالمقارنة مع سرعة عمل المعالج. من أجل ذلك توجد على الحاسب ذاكرة صغيرة تعمل مثل مخزون cache لترجمة الصفحات. تحتوي هذه الذاكرة على العناوين الحقيقية للصفحات التي تم الولوج إليها مؤخرا. هكذا بدل الولوج إلى جداول الترجمة عند كل ولوج إلى الذاكرة فإن المعالج يقوم أولا بالنظر ما إذا كانت الترجمة متوفرة في مخزون الترجمة TLB. باستعمال هذه التقنية فإن الأداء قد يتحسن بصورة ملحوظة إذا كان البرنامج يستعمل عناوين متقاربة - بلغة تقنية نقول أن البرنامج لديه محلية جيدة Good locality -.

 

ملحوظة: لا يجب الخلط بين مخزون الترجمة TLB و مخزونات المعالج الأخرى L1 & L2 caches. برغم أن مبدأ المحلية يسري على جميع هذه المخزونات.

 

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

 

لتحديث مداخل الTLB يمكننا استعمال التعليمة

invlpg <عنوان الصفحة>

 

 

2- التنجيز

 

في هذا الدرس سننشئ فئة جديدة Pager مهمتها :

 

1-      تنشيط الذاكرة الافتراضية

2-      ترسيم الصفحات أو محوها من الذاكرة الافتراضية.

 

فيما يلي تعريف الفئة Pager.

 

 class Pager

{     …

هذين المتغيرين يعرفان حدود المنطقة الممنوع الترسيم فيها: من بداية ذاكرة الفيديو إلى نهاية النواة

      DWORD baseDeneid, topDeneid;

     

      …

public:

مصنع الفئة يأخذ مؤشرا على مخصص الذاكرة الحقيقية

      Pager(PhysMManager *pmm);

     

     

تقوم بتثبيت جداول الترجمة و تنشيط الذاكرة الافتراضية

      pde_t* initPaging(physAddr_t kbase, physAddr_t ktop);

     

تقوم بترسيم صورة صفحة paddr في الصفحة الافتراضية vaddr

      int map(virtAddr_t vaddr, physAddr_t paddr, DWORD attribs);

 

تقوم بمحو صورة الصفحة في العنوان الافتراضي vaddr

      int unmap(virtAddr_t vaddr);

};

 

لتنجيز الفئة Pager نحتاج أولا لتعريف بعض القيم و المتغيرات مثل منطقة الترسيم

 

رقم المدخل الذاتي = ترتيب مجلد الصفحات في منطقة الترسيم

 

#define SELF_MAP_INDEX  255

عنوان منطقة الترسيم : في آخر 4ميغ في فضاء النواة 4ميغ-1جيغ = المدخل الذاتي*4ميغ

#define     ZONE_MAPPING      (SELF_MAP_INDEX<<22)

 

حجم منطقة الترسيم 4ميغ

#define     ZONE_MAPPING_SIZE (1UL<<22)

 

عنوان مجلد الصفحات = عنوان منطقة الترسيم+(رقم المدخل الذاتي*4كيلب)

#define     PAGE_DIR          (ZONE_MAPPING+(SELF_MAP_INDEX<<12))

 

 

عنوان المدخل الذاتي في مجلد الصفحات=عنوان مجلد الصفحات+المدخل الذاتي*4

هذا المدخل يجعل مجلد الصفحات يؤشر على نفسه كجدول صفحات.

#define     SELF_PDE_MAP      (PAGE_DIR+(SELF_MAP_INDEX*4))

 

عنوان بداية فضاء تطبيقات المستعمل: بعد فضاء النواة مباشرة

#define USER_ZONE       (ZONE_MAPPING+ ZONE_MAPPING_SIZE)

 

حجم الصفحة الافتراضية

#define VPAGE_SHIFT     12                     

#define VPAGE_SIZE      4096

لاستخلاص البيتات ما بعد 12 بيت

#define VPAGE_MASK      (VAGE_SIZE-1)  

#define PDE_FLAGS_MASk  (VPAGE_SIZE-1)

#define PTE_FLAGS_MASk  (VPAGE_SIZE-1)

 

 

#define PDE_SHIFT 22

#define PTE_SHIFT 12

 

تعريف البيتات المشكلة لمداخل جداول الترجمة

#define     PRESENT     1

#define     WRITE 2

#define     USER  4

#define     WRITE_THROUGH     8

#define     CACHE_DISABLE     16

#define     ACESSED     32

#define     DIRTY 64

#define     PT_ATTRIB   128

#define     GLOBAL      256

 

سنستعمل unsigned int لتعريف مداخل مجلد و جداول الصفحات.

typedef     unsigned int      pde_t;      //مدخل في مجلد الصفحات

typedef     unsigned int      pte_t;      //مدخل في جدول الصفحات

#define NULL_PDE  0

#define NULL_PTE  0

 

ثم بعض الدوال المساعدة للولوج

 

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

const DWORD selfIndex = 255;

pte_t *pageTables = (pte_t *) ZONE_MAPPING;

pde_t *pageDir = (pde_t *)PAGE_DIR;

pde_t *selfPde = (pde_t *)SELF_PDE_MAP;

 

بعض الدوال لصنع مداخل جداول الترجمة

inline void makePde(pde_t *pde, physAddr_t paddr, DWORD flags) {

      *pde = paddr | (flags & PDE_FLAGS_MASk);

}

 

inline void makePte(pte_t *pte, physAddr_t paddr, DWORD flags) {

      *pte = paddr | (flags & PTE_FLAGS_MASk);

}

 

دوال لاستخراج رقم المدخل في جداول الترجمة من العنوان الافتراضي

inline DWORD pdeIndex(virtAddr_t vaddr) {

      return      (vaddr >> PDE_SHIFT);

}

 

inline DWORD pteIndex(virtAddr_t vaddr) {

      return      ((vaddr >> PTE_SHIFT)& 0x3ff);

}

تستخرج البعد داخل الصفحة من العنوان الافتراضي

inline DWORD pageOffset(virtAddr_t vaddr) {

      return      (vaddr & VPAGE_MASK);

}

 

تعيد لنا عنوان المدخل في مجلد الصفحات مباشرة من العنوان الافتراضي

inline pde_t* pdeAddr(virtAddr_t vaddr) {

      return      (pageDir+pdeIndex(vaddr));

}

 

 

تعيد لنا عنوان المدخل في جدول الصفحات مباشرة من العنوان الافتراضي

inline pte_t* pteAddr(virtAddr_t vaddr) {

      return      pageTables+(vaddr>>VPAGE_SHIFT);

}

 

للتيقن من أن العنوان لا ينتمي إلى المنطقة المحظورة

inline bool isSystemAddr(virtAddr_t vaddr) {

      return ((vaddr >= baseDeneid &&  vaddr < topDeneid) || !vaddr);

};

 

للتيقن من أن العنوان لا ينتمي إلى منطقة الترسيم

inline bool isMapAddr(virtAddr_t vaddr) {

      return (vaddr >= ZONE_MAPPING && vaddr < USER_ZONE);

};

 

تقوم بتحديث مدخل في TLB. ضرورية بعد تغيير ترسيم الصفحات

void invlPage(virtAddr_t page) {

asm volatile("invlpg %0" ::"m"(*(DWORD *)page));    

}

 

لتحديث جميع مداخل الTLB. يكفي إعادة تحميل المسجل cr3

void invlAllPages() {

      DWORD tmp;

      asm volatile(

            "mov %cr3, %0 \n\t\

            mov %0, %cr3\n\t"

            :"=r"(tmp)::"memory"); 

}

 

لتخصيص صورة صفحة في الذاكرة الحقيقية. إذا استنفذت الذاكرة في هذه المرحلة لا نستطيع الاستمرار

inline DWORD criticalAlloc(PhysMManager *pmm, DWORD flgs) {

            DWORD frame = pmm->allocFrame(flgs);

            if(!frame)

                  // MSG: no enough memory

                  Kernel::FATAL_ERROR("لا توجد ذاكرة كافية\n");

            return frame;

}

 

الطريقة initPaging تقوم بتثبيت جداول الترجمة ثم تنشيط الذاكرة الافتراضية

 

kbase: بداية المنطقة المحظورة – ktop: نهاية النواة (العناوين مسطرة على 4كيلب)

 

pde_t* Pager::initPaging(physAddr_t kbase, physAddr_t ktop) {

تخصيص صورة صفحة لمجلد الصفحات

      pde_t *pd = (pde_t*)criticalAlloc(pmem, 0);

ثم ملئ جميع المداخل ب0 (= غير مموجودة Not present)

for (int i = 0; i < 1024; ++i) {

            pd[i] = 0;

      }

     

تثبيت الترسيم المثلي identity mapping للمنطقة المحظورة

      pde_t *kpde;

      pte_t *kpte;

      for (physAddr_t paddr = kbase; paddr < ktop; paddr+=PAGE_SIZE) {

استخراج المدخل في مجلد الصفحات لkbase. انتبه لا زلنا نشتغل في الذاكرة الحقيقية

            kpde = pd + pdeIndex(paddr);

            if(!(*kpde & PRESENT)) {

جدول الصفحات غير موجود في هذا المدخل، نخصص صورة صفحة و نصنع جدول الصفحات

                  kpte = (pte_t*)criticalAlloc(pmem, 0);

                  for (int i = 0; i < 1024; ++i) {

                        kpte[i] = 0;

                  }

                  makePde(kpde, (physAddr_t)kpte, PRESENT|WRITE);

            }

            makePte(kpte+pteIndex(paddr), paddr, PRESENT|WRITE);

      }

     

تثبيت الترسيم الذاتي self mapping. المدخل selfIndex يشير إلى مجلد الصفحات نفسه

      makePde(pd+selfIndex, (physAddr_t)pd, PRESENT|WRITE);

     

إعداد قيمة المسجل cr3 وتنشيط الفهرسة

      pbdr = ((DWORD)pd);

      asm volatile (

             "movl %0, %%cr3  \n\t\

             movl %%cr0, %%eax \n\t\

             orl $0x80010000, %%eax \n\t\

             movl %%eax, %%cr0      \n\t\

             jmp  1f    \n\t\      #prefetch queue لتنظيف

             1: \n\t\" ::"r"(pbdr):"memory","eax");

      baseDeneid = kbase;

      topDeneid = ktop;

      return pd;

     

}

 

الطريقة map تتكفل بترسيم صور الصفحات في الذاكرة الافتراضية

 

vaddr العنوان الافتراضي الذي سيتم الترسيم فيه

paddr عنوان صورة الصفحة المطلوب ترسيمها

user هل الصفحة توجد في فضاء المستعمل (<> فضاء النواة)

 

int Pager::map(virtAddr_t vaddr, physAddr_t paddr, DWORD attribs) {

أولا التأكد من تسطير العناوين على 4كيلب

      vaddr = pageBase(vaddr);

      paddr = pageBase(paddr);

     

لا يسمح بالترسيم في صورة النواة و منطقة الترسيم

      if(isSystemAddr(vaddr) || isMapAddr(vaddr))

            return INVPARAM;

  

استخلاص عنوان المدخل في مجلد الصفحات

      pde_t *pde = pdeAddr(vaddr);

     

إذا كان المدخل فارغا. نصنع جدول صفحات جديد     

      if(!(*pde & PRESENT)) {

عنوان جدول الصفحة مكافئ لترتيبه في مجلد الصفحات

            pte_t *pt = (pte_t*)(PAGES_TABLES+(pdeIndex(vaddr)*PAGE_SIZE));

            physAddr_t ppt = pmem->allocFrame(0);

            if(!ppt)

                  return NOMEM;

            DWORD flags = PRESENT | WRITE | ((vaddr < USER_ZONE)?0:USER);

 

لترسيم جدول صفحات يكفي تغيير قيمة المدخل في مجلد الصفحات

            makePde(pde, (physAddr_t)ppt, flags);

و لا ننسى تحديث بيانات الTLB

            invlPage((DWORD)pt);

            for (int i = 0; i < 1024; ++i) {

                  pt[i] = 0;

            }

      }

           

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

      pte_t *pte = pteAddr(vaddr);

      makePte(pte, (physAddr_t)paddr, attribs);

      invlPage(vaddr);

      return SUCCESS;

}

 

عكسا، الطريقة unmap تقوم بمحو ترسيم صورة صفحة في الذاكرة

 

vaddr عنوان الصفحة (الافتراضي) المطلوب إزالتها

 

int Pager::unmap(virtAddr_t vaddr) {

     

      vaddr = pageBase(vaddr);

     

غير مسموح إزالة صفحات النواة و منطقة الترسيم

      if(isSystemAddr(vaddr) || isMapAddr(vaddr))

            return INVPARAM;

     

استخراج المدخل في مجلد الصفحات

      pde_t *pde = pdeAddr(vaddr);

     

إذا كان جدول الصفحات موجودا نزيل ترسيم الصفحة منه

      if((*pde & PRESENT)) {

            *(pteAddr(vaddr)) = 0;

      }

      return SUCCESS;

}

 

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

 

void Kernel::initPaging() {

      pager = new Pager(physMManager);

ننادي الطريقة initPaging للعنصر pager. بداية الترسيم المثلي في START_VGA حيث تبدأ ذاكرة الفيجا، ونهايته مع نهاية النواة مسطرة على 4كيلب.

      pager->initPaging(START_VGA, pageTop(kernelEnd));

}

 

كل ما علينا فعله الآن هو نداء Kernel::initPaging في الدالة Kmain.

 

 

 

 

تجربة الدرس

 

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

 

extern "C" void kmain(DWORD magic, multiboot_info *mbi) {

 

...

Kernel::initPhysMManager();

 

قبل تنشيط الذاكرة الافتراضية نخصص صفحة و نكتب فيها العدد 0x55AA

DWORD *paddr = (DWORD *)Kernel::physMManager->allocFrame(0);

      *paddr = 0x55AA;

video->printf("القيمة في العنوان %x = %x\n", (DWORD)paddr, *paddr);

...

ننشط الذاكرة الافتراضية

Kernel::initPaging();

      video->printf("[نجح]\n");

 

      video->printf("تجريب المفهرس الافتراضي\n");

 

سنرسم صورة الصفحة السابقة في عنوانين مختلفين ثم نتأكد من وجود القيمة 0x55AA فيهما عبر الدالة testVirtMem

      DWORD *vad = (DWORD *) 0xA00000;

      DWORD *vad1 = (DWORD *) 0xB10000;

     

      testVirtMem(paddr, vad);

      testVirtMem(paddr, vad1);

 

نمحي ترسيم الصفحة في 0xB10000 ثم نحاول الكتابة في الصفحة. بما أن الصفحة لم تعد موجودة فإن المعالج ينبغي أن يعلن استثناء Page Fault في هذا العنوان

      Kernel::pager->unmap((virtAddr_t)0xB10000);

      *(vad1+1) = 0x51;

...

 

}

 

تقوم بترسيم صورة الصفحة السابقة و التأكد من وجود العدد 0x55AA

void testVirtMem(DWORD *paddr, DWORD *vaddr) {

      int err = Kernel::pager->map((virtAddr_t)vaddr,

                        (physAddr_t)paddr, false);

      if(err<0)

            error(video, err);

     

      if(*vaddr != 0x55AA)

            video->printf("خطأ، القيمة في العنوان %x = %x غير مطابقة\n",

                             (DWORD)vaddr, *vaddr );

      else

            video->printf("نجح،القيمة في العنوان %x = %x مطابقة\n",

                        (DWORD)vaddr, *vaddr);

}

 

ملف الmakefile للترجمة (لا زلت متكاسلا في إدراج الترابطات dependencoes L).

 

CPPFLAGS = -c -fno-builtin -fno-rtti -fno-exceptions

 

.PHONY:  clean

 

OBJS = multiboot.o system.o Machine.o pic.o Kernel.o IntManager.o \

main.o FontMap.o video.o isrs.o exception.o timer.o PFrameStack.o \ physMManager.o errors.o  Pager.o

 

 

all: kernel.elf

 

kernel.elf: $(OBJS) link.lds

      ld -T link.lds $(OBJS) -o kernel.tmp

      objcopy -O elf32-i386 kernel.tmp kernel.elf

      $(RM) kernel.tmp

     

clean:

      $(RM) $(OBJS)

     

     

%.o: %.cpp

      g++ $(CPPFLAGS)  $< -o $@

     

     

%.o: %.s

      nasm -f elf  -DLEADING_USCORE $< -o $@

 

-DLEADING_USCORE فقط إذا كان المترجم يضيف _ على أسماء الرموز (كما في ويندوز)

 

لمحة من الشاشة

 

 

 

لا تنسوا الدرس القادم حيث سننجز مخصص الذاكرة الافتراضية. في انتظار ذلك أرجو قراء موفقة للجميع.

 

ياسين الوافي طنجة – المغرب 17-03-2006 الساعة 17h35mn