阅读本文大约需要 13 分钟。 这篇文章我想和你聊一聊「时间」这个话题。 时间总是在不经意间流逝,我们在写代码时,也经常会调用「时间 API」,你有思考过这背后的原理吗? 关于时间的问题还有很多,例如: 为什么计算机的时间有时候「走不准」? 计算机究竟是怎么「自动校准」时间的? 我们经常看到的 UTC 时间,到底是什么? 我们在新闻上看到的「北京时间」,真的来自北京吗? 这篇文章,我们就来揭秘时间背后的秘密。 这篇文章非常有意思,希望你可以耐心读完。 时间为什么总是走“不准”? 你肯定遇到过这样的场景,家里买了一个钟表,时间一长,就会发现它走得「不准」了。 又或者,一台长时间不使用的电脑,它的时间也会发生偏差。 遇到这些情况,你可能会不以为然。时间不准,那我们就「人工」调准它。 但你有没有停下来想一想,为什么它们的时间会越走越不准呢? 要回答这个问题其实不难,我们只需要搞清楚,它们的时间是怎么来的。 钟表和计算机内部都有一个叫做「晶体振荡器」的东西,给它加上电压,它就会以固定的频率振动。但这个振动频率的「稳定性」,取决于它的制造工艺,以及外界环境的影响。 出于成本的考虑,钟表的制作工艺没那么高,所以它更容易有误差。而电脑制造工艺虽然比较高,但它内部的晶体振荡器也会受到「温度」变化带来的影响,在工作过程中,也会有产生误差。 虽然它们的误差很小,但日积月累下来,误差就越来越明显。 因此,我们现在使用的计算机,都有「自动校准」时间的功能。但是如何校准呢? 如何校准时间? 很简单,只要你把电脑连上了「网络」,你会发现,它会自动与「网络时间」保持同步。 可问题是,这个「网络时间」哪儿来的? 我猜你大脑的第一反应是,每台电脑肯定配置了一个「时间服务器」,之后这台电脑会与服务器定时同步,自动校准。 没错,确实是这样,不光是电脑,我们平时使用的手机、平板、智能手表等电子设备,只要能连接网络,都会自动同步网络时间。 那继续追问,这个「时间服务器」的时间就一定是准的吗? 理论来讲,它应该也是一台计算机,难道它不会遇到我们前面说的问题吗? 此外,这个网络时间究竟是怎么「同步」到我们的电脑上的? 你可能会说,那肯定是通过网络数据包。 问题又来了,网络传输数据也是有「延迟」的,同步服务器时间,不还是存在误差吗? 环环相扣,像一个俄罗斯套娃,很难解释清楚。 要想彻底搞清楚这些问题,就要深入到时间的「源头」来寻找答案。 时间是怎么来的? 时间是一个非常抽象的概念,多少年来,吸引着无数科学家、物理学家、甚至哲学家花费毕生精力去解释时间的本质是什么,从宇宙大爆炸到时空相对论,从黑洞到量子力学,都能看到关于时间这个问题的身影。 这里我们不探讨高深莫测的学术知识,只把目光放聚焦在计算机这个很小的范畴内。但要想清楚解释这个问题,也并非想的那么简单。 我们从最简单的开始说起。 想要知道时间是怎么被定义的,首先要知道「天」是怎么来的? 答案是:观察太阳。 由于地球的「自转」,人们可以看到日出日落,人们日出而作,日落而息,所以就把这一周期现象定义为「天」。 地球除了自转,还在围绕太阳公转,所以公转一周就被定义为一「年」。 从这些现象就能看出来,很早之前的人们,是以「天文现象」来确定时间的。 再后来,人们为了把时间定义得更「精确」,就把一天平均划分为 24 等份,这就是「时」。 同样地,把 1 小时划分 60「分钟」,1 分钟划分为 60「秒」。… Continue reading 计算机时间到底是怎么来的?
Category: 操作系统
CPU到底如何切换线程的?
计算机系统中有很多程序员习以为常但又十分神秘的存在:函数调用、系统调用、进程切换、线程切换以及中断处理。 函数调用能让程序员提高代码可复用性,系统调用能让程序员向操作系统发起请求,进程线程切换让多任务成为可能,中断处理能让操作系统管理外部设备。 这些机制是计算机系统中的基石,可是你知道这些机制是如何实现的吗? 这篇文章将告诉你答案,其背后的实现如此优雅且一致。 速度与激。。寄存器 你有没有想过,CPU为什么需要寄存器? 原因很简单:速度。 通常CPU可以在一个时钟周期内访问一次寄存器,CPU访问内存的速度大概要比访问寄存器慢100倍左右。 因此如果CPU没有寄存器而完全依赖内存的话,那么计算速度将比现在慢的多。 作为程序员来说,当我们使用高级语言编写的程序时,其操作的数据都存放在内存中,而对于负责运算类的机器指令来说其操作的数据都存放在寄存器中。 实际上寄存器和内存没有什么本质的区别,都是用来存储信息的。 当然,除了临时保存中间计算结果之外,还有很多有趣的寄存器。根据用途,寄存器有很多类型,但是,我们感兴趣的有以下几种寄存器。 栈寄存器:Stack Pointer 函数在运行时都有一个运行时栈,对于栈来说最重要的信息就是栈顶,栈顶信息就保存在栈寄存器中,stack pointer,通过该寄存器就能跟踪函数的调用栈。 最为程序员我们知道,函数在运行时会有一块独立的内存空间,用来保存函数内定义的局部变量、传递的参数以及返回值信息等,这块独立的内存空间就叫栈帧,随着函数调用层次的加深,栈帧也随之增加;当函数调用完成后栈帧由按照相反的顺序依次减少,这些栈帧就构成了栈区。 函数的运行时栈信息是关于程序运行状态最重要的信息之一。 那么其它的之一呢? 指令地址寄存器:Program Counter 这类寄存器的名称比较多,基于历史原因,大部分将其称为Program Counter,PC,即我们熟悉的程序计数器;在x86下则被称为Instruction Pointer,IP,怎么称呼不重要,重要的是理解其作用。在本文中统一将其称为PC寄存器。 我们都知道,程序员用高级语言编写的程序最终通过编译器生成最终的机器指令,那么一个问题就是在茫茫的机器指令海洋中,CPU怎么知道该去执行哪条机器指令呢? 原来,奥秘就藏在指令地址寄存器中。 程序在启动时会把机器指令的首地址写入到PC寄存器中,这样CPU需要做的就是根据PC寄存器中的地址去内存中取出指令。 通常来说,指令都是顺序执行的,也就是说PC寄存器中的值不断的+1,但对于一些涉及控制转移的机器指令来说,这些指令会把一个新的指令地址放到PC寄存器中,这包括分支跳转——也就是if语句、函数调用以及返回等。 控制了CPU的PC寄存器就掌握了CPU的航向,机器指令自己会根据执行状态指挥CPU接下来该去执行哪些指令,这才是真正的自动驾驶,非常amazing有没有! 指令地址寄存器是关于程序运行时状态另一个最重要的信息之一。 状态寄存器:Status Register CPU内部除了上述两类寄存器外,还有一类状态寄存器,Status Register;在x86架构下被称为FLAGS register,ARM架构下被称为application program status register,以下统称状态寄存器。 从名字也能看出来,该寄存器是保存状态信息的,有什么有趣的状态信息呢? 比如对于涉及到算术运算的指令来说,其在执行过程中可能会产生进位,也可能会溢出,那么这些信息就保存在状态寄存器中。 除此之外,你肯定听说过程序的执行一般有两种模式:内核态和用户态。 对于大部分的程序员其编写的应用程序运行在用户态,在用户态下不能执行特权指令,比如你没办法写一个程序直接去控制系统中的各种硬件资源。 而在内核态下,CPU可以执行任意的特权指令,内核就工作在内核态,因此内核可以掌控一切。关于用户态内核态完整的阐述参见博主深入理解操作系统第2章,关注公众号码农的荒岛求生并回复操作系统即可。 那么我们怎么知道当前程序运行在哪种状态呢? 答案就在CPU内部的状态寄存器中,该寄存器中有特定的比特位来标记当前CPU正工作在哪种模式下。 现在你应该知道寄存器的重要作用了吧。 上下文:Context 通过这些寄存器,你可以知道程序运行到当前这一刻时最细粒度的切面,这一时刻这些寄存器中保存的所有信息就是我们通常所说的上下文,context。 上下文的作用是什么呢? 只要你能拿到一个程序运行时的上下文并保存起来,那么你可以随时暂停该程序的运行,也可以随时利用该信息恢复该程序的运行。 为什么要保存和恢复上下文信息呢?原因就在于CPU的个数是有限的,这就意味一个CPU可能会执行多个进程,即这些进程要共享该CPU资源,更具体的是CPU的计算资源和这里所说的各种寄存器。 这是实现函数调用、系统调用、进程切换、线程切换以及中断处理的基本机制。 而程序在运行过程中逃不出函数调用、系统调用、进程切换、线程切换以及中断处理这几项操作,由此可见上下文信息的保存和恢复在计算机科学中重要的作用。 那么上下文信息又该如何保存呢?保存到哪里呢?又该怎么恢复呢?函数调用、系统调用、进程切换、线程切换以及中断处理又是怎样实现的呢?… Continue reading CPU到底如何切换线程的?
带你深入理解内存对齐最底层原理
相信绝大多数的人都了解内存对齐,对齐后性能高。但是其最最底层的原理是啥呢? 有的人可能会说,因为高速缓存的工作机制。读者你很聪明,这是原因之一。但我今天想挖的是更底层一点的原理,让我们去内存的物理构成里找找答案! 内存物理结构 我们来了解一下内存的物理构造,一般内存的外形图片如下图: 图1 内存外形图 一个内存是由若干个黑色的内存颗粒构成的。每一个内存颗粒叫做一个chip。每个chip内部,是由8个bank组成的。其构造如下图: 图2 chip内部构成 而每一个bank是一个二维平面上的矩阵,前面文章中我们说到过。矩阵中每一个元素中都是保存了1个字节,也就是8个bit。 图3 bank内部构成 内存编址方式 那么对于我们在应用程序中内存中地址连续的8个字节,例如0x0000-0x0007,是从位于bank上的呢?直观感觉,应该是在第一个bank上吗?其实不是的,程序员视角看起来连续的地址0x0000-0x0007,实际上是位于8个bank中的,每一个bank只保存了一个字节。在物理上,他们并不连续。下图很好地阐述了实际情况。 图4 连续8字节在内存中实际分布 你可能想知道这是为什么,原因是电路工作效率。内存中的8个bank是可以并行工作的。如果你想读取地址0x0000-0x0007,每个bank工作一次,拼起来就是你要的数据,IO效率会比较高。但要存在一个bank里,那这个bank只能自己干活。只能串行进行读取,需要读8次,这样速度会慢很多。 结论 所以,内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个bank中各自读取一个字节拼起来的。从内存的0地址开始,0-7字节的数据可以一次IO读取出来,8-15字节的数据也可以一次读取出来。 换个例子,假如你指定要获取的是0x0001-0x0008,也是8字节,但是不是0开头的,内存需要怎么工作呢?没有好办法,内存只好先工作一次把0x0000-0x0007取出来,然后再把0x0008-0x0015取出来,把两次的结果都返回给你。CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。这样你的应用程序就会变慢,算是计算机因为你不懂内存对齐而给你的一点点惩罚。 扩展1:事实上,编译和链接器会自动替开发者对齐内存的,尽量帮你保证一个变量不跨列寻址。但是他不能做到十分完美。 扩展2:其实在内存硬件层上,还有操作系统层。操作系统还管理了CPU的一级、二级、三级缓存。不知道你有没有印象,我们前面的文章说过高速缓存里的Cache Line是64字节,它是内存IO单位的8倍,不会让内存IO浪费。