了解计算机底层原理的好处在于,你不必再去猜测一个行为可能的实现机制,而是你可以彻底地看到它实际上是如何发生的。你可以根据底层的原理和想要解释的行为,进行更精细地「如何实现」的推断。而不必像以往那样,只能通过需要解释的行为,开始 YY 可能的机制。

0x01

例如,对于 thread 和所在 process 的 shared data 的问题。如果只是从 YY 角度去考虑,那么你很可能会纠结:如何将 process 上的 shared data 拷贝到每个 thread,又或者是如何 copy 这个 shared data 的 reference。

但是,有了 process 和 thread 的 virtual memory layout,这个问题就特别容易回答。由于每个 thread 有各自独立的 register,那么,对于同样一条 ++i 的 code,就可以被拆分到各自的 register 去做 3-step instructions:

  • read data from ‘shared data memory virtual address‘ to thread local register.
  • add 1 to thread local register.
  • write the added register value back to shared data.

如此,shared data 的 copy 问题,就一清二楚了。对于每个 thread 的运行来讲,它需要的仅有两部分:

  • instruction:从 memory 来 copy data。
  • register:从共同的 virtual memory code part 来获取。

也即是,这个 copy data 的过程,根本不是从 process 的 memory,copy 到 thread 的 memory。而是从 process 的 memory,copy 到 thread 的 CPU 部分,即:register。所以 memory 的纠结,也就不再存在。

而最后的写入,则是从 register 部分 copy back 到 shared data。有了这样精细的拆分,你不必再去猜测 thread/process 的 data manage 机制,你可以非常有信心地精准论述。

0x02

想来,很多时候对 instruction 执行,和相应 data 的获取、参与计算搞不清楚的根本原因在于:没有 register 的概念。

错误地以为只要进入了 memory,就会参与 CPU 的计算。这中间其实还差了一步 copy to register 的过程。

并且,instruction 和 data 是有先后顺序的。先是 instruction 扔进 register。再来是根据 instruction 的内容,再将相应的 data 扔到其它的 register。进而,它们共同参与计算。

有了这个中间步骤,很多复杂而玄妙的问题,都能够迎刃而解。

0x03

想来,很多人通常无法意识到的、总会不由自主地把 assemble instruction 的操作当做玄学的原因在于:为什么对于 c = a + b 这样的操作,非得要引入一个中间变量来保存 a + b 的值,再把计算结果赋值给 c?这非常奇怪。

既然都是 assemble instruction 了,那自然是可以获得 a、b、c 的各种 meta info。如此,为何要多此一举地引入一个中间变量?直接保存到 c 里面,不是更为简洁、高效、节省空间吗?

其实,这里大部分材料没有点名的地方在于,这里并非是引入一个中间变量,而是对于 CPU 来讲,一切的操作都必须借助于 register 来完成,而无法直接通过操作 memory 来完成。如果是直接可以拿 memory 的数据就开始运算,那确实是多此一举。

可是,如果所有的运算和数据,都必须借助 register 拿给 CPU 的 ALU(可以将 register 理解为计算时的草稿纸),那么,这一切也就自然而然了。你首先得把 a、b 变量中的某个值扔给一个 register,然后 ALU 通过直接使用这个 register 的值和 memory 中的另一个值来完成一次计算,即: add %0x23, %eax 的形式,并将结果保存在 register 上。

所以,这里引入中间变量的必要性,其实是因为 CPU 必须借助 register 来完成运算。自然,这个 register 就充当了所谓的中间变量。

0x04

既然可以使用 add %0x23, %eax 的形式,让其中一个变量的值直接来自于 memory,为啥不能让两个值都来自于 memory?

(事实上,确实是存在这样的 instruction set 的,通常是通过组合借助 register 的操作来实现的,如「复杂指令集」,而非「精简指令集」。)

我想,这里的一个可以 YY 的设计考虑是:

  1. add 操作本身其实涉及写数据,也即是把计算结果至少要写到其中一个操作数据上。
  2. 如此,那么 add 操作就涉及既读又写。
  3. 而显然一条 instruction 是 atomic 的操作,如此就会引发一个:对涉及的 memory data 加锁等待的问题。(硬件当然可以实现,但只是说要不要这样做设计,是否拆分成更简单的操作做组合更好?)

但是,如果引入了 register 这样的 CPU kernel local 的变量,那么,「读」和「写」就能够分离。在计算出结果后,可以灵活自由地继续切换,仅仅在写的那一步做数据改变的操作。