文章目录
写在前面:自制操作系统Gos 第二章第十篇:主要内容是如何协调操作系统中各部件工作频率的组件定时器
Gos完整代码:Github
时钟
之前,我其实在中断的实现机制那篇博客中提到了时钟中断这个概念,而且它被放在RPQ0这个最为重要的位置。那它是干什么的呢?为什么会这么重要呢?
我们可以想想在平时生活中,我们如何跟其他人进行工作中的同步呢?
以下为虚拟场景:
“小龚,这个任务下周一前解决”
“好的”
其实主要就是依靠时间来完成这个过程,计算机系统中也一样,为了让所有设备之间的通信尽然有序,就必然有一个大家都遵守的时间规约,而这个就被称之为时钟。它并不是计算机处理速度的衡量,而是一种使设备相互配合而避免发生冲突的节拍。
一般来说时钟会分为两种:
- 内部时钟:一般由晶体振荡器产生(晶振),位于主板上。其频率经过分频之后就是主板的外频,Intel将此外频×某个倍数(倍频)就被称之为主频。一般用作CPU中的内部元件,如运算器或者控制器的工作时序。主要用于控制、同步内部工作过程的。
- 外部时钟:其是至处理器和外部设备或外部设备之间通信采用的时序,粒度比较大,一般是毫秒或者秒级别。
其中内部时钟一般使无法改变的,我们想要在操作系统中协调组件就只能对外频进行更改。其实也有两种手段:
- 软件:通过类似于让CPU空转来达到目的:
int clock_times = 1000;
while(clock_times>=0)
{
clock_times--;
)
- 硬件:一般采用定时器,定时器会定时发信号,当达到所计数的时间,计数器就可以自动发出一个信号,这个信号就会产生一个时钟中断。操作系统开启中断后,可以编写时钟中断的处理程序,一般这个最主要的作用其实就是在进程间切换啦,也就是大名鼎鼎的时间片轮转法!
所以,综上来看:软件控制外部时钟太浪费CPU资源啦,我们最好的方法就是采用硬件控制外部时钟。
定时器8253
定时器主要使分为不可编程定时器和可编程定时器两种。我们主要用到的就是可编程定时器(programmable interval timer,PIT)。
注:
不可编程定时器的有关资料我也没有找到,由同学找到可以在评论区@我一下。
常见的定时器有Intel 8253/8254/82C54A,我们只需要会使用最简单的8253就可以了。
定时器计时
定时器计时方式主要有两种:
- 正计时:每一次时钟脉冲发生的时候,将当前计数值+1,直到加到一个阈值为值。代码表示如下:
int clock_times = 0;
int end_times = 1000;
while(clock_times <= end_times)
{
clock_times++;
}
- 倒计时:先设定好定时器的值,每一次时钟脉冲发生的时候将计数值-1,直到为零。代码表示如下:
int clock_times = 1000;
while(clock_times >= 0)
{
clock_times--;
}
其实倒计时是我们用的最多的,比如时间片到期、闹钟啊等等。
8253入门
8253内部有三分独立的计时器,其分别对应端口0x40~0x42
。这三个计时器相互之间不依赖,独立工作,每个都有自己一套寄存器资源(一个16位计数初值寄存器+一个计数器执行部件+一个输出锁寄存器),8253结构如下:
- 数据总线寄存器:直接和CPU相连,是8253和CPU交互的必经之路。
读/写逻辑寄存器
- A0和A1:用来两位来表示计数器0~2 。
- RD#:表示读控制信号,由CPU输入,其位如果为1,表示A0、A1表示的那个计数器进行读操作。
- WR#:写控制信号,由CPU输入,其位如果为0,表示A0、A1表示的那个计数器进行写操作
-
CS#:禁止信号,表示A0和A1表示的计数器不操作。
计数器
我们首先来看一下计数器的工作原理和组成:
- CLK:表示时钟输入信号,其实也就是计数器自己工作的节拍。每当引脚收到一个时钟信号,减法计数器就会将计数值减一。
- GATE:表示门控输入信号,在部分工作方式下用于控制计数器开始计数。
- OUT:表示计数器输出信号,当计数值为0,也就是定时工作结束,这个时候在OUT新教输入相应的信号。
- 计数初值寄存器:用来保存计数器的初始值。
- 减法计数器:计数器的执行部件,一开始工作会在计数初值寄存器中读值。之后每收到CLK传来的脉冲信号之后,就会将当前值-1,之后同步当前值在输出锁寄存器。
- 输出锁寄存器:保存当前值,方便进程读取。
当计数开始前,数值会通过总线保存到计数初值寄存器,每当CLK引脚收到一个脉冲信号就会通过减法计数器这个执行部件将计数初值减一,同时将当前值保存在输出锁寄存器。当计数值减到0,表示定时工作结束,这个时候通过OUT引脚发出信号。
而我们这里可以看到8253里面有三个计数器,它们的作用各不相同:
计数器 | 端口 | 作用 |
---|---|---|
计数器 0 | 0x40 | 专用于产生实时时钟信号,采用工作方式3 |
计数器 1 | 0x41 | 专用于DRAM的定时刷新控制 |
计数器 2 | 0x42 | 专用于内部扬声器发生不同声调的声音 |
控制字寄存器
控制字寄存器也被称之为模式控制寄存器,其操作端口是0x43
。在控制器中保存的内容被称为控制字,控制字用来设置所指定的计数器的工作方式、读写格式以及数制。刚刚讲过的三个计数器是独立工作的,每个计数器都必须明确自己的控制模式才知道怎样去工作,而它们的工作模式其实也就是这个控制字寄存器来设定的。
首先,我们来看一下控制字的结构:
-
SC1和SC0:选择是哪个计数器。
-
RW1和RW0:控制读写方式,详情如下表:
-
M2~M0:选择不同的工作方式,总共有6种,
000
表示方式0 -
BCD:数制位,其为1表示BCD码;其为0表示二进制
8253工作方式
详情如下表:
8253初始化
8253开始工作的方法很见到,只用通过控制字寄存器选择哪个计数器,指定其控制模式,再写入初值就可以啦:
- 往控制字寄存器端口0x43写入控制字
- 在所指定使用的计数器端口写入计数初值
/*
* @brief 计时器属性以及初始值设置
* @param counter_port 计时器端口
* @param counter_no 计时器标号
* @param rwl 读写方式
* @param counter_mode 计时器模式
* @param counter_value 计时器初始值
*/
static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value)
{
//往控制字寄存器0x43写入控制字
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
//先写入低8位
outb(counter_port, (uint8_t)counter_value);
outb(counter_port, (uint8_t)counter_value >> 8);
}
定时器初始化
定时器初始化主要有两个步骤:
- 8253进行初始化
//这里写入的是0011 0100 -->表示0x40端口,计数器选择0,先读写低字节再读写高字节,工作方式为第二种
frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
注:
这里中断的初始值被设置为11932,这是我们想要中断信号的频率为100Hz(每秒发生100次中断)。
即1.19318Mhz(计数器的频率)/100 = 11932.
- 注册时间中断处理函数
register_handler(0x20, intr_timer_handler);
这样每当中断0x20
发生,即8253发送过来一个信号就会去调用中断处理函数。中断处理函数主要做的工作就是将当前进程的时间片-1,如果时间片无了,就从就绪队列中取出一个进程,执行取出的进程。这就是大名鼎鼎的时间片轮转法。
/*
* @brief 时钟的中断处理函数
*/
static void intr_timer_handler(void)
{
struct task_struct *current_thread = running_thread();
//检查是否栈溢出,0x20000314是个魔数,我生日,随便设置其他数也可以
ASSERT(current_thread->stack_magic == 0x20000314);
current_thread->elapsed_ticks++; //记录此线程占用的cpu时间数
ticks++; //内核时间++
if (current_thread->ticks == 0)
{
schedule();
}
else
{
current_thread->ticks--;
}
}
参考文献
[1] 操作系统真相还原
[2] 百度文库.8253内部结构与工作方式