本文为学习李治军老师《操作系统原理、实现与实践》第八章的总结,主要讲述显示器、键盘设备驱动。
参考资料:
第八章 设备驱动
设备驱动的基本原理
-
外设工作原理
-
从CPU开始:CPU发送命令(给外设中的寄存器),最终表现为执行指令“out ax, 端口号”;
-
从外设开始:外设在工作完成后或出现状态变化时产生中断 ,CPU 通过中断处理程序完成后续工作;
-
-
文件视图
-
不论什么样的外设,操作系统都将其统一抽象成一个文件,程序员通过文件接口open, read, write, close来使用外设;
-
不同的设备对应不同的设备文件:/dev/xxx;
-
上层用户使用外设的基本结构:
int fd = open(“/dev/xxx”);//打开外设文件 for (int i = 0; i < 10; i++) { write(fd,i,sizeof(int));//向外设文件写入 } close(fd);//关闭外设文件
-
-
补充:设备文件(参考IO设备)
-
操作系统把一切外设都映射为文件,被称作设备文件(如磁盘文件),常见的设备文件又分为三种:
1.字符设备 如键盘,鼠标,串口等(以字节为单位顺序访问);2.块设备 如磁盘驱动器,磁带驱动器,光驱等(均匀的数据库访问);
3.网络设备 如以太网,无线,蓝牙等(格式化报文交换);
-
显示器的驱动
-
显示器和键盘构成了终端设备 dev/tty,显示器只写,键盘只读;
-
以printf为例:
-
printf()
调用系统调用write(1, buf, count)
; -
write
的内核实现是sys_write
:找到所写文件的属性,根据设备文件中存放的设备属性信息分支到相应的操作命令;-
分支1:根据文件属性(是否是字符设备);
//在linux/fs/read_write.c中 int sys_write(unsigned int fd, char *buf,int cnt) { inode = file->f_inode; if(S_ISCHR(inode->i_mode)) return rw_char(WRITE,inode->i_zone[0], buf,cnt); ...
-
分支2:在
rw_char
中根据不同的读写函数://在linux/fs/char_dev.c中 int rw_char(int rw, int dev, char *buf, int cnt) { //以主设备号MAJOR(dev)为索引从函数表crw_table中找到和终端设备对应的读写函数rw_ttyx并调用 crw_ptr call_addr=crw_table[MAJOR(dev)]; call_addr(rw, dev, buf, cnt); ... }
-
分支3:根据读/写操作:
//根据是设备读操作还是设备写操作调用相应的函数 static int rw_ttyx(int rw, unsigned minor, char *buf, int count) { return ((rw==READ)? tty_read(minor,buf): tty_write(minor,buf)); }
-
-
printf
是输出所以调用tty_write(minor,buf)
实现输出:先将内容写到缓冲区里,由操作系统统一将队列中的内容输出到显示器上,如果缓冲区已满,就睡眠等待;在linux/kernel/tty_io.c中 int tty_write(unsigned channel,char *buf,int nr) { struct tty_struct *tty;tty=channel+tty_table; sleep_if_full(&tty->write_q); //输出就是放入队列 ... }
-
如果输出完成或写队列满
int tty_write(unsigned channel,char *buf,int nr) { ... tty->write(tty);//用tty中write函数指针来进行真实的显示器输出 }
-
最终调用con_write向终端写数据,核心代码为:
mov c, al mov attr, ah mov ax, [pos] ;显存和内存独立编址用out,显存和内存混合编址用mov
其中pos为开机后当前光标所在的显存地址;
-
-
printf()输出函数的完整文件视图路线
- printf -> write -> sys_write -> rw_char -> rw_ttyx ->tty_write -> write_q -> con_write -> “mov ax, [pos]”
键盘的驱动
-
从键盘中断开始
-
设置键盘中断号,按下键盘会产生 0x21 号中断,执行
keyboard_interrupt
中的inb $0x60,%al
;void con_init(void) //应为键盘也是console的一部分 { set_trap_gate(0x21, &keyboard_interrupt); } //在kernel/chr_drv/keyboard.S中 .globl _keyboard_interrupt _keyboard_interrupt: inb $0x60,%al //从端口0x60读扫描码 call key_table(,%eax,4) //根据扫描码调用不同处理函数key_table()来处理各个按键 ... push $0 call _do_tty_interrupt
key_table
是一个函数数组,在其中调用do_self()函数处理显示字符,其他特殊按键由func 等其他函数来处理;
-
do_self
函数- 从键盘对应的ASCII码表(key_map中以当前案件的扫描码为索引找到当前案件的ASCII码);
- 找到 tty 结构体中的 read_q 队列,键盘和显示器使用同一个 tty 结构体 tty_table[0],只是键盘使用的读队列,而显示器使用的写队列;
- 将 ASCII 码放到缓冲队列 read_q 中;
-
字符回显,先调用
do_tty_interrupt
返回文件视图(与显示器输出中将字符先放在缓冲队列 write_q 中再显示到屏幕上同理)void do_tty_interrupt(int tty) { copy_to_cooked(tty_table + tty); } void copy_to_cooked(struct tty_struct *tty) { GETCH(tty->read_q, c); PUTCH(c, tty->secondary); ... wake_up(...); }
-
copy_to_cooked
从read_q
中取出字符并放在tty->secondary
队列中,唤醒等待在该队列中的进程; -
对于键盘,用户发起的文件操作是
scanf
,scanf
调用sys_read
,根据printf类似的分支,最终调用tty_read
; -
tty_read
:一旦tty->secondary
有内容了,scanf会将队列中的字符诸葛取出来并复制到用户内存,从设备开始的路线最后回到了文件操作scanf;
-
-
键盘操作的完整文件视图:
- keyboard_interrupt -> inb 0x60, al -> do_self -> read_q -> copy_to_cooked -> secondary -> wake_up -> tty_read -> rw_char ->sys_read ->read ->scanf;
实践项目7
终端设备字符显示的控制