Pada artikel sebelumnya, kami belajar cara bekerja dengan ruang alamat virtual. Hari ini kami akan menambahkan dukungan multitasking.
Daftar isi
Membangun sistem (make, gcc, gas). Boot awal (multiboot). Luncurkan (qemu). Pustaka C (strcpy, memcpy, strext).
Pustaka C (sprintf, strcpy, strcmp, strtok, va_list ...). Membangun perpustakaan dalam mode kernel dan mode aplikasi pengguna.
Log sistem kernel. Memori video Output ke terminal (kprintf, kpanic, kassert).
Memori dinamis, tumpukan (kmalloc, kfree).
Organisasi memori dan penanganan interupsi (GDT, IDT, PIC, syscall). Pengecualian
Memori virtual (direktori halaman dan tabel halaman).
Proses Perencana Multitasking. Panggilan sistem (bunuh, keluar, ps).Sistem file kernel (initrd), elf, dan internalnya. Panggilan sistem (exec).
Driver perangkat karakter. Panggilan sistem (ioctl, fopen, fread, fwrite). Pustaka C (fopen, fclose, fprintf, fscanf).
Shell sebagai program lengkap untuk kernel.
Mode perlindungan pengguna (ring3). Segmen Status Tugas (tss).
Multitasking
Seperti yang Anda ingat, setiap proses harus memiliki ruang alamatnya sendiri.
Untuk melakukan ini, Anda perlu menghitung halaman yang digunakan oleh proses.
Karena itu, kita harus menggambarkan memori proses:
struct task_mem_t { void* pages; u_int pages_count; void* page_dir; void* page_table; };
Proses entah bagaimana harus saling bertukar informasi. Untuk melakukan ini, kami memperkenalkan konsep pesan:
struct message_t { u_short type; u_int len; u8 data[IPC_MSG_DATA_BUFF_SIZE]; };
Setelah itu, Anda perlu menggambarkan proses itu sendiri sebagai elemen dari daftar siklik.
Daftar siklik di sini nyaman karena dalam penjadwal Anda perlu memilih tugas berikutnya dari setiap sampai daftar kosong.
Dengan ini kita menghapus kasus degenerasi, yang berarti kita menyederhanakan logika.
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);
Kami akan mengalokasikan kuota untuk setiap proses, jumlah gangguan penghitung waktu setelah mana proses akan dijadwal ulang.
#define TASK_QUOTA 3
Selanjutnya, Anda perlu menulis fungsi untuk membuat proses baru.
Meskipun kami tidak memiliki level privilege, kami akan menggunakan pemilih kernel.
Kita akan membutuhkan 2 tumpukan untuk masa depan, satu untuk mode pengguna dan satu untuk kernel.
Kami akan menetapkan status awal sebagai TASK_UNINTERRUPTABLE sehingga tugas tidak memiliki waktu untuk menyelesaikan sebelum inisialisasi penuh.
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; }
Proses akan dihapus oleh penjadwal jika status prosesnya adalah TASK_KILLING.
Saat menghapus, kita hanya perlu membebaskan tumpukan, halaman yang dialokasikan untuk bagian data dan kode, dan juga menghancurkan direktori halaman proses.
Secara umum, untuk tumpukan pengguna yang baik, Anda dapat mengalokasikan melalui manajer memori, tetapi untuk kenyamanan, debugging sementara kami menerapkannya di tumpukan kernel, yang selalu ada di direktori halaman.
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); }
Sekarang Anda perlu menulis penjadwal itu sendiri.
Pertama kita perlu memahami apakah tugas saat ini sedang berjalan atau apakah ini yang pertama kali dijalankan.
Jika tugas sedang dieksekusi, Anda perlu memeriksa apakah kuotanya telah habis.
Jika demikian, maka Anda perlu menyimpan kondisinya dengan secara eksplisit menentukan tanda aktifkan interupsi.
Setelah itu, Anda perlu mencari tugas selanjutnya untuk dieksekusi dalam status TASK_RUNNING.
Selanjutnya, Anda harus menyelesaikan tugas saat ini jika dalam status TASK_KILLING.
Setelah itu, kami menyiapkan bingkai tumpukan untuk tugas selanjutnya dan beralih konteks.
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); }
Masih menulis fungsi switching konteks tugas.
Untuk melakukan ini, Anda perlu mengunduh direktori halaman baru dan mengembalikan semua register umum, termasuk bendera.
Anda juga perlu beralih ke tumpukan tugas berikutnya.
Selanjutnya, Anda perlu melakukan pengembalian dari interupsi, karena kami membentuk bingkai tumpukan khusus untuk ini.
Untuk fakta bahwa nanti kita akan memerlukan ini untuk beralih tingkat hak istimewa.
/* * 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
Kami menerapkan mekanisme pengiriman pesan paling sederhana di antara berbagai proses.
Ketika kami mengirim pesan ke suatu proses, ia harus dicairkan jika dibekukan karena menunggu pesan.
Tetapi ketika kita menerima pesan, kita harus menghentikan proses jika antrian pesan kosong.
Setelah itu, Anda perlu mentransfer kontrol ke penjadwal.
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); }
Sekarang Anda perlu menulis panggilan sistem untuk bekerja dengan proses dari aplikasi pengguna.
Di sana kami akan melakukan panggilan sistem untuk mengirim dan menerima pesan.
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; }
Untuk menggunakan panggilan sistem dari pustaka C kita, kita perlu menulis fungsi yang menyebabkan interupsi kernel. Demi kesederhanaan, kami tidak akan menggunakan perintah Intel khusus, karena kami tidak memiliki level privilege. Sebagai argumen, kami akan memberikan nomor fungsi sistem dan argumen yang dibutuhkan.
/* * 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
Ini cukup untuk menerapkan multitasking paling sederhana tanpa dukungan cincin perlindungan. Sekarang buka
tutorial video dan tonton semuanya secara berurutan!
Referensi
Detail dan penjelasan dalam
tutorial video .
Kode sumber
di repositori git (Anda memerlukan cabang lesson7).
Referensi
1. James Molloy. Gulung mainan Anda sendiri UNIX-clone OS.
2. Gigi. Assembler untuk DOS, Windows, Unix
3. Kalashnikov. Assembler mudah!
4. Tanenbaum. Sistem operasi. Implementasi dan pengembangan.
5. Robert Love. Kernel Linux Deskripsi proses pengembangan.