码迷,mamicode.com
首页 > 其他好文 > 详细

【自制操作系统14】实现键盘输入

时间:2020-03-22 00:59:02      阅读:124      评论:0      收藏:0      [点我收藏+]

标签:管理系统   不为   入队   变量   消费   print   简单   esc   --   

一、到目前为止的程序流程图

  为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下。(红色部分就是我们今天要实现的)

技术图片

 

二、简单打通键盘中断

  既然要打通键盘中断,那必然需要你回顾一下 【自制操作系统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 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

目前的系列包括

 微信公众号

  我要去阿里(woyaoquali)

 小助手微信号

  Angel(angel19980323)

【自制操作系统14】实现键盘输入

标签:管理系统   不为   入队   变量   消费   print   简单   esc   --   

原文地址:https://www.cnblogs.com/flashsun/p/12490765.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!