• 操作系统I/O与显示器---16



    继续那台“计算机”

    在这里插入图片描述
    在学完了进程和内存管理之后,下面进入到磁盘驱动和相关IO设备驱动的章节。


    让外设工作起来

    计算机如何让外设工作起来呢?

    每个外设,例如: 显示器有对应的显卡,显卡里面有相关的寄存器,通过往这些寄存器中设置对应的值,就可以控制该外设工作起来了。

    在这里插入图片描述
    CPU通过向外部设备对应的控制器某个寄存器中写入命令,就可以操作对应外部设备的工作了,例如: 常用的out和in指令,来控制往某个外设寄存器中写入或者读取数据。

    汇编语言中断及外部设备操作篇–06

    而当外部设备处理完后,通过中断方式通知CPU进行处理。


    向设备控制器的寄存器写不就可以了吗?

    操作系统操作外设,首先需要查寄存器地址、还需要查找对应的硬件手册,来了解相关操作命令的格式和语义。

    硬件设备种类繁杂,如果直接让用户面向这些外设控制器中的寄存器来进行操作,那么显然过于麻烦,因此操作系统要给用户提供一个简单 视图—文件视图,这样方便。

    通过文件视图,就相当于提供了一个统一操作外设的接口,例如: 向显示器输出字符,都通过print这个接口即可,不需要管后面显示器的类型是什么。

    总的来说,操作外设就是下面这三个步骤:

    • 提供统一操作某个外设的接口
    • 用户调用该接口,接口最终通过out指令,将操作命令发送到对应的外设寄存器中
    • 外设通过中断,通知操作系统任务处理完毕

    一段操纵外设的程序

    //去打开对应的设备文件---通过统一的open接口
    int fd = open(/dev/xxx”);
    for (int i = 0; i < 10; i++) {
    //向外设写数据----通过统一的write接口
    write(fd,i,sizeof(int));
    }
    //关闭外设----通过统一的close接口
    close(fd);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (1) 不论什么设备都是open, read, write, close

    • 操作系统为用户提供统一的接口

    (2) 不同的设备对应不同的设备文件(/dev/xxx)

    • 根据设备文件找到控制器的地址、内容格式等等!

    一个统一的视图-文件视图

    在这里插入图片描述
    操作系统提供给用户操作硬件的系统接口都是固定的,但是在操作具体硬件时,需要传入当前操作的文件描述符,然后操作系统根据文件描述符来决定当前操作的具体是哪个硬件,然后进行对应的处理。


    概念有了,开始给显示器输出…

    从哪里开始这个故事呢?

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

    printf库展开的部分我们已经知道:先创建缓存buf将格式化输出都写到那里, 然后再write(1,buf,…)

    在linux/fs/read_write.c中
    //fd是找到file的索引--向屏幕输出时,传入的fd=1,此时1表示的是标准输出的文件描述符号
    int sys_write(unsigned int fd, char *buf, int count){ 
    struct file* file;
    //current就是当前进程,进程是带动整个系统的视图
    //通过传入的文件描述符,找到对应的文件
    file = current->filp[fd];
    //获得文件对应的节点--该节点内封装了当前文件的信息
    inode = file->f_inode;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    file的目的是得到inode,显示器信息应该就在这里


    fd=1的filp从哪里来?

    • 因为是被current指向,所以是从fork中来
    int copy_process(...){
    *p = *current;
    for (i=0; i<NR_OPEN;i++)
    if ((f=p->filp[i])) f->f_count++;
    
    • 1
    • 2
    • 3
    • 4

    显然filp文件打开指针都是从父进程拷贝来的,那么是谁一开始打开的?

    • shell进程启动了whoami命令,shell是其父进程

    shell 0号进程的初始化过程中对相关文件指针进行从初始化:

    void main(void)
    { if(!fork()){ init(); }
    
    • 1
    • 2
    void init(void){
    //系统初始化的时候打开了一个文件,并且拷贝了两份
    //所以filp下标1对应的打开文件指针也是dev/tty0,即终端设备文件
    open(“dev/tty0”,O_RDWR,0);dup(0);dup(0);
    execve("/bin/sh",argv,envp)}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    open系统调用完成了什么?

    open系统调用,最终会调用到sys_open。

    sys_open的作用如下:

    • 解析目录,找到inode
    在linux/fs/open.c中
    int sys_open(const char* filename, int flag,int mode){ 
    struct file* f;
    int i,fd;
    //调用函数open namei执行打开操作,若返回值小于0,则说明出错,于是释放
    //刚申请到的文件结构,返回出错码i。若文件打开操作成功,则inode是已打开文件的i节点指针。
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 核心就是建立这样一个链

    在这里插入图片描述
    open(“dev/tty0”,O_RDWR,0)最终目的就是将dev/tty0设备文件对应的inode读入到内存中来,当需要操作该文件设备时,首先就需要获得当前设备对应的inode信息。


    准备好了,真正向屏幕输出!

    • 继续sys_write!
    在linux/fs/read_write.c中
    int sys_write(unsigned int fd, char *buf,int cnt){ 
    //拿到当前要操作文件的inode后
    inode = file->f_inode;
    //判断对应的文件设备是否是字符设备---通过i_mode进行区分
    ///dev/tty0的inode中的 信息是字符设备
    if(S_ISCHR(inode->i_mode))
    //往该设备上写入字符,第二个参数是设备号,以及对应要写入的数据位于内存中那个buf缓冲区中
    return rw_char(WRITE,inode->i_zone[0], buf, 
    cnt); ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    • 转到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)]; 
    //通过函数指针调用具体找到的函数
    call_addr(rw, dev, buf, cnt); ...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    看看crw_table!

    //设备号为4,去查询对应的函数表下标为4的地址,得到rw_ttyx函数指针
    static crw_ptr crw_table[]={...,rw_ttyx,};
    typedef (*crw_ptr)(int rw, unsigned minor, char *buf, int count)
    
    • 1
    • 2
    • 3

    可以猜测到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));}
    
    • 1
    • 2
    • 3

    再转到tty_write! //实现输出的核心函数

    在linux/kernel/tty_io.c中
    int tty_write(unsigned channel,char *buf,int nr){ 
    //tty_struct可以猜测到就是对设备抽象出来的一个结构体对象
    struct tty_struct *tty;
    //tty_table可以猜测到是设备列表,通过当前索引,查到当前设备对应的tty对象
    tty=channel+tty_table;
    //可以猜测:输出就是 放入队列!
    sleep_if_full(&tty->write_q);
    ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    由于内存的速度和其他外部设备,例如: 显示器相比,差距很大,所以。

    一般在内存与外设之间进行数据传输时,都需要一个共享缓冲区域,即我们需要先将数据写入到内存的一块缓冲区内,然后外设慢慢去读取数据。

    这里是CPU将数据写入到当前设备关联的输出队列中去,然后外设去读取输出队列中的数据,因此CPU将数据写入到这个输出队列前,需要先判断该队列是否已经满了,如果队列已经塞满了,就阻塞等待。

    sleep_if_full(&tty->write_q);
    
    • 1

    sleep_if_full判断当前写入队列是否已经满了,如果已经满了,当前进程就需要进入阻塞等待。

    这边涉及到共享内存,因此就需要之前讲到的信号量对共享内存进行保护


    继续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)) { 
     //从用户共享缓冲区中读取数据
     //fs:从用户缓存区读!
      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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    tty->write应该就是真的开始输出屏幕了!


    看看tty->write

    先来看看tty_struct结构体的模样:

    在include/linux/tty.h中
    struct tty_struct{ void (*write)(struct tty_struct 
    *tty); struct tty_queue read_q, write_q; }
    
    • 1
    • 2
    • 3

    所有设备都会被抽象为一个tty对象,并且该tty对象内部给出的接口也都是固定的,常见的write和read接口等,不同的设备对应不同的tty对象,他们需要给出不同的接口具体方法实现,然后将这些tty对象加入一个tty_table表中。

    • 需要看tty_struct结构的初始化!
    struct tty_struct tty_table[] = {
    {con_write,{0,0,0,0,””},{0,0,0,0,””}},{},};
    
    • 1
    • 2

    如果我们需要操作屏幕输出,那么首先需要查表得到屏幕输出对应的tty对象,这里是con_write.

    外部设备也分为输入和输出设备,这里屏幕属于输出设备,而常见的键盘和鼠标等,属于输入设备,下一节会讲

    • 到了con_write,真正写显示器!
    在linux/kernel/chr_drv/console.c中
    void con_write(struct tty_struct *tty){ 
    //从队列中取出字符,然后赋值给C
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    有的外设控制器地址可以和内存统一编址,这时寻址就使用mov,而如果采用独立编址,寻址时则采用out和in

    显存特别大,因此通常都是和内存统一编址,因此访问显存时,使用mov指令。


    只有一句话:mov pos

    在这里插入图片描述
    在系统启动的时候,会根据BIOS中断,取出硬件参数,其中包括光标的位置,然后将光标的位置放置在90000处。
    光标的位置就是显存的位置。


    pos的修改

    在这里插入图片描述


    printf的整个过程!

    在这里插入图片描述

  • 相关阅读:
    grid实现“品”字布局
    python matplot画图攻略
    记一次Golang中一次内存泄漏的问题排查
    金仓数据库 KingbaseES 插件参考手册 S (2)
    运维常用概念
    ubuntu忘记mysql密码,怎么办
    【linux kernel】linux的platform设备驱动框架分析
    任务及任务切换
    光伏组件机器视觉新突破!维视智造上线汇流带引线焊接检测新方案 “误检率”低至0.01%
    在线智慧礼佛供品程序开发
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126129550