linux下挂载键盘
试想一下,当按下键盘上某个字符时,操作系统是怎样获取到这个字符的呢?然后又怎样回显到终端的呢?
按下键盘触发键盘中断
void main(void)
{
...
tty_init();
...
}
void tty_init(void)
{
rs_init();
con_init();
}
void con_init(void)
{
...
// 设置键盘中断处理函数
set_trap_gate(0x21,&keyboard_interrupt);
...
}
可以从系统的main函数初始化代码中找到键盘中断的初始化设置,并映射一个键盘处理函数。有此可知,当我们按下键盘时,系统会跳到keyboard_interrupt这个中断函数中执行,下面我们来看看keyboard_interrupt这个函数。
分析keyboard_interrupt
// linux-0.11/kernel/chr_drv/keyboard.s
keyboard_interrupt:
...
movl $0x10,%eax /* 进入内核空间 */
mov %ax,%ds
mov %ax,%es
xor %al,%al /* %eax is scan code */
inb $0x60,%al /* 获取键盘字符关键代码,从键盘控制器的0x60端口读取扫描码到ax中 */
cmpb $0xe0,%al
je set_e0
cmpb $0xe1,%al
je set_e1
// 调用key_table表,后面的(,%eax,4)表示key_table
// 的地址偏移参数,在此表示的偏移:key_table + eax*4
// 其中eax中装的就是此时键盘按下的字符
// 4表示系数,因为key_table表中的变量都是32位长度,对应4个字节大小
call key_table(,%eax,4)
...
// 键盘字符的回显在这里实现的,最后再讲
pushl $0
call do_tty_interrupt
...
上面最后会进入key_table中寻找函数指针,下面我们来看看key_table。
分析key_table
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 */
...
.long alt,do_self,caps,func /* 38-3B alt 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 sysreq ? < f11 */
.long func,none,none,none /* 58-5B f12 ? ? ? */
...
从表中可以看出是一堆32位长度的变量,这些变量就对应着字符的进一步处理的函数指针,假如我们刚才按下的字符是a,那么它就会进入do_self这个函数指针中执行。
分析do_self
do_self:
// 这部分的代码功能主要是将alt_map或者
// shift_map或者key_map的首地址放入ebx寄存器中
// 为啥这样做呢?因为上面是字符映射表来着,换句话说,
// 现在给这些首地址加一个偏移量,之后取出的字符就是我们按下的键盘字符
lea alt_map,%ebx
testb $0x20,mode /* alt-gr */
jne 1f
lea shift_map,%ebx
testb $0x03,mode
jne 1f
lea key_map,%ebx
// 这句代码的意思是将我们按下的字符的ascill保存到ax中
1: movb (%ebx,%eax),%al
...
// 重点来了,现在调用put_queue这个函数
call put_queue
分析put_queue
put_queue:
pushl %ecx
pushl %edx
// 这句代码的意思是将这个表的首地址保存在edx中
// 其实这个首地址就是&tty_table[0].read_q
// 为啥呢,我们看下table_list就可以看到了
movl table_list,%edx
// 这句代码的意思是将读队列中的head值保存在ecx中
movl head(%edx),%ecx
// 重点来了,此时这里的edx表示的是读队列结构体的地址,
// ecx表示读队列中head的值,buf在这里等于16,
// 其实buf(%edx,%ecx)这玩意对应的就是读队列结构体中
// 的buf数组中索引号为head元素的地址。
// 现在将ax中的数据复制到上面那buf中,至此
// 键盘字符的ascill已被保存到指定的内存中,也就是
// 终端的读队列中。
1: movb %al,buf(%edx,%ecx)
...
从上面分析看,我们先看下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,
&tty_table[2].read_q, &tty_table[2].write_q
};
从这个表可以知道table_list的首地址表示的是tty_table[0].read_q这个地址,也就是终端读队列的地址。
从上面代码可以知道我们已经分析到将字符写到内存中,下面我们如何将这个字符显示到终端屏幕上呢?接下来我们继续分析回显,回显函数其实就是上面提到的do_tty_interrupt函数。
键盘字符回显
分析do_tty_interrupt
keyboard_interrupt:
...
// 压栈参数0,目的为do_tty_interrupt函数提供参数
pushl $0
call do_tty_interrupt
...
void do_tty_interrupt(int tty)
{
copy_to_cooked(tty_table+tty);
}
从这可以看出会调用copy_to_cooked(tty_table)这个函数,其中参数就是tty_table数组的第一个函数指针,也就是终端设备处理函数指针项。下面我们来分析这个函数。
分析copy_to_table
void copy_to_cooked(struct tty_struct * tty)
{
signed char c;
while (!EMPTY(tty->read_q) && !FULL(tty->secondary)) {
...
// 核心代码:获取读队列中的一个数据放入c中
GETCH(tty->read_q,c);
...
// 核心代码:将c中的数据放入写队列中
PUTCH(c,tty->write_q);
...
// 调用write函数将写缓冲区中的数据传入到终端屏幕上
tty->write(tty);
}
PUTCH(c,tty->secondary);
}
到这里回显就完成了。。。。
总结
键盘的中断捕捉需要研究一下。