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进行中断处理(显示器:显示图像;键盘:读数据到内存)