在Linux相关的文档中,可以经常看到tty、console、terminal、shell等单词。在一些不那么严格的场景中,它们会被混用,如在一些中文翻译中有可能都被翻译成终端。但实际上,在计算机发展的早期,它们分别对应不同的实体。早期的计算机往往会配备一个操作面板,上面带有大量开关和指示灯,操作员就通过这个面板操作计算机,这个面板就叫作console。Windows系统上的“控制面板”这个名词也来源于此。一台电脑通常只有一个console。随着技术的进步,计算机可以支持多个用户远程连接,每个用户手上有一个专用硬件用于登录,这个专用硬件就是terminal,中文可以翻译为终端。终端的形态是多种多样的,tele typewriter,即远程打字机,缩写为tty。所以慢慢地人们也会使用tty这个缩写来指代终端。而shell则是与内核相对应的一个概念,它是一个运行在console上的进程,也是一个负责读入用户输入、处理用户请求和用户交互的应用程序。
搞清楚这些概念以后,再来阅读Linux源码就能轻松区分代码中的这些概念了。终端tty不只包含屏幕的输出,还包括键盘的输入,你慢慢会发现原来串口通信也是一种终端,所以我们就可以先实现终端的初始化函数,然后在这个函数中进行屏幕、键盘和串口输入/输出的初始化。tty_init就负责初始化终端,而con_init则负责初始化控制台,在屏幕上进行显示这个工作显然适合由控制台来实现。console的初始化的代码如代码清单3-2所示。
1 /* kernel/chr_drv/console.c */
2 #include<linux/tty.h>
3
4 #define ORIG_X (*(unsigned char *)0x90000)
5 #define ORIG_Y (*(unsigned char *)0x90001)
6 #define ORIG_VIDEO_PAGE (*(unsigned short *)0x90004)
7 #define ORIG_VIDEO_MODE ((*(unsigned short *)0x90006) & 0xff)
8 #define ORIG_VIDEO_COLS (((*(unsigned short *)0x90006) & 0xff00)>>8)
9 #define ORIG_VIDEO_LINES ((*(unsigned short *)0x9000e) & 0xff)
10 #define ORIG_VIDEO_EGA_AX (*(unsigned short *)0x90008)
11 #define ORIG_VIDEO_EGA_BX (*(unsigned short *)0x9000a)
12 #define ORIG_VIDEO_EGA_CX (*(unsigned short *)0x9000c)
13
14 #define VIDEO_TYPE_MDA 0x10
15 #define VIDEO_TYPE_CGA 0x11
16 #define VIDEO_TYPE_EGAM 0x20
17 #define VIDEO_TYPE_EGAC 0x21
18
19 static unsigned char video_type;
20 static unsigned long video_num_columns;
21 static unsigned long video_num_lines;
22 static unsigned long video_mem_base;
23 static unsigned long video_mem_term;
24 static unsigned long video_size_row;
25 static unsigned char video_page;
26 static unsigned short video_port_reg;
27 static unsigned short video_port_val;
28
29 void con_init(){
30 char *display_desc="????";
31 char *display_ptr;
32
33 video_num_columns=ORIG_VIDEO_COLS;
34 video_size_row=video_num_columns *2;
35 video_num_lines=ORIG_VIDEO_LINES;
36 video_page=ORIG_VIDEO_PAGE;
37
38 /* 这是一个单色显示器吗? */
39 if(ORIG_VIDEO_MODE==7){
40 //部分代码略
41 }
42 else{/* color display */
43 video_mem_base=0xb8000;
44 video_port_reg=0x3d4;
45 video_port_val=0x3d5;
46
47 if((ORIG_VIDEO_EGA_BX & 0xff)!=0x10){
48 video_type=VIDEO_TYPE_EGAC;
49 video_mem_term=0xc0000;
50 display_desc="EGAc";
51 }
52 else{
53 //部分代码略
54 }
55 }
56
57 display_ptr=((char *)video_mem_base)+video_size_row-8;
58 while(*display_desc){
59 *display_ptr++= *display_desc++;
60 *display_ptr++;
61 }
62 }
因为QEMU和Bochs虚拟机都采用了彩色模式的EGA,所以上述代码在执行初始化的过程就会走到EGA的分支。其他分支是用于支持更早期的单色显示器,已经不重要了,所以正文里的代码就把其他分支略去了。第43行代码把显存的起始位置设为0xb8000,因为已经启用了保护模式分页机制,所以这里的地址实际上是虚拟地址,尽管这个值与物理地址是相同的。第49行设置显存结束地址为0xc0000。
setup模块通过使用BIOS中断取得计算机的显卡、内存、硬盘等信息,然后把它们都存储在0x90000的位置,这里就是使用显卡信息的地方。video_num_columns指示了显示器的列数,video_num_lines指示了显示器的行数。除此之外,与显卡相关的其他信息会在后面用到时再介绍。结合上述分析可以知道,第57行~第61行的作用是把代表显示器类型的字符串打印到屏幕的右上角。因为一个字符占据两个字节,第一个字节指示字符的颜色和背景色,第二个字节是字符的ASCII值,所以display_ptr就指向了第一行往前的8个字节,这里预留了显示4个字符的宽度。
同时,main函数也要增加对con_init的调用:
1 //kernel/main.c
2 void main(void){
3 tty_init();
4 __asm__ __volatile__(
5 "loop:\n\r"
6 "jmp loop"
7 ::);
8 }
9
10
11 //kernel/chr_drv/tty_io.c
12 #include<linux/tty.h>
13
14 void tty_init(){
15 con_init();
16 }
最后一步,再把这两个新增的C文件移到chr_drv目录下,并新建makefile文件以编译这个目录。目录名是character drvier的缩写,这个目录的作用是处理字符设备。字符设备的特点是它们的输入/输出都是以字符为单位的,例如本节显示器里显示字符串的操作就是向显存中逐个复制字符。与字符设备相对应的是块设备,块设备的输入/输出都是以数据块为单位的,比如读取硬盘的动作就是以扇区为单位的,一次输入/输出会传输512B,块设备的驱动会在第6章进行介绍,这里就不再详细展开。
chr_drv目录中的makefile内容如下:
1 AR:=ar
2 LD:=ld
3 GCC:=gcc
4 CCFLAG:=-m32-I../../include-nostdinc-Wall-fomit-frame-pointer-c
5 vOBJS :=tty_io.o console.o
6
7 tty_io.o:tty_io.c
8 $(GCC) $(CCFLAG)-o$@$<
9
10 console.o:console.c
11 $(GCC) $(CCFLAG)-o$@$<
12
13 chr_drv.a: $(OBJS)
14 $(AR)rcs$@$^
15 sync
16
17 clean:
18 rm *.o
19 rm chr_drv.a
这个目录最后生成的目录文件是chr_drv.a,这是一个静态库文件。静态库文件本质是一个压缩文件,是将目标文件(.o文件)打包压缩在一起,并不会对目标文件进行链接、解析符号等操作。而真正的链接操作仍然发生在构建system文件时,所以我们还要记得在kernel目录的makefile文件中添加链接chr_drv.a的操作:
1 OBJS:=head.o main.o sched.o chr_drv/chr_drv.a
2
3 system: $(OBJS)
4 $(LD) $(LDFLAG)-e startup_32-o$@$^
5
6 ...
7
8 chr_drv/chr_drv.a:chr_drv/* .c
9 cd chr_drv;make chr_drv.a;cd..
10
11 clean:
12 rm *.o
13 rm system
14 cd chr_drv;make clean;cd..
重新编译整个目录,并且在虚拟机中运行新的linux.img,就可以看到屏幕的右上角正确地打印出了字符串“EGAc”。到这里,控制台的初始化工作就完成了。