Im vorherigen Artikel haben wir gelernt, wie man mit virtuellem Adressraum arbeitet. Heute werden wir Multitasking-Unterstützung hinzufügen.
Inhaltsverzeichnis
Build-System (make, gcc, gas). Erster Start (Multiboot). Starten Sie (qemu). C-Bibliothek (strcpy, memcpy, strext).
C-Bibliothek (sprintf, strcpy, strcmp, strtok, va_list ...). Erstellen der Bibliothek im Kernelmodus und im Benutzeranwendungsmodus.
Das Kernel-Systemprotokoll. Videospeicher Ausgabe an das Terminal (kprintf, kpanic, kassert).
Dynamischer Speicher, Heap (kmalloc, kfree).
Organisation der Speicher- und Interrupt-Behandlung (GDT, IDT, PIC, Syscall). Ausnahmen
Virtueller Speicher (Seitenverzeichnis und Seitentabelle).
Prozess. Planer Multitasking. Systemaufrufe (kill, exit, ps).Das Dateisystem des Kernels (initrd), elf und seiner Interna. Systemaufrufe (exec).
Zeichengerätetreiber. Systemaufrufe (ioctl, fopen, fread, fwrite). C-Bibliothek (fopen, fclose, fprintf, fscanf).
Shell als komplettes Programm für den Kernel.
Benutzerschutzmodus (Ring3). Aufgabenstatussegment (tss).
Multitasking
Wie Sie sich erinnern, sollte jeder Prozess einen eigenen Adressraum haben.
Dazu müssen Sie die vom Prozess verwendeten Seiten zählen.
Daher müssen wir die Erinnerung an den Prozess beschreiben:
struct task_mem_t { void* pages; u_int pages_count; void* page_dir; void* page_table; };
Prozesse müssen irgendwie Informationen miteinander austauschen. Dazu führen wir das Konzept einer Nachricht ein:
struct message_t { u_short type; u_int len; u8 data[IPC_MSG_DATA_BUFF_SIZE]; };
Danach müssen Sie den Prozess selbst als Element einer zyklischen Liste beschreiben.
Die zyklische Liste hier ist praktisch, da Sie im Scheduler aus jeder Aufgabe die nächste auswählen müssen, bis die Liste leer ist.
Auf diese Weise entfernen wir den entarteten Fall, was bedeutet, dass wir die Logik vereinfachen.
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);
Wir werden jedem Prozess ein Kontingent zuweisen, die Anzahl der Timer-Unterbrechungen, nach denen der Prozess neu geplant wird.
#define TASK_QUOTA 3
Als nächstes müssen Sie eine Funktion schreiben, um einen neuen Prozess zu erstellen.
Wir haben zwar keine Berechtigungsstufen, verwenden jedoch Kernel-Selektoren.
Wir werden für die Zukunft 2 Stapel benötigen, einen für den Benutzermodus und einen für den Kernel.
Wir setzen den Anfangsstatus als TASK_UNINTERRUPTABLE, damit die Aufgabe vor der vollständigen Initialisierung keine Zeit zum Abschließen hat.
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; }
Der Prozess wird vom Scheduler gelöscht, wenn der Status des Prozesses TASK_KILLING lautet.
Beim Löschen müssen nur die Stapel, Seiten für Daten- und Codeabschnitte freigegeben und das Verzeichnis der Prozessseiten zerstört werden.
Im Allgemeinen können Sie für einen guten Benutzerstapel über den Speichermanager zuweisen, aber der Einfachheit halber das Debuggen, während wir es im Heap des Kernels implementieren, der sich immer in den Seitenverzeichnissen befindet.
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); }
Jetzt müssen Sie den Scheduler selbst schreiben.
Zuerst müssen wir verstehen, ob die aktuelle Aufgabe ausgeführt wird oder ob dies die erste Ausführung ist.
Wenn die Aufgabe ausgeführt wird, müssen Sie überprüfen, ob das Kontingent erschöpft ist.
Wenn ja, müssen Sie den Status speichern, indem Sie das Aktivierungs-Interrupt-Flag explizit angeben.
Danach müssen Sie die nächste auszuführende Aufgabe im Status TASK_RUNNING finden.
Als Nächstes müssen Sie die aktuelle Aufgabe abschließen, wenn sie sich im Status TASK_KILLING befindet.
Danach bereiten wir den Stapelrahmen für die nächste Aufgabe vor und wechseln den Kontext.
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); }
Es bleibt eine Taskkontextumschaltfunktion zu schreiben.
Dazu müssen Sie ein neues Seitenverzeichnis herunterladen und alle allgemeinen Register einschließlich der Flags wiederherstellen.
Sie müssen auch zum Stapel der nächsten Aufgabe wechseln.
Als nächstes müssen Sie vom Interrupt zurückkehren, da wir hierfür einen speziellen Stapelrahmen gebildet haben.
Für die Tatsache, dass wir dies später benötigen, um die Berechtigungsstufen zu wechseln.
/* * 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
Wir implementieren den einfachsten Messaging-Mechanismus zwischen Prozessen.
Wenn wir eine Nachricht an einen Prozess senden, muss sie aufgetaut werden, wenn sie eingefroren ist, weil sie auf Nachrichten wartet.
Wenn wir jedoch eine Nachricht erhalten, müssen wir den Prozess einfrieren, wenn die Nachrichtenwarteschlange leer ist.
Danach müssen Sie die Kontrolle an den Scheduler übertragen.
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); }
Jetzt müssen Sie Systemaufrufe schreiben, um mit Prozessen aus Benutzeranwendungen arbeiten zu können.
Dort werden wir Systemaufrufe auslösen, um Nachrichten zu senden und zu empfangen.
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; }
Um die Systemaufrufe aus unserer C-Bibliothek verwenden zu können, müssen wir eine Funktion schreiben, die einen Kernel-Interrupt verursacht. Der Einfachheit halber werden wir keine speziellen Intel-Befehle verwenden, da wir keine Berechtigungsstufen haben. Als Argument übergeben wir die Nummer der Systemfunktion und die Argumente, die sie benötigt.
/* * 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
Dies reicht völlig aus, um das einfachste Multitasking ohne die Unterstützung von Schutzringen zu implementieren. Öffnen Sie jetzt das
Video-Tutorial und schauen Sie sich alles in der
richtigen Reihenfolge an!
Referenzen
Details und Erklärungen im
Video-Tutorial .
Der Quellcode
im Git-Repository (Sie benötigen den Lektion7-Zweig).
Referenzliste
1. James Molloy. Rollen Sie Ihr eigenes UNIX-Klon-Betriebssystem.
2. Zähne. Assembler für DOS, Windows, Unix
3. Kalaschnikow. Assembler ist einfach!
4. Tanenbaum. Betriebssysteme. Implementierung und Entwicklung.
5. Robert Love. Linux-Kernel Beschreibung des Entwicklungsprozesses.