从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.21\arch\i386\kernel\setup.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 "\b\234", <incomplete sequence \317>, count=0) at drivers/char/vt.c:2135
#2 0xc03eab1c in con_put_char (tty=0xcf986000, ch=13 '\r') at drivers/char/vt.c:2449
#3 0xc03d50de in opost (c=10 '\n', tty=0xcf986000) at drivers/char/n_tty.c:277
#4 0xc03d8e75 in write_chan (tty=0xcf986000, file=0xcfea7a80,
buf=0xcf9c0800 "\nPlease 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 "\nPlease 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 "\nPlease 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 "\nPlease 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 "\nPlease 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接口,在接下来一篇中讨论。