في المقالة السابقة ، تعلمنا كيفية التعامل مع مساحة العنوان الافتراضية. اليوم سنضيف دعم تعدد المهام.
جدول المحتويات
بناء نظام (جعل ، مجلس التعاون الخليجي ، الغاز). التمهيد الأولي (متعدد التمهيد). إطلاق (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).
تعدد المهام
كما تتذكر ، يجب أن يكون لكل عملية مساحة عنوان خاصة بها.
للقيام بذلك ، تحتاج إلى حساب الصفحات المستخدمة من قبل العملية.
لذلك ، يجب أن نصف ذكرى العملية:
struct task_mem_t { void* pages; u_int pages_count; void* page_dir; void* page_table; };
يتعين على العمليات بطريقة ما تبادل المعلومات مع بعضها البعض. للقيام بذلك ، نقدم مفهوم الرسالة:
struct message_t { u_short type; u_int len; u8 data[IPC_MSG_DATA_BUFF_SIZE]; };
بعد ذلك ، تحتاج إلى وصف العملية نفسها كعنصر في قائمة دائرية.
القائمة الدورية هنا مريحة لأنه في المجدول ، تحتاج إلى تحديد التالي من كل مهمة حتى تكون القائمة فارغة.
بهذا نزيل حالة الانحطاط ، مما يعني أننا نقوم بتبسيط المنطق.
struct task_t { struct clist_head_t list_head; u_short tid; char name[8]; struct gp_registers_t gp_registers; struct op_registers_t op_registers; struct flags_t flags; u_int time; bool reschedule; u_short status; int msg_count_in; struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; void* kstack; void* ustack; struct task_mem_t task_mem; } attribute(packed);
سنخصص حصة لكل عملية ، وعدد مرات انقطاع المؤقت التي سيتم بعدها إعادة جدولة العملية.
#define TASK_QUOTA 3
بعد ذلك ، تحتاج إلى كتابة دالة لإنشاء عملية جديدة.
على الرغم من أننا لا نملك مستويات امتياز ، إلا أننا سنستخدم محددات kernel.
سنحتاج إلى مجموعتي تخزين للمستقبل ، واحدة لوضع المستخدم وواحدة للنواة.
سنقوم بتعيين الحالة الأولية على أنها TASK_UNINTERRUPTABLE بحيث لا يكون للمهمة وقت لإكمالها قبل التهيئة الكاملة.
extern bool task_create(u_short tid, void* address, struct task_mem_t *task_mem) { struct task_t* task; struct clist_head_t* entry; entry = clist_insert_entry_after(&task_list, task_list.head); task = (struct task_t*)entry->data; task->kstack = malloc(TASK_KSTACK_SIZE); task->ustack = malloc(TASK_USTACK_SIZE); task->tid = tid; task->name[0] = '\0'; task->status = TASK_UNINTERRUPTABLE; task->msg_count_in = 0; task->time = 0; memcpy(&task->task_mem, task_mem, sizeof(struct task_mem_t)); *(u32*)(&task->flags) = asm_get_eflags() | 0x200; memset(&task->gp_registers, 0, sizeof(struct gp_registers_t)); task->op_registers.cs = GDT_KCODE_SELECTOR; task->op_registers.ds = GDT_KDATA_SELECTOR; task->op_registers.ss = GDT_KSTACK_SELECTOR; task->op_registers.eip = (size_t)address; task->op_registers.cr3 = (size_t)task_mem->page_dir; task->op_registers.k_esp = (u32)task->kstack + TASK_KSTACK_SIZE; task->op_registers.u_esp = (u32)task->ustack + TASK_USTACK_SIZE; printf(MSG_SCHED_TID_CREATE, tid, (u_int)address, task->kstack, task->ustack); return true; }
سيتم حذف العملية بواسطة المجدول إذا كانت حالة العملية هي TASK_KILLING.
عند الحذف ، نحتاج فقط إلى تحرير مجموعات البيانات والصفحات المخصصة لأقسام البيانات والشفرات ، وكذلك إتلاف دليل صفحات العمليات.
بشكل عام ، للحصول على مكدس مستخدم جيد ، يمكنك التخصيص من خلال مدير الذاكرة ، ولكن للراحة ، تصحيح الأخطاء أثناء قيامنا بتطبيقه في كومة الذاكرة المؤقتة kernel ، والتي توجد دائمًا في أدلة الصفحات.
extern void task_delete(struct task_t* task) { printf(MSG_SCHED_TID_DELETE, (u_int)task->tid); assert(task != null); free(task->kstack); free(task->ustack); task->kstack = null; task->ustack = null; if (task->task_mem.pages_count > 0) { mm_phys_free_pages(task->task_mem.pages, task->task_mem.pages_count); task->task_mem.pages = null; task->task_mem.pages_count = 0; } if (task->task_mem.page_dir != null) { mmu_destroy_user_page_directory(task->task_mem.page_dir, task->task_mem.page_table); } clist_delete_entry(&task_list, (struct clist_head_t*)task); }
الآن تحتاج إلى كتابة المجدول نفسه.
نحتاج أولاً إلى فهم ما إذا كانت المهمة الحالية قيد التشغيل أم أن هذه هي العملية الأولى.
إذا تم تنفيذ المهمة ، فأنت بحاجة إلى التحقق من استنفاد حصتها.
إذا كان الأمر كذلك ، فأنت بحاجة إلى حفظ حالته عن طريق تحديد إشارة المقاطعة تمكين بشكل صريح.
بعد ذلك ، تحتاج إلى العثور على المهمة التالية للتنفيذ في الحالة TASK_RUNNING.
بعد ذلك ، تحتاج إلى إكمال المهمة الحالية إذا كانت في حالة TASK_KILLING.
بعد ذلك ، نقوم بإعداد إطار المكدس للمهمة التالية وتبديل السياق.
extern void sched_schedule(size_t* ret_addr, size_t* reg_addr) { struct task_t* next_task = null; if (current_task != null) { current_task->time += 1; if (current_task->time < TASK_QUOTA && !current_task->reschedule) { return; } current_task->time = 0; current_task->reschedule = false; current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16*)((size_t)ret_addr + 4); *(u32*)(¤t_task->flags) = *(u32*)((size_t)ret_addr + 6) | 0x200; current_task->op_registers.u_esp = (size_t)ret_addr + 12; current_task->gp_registers.esp = current_task->op_registers.u_esp; memcpy(¤t_task->gp_registers, (void*)reg_addr, sizeof(struct gp_registers_t)); } if (current_task) { next_task = task_get_next_by_status(TASK_RUNNING, current_task); } else { next_task = task_get_by_status(TASK_RUNNING); tss_set_kernel_stack(next_task->kstack); } assert(next_task != null); if (current_task && current_task->status == TASK_KILLING) { task_delete(current_task); } else { struct task_t* task; task = task_find_by_status(TASK_KILLING); if (task) { task_delete(task); } } next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = (*(u16*)(&next_task->flags)) | 0x200; next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = next_task->op_registers.cs; next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = next_task->op_registers.eip; next_task->gp_registers.esp = next_task->op_registers.u_esp; next_task->op_registers.u_esp -= sizeof(struct gp_registers_t); memcpy((void*)next_task->op_registers.u_esp, (void*)&next_task->gp_registers, sizeof(struct gp_registers_t)); current_task = next_task; printf(MSG_SCHED_NEXT, next_task->tid, next_task->op_registers.u_esp, *ret_addr, next_task->op_registers.eip); asm_switch_ucontext(next_task->op_registers.u_esp, next_task->op_registers.cr3); }
يبقى لكتابة وظيفة تبديل سياق المهمة.
للقيام بذلك ، تحتاج إلى تنزيل دليل صفحة جديد واستعادة جميع السجلات العامة ، بما في ذلك العلامات.
تحتاج أيضًا إلى التبديل إلى مكدس المهمة التالية.
بعد ذلك ، تحتاج إلى العودة من المقاطعة ، حيث قمنا بتكوين إطار مكدس خاص لهذا الغرض.
لحقيقة أننا سنحتاج لاحقًا إلى هذا لتبديل مستويات الامتياز.
/* * Switch context (to kernel ring) * void asm_switch_kcontext(u32 esp, u32 cr3) */ asm_switch_kcontext: mov 4(%esp),%ebp # ebp = esp mov 8(%esp),%eax # eax = cr3 mov %cr0,%ebx # ebx = cr0 and $0x7FFFFFFF,%ebx # unset PG bit mov %ebx,%cr0 mov %eax,%cr3 or $0x80000001,%ebx # set PE & PG bits mov %ebx,%cr0 mov %ebp,%esp popal sti iretl
نحن ننفذ أبسط آلية المراسلة بين العمليات.
عندما نرسل رسالة إلى عملية ما ، يجب حلها إذا كانت مجمدة لأنها تنتظر الرسائل.
ولكن عندما نتلقى رسالة ، يجب علينا تجميد العملية إذا كانت قائمة انتظار الرسائل فارغة.
بعد ذلك ، تحتاج إلى نقل التحكم إلى المجدول.
extern void ksend(u_short tid, struct message_t* msg) { struct task_t* task; task = task_get_by_id(tid); task_pack_message(task, msg); if (task->status == TASK_INTERRUPTABLE) { task->status = TASK_RUNNING; } } extern void kreceive(u_short tid, struct message_t* msg) { struct task_t* task_before; struct task_t* task_after; task_before = sched_get_current_task(); assert(tid == task_before->tid); assert(task_before->status == TASK_RUNNING); if (task_before->msg_count_in == 0) { task_before->status = TASK_INTERRUPTABLE; } sched_yield(); task_after = sched_get_current_task(); assert(task_after == task_before); assert(tid == task_after->tid); assert(task_after->status == TASK_RUNNING); task_extract_message(task_after, msg); assert(msg != null); }
أنت الآن بحاجة إلى كتابة مكالمات النظام للعمل مع العمليات من تطبيقات المستخدم.
هناك سوف نلقي مكالمات النظام لإرسال واستقبال الرسائل.
extern size_t ih_syscall(u_int* function) { size_t params_addr = ((size_t)function + sizeof(u_int)); size_t result = 0; struct task_t *current = sched_get_current_task(); printf(MSG_SYSCALL, *function, current->tid); asm_lock(); switch (*function) { case SYSCALL_KSEND: { u_short tid = *(u_int*)params_addr; ksend(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KRECEIVE: { u_short tid = *(u_int*)params_addr; kreceive(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KILL: { u_short tid = *(u_int*)params_addr; struct task_t* task = task_find_by_id(tid); if (task != null) { assert(task->tid == tid); task->status = TASK_KILLING; task->reschedule = true; result = true; } else { result = false; } break; } case SYSCALL_EXIT: { int errno = *(int*)params_addr; u_int tid = current->tid; printf(MSG_TASK_FINISHED, tid, errno); current->status = TASK_KILLING; sched_yield(); break; } case SYSCALL_TASK_LIST: { result = (size_t)task_get_task_list(); break; } default: unreachable(); } printf(MSG_SYSCALL_RET, *function); asm_unlock(); return result; }
لاستخدام مكالمات النظام من مكتبة C الخاصة بنا ، نحتاج إلى كتابة دالة تؤدي إلى مقاطعة kernel. من أجل البساطة ، لن نستخدم أوامر Intel الخاصة ، نظرًا لأننا لا نملك مستويات امتياز. كوسيطة ، سنقوم بتمرير رقم وظيفة النظام والوسائط التي يحتاجها.
/* * Call kernel * void asm_syscall(unsigned int function, ...) */ asm_syscall: push %ebx push %ebp mov %esp,%ebp mov %ebp,%ebx add $8,%ebx # skip registers add $4,%ebx # skip return address push %ebx # &function int $0x80 mov %ebp,%esp pop %ebp pop %ebx ret
هذا يكفي لتنفيذ أبسط مهام متعددة دون دعم حلقات الحماية. الآن افتح
الفيديو التعليمي وشاهد كل شيء بالترتيب!
مراجع
التفاصيل والشروحات في
الفيديو التعليمي .
شفرة المصدر
في مستودع بوابة (تحتاج إلى فرع الدرس 7).
مراجع
1. جيمس مولوي. لفة نظام التشغيل الخاص بك UNIX استنساخ.
2. الأسنان. مجمع ل DOS ، ويندوز ، يونيكس
3. كلاشينكوف. المجمع سهل!
4. Tanenbaum. أنظمة التشغيل. التنفيذ والتطوير.
5. روبرت لوف. نواة لينكس وصف عملية التطوير.