爲了讓你們清楚目前的程序進度,畫了到目前爲止的程序流程圖,以下。(紅色部分就是咱們今天要實現的)html
既然要打通鍵盤中斷,那必然須要你回顧一下 【自制操做系統08】中斷 所講述的外部中斷的流程,下面我把圖貼上。git
如圖所示,將上圖中的某外部設備,換成下圖中的具體的鍵盤,就是鍵盤中斷流程啦。簡單說就是:shell
那咱們 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.cide
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 }
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 }
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 的兩個線程取出來,打印在屏幕上,就這麼點事,但每一個字符都要細心處理,十分繁瑣。
因此再也不贅述,不影響咱們理解操做系統主流程,運行後結果以下
若是你對自制一個操做系統感興趣,不妨跟隨這個系列課程看下去,甚至加入咱們(下方有公衆號和小助手微信),一塊兒來開發。
《操做系統真相還原》這本書真的贊!強烈推薦
當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你能夠經過提交記錄歷史來查看歷史的代碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的代碼。固然文章中的代碼也是全的,採用複製粘貼的方式也是徹底能夠的。
若是你有興趣加入這個自制操做系統的大軍,也能夠在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。
本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。
目前的系列包括
微信公衆號
我要去阿里(woyaoquali)
小助手微信號
Angel(angel19980323)