Multitasking Microkernel OS Development - Scheduler

Setelah Anda membaca langkah-langkah dasar untuk menulis kernel Hello World dari serangkaian artikel yang tersedia di Habré, sekarang saatnya untuk mulai mengembangkan alat-alat paling mendasar secara serius: tumpukan pengalokasi dan penjadwal.

Penting :
1. Serangkaian pelajaran untuk pemula. Tujuannya adalah untuk membentuk gambaran dunia yang holistik. Untuk waktu yang sangat lama saya memiliki teori Tanenbaum di kepala saya dan saya tidak bisa menghubungkannya dengan praktik. Bagi mereka yang memiliki hal yang sama - saya persembahkan artikel ini.
2. Kode adalah yang paling sederhana dan paling bodoh, tetapi sejelas mungkin. Tujuan saya adalah memberi Anda pemahaman sehingga Anda dapat menulis kernel Anda sendiri, jauh lebih keren dari itu.
3. Saya akan menerbitkan repo segera setelah saya menganggapnya siap untuk berbagai macam. Saya menulis sebagian kecil, men-debug, dan merekam tutorial video. Saya tidak punya produk jadi.

Sejujurnya, saya berpikir lama untuk mulai menulis artikel dan melakukan tutorial video tentang topik yang digulingkan. Tetapi hasrat untuk pemrograman sistem dan kurangnya informasi terstruktur dalam bahasa Rusia tetap mendorong saya untuk percobaan ini. Karena itu, jika pekerjaan saya diminati, saya berencana untuk menerbitkan artikel setidaknya sebulan sekali dan tidak lebih dari sekali seminggu.

Saya bermimpi menulis OS saya di kelas sepuluh. Buku Tanenbaum memiliki properti yang menakjubkan. Siapa pun yang menyentuhnya cepat atau lambat mulai bermimpi untuk menulis sistem operasi mereka sendiri. Tapi saya tidak menulis apa pun saat itu, setelah saya menyadari bahwa tidak mungkin untuk mengkompilasi dan menautkan biner bersih dengan kode pada hari mastday. Saya harus belajar Linux, kuliah, dan bekerja. Semua tidak akan ada artinya. Ya, hanya mimpi yang menjadi kenyataan hanya sepuluh tahun kemudian. Sekarang, membuat cetakan membosankan pada React, saya melihat ke belakang, mengingat gairah yang saya habiskan berjam-jam untuk disassembler dan debugger, menghapus pengepak, memecahkan kotak retak, saya membaca banyak artikel dari novel sekolah pada malam hari ... dan saya mengerti bahwa hidup saya bisa berubah secara berbeda. Cukup berbeda. Andai saja saya memiliki seseorang yang dapat membantu saya sejak awal. Tapi waktu sudah habis. Program menjadi sulit dipecah. Kernel Linux telah berkembang ke ukuran yang luar biasa. Hypervisor muncul. Assembler Intel telah menjadi sangat besar sehingga melihat satu daftar perintah dalam manual, setiap antusiasme untuk mempelajarinya menghilang. Kami harus mendapatkan roti di web. Ya Dunia pemrograman sistem telah melewati tahun-tahun emasnya.

Oleh karena itu, untuk semua siswa yang antusiasmenya tidak kehabisan, saya mencurahkan tutorial terperinci tentang cara mengambil dan menulis dari awal inti microkernel multitasking paling sederhana dengan cangkangnya sendiri. Dan saya ingin memperingatkan semua orang yang belum muncul bahwa ini adalah perselingkuhan yang tidak berterima kasih dan tidak membantu, meskipun sangat menarik. Dan remaja, seperti yang Anda tahu, memiliki keprihatinan sendiri.

Ayo pergi!

Artikel ini berisi cuplikan video tutorial di studio rumah saya. Itu didedikasikan langsung ke kode.

Mari kita mulai mengembangkan manajer tumpukan.

extern void *kmalloc(size_t size); extern void kfree(void *addr); 

Untuk menulis pengalokasian tumpukan, Anda perlu menyimpan wilayah alamat yang bebas dan sibuk. Ini paling mudah untuk diterapkan sebagai daftar terarah. Implementasi daftar terarah itu sendiri harus dilakukan melalui array statis.

 struct slist_head_t { struct slist_head_t *prev; struct slist_head_t *next; bool is_valid; void *data; } attribute(packed); struct slist_definition_t { size_t base; u_int slots; size_t slot_size; struct slist_head_t *head; struct slist_head_t *tail; }; 

Elemen-elemen dari daftar terbatas tersebut akan menjadi catatan wilayah memori. Terlebih lagi, antara daerah yang berdekatan tidak mungkin ada lubang. Jika ada memori bebas, itu dijelaskan oleh item daftar terpisah.

 struct kheap_entry_t { struct slist_head_t list_head; /* static (array placed) list */ size_t addr; /* physical address */ size_t size; /* memory block size */ bool is_buzy; /* whether block used */ } attribute(packed); struct kheap_entry_t kheap_blocks[KHEAP_MAX_ENTRIES]; struct slist_definition_t kheap_list = { .head = null, .tail = null, .slot_size = sizeof(struct kheap_entry_t), .slots = KHEAP_MAX_ENTRIES, .base = (size_t)kheap_blocks }; 

Pada awal struktur kheap_entry_t adalah slist_head_t, yang memungkinkan Anda untuk dengan mudah memasukkan tipe record heap ke item daftar.

Sekarang pertimbangkan algoritma pengalokasi tumpukan paling sederhana (kmalloc):

  1. Jika tidak ada blok gratis, pilih blok baru dari ukuran yang diperlukan (tetapi tidak lebih dari ukuran heap maksimum).
  2. Kalau tidak, kami melihat melalui semua blok gratis. Jika kami menemukan blok gratis, lihat ukurannya. Jika sudah cukup, ambillah. Jika ada surplus maka buat blok kosong baru dari ukuran surplus. Jika tidak, jika tidak mencukupi dan yang terakhir, kami memperluas batas tumpukan (tetapi tidak lebih dari maksimum).

Algoritma bebas memori akan serupa (kfree):

  1. Temukan blok yang alamatnya dimulai dengan alamat yang akan dibebaskan. Pastikan dia sibuk. Tandai sebagai gratis.
  2. Jika ada tetangga bebas di sebelah kanan atau kiri untuk bersatu dengannya dalam satu blok.

Artikel selanjutnya akan fokus pada implementasi algoritma heap.

Mari menulis penjadwal paling sederhana. Tugas akan terlihat seperti ini:

 struct task_t { struct clist_head_t list_head; /* cyclic list */ u_short tid; /* task id */ 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 */ } attribute(packed); struct clist_definition_t task_list = { .head = null, .slot_size = sizeof(struct task_t) }; 

Kita sudah tahu bagaimana mengalokasikan memori dan karenanya mengatur tugas sebagai daftar dering. Jadi lebih nyaman untuk mengambil tugas berikut ini relatif terhadap yang sekarang dengan status yang diinginkan (kami akan menggunakan status TASK_RUNNING jika tugas dilakukan). Untuk memulainya, kami akan menganggap bahwa semua tugas dilakukan di lingkaran perlindungan kernel (lebih mudah untuk men-debug penjadwal) dan bertahan dengan satu tumpukan. Di masa depan kami akan mempercepat TSS.

Kami memilah-milah tugas, sekarang perencanaan itu sendiri. Tambahkan pengatur waktu ke IDT dan aktifkan interupsi dari garis IRQ yang diinginkan dalam PIC. Dengan menginterupsi timer (dan pada akhir kode inisialisasi kernel), kami mentransfer kontrol ke scheduler, meneruskan alamat kembali dan alamat register yang sebelumnya disimpan dari interupsi timer:

 /* * Handle IRQ0 * void asm_ih_timer(unsigned long *addr) */ asm_ih_timer: cli pushal mov %esp,%ebp mov %ebp,%ebx pushl %ebx # &reg add $32,%ebx pushl %ebx # &ret addr call ih_timer mov %ebp,%esp popal sti iretl 

Di penjadwal, kami memeriksa apakah ada tugas yang dilakukan saat ini. Jika demikian, kami menambah penghitung waktu eksekusi dan melihat apakah itu melebihi kuota. Jika tidak, kembali dengan tenang. Jika melebihi, Anda harus menjadwal ulang tugas. Kami menyimpan statusnya (alamat register yang disimpan dan alamat pengirim yang disimpan dalam tumpukan dengan menyela timer akan sangat berguna).

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

Kami mengambil alamat tumpukan tugas baru dan membentuk bingkai pengembalian untuk perintah iret di sana. Kemudian kita memanggil fungsi assembler untuk mengalihkan konteks.

  /* 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 */ asm_switch_context(next_task->op_registers.u_esp); 

Switch konteks itu sendiri terlihat seperti ini:

 # # Switch context # void asm_switch_context(u32 esp) # asm_switch_context: mov 4(%esp),%ebp # ebp = esp mov %ebp,%esp popal sti iretl 

Penjadwal sudah siap.

Lihat kode lengkap dalam tutorial video yang dilampirkan pada artikel. Ini mempertimbangkan tidak hanya penjadwal, tetapi juga proses pemuatan, kompilasi, dan konfigurasi IDT bagi mereka yang telah lupa:

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


All Articles