Linux0.11 键盘中断处理过程

Linux0.11键盘中断处理
本文详细解析了Linux0.11操作系统中键盘中断的处理流程,包括键盘中断的初始化过程、键盘扫描码的读取及处理、字符映射表的应用、以及最终将键盘输入显示在屏幕上的全过程。

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_interruptkernel/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()向显示器发送光标位置,并且显示在显示器上。

此时,在键盘上按下的按键对应的字符就在显示器上显示出来了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值