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);}}elsePUTCH (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) {//如果当前光标在行末端或者末端外,则将光标移到下行头列,//并且调整光标位置对应的内存指针posif (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 键盘中断处理过程相关推荐

  1. Linux0.11内核剖析--内核体系结构

    一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: ...

  2. Linux0.11启动过程

    从开机加电,到执行main函数之前的过程 好吧,这里应该是有执行3个汇编的文件,但是我不太了解.囧 从main函数,到启动OK(即可以响应用户操作了) 这个步骤做了3件事情: 创建进程0,使之具备在主 ...

  3. Linux0.11内核引导启动过程概述

    Linux0.11仅支持x86架构.它的内核引导启动程序在文件夹boot内,共有三个汇编代码文件.按照启动流程依次是: (1)bootsect.s.boot是启动引导的意思,sect即sector,是 ...

  4. main 函数解析(二)—— Linux-0.11 学习笔记(六)

    main函数解析(二)--Linux-0.11 学习笔记(六) 4.6 blk_dev_init函数 void blk_dev_init(void) {int i;for (i=0 ; i<NR ...

  5. Linux0.11内核源码解析-setup.s

    学习资料: Linux内核完全注释 操作系统真像还原 极客时间-Linux内核源码趣读 Linux0.11内核源码 ->setup程序将system模块从0x10000~0x8ffff整块向下移 ...

  6. linux信号嵌套,LINUX0.11信号机制

    原标题:LINUX0.11信号机制 一.信号的本质 信号(signal)是Linux操作系统在软件层面上对中断的模拟,是一种异步通信机制.进程之间可以相互发送信号来通知对方发生了什么事情,一个进程不必 ...

  7. 中断处理过程示意图_Linux中断处理

    简介 1.基于Linux0.11代码进行分析. 2.中断类型分类以及具体的中断. 3.中断向量的注册. 4.中断处理流程. 5.各类型中断的具体执行流程. 中断的类型及具体的种类 Linux0.11内 ...

  8. linux0.11字符设备驱动及访问请求管理程序阅读注释笔记

    [ 1] linux0.11引导程序阅读注释. [ 2] linux0.11由实模式进入保护模式程序阅读注释 . [ 3] linux0.11护模式初始化程序阅读注释. [ 4] linux0.11主 ...

  9. LINUX0.11内核阅读笔记

    我是通过阅读赵炯老师编的厚厚的linux内核完全剖析看完LINUX0.11的代码,不得不发自内心的说Linus真的是个天才.虽然我觉得很多OS设计的思想他是从UNIX学来的,但是他自己很周全很漂亮很巧 ...

最新文章

  1. 机器人导航两篇顶级会议论文解析
  2. 添加背景音乐(解决苹果手机不能自动播放问题)
  3. java 让进度条动起来_自定义进度条动起来
  4. 5G UE — USIM Card — 5G 的 USIM 卡
  5. mapreduce原理
  6. hibernate中merge/attachDirty/attachClean
  7. 从头到尾再讲一遍ThreadLocal
  8. 如何处理分析Flink作业反压的问题?
  9. android(安卓)手机 markdown不错的编辑器
  10. 5G时代需要新的商业模式,国产芯片将不再落后
  11. 在学校外边找了份工作
  12. 2022跨年烟花代码(五)HTML5全屏烟花特效
  13. scratch炫酷格斗游戏
  14. html中form表单提交中文乱码问题基本解决办法
  15. windows10家庭版升级为专业版
  16. 大数据——Flume组件Source、Channel和Sink具体使用
  17. 动环监控串口,动环监控系统接口
  18. php socket 介绍
  19. 职业高一计算机试题,信息技术教师考试题库
  20. JSON转换为JS对象和JS对象转换为JSON

热门文章

  1. flash图片如何镜像翻转_在canvas上实现元素图片镜像翻转动画效果的方法
  2. Mysql 中null与空字符串陷进
  3. 赋予物联网以AI能力,阿里云Link推万亿唤醒计划
  4. ECG分析:基于深度学习的ECG心律失常分类入门(3)
  5. webpack 最佳实践
  6. 虾米音乐代码注释惊现 “穷逼 vip”,程序员要出来接锅了
  7. Linux 查看系统相关信息命令
  8. 基于物联网的NodeJs-5天学习入门指引
  9. uva662DP+回溯
  10. Nextcloud私有云 - 零基础搭建私有云盘