TIME
相信很多朋友接触计算机的时候都有这么一个疑惑,为什么计算机在关机断电,隔一段时间后重启的时间依然正确?
这背后的原因其实不难猜测,关机后重启的时间正确说明关机的情况下时钟仍然在工作,关机的情况下时钟仍然在工作,说明这个时钟应是有备用电源支持它工作的。
这个时钟叫做 , ,可以“永久”的存放系统时间,也就是说它在系统关闭,没有电源的情况下也能继续工作。这里说的是没有计算机的电源(那大个儿电池),电子设备要工作那肯定还是需要电源的,这个电源是主板上的一个微型纽扣电池,计算机断电后实时时钟 就靠它来供电工作,持续对系统计时,这也就是为什么关机重新启动后时间还是正确的原因。
时间存放在 中,, ,互补金属氧化物半导体,别管这绕口名字的本身含义,只需要知道 就是一 芯片,是 的 。 有各种的设置信息,这些信息没有存放在 本身的芯片里,而是存放在 里面。
对 中的数据的读写是通过两个 端口来实现的,其中,端口 是一个字节的只写端口,用它来选择 的寄存器,然后再通过 端口来读写选择的寄存器,也就是前面所说的 的方式访问 数据。下面就来看看 CMOS 中与时间相关的一些数据(寄存器):
CMOS 寄存器(index)
时间
-
,系统时间“秒数”字段 -
,系统时间“分钟”字段 -
,系统时间“小时”字段 -
,系统“日期”字段(0~31) -
,系统“月份”字段(0~12) -
,系统公元纪年的后两位(00 表示 2000,01 表示 2001,依次类推)
状态
-
,状态寄存器 -
bit 7,0 表示目前可读时间,1 表示日期正在更新,稍后读取。 -
,状态寄存器B -
bit 2,0 表示使用 格式,1 表示二进制格式
上述就是与时间有关的一些寄存器,其他部分有兴趣的可以看后面的链接。另外在前面启动我们曾提到过 号寄存器存放的是 , 在启动 时将 设置为 使得 跳去执行 记录的引导程序,这部分详见启动理论部分(整理版)
相关函数
读取CMOS寄存器
static uint
cmos_read(uint reg) //0x70端口选择寄存器,0x71端口读出来
{
outb(CMOS_PORT, reg); //选择寄存器:向70h端口写寄存器索引
microdelay(200); //等一会儿
return inb(CMOS_RETURN); //从71h端口将数据读出来
}
这个函数就是向 端口写要读写的寄存器索引,然后再从 端口操作该寄存器,这里就是从 端口将数据给读出来
读取时间
struct rtcdate {
uint second;
uint minute;
uint hour;
uint day;
uint month;
uint year;
};
static void fill_rtcdate(struct rtcdate *r) //读取时间
{
r->second = cmos_read(SECS); //秒数
r->minute = cmos_read(MINS); //分钟
r->hour = cmos_read(HOURS); //小时
r->day = cmos_read(DAY); //日期
r->month = cmos_read(MONTH); //月份
r->year = cmos_read(YEAR); //年份
}c
这个函数就是调用 将存储在 中的墙上时间给读取出来,这个函数之上还封装了一层 :
void cmostime(struct rtcdate *r)
{
struct rtcdate t1, t2;
int sb, bcd;
sb = cmos_read(CMOS_STATB); //读取状态寄存器B
bcd = (sb & (1 << 2)) == 0; //0是BCD格式,为默认值,1是二进制值
// make sure CMOS doesn't modify time while we read it
for(;;) {
fill_rtcdate(&t1); //读取时间
if(cmos_read(CMOS_STATA) & CMOS_UIP) //如果时间正更新,稍后读取
continue;
fill_rtcdate(&t2);
if(memcmp(&t1, &t2, sizeof(t1)) == 0) //如果两者一样,break,如此操作应是为了确保时间准确
break;
}
// convert
if(bcd) {
#define CONV(x) (t1.x = ((t1.x >> 4) * 10) + (t1.x & 0xf)) //BCD码转10进制
CONV(second);
CONV(minute);
CONV(hour );
CONV(day );
CONV(month );
CONV(year );
#undef CONV
}
*r = t1;
r->year += 2000; //读取出来的year位公元纪年的后两位,所以加上2000
}
整个流程应该是很简单的,主要注意一下 码,所谓 码,就是使用 4 位二进制来表示十进制数 , 码也分为多种,最常见的就是 码, 表示各位的权重为 ,这很像二进制表示。举个例子来看看其中差别, 码 表示十进制 5,这似乎与 5 的二进制表示没什么不同,但如果表示十进制数 15, 码为 ,而 15 的二进制表示为 ,这就是它们之间的差别,十进制的每一位都用 4 位 二进制来表示。有了这认识之后来看如何 BCD 码如何转化为十进制:
unsigned char bcd2dec(unsigned char bcd)
{
return ((bcd & 0xf) + ((bcd>>4)*10));
}
原理上很简单, 码从低到高每四位表示着十进制数中的一位,其权重从低到高分别为个十百千,这里因为表示时间的数都很小,只用到了 8 位 码,所以前四位对应十位,需要乘 10,再加上个位(后四位)就是对应的十进制数字了。
上述就是获取读取 时间的操作,其实很简单啊,操作 的 和 端口再作转化就完事了。这些操作有特权级限制的,因为涉及到了 操作指令 in
和 out
。对于指令的分类除了常见的什么数据转移指令,算数类指令等等,还有一些特殊的指令分类:特权指令和敏感指令,这部分本应该在启动理论那一块儿就该讲述的,结果那一块的零散知识点太多,写到最后搞忘了,实在抱歉这里补上。
先说敏感指令,敏感指的是对 特权级敏感,这类指令有 in
ins
out
outs
sti
cli
应该都很熟悉我就不解释它们有什么作用了。敏感指令涉及到了 特权级,这部分我在深入理解进程之数据结构篇讲述 , 位图的时候详细说过,这里再简单回顾一下。这类指令的执行会检查 ,级检查当前特权级是否高于 中记录的 特权级,如果当前特权级大,直接执行没什么问题,如果当前特权级小,那么再检查 位图对应的端口是否被禁止访问,如果没有禁止,则执行指令,如果禁止了,则抛出一般保护性错。而在 里面, 设置的是 0,没有使用 位图即默认禁止所有端口,所以 里的敏感指令只能在内核态下运行
而特权指令则是只有 即在内核态下才能执行的指令,这类指令都关乎中断,需要最高权限才能执行,有很多,咱们随便看看几个比较熟悉的: 加载 用的, 加载任务寄存器,与控制寄存器相关的 指令, 停机指令等等,其他特权指令有兴趣的见 手册卷三 ,手册在我公众号后台回复 手册
即可获取链接。
好了本文就到这里吧,有什么问题还请批评指正,也欢迎大家来同我探讨交流一起学习一起进步。
往期推荐:
原文始发于微信公众号(Rand):为什么计算机关机重启后的时间始终正确?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/41670.html