Unix-ähnliche Betriebssystementwicklung - Multitasking und Systemaufrufe (7)

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:

/* * Process memory description */ struct task_mem_t { void* pages; /* task physical pages */ u_int pages_count; /* task physical pages count */ void* page_dir; /* page directory */ void* page_table; /* page table */ }; 

Prozesse müssen irgendwie Informationen miteinander austauschen. Dazu führen wir das Konzept einer Nachricht ein:

 struct message_t { u_short type; /* message type */ u_int len; /* data length */ u8 data[IPC_MSG_DATA_BUFF_SIZE]; /* message data */ }; 

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.

 /* * Process descriptor */ struct task_t { struct clist_head_t list_head; /* should be at first */ u_short tid; /* task id */ char name[8]; /* task name */ struct gp_registers_t gp_registers; /* general purpose registers */ struct op_registers_t op_registers; /* other purpose registers */ struct flags_t flags; /* processor flags */ u_int time; /* time of task execution */ bool reschedule; /* whether task need to be rescheduled */ u_short status; /* task status */ int msg_count_in; /* count of incomming messages */ struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; /* task message buffer */ void* kstack; /* kernel stack top */ void* ustack; /* user stack top */ struct task_mem_t task_mem; /* task memory */ } 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.

 /* * Api - Create new task */ extern bool task_create(u_short tid, void* address, struct task_mem_t *task_mem) { struct task_t* task; struct clist_head_t* entry; /* allocate memory */ 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); /* fill data */ 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)); /* set flags */ *(u32*)(&task->flags) = asm_get_eflags() | 0x200; /* set general purpose registers */ memset(&task->gp_registers, 0, sizeof(struct gp_registers_t)); /* set other purpose registers */ 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.

 /* * Api - Delete task by id */ extern void task_delete(struct task_t* task) { printf(MSG_SCHED_TID_DELETE, (u_int)task->tid); assert(task != null); /* free stack memory */ free(task->kstack); free(task->ustack); task->kstack = null; task->ustack = null; /* free user pages memory */ 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; } /* clear resources */ 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.

 /* * Api - Schedule task to run */ extern void sched_schedule(size_t* ret_addr, size_t* reg_addr) { struct task_t* next_task = null; /* finish current task */ if (current_task != null) { /* update running time */ current_task->time += 1; /* check quota exceed */ if (current_task->time < TASK_QUOTA && !current_task->reschedule) { return; /* continue task execution */ } /* reset quota */ current_task->time = 0; current_task->reschedule = false; /* save task state */ current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16*)((size_t)ret_addr + 4); *(u32*)(&current_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(&current_task->gp_registers, (void*)reg_addr, sizeof(struct gp_registers_t)); } /* pick next task */ 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); /* whether should kill current task */ if (current_task && current_task->status == TASK_KILLING) { /* kill current task */ task_delete(current_task); } else { /* try to kill killing tasks */ struct task_t* task; task = task_find_by_status(TASK_KILLING); if (task) { task_delete(task); } } /* prepare context for the next 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)); /* update current task pointer */ current_task = next_task; /* run 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.

 /* * Api - Send message to task */ extern void ksend(u_short tid, struct message_t* msg) { struct task_t* task; /* get target task */ task = task_get_by_id(tid); /* put message to task buffer */ task_pack_message(task, msg); /* whether task has frozen */ if (task->status == TASK_INTERRUPTABLE) { /* defrost task */ task->status = TASK_RUNNING; } } /* * Api - Receive message to task * This function has blocking behaviour */ extern void kreceive(u_short tid, struct message_t* msg) { struct task_t* task_before; /* before yield */ struct task_t* task_after; /* after yield */ /* get current task */ task_before = sched_get_current_task(); assert(tid == task_before->tid); assert(task_before->status == TASK_RUNNING); /* whether task has not incomming messages */ if (task_before->msg_count_in == 0) { /* freeze task */ task_before->status = TASK_INTERRUPTABLE; } /* wait fot message */ sched_yield(); /* get current task */ task_after = sched_get_current_task(); assert(task_after == task_before); assert(tid == task_after->tid); assert(task_after->status == TASK_RUNNING); /* fetch message from buffer */ 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.

 /* * Api - Syscall handler */ 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(); /* handle syscall */ switch (*function) { case SYSCALL_KSEND: { /* send message */ u_short tid = *(u_int*)params_addr; ksend(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KRECEIVE: { /* receive message */ u_short tid = *(u_int*)params_addr; kreceive(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KILL: { /* kill task */ 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: { /* exit task */ 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: { /* get tasks 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.

Source: https://habr.com/ru/post/de468057/


All Articles