【自制操做系統14】實現鍵盤輸入

1、到目前爲止的程序流程圖

  爲了讓你們清楚目前的程序進度,畫了到目前爲止的程序流程圖,以下。(紅色部分就是咱們今天要實現的)html

 

 

 

2、簡單打通鍵盤中斷

  既然要打通鍵盤中斷,那必然須要你回顧一下 【自制操做系統08】中斷 所講述的外部中斷的流程,下面我把圖貼上。git

如圖所示,將上圖中的某外部設備,換成下圖中的具體的鍵盤,就是鍵盤中斷流程啦。簡單說就是:shell

  • 所以每當有擊鍵發生時,鍵盤中的設備 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.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’,但仍是很興奮,我以爲每每最難的地方就是打通和硬件的交互,把控制權交給軟件,剩下的事就掌控在咱們手中啦,咱們繼續往下看。

 

3、實現輸入字符緩衝區

  這塊我懶了,不想看代碼了,直接把書中的代碼所有 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 私信我您的聯繫方式。

課程規劃

本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。

目前的系列包括

 微信公衆號

  我要去阿里(woyaoquali)

 小助手微信號

Angel(angel19980323)

相關文章
相關標籤/搜索