zoukankan      html  css  js  c++  java
  • 从printXX看tty设备(2)VGA显示模拟

    一、虚拟终端模拟的问题

    前面曾经说过,所谓控制台是对tty设备的一种模拟。tty和主机之间就一根线,所有的交互都在这条串行线上一个bit一个bit的交互,可以看做是“竹筒倒豆子”--直来直去的模式。进一步说,主机不能(也没有义务)直接控制tty设备上的显示设备(比如显示设备对应的内存、显示控制寄存器等坐落于终端上等组件),虽然主机可以控制自己一端tty设备的数据发送和接收。

    现在使用显卡来模拟一个终端,此时为了兼容之前的功能,当我们模拟一个终端的时候,主机要在自己的本地显示器上显示出指定的效果,比如说高亮一些字符,移动光标,滚屏的操作。一个用户态的shell使用的还是终端的协议,就是向一个串口中发送bit流。但是对于主机上的显卡来说,它并不是一个真正的终端命令解释器,甚至可以看到,在VGA显卡中,如果要在屏幕上高亮一个字符,是需要设置这个字符的属性byte。这里的编程模式和tty设备的模式有截然不同的接口和实现,用户需要且只需要将这个属性byte和ascii值写入内存中的指定区域,从而由显示器来自动的显示出来,这个内存区就是PC中著名的“BIOS空洞”。对应于qemu模拟的设备,其地址从0xb8000开始,到0xc0000结束,这么长的地址作为显卡内存。当我们需要显示器显示某个ASCII字符的时候,就向这片内存中写入该字符对应的ASCII码的值,显卡会根据自己ROM中的字模将这个字符显示到显示器上。

    总之,当使用显卡模拟终端的时候,需要内核将终端协议转换为显示器内存操作指令,从而相当于将终端的解析和显示功能放在了自己的显卡上来完成。

    另外一个问题就是输入的问题,当使用真正的终端的时候,用户的输入来自串口,使用PC模拟终端的时候,此时系统一般只有一个键盘,此时键盘消息需要发送给串口的读入者,从而实现和使用者的交互,这个内核同样需要考虑。

    二、显示系统的初始化

    linux-2.6.21archi386kernelsetup.c:setup_arch()#ifdef CONFIG_VT
    #if defined(CONFIG_VGA_CONSOLE)
     if (!efi_enabled || (efi_mem_type(0xa0000) != EFI_CONVENTIONAL_MEMORY))
      conswitchp = &vga_con;
    #elif defined(CONFIG_DUMMY_CONSOLE)
     conswitchp = &dummy_con;
    #endif
    #endif

    此处初始化了一个全局变量,也就是conswitchp指针,这个指针就是指向了控制台实现(内核成为控制台切换 console switch,因为系统的控制台可以在运行时变化)。当该变量初始化之后,在con_init函数中将会调用者这里注册的指针中对应的start_up实现:

     if (conswitchp)
      display_desc = conswitchp->con_startup();

    反过来看上面注册的vga_con中con_startup指针指向的为

    static const char *vgacon_startup(void),对于qemu的运行中,此处走的流程为

     } else {
      /* If not, it is color. */
      vga_can_do_color = 1;
      vga_vram_base = 0xb8000;
      vga_video_port_reg = VGA_CRT_IC;
      vga_video_port_val = VGA_CRT_DC;
      if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {
       int i;

       vga_vram_size = 0x8000;这两个大小很重要,将会在这个文件中共享,该文件中很多函数会引用这文件静态变量。

    这里设置了两个重要的全局变量,一个是vga内存的起始物理地址,一个是这个显卡区的大小,分别为0xb8000和0x8000,刚好到0xc0000结束。由于这里所说的地址都是物理地址,而内核明显都是使用逻辑地址的,所以同样要把这个物理地址转换为逻辑地址,所以在该函数中有一个转换操作

     vga_vram_base = VGA_MAP_MEM(vga_vram_base, vga_vram_size);这个转换起始比较简单,直接是物理地址加上0xc0000000.
     vga_vram_end = vga_vram_base + vga_vram_size;

    例如,在初始化vc结构中显卡地址的时候,将会执行下面的代码

    static int vgacon_set_origin(struct vc_data *c)
    {
     if (vga_is_gfx || /* We don't play origin tricks in graphic modes */
         (console_blanked && !vga_palette_blanked)) /* Nor we write to blanked screens */
      return 0;
     c->vc_origin = c->vc_visible_origin = vga_vram_base;
     vga_set_mem_top(c);
     vga_rolled_over = 0;
     return 1;
    }

    三、串口协议模拟

    当我们通过printf向标准输出中打印一个字符串的时候,经过的调用连为

    #0  do_con_trol (tty=0x296, vc=0xcf986054, c=658) at drivers/char/vt.c:1546
    #1  0xc03ea1dd in do_con_write (tty=0xcf986000, 
        buf=0xcfe15df1 "234", <incomplete sequence 317>, count=0) at drivers/char/vt.c:2135
    #2  0xc03eab1c in con_put_char (tty=0xcf986000, ch=13 ' ') at drivers/char/vt.c:2449
    #3  0xc03d50de in opost (c=10 ' ', tty=0xcf986000) at drivers/char/n_tty.c:277
    #4  0xc03d8e75 in write_chan (tty=0xcf986000, file=0xcfea7a80, 
        buf=0xcf9c0800 " Please press Enter to activate this console. ", nr=46)
        at drivers/char/n_tty.c:1468
    #5  0xc03d0485 in do_tty_write (count=46, 
        buf=0x81bd854 " Please press Enter to activate this console. ", file=0xcfea7a80, 
        tty=0xcf986000, write=0xc03d8bec <write_chan>) at drivers/char/tty_io.c:1746
    #6  tty_write (count=46, buf=0x81bd854 " Please press Enter to activate this console. ", 
        file=0xcfea7a80, tty=0xcf986000, write=0xc03d8bec <write_chan>)
        at drivers/char/tty_io.c:1806
    #7  0xc01bf5cd in vfs_write (file=0xcfea7a80, 
        buf=0x81bd854 " Please press Enter to activate this console. ", count=46, pos=0xcfe15f84)
        at fs/read_write.c:330
    #8  0xc01bf7d1 in sys_write (fd=1, 
        buf=0x81bd854 " Please press Enter to activate this console. ", count=46)
        at fs/read_write.c:383

    模拟一下对于 e[34;41m这个序列的内核解析过程:

    do_con_trol中

    case 27:
      vc->vc_state = ESesc; 这只这个控制台当前状态为ESesc。
      return;
    ……

    switch(vc->vc_state) {
     case ESesc:
      vc->vc_state = ESnormal;
      switch (c) {
      case '[':
       vc->vc_state = ESsquare;
       return;

    ……

    case ESsquare:
      for (vc->vc_npar = 0; vc->vc_npar < NPAR; vc->vc_npar++)
       vc->vc_par[vc->vc_npar] = 0;
      vc->vc_npar = 0;
      vc->vc_state = ESgetpars;
      if (c == '[') { /* Function key */
       vc->vc_state=ESfunckey;
       return;
      }
      vc->vc_ques = (c == '?');
      if (vc->vc_ques)
       return;注意:这里并没有返回,根据case的规则,没有break将会继续执行,所以将会执行到接下来的ESgetpars序列
     case ESgetpars:
      if (c == ';' && vc->vc_npar < NPAR - 1) {这里通过分号来区分不同的参数
       vc->vc_npar++;
       return;
      } else if (c>='0' && c<='9') {
       vc->vc_par[vc->vc_npar] *= 10;
       vc->vc_par[vc->vc_npar] += c - '0';均为十进制数,不识别十六进制数。
       return;
      } else
       vc->vc_state = ESgotpars; 这里同样没有返回,继续执行接下来的ESgotpars分支。

    case ESgotpars:
      vc->vc_state = ESnormal;
      switch(c) {

    ……

    case 'm':  
       if (vc->vc_ques) {注意:这里我们来说,这个条件并不满足,这个vc_ques是在前面遇到'?'的时候设置的,由于没有这个字符,所以这里是不满足的,不会走这个分支
        clear_selection();
        if (vc->vc_par[0])
         vc->vc_complement_mask = vc->vc_par[0] << 8 | vc->vc_par[1];这里对应的是查询标志
        else
         vc->vc_complement_mask = vc->vc_s_complement_mask;
        return;
       }
       break;这个break将会跳转到下面的位置

    ……

    if (vc->vc_ques) {
       vc->vc_ques = 0;
       return;
      }
      switch(c) {

    ……

    case 'm':
       csi_m(vc);
       return;

    在sci_m中

    default:
        if (vc->vc_par[i] >= 30 && vc->vc_par[i] <= 37)可以看到,30--37作为前台颜色,
         vc->vc_color = color_table[vc->vc_par[i] - 30]
          | (vc->vc_color & 0xf0);
        else if (vc->vc_par[i] >= 40 && vc->vc_par[i] <= 47)40--47作为后台背景颜色,然后设置到字面的属性中。
         vc->vc_color = (color_table[vc->vc_par[i] - 40] << 4)
          | (vc->vc_color & 0x0f);
        break;
      }
     update_attr(vc);设置入属性成员中。

    static void update_attr(struct vc_data *vc)
    {
     vc->vc_attr = build_attr(vc, vc->vc_color, vc->vc_intensity, vc->vc_blink, vc->vc_underline, vc->vc_reverse ^ vc->vc_decscnm);
     vc->vc_video_erase_char = (build_attr(vc, vc->vc_color, 1, vc->vc_blink, 0, vc->vc_decscnm) << 8) | ' ';
    }

    当显示一个字符的时候,在static int do_con_write(struct tty_struct *tty, const unsigned char *buf, int count)中将会向制定位置显示字符,这里的写入操作是通过scr_writew来实现的,从这里可以看到,其中有对vc->vc_attr的使用。从这里我们可以看到的是,对于显存,每个字符本身占用一个字节的ASCII码,然后紧邻的一个byte是这个字符的属性标志。

       scr_writew(himask ?
             ((vc->vc_attr << 8) & ~himask) + ((tc & 0x100) ? himask : 0) + (tc & 0xff) :
             (vc->vc_attr << 8) + tc,
           (u16 *) vc->vc_pos);

    最后看一下这个scr_writew的实现

    #define scr_writew(val, addr) (*(addr) = (val))


    由于前面调用的时候强制转换为了 u16*类型,所以这里的复制是一个short类型的赋值。结合前面的显卡初始化方法就可以知道,当前的VGA显卡显示的时候是将真正希望显示的ASCII码和对应的属性直接写入内存来显示的。

    四、显卡编程的一个基础

    看来intel是比较喜欢这样的一个硬件编程模型:使用两个寄存器,一个是地址寄存器,专门用来写地址,或者说用来作为寄存器选择寄存器,然后另一个地址作为数据寄存器。编程时首先向地址选择寄存器中写入将要操作的寄存器,然后从另一个数据寄存器中读出这个值。这一点在intel的IOAPIC和PCI系列中均有体现。大家可以理解为C中的指针就好了,虽然这里有点绕。

    在VGA中,这两个寄存器分别为

    /* VGA index register ports */
    #define VGA_CRT_IC   0x3D4 /* CRT Controller Index - color emulation */

    /* VGA data register ports */
    #define VGA_CRT_DC   0x3D5 /* CRT Controller Data Register - color emulation */
    例如

    static inline void vga_set_mem_top(struct vc_data *c)
    {
     write_vga(12, (c->vc_visible_origin - vga_vram_base) / 2);
    }

    这里还没有涉及VT的另一个重要部分,就是和显示对应的就是输入,也就是PC的键盘处理,对应的就是tty的read接口,在接下来一篇中讨论。

  • 相关阅读:
    线程与进程
    Java集合框架体系JCF
    Java异常
    抽象,接口和Object类
    Java三大特性
    面向对象
    数组
    Java 控制结构与方法
    数据类型与变量
    Java基础之入门
  • 原文地址:https://www.cnblogs.com/tsecer/p/10485876.html
Copyright © 2011-2022 走看看