【自制操作系統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 私信我您的聯系方式。

課程規划

本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的操作系統,我覺得這是最好的學習操作系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。

目前的系列包括


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM