Linux0.11 键盘中断处理过程
键盘中断初始化
在console.c的con_init(void)中:
void con_init(void)
{
...
set_trap_gate (0x21, &keyboard_interrupt);
...
}
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ( "movw %%dx,%%ax\n\t" \
// 将偏移地址低字与选择符组合成描述符低4 字节(eax)。
"movw %0,%%dx\n\t" \
// 将类型标志字与偏移高字组合成描述符高4 字节(edx)。
"movl %%eax,%1\n\t" \
// 分别设置门描述符的低4 字节和高4 字节。
"movl %%edx,%2":
:"i" ((short) (0x8000 + (dpl << 13) + (type << 8))),
"o" (*((char *) (gate_addr))),
"o" (*(4 + (char *) (gate_addr))), "d" ((char *) (addr)), "a" (0x00080000))
IDT中表项的结构为:

设置键盘中断陷阱门,其特权级为0,中断描述符类型为15,中断号为21。
movw %%dx,%%ax指令结束后,ax的值为0x00addr,长度为两个字节;
movw %0,%%dx指令中,%0的值为0x8000 + (dpl << 13) + (type << 8) = 0x8F00,所以dx也为0x8F00;
movl %%eax,%1指令将eax中的值赋给idt表项的低32位,值为0x0008 00addr,
movl %%edx,%2指令将edx中的值赋给idt表项的高32位,值为0x0000 8F00.
此时就完成了中断门的初始化。
键盘中断流程

参考: 键盘中断
当按下键盘时,中断控制器向CPU发送中断请求,通过中断号调用idt表的keyboard_interrupt中断程序:
_keyboard_interrupt:
pushl %eax
pushl %ebx
pushl %ecx
pushl %edx
push %ds
push %es
movl $0x10,%eax // 将ds、es 段寄存器置为内核数据段。
mov %ax,%ds
mov %ax,%es
xorl %al,%al /* %eax is scan code */ /* eax 中是扫描码 */
inb $0x60,%al
cmpb $0xe0,%al //
cmpb $0xe1,%al // 扫描码是0xe1 吗?如果是则跳转到设置e1 标志代码处。
je set_e1
call key_table(,%eax,4) // 调用键处理程序ker_table + eax * 4(参见下面502 行)。
movb $0,e0 // 复位e0 标志。
// 下面这段代码(55-65 行)是针对使用8255A 的PC 标准键盘电路进行硬件复位处理。端口0x61 是
// 8255A 输出口B 的地址,该输出端口的第7 位(PB7)用于禁止和允许对键盘数据的处理。
// 这段程序用于对收到的扫描码做出应答。方法是首先禁止键盘,然后立刻重新允许键盘工作。
e0_e1: inb $0x61,%al // 取PPI 端口B 状态,其位7 用于允许/禁止(0/1)键盘。
jmp 1f // 延迟一会。
1: jmp 1f
1: orb $0x80,%al // al 位7 置位(禁止键盘工作)。
jmp 1f // 再延迟一会。
1: jmp 1f
1: outb %al,$0x61 // 使PPI PB7 位置位。
jmp 1f // 延迟一会。
1: jmp 1f
1: andb $0x7F,%al // al 位7 复位。
outb %al,$0x61 // 使PPI PB7 位复位(允许键盘工作)。
movb $0x20,%al // 向8259 中断芯片发送EOI(中断结束)信号。
outb %al,$0x20
pushl $0 // 控制台tty 号=0,作为参数入栈。
call _do_tty_interrupt // 将收到的数据复制成规范模式数据并存放在规范字符缓冲队列中。
addl $4,%esp // 丢弃入栈的参数,弹出保留的寄存器,并中断返回。
pop %es
pop %ds
popl %edx
popl %ecx
popl %ebx
popl %eax
iret
通过inb $0x60,%al 读取扫描码放入al, 判断扫描码是否为0xe0或0xe0,0xe0、0xe1说明这个键的扫描码是有多个字节的,需要保存下来等待接下来的扫描码,组合成完整的扫描码。
现在按照扫描码为普通的按键扫描码来进行,接下来调用对应按键的处理程序call key_table(,%eax,4),也就是call key_table + 4 * %eax
key_table:
.long none,do_self,do_self,do_self /* 00-03 s0 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 */
...
普通按键调用do_self函数:
do_self:
// 用于根据模式标志mode 选择alt_map、shift_map 或key_map 映射表之一。
lea alt_map,%ebx // alt 键同时按下时的映射表基址alt_map -> ebx。
testb $0x20,mode
jne 1f // 是,则向前跳转到标号1 处。
lea shift_map,%ebx // shift 键同时按下时的映射表基址shift_map -> ebx。
testb $0x03,mode // 有shift 键同时按下了吗?
jne 1f // 有,则向前跳转到标号1 处。
lea key_map,%ebx // 否则使用普通映射表key_map。
1: movb (%ebx,%eax),%al
...
call put_queue // 将字符放入缓冲队列中。
none: ret
通过movb (%ebx,%eax),%al来查key_map表,eax中存放的是扫描码,ebx为key_map表的基址,将查到的ASCII码存入al。最后调用put_queue函数,将取得的字符放入缓冲队列中。
// 以下是美式键盘的扫描码映射表。
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
put_queue代码:
put_queue:
pushl %ecx // 保存ecx,edx 内容。
pushl %edx // 取控制台tty 结构中读缓冲队列指针。
movl table_list,%edx
movl head(%edx),%ecx // 取缓冲队列中头指针放入ecx。
1: movb %al,buf(%edx,%ecx) // 将al 中的字符放入缓冲队列头指针位置处。
incl %ecx // 头指针前移1 字节。
andl $size-1,%ecx // 以缓冲区大小调整头指针(若超出则返回缓冲区开始)。
cmpl tail(%edx),%ecx # buffer full - discard everything
// 头指针==尾指针吗(缓冲队列满)?
je 3f // 如果已满,则后面未放入的字符全抛弃。
shrdl $8,%ebx,%eax // 将ebx 中8 位比特位右移8 位到eax 中,但ebx 不变。
je 2f // 还有字符吗?若没有(等于0)则跳转。
shrl $8,%ebx // 将ebx 中比特位右移8 位,并跳转到标号1 继续操作。
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
table_list是tty缓冲队列地址表的首地址,table_list的结构如下所示:
struct tty_queue *table_list[] = {
&tty_table[0].read_q, &tty_table[0].write_q, // 控制台终端读、写缓冲队列地址。
&tty_table[1].read_q, &tty_table[1].write_q, // 串行口1 终端读、写缓冲队列地址。
&tty_table[2].read_q, &tty_table[2].write_q // 串行口2 终端读、写缓冲队列地址。
};
tty_queue结构如下所示:
// tty 等待队列数据结构。
struct tty_queue
{
unsigned long data; // 等待队列缓冲区中当前数据指针字符数)。
// 对于串口终端,则存放串行端口地址。
unsigned long head; // 缓冲区中数据头指针。
unsigned long tail; // 缓冲区中数据尾指针。
struct task_struct *proc_list; // 等待进程列表。
char buf[TTY_BUF_SIZE]; // 队列的缓冲区。
};
head定义为4,所以movl head(%edx),%ecx执行后,ecx中为读缓冲区中数据头指针。
buf定义为16,为队列缓冲区的偏移,movb %al,buf(%edx,%ecx)中,buf(%edx,%ecx)为在缓冲区中,头指针所在的位置。其中tty_queue结构体中的头指针和尾指针都为相对地址。
put_queue函数和do_self函数返回后,就执行call do_tty_interrupt函数,作用是将收到的数据复制并存放在规范字符缓冲区。
do_tty_interrupt在kernel/chr_drv/tty_io.c:
void do_tty_interrupt (int tty)
{
copy_to_cooked (tty_table + tty);
}
void copy_to_cooked (struct tty_struct *tty)
{
signed char c;
// 如果tty 的读队列缓冲区不空并且辅助队列缓冲区为空,则循环执行下列代码。
while (!EMPTY (tty->read_q) && !FULL (tty->secondary))
{
// 从队列尾处取一字符到c,并前移尾指针。
GETCH (tty->read_q, c);
//#define GETCH(queue,c) \
//(void)({c=(queue).buf[(queue).head]=(c);INC((queue).head);})
//具体见include/linux/tty.h
// 下面对输入字符,利用输入模式标志集进行处理。
// 如果该字符是回车符CR(13),则:若回车转换行标志CRNL 置位则将该字符转换为换行符NL(10);
// 否则若忽略回车标志NOCR 置位,则忽略该字符,继续处理其它字符。
if (c == 13)
if (I_CRNL (tty))
c = 10;
else if (I_NOCR (tty))
continue;
else;
// 如果该字符是换行符NL(10)并且换行转回车标志NLCR 置位,则将其转换为回车符CR(13)。
else if (c == 10 && I_NLCR (tty))
c = 13;
// 如果大写转小写标志UCLC 置位,则将该字符转换为小写字符。
if (I_UCLC (tty))
c = tolower (c);
// 如果本地模式标志集中规范(熟)模式标志CANON 置位,则进行以下处理。
if (L_CANON (tty))
{
// 如果该字符是键盘终止控制字符KILL(^U),则进行删除输入行处理。
if (c == KILL_CHAR (tty))
{
/* deal with killing the input line *//* 删除输入行处理 */
// 如果tty 辅助队列不空,或者辅助队列中最后一个字符是换行NL(10),或者该字符是文件结束字符
// (^D),则循环执行下列代码。
while (!(EMPTY (tty->secondary) ||
(c = LAST (tty->secondary)) == 10 ||
c == EOF_CHAR (tty)))
{
// 如果本地回显标志ECHO 置位,那么:若字符是控制字符(值<32),则往tty 的写队列中放入擦除
// 字符ERASE。再放入一个擦除字符ERASE,并且调用该tty 的写函数。
if (L_ECHO (tty))
{
if (c < 32)
PUTCH (127, tty->write_q);
PUTCH (127, tty->write_q);
tty->write (tty);
}
// 将tty 辅助队列头指针后退1 字节。
DEC (tty->secondary.head);
}
continue; // 继续读取并处理其它字符。
}
// 如果该字符是删除控制字符ERASE(^H),那么:
if (c == ERASE_CHAR (tty))
{
// 若tty 的辅助队列为空,或者其最后一个字符是换行符NL(10),或者是文件结束符,继续处理
// 其它字符。
if (EMPTY (tty->secondary) ||
(c = LAST (tty->secondary)) == 10 || c == EOF_CHAR (tty))
continue;
// 如果本地回显标志ECHO 置位,那么:若字符是控制字符(值<32),则往tty 的写队列中放入擦除
// 字符ERASE。再放入一个擦除字符ERASE,并且调用该tty 的写函数。
if (L_ECHO (tty))
{
if (c < 32)
PUTCH (127, tty->write_q);
PUTCH (127, tty->write_q);
tty->write (tty);
}
// 将tty 辅助队列头指针后退1 字节,继续处理其它字符。
DEC (tty->secondary.head);
continue;
}
//如果该字符是停止字符(^S),则置tty 停止标志,继续处理其它字符。
if (c == STOP_CHAR (tty))
{
tty->stopped = 1;
continue;
}
// 如果该字符是停止字符(^Q),则复位tty 停止标志,继续处理其它字符。
if (c == START_CHAR (tty))
{
tty->stopped = 0;
continue;
}
}
// 若输入模式标志集中ISIG 标志置位,则在收到INTR、QUIT、SUSP 或DSUSP 字符时,需要为进程
// 产生相应的信号。
if (L_ISIG (tty))
{
// 如果该字符是键盘中断符(^C),则向当前进程发送键盘中断信号,并继续处理下一字符。
if (c == INTR_CHAR (tty))
{
tty_intr (tty, INTMASK);
continue;
}
// 如果该字符是键盘中断符(^\),则向当前进程发送键盘退出信号,并继续处理下一字符。
if (c == QUIT_CHAR (tty))
{
tty_intr (tty, QUITMASK);
continue;
}
}
// 如果该字符是换行符NL(10),或者是文件结束符EOF(^D),辅助缓冲队列字符数加1。[??]
if (c == 10 || c == EOF_CHAR (tty))
tty->secondary.data++;
// 如果本地模式标志集中回显标志ECHO 置位,那么,如果字符是换行符NL(10),则将换行符NL(10)
// 和回车符CR(13)放入tty 写队列缓冲区中;如果字符是控制字符(字符值<32)并且回显控制字符标志
// ECHOCTL 置位,则将字符'^'和字符c+64 放入tty 写队列中(也即会显示^C、^H 等);否则将该字符
// 直接放入tty 写缓冲队列中。最后调用该tty 的写操作函数。
if (L_ECHO (tty))
{
if (c == 10)
{
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);
}
// 将该字符放入辅助队列中。
PUTCH (c, tty->secondary);
}
// 唤醒等待该辅助缓冲队列的进程(如果有的话)。
wake_up (&tty->secondary.proc_list);
}
其中,tty->write(tty)调用的是kernel/chr_drv/console.c中的con_write()函数:
void con_write(struct tty_struct * tty)
{
int nr;
char c;
//取得写缓冲队列中字符数nr,然后针对每个字符进行处理。
nr = CHARS(tty->write_q);
while (nr--) {
//取一个字符,根据前面处理字符的状态,来确定state,state转换关系:
//state=0:初始状态,或者原是state=4,或者原是状态1,但字符不是‘[';
//1:原是状态0,并且字符是转义字符ESC(0x1b = 033 = 27)
//2:原是状态1,并且字符是'['
//3:原是状态2,或者原是状态3,并且字符是‘;’或数字
//4:原是状态3,并且字符不是‘;’或者数字
GETCH(tty->write_q,c);
switch(state) {
case 0:
//如果字符不是控制字符(c>31)并且不是扩展字符(c>127)
if (c>31 && c<127) {
//如果当前光标在行末端或者末端外,则将光标移到下行头列,
//并且调整光标位置对应的内存指针pos
if (x>=video_num_columns) {
x -= video_num_columns;
pos -= video_size_row;
lf();
}
//将字符c写到显示内存中pos处,并且光标右移1列,同时pos对应移动两个字节
__asm__("movb attr,%%ah\n\t"
"movw %%ax,%1\n\t"
::"a" (c),"m" (*(short *)pos)
);
pos += 2;
x++;
//接下来是对于其他特殊字符的分析,具体见linux0.11源码分析,这里就讨论普通字符。
} else if (c==27)
state=1;
else if (c==10 || c==11 || c==12)
lf();
else if (c==13)
cr();
else if (c==ERASE_CHAR(tty))
del();
else if (c==8) {
if (x) {
x--;
pos -= 2;
}
} else if (c==9) {
c=8-(x&7);
x += c;
pos += c<<1;
if (x>video_num_columns) {
x -= video_num_columns;
pos -= video_size_row;
lf();
}
c=9;
} else if (c==7)
sysbeep();
break;
case 1:
state=0;
if (c=='[')
state=2;
else if (c=='E')
gotoxy(0,y+1);
else if (c=='M')
ri();
else if (c=='D')
lf();
else if (c=='Z')
respond(tty);
else if (x=='7')
save_cur();
else if (x=='8')
restore_cur();
break;
case 2:
for(npar=0;npar<NPAR;npar++)
par[npar]=0;
npar=0;
state=3;
if ((ques=(c=='?')))
break;
case 3:
if (c==';' && npar<NPAR-1) {
npar++;
break;
} else if (c>='0' && c<='9') {
par[npar]=10*par[npar]+c-'0';
break;
} else state=4;
case 4:
state=0;
switch(c) {
case 'G': case '`':
if (par[0]) par[0]--;
gotoxy(par[0],y);
break;
case 'A':
if (!par[0]) par[0]++;
gotoxy(x,y-par[0]);
break;
case 'B': case 'e':
if (!par[0]) par[0]++;
gotoxy(x,y+par[0]);
break;
case 'C': case 'a':
if (!par[0]) par[0]++;
gotoxy(x+par[0],y);
break;
case 'D':
if (!par[0]) par[0]++;
gotoxy(x-par[0],y);
break;
case 'E':
if (!par[0]) par[0]++;
gotoxy(0,y+par[0]);
break;
case 'F':
if (!par[0]) par[0]++;
gotoxy(0,y-par[0]);
break;
case 'd':
if (par[0]) par[0]--;
gotoxy(x,par[0]);
break;
case 'H': case 'f':
if (par[0]) par[0]--;
if (par[1]) par[1]--;
gotoxy(par[1],par[0]);
break;
case 'J':
csi_J(par[0]);
break;
case 'K':
csi_K(par[0]);
break;
case 'L':
csi_L(par[0]);
break;
case 'M':
csi_M(par[0]);
break;
case 'P':
csi_P(par[0]);
break;
case '@':
csi_at(par[0]);
break;
case 'm':
csi_m();
break;
case 'r':
if (par[0]) par[0]--;
if (!par[1]) par[1] = video_num_lines;
if (par[0] < par[1] &&
par[1] <= video_num_lines) {
top=par[0];
bottom=par[1];
}
break;
case 's':
save_cur();
break;
case 'u':
restore_cur();
break;
}
}
}
set_cursor();
}
最后通过set_cursor()向显示器发送光标位置,并且显示在显示器上。
此时,在键盘上按下的按键对应的字符就在显示器上显示出来了。
Linux0.11键盘中断处理
本文详细解析了Linux0.11操作系统中键盘中断的处理流程,包括键盘中断的初始化过程、键盘扫描码的读取及处理、字符映射表的应用、以及最终将键盘输入显示在屏幕上的全过程。
6882

被折叠的 条评论
为什么被折叠?



