让外设工作起来
CPU是如何让外设工作起来的?
实际上每一个外设如显示器,键盘等都会各自的寄存器,而CPU就向这些寄存器发送指令,然后切换到其他进程执行,等外设计算完成之后,向CPU发起中断,CPU再进行中断处理,这样CPU就让外设工作起来了。
看起来,CPU要让外设工作起来其实很简单,就是1.向外设寄存器写内容,2.然后进行中断处理,但是为什么就是这样一条简单的指令,却需要那么多的代码?
其实,CPU向外设寄存器写内容并不是一项简单的工作,CPU需要查寄存器地址,内容的格式和语义,而不同公司生产的显示器等外设设备这些一般是不一样的,因此CPU向外设寄存器写内容就变得非常难了,所有操作系统要给用户提供一个简单的视图–文件视图
文件视图
一段操纵外设的程序
int fd = open("/dev/xxx");
for(int i=0; i<10; i++)
write(fd,i,sizeof(int));
close(fd);
- 无论上面设备都是open,read,write,close,
操作系统为用户提供统一的接口
- 不同的设备对应不同的设备文件("/dev/xxx"),
根据设备文件找到控制器的地址,内容格式等等
printf是如何输出到显示器的
输出一段字符
printf("Host Name: %s", name);
从文件视图那里我们知道,实际上printf就是从系统调用open,write等系统调用开始,然后write再根据文件系统找到控制器的地址等内容,现在问题就是write如何找到对应的控制器?
实际上write还会进行分支,分支到不同的控制器,让我们看一下write的代码,我们就知道write是如何找到对应的控制器了
//在linux/fs/read_write.c中
int sys_write(unsigned int fd, char *buffer, int count)
//fd根据前面文件系统我们大概知道是什么东西了吧,实际上就是找到file的索引
{
struct file *file;
file = current->filp[fd];
inode = file->f_inode; //inode取出找出对应控制器的需要的信息
}
current->filp中的filp是什么?
实际上,子进程都是从父进程拷贝过来的,我们只需要找到最开始的进程就知道filp是什么了;
void main(void)
{
if(!fork())
{
init();
}
}
void init(void)
{
open("dev/tty0",O_RDWR,0);
dup(0), dup(0); //拷贝,对应的分别为1,2
execve("/bin/sh",argv,envp);
}
所以file = current->filp[fd]
就是"dev/tty0",tty就是终端设备
open系统调用完成了什么?
在linux/fs/open.c中
int sys_open(const char* filename, int flag)
{
i=open_namei(filename, flag, &inode);
current->filp[fd] = f;
f->f_mode = inode->i_mode;
f->f_inode = inode;
f->f_count = 1;
return fd;
}
上面的代码实际上就是根据传入进来的文件名找到对应的终端设备信息,然后在PCB中的filp保存对应终端设备的信息即inode;
接下来开始真正向屏幕输出
前面说过,write实际上有很多分支,这些分支指向不同的终端设备,让我们看一下这些分支是怎么实现的
继续sys_write!
//在linux/fs/read_write.c中
int sys_write(unsigned int fd, char *buf, int cnt)
{
inode = file->f_inode;
if(S_ISCHAR(inode->i_node)) //第一个分支,判断是否是字符设备
return rw_char(WRITE, inode->i_zone[0], buf, cnt);
// /dev/tty0的inode中的信息是字符设备
...
}
转到rw_char!
//在linux/fs/char_dev.c中
int rw_char(int rw, int dev, char *buf, int cnt)
{
crw_ptr call_addr=crw_table[MAJOR(dev)];
//crw就是一个函数指针表,根据你是第几个设备找到对应的处理函数
call_addr(rw,dev,buf,cnt);
...
}
我们看一下crw表
static crw_ptr crw_table[]={...,rw_ttyx,}; //rw_ttyx就是我们要的函数
这里又是一个分支
继续rw_ttyx函数
static int rw_ttyx(int rw, unsigned minor, char *buf, int count)
{
return((rw==READ)? tty_read(minor,buf):
tty_write(minor,buf));
}
显然,根据前面传递下来的write,这里要的写,因此继续分支tty_write
tty_write
int tty_write(unsigned channel, char *buf, int nr)
{
struct tty_struct *tty;
tty = channel + tty_table; //在tty_table中取出一个tty
sleep_if_full(&tty->write_q);
//实际上,就是把输出放在这个队列中,如果队列满了,进程睡眠
//这就是共享缓冲区,消费者与生产者模型
}
继续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); //从buf里边取出一个字符
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); //这个函数实现屏幕输出
}
先看一下tty的结构
struct tty_struct{
void(*write)
{
struct tty_struct *tty;
}
struct tty_queue read_q, write_q;
}
tty_struct结构初始化
struct tty_struct tty_table[]={
{con_write,{0,0,0,0,""},{0,0,0,0,""},{},...};
}
//console即控制台,con_write就是真正的输出函数了
con_write
在linux/kernel/chr_drv/console.c中
void con_write(struct tty_struct *tty)
{
GETCH(tty->write_q,c); //从队列中取出一个字符,将字符out到显示器上
if(c>31 && c<127)
{
__asm__("movb _attr, &&ah\n\t" //将attr(属性)赋给ah
"movw %%ax, %1\n\t"::"a"(c),
//将c赋给al,将ax赋给1(即pos),pos就是显卡的寄存器
"m"(*(short*)pos:"ax"));
}
pos+=2;
}
mov pos,c
pos指向显存:pos=0xA0000
con_init();
void con_init(void)
{
gotoxy(ORIG_X, ORIG_Y);
}
static inline void gotoxy()
{
pos = origin+y*video_size_row + x<<1;
}
#define ORIG_X(*(unsigned char*)0x90000); //初始光标列号
#define ORIG_Y(*(unsigned char*)0x90001); //初始光标行号
总结
- 系统调用函数在dev目录下找到终端设备的信息inode;
- 根据inode信息分支查找对应的tty
- 将out|mov指令写入终端设备的寄存器上