IO设备-显示器与键盘#
学习了CPU管理,进程管理,内存管理,下面就开始设备管理的学习了
一、文件视图#
对于管理外设,首先我们要让外设设备工作运行
使用外设:
(1)向外设对应的端口地址发送 CPU 命令
(2)CPU 通过端口地址发送对外设的工作要求,通常就是命令“out ax, 端口号”,其中 AX 寄存器存放的就是让外设工作的具体内容
(3)外设开始工作,工作完成后产生中断 ,CPU 会在中断处理程序中处理外设的工作结果
但是对不同的设备进行操作很麻烦,需要查寄存器地址、内容的格式和语义, 所以操作系统要给用户提供一个简单视图—==文件视图==
控制外设:
1
2
3
4
5
|
int fd = open(“/dev/xxx”);//打开外设文件 - 不同的设备文件名对应不同的外设
for (int i = 0; i < 10; i++) {
write(fd,i,sizeof(int));//向外设文件写入
}
close(fd);关闭外设文件
|
(1) ==不论什么设备都是open, read, write, close==
操作系统为用户提供统一的接口!
(2) 不同的设备对应不同的设备文件(/dev/xxx)
根据设备文件找到控制器的地址、 内容格式
注:
操作系统把一切外设都映射为文件,被称作设备文件(如磁盘文件)
常见的设备文件又分为三种:
1.字符设备 如键盘,鼠标,串口等(以字节为单位顺序访问)
2.块设备 如磁盘驱动器,磁带驱动器,光驱等(均匀的数据库访问)
3.网络设备 如以太网,无线,蓝牙等(格式化报文交换)
二、显示器输出#
以printf为例剖析操作系统怎么工作的
1
|
printf(“Host Name: %s”, name);
|
printf()函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
//printf()产生格式化信息输出到标准设备stdout(1),在屏幕上显示
// 参数fmt:指定输出将采用的格式
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));//printbuf数组
va_end(args);
return i;
}
|
先创建缓存buf将格式化输出都写到那里,然后再write(1,buf,…)
write 的内核实现是 sys_write,sys_write 首先要做的事是找到所写文件的属性,即到底是普通文件还是设备文件。
如果是设备文件,sys_write 要根据设备文件中存放的设备属性信息分支到相应的操作命令中
fd是文件描述符,file的目的是得到inode, 显示器信息应该就在这里
fd=1的文件来源于父进程(fork()
)
current->filp 数据中存放当前进程打开的文件,如果一个文件不是当前进程打开的,那么就一定是其父进程打开后再由子进程继承来的
1
2
3
4
5
6
|
void main(void)
{ if(!fork()){ init(); }
void init(void)
{open(“dev/tty0”,O_RDWR,0);dup(0);dup(0);
execve("/bin/sh",argv,envp)}
|
系统初始化时init()打开了终端设备,dup()是复制,tty0是终端设备。
在 init 函数中我们调用 open 打开一个名为“/dev/tty0”的文件,由于这是该进程打开的第一个文件,所以对应的文件句柄 fd = 0,==接下来使用了两次 dup,使得 fd = 1,fd = 2==也都指向了“/dev/tty0” 的 FCB(文件控制块)。
open系统调用
1
2
3
4
5
6
7
8
9
|
在linux/fs/open.c中
int sys_open(const char* filename, int flag)
{
i=open_namei(filename,flag,&inode);
cuurent->filp[fd]=f; //第一个空闲的fd
f->f_mode=inode->i_mode; f->f_inode=inode;
f->f_count=1;
return fd;
}
|
用open()把设备信息(dev/tty0)的读进来备用,open_namei根据文件名字读入inode,==inode是存放在磁盘上的设备信息==,flip存储在进程的PCB中。
核心就是建立下面的关系:
每个进程(PCB)都有一个自己的file_table,存放inode
inode找到了,继续完成sys_write()
1
2
3
4
5
6
7
|
在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);
...
|
==根据 inode 中的信息判断该文件对应的设备是否是一个字符设备,显示器是字符设备==
如果是字符设备,sys_write 调用函数 rw_char() 中去执行,写设备传入write,==inode->i_zone[0] 中存放的就是该设备的主设备号4和次设备号0==
1
2
3
4
5
6
7
|
在linux/fs/char_dev.c中
int rw_char(int rw, int dev, char *buf, int cnt)
{
crw_ptr call_addr=crw_table[MAJOR(dev)];
call_addr(rw, dev, buf, cnt);
...
}
|
rw_char() 函数中以主设备号(MAJOR(dev))为索引从一个函数表 crw_table 中找到和终端设备对应的读写函数 rw_ttyx,并调用
crw_table定义
1
2
|
static crw_ptr crw_table[]={...,rw_ttyx,};
typedef (*crw_ptr)(int rw, unsigned minor, char *buf, int count)
|
函数 rw_ttyx 中根据是设备读操作还是设备写操作调用相应的函数,
显示器和键盘构成了终端设备 tty,显示器只写,键盘只读
1
2
3
4
|
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)
1
2
3
4
5
6
7
|
在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); //输出就是放入队列
...
}
|
这个函数就是实现输出的核心函数,由于CPU速度快,但是往显示器上写内容速度很慢,所以先将内容写到缓冲区里,即一个队列中,等到合适的时候,由操作系统统一将队列中的内容输出到显示器上,如果缓冲区已满,就睡眠等待
如果没有满,继续看tty_write
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
在linux/kernel/tty_io.c中
int tty_write(unsigned channel, char *buf, int nr)
{
...
char c, *b=buf;
while(nr>0&&!FULL(tty->write_q))
{
c = get_fs_byte(b);
if(c==‘r’){PUTCH(13,tty->write_q);continue;}
if(O_LCUC(tty)) c = toupper(c);
b++; nr--; PUTCH(c,tty->write_q);
} //输出完事或写队列满!
tty->write(tty);
}
|
如果队列没有满,就从用户缓存区读出一个字符(==get_fs_byte()==),进行一些判断和操作后,将字符放入队列tty->write_q 中(PUTCH()),如果读出的字符是 r 或 写队列满后,跳出循环。
继续调用 tty->write()
tty_write结构体
1
2
3
4
5
6
|
在include/linux/tty.h中
struct tty_struct
{
void (*write)(struct tty_struct *tty);
struct tty_queue read_q, write_q;
}
|
在 tty 结构体中可以看到 write 函数,根据对 tty 结构体的初始化可以看出,tty->write 调用的函数是 con_write
。
tty_table的定义及初始化
1
2
3
|
struct tty_struct tty_table[] =
{
{con_write,{0,0,0,0,””},{0,0,0,0,””}},{},…};
|
con_write向终端写数据(内嵌汇编)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
在linux/kernel/chr_drv/console.c中
void con_write(struct tty_struct *tty)
{
GETCH(tty->write_q,c);
if(c>31&&c<127)
{
__asm__(“movb _attr,%%ahnt”
“movw %%ax,%1nt”::”a”(c),
”m”(*(short*)pos):”ax”);
pos+=2;
}
......
}
|
在 con_write() 中,先从缓冲区中读取字符,然后将字符 out 到显示器上
内嵌汇编部分:
ah存储属性(颜色,闪烁),al存储字符,然后将ax里的内容out就行
转为汇编就是mov ax,pos
,将字符打印在了显示器上(将 printf 要显示的字符放在显存的当前光标位置处,pos是显卡的寄存器
在显示器上显示数据,只要往内存的显存中写数据即可
将pos指向显存的当前地址,可以通过con_init()和gotoxy()获取pos坐标
如果显存和内存独立编址则用out,这里显存和内存混合编址则用mov ax, pos
初始化以后 pos 就是开机以后当前光标所在的显存位置。
每次输出后移两位,ax是16寄存器
屏幕上的一个字符在显存中除了字符本身还应该有字符的属性(如颜色等)
所以,printf()输出函数整个工作过程
三、键盘输入#
由于操作系统并不知道,用户什么时候会通过键盘输入数据,所以键盘的输入与中断有关
从键盘中断开始, 从中断初始化开始
设置键盘中断号,按下键盘会产生 0x21 号中断
1
2
|
void con_init(void) //应为键盘也是console的一部分
{ set_trap_gate(0x21, &keyboard_interrupt); }
|
键盘中断处理函数
1
2
3
4
5
6
|
在kernel/chr_drv/keyboard.S中
.globl _keyboard_interrupt
_keyboard_interrupt:
inb $0x60,%al //从端口0x60读扫描吗
call key_table(,%eax,4) //调用key_table+eax*4
... push $0 call _do_tty_interrupt
|
先从键盘的 0x60 端口上获得按键扫描码,然后要根据这个扫描码调用不同的处理函数 key_table()来处理各个按键
key_table是一个函数数组
1
2
3
4
|
在kernel/chr_drv/keyboard.S中
key_table:
.long none,do_self,do_self,do_self //扫描码00-03
.long do_self, ...,func, scroll, cursor ...
|
显示字符通常都用do_self()函数处理,其他特殊按键由func 等其他函数来处理
do_self 先从键盘对应的 ASCII 码表(key_map)中以当前按键的扫描码(存在寄存器 EAX 中)为索引找到当前按键的 ASCII 码
从key_map中取出ASCII码
1
2
3
4
|
#if defined(KBD_US)
key_map: .byte 0,27 .ascii “1234567890-=“ ...
shift_map: .byte 0,27 .ascii “!@#$%^&*()_+” ...
#elif defined(KBD_GR) ...
|
do_self函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
mode: .byte 0
do_self:
lea alt_map,%ebx//找到映射表, 如a的key_map映射为a, 而shift_map映射为A
testb $0x20,mode //alt键是否同时按下 jne 1f
lea shift_map,%ebx testb $0x03,mode jne 1f
lea key_map,%ebx
1: movb (%ebx,%eax),%al //扫描码索引, ASCII码àal
orb %al,%al je none //没有对应的ASCII码
testb $0x4c,mode //看caps是否亮
je 2f cmpb $’a,%al jb 2f
cmpb $’},%al ja 2f subb $32,%al //变大写
2:testb $??,mode //处理其他模式, 如ctrl同时按下
3:andl $0xff,%eax call put_queue
none:ret
|
然后找到 tty 结构体中的 read_q 队列,键盘和显示器使用同一个 tty 结构体 tty_table[0],只是键盘使用的读队列,而显示器使用的写队列
1
2
3
4
5
6
|
struct tty_queue *table_list[]=
{
&tty_table[0].read_q,
&tty_table[0].write_q;
...
};
|
再将 ASCII 码放到缓冲队列 read_q 中用put_queue
1
2
3
4
|
put_queue:
movl _table_list,%edx
movl head(%edx),%ecx
1:movb %al,buf(%edx,%ecx)
|
将 ASCII 码放到缓冲队列 read_q 中后,可显示的字符要回显,先放在缓冲队列 write_q 中,再显示到屏幕上
所以键盘操作的整个过程
I/O读写整体框架:
I/O读写整体三步原理
1.cpu取址执行通过out指令向外设发送命令
2.将命令通过文件形成统一文件视图进行解释
3.外设执行完命令后返回给cpu进行中断处理(显示器:显示图像;键盘:读数据到内存)