《ORANGE-s-一个操作系统实现》输入输出系统(5-2)

显示器

初识TTY

TTY在Linux中就是终端。当按下ALT+F1、ALT+F2、ALT+F3等组合键时,会切换到不同的屏幕。对于不同的TTY可以理解成下面:

mark
虽然不同的TTY对应的输入设备是同一个键盘,但是输出却好比是在不同的显示器上,因为不同的TTY对应的屏幕画面可能不同。实际上,画面的不同,仅仅是显示了显存的不同位置罢了。
既然3个CONSOLE公用一块显存,那就有一种方式在切换CONSOLE的瞬间,让屏幕显示显存中某个位置的内容。
屏幕上每一个字符对应的2字节的定义如下所示:
mark
可以看到,第字节表示的是字符本身,高字节用来定义字符的颜色。
mark

寄存器

VGA视频子系统的寄存器如下:
mark
这么多的寄存器,只有一个端口0X3D5,然后配合Address Register。下图中每一个寄存器都对应一个索引值,当想要访问其中一个的时候,只需要先向Adress Register写对应的索引值,然后在通过端口0x3D5进程的操作就是针对索引值对应的寄存器了。
![mark](http://p29pmm8g4.bkt.clouddn.com/blog/180306/6aKGfjHmCk
下面我们让光标跟随我们敲入的字符,设置光标位置:

1
2
3
4
5
6
disable_int();
out_byte(CRTC_ADDR_REG, CURSOR_H);
out_byte(CRTC_DATA_REG, ((disp_pos/2)>>8)&0xFF);
out_byte(CRTC_ADDR_REG, CURSOR_L);
out_byte(CRTC_DATA_REG, (disp_pos/2)&0xFF);
enable_int();

make运行结果如下:
mark
接着,我们通过设置Start Adressb High Register和Start Adress Low Register来重新设置显示开始地址,从而实现滚屏的功能。当我们按下shift+上箭头时,则卷动屏幕向上15行
mark

TTY任务

在TTY任务中执行一个循环,这个循环将轮询每一个TTY,处理它的事件,包括从键盘缓冲区读取数据、显示字符等。它的运行方式如下:
mark
其实轮询到每一个TTY时,不外乎做两件事:

  • 处理输入:查看是不是当前TTY,如果是则从键盘缓冲区读取数据。
  • 处理输出:如果有要显示的内容则显示它。
    下面要做的TTY任务不再简单,主要表现为:
  • 每一个TTY都应该有自己的读和写的动作。所以在keyboard_read()内部,函数需要了解自己是被哪一个TTY调用。我们通过为函数传入一个参数来做到这一点,这个参数是指向当前TTY的指针。
  • 为了让输入输出分离,被keyboard_read()调用的in_process()不应该再直接回显示符,而应该将回显的任务交给TTY来完成,这样我们就需要为每个TTY建立一块缓冲区,用以放置将被回显的字符。
  • 每个TTY回显字符时操作的console是不同的,所以每个TTY都应该有个成员来记载其对应的console信息。

    TTY任务框架的搭建

    TTY结构如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #define TTY_IN_BYTES	256	/* tty input queue size */

    struct s_console;

    /* TTY */
    typedef struct s_tty
    {
    u32 in_buf[TTY_IN_BYTES]; /* TTY 输入缓冲区 */
    u32* p_inbuf_head; /* 指向缓冲区中下一个空闲位置 */
    u32* p_inbuf_tail; /* 指向键盘任务应处理的键值 */
    int inbuf_count; /* 缓冲区中已经填充了多少 */

    struct s_console * p_console;
    }TTY;

CONSOLE结构如下:

1
2
3
4
5
6
7
typedef struct s_console
{
unsigned int current_start_addr; /* 当前显示到了什么位置 */
unsigned int original_addr; /* 当前控制台对应显存位置 */
unsigned int v_mem_limit; /* 当前控制台占的显存大小 */
unsigned int cursor; /* 当前光标位置 */
}CONSOLE;

整个程序的流程如下:
mark
在task_tty()中,通过循环来处理每一个TTY的读和写操作,读写操作都放在了tty_do_read()和tty_do_write()两个函数中,这样就让taske_tty()很简洁,而且逻辑清晰。读操作会调用keyboard_read(),当然此时已经多了一个参数;写操作会调用out_char(),它会将字符写入指定的CONSOLE。,当TTY任务开始运行时,所有TTY都将被初始化,并且全局变量nr_current_console会被赋值为0.然后循环开始并一直进行下去。对于每一个TTY,首先执行tty_do_read(),它将调用kerboard_read()并将读入的字符交给函数in_process()来处理,如果是需要输出的字符,会被in_process()放入当前接受处理的TTY的缓冲区中。然后tty_do_write()会接着执行,如果缓冲区中有数据,就被送入out_char显示出来。

多控制台

下面是多控制台示意图:
mark
表示了某时刻显存的使用情况。其中灰色框表示当前屏幕,黑色小方格显示显存已经写入的字符。
运行结果如下:
在控制台0按下数次shift+箭头上
mark

完善键盘处理

回车键和退格键

当敲击回车键和退格键时,我们王TTY缓冲区中写入“\n”和“\b”,然后在out_char中做出相应的处理,如下:

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
PUBLIC void in_process(TTY* p_tty, u32 key)
{
char output[2] = {'\0', '\0'};

if (!(key & FLAG_EXT)) {
put_key(p_tty, key);
}
else {
int raw_code = key & MASK_RAW;
switch(raw_code) {
case ENTER:
put_key(p_tty, '\n');
break;
case BACKSPACE:
put_key(p_tty, '\b');
break;

。。。。。。
PRIVATE void put_key(TTY* p_tty, u32 key)
{
if (p_tty->inbuf_count < TTY_IN_BYTES) {
*(p_tty->p_inbuf_head) = key;
p_tty->p_inbuf_head++;
if (p_tty->p_inbuf_head == p_tty->in_buf + TTY_IN_BYTES) {
p_tty->p_inbuf_head = p_tty->in_buf;
}
p_tty->inbuf_count++;
}
}

然后修改out_char:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
PUBLIC void out_char(CONSOLE* p_con, char ch)
{
u8* p_vmem = (u8*)(V_MEM_BASE + p_con->cursor * 2);

switch(ch) {
case '\n':
if (p_con->cursor < p_con->original_addr +
p_con->v_mem_limit - SCREEN_WIDTH) {
p_con->cursor = p_con->original_addr + SCREEN_WIDTH *
((p_con->cursor - p_con->original_addr) /
SCREEN_WIDTH + 1);
}
break;
case '\b':
if (p_con->cursor > p_con->original_addr) {
p_con->cursor--;
*(p_vmem-2) = ' ';
*(p_vmem-1) = DEFAULT_CHAR_COLOR;
}
break;
default:
if (p_con->cursor <
p_con->original_addr + p_con->v_mem_limit - 1) {
*p_vmem++ = ch;
*p_vmem++ = DEFAULT_CHAR_COLOR;
p_con->cursor++;
}
break;
}

while (p_con->cursor >= p_con->current_start_addr + SCREEN_SIZE) {
scroll_screen(p_con, SCR_DN);
}

flush(p_con);
}

/*======================================================================*
flush
*======================================================================*/
PRIVATE void flush(CONSOLE* p_con)
{
set_cursor(p_con->cursor);
set_video_start_addr(p_con->current_start_addr);
}

可以看到回车键直接把光标挪到了下一行的开头,而退格键则把光标挪到上一个字符的位置,并在那里写一个空格,以便清除原来的字符。
由于不断的回车会让光标快速的移动到屏幕的底端。所以在这里还要判断光标是否已经移出了屏幕,如果是的话将会触发屏幕滚动。
另外,输出的任何类型的字符时,都做了边界检验,以防止影响到别的控制台,甚至试图写到显存之外的内存。
运行结果如下:
mark

区分任务和用户进程

前面的TTY我们称之为任务,A、B、C则为用户进程。在具体的实现上,让用户进程运行在ring3,任务继续留在ring1。如下图:
mark

printf

为进程指定TTY

当某个进程调用printf时,操作系统必须知道往哪个控制台输出才行。而当系统调用发生,ring3跳入ring0时,系统只能知道当前系统调用是由哪个进程触发的。所以我们必须为每个进程指定一个与之相对应的TTY,这可以通过在进程表中增加一个成员来实现。

printf()的实现

printf()的实现并不简单,首先是它的参数个数和类型都可变,而且其表示格式的参数形式多样,在printf()中。都要加以识别。
下面我们先实现printf()只支持%X一种格式。如下:

1
2
3
4
5
6
7
8
9
int printf(const char *fmt,...)
{
int i;
char buf[256];
va_list arg=(va_list)((char*)(&fmt)+4);
i=vsprintf(buf,fmt,arg);
write(buf,i);
return i;
}

系统调用

增加一个系统调用的过程如下所示:
mark

使用print分()

make运行结果如下图:
mark

您的支持将鼓励我继续创作!