في المقالة السابقة ، درسنا أساسيات العمل في الوضع المحمي IA-32. اليوم حان الوقت لمعرفة كيفية العمل مع مساحة العنوان الافتراضية.
جدول المحتويات
بناء نظام (جعل ، مجلس التعاون الخليجي ، الغاز). التمهيد الأولي (متعدد التمهيد). إطلاق (qemu). مكتبة C (strcpy ، memcpy ، strext).
مكتبة C (sprintf ، strcpy ، strcmp ، strtok ، va_list ...). بناء المكتبة في وضع kernel ووضع تطبيق المستخدم.
سجل نظام النواة. ذاكرة الفيديو الإخراج إلى المحطة (kprintf ، kpanic ، kassert).
الذاكرة الديناميكية ، الكومة (kmalloc ، kfree).
تنظيم الذاكرة والتعامل مع المقاطعة (GDT ، IDT ، PIC ، syscall). الاستثناءات.
الذاكرة الظاهرية (دليل الصفحة وجدول الصفحة).العملية. المجدول. تعدد المهام. مكالمات النظام (القتل ، الخروج ، ملاحظة).
نظام ملفات kernel (initrd) ، قزم ، وداخله. مكالمات النظام (exec).
برامج تشغيل الأجهزة الشخصية. مكالمات النظام (ioctl ، fopen ، fread ، fwrite). مكتبة C (fopen ، fclose ، fprintf ، fscanf).
شل كبرنامج كامل للنواة.
وضع حماية المستخدم (ring3). قسم حالة المهمة (tss).
الذاكرة الافتراضية
هناك حاجة إلى ذاكرة افتراضية حتى يمكن عزل كل عملية عن الأخرى ، أي لا يمكن أن يوقفه. إذا لم تكن هناك ذاكرة افتراضية ، فسنضطر إلى تحميل ملفات قزمية في عناوين مختلفة في الذاكرة في كل مرة. ولكن كما تعلم ، قد تحتوي الملفات القابلة للتنفيذ على روابط لعناوين محددة (مطلقة). لذلك ، عند تجميع قزم ، فمن المعروف بالفعل في أي عنوان في الذاكرة سيتم تحميله (انظر البرنامج النصي رابط). لذلك ، لا يمكننا تحميل ملفين قزم بدون ذاكرة افتراضية. ولكن حتى مع تشغيل الذاكرة الظاهرية ، توجد مكتبات حيوية (مثل .so) يمكن تحميلها في أي عنوان. يمكن تنزيلها على أي عنوان نظرًا لوجود قسم نقل. في هذا القسم ، يتم تسجيل جميع الأماكن التي يتم فيها استخدام العنونة المطلقة ، ويجب على النواة ، عند تحميل ملف قزم ، إصلاح هذه العناوين باستخدام الأقلام ، أي أضف لهم الفرق بين عنوان التنزيل الحقيقي والمطلوب.
سننظر في العمل مع 4 صفحات كيلوبايت. في هذه الحالة ، يمكننا معالجة ما يصل إلى 4 ميغابايت من ذاكرة الوصول العشوائي. هذا يكفي لنا. ستبدو خريطة العنوان بالشكل التالي:
0-1 ميغابايت : لا تلمس.
1-2 ميغابايت : بيانات الرمز و kernel.
2-3 ميغابايت : مجموعة من النواة.
3-4 ميغابايت : تم تحميل الصفحات المخصصة لملفات قزم.
العنوان الخطي (الذي تم الحصول عليه من النموذج المسطح) عند تمكين ترحيل الصفحات في الصفحة لا يساوي العنوان الفعلي. بدلاً من ذلك ، يتم تقسيم العنوان بواسطة الإزاحة (بتات منخفضة الترتيب) ، وفهرس الإدخال في جدول الصفحة ، وفهرس دليل الصفحة (بتات عالية الترتيب). سيكون لكل عملية دليل الصفحات الخاص بها ، وبالتالي جداول الصفحات. هذا هو ما يسمح لك بتنظيم مساحة عنوان افتراضية.
يبدو إدخال دليل الصفحة كما يلي:
struct page_directory_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 zero : 1; u8 page_size : 1; u8 ignored : 1; u8 available : 3; u32 page_table_addr : 20; } attribute(packed);
يبدو عنصر جدول الصفحة كما يلي:
struct page_table_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 dirty : 1; u8 zero : 1; u8 global : 1; u8 available : 3; u32 page_phys_addr : 20; } attribute(packed);
بالنسبة للنواة ، سنصف دليل الصفحة وجدول الصفحة كمتغيرات ثابتة. صحيح ، هناك حاجة إلى محاذاتها على حدود الصفحة.
static struct page_directory_entry_t kpage_directory attribute(aligned(4096)); static struct page_table_entry_t kpage_table[MMU_PAGE_TABLE_ENTRIES_COUNT] attribute(aligned(4096));
سنذهب إلى الطريق البسيط ونجعل مساحة العنوان الفعلية بأكملها في متناول النواة. يجب أن يكون مستوى الامتياز لصفحات kernel مشرفًا حتى لا يصعد أحد إليها. ستشارك العمليات الخفيفة التي يجب تشغيلها في وضع kernel في مساحة العنوان نفسها مع kernel. مع هذه العملية ، سيكون لدينا قائمة انتظار تنفيذ معلقة لمعالجة المقاطعات المعلقة. ولكن المزيد عن ذلك في الدرس حول برامج تشغيل الأجهزة الشخصية. لا يزال يتعين علينا تحقيق تعدد المهام قبل النظر في هذا الموضوع.
قم بإنشاء دليل لصفحات kernel وجدول صفحة مطابق. عند تهيئة النواة ، ستكون نشطة.
extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); kpage_directory.zero = 1; kpage_directory.accessed = 0; kpage_directory.available = 0; kpage_directory.cache_disabled = 0; kpage_directory.ignored = 0; kpage_directory.page_size = 0; kpage_directory.present = 1; kpage_directory.read_write = 1; kpage_directory.user_supervisor = 1; kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; for (int i = 0; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { kpage_table[i].zero = 0; kpage_table[i].accessed = 0; kpage_table[i].available = 0; kpage_table[i].cache_disabled = 0; kpage_table[i].dirty = 0; kpage_table[i].global = 1; kpage_table[i].present = 1; kpage_table[i].read_write = 1; kpage_table[i].user_supervisor = 1; kpage_table[i].write_through = 1; kpage_table[i].page_phys_addr = (i * 4096) >> 12; } }
عندما نقوم بتحميل ملفات قزم ، سنحتاج إلى إنشاء دليل صفحة لعملية المستخدم. يمكنك القيام بذلك من خلال الوظيفة التالية:
extern struct page_directory_entry_t* mmu_create_user_page_directory(struct page_table_entry_t* page_table) { struct page_directory_entry_t* upage_dir; upage_dir = malloc_a(sizeof(struct page_directory_entry_t), 4096); upage_dir->zero = 1; upage_dir->accessed = 0; upage_dir->available = 0; upage_dir->cache_disabled = 0; upage_dir->ignored = 0; upage_dir->page_size = 0; upage_dir->present = 1; upage_dir->read_write = 1; upage_dir->user_supervisor = 0; upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; return upage_dir; }
بشكل افتراضي ، سوف يحتوي جدول صفحة العملية على صفحات kernel وإدخالات فارغة لصفحات العملية المستقبلية ، أي سجلات مع العلم الحالي مسح وعنوان الصفحة الفعلي في 0.
extern struct page_table_entry_t* mmu_create_user_page_table() { struct page_table_entry_t* upage_table; upage_table = malloc_a(sizeof(struct page_table_entry_t) * MMU_PAGE_TABLE_ENTRIES_COUNT, 4096); memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 0; current->read_write = 1; current->user_supervisor = 0; current->write_through = 1; current->page_phys_addr = 0; } return upage_table; }
نحتاج إلى معرفة كيفية إضافة صفحات فعلية جديدة إلى جدول صفحة العملية ، حيث لن تكون هناك أي صفحات افتراضيًا. سنحتاج إلى ذلك عند تحميل ملفات قزم في الذاكرة ، عندما نقوم بتحميل الأجزاء الموصوفة في رؤوس البرامج. سوف تساعدنا الوظيفة في هذا:
extern bool mmu_occupy_user_page(struct page_table_entry_t* upage_table, void* phys_addr) { for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; if (current->present) { continue; } current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 1; current->read_write = 1; current->user_supervisor = 0; current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; return true; } return false; }
يتم تشغيل وضع الترحيل وإيقاف تشغيله قليلاً في سجل إشارة المعالج.
/* * Enable paging * void asm_enable_paging(void *page_directory) */ asm_enable_paging: mov 4(%esp),%eax # page_directory mov %eax,%cr3 mov %cr0,%eax or $0x80000001,%eax # set PE & PG bits mov %eax,%cr0 ret /* * Disable paging * void asm_disable_paging() */ asm_disable_paging: mov %eax,%cr3 mov %cr0,%eax xor $0x80000000,%eax # unset PG bit mov %eax,%cr0 ret
بعد أن تعلمنا كيفية إنشاء مساحة عناوين العمليات ، نحتاج إلى إدارة الصفحات الفعلية بطريقة أو بأخرى ، أيها مشغول وأيها مجاني. هناك آلية الصورة النقطية لهذا ، بت واحد لكل صفحة. لن نصف الصفحات التي يصل حجمها إلى 3 ميغابايت ، لأنها تنتمي إلى النواة وتكون مشغولة دائمًا. نبدأ في اختيار صفحات المستخدم من 3 إلى 4 ميغابايت.
static u32 bitmap[MM_BITMAP_SIZE];
يتم تخصيص الصفحات الفعلية وإلغاء تخصيصها وفقًا للوظائف التالية. في الواقع ، نجد ببساطة الشيء المطلوب في الخريطة على العنوان الفعلي للصفحة والعكس صحيح. الإزعاج هو أن لدينا حجم محدود لخلية الذاكرة ، لذلك يجب عليك استخدام إحداثيتين: رقم البايت ورقم البت.
extern void* mm_phys_alloc_pages(u_int count) { for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { bool is_found = true; for (int j = 0; j < count; ++j) { is_found = is_found && !mm_get_bit(i + j); } if (is_found) { for (int j = 0; j < count; ++j) { assert(!mm_get_bit(i + j)); mm_set_bit(i + j); } return (void *)mm_get_addr(i); } } return null; } extern bool mm_phys_free_pages(void* ptr, u_int count) { size_t address = (size_t)ptr; assert(address >= MM_AREA_START); assert(address % MM_PAGE_SIZE == 0); for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { for (int j = 0; j < count; ++j) { assert(mm_get_bit(i + j)); mm_clear_bit(i + j); } return true; } } return false; }
هذا يكفي لتقديم الدعم الكامل للذاكرة الافتراضية في النواة.
مراجع
التفاصيل والشروحات في
الفيديو التعليمي .
شفرة المصدر
في مستودع بوابة (تحتاج إلى فرع الدرس 6).
مراجع
1. جيمس مولوي. لفة نظام التشغيل الخاص بك UNIX استنساخ.
2. الأسنان. مجمع ل DOS ، ويندوز ، يونيكس
3. كلاشينكوف. المجمع سهل!
4. Tanenbaum. أنظمة التشغيل. التنفيذ والتطوير.
5. روبرت لوف. نواة لينكس وصف عملية التطوير.