从零编写linux0.11 - 第七章 完善终端

200 阅读16分钟

编程环境:Ubuntu Kylin 16.04、gcc-7.3.0

代码仓库:gitee.com/AprilSloan/…

linux0.11源码下载(不能直接编译,需进行修改)

本章目标 本章将会完善终端,实现输入功能,完善输出功能。知识点涉及到键盘和终端控制。

1.数据结构介绍 之前我们在用 printk 函数打印字符串时,printk 函数调用 tty_write,tty_write 调用 con_write 都是直接对字符串进行操作的。这种方式并没有什么不好,只是不够灵活,想要实现更多的功能有难度,这一节,我们要使用一种数据结构替代字符串。

// tty.h #define TTY_BUF_SIZE 1024 struct tty_queue { unsigned long data; // 缓冲区的字符行数 unsigned long head; // 缓冲区数据头指针 unsigned long tail; // 缓冲区数据尾指针 struct task_struct *proc_list; // 等待进程列表 char buf[TTY_BUF_SIZE]; // 队列缓冲区 }; 1 2 3 4 5 6 7 8 9 采用的数据结构是循环队列,当缓冲区头尾指针超过缓冲区大小时,它们就会变成0,形成循环。

为了方便队列的操作,我们需要定义一些操作队列的宏定义。

// tty.h #define INC(a) ((a) = ((a)+1) & (TTY_BUF_SIZE-1)) #define DEC(a) ((a) = ((a)-1) & (TTY_BUF_SIZE-1)) #define EMPTY(a) ((a).head == (a).tail) #define LEFT(a) (((a).tail-(a).head-1)&(TTY_BUF_SIZE-1)) #define LAST(a) ((a).buf[(TTY_BUF_SIZE-1)&((a).head-1)]) #define FULL(a) (!LEFT(a)) #define CHARS(a) (((a).head-(a).tail)&(TTY_BUF_SIZE-1)) #define GETCH(queue,c)
(void)({c=(queue).buf[(queue).tail];INC((queue).tail);}) #define PUTCH(c,queue)
(void)({(queue).buf[(queue).head]=(c);INC((queue).head);}) 1 2 3 4 5 6 7 8 9 10 11 12 有了这些宏定义,我们可以很方便地向队列写数据,从队列读数据,获取队列长度,判断队列是否为空等等。

// tty.h struct tty_struct { struct termios termios; // 终端io属性和控制字符数据结构 int pgrp; // 所属进程组 int stopped; // 停止标志 void (*write)(struct tty_struct *tty); // tty写函数指针 struct tty_queue read_q; // tty读队列 struct tty_queue write_q; // tty写队列 struct tty_queue secondary; // tty辅助队列(存放规范模式字符序列) }; 1 2 3 4 5 6 7 8 9 10 struct termios 定义在 termios.h 中,termios.h 中还有许多与输入输出控制模式相关地宏定义,我们之后会用到这些宏定义,通过这些宏定义设置终端的输入输出模式。

对于键盘输入,我们会将字符放在读队列中,对于 printk 输出,我们会将字符放在写队列中。write 指向用于输出写队列字符的函数。

// tty_io.c struct tty_struct tty_table[] = { { {ICRNL, // 将输入的CR转换为NL OPOST | ONLCR, // 将输出的NL转换为CRNL 0, // 控制模式初始化为0 ISIG | ICANON | ECHO | ECHOCTL | ECHOKE, // 本地模式标志 0, INIT_C_CC}, // 控制字符数组 0, 0, con_write, {0, 0, 0, 0, ""}, {0, 0, 0, 0, ""}, {0, 0, 0, 0, ""} } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 第4-9行是关于 termios 结构体的初始化。第4,5行的这种转换有什么用呢?我们按下回车键,系统会收到 CR,CR 在存入读队列时会转换为 NL,当我们把键盘读入的字符打印到屏幕上时,NL 会转换为 CRNL,实现按下回车实现回车换行的功能。同时,使用 printk 打印时,\n 就可以实现回车换行。

第7行是设置本地模式标志,将终端设置为收到 INTR/QUIT/SUSP/DSUSP 会产生信号(ISIG),显示输入字符(ECHO)等,详细信息请参考这篇博客:C语言实现串口通信。在使用终端时,我们经常使用 Ctrl+C 结束一个任务,其实 Ctrl+C 代表 INTR,由于设置了 ISIG,任务会产生信号,处理信号时就会结束该任务。

第13-15行是对循环队列的初始化,暂且将它们都设置为0。

INIT_C_CC 的定义如下。

// tty.h /* intr=^C quit=^| erase=del kill=^U eof=^D vtime=\0 vmin=\1 sxtc=\0 start=^Q stop=^S susp=^Z eol=\0 reprint=^R discard=^U werase=^W lnext=^V eol2=\0 */ #define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0" 1 2 3 4 5 6 7 8 ^C 代表 Ctrl+C,^Z 代表 Ctrl+Z,以此类推。这里的数字都是8进制数,177(八进制)= 127(十进制)。我们也定义一些宏定义方便辨认这些字符。

// tty.h #define INTR_CHAR(tty) ((tty)->termios.c_cc[VINTR]) #define QUIT_CHAR(tty) ((tty)->termios.c_cc[VQUIT]) #define ERASE_CHAR(tty) ((tty)->termios.c_cc[VERASE]) #define KILL_CHAR(tty) ((tty)->termios.c_cc[VKILL]) #define EOF_CHAR(tty) ((tty)->termios.c_cc[VEOF]) #define START_CHAR(tty) ((tty)->termios.c_cc[VSTART]) #define STOP_CHAR(tty) ((tty)->termios.c_cc[VSTOP]) #define SUSPEND_CHAR(tty) ((tty)->termios.c_cc[VSUSP]) 1 2 3 4 5 6 7 8 9 下面再来修改一下代码。

// printk.c int printk(const char *fmt, ...) { va_list args; int i;

va_start(args, fmt);
i = vsprintf(buf, fmt, args);
va_end(args);
__asm__("push %%fs\n\t"
    "push %%ds\n\t"
    "pop %%fs\n\t"      // fs = ds
    "pushl %0\n\t"      // 字符串长度
    "pushl $buf\n\t"
    "pushl $0\n\t"      // tty0
    "call tty_write\n\t"
    "addl $8, %%esp\n\t"
    "popl %0\n\t"
    "pop %%fs"
    ::"r"(i));
return i;

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 第11-12行相当于将 ds 的值放入 fs 中。第13-15行将参数入栈,第16行调用 tty_write 打印字符串。第17-19行清空栈中多余的数据。

// tty_io.c int tty_write(unsigned channel, char *buf, int nr) { struct tty_struct *tty; char c, *b = buf;

if (channel > 0 || nr < 0)
    return -1;
tty = channel + tty_table;

while (nr > 0) {
    if (current->signal)
        break;
    while (nr > 0 && !FULL(tty->write_q)) {
        c = *b;
        b++; nr--;
        PUTCH(c, tty->write_q);
    }
    tty->write(tty);	// con_write(tty);
    if (nr > 0)
        schedule();
}
return (b - buf);

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 确定终端号和字符长度没问题后,找到要使用的终端。如果终端缓冲区未满而且还有字符没放入缓冲区中,就一直向缓冲区中存放数据。存放完毕后,调用写函数将缓冲区的数据打印到屏幕上。如果还有字符没放入缓冲区中,说明此时缓冲区已满,先调度到其它任务去。等再次调度到这个任务后,执行上述操作,直至打印出所有的字符。

// console.c void con_write(struct tty_struct *tty) { int nr; char c; www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E6%BD%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E5%94%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%…

nr = CHARS(tty->write_q);
while (nr--) {
    GETCH(tty->write_q, c);
    if (c > 31 && c < 127) {
        ...
    }
    else if (c == 10 || c == 11 || c == 12) // '\n',换行,使光标下移一格
        lf();
    else if (c == 13)       // '\r',回车,使光标移至行首
        cr();
    else if (c == ERASE_CHAR(tty))  // 删除
        del();
    ...

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 con_write 的改动不大,只是获取字符串长度和获得字符的方式变了,第17行改变了对删除的判断(其实就是换了层皮而已)。

运行看看有没有报错。

可以看到,打印功能没什么问题。

2.键盘中断1 也是时候对键盘动手了。我们这一节的目标是按下按键,在屏幕上显示按键的内容。

说到键盘,那必定先讲键盘中断。

keyboard.S

keyboard_interrupt: pushl %eax pushl %ebx pushl %ecx pushl %edx push %ds push %es movl 0x10,movmovxorinb0x10, %eax mov %ax, %ds mov %ax, %es xor %al, %al inb 0x60, %al # 保存扫描码 call key_table(, %eax, 4) inb 0x61,jmp1f1:jmp1f1:orb0x61, %al # 获得PPI(可编程外设接口)端口B状态,其位7用于允许/禁止(0/1)键盘 jmp 1f 1: jmp 1f 1: orb 0x80, %al jmp 1f 1: jmp 1f 1: outb %al, 0x61 # 禁止键盘工作 jmp 1f 1: jmp 1f 1: andb 0x7F, %al outb %al, 0x61 # 允许键盘工作 movb 0x20, %al outb %al, 0x20 # 向8259芯片发送中断结束信号 pushl 0 call do_tty_interrupt addl $4, %esp pop %es pop %ds popl %edx popl %ecx popl %ebx popl %eax iret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 一开头还是中断的老操作,寄存器入栈,修改段寄存器的值。然后将 0x60 端口的数据存入 al 中。0x60 端口是干什么用的?0x60 端口属于 8042芯片(键盘控制器),无论键盘的按键被按下还是松开,都会发送数据到 0x60 端口的寄存器,这个数据我们称之为扫描码。比如,按下 A 键,扫描码为 0x1E,松开 A 键,扫描码为 0x9E,我们可以通过扫描码知道按下或松开了哪个键。更多的扫描码,可以看这篇博客:键盘扫描码集(共三版)。请勿把扫描码与 ASCII 码混淆。

根据不同的按键,我们执行不同的函数,函数列表如下。

这一节,我们只处理普通的按键,如数字、字符、符号等,Shift、Ctrl、Alt、方向键等会在之后的内容添加。

如果普通的按键按下,我们统一执行 do_self,对于松开按键,我们执行 none,也就是什么也不做。

do_self 函数会将按下的字符保存到终端的缓冲区中。

将按下的字符保存到终端的缓冲区之后,我们需要对收到的扫描码做出应答,具体做法就是先禁止键盘,然后立刻重新允许键盘工作,对应第15-25行代码。接着,我们需要向8259芯片发送中断结束信号,表示我们已经响应中断了。

将0作为 do_tty_interrupt 的参数入栈,调用 do_tty_interrupt 函数打印字符,之后将寄存器出栈,iret 结束中断处理函数。

keyboard.S

key_table: .long none,do_self,do_self,do_self /* 00-03 br esc 1 2 / .long do_self,do_self,do_self,do_self / 04-07 3 4 5 6 / .long do_self,do_self,do_self,do_self / 08-0B 7 8 9 0 / .long do_self,do_self,do_self,do_self / 0C-0F + ' bs tab / .long do_self,do_self,do_self,do_self / 10-13 q w e r / .long do_self,do_self,do_self,do_self / 14-17 t y u i / .long do_self,do_self,do_self,do_self / 18-1B o p } ^ / .long do_self,none,do_self,do_self / 1C-1F enter br a s / .long do_self,do_self,do_self,do_self / 20-23 d f g h / .long do_self,do_self,do_self,do_self / 24-27 j k l | / .long do_self,do_self,none,do_self / 28-2B { para br , / .long do_self,do_self,do_self,do_self / 2C-2F z x c v / .long do_self,do_self,do_self,do_self / 30-33 b n m , / .long do_self,do_self,none,do_self / 34-37 . / br * / .long none,do_self,none,none / 38-3B br sp br br / .long none,none,none,none / 3C-3F br br br br / .long none,none,none,none / 40-43 br br br br / .long none,none,none,none / 44-47 br br br br / .long none,none,do_self,none / 48-4B br br - br / .long none,none,do_self,none / 4C-4F br br + br / .long none,none,none,none / 50-53 br br br br / .long none,none,do_self,none / 54-57 br br < br */ ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 我们先讲 do_self 函数,再讲 do_tty_interrupt 吧。

keyboard.S

size = 1024

key_map: .byte 0,27 .ascii "1234567890-=" .byte 127,9 .ascii "qwertyuiop[]" .byte 13,0 .ascii "asdfghjkl;'" .byte '`,0 .ascii "\zxcvbnm,./" .byte 0,',0,32 / 36-39 / .fill 16,1,0 / 3A-49 / .byte '-,0,0,0,'+ / 4A-4E / .byte 0,0,0,0,0,0,0 / 4F-55 */ .byte '< .fill 10,1,0

do_self: lea key_map, %ebx 1: movb (%ebx, %eax), %al orb %al, %al je none andl $0xff, %eax xorl %ebx, %ebx call put_queue none: ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 key_map 是扫描码-ASCII 字符映射表,这是美国键盘的映射表,我们日常使用的键盘也是这个映射表。映射表怎么使用呢?还是以 A 为例,按下 A 键,扫描码为 0x1E,‘a’ 字符相对于映射表起始地址的偏移就是 0x1E,我们这就通过扫描码找到了 ASCII 字符。

将映射表地址存入 ebx 中(第21行),通过映射表和扫描码找到 ASCII 字符保存到 al 中(第22行),如果 al 为0,就跳转到 none。只保存 eax 的低8位(第25行),将 ebx 清零(第26行),将 ASCII 字符保存到终端的缓冲区中(第27行)。

// tty_io.c struct tty_queue *table_list[] = { &tty_table[0].read_q, &tty_table[0].write_q, }; 1 2 3 4

keyboard.S

put_queue: pushl %ecx pushl %edx movl table_list, %edx # 终端读队列的地址 movl head(%edx), %ecx 1: movb %al, buf(%edx, %ecx) incl %ecx andl size1,cmpltail(je3fshrdlsize - 1,%ecx cmpl tail(%edx), %ecx je 3f shrdl 8, %ebx, %eax je 2f # 如果没有字符就跳转到2 shrl 8,jmp1b2:movlmovlproclist(testlje3fmovl8, %ebx jmp 1b 2: movl %ecx, head(%edx) movl proc_list(%edx), %ecx testl %ecx, %ecx je 3f movl 0, (%ecx) 3: popl %edx popl %ecx ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 我们先找到终端读队列的地址,基于此得到读队列的头指针地址,将 ASCII 字符存入缓冲区中,第7-9行代码与 C 语言下PUTCH(al, tty_table[0].read_q)作用相同。检查缓冲区是否还有空间存放数据(第10行,与FULL(tty_table[0].read_q)相同),如果这个操作会导致缓冲区填满,就舍弃数据,结束。

shrdl会将 ebx 的低8位移动到 eax 的高8位上,而 ebx 并不会发生改变。如下图所示。

在 put_queue 之前,我们会把要存入队列的字符放在 eax 中,eax 最多可以存放4个字符,ebx 一般为0。如果 eax 不为0,说明还有字符需要存入队列中,就把 ebx 右移8位,然后继续执行1标签。如果没有就跳转到2标签,将 ecx 的值存入读队列头指针。检查有无等待该队列的任务,有就把它的状态设置为可运行态。

总结一下键盘中断都干了什么。

寄存器入栈并修改段寄存器。

获得键盘扫描码

执行与扫描码对应的函数:获得映射表地址,得到 ASCII 字符,将字符放入队列的缓冲区

对收到的扫描码做应答

整理数据,打印

寄存器出栈,结束

我们还没有对第5步进行说明。

// tty_io.c #define _L_FLAG(tty,f) ((tty)->termios.c_lflag & f) #define L_ECHO(tty) _L_FLAG((tty),ECHO) #define L_ECHOCTL(tty) _L_FLAG((tty),ECHOCTL) void copy_to_cooked(struct tty_struct *tty) { signed char c; while (!EMPTY(tty->read_q)) { GETCH(tty->read_q, c); if (L_ECHO(tty)) { if (c == 13) { PUTCH(10, tty->write_q); PUTCH(13, tty->write_q); } else if (c < 32) { if (L_ECHOCTL(tty)) { PUTCH('^', tty->write_q); PUTCH(c + 64, tty->write_q); } } else PUTCH(c, tty->write_q); tty->write(tty); } } }

void do_tty_interrupt(int tty) { copy_to_cooked(tty_table + tty); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 我们要读取终端的读队列,如果终端被设置可以显示字符,根据不同的字符,将不同的内容放入终端的写队列。对于 ‘\r’ 就写入 ‘\n\r’,对于其它不可显示字符,如果可以显示控制字符,就显示类似 ^C 、 ^Z 的形式,其它字符就直接入队列。最后调用写函数将写队列的数据打印到屏幕上。

我们还没有注册键盘中断处理程序,找个位置加上它吧。

// console.c void con_init(void) { unsigned char a; ... gotoxy(ORIG_X, ORIG_Y); set_trap_gate(0x21, &keyboard_interrupt); outb_p(inb_p(0x21) & 0xfd, 0x21); a = inb_p(0x61); outb_p(a | 0x80, 0x61); outb(a, 0x61); } 1 2 3 4 5 6 7 8 9 10 11 12 第8-11行用于复位键盘。

最后再修改一点代码,测试我们的程序可否正确执行。

// main.c www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E6%B5%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E7%BB%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… www.iimedia.cn/tag/%E5%A4%… void init(void) { while (1); } 1 2 3 4 5 我们就让这个任务进入死循环,当我们按下按键时,触发键盘中断,屏幕会显示按键对应的字符。

本来我是想用感叹号的,但是 Shift 键还不能用,所以就用了句号。现在 Ctrl、Shift、Alt、数字小键盘、方向键、Home键等,都没有相应的代码,我们会在之后的小节中逐步完善。

3.键盘中断2 这次,我们要处理一些特殊的按键:Shift,Ctrl,Alt,Cap(大小写),num(键盘锁),scroll。

键盘左右两边都有 Shift,Ctrl,Alt 键,两边的键盘扫描码并不相同,按下左侧的 Ctrl 会产生扫描码 0x1d,按下右侧的 Ctrl 会产生扫描码 0xe0 和 0x1d。

我们会使用一个变量记录 Shift,Ctrl,Alt,Cap 键的状态,如果按下了这些按键,使用或运算在变量的不同位置1,当松开按键时,将变量的相应位置0。如果按键产生了两个扫描码,我们也需要单独做一些处理。

按键 位号 左Shift 0 右Shift 1 左Ctrl 2 右Ctrl 3 左Alt 4 右Alt 5 Cap 6,7 1.Shift 键

keyboard.S

mode: .byte 0

lshift: orb 0x01,moderetunlshift:andb0x01, mode ret unlshift: andb 0xfe, mode ret rshift: orb 0x02,moderetunrshift:andb0x02, mode ret unrshift: andb 0xfd, mode ret

key_table: ... .long do_self,do_self,lshift,do_self /* 28-2B { para lshift , / ... .long do_self,do_self,rshift,do_self / 34-37 . / rshift * / ... .long none,none,unlshift,none / A8-AB br br unlshift br / ... .long none,none,unrshift,none / B4-B7 br br unrshift br */ ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 使用 mode 记录 Shift,Ctrl,Alt,Cap 键的状态。如上面的表格所示,我们使用第0位和第1位记录 Shift 的状态。

按下 Shift 键产生的扫描码为 0x2a 或 0x36,我们会跳转的相应的函数中,将 mode 的第0位或第1位置1。松开 Shift 将 mode 的第0位或第1位置0。

我们以按下 Shift+A 为例,这时应该将字符 ‘A’ 送入终端队列中,而不是字符 ‘a’。之前的扫描码-ASCII 字符映射表不能满足需求,我们需要创建一张 Shift 的扫描码-ASCII 字符映射表。其映射表如下所示。

keyboard.S

shift_map: .byte 0,27 .ascii "!@#$%^&()_+" .byte 127,9 .ascii "QWERTYUIOP{}" .byte 13,0 .ascii "ASDFGHJKL:"" .byte '~,0 .ascii "|ZXCVBNM<>?" .byte 0,',0,32 /* 36-39 / .fill 16,1,0 / 3A-49 / .byte '-,0,0,0,'+ / 4A-4E / .byte 0,0,0,0,0,0,0 / 4F-55 */ .byte '> .fill 10,1,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 处理 Shift 键之后,我们还需要处理 a 键,原本的 do_self 函数检查 Shift 键的状态,需要进行修改。

keyboard.S

do_self: ... # alt键的处理 lea shift_map, %ebx testb 0x03, mode jne 1f lea key_map, %ebx 1: movb (%ebx, %eax), %al orb %al, %al je none ... # cap,ctrl,alt键的处理 4: andl 0xff, %eax xorl %ebx, %ebx call put_queue none: ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 如果没有按下 Cap,Ctrl,Alt键,我们可以认为处理这些按键的代码不存在。

将 Shift 映射表的地址存入 ebx 中,如果按下了 Shift 键,则 testb 指令的结果不为0,跳转到第8行。此时,ebx 中是 Shift 映射表地址,eax 中是 a 的扫描码,通过它们可以得到字符 A,然后存入 al 中。

最后会在屏幕上显示字符 A,这个过程并不难理解吧。

2.Ctrl 键

左侧 Ctrl 键的扫描码为 0x1d,右侧的 Ctrl 键有2个扫描码:0xe0、0x1d。

0xe0 代表按下该按键会产生2个扫描码,0xe1 代表按下该按键会产生3个扫描码。(按下 Pause 键会产生3个扫描码:0xe1,0x1d,0x45)

keyboard.S

e0: .byte 0

keyboard_interrupt: ... inb 0x60,cmpb0x60, %al # 保存扫描码 cmpb 0xe0, %al je set_e0 cmpb 0xe1,jesete1callkeytable(,movb0xe1, %al je set_e1 call key_table(, %eax, 4) movb 0, e0 e0_e1: inb 0x61,...iretsete0:movb0x61, %al # 获得PPI(可编程外设接口)端口B状态,其位7用于允许/禁止(0/1)键盘 ... iret set_e0: movb 1, e0 jmp e0_e1 set_e1: movb $2, e0 jmp e0_e1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 我们使用一个变量 e0 来标记扫描码中是否有 0xe0 或 0xe1。e0 = 1 表示扫描码中有 0xe0,e0 = 2 表示扫描码中有 0xe1。

以按下 Ctrl+C 为例。如果是右侧的 Ctrl 键,此时会先产生扫描码 0xe0,将变量 e0 设置为1。由于没有字符入终端队列的操作,所以什么也不会打印。之后又会触发键盘中断,读入扫描码 0x1d,然后调用 ctrl 函数。如果是左侧的 Ctrl 键,则会直接读入扫描码 0x1d,然后调用 ctrl 函数。

keyboard.S

ctrl: movb 0x04,cmpb0x04, %al cmpb 0,e0 je 2f addb %al,%al 2: orb %al,mode ret unctrl: movb 0x04,cmpb0x04, %al cmpb 0,e0 je 2f addb %al,%al 2: notb %al andb %al,mode ret

key_table: ... .long do_self,ctrl,do_self,do_self /* 1C-1F enter ctrl a s / ... .long none,unctrl,none,none / 9C-9F br unctrl br br */ ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 第2-7行表示按下 Ctrl 会将 mode 的第2位或第3位置1。第8-14行表示松开 Ctrl 会将 mode 的第2位或第3位置0。

之后,我们都会同时检查 mode 的第2位或第3位,所以无论按下的是左侧的 Ctrl 键还是右侧的 Ctrl 键,最终得到的结果都是相同的。

因为按下了 Ctrl 键,所以 mode 的第2位或第3位会置1。之后一直到结束中断也不会做特别的操作。然后再次触发中断,处理 c 键。

keyboard.S

do_self: ... # alt键的处理 lea shift_map, %ebx testb 0x03, mode # 右alt jne 1f lea key_map, %ebx 1: movb (%ebx, %eax), %al orb %al, %al je none testb 0x4c, mode # ctrl或caps je 2f cmpb a,jb2fcmpb'a, %al jb 2f cmpb '}, %al ja 2f subb 32,2:testb32, %al 2: testb 0x0c, mode # ctrl je 3f cmpb 64,jb3fcmpb64, %al jb 3f cmpb 64 + 32, %al jae 3f subb 64, %al 3: ... # alt键的处理 4: andl 0xff, %eax xorl %ebx, %ebx call put_queue none: ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 c 键按下的扫描码是 0x2e,所以在执行 do_self 之前,al 的值为 0x2e。Ctrl 使用普通的键盘映射表,c 键对映的 ASCII 码是0x63,al 的值变为 0x63。如果按键的 ASCII 码大于等于 ‘a’ ,小于等于 ‘}’,则将 al 的值减去32(第13-17行),al 的值变为 0x43(67)。如果 al 的值大于等于64,小于96,则将 al 的值减去64(第20-24行),al 的值变为3。之后还是将 al 的值保存到读队列中。

在 copy_to_cooked 函数中,会将3解析为 ^C 这两个字符并保存在写队列中。最后打印在屏幕上。

alt 键的处理代码并不会影响 ctrl 键的处理,当做没有就行了。

3.Alt 键

左侧 Alt 键的扫描码为 0x38,右侧的 Alt 键有2个扫描码:0xe0、0x38。

Alt 键的处理函数如下。Alt 键的处理函数仅修改 mode 的值。

keyboard.S

alt: movb 0x10,cmpb0x10, %al cmpb 0, e0 je 2f addb %al, %al 2: orb %al, mode ret unalt: movb 0x10,cmpb0x10, %al cmpb 0,e0 je 2f addb %al, %al 2: notb %al andb %al, mode ret

key_table: ... .long alt,do_self,caps,none /* 38-3B alt sp caps br / ... .long unalt,none,uncaps,none / B8-BB unalt br uncaps br */ ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 按下左 Alt 键,将 mode 的第4位置1,松开左 Alt 键,将 mode 的第4位置0。

按下右 Alt 键,将 mode 的第5位置1,松开右 Alt 键,将 mode 的第5位置0。

Alt 键有专门的键盘映射表,可以看到,它的映射表大多数的值为0。

keyboard.S

alt_map: .byte 0,0 .ascii "\0@\0$\0\0{[]}\\0" .byte 0,0 .byte 0,0,0,0,0,0,0,0,0,0,0 .byte '~,13,0 .byte 0,0,0,0,0,0,0,0,0,0,0 .byte 0,0 .byte 0,0,0,0,0,0,0,0,0,0,0 .byte 0,0,0,0 /* 36-39 / .fill 16,1,0 / 3A-49 / .byte 0,0,0,0,0 / 4A-4E / .byte 0,0,0,0,0,0,0 / 4F-55 */ .byte '| .fill 10,1,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 我们以 Alt+2 为例,讲解 Alt 键的处理流程。

keyboard.S

do_self: lea alt_map, %ebx testb 0x20, mode # 右alt jne 1f ... # shift lea key_map, %ebx 1: movb (%ebx, %eax), %al orb %al, %al je none ... # ctrl或caps 2: ... # ctrl 3: testb 0x10, mode # 左alt je 4f orb 0x80,4:andl0x80, %al 4: andl 0xff, %eax xorl %ebx, %ebx call put_queue none: ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 可以看到,对于左右两边的 Alt 键的处理并不相同。右 Alt 键才会使用 Alt 的键盘映射表,会将64送入读队列,最后打印出 @。左 Alt 键不使用映射表,al 的值为50(第7行),之后会变为178(第14行),最后打印出 ^。

bochs 模拟器对于 Alt 键的支持并不是很好,建议大家用 vmware 虚拟机进行测试。新建虚拟机的步骤如这篇博客所示:【操作系统】30天自制操作系统–(1)虚拟机加载最小操作系统。

4.Caps、num、scroll 键

一般来说,键盘的右上角有三个灯,它们分别表示 Caps、num 和 scroll 键的状态。第一次按下这些按键时,相应的灯会亮起来,再次按下则会熄灭。我们的处理函数需要达到这种效果。

keyboard.S

leds: .byte 0

caps: testb 0x80,modejne1fxorb0x80, mode jne 1f xorb 4, leds xorb 0x40,modeorb0x40, mode orb 0x80, mode set_leds: call kb_wait movb 0xed,outb0xed, %al /* set leds command */ outb %al, 0x60 call kb_wait movb leds, %al outb %al, 0x60retuncaps:andb0x60 ret uncaps: andb 0x7f, mode ret scroll: xorb 1,ledsjmpsetledsnum:xorb1, leds jmp set_leds num:xorb 2, leds jmp set_leds

kb_wait: pushl %eax 1: inb 0x64,testb0x64,%al testb 0x02,%al jne 1b popl %eax ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 leds 的第0位代表 scroll 的状态,第1位代表 num 的状态,第2位代表 caps 的状态。

mode 的第6位代表 Caps 键是否工作,第7位代表 Caps 键是否按下。

caps、num、scroll 的处理函数都要设置 leds 的位,caps 还需要修改 mode 的值,之后就需要控制灯的亮灭。松开 caps 需要将相应位置0。

kb_wait 用于检查是否可以向 8042 芯片写入数据,它会一直循环直至可以写入数据。

当 0x60 端口收到 0xed 命令后,一个 led 设置会话开始,它会等待一个 led 设置字节。通过 leds 的值设置不同 led 灯的亮灭。

另外,按下 Caps 会把小写字母转换为大写字母。(小写字母的 ASCII 码值减去32就得到了对应的大写字符)

这些按键处理函数的分布如下。

keyboard.S

key_table: ... .long alt,do_self,caps,none /* 38-3B alt sp caps br / ... .long none,num,scroll,none / 44-47 br num scr br / ... .long unalt,none,uncaps,none / B8-BB unalt br uncaps br */ ... 1 2 3 4 5 6 7 8 9 4.键盘中断3 我们还剩一些按键没有处理:F1-F12,Insert-PageDown,方向键,小键盘数字键。这一节会全部解决掉。

首先解决 F1-F12 这12个按键。

keyboard.S

func: pushl %eax pushl %ecx pushl %edx call show_stat popl %edx popl %ecx popl %eax subb 0x3B,jbendfunccmpb0x3B, %al jb end_func cmpb 9, %al jbe ok_func subb 18,cmpb18, %al cmpb 10, %al jb end_func cmpb 11,jaendfuncokfunc:cmpl11, %al ja end_func ok_func: cmpl 4, %ecx /* check that there is enough room */ jl end_func movl func_table(, %eax, 4), %eax xorl %ebx, %ebx jmp put_queue end_func: ret

func_table: .long 0x415b5b1b, 0x425b5b1b, 0x435b5b1b, 0x445b5b1b .long 0x455b5b1b, 0x465b5b1b, 0x475b5b1b, 0x485b5b1b .long 0x495b5b1b, 0x4a5b5b1b, 0x4b5b5b1b, 0x4c5b5b1b 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 第3-9行调用 show_stat 函数,打印任务的信息,这个函数的解释在下面。

第10-18行判断 al 的取值是否为 0x3B-0x44,0x57,0x58,如果不是就直接结束。(F1-F12 按键的扫描码为 0x3B-0x44,0x57,0x58,所以在进入 func 时,al 的取值应该是 0x3B-0x44,0x57,0x58)

func_table 中的12个数字代表 F1-F12 映射的 ASCII 码字符。F1 对应于 ESC [[A,F2 对应于 ESC [[B,以此类推。put_queue 会把 eax 中的4个字符都存入读队列中,最后打印出来。

// sched.c void show_task(int nr,struct task_struct *p) { int i, j = 4096 - sizeof(struct task_struct);

printk("%d: pid=%d, state=%d, ", nr, p->pid, p->state);
i = 0;
while (i < j && !((char *)(p + 1))[i])
    i++;
printk("%d (of %d) chars free in kernel stack\r\n", i, j);

}

void show_stat(void) { int i;

for (i = 0; i < NR_TASKS; i++)
    if (task[i])
        show_task(i, task[i]);

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 show_stat 会把系统中存在的所有任务的 pid, state 以及该任务在内核栈的空闲字节数打印出来。

运行结果如下。

Insert-PageDown,方向键,小键盘数字键这些按键是一起处理的。

可以看到,左边按键的扫描码比右边按键的扫描码多一个 0xE0,所以我们可以一个处理函数处理这些按键,用 e0 变量区别左右两边的按键。

keyboard.S

cursor: subb 0x47,jb1fcmpb0x47, %al jb 1f cmpb 12, %al ja 1f jne cur2 # 不是delete或小数点键则跳转 testb 0x0c, mode # 是否按下Ctrl je cur2 testb 0x30, mode # 是否按下Alt jne reboot cur2: cmpb 0x01, e0 # 扫描码中是否有e0 je cur testb 0x02, leds # 数字锁是否打开 je cur testb $0x03, mode # 是否按下Shift jne cur xorl %ebx, %ebx movb num_table(%eax), %al jmp put_queue 1: ret

cur:movb cur_table(%eax), %al cmpb 9,jaokcurmovb'9, %al ja ok_cur movb '~, %ah ok_cur: shll 16,movw16, %eax movw 0x5b1b, %ax xorl %ebx, %ebx jmp put_queue

num_table: .ascii "789 456 1230."

cur_table: .ascii "HA5 DGC YB623"

reboot: call kb_wait movw 0x1234,0x472/dontdomemorycheck/movb0x1234,0x472 /* don't do memory check */ movb 0xfc,%al /* pulse reset and A20 low */ outb %al,$0x64 die:jmp die 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 第3-6行判断扫描码是否在合理的范围内,如果不是就返回。如果同时按下 Ctrl、Alt 以及 delete 或 小数点键,会跳转到 reboot,该子程序通过设置键盘控制器,向复位线输出负脉冲,使系统复位重启。但是,无论在 bochs 模拟器还是在 vmware 虚拟机都无法测试该功能,只能在实体机上测试了。

第13-18行,没有 e0 或没打开数字锁或没按下 Shift 就直接跳转到 cur。这就表示按下的是数字小键盘的按键,将数字映射表的 ASCII 码存入读队列中。

第24-32行会将3或4个字符存入读队列中。以 Home 和 Insert 为例,按下 Home 会向读队列中放入3个字符,打印 ^[[H,按下 Insert 会向读队列中放入4个字符,打印 ^[[2~。

下面是这些按键的分布。

key_table: ... .long alt,do_self,caps,func /* 38-3B br sp caps f1 / .long func,func,func,func / 3C-3F f2 f3 f4 f5 / .long func,func,func,func / 40-43 f6 f7 f8 f9 / .long func,num,scroll,cursor / 44-47 f10 num scr home / .long cursor,cursor,do_self,cursor / 48-4B up pgup - left / .long cursor,cursor,do_self,cursor / 4C-4F n5 right + end / .long cursor,cursor,cursor,cursor / 50-53 dn pgdn ins del / .long none,none,do_self,func / 54-57 br br < f11 / .long func,none,none,none / 58-5B f12 br br br */ ... 1 2 3 4 5 6 7 8 9 10 11 12 键盘中断的内容终于结束了,我也觉得这内容有点多而且繁琐。感觉这些内容了解就好,不必深究,毕竟学习操作系统,任务管理、文件系统这些才是精华。

5.完善终端 我们的终端还有一些小 bug 需要修复。比如,在换行八十几次后,光标就跑到了屏幕首行。

这应该是滚屏的时候出现了问题。具体出错位置还需要通过调试一步一步定位。

排查后发现,果然是滚屏的时候出了问题,具体是在超出显存的时候出的问题。编译器生成的汇编代码与我写的C语言代码的逻辑不一样,这种问题就很尴尬了,不好做修改。经过多次尝试,我发现更改代码顺序就好了,修改的代码如下所示。

// console.c static void scrup(void) { if (video_type == VIDEO_TYPE_EGAC || video_type == VIDEO_TYPE_EGAM) { if (!top && bottom == video_num_lines) { ... if (scr_end <= video_mem_end) { ... } else { int tmp = origin; origin = video_mem_start; pos = video_mem_start + (video_num_lines - 1) * video_size_row; scr_end = pos + video_size_row; asm("cld\n\t" "rep\n\t" "movsd\n\t" "movl %2, %%ecx\n\t" "rep\n\t" "stosw" ::"a"(video_erase_char), "c"((video_num_lines - 1) * video_num_columns >> 1), "m"(video_num_columns), "D"(video_mem_start), "S"(tmp) ); y = bottom - 1; x = 0; } set_origin(); } ... } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 bug 修复后,我们把注意力放回到终端上。终端的功能目前已经够用了,但是不够完善,不够强大。

// tty_io.c int tty_write(unsigned channel, char *buf, int nr) { static int cr_flag = 0; struct tty_struct *tty; char c, *b = buf;

if (channel > 0 || nr < 0)
    return -1;
tty = channel + tty_table;

while (nr > 0) {
    if (current->signal)
        break;
    while (nr > 0 && !FULL(tty->write_q)) {
        c = get_fs_byte(b);
        if (O_POST(tty)) {
            if (c == '\r' && O_CRNL(tty))
                c = '\n';
            else if (c=='\n' && O_NLRET(tty))
                c = '\r';
            if (c == '\n' && !cr_flag && O_NLCR(tty)) {
                cr_flag = 1;
                PUTCH(13, tty->write_q);
                continue;
            }
            if (O_LCUC(tty))
                c = toupper(c);
        }
        b++; nr--;
        PUTCH(c, tty->write_q);
    }
    tty->write(tty);
    if (nr > 0)
        schedule();
}
return (b - buf);

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 tty_write 函数中添加的内容主要是对换行符的处理。我们之前将终端的输出模式设置为 OPOST | ONLCR。这代表需要对字符处理后才放入写队列,将换行转换成回车换行(即将 \n 转换为 \r\n)。修改 tty_write后,printk 的字符串中只需加 \n 就可以完成换行了。

// tty_io.c void copy_to_cooked(struct tty_struct *tty) { signed char c; while (!EMPTY(tty->read_q) && !FULL(tty->secondary)) { ... if (I_UCLC(tty)) // 所有字母都转换为小写 c = tolower(c); if (L_CANON(tty)) { // 使用标准输入模式 if (c == KILL_CHAR(tty)) { // ^U 删除当前行的字符 while(!(EMPTY(tty->secondary) || (c = LAST(tty->secondary)) == 10 || c == EOF_CHAR(tty))) { // \0 if (L_ECHO(tty)) { if (c < 32) PUTCH(127, tty->write_q); PUTCH(127, tty->write_q); tty->write(tty); } DEC(tty->secondary.head); } continue; } if (c == ERASE_CHAR(tty)) { // del 删除一个字符 if (EMPTY(tty->secondary) || (c = LAST(tty->secondary)) == 10 || c == EOF_CHAR(tty)) continue; if (L_ECHO(tty)) { if (c < 32) PUTCH(127, tty->write_q); PUTCH(127, tty->write_q); tty->write(tty); } DEC(tty->secondary.head); continue; } if (c == STOP_CHAR(tty)) { // ^S 停止终端 tty->stopped = 1; continue; } if (c == START_CHAR(tty)) { // ^Q 启动终端 tty->stopped = 0; continue; } } ... if (L_ECHO(tty)) { ... tty->write(tty); } PUTCH(c, tty->secondary); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 第10-45行是对一些特殊字符的处理。

^U会删除当前行的所有字符,secondary 队列用于保存之前输出的字符,如果之前没有输出字符(即 secondary 队列为空),或最后输出的是换行符或 \0,就什么也不做,重新获取字符。否则向写队列中添加 del 字符(ASCII 码 127),对于 ASCII 码值小于32的字符,我们会以 ^ + 字母的形式输出,所以需要删除2个字符,更新 secondary 队列,循环直至退出。删除一个字符的操作与 ^U 的操作差不多,不多赘述。

^S 和 ^Q 会设置 stopped 成员,控制终端的运行。它们需要结合 sh 可执行文件才有用。

第52行将当前字符存入 secondary 队列中。

// tty_io.c #define INTMASK (1<<(SIGINT-1)) #define QUITMASK (1<<(SIGQUIT-1)) void tty_intr(struct tty_struct *tty, int mask) { int i;

if (tty->pgrp <= 0)
    return;
for (i = 0; i < NR_TASKS; i++)
    if (task[i] && task[i]->pgrp == tty->pgrp)
        task[i]->signal |= mask;

}

static void sleep_if_empty(struct tty_queue *queue) { cli(); while (!current->signal && EMPTY(*queue)) interruptible_sleep_on(&queue->proc_list); sti(); }

void wait_for_keypress(void) { sleep_if_empty(&tty_table[0].secondary); }

void copy_to_cooked(struct tty_struct *tty) { signed char c; while (!EMPTY(tty->read_q) && !FULL(tty->secondary)) { ... if (L_ISIG(tty)) { if (c == INTR_CHAR(tty)) { // ^C tty_intr(tty, INTMASK); continue; } if (c == QUIT_CHAR(tty)) { // ^I tty_intr(tty, QUITMASK); continue; } } if (c == 10 || c == EOF_CHAR(tty)) tty->secondary.data++; if (L_ECHO(tty)) { ... } PUTCH(c, tty->secondary); } wake_up(&tty->secondary.proc_list); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 copy_to_cooked 的内容过多,所以我将它分为两部分进行讲解。我们对终端设置了 ISIG 标志位,当输入 INTR、QUIT、SUSP 或 DSUSP 时,会产生相应的信号。当按下 ^C 或 ^I 时,会向终端所属的进程组的每一个任务发送信号。

wait_for_keypress 会阻塞当前任务,当按下按键才会使该任务重新运行。按下按键,程序会将按键 ASCII 码保存到辅助队列中(第48行),之后会唤醒任务(第50行)。任务被唤醒后,会回到第18行的循环中,由于此时辅助队列不为空,退出循环,任务可以重新运行。我们在加载文件系统的时候会用到这个函数。

终端的内容告一段落,下一章是文件系统,这部分内容真的很难,我还没把代码划分出来。