NJU-OS-lab0(1)--初始化工作

时间:2021-11-02 03:56:36

NJU-OS-lab0(1)--初始化工作哈哈,终于开始写操作系统实验啦,简直就是心魔了,看我这次怎么打败这个大boss!

主程序

首先从os-lab/src/main.c这里开始吧,看看主程序做了些什么。

void game_init()
{
init_serial();
init_timer();
init_idt();
init_intr();
set_timer_intr_handler(timer_event);
set_keyboard_intr_handler(keyboard_event);
printk("game start!\n");
enable_interrupt();
main_loop();
assert(0);
}

串行接口

第一句话就是初始化串行串口,它在os-lab0/include/game.h中定义,在os-lab0/src/device/serial.c中实现

微信计算机主机与外部设备连接,基本使用了两类接口:串行接口serial port和并行接口parallel port。

并行接口是指数据的各位同时进行传说,所以传输速度快,但是当传输距离较远,位数也多时,会使通信线路变复杂,成本也就提高了。

串行接口则是将数据一位位地顺序传送,所以通信线路简单,只需要一对传输线就可以实现双向通信,还可以使用电话线,使得成本大大降低。

NJU-OS-lab0(1)--初始化工作

内联汇编

in_byte和out_byte是对计算机硬件中的端口进行读写,所以需要用到内联汇编,相关可见。。。

in_byte便是从端口port处读入数据,out_byte则是将数据写入端口port处。

NJU-OS-lab0(1)--初始化工作

COM port

从代码中可以看出串行接口地址为0x3F8,why?还有接下来那些写端口的操作都是在干嘛呢?wiki中是这样介绍的

串行接口一般有4组COM(comunication) port,一般是使用前两组COM1和COM2,四组COM port的端口起始位置分别为:

COM Port IO Port
COM1 3F8h
COM2 2F8h
COM3 3E8h
COM4 2E8h
通过COM1的端口基地址0x3F8,之后就可以通过offset访问这组接口中的寄存器。

偏移量可以从0~7,不同偏移量对应不同寄存器,对应关系如下:

IO Port Offset Setting of DLAB Register mapped to this port
+0 0 Data register. Reading this registers read from the Receive buffer. Writing to this register writes to the Transmit buffer.
+1 0 Interrupt Enable Register.
+0 1 With DLAB set to 1, this is the least significant byte of the divisor value for setting the baud rate.
+1 1 With DLAB set to 1, this is the most significant byte of the divisor value.
+2 - Interrupt Identification and FIFO control registers
+3 - Line Control Register. The most significant bit of this register is the DLAB.
+4 - Modem Control Register.
+5 - Line Status Register.
+6 - Modem Status Register.
+7 - Scratch Register.
从图中可以看出一共有10个寄存器,这些寄存器都是8位寄存器。

偏移为0,1时,在DLAB不同情况下会分别对应两个不同的寄存器。

那DLAB是什么呢?

DLAB

上图中+3偏移量对应线路控制寄存器(line control register),它的最高位便是DLAB(Divisor Latch Access Bit),除数锁存访问位,这是用来干什么的? 在说它用来干嘛之前,首先要介绍一下波特率。 波特率是计算机串口通信时的速率,即信号被调制以后单位时间内的变化。 比如比如1s传送200个字符,每个字符有10位,那么波特率便是200Bd(Baud),比特率则是200*10bps = 2000 bps。 波特率描述了单位时间内设备接受或发送的码元个数,通过不同的调制方式(编码方式),比如在计算机通信中会用到各种不同的编码,那么对1byte大小的数据,可能得到2byte的编码数据,也可能得到10位的编码数据,而波特率并不关心每个码元的位数,而只是考虑码元个数。
串行控制器the serial controller(UART)在内部有有一个每秒滴答115200次的时候,为了控制系统的波特率,便引入a clock divisor,系统波特率便为115200/divisor Bd。 所以DLAB的引入便是为了设置这个divisor。 设置divisor的过程如下:
  1. 首先将Line Control Register的最高位设为1,也就是DLAB位设为1,这样offset = 0,1便分别指向了the divisor register的低8位和高8位。
  2. 将divisor的低字节(也就是低8位)信息写入到端口offset=0指向的寄存器
  3. 将divisor的高字节(也就是高8位)信息写入到端口offset=1指向的寄存器。
  4. 写完之后便将Line Control Register的DLAB位清0,这样offset = 0指向数据寄存器,offset = 1指回中断使能寄存器(Interrupt Enable Register)

那我们回过头来看代码:

#define SERIAL_PORT 0x3F8
这里将端口基地址设为0x3F8,也就是使用COM1 port。

out_byte(SERIAL_PORT + 1, 0x00);
因为offset = 1端口指向的是中断使能寄存器,所以清零便是关中断。

out_byte(SERIAL_PORT + 3, 0x80);
这一句将Line Control Register的最高位也就是DLAB设为1,所以offset = 0,1端口便可以用来设置divisor,下面做的也正是如此。
out_byte(SERIAL_PORT + 0, 0x01);out_byte(SERIAL_PORT + 1, 0x00);
将divisor设为1,所以波特率为115200Bd。

线性控制寄存器

out_byte(SERIAL_PORT + 3, 0x03);
这一步设置线性控制寄存器。

线性控制寄存器可以控制好几部分内容

DLAB为最高位;

6~4位用于控制校验位,具体如下:

Bit 5 Bit 4 Bit 3 Parity
- - 0 NONE
0 0 1 ODD
0 1 1 EVEN
1 0 1 MARK
1 1 1 SPACE
None即没有,ODD和EVEN便是我们耳熟能详的奇偶校验位啦,剩下两个我不清楚,没见过诶。

最低两位用于设置字符长度,也即每个码元的长度:

Bit 1 Bit 0 Character Length (bits)
0 0 5
0 1 6
1 0 7
1 1 8

第3位用于设置终止位的位数。

Bit 2 Stop bits
0 1
1 1.5 / 2 (depending on character length)
如果字符长度为5,那么终止位只能为1/1.5位,其他情况下,终止位则为1/2位。

所以上面那句首先清空DLAB,设置校验位为无,终止位位数为1,字符长度则为8。


时钟初始化

串行接口初始完之后就轮到时钟了。

NJU-OS-lab0(1)--初始化工作

关于时钟的编程叫做Programmable Interval Timer(PIT,可编程时钟计时器)。

PIT芯片由一个oscillator振荡器,一个prescalar预分频器以及3个独立的frquency dividers分频器组成。

每一个分频器都有一个输出,用于控制外部电路(比如输出IRQ 0)。

oscillator

oscillator用于产生一个大致为1.193182MHz的频率。

Frequency Dividers

分频器的基本思路是用一个数去除oscillator产生的频率,这样便可以得到一个更低的频率。 所以这里便需要一个counter来做除数。 oscillator每一次产生脉冲信号,都会使得counter减1,当counter减为0时,便可以输出一个信号,并且counter重置为初值。 如果有一个200Hz的输入信号,counter设置为10,那么得到的输出信号频率便是20Hz啦。 PIT芯片中为frequency divider有一个16位寄存器,所以counter可以取值0~65535,因为0不能做除数,所以大多情况下都会用0来代替65536。 PIT芯片有3个单独的分频器,分别代表了3个不同的channels通道。 channel0直接与IRQ0相连,所以它是产生中断信号的最好选择哦 channel1已经被淘汰啦,以后也没什么用。 channel与PC Speaker相连。 在启动BIOS的时候,channel0的分频器便被设为0(也就是65536),如此便得到18.2065HZ的一个输出频率。

I/O Ports

PIT芯片使用如下的I/O端口: I/O port     Usage 0x40         Channel 0 data port (read/write) 0x41         Channel 1 data port 0x42         Channel 2 data port 0x43         Mode/Command register(write only, a read is ignored) 每一个8位数据端口都是一样的,用于设置counter的16位reload值,或是读取channel目前的16位counter值。 当channel的counter减为0时,此channel的输出边作出改变,接着counter被重置为reload value。

Mode/Command register

Bits        Usage 6,7 select Channel 00          = channel0 01          = channel1 10          =channel2 11          Read-back Command 4,5        Access Mode 00         =Latch count value command 01         =lobyte only 10         =hibyte only 11         =lobyte/hibyte 1,2,3     Operating Mode 001       =hardware re-triggrable one-shot 010       =rate generator 011       =square wave generator 100       =softwae triggered strobe 101       =hardware trigger strobe 110       =same as 010 111       =same as 011 0           BCD/Binary Mode:0=16-bid binary,1=four-digit BSC 来看代码out_byte(TIMER_PORT, 0X34) 所以选择channel0,访问模式则为lobyte/hibyte,Operating Mode为rate generator, 16-bit binary。 因为数据端口是8位,而counter是16位,所以芯片就需要知道数据端口是读/写counter的高字节位还是低字节位。 “lobyte only"则只对低字节进行处理(低8位),”hibyte only“则是对counter的高字节位进行处理。 在"lobyte/hibyte"访问模式中,counter的16位被设定为,在8位的数据端口上,先访问低8位数据,紧接着访问高8位数据。 所以在代码中我们能看到先写低8位 out_byte(TIMER_PORT + 0, counter % 256); 再写高8位, out_byte(IMTER_PORT + 0, counter / 256); rate generator和square wave generator两种操作模式都可以用来生成IRQ定时器,一般情况下OS和BIOS都是采用Square wave generator模式,如果采用rate generator模式,则可以增加频率准确率。

IDT初始化

NJU-OS-lab0(1)--初始化工作
NR_IRQ在cpu.h中定义,为256。GateDescriptor在memory.h中定义。NJU-OS-lab0(1)--初始化工作
首先看门描述符,它长度是8个字节,也就是64位,所以"offset_15_0 :16"也就表明offset_15_0长度为16位bits。门描述符也就是一个段描述符,下面对Intel x86的分段存储结构进行一些说明,具体参考的是孙钟秀教授主编的操作系统教程中4.6节。

Intel x86的分段

Intel x86系列CPU有三种工作模式:实地址模式,保护模式以及虚拟8086模式。在保护模式下,采用分段机制,可启用分页机制,所以有段式,页式,段页式三种虚拟存储管理模式。Inel x86实现虚拟存储管理的核心便是内存中的两张描述符表GDT(Global Descriptor Table)以及LDT(Local Descriptor Table)。每个进程都有它自己的LDT,用来描述这个进程的代码段,数据段,堆栈段以及扩充段等的基地址,段大小和有关控制信息。系统的所有进程则共享一个GDT,用于描述系统段,包括操作系统的段大小,段基址,相关控制信息以及所有进程共享的系统资源。所有当发生进程切换的时候,LDT则更换为待运行进程的LDT,GDT则保持不变,分别用LDTR,GDTR两个寄存器保持LDT,GDT的基地址。
下面说一下段描述符是如何使用的:现在物理地址都是2^32 = 4GB大小,而虚拟地址是48位,也就有2^48 = 64TB大小。因为物理地址是32位长度,所以48位中的低32位为offset,高16位则为段选择符:段选择符47:32  偏移量31:016位段选择符的结构如下:index15:3  T:2  RPL1:0T = 0时从GDT中选择描述符,T= 1时从LDT中选择描述符。RPL是描述符请求的特权级,此处不管它,所以用于做下标的index有13位的。那么LDT与GDT分别都有2^13 = 8192个存储器分段。
于是流程便是:首先,将虚拟地址的高16位取出,放入机器的6个段寄存器之一,比如代码段选择符放入CS中,数据段选择符则放入DS中,堆栈段选择符放入SS中。之后,根据段选择符的第3位T决定是从LDT还是GDT中查找段描述符。接着根据段选择符的高13位index从段描述符表中取出对应的段描述符,然后从段描述符中得到32位段基址,与虚拟地址的32位偏移相加就得到了32位的线性地址。假如不采用分页机制,那么此时就得到了物理地址。一个段描述符构造如下:
struct Descriptro
{
//段限长一共20位
uint32_t limit_15_0 :16;
//段基址一共32位
uint32_t base_15_0 :16;
uint32_t base23_16 :8;
//访问位,=0未访问,=1已访问,用于淘汰页面
uint32_t A :1;
//段类型和保护方式,比如可执行代码段或是只读数据段
uint32_t TYPE :3;
//段内容标志,=1位代码或数据段,=0是系统段。
uint32_t S :1;
//描述符特权级0~3
uint32_t DPL :2;
//=1表示段包含有效基址和段界限,否则无定义
uint32_t P :1;
uint32_t limt_19_16 :4;
//用户编程可用位
uint32_t AVL :1;
//就是0
uint32_t zero :1;
//=1为32位代码段,=0为16位代码段
uint32_t D :1;
//=0表示以字节为单位,段长度则为2^20B,=1表示以页面为单位,一个页面4KB,所以段长为2^20 * 4KB = 4GB。
uint32_t G :1;
uint32_t base_31_24 :8;
};
在代码中实现的是中断门描述符和陷阱门描述符,关于门描述符还有调用门描述符,任务门描述符,暂且不提。中断门描述符以及陷阱门描述符都存储在IDT中,IDT包含256个中断描述符,与中断,异常一一对应。所以每个中断/异常都有一个在0~255的向量号,用于在IDT中的索引,其中0x80即128号是用于系统调用的。下图来自http://book.2cto.com/201310/34281.html ,比较形象地说明了根据中断向量号得到中断处理程序的过程。首先48位寄存器IDTR中的高32位存储了IDT基址,低16位存储的是IDT大小。根据IDT基址,加上向量号索引,得到了中断门描述符。从中断门描述符中我们可以得到16位段选择器,放入CS寄存器,之后从GDT中得到段基址,之后加上32位偏移,便可以得到中断处理程序地址啦。NJU-OS-lab0(1)--初始化工作在idt.c中首先定义IDT表,门描述符与普通的段描述符之间的差别体现在:1.type变为4位2.没有段基址,有段选择符所以下面对中断门的初始化,首先是偏移的设置,之后是选择符的设置,selector只有13位,向左偏移3位之后得到的T=0,说明是从GDT中找段基址,RPL则为0,是最大权限了。pad0是8位0,没什么用的。4位type设置为0xE。system设置为false,因为不是系统段。之后设置优先级权限privilege_level。present表明这个描述符是有效的。最后是偏移量的高16位设置。陷阱门的初始化除了type不同,其它设置都一样啦。

NJU-OS-lab0(1)--初始化工作


之后我们看它对陷阱门以及中断门的初始化NJU-OS-lab0(1)--初始化工作

我们回过头来看init_idt函数,它首先将IDT中所有项都指向irq_empty()处理函数。之后将0~13号异常分别指向vec0,...,vec13()处理函数。接着设置32号中断处理函数为irq0()。关于IDT中的向量号,0~31号对应异常或硬件非屏蔽中断,32~47对应硬件可屏蔽中断,48~255则分配给软件中断,0x80(128)用来实现系统调用。关于段选择符selector的设置,我们看memory.h中的定义:一共定义了3个段,内核代码段偏移为1,内核数据段偏移为2,因为中断处理程序是放在代码段中,所以selector都设置为SEG_KERNAL_CODE。优先级dpl则都设置为最高的DPL_KERNAL。NJU-OS-lab0(1)--初始化工作

关于中断处理程序,在do_irq.S中定义,是汇编代码NJU-OS-lab0(1)--初始化工作
.global告诉编译器后面跟的是全局变量或是全局函数,这里定义的便是全局函数啦。pushl $0:将常量0压入栈顶,push指令的一般形式为[pushl S],其中l代表数据格式为双字,S为源操作数,目的操作数默认为栈顶。asm_do_irq如下:NJU-OS-lab0(1)--初始化工作
 pushal  /*保存通用寄存器中的上下文环境*/  
/* 
在pushal指令中各寄存器的入栈顺序分别为: 
%eax->%ecx->%edx->%ebx->%esp->%ebp->%esi->%edi 
总共占用4*8=32字节 
*/  
与pushal相似的还有pushfl,将flags寄存器的值放入栈顶。NJU-OS-lab0(1)--初始化工作

这样经过pushl $0;pushal;一个TrapFrame就填充好了。之后入栈下%esp,调用irq_handle,接着出栈%esp,所以在%esp上加4,因为栈指针是往下生长的,入栈的话,栈指针要减。接着继续清栈,将通用寄存器出栈popal,最后就是中断向量号出栈,addl $4,%esp。然后就返回啦。
NJU-OS-lab0(1)--初始化工作 irq_handle根据传入的TrapFrame得到中断向量号tf->irq。注意啊,因为上面只有时钟中断irq0(向量号1000)被初始化了,所以我们按键盘时输出的tf->irq只会是-1,即用irq_empty来处理的。所以为了得到正确结果,这里我们是要自己将键盘输入中断也加进去的呀。


Intr初始化

最后一个啦,NJU-OS-lab0(1)--初始化工作

NJU-OS-lab0(1)--初始化工作暂时没找到相关资料,暂且不提这个啦。