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_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);}}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 键盘中断处理过程相关推荐
- Linux0.11内核剖析--内核体系结构
一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: ...
- Linux0.11启动过程
从开机加电,到执行main函数之前的过程 好吧,这里应该是有执行3个汇编的文件,但是我不太了解.囧 从main函数,到启动OK(即可以响应用户操作了) 这个步骤做了3件事情: 创建进程0,使之具备在主 ...
- Linux0.11内核引导启动过程概述
Linux0.11仅支持x86架构.它的内核引导启动程序在文件夹boot内,共有三个汇编代码文件.按照启动流程依次是: (1)bootsect.s.boot是启动引导的意思,sect即sector,是 ...
- 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 ...
- Linux0.11内核源码解析-setup.s
学习资料: Linux内核完全注释 操作系统真像还原 极客时间-Linux内核源码趣读 Linux0.11内核源码 ->setup程序将system模块从0x10000~0x8ffff整块向下移 ...
- linux信号嵌套,LINUX0.11信号机制
原标题:LINUX0.11信号机制 一.信号的本质 信号(signal)是Linux操作系统在软件层面上对中断的模拟,是一种异步通信机制.进程之间可以相互发送信号来通知对方发生了什么事情,一个进程不必 ...
- 中断处理过程示意图_Linux中断处理
简介 1.基于Linux0.11代码进行分析. 2.中断类型分类以及具体的中断. 3.中断向量的注册. 4.中断处理流程. 5.各类型中断的具体执行流程. 中断的类型及具体的种类 Linux0.11内 ...
- linux0.11字符设备驱动及访问请求管理程序阅读注释笔记
[ 1] linux0.11引导程序阅读注释. [ 2] linux0.11由实模式进入保护模式程序阅读注释 . [ 3] linux0.11护模式初始化程序阅读注释. [ 4] linux0.11主 ...
- LINUX0.11内核阅读笔记
我是通过阅读赵炯老师编的厚厚的linux内核完全剖析看完LINUX0.11的代码,不得不发自内心的说Linus真的是个天才.虽然我觉得很多OS设计的思想他是从UNIX学来的,但是他自己很周全很漂亮很巧 ...
最新文章
- 机器人导航两篇顶级会议论文解析
- 添加背景音乐(解决苹果手机不能自动播放问题)
- java 让进度条动起来_自定义进度条动起来
- 5G UE — USIM Card — 5G 的 USIM 卡
- mapreduce原理
- hibernate中merge/attachDirty/attachClean
- 从头到尾再讲一遍ThreadLocal
- 如何处理分析Flink作业反压的问题?
- android(安卓)手机 markdown不错的编辑器
- 5G时代需要新的商业模式,国产芯片将不再落后
- 在学校外边找了份工作
- 2022跨年烟花代码(五)HTML5全屏烟花特效
- scratch炫酷格斗游戏
- html中form表单提交中文乱码问题基本解决办法
- windows10家庭版升级为专业版
- 大数据——Flume组件Source、Channel和Sink具体使用
- 动环监控串口,动环监控系统接口
- php socket 介绍
- 职业高一计算机试题,信息技术教师考试题库
- JSON转换为JS对象和JS对象转换为JSON