为什么计算重启后时间依然正确?

TIME

  • 首发公号:Rand_cs

相信很多朋友接触计算机的时候都有这么一个疑惑,为什么计算机在关机断电,隔一段时间后重启的时间依然正确?

这背后的原因其实不难猜测,关机后重启的时间正确说明关机的情况下时钟仍然在工作,关机的情况下时钟仍然在工作,说明这个时钟应是有备用电源支持它工作的。

这个时钟叫做 R T C RTC RTC, R e a l Real Real T i m e Time Time C l o c k Clock Clock,可以“永久”的存放系统时间,也就是说它在系统关闭,没有电源的情况下也能继续工作。这里说的是没有计算机的电源(那大个儿电池),电子设备要工作那肯定还是需要电源的,这个电源是主板上的一个微型纽扣电池,计算机断电后实时时钟 R T C RTC RTC 就靠它来供电工作,持续对系统计时,这也就是为什么关机重新启动后时间还是正确的原因。

R T C RTC RTC 时间存放在 C M O S CMOS CMOS 中, C M O S CMOS CMOS, C o m p l e m e n t a r y Complementary Complementary M e t a l Metal Metal O x i d e Oxide Oxide S e m i c o n d u c t o r Semiconductor Semiconductor,互补金属氧化物半导体,别管这绕口名字的本身含义,只需要知道 C M O S CMOS CMOS 就是一 R A M RAM RAM 芯片,是 B I O S BIOS BIOS 的 M e m o r y Memory Memory。 B I O S BIOS BIOS 有各种的设置信息,这些信息没有存放在 B I O S BIOS BIOS 本身的芯片里,而是存放在 C M O S CMOS CMOS 里面。

对 C M O S CMOS CMOS 中的数据的读写是通过两个 I / O I/O I/O 端口来实现的,其中,端口 0 x 70 0x70 0x70 是一个字节的只写端口,用它来选择 C M O S CMOS CMOS 的寄存器,然后再通过 0 x 71 0x71 0x71 端口来读写选择的寄存器,也就是前面所说的 i n d e x / d a t a index/data index/data 的方式访问 C M O S CMOS CMOS 数据。下面就来看看 CMOS 中与时间相关的一些数据(寄存器):

CMOS 寄存器(index)

时间

  • 00 h 00h 00h,系统时间“秒数”字段
  • 02 h 02h 02h,系统时间“分钟”字段
  • 04 h 04h 04h,系统时间“小时”字段
  • 07 h 07h 07h,系统“日期”字段(0~31)
  • 08 h 08h 08h,系统“月份"字段(0~12)
  • 09 h 09h 09h,系统公元纪年的后两位(00 表示 2000,01 表示 2001,依次类推)

状态

  • 0 A h 0Ah 0Ah,状态寄存器 A A A
    • bit 7,0 表示目前可读时间,1 表示日期正在更新,稍后读取。
  • 0 B h 0Bh 0Bh,状态寄存器B
    • bit 2,0 表示使用 b c d bcd bcd 格式,1 表示二进制格式

上述就是与时间有关的一些寄存器,其他部分有兴趣的可以看后面的链接。另外在前面启动我们曾提到过 0 x F 0xF 0xF 号寄存器存放的是 s h u t d o w n shutdown shutdown c o d e code code, B S P BSP BSP 在启动 A P AP AP 时将 s h u t d o w n shutdown shutdown c o d e code code 设置为 0 x A 0xA 0xA 使得 A P AP AP 跳去执行 w a r m warm warm r e s e t reset reset v e c t o r vector vector 记录的引导程序,这部分详见启动理论部分@@@@@@@@

相关函数

读取CMOS寄存器

static uint
cmos_read(uint reg)         //0x70端口选择寄存器,0x71端口读出来
{
  outb(CMOS_PORT,  reg);   //选择寄存器:向70h端口写寄存器索引
  microdelay(200);         //等一会儿

  return inb(CMOS_RETURN); //从71h端口将数据读出来
}

这个函数就是向 0 x 70 0x70 0x70 端口写要读写的寄存器索引,然后再从 0 x 71 0x71 0x71 端口操作该寄存器,这里就是从 0 x 71 0x71 0x71 端口将数据给读出来

读取时间

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

这个函数就是调用 c m o s _ r e a d cmos\_read cmos_read 将存储在 C M O S CMOS CMOS 中的墙上时间给读取出来,这个函数之上还封装了一层 c m o s t i m e cmostime cmostime:

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
}

整个流程应该是很简单的,主要注意一下 B C D BCD BCD 码,所谓 B C D BCD BCD 码,就是使用 4 位二进制来表示十进制数 0 ∼ 9 0 \sim 9 0∼9, B C D BCD BCD 码也分为多种,最常见的就是 8421 8421 8421 码, 8421 8421 8421 表示各位的权重为 8 8 8 4 4 4 2 2 2 1 1 1,这很像二进制表示。举个例子来看看其中差别, B C D BCD BCD 码 0101 0101 0101 表示十进制 5,这似乎与 5 的二进制表示没什么不同,但如果表示十进制数 15, B C D BCD BCD 码为 0001 0001 0001 0101 0101 0101,而 15 的二进制表示为 1111 1111 1111,这就是它们之间的差别,十进制的每一位都用 4 位 二进制来表示。有了这认识之后来看如何 BCD 码如何转化为十进制:

unsigned char bcd2dec(unsigned char bcd)
{
      return ((bcd & 0xf) + ((bcd>>4)*10));
}

原理上很简单, B C D BCD BCD 码从低到高每四位表示着十进制数中的一位,其权重从低到高分别为个十百千,这里因为表示时间的数都很小,只用到了 8 位 B C D BCD BCD 码,所以前四位对应十位,需要乘 10,再加上个位(后四位)就是对应的十进制数字了。

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

先说敏感指令,敏感指的是对 I O IO IO 特权级敏感,这类指令有 in ins out outs sti cli 应该都很熟悉我就不解释它们有什么作用了。敏感指令涉及到了 I O IO IO 特权级,这部分我在进程讲述 T S S TSS TSS, I O IO IO 位图的时候详细说过,这里再简单回顾一下。这类指令的执行会检查 C P L ≤ I O P L CPL \le IOPL CPL≤IOPL,级检查当前特权级是否高于 E L F A G S ELFAGS ELFAGS 中记录的 I O IO IO 特权级,如果当前特权级大,直接执行没什么问题,如果当前特权级小,那么再检查 I O IO IO 位图对应的端口是否被禁止访问,如果没有禁止,则执行指令,如果禁止了,则抛出一般保护性错。而再 x v 6 xv6 xv6 里面, I O P L IOPL IOPL 设置的是 0,没有使用 I O IO IO 位图即默认禁止所有端口,所以 x v 6 xv6 xv6 里的敏感指令只能在内核态下运行

而特权指令则是只有 C P L = 0 CPL=0 CPL=0 即在内核态下才能执行的指令,这类指令都关乎中断,需要最高权限才能执行,有很多,咱们随便看看几个比较熟悉的: l g d t lgdt lgdt 加载 G D T GDT GDT 用的, l t r ltr ltr 加载任务寄存器,与控制寄存器相关的 m o v mov mov 指令, h l t hlt hlt 停机指令等等,其他特权指令有兴趣的见 i n t e l intel intel 手册卷三 5.9 5.9 5.9,手册在我公众号后台回复 手册 即可获取链接。

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

上一篇:关于TCP 半连接队列和全连接队列


下一篇:数电基础---MOS管,三极管和门电路