操作系统之键盘捕捉

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);
}

到这里回显就完成了。。。。

总结

键盘的中断捕捉需要研究一下。

猜你喜欢

转载自blog.csdn.net/m0_38099380/article/details/89199770