Pengembangan OS seperti Unix - Multitasking dan panggilan sistem (7)

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:

/* * 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 */ }; 

Proses entah bagaimana harus saling bertukar informasi. Untuk melakukan ini, kami memperkenalkan konsep pesan:

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

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.

 /* * 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); 

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.

 /* * 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; } 

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.

 /* * 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); } 

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.

 /* * 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); } 

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.

 /* * 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); } 

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.

 /* * 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; } 

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.

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


All Articles