为什么计算机关机重启后的时间始终正确?

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 t1t2;
  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,再加上个位(后四位)就是对应的十进制数字了。

上述就是获取读取 时间的操作,其实很简单啊,操作 端口再作转化就完事了。这些操作有特权级限制的,因为涉及到了 操作指令 inout。对于指令的分类除了常见的什么数据转移指令,算数类指令等等,还有一些特殊的指令分类:特权指令和敏感指令,这部分本应该在启动理论那一块儿就该讲述的,结果那一块的零散知识点太多,写到最后搞忘了,实在抱歉这里补上。

先说敏感指令,敏感指的是对 特权级敏感,这类指令有 in  ins  out  outs  sti  cli  应该都很熟悉我就不解释它们有什么作用了。敏感指令涉及到了 特权级,这部分我在深入理解进程之数据结构篇讲述 位图的时候详细说过,这里再简单回顾一下。这类指令的执行会检查 ,级检查当前特权级是否高于 中记录的 特权级,如果当前特权级大,直接执行没什么问题,如果当前特权级小,那么再检查 位图对应的端口是否被禁止访问,如果没有禁止,则执行指令,如果禁止了,则抛出一般保护性错。而在  里面, 设置的是 0,没有使用 位图即默认禁止所有端口,所以 里的敏感指令只能在内核态下运行

而特权指令则是只有 即在内核态下才能执行的指令,这类指令都关乎中断,需要最高权限才能执行,有很多,咱们随便看看几个比较熟悉的: 加载 用的, 加载任务寄存器,与控制寄存器相关的 指令, 停机指令等等,其他特权指令有兴趣的见 手册卷三 ,手册在我公众号后台回复 手册 即可获取链接。

好了本文就到这里吧,有什么问题还请批评指正,也欢迎大家来同我探讨交流一起学习一起进步。

往期推荐:

给操作系统捋条线

中断理论部分(整理重制版)

中断机制代码篇(重制版)

原文始发于微信公众号(Rand):为什么计算机关机重启后的时间始终正确?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/41670.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!