linux0.11内核源码修仙传第八章——控制台初始化

发布于:2025-03-22 ⋅ 阅读:(13) ⋅ 点赞:(0)

🚀 前言

    截止目前为止,我们的键盘输入还是没有完成,有人说,博主博主,这不是中断的内容吗?是的,但是在中断初始化部分其实并没有实现键盘输入。这部分的实现交给了控制台输出化,通过这一章节的初始化后,我们就可以和操作系统进行交互了。本文对应书中第16回。希望各位给个三连,拜托啦,这对我真的很重要!!!

🏆 开启键盘输入中断

    首先单独解决一个问题,就是键盘输入的问题,对应的函数在main函数里面的控制台初始化tty_init()函数里面的con_init()函数内。详细代码如下:

void main(void)
{
	···
	tty_init();
	···
}

void tty_init(void)
{
	rs_init();
	con_init();
}

void con_init(void)
{
	···
	set_trap_gate(0x21,&keyboard_interrupt);
	···
}

    可以看到调用的依旧是中断那一节所提到的set_trap_gate函数,关于这个函数以及中断的详细知识可以查看我之前的博客:linux0.11内核源码修仙传第六章——中断初始化。这里简单提一下,这个函数的作用就是设置了键盘中断函数,中断号是0x21,对应的中断处理函数为keyboard_interrupt。在设置完成之后,main函数在进入死循环前最后会使用sti()函数开启中断,这样键盘就会开始生效,可以和操作系统进行交互啦!~

🏆 字符显示

    光是开启键盘与操作系统的交互还不行,我们还需要将我们输入的内容输出到显示屏上供我们查看。那么就会面临一个问题, 如何将键盘输入的字符显示到屏幕上呢

📃如何显示一个字符

    我们首先立下一个共识,键盘输入的字符都是进入内存的,而屏幕上的显示肯定也是到内存中取数据的。以字符’a’为例,假设我们可以随意操作内存,如何将这个字符显示出来?

    内存中有一块专门的区域是和显存映射的 ,这块区域写入什么,就相当于写在显存上,而往显存写数据,就相当于在屏幕上输出文本了。其在内存的位置如下:

在这里插入图片描述

    假设写一行汇编语句,是往0xB8000处写入字符'a'

mov [0xB8000],  'a'

    那么就会往内存0xB8000处写入一个字符a,只要写入,屏幕上就会显示a

    那么像是如果想要error那种红色,或者警告那种黄色呢?实际上这片内存每两个字节表示一个显示在屏幕上的字符,第一个字符是编码,即要显示的内容;第二个字符代表颜色。如果要显示hello,代码应该如下所示

mov [0xB8000], 'h'
mov [0xB8002], 'e'
mov [0xB8004], 'l'
mov [0xB8006], 'l'
mov [0xB8008], 'o'

在这里插入图片描述

📃显示模式

    详细的代码处理依旧是在con_init()这个超长的函数里,首先把这个函数的框架梳理出来:

#define ORIG_VIDEO_MODE		((*(unsigned short *)0x90006) & 0xff)

void con_init(void)
{
	···
	if (ORIG_VIDEO_MODE == 7)			/* Is this a monochrome display? */
	{
		···
		if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {···}
		else {...}
		···
	}
	else 								/* If not, it is color. */
	{
		···
		if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {···}
		else {...}
		···
	}
	···
}

    这里面其实就是找出当前的显示模式,即只需要看一个即可,这里可以选择的是两个,一个是单色模式(monochrome display),一个是色彩模式(color)。模式取决于ORIG_VIDEO_EGA_BX 变量,而这个变量依旧是在临时变量区,内存的0x90006处,此处再把那张表拿出来,详情可见:linux0.11内核源码修仙传第二章——setup.s

在这里插入图片描述

    在知道前面这里是选择显示模式后,将con_init函数缩写如下:

void con_init(void)
{
	register unsigned char a;
	char *display_desc = "????";
	char *display_ptr;
	// 获取显示模式相关参数
	video_num_columns = (((*(unsigned short *)0x90006) & 0xff00) >> 8);
	video_size_row = video_num_columns * 2;
	video_num_lines = 25;
	video_page = (*(unsigned short *)0x90004);
	video_erase_char = 0x0720;
	
	// 显存映射的内存区域
	video_mem_start = 0xb8000;
	video_port_reg = 0x3b4;
	video_port_val = 0x3b5;
	video_mem_end = 0xba000;
	
	// 滚动屏幕操作时的信息
	origin	= video_mem_start;
	scr_end	= video_mem_start + video_num_lines * video_size_row;
	top	= 0;
	bottom	= video_num_lines;
	
	// 定位光标并开启键盘中断
	gotoxy(ORIG_X,ORIG_Y);
	set_trap_gate(0x21,&keyboard_interrupt);
	outb_p(inb_p(0x21)&0xfd,0x21);
	a=inb_p(0x61);
	outb_p(a|0x80,0x61);
	outb(a,0x61);
}

    可以看到这个函数其实就做了四件事:

  • 获取显示模式相关信息 :获取 0x90006 地址处的数据,就是获取显示模式等相关信息(见上表)。
  • 显存映射的内存区域 :显存映射的内存地址范围,由于假设是CGA 类型的文本模式,所以映射的内存是从 0xB8000 到 0xBA000。
  • 滚动屏幕操作时的信息 :设置一些滚动屏幕时需要的参数,定义顶行和底行是哪里,这里顶行就是第一行,底行就是最后一行。
  • 定位光标并开启键盘中断 :把光标定位到之前保存的光标位置处(取内存地址 0x90000 处的数据),然后设置并开启键盘中断。

📃光标

    众所周知,在控制台,除了输入,还有光标也是重要的构成。 光标其实本质上就是x和y坐标对应的内存指针 ,往这个位置进行读取或者写入。上一小节中,定位光标使用的是gotoxy函数。

static inline void gotoxy(unsigned int new_x,unsigned int new_y)
{
	if (new_x > video_num_columns || new_y >= video_num_lines)
		return;
	x=new_x;
	y=new_y;
	pos=origin + y*video_size_row + (x<<1);
}

    gotoxy中x表示光标的列,y表示光标的行,pos是对应位置的内存指针。现在如果

_keyboard_interrupt:
    ...
    call _do_tty_interrupt
    ...

void do_tty_interrupt(int tty)
{
	copy_to_cooked(tty_table+tty);
}

void copy_to_cooked(struct tty_struct * tty) {
    ...
    tty->write(tty);
    ...
}

// 控制台时 tty 的 write 为 con_write 函数
void con_write(struct tty_struct * tty) {
    ...
    __asm__("movb _attr,%%ah\n\t"
      "movw %%ax,%1\n\t"
      ::"a" (c),"m" (*(short *)pos)
      :"ax");
     pos += 2;
     x++;
    ...
}

    最后一个函数con_write是内联汇编,就是将字符c写入pos指向的内存,相当于往屏幕上进行输出。之后pos加两个字节,x向后调整,即光标向右调整一个位置。

    在右侧到达边界后,就需要进行换行,其实就是把光标计算出一个新值,使其处于下一行开头。

void con_write(struct tty_struct * tty) {
    ...
    if (x>=video_num_columns) {
        x -= video_num_columns;
        pos -= video_size_row;
        lf();
  }
  ...
}

static void lf(void) {
   if (y+1<bottom) {
      y++;
      pos += video_size_row;
      return;
   }
 ...
}

    其具体做法也很简单,con_write函数负责监测是否到达边界并设置xposlf函数中负责调整行的位置,即y以及据此接着调整对应pos指向的内存位置。

    滚屏效果也是可以实现的,同样也是先检测再调整。当光标位置x和y出现在最后一行的最后一列时,将每一行的字符复制到上一行。向上滚动也是同理,这里看一个向下滚动的函数:

static void scrdown(void)
{
	__asm__("std\n\t"
		"rep\n\t"
		"movsl\n\t"
		"addl $2,%%edi\n\t"	/* %edi has been decremented by 4 */
		"movl _video_num_columns,%%ecx\n\t"
		"rep\n\t"
		"stosw"
		::"a" (video_erase_char),
		"c" ((bottom-top-1)*video_num_columns>>1),
		"D" (origin+video_size_row*bottom-4),
		"S" (origin+video_size_row*(bottom-1)-4)
		:"ax","cx","di","si");
}

    这里简化掉了显示模式选择,直接看实现方式。里面是同样是采用内联汇编的方式,其中简单来说就是,操作循环次数的是ECX寄存器,将行数赋值给这个寄存器后通过rep指令循环操作。stosw是串操作,rep对其进行多次执行。串操作的输入参数如下,每个值赋值的寄存器则分别是ax,cx,di,si

  • "a" (video_erase_char):把 video_erase_char 的值赋给寄存器 AX,此值会在 stosw 操作中用于填充屏幕的最后一行。
  • "c" ((bottom - top - 1) * video_num_columns >> 1):将计算得到的值赋给寄存器 ECX,这个值决定了 rep movsl 和 rep stosw 操作的重复次数。
  • "D" (origin + video_size_row * bottom - 4):把计算得到的地址赋给寄存器 EDI,该地址是数据复制的目标地址。
  • "S" (origin + video_size_row * (bottom - 1) - 4):把计算得到的地址赋给寄存器 ESI,该地址是数据复制的源地址。

    采用这些想法,可以实现常见的控制台操作:回车,删除,清屏等,对应函数名称如下:

// 定位光标的
static inline void gotoxy(unsigned int new_x, unsigned int new_y){}
// 滚屏,即内容向上滚动一行
static void scrup(void){}
// 光标同列位置下移一行
static void lf(int currcons){}
// 光标回到第一列
static void cr(void){}
...
// 删除一行
static void delete_line(void){}

🎯总结

    本文主要介绍了开启键盘中断,使得我们可以与控制台进行交互,同时如何将字符显示到屏幕上。其中字符显示到屏幕上的本质就是映射一部分内存为显存,然后往该内存里面写东西就可以显示到屏幕上。对于其中输入位置以及滚动,清屏,换行等操作则是通过代表屏幕坐标的xy以及对应位置的内存坐标指针pos进行算法编写实现的。

📖参考资料

[1] linux源码趣读
[2] 一个64位操作系统的设计与实现