【自制作業系統14】實現鍵盤輸入
一、到目前為止的程式流程圖
為了讓大家清楚目前的程式進度,畫了到目前為止的程式流程圖,如下。(紅色部分就是我們今天要實現的)
二、簡單打通鍵盤中斷
既然要打通鍵盤中斷,那必然需要你回顧一下 【自制作業系統08】中斷 所講述的外部中斷的流程,下面我把圖貼上。
如圖所示,將上圖中的某外部裝置,換成下圖中的具體的鍵盤,就是鍵盤中斷流程啦。簡單說就是:
- 因此每當有擊鍵發生時,鍵盤中的裝置 8048 會把鍵盤掃描碼發給主機板上的裝置 8042。
- 8042 是按位元組來處理的,每處理一個位元組的掃描碼後,將其儲存到自己的 輸出緩衝區 暫存器。
- 然後向中斷代理 8059A 發中斷訊號,這樣我們的鍵盤 中斷處理程式 通過讀取 8042 的輸出緩衝區暫存器,會獲得鍵盤掃描碼。
那我們 CPU 收到的中斷號是多少呢?我們看下面兩段程式碼
1 static void pic_init(void) { 2 3 /*初始化主片 */ 4 outb (PIC_M_CTRL, 0x11); // ICW1: 邊沿觸發,級聯8259, 需要ICW4 5 outb (PIC_M_DATA, 0x20); // ICW2: 起始中斷向量號為0x20, 也就是IR[0-7] 為 0x20 ~ 0x27 6 outb (PIC_M_DATA, 0x04); // ICW3: IR2 接從片 7 outb (PIC_M_DATA, 0x01); // ICW4: 8086 模式, 正常EOI 8 9 /*初始化從片 */ 10 outb (PIC_S_CTRL, 0x11); // ICW1: 邊沿觸發,級聯8259, 需要ICW4 11 outb (PIC_S_DATA, 0x28); // ICW2: 起始中斷向量號為0x28, 也就是IR[8-15]為0x28 ~ 0x2F 12 outb (PIC_S_DATA, 0x02); // ICW3: 設定從片連線到主片的IR2 引腳 13 outb (PIC_S_DATA, 0x01); // ICW4: 8086 模式, 正常EOI 14 15 /*開啟主片上IR0,也就是目前只接受時鐘產生的中斷 */ 16 // 測試鍵盤中斷 0xfd 17 outb (PIC_M_DATA, 0xfd); 18 outb (PIC_S_DATA, 0xff); 19 ... 20 }
1 VECTOR 0x20,ZERO ;時鐘中斷對應的入口 2 VECTOR 0x21,ZERO ;鍵盤中斷對應的入口 3 VECTOR 0x22,ZERO ;級聯用的 4 VECTOR 0x23,ZERO ;串列埠2 對應的入口 5 VECTOR 0x24,ZERO ;串列埠1 對應的入口 6 VECTOR 0x25,ZERO ;並口2 對應的入口 7 VECTOR 0x26,ZERO ;軟盤對應的入口 8 VECTOR 0x27,ZERO ;並口1 對應的入口 9 VECTOR 0x28,ZERO ;實時時鐘對應的入口 10 VECTOR 0x29,ZERO ;重定向 11 VECTOR 0x2a,ZERO ;保留 12 VECTOR 0x2b,ZERO ;保留 13 VECTOR 0x2c,ZERO ;ps/2 滑鼠 14 VECTOR 0x2d,ZERO ;fpu 浮點單元異常 15 VECTOR 0x2e,ZERO ;硬碟 16 VECTOR 0x2f,ZERO ;保留
我們將 8059A 這個裝置的 IR0 埠設定了起始中斷號為 0x20,這是我們自己定義的,也就是說可以改的,再看下硬體定死的東西。
可以看出,鍵盤被固定連線在了 IR1 口上。也就是說,通過硬體的固定連線,以及我們軟體將 IR0 設定為了初始中斷號 0x20,所以導致了我們按下鍵盤後的中斷向量號為 20。這塊說出來真的很簡單很直觀,但我剛學的時候,硬是沒想明白這個道理。
OK,大功告成,接下來我們用之前已有的程式碼就好了,就是將一段中斷程式,對應給 0x21 這個中斷向量號。
keyboard.c
1 #include "keyboard.h" 2 #include "print.h" 3 #include "interrupt.h" 4 #include "io.h" 5 #include "global.h" 6 7 #define KBD_BUF_PORT 0x60 // 鍵盤 buffer 暫存器埠號為 0x60 8 9 // 鍵盤中斷處理程式 10 static void intr_keyboard_handler(void) { 11 put_char('k'); 12 inb(KBD_BUF_PORT); 13 return; 14 } 15 16 // 鍵盤初始化 17 void keyboard_init() { 18 put_str("keyboard init start\n"); 19 register_handler(0x21, intr_keyboard_handler); 20 put_str("keyboard init done\n"); 21 }
init.c
1 ... 2 mem_init(); // 初始化記憶體管理 3 thread_init(); // 初始化執行緒相關結構 4 console_init(); // 控制檯初始化 5 keyboard_init(); // 鍵盤初始化 6 ...
執行後可以看到,每次我們按下鍵盤,就在螢幕上輸出 ‘k',由於我們沒做其他處理,不論按什麼鍵,都會只在螢幕上輸出 ’k',有個細節就是按下一個鍵會輸出兩個‘k’,是因為鍵盤的按下和彈起是會傳輸兩個鍵盤碼,也會發生兩次中斷。
雖然只是簡簡單單輸出一個‘k’,但還是很興奮,我覺得往往最難的地方就是打通和硬體的互動,把控制權交給軟體,剩下的事就掌控在我們手中啦,我們繼續往下看。
三、實現輸入字元緩衝區
這塊我懶了,不想看程式碼了,直接把書中的程式碼全部 copy 過來了。一是因為這塊是為後續的使用者互動程序,也就是 shell 做準備的;二是因為這塊十分繁瑣,又很好理解,簡單說就是,把輸入進來的鍵盤碼轉換成 ASCII 碼,並輸出到一個緩衝區(我們用佇列結構實現)裡,另外意思一下跑兩個執行緒從緩衝區裡拿資料,直接輸出到螢幕上。
那不難想象後續的使用者程序,無非就是 shell 程序讀取緩衝區資料,輸出到螢幕,遇到回車後把整個字串理解一下,交給指定程式去處理,我猜的啊。
所以這塊直接把程式碼放上來。
1 #include "ioqueue.h" 2 #include "interrupt.h" 3 #include "global.h" 4 #include "debug.h" 5 6 /* 初始化io佇列ioq */ 7 void ioqueue_init(struct ioqueue* ioq) { 8 lock_init(&ioq->lock); // 初始化io佇列的鎖 9 ioq->producer = ioq->consumer = NULL; // 生產者和消費者置空 10 ioq->head = ioq->tail = 0; // 佇列的首尾指標指向緩衝區陣列第0個位置 11 } 12 13 /* 返回pos在緩衝區中的下一個位置值 */ 14 static int32_t next_pos(int32_t pos) { 15 return (pos + 1) % bufsize; 16 } 17 18 /* 判斷佇列是否已滿 */ 19 bool ioq_full(struct ioqueue* ioq) { 20 ASSERT(intr_get_status() == INTR_OFF); 21 return next_pos(ioq->head) == ioq->tail; 22 } 23 24 /* 判斷佇列是否已空 */ 25 bool ioq_empty(struct ioqueue* ioq) { 26 ASSERT(intr_get_status() == INTR_OFF); 27 return ioq->head == ioq->tail; 28 } 29 30 /* 使當前生產者或消費者在此緩衝區上等待 */ 31 static void ioq_wait(struct task_struct** waiter) { 32 ASSERT(*waiter == NULL && waiter != NULL); 33 *waiter = running_thread(); 34 thread_block(TASK_BLOCKED); 35 } 36 37 /* 喚醒waiter */ 38 static void wakeup(struct task_struct** waiter) { 39 ASSERT(*waiter != NULL); 40 thread_unblock(*waiter); 41 *waiter = NULL; 42 } 43 44 /* 消費者從ioq佇列中獲取一個字元 */ 45 char ioq_getchar(struct ioqueue* ioq) { 46 ASSERT(intr_get_status() == INTR_OFF); 47 48 /* 若緩衝區(佇列)為空,把消費者ioq->consumer記為當前執行緒自己, 49 * 目的是將來生產者往緩衝區裡裝商品後,生產者知道喚醒哪個消費者, 50 * 也就是喚醒當前執行緒自己*/ 51 while (ioq_empty(ioq)) { 52 lock_acquire(&ioq->lock); 53 ioq_wait(&ioq->consumer); 54 lock_release(&ioq->lock); 55 } 56 57 char byte = ioq->buf[ioq->tail]; // 從緩衝區中取出 58 ioq->tail = next_pos(ioq->tail); // 把讀遊標移到下一位置 59 60 if (ioq->producer != NULL) { 61 wakeup(&ioq->producer); // 喚醒生產者 62 } 63 64 return byte; 65 } 66 67 /* 生產者往ioq佇列中寫入一個字元byte */ 68 void ioq_putchar(struct ioqueue* ioq, char byte) { 69 ASSERT(intr_get_status() == INTR_OFF); 70 71 /* 若緩衝區(佇列)已經滿了,把生產者ioq->producer記為自己, 72 * 為的是當緩衝區裡的東西被消費者取完後讓消費者知道喚醒哪個生產者, 73 * 也就是喚醒當前執行緒自己*/ 74 while (ioq_full(ioq)) { 75 lock_acquire(&ioq->lock); 76 ioq_wait(&ioq->producer); 77 lock_release(&ioq->lock); 78 } 79 ioq->buf[ioq->head] = byte; // 把位元組放入緩衝區中 80 ioq->head = next_pos(ioq->head); // 把寫遊標移到下一位置 81 82 if (ioq->consumer != NULL) { 83 wakeup(&ioq->consumer); // 喚醒消費者 84 } 85 } 86 87 /* 返回環形緩衝區中的資料長度 */ 88 uint32_t ioq_length(struct ioqueue* ioq) { 89 uint32_t len = 0; 90 if (ioq->head >= ioq->tail) { 91 len = ioq->head - ioq->tail; 92 } else { 93 len = bufsize - (ioq->tail - ioq->head); 94 } 95 return len; 96 }ioqueue.c
1 #include "interrupt.h" 2 #include "io.h" 3 #include "global.h" 4 #include "ioqueue.h" 5 6 #define KBD_BUF_PORT 0x60 // 鍵盤 buffer 暫存器埠號為 0x60 7 #define KBD_BUF_PORT 0x60 // 鍵盤buffer暫存器埠號為0x60 8 9 // 鍵盤中斷處理程式 10 /* 用轉義字元定義部分控制字元 */ 11 #define esc '\033' // 八進位制表示字元,也可以用十六進位制'\x1b' 12 #define backspace '\b' 13 #define tab '\t' 14 #define enter '\r' 15 #define delete '\177' // 八進位制表示字元,十六進位制為'\x7f' 16 17 /* 以上不可見字元一律定義為0 */ 18 #define char_invisible 0 19 #define ctrl_l_char char_invisible 20 #define ctrl_r_char char_invisible 21 #define shift_l_char char_invisible 22 #define shift_r_char char_invisible 23 #define alt_l_char char_invisible 24 #define alt_r_char char_invisible 25 #define caps_lock_char char_invisible 26 27 /* 定義控制字元的通碼和斷碼 */ 28 #define shift_l_make 0x2a 29 #define shift_r_make 0x36 30 #define alt_l_make 0x38 31 #define alt_r_make 0xe038 32 #define alt_r_break 0xe0b8 33 #define ctrl_l_make 0x1d 34 #define ctrl_r_make 0xe01d 35 #define ctrl_r_break 0xe09d 36 #define caps_lock_make 0x3a 37 38 struct ioqueue kbd_buf; // 定義鍵盤緩衝區 39 40 /* 定義以下變數記錄相應鍵是否按下的狀態, 41 * ext_scancode用於記錄makecode是否以0xe0開頭 */ 42 static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode; 43 44 /* 以通碼make_code為索引的二維陣列 */ 45 static char keymap[][2] = { 46 /* 掃描碼 未與shift組合 與shift組合*/ 47 /* ---------------------------------- */ 48 /* 0x00 */ {0, 0}, 49 /* 0x01 */ {esc, esc}, 50 /* 0x02 */ {'1', '!'}, 51 /* 0x03 */ {'2', '@'}, 52 /* 0x04 */ {'3', '#'}, 53 /* 0x05 */ {'4', '$'}, 54 /* 0x06 */ {'5', '%'}, 55 /* 0x07 */ {'6', '^'}, 56 /* 0x08 */ {'7', '&'}, 57 /* 0x09 */ {'8', '*'}, 58 /* 0x0A */ {'9', '('}, 59 /* 0x0B */ {'0', ')'}, 60 /* 0x0C */ {'-', '_'}, 61 /* 0x0D */ {'=', '+'}, 62 /* 0x0E */ {backspace, backspace}, 63 /* 0x0F */ {tab, tab}, 64 /* 0x10 */ {'q', 'Q'}, 65 /* 0x11 */ {'w', 'W'}, 66 /* 0x12 */ {'e', 'E'}, 67 /* 0x13 */ {'r', 'R'}, 68 /* 0x14 */ {'t', 'T'}, 69 /* 0x15 */ {'y', 'Y'}, 70 /* 0x16 */ {'u', 'U'}, 71 /* 0x17 */ {'i', 'I'}, 72 /* 0x18 */ {'o', 'O'}, 73 /* 0x19 */ {'p', 'P'}, 74 /* 0x1A */ {'[', '{'}, 75 /* 0x1B */ {']', '}'}, 76 /* 0x1C */ {enter, enter}, 77 /* 0x1D */ {ctrl_l_char, ctrl_l_char}, 78 /* 0x1E */ {'a', 'A'}, 79 /* 0x1F */ {'s', 'S'}, 80 /* 0x20 */ {'d', 'D'}, 81 /* 0x21 */ {'f', 'F'}, 82 /* 0x22 */ {'g', 'G'}, 83 /* 0x23 */ {'h', 'H'}, 84 /* 0x24 */ {'j', 'J'}, 85 /* 0x25 */ {'k', 'K'}, 86 /* 0x26 */ {'l', 'L'}, 87 /* 0x27 */ {';', ':'}, 88 /* 0x28 */ {'\'', '"'}, 89 /* 0x29 */ {'`', '~'}, 90 /* 0x2A */ {shift_l_char, shift_l_char}, 91 /* 0x2B */ {'\\', '|'}, 92 /* 0x2C */ {'z', 'Z'}, 93 /* 0x2D */ {'x', 'X'}, 94 /* 0x2E */ {'c', 'C'}, 95 /* 0x2F */ {'v', 'V'}, 96 /* 0x30 */ {'b', 'B'}, 97 /* 0x31 */ {'n', 'N'}, 98 /* 0x32 */ {'m', 'M'}, 99 /* 0x33 */ {',', '<'}, 100 /* 0x34 */ {'.', '>'}, 101 /* 0x35 */ {'/', '?'}, 102 /* 0x36 */ {shift_r_char, shift_r_char}, 103 /* 0x37 */ {'*', '*'}, 104 /* 0x38 */ {alt_l_char, alt_l_char}, 105 /* 0x39 */ {' ', ' '}, 106 /* 0x3A */ {caps_lock_char, caps_lock_char} 107 /*其它按鍵暫不處理*/ 108 }; 109 110 /* 鍵盤中斷處理程式 */ 111 static void intr_keyboard_handler(void) { 112 put_char('k'); 113 inb(KBD_BUF_PORT); 114 return; 115 116 /* 這次中斷髮生前的上一次中斷,以下任意三個鍵是否有按下 */ 117 bool ctrl_down_last = ctrl_status; 118 bool shift_down_last = shift_status; 119 bool caps_lock_last = caps_lock_status; 120 121 bool break_code; 122 uint16_t scancode = inb(KBD_BUF_PORT); 123 124 /* 若掃描碼是e0開頭的,表示此鍵的按下將產生多個掃描碼, 125 * 所以馬上結束此次中斷處理函式,等待下一個掃描碼進來*/ 126 if (scancode == 0xe0) { 127 ext_scancode = true; // 開啟e0標記 128 return; 129 } 130 131 /* 如果上次是以0xe0開頭,將掃描碼合併 */ 132 if (ext_scancode) { 133 scancode = ((0xe000) | scancode); 134 ext_scancode = false; // 關閉e0標記 135 } 136 137 break_code = ((scancode & 0x0080) != 0); // 獲取break_code 138 139 if (break_code) { // 若是斷碼break_code(按鍵彈起時產生的掃描碼) 140 141 /* 由於ctrl_r 和alt_r的make_code和break_code都是兩位元組, 142 所以可用下面的方法取make_code,多位元組的掃描碼暫不處理 */ 143 uint16_t make_code = (scancode &= 0xff7f); // 得到其make_code(按鍵按下時產生的掃描碼) 144 145 /* 若是任意以下三個鍵彈起了,將狀態置為false */ 146 if (make_code == ctrl_l_make || make_code == ctrl_r_make) { 147 ctrl_status = false; 148 } else if (make_code == shift_l_make || make_code == shift_r_make) { 149 shift_status = false; 150 } else if (make_code == alt_l_make || make_code == alt_r_make) { 151 alt_status = false; 152 } /* 由於caps_lock不是彈起後關閉,所以需要單獨處理 */ 153 154 return; // 直接返回結束此次中斷處理程式 155 156 } 157 /* 若為通碼,只處理陣列中定義的鍵以及alt_right和ctrl鍵,全是make_code */ 158 else if ((scancode > 0x00 && scancode < 0x3b) || \ 159 (scancode == alt_r_make) || \ 160 (scancode == ctrl_r_make)) { 161 bool shift = false; // 判斷是否與shift組合,用來在一維陣列中索引對應的字元 162 if ((scancode < 0x0e) || (scancode == 0x29) || \ 163 (scancode == 0x1a) || (scancode == 0x1b) || \ 164 (scancode == 0x2b) || (scancode == 0x27) || \ 165 (scancode == 0x28) || (scancode == 0x33) || \ 166 (scancode == 0x34) || (scancode == 0x35)) { 167 /****** 代表兩個字母的鍵 ******** 168 0x0e 數字'0'~'9',字元'-',字元'=' 169 0x29 字元'`' 170 0x1a 字元'[' 171 0x1b 字元']' 172 0x2b 字元'\\' 173 0x27 字元';' 174 0x28 字元'\'' 175 0x33 字元',' 176 0x34 字元'.' 177 0x35 字元'/' 178 *******************************/ 179 if (shift_down_last) { // 如果同時按下了shift鍵 180 shift = true; 181 } 182 } else { // 預設為字母鍵 183 if (shift_down_last && caps_lock_last) { // 如果shift和capslock同時按下 184 shift = false; 185 } else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下 186 shift = true; 187 } else { 188 shift = false; 189 } 190 } 191 192 uint8_t index = (scancode &= 0x00ff); // 將掃描碼的高位元組置0,主要是針對高位元組是e0的掃描碼. 193 char cur_char = keymap[index][shift]; // 在陣列中找到對應的字元 194 195 /* 如果cur_char不為0,也就是ascii碼為除'\0'外的字元就加入鍵盤緩衝區中 */ 196 if (cur_char) { 197 198 /***************** 快捷鍵ctrl+l和ctrl+u的處理 ********************* 199 * 下面是把ctrl+l和ctrl+u這兩種組合鍵產生的字元置為: 200 * cur_char的asc碼-字元a的asc碼, 此差值比較小, 201 * 屬於asc碼錶中不可見的字元部分.故不會產生可見字元. 202 * 我們在shell中將ascii值為l-a和u-a的分別處理為清屏和刪除輸入的快捷鍵*/ 203 if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) { 204 cur_char -= 'a'; 205 } 206 /****************************************************************/ 207 208 /* 若kbd_buf中未滿並且待加入的cur_char不為0, 209 * 則將其加入到緩衝區kbd_buf中 */ 210 if (!ioq_full(&kbd_buf)) { 211 ioq_putchar(&kbd_buf, cur_char); 212 } 213 return; 214 } 215 216 /* 記錄本次是否按下了下面幾類控制鍵之一,供下次鍵入時判斷組合鍵 */ 217 if (scancode == ctrl_l_make || scancode == ctrl_r_make) { 218 ctrl_status = true; 219 } else if (scancode == shift_l_make || scancode == shift_r_make) { 220 shift_status = true; 221 } else if (scancode == alt_l_make || scancode == alt_r_make) { 222 alt_status = true; 223 } else if (scancode == caps_lock_make) { 224 /* 不管之前是否有按下caps_lock鍵,當再次按下時則狀態取反, 225 * 即:已經開啟時,再按下同樣的鍵是關閉。關閉時按下表示開啟。*/ 226 caps_lock_status = !caps_lock_status; 227 } 228 } else { 229 put_str("unknown key\n"); 230 } 231 } 232 233 // 鍵盤初始化 234 /* 鍵盤初始化 */ 235 void keyboard_init() { 236 put_str("keyboard init start\n"); 237 register_handler(0x21, intr_keyboard_handler); 238 put_str("keyboard init done\n"); 239 put_str("keyboard init start\n"); 240 ioqueue_init(&kbd_buf); 241 register_handler(0x21, intr_keyboard_handler); 242 put_str("keyboard init done\n"); 243 }keyboard.c
main.c
1 #include "print.h" 2 #include "init.h" 3 #include "thread.h" 4 #include "interrupt.h" 5 6 #include "ioqueue.h" 7 #include "keyboard.h" 8 9 void k_thread_a(void*); 10 void k_thread_b(void*); 11 12 int main(void){ 13 put_str("I am kernel\n"); 14 init_all(); 15 16 thread_start("consumer_a", 31, k_thread_a, "AOUT_"); 17 thread_start("consumer_b", 31, k_thread_b, "BOUT_"); 18 19 intr_enable(); 20 21 while(1) { 22 //console_put_str("Main "); 23 } 24 return 0; 25 } 26 27 void k_thread_a(void* arg) { 28 while(1) { 29 enum intr_status old_status = intr_disable(); 30 if (!ioq_empty(&kbd_buf)) { 31 console_put_str(arg); 32 char byte = ioq_getchar(&kbd_buf); 33 console_put_char(byte); 34 console_put_str("\n"); 35 } 36 intr_set_status(old_status); 37 } 38 } 39 40 void k_thread_b(void* arg) { 41 while(1) { 42 enum intr_status old_status = intr_disable(); 43 if (!ioq_empty(&kbd_buf)) { 44 console_put_str(arg); 45 char byte = ioq_getchar(&kbd_buf); 46 console_put_char(byte); 47 console_put_str("\n"); 48 } 49 intr_set_status(old_status); 50 } 51 }
第一個 ioqueue.c 就是個佇列的實現類,準確說是個執行緒安全佇列。第二個 keyboard.c 從我們原來無論按什麼鍵都輸出 ‘k’,變成了把鍵盤碼轉換成 ASCII,還包括對 controll 鍵等處理,反正就是一堆雜事,轉換成我們平時認知中按鍵應該對應的字元,把這個字元的 ASCII 碼放入佇列,等著 main.c 的兩個執行緒取出來,列印在螢幕上,就這麼點事,但每個字元都要細心處理,十分繁瑣。
所以不再贅述,不影響我們理解作業系統主流程,執行後結果如下
寫在最後:開源專案和課程規劃
如果你對自制一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們(下方有公眾號和小助手微信),一起來開發。
參考書籍
《作業系統真相還原》這本書真的贊!強烈推薦
專案開源
專案開源地址:https://gitee.com/sunym1993/flashos
當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來檢視歷史的程式碼,我會慢慢梳理提交歷史以及專案說明文件,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製貼上的方式也是完全可以的。
如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯絡方式,或者在 gitee 私信我您的聯絡方式。
課程規劃
本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。
目前的系列包括
- 【自制作業系統01】硬核講解計算機的啟動過程
- 【自制作業系統02】環境準備與啟動區實現
- 【自制作業系統03】讀取硬碟中的資料
- 【自制作業系統04】從真實模式到保護模式
- 【自制作業系統05】開啟記憶體分頁機制
- 【自制作業系統06】終於開始用 C 語言了,第一行核心程式碼!
- 【自制作業系統07】深入淺出特權級
- 【自制作業系統08】中斷
- 【自制作業系統09】中斷的程式碼實現
- 【自制作業系統10】記憶體管理系統
- 【自制作業系統11】中場休息之細節是魔鬼
- 【自制作業系統12】熟悉而陌生的多執行緒
- 【自制作業系統13】鎖
微信公眾號
我要去阿里(woyaoquali)
小助手微訊號
Angel(angel1998032