IO设备-显示器与键盘


前言

学习了CPU管理,进程管理,内存管理,下面就开始设备管理的学习了


提示:以下是本篇文章正文内容

一、文件视图

对于管理外设,首先我们要让外设设备工作运行

使用外设:

(1)向外设对应的端口地址发送 CPU 命令

(2)CPU 通过端口地址发送对外设的工作要求,通常就是命令“out ax, 端口号”,其中 AX 寄存器存放的就是让外设工作的具体内容

(3)外设开始工作,工作完成后产生中断 ,CPU 会在中断处理程序中处理外设的工作结果
在这里插入图片描述

但是对不同的设备进行操作很麻烦,需要查寄存器地址、内容的格式和语义,
所以操作系统要给用户提供一个简单视图—文件视图

在这里插入图片描述
控制外设:

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为例剖析操作系统怎么工作的

printf(“Host Name: %s”, name);

printf()函数:

//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 数据中存放当前进程打开的文件,如果一个文件不是当前进程打开的,那么就一定是其父进程打开后再由子进程继承来的

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系统调用

在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()

在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

在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定义

static crw_ptr crw_table[]={
    
    ...,rw_ttyx,}; 
typedef (*crw_ptr)(int rw, unsigned minor, char *buf, int count)

函数 rw_ttyx 中根据是设备读操作还是设备写操作调用相应的函数,
显示器和键盘构成了终端设备 tty,显示器只写,键盘只读

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); //输出就是放入队列
	...
}

这个函数就是实现输出的核心函数,由于CPU速度快,但是往显示器上写内容速度很慢,所以先将内容写到缓冲区里,即一个队列中,等到合适的时候,由操作系统统一将队列中的内容输出到显示器上,如果缓冲区已满,就睡眠等待

如果没有满,继续看tty_write

在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结构体

在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的定义及初始化

struct tty_struct tty_table[] = 
{
    
    
	{
    
    con_write,{
    
    0,0,0,0,””},{
    
    0,0,0,0,””}},{
    
    },};

con_write向终端写数据(内嵌汇编)

在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,%%ah\n\t”
		“movw %%ax,%1\n\t”::”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 号中断

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+eax*4
... push $0 call _do_tty_interrupt

先从键盘的 0x60 端口上获得按键扫描码,然后要根据这个扫描码调用不同的处理函数 key_table()来处理各个按键

key_table是一个函数数组

在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码

#if defined(KBD_US)
key_map: .byte 0,27 .ascii “1234567890-=...
shift_map: .byte 0,27 .ascii “!@#$%^&*()_+...
#elif defined(KBD_GR) ...

do_self函数

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],只是键盘使用的读队列,而显示器使用的写队列

struct tty_queue *table_list[]=
{
    
    
	&tty_table[0].read_q,
	&tty_table[0].write_q;
	...
};

再将 ASCII 码放到缓冲队列 read_q 中用put_queue

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

Guess you like

Origin blog.csdn.net/qq_53144843/article/details/120602914