12)控制台初始化

tty_init

1
2
3
4
5
6
// kernel/chr_drv/tty_io.c
void tty_init(void)
{
rs_init();
con_init();
}

这个方法被拆分成了两个子方法

rs_init

1
2
3
4
5
6
7
8
9
// kernel/chr_drv/serial.c
void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt);
set_intr_gate(0x23,rs2_interrupt);
init(tty_table[1].read_q.data);
init(tty_table[2].read_q.data);
outb(inb_p(0x21)&0xE7,0x21);
}

这个方法开启了串口中断,设置了对应的中断处理程序。串口在我们现在的 PC 机上已经很少用到了

con_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// kernel/chr_drv/console.c
void con_init(void) {
...
if (ORIG_VIDEO_MODE == 7) {
...
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {...}
else {...}
} else {
...
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {...}
else {...}
}
...
}

这些判断是在根据不同的显示模式设置不同的参数

直写显存

如果可以随意操作内存和 CPU 等设备,怎么操作才能让显示器上显示一个字符“h”呢?

内存中有这样一部分区域,是和显存映射的。啥意思,就是你往上图的这些内存区域中写数据,相当于写在了显存中。而往显存中写数据,就相当于写在了屏幕上。

1
mov [0xB8000],'h'

当往现存里写一个h,屏幕就会出现一个h

这片内存每两个字节可以表示一个显示在屏幕上的字符,第一个是字符的编码,第二个是字符的颜色。我们先看编码,先不管颜色,如果多写几个字符就像下面这样。

1
2
3
4
5
mov [0xB8000],'h'
mov [0xB8002],'e'
mov [0xB8004],'l'
mov [0xB8006],'l'
mov [0xB8008],'o'

屏幕就会出现hello:

我们就假设显示模式是我们现在的这种文本模式,可以简化成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// kernel/chr_drv/console.c
#define ORIG_X (*(unsigned char *)0x90000)
#define ORIG_Y (*(unsigned char *)0x90001)
void con_init(void) {
register unsigned char a;
// 第一部分 获取显示模式相关信息
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 = 0x3d4;
video_port_val = 0x3d5;
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 处的数据),然后设置并开启键盘中断。

开启键盘中断后,在键盘上敲击一个按键后就会触发中断,中断程序会读键盘码转换成 ASCII 码,然后写到光标处的内存地址,其实也就相当于往显存里写,于是这个键盘敲击的字符就显示在了屏幕上。

为 x、y、pos 参数赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// kernel/chr_drv/console.c
#define ORIG_X (*(unsigned char *)0x90000)
#define ORIG_Y (*(unsigned char *)0x90001)
void con_init(void) {
...
// 第四部分 定位光标并开启键盘中断
gotoxy(ORIG_X, ORIG_Y);
...
}

// kernel/chr_drv/console.c
static inline void gotoxy(unsigned int new_x,unsigned int new_y) {
...
x = new_x;
y = new_y;
pos = origin + y*video_size_row + (x<<1);
}

就是给 x、y、pos 这三个参数附上了值。其中 x 表示光标在哪一列,y 表示光标在哪一行,pos 表示根据列号和行号计算出来的内存指针。也就是说,往这个 pos 指向的地址写数据,就相当于往控制台的 x 列 y 行处写入字符了

当你按下键盘后,触发键盘中断,之后的程序调用链是下面这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_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++;
...
}

通过中断调用到中断处理函数_keyboard_interrupt,然后一路调用到 con_write 中的关键代码。这段由 asm 包裹的内联汇编代码,就是把键盘输入的字符 c 写入 pos 指针指向的内存,相当于往屏幕输出了。而之后两行 pos+=2 和 x++,就是调整所谓的光标。


12)控制台初始化
https://carl-5535.github.io/2024/08/07/Linux0.11/12)控制台初始化/
作者
Carl Chen
发布于
2024年8月7日
许可协议