操作系统(七)

外设的工作原理

计算机外设的工作原理,即CPU对外设的使用主要由两条主线构成:第一条主线是从CPU开始,CPU发送命令给外设,最终表现为CPU执行指令“out ax,端口号”;第二条主线是从外设开始,外设完成工作后或出现状态变化时中断通知CPU,CPU通过中断处理程序完成后续工作
在这里插入图片描述

文件视图

如果按照如图所示的方式来使用外设,程序员需要直到外设的端口、设备操控指令的详细格式和指令流程等信息。这些信息对于一个普通的应用程序员来说,时一项繁杂且无趣的任务,需查阅大量硬件手册,需进行很多繁琐的跳是。而且每种计算机外设又很可能对应不同的厂商、不同的型号等,英雌让应用程序员通过命令这季节来操作操作计算机外设的想法几乎不可行。
这样就引入了文件视图,不管是什么样的外设,操作系统都将其同一抽象成一个文件,程序员通过对文件接口open,read,write来使用这些外设。例如向“显示器文件”里写了一个字符串“hello world”,那么就会在显示器中显示“hello world”
下面的代码给出的就是文件视图下上层用户使用外设的基本结构

main()
{
	int fd = open("/dev/xxx")
	for (int i=0;i<10;i++)
	{
		write(fd,i,sizeof(int));
	}
	close(fd)
}

采用了这样的统一结构以后,在上层用户眼里,对外设的操作和对文件的操作完全一样的,上层用户可以完全忽略诸如端口号,设备指令格式等诸多细节。当然,这些细节是交给操作系统完成的,操作系统将会负责将“设备文件”的读写过程展开成对设备的具体操作,形成一些列Out语句。而完成这个展开工作就是操作系统外设管理的核心任务
在这里插入图片描述

显示器的驱动

从printf开始

printf是一个库函数,该函数会将%d,%c等内容同一处理位字符串,然后以该字符串所在的内存地址Buf和字符串长度count为参数调用系统write(1,buf,count)
write的核心实现是sys_write,所以printf的下一步就是sys_write。sys_write首先要做的就是找到这些文件的属性,即到底是腹痛文件还是设备文件,如果是设别文件,sys_write要根据设备文件中存放的设备属性信息转到相应的操作命令。
设备信息存档在描述文件本身的数据结构中,这个数据结构就是著名的文件控制块。所以sys_write的第一步工作就应该是找到要写的“文件”的FCB

int sys_write(unsigned int fd,char *buf,int count)
{
	struct file *file
	file = current->flip[fd];
	inode = file->f_inode;
	.....
}

为了找到“文件”FCB,首先要做的工作就是当前的PCB中找到打开文件的句柄标志,即fd,对于显示器而言,这个fd=1,然后根据这个fd可以找到FCB,即代码中的inode。这个inode到底对应了什么设备?里面存放了什么内容?需要从为inode赋值说起。
current->filp中存放了当前进程打开的文件,如果文件不是当前进程打开的,那么就一定是其父进程打开后再由子进程继承来的。这是UNIX类操作系统执行fork的结果,因为再fork的核心实现copy_process()中有这样的资源复制。

int copy_process()
{
	*p = *current;
	for (i=0;i<NR_OPEN;i++)
	{
		if(f=p->filp[i]))
		f->count++;
		.....
	}
}

fd=1的文件对应标准输出,因为每个进程都可能用到标准输出,所以每个进程都会打开这个文件。既然所有进程都要打开这个设备文件,操作系统初始化时的1号进程会首次达赖这个设备文件,然后其他进程继承这个文件句柄。在系统启动后 创建1号进程的地方,即在int函数中调用open来打开一个名“/dev/tty0”的文件。由于这是该进程打开的第一个文件,所以对应的文件句柄为fd=0,接下来调用了两次dup,使得fd=1,fd=2也都指向了“/dev/tty0”的FCB,如图所示
在这里插入图片描述
系统调用open的核心工作就是找到文件的FCB(存储在磁盘上),将其读入到内存里并和进程PCB关联如图所示dup的功能就是将上一个fd的内容复制到下一个fd中

文件视图中的大量分支

int sys_write(unsigned int fd,char *buf,int cnt)
{
	if(S_ISCHR(inode->i_mode))
	{
		return rw_char(WRITE,inode->z_zone[0],buf,cnt)
		.....
	}
}

首先根据inode中的信息判断该文件对应的设备受否是一个字符设备,显示器就是一个字符设备
显示器是字符设备,现在sys-write就是分支到函数rw_char(WRITE,inode->i_zone[0],buf,cnt)中去执行其中inode->i_zone[0]中存放的就是该设备的主设备号和次设备号。继续向下执行相应的字符设备处理代码

int rw_char(int rw,int dev,char *buf.int cnt)
{
	crw_ptr call_addr = crw_table[major(dev)];
	call addr(re,dev,buf,cnt)
static crw_ptr crw_table[] = {....rw_ttyx,....};
static int rw_ttyx(int rw, unsigned monor, char *buf, int count)
{
	return ((rw==READ)?tty_read(minor,buf):tty_write(minor,buf))
}
}

不难看出,rw_char中以主设备号为(MAJOR(dev))从索引从一个函数表rw_table中查找和中断设备对用对应的读写函数rw_ttyx,然后调用这个函数。
函数rw_ttyx中根据是设备读操作还是设备写操作继续分直。显示器和键盘合在一起构成了终端设备tty,显示器只写,键盘只读。此处是显示器,所以对应写操作,将调用函数tty_write(minor,buf)。根据文件的属性,即inode中的信息,经过大量分直以后,从写文件write最终到达了真正显示器的tty_write

最终到达mov ax,[pos]

经过漫长的分支以后,tty_write开始真正向显示器输出了
tty_write首先获得一个结构体tty_struct,主要目的是在这个结构体中找到队列tty_wriye_q。实际上,站在用户的角度,输出到显示器就是输出到这个队列中,最终要等到合适的时候,由操作系统同一将队列中的内容显示到显示器上,这就是著名的缓冲机制上。
缓冲是指两个速度存在差异的设备之间通过缓冲队列来弥补这种差异的一种方式,具体而言,就是高速设备将数据存到高速缓冲队列中,然后高速设备设备去做气压事,低俗设备在合适的时候从缓冲队列中取走内容进行输出,这样高速设备不必一致同步等待低俗设备,提高了系统的整体效率。在这里,高速设备就是CPU,低速设备就是显示器。缓冲机制是操作系统子啊外设管理时通常使用的基本机制

int tty_write(unsigned channel,char *buf,int nr)
{
	struct tty_struct *tty;
	tty = channel +tty_table
	sleep_if_full(&tty->write_q)
	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);
		PUTCH(c,tty->write_q);
		b++;nr--;
	}
	tty->write(tty)
}

当然在写显示队列之前,需要判断显示队列是否已满,这是典型的生产者——消费者问题。tty_write是生产者,用sleep_if_full(&tty->write_q)进行P操作,如果发现队列已满,就睡眠等待。如果tty->write_q队列没有满,就那么就从用户态逐个取出字符串c,即执行c = get_fs_byte(b)。获得c以后要仅从一些判断,如果是换行,即if(c==‘r’)则将13个放入tty->write_q中;如果设置了大小标志,则将c变成大写字母后再放入tty->write_q中等。此次printf要输出完毕或是队列tty->write_q已满时,就调用tty->write(tty)即用tty结构体中write函数指针来进行真是的显示器输出。
现在Printf要输出的字符串已经被放到显示器缓冲队列中了,那么接下来调用tty->write(tty)就应该从这个缓冲队列中取出字符真正显示到显示器上。从对tty结构体初始化可以看出,tty_write调用的函数con_write

struct tty_struct tty_table[]={{con_write,{0,0,0,0," "},{0,0,0,0," "}},{},...}
void con_write(struct tty_struct *tty)
{
	GETCH(tty->write_q,c)
	if(c>31 && c < 127)
	{
		__asm__("movb,attr,%%ah"
		"movw %%ax, %1" ::" a"(c),"m"(*(short*)pos):"ax")
		pos+=2;
	}
}

con_write核心代码是一段嵌入汇编代码,具体完成工作是

	mov c,al
	mov attr,ah
	mov ax,[pos]

即将要输出的字符放在寄存器AX的第8位,将显示属性attr放在AX的高8位,然后将AX输出到pos处,显示器输出工作最终真正到达了“out"指令,即令整个文件视图的最后一个环节。
为了故事的完整性,我们需要知道pos到底是什么。con_write中每输出一个AX都让pos加2,这是必然的,因为AX就是两个字节。所以理解pos的关键在于pos的初值,再小护士话函数con_init中,调用gotoxy将pos初始化为origin+[0x90001]*video_size_row+([0x9000]<<1)。在系统启动setup.s时,利用BIOS终端将当前光标的行和列位置量取出来放到了0x90000和0x90001处。而oringin时显存在内存中的初始位置。因此初始化以后Pos就是开机以后当前光标的显存位置

键盘的驱动

从键盘终端开始

硬件手册告诉我们,按下键盘会产生0x21号中断,所以整个故事要从设置0x21号中断的中断处理函数开始

void con_init(void){set_trap_gate(0x21,&keyboard_interrupt);}
keyboard_interrupt:
	int$0x61,%al
	call key_table(,%eax,4)
	...
	push $0
	call do_tty_interrupt

CPU向设备out写的最终命令是out,那么读设备的第一步就应该是“in”,即从设备上取出内容交给CPU。键盘中断的第一条语句,就是哦那个家农安0x60端口获得键盘扫描码,然后根据这个扫描码调用不同的处理函数来处理各种按键,即call key_table(,%eax,4)

key_table:
	.long none,do_self,do_self,do_self
	.long do self,...,func.scroll.cursor

扫描码02对应键盘左上角的数字键1,01对应Esc键,12对应字母键E等。可以看出,绝大多数按键(如字母,数字键)都用do_self函数处理
函数d0_self要做的第一件事就是从键盘的ASCII码表(key_map)中以当前按键扫描码(存在EAX中)为索引找到当前按键的ASCII码。do_self要做的第二件事就是找到tty结构体中的read_q队列。键盘和显示器使用了同一个tty结构体tty_table[0],只是键盘使用的是读队列,而显示器使用的是写队列。由于键盘和显示器合起来构成一套中断设备,所以分别使用tty_table[0]的read_1和write_q也不奇怪。得到ASCII码并找到ASCII码对应的缓冲序列read_q以后,接下来就是第三件事了,即将ASCII码放到换冲队列read_q中

do_self:
	lea key_map,%ebx
	movb (%ebx, %eax),%al
	mov1 table_list, %edx
	mov1 head(%edx),%ecx
	wovb %al  buf(%edx,%ecx)
key_map: .byte 0,27 .ascii "1234567890-="...
struct tty_queue *table_list[]={
&tty_table[0].read_q
&tty_table[0].write_q
}

从缓冲队列最终到scanf

现在已经将按键ASCII码放到缓冲队列tty_read_q中了。接下来应该再一次回到哪个文件视图
键盘中断处理程序keyboard_interrupt在调用do_self将按键的ASCII码放到read_1队列后,要调用do_tty_interrupt返回文件视图。这和显示器输出中在将字符放到wriye_q队列以后再调用tty_write(tty)的想法完全一样。函数do_tty_interrupt(int tty)要调用copy_to_cooked(tty_table[0]),来处理键盘ASCII码所存放的哪个队列,即tty_read_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(&tty->secondary.proc_list)
}

copy_to_cooked具体要做的工作就是从read_q队列中取出字符,将该自此放在tty->secondary队列中,同时唤醒等待这个队列上的进程。
实际上这个构成了从设备中断出发又回到文件视图的一个基本结构:京城通过文件读接口“read”发起一个设备读动作,接下来该金恒会再文件视图路线上发生阻塞,因为这个时候设备还没有将读的东西准备好。设备开始工作,工作完成以后会中断CPU,操作系统再设备中断处理时,会将设备上的内容放入缓存区中,并唤醒阻塞等待的一个进程,醒来的哪个进程从缓冲区取出内容并仅从处理。文件操作时由用户发起的。即用户系统的中断处理函数负责处理,两条线之间通过上述同步机制联系到一起。
现在继续分析从设备出发的那条线,对于键盘,用户发现的文件操作时scanf,而sancf时sys_read(0,buf,count),其中fd=0表示标准输入。找到设备FCBu哦精辟发现这个设备文件任然是/dev/tty0,根据printf的文件视图路线不难类推,通过一系列分支后最后执行tty_read。而tty_read的核心就是要和键盘中断处理程序中wake)up接上,所以这里面非常重要的语句:sleep_if_empty(&tty->secondary)

int tty-read(unsigned channel,char * buf,int nr)
{
	sleep_if_empty(&tty->secondary)
	do{
		GETCH(tty->secondary,c)
		tty->secondary.data--;
		put_fs_byte(c,b++)
	}while(nr>0&& !EMPTY(tty->secondary));
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值