【第 1 周】深入理解计算机系统共读心得体会

请大家以回帖的方式将你在共读第一周的心得体会用你自己的话表达出来。

样例:

  1. 所在小组:第一组
  2. 组内昵称:加油
  3. 你的心得体会(可以是基于不同知识点进行,但是每人每周都只发一个帖子)
  • 一段自己的阐述
  • 第二-N段自己的阐述
4赞
  1. 所在小组:第七组
  2. 组内昵称:杨文
  3. 心得体会
  • Amdahl 定律,提升部分性能时,并不一定能产生最大价值,它还取决于这部分在系统中的重要性,以及很关键的点:加速度。怎么理解这个加速度呢?你所作用的可能是主要部分,但是它相较于系统的加速度如果要明显小的话,那么他就可能价值变小了。(这一点可能是我们很少关注到的,而且它也并不一定适用普通场景,文中定律所说,我只能说那是理论上的完整解读,但是在实践中我还是相信优化2/8原则是可以足够的,也能产生效益的),文中说我们想显著加速整个系统,我们必须提升全系统中相当大的部分的速度。(这个点也并不一定是适用于现在的大部分场景的,不知道各位对此又是如何看待的呢?
  • 对于性能提升的表示方法,我们要用倍速,百分比比较难以理解。(但是统计上好像还是用百分比比较? benchstat 的结果就是正负 210%)
  • 如何阅读第二章?非常核心重要的一章,可以说你不管如何精简,这一章你都不得不读懂它。包括其中的一些数学形式,证明,推导。究竟应该怎么读呢?
    • 先读完一遍,不纠结于那些数学证明和推导
    • 再读第二遍,不纠结数学证明和推导,但是必须去试着做一些练习题
    • 再读第三遍,尝试着理解那些数学证明和推导(掌握高中代数知识的人都具备理解他们。) - 我感觉我好像高中未毕业:sweat_smile:
  1. 所在小组:第三组
  2. 组内昵称:wyhqaq
  3. 心得体会
  • 系统的硬件组成:
    • 一条总线贯穿整个系统。硬件通过实现通用的对应的适配器/控制器连接到总线上,各个部件对数据传输率的不同要求,总线又分成I/O总线,内存总线,系统总线等,用不同层次的总线进行互连,以适应各自的特性与需求。
      (软件系统可以抽象成这种结构吗?假如微服务采用这种结构,首先消息队列代替总线,数据库假设成硬盘,然后各种服务解耦成不同的设备,那设备控制器呢?这层的功能应该是为了适应不同厂商的设备提供一个统一的接入口,但是服务能这样再抽象出来吗?比如存储服务控制器,下面包含数据库存储、ceph存储等, 这种结构很像k8s了。所以软件和硬件设计有相似之处,某些方面是可以相互借鉴学习的)
  • 高速缓存至关重要:
    • 那是不是代表着没缓存就系统就无法work?我理解缓存应该在能预料的范围里再上。为什么,第一增加系统复杂性;第二增加维护成本;第三数据一致性。
    • 进程切换:
      • 进程切换需要保存当前进程的上下文(状态信息),这部分需要内核代码来参与,涉及到很多寄存器数据改变,开销很大。操作系统对进程最大数一般有限制(cat /proc/sys/kernel/pid_max && sysctl -a |grep kernel.pid_max)。超线程实际上部分解决这个问题,通过多加套pc和寄存器,减少这种切换。
    • Amdahl定律:
      • 要想显著加速整个系统,必须提升全系统中相当大的部分的速度。木桶原理是说要注意最短板,最短板就是耗时最久,最拖累系统整体的部分,所以两者宏观上表达的都是要抓住问题核心,从核心入手。
    • 计算机系统中抽象的重要性:
      • 不言而喻啊~~自己也要提高抽象能力,多观察一些优美的设计,想想为什么它要这么做,好处坏处。
    • 寻址和字节顺序:
      • 为什么要分小端模式和大端模式?
        参考了这两篇
        why-are-both-little-and-big-endian-in-use
        理解字节序
        计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
        但是也有不同观点,所以不用纠结了,就是个历史原因。
  1. 所在小组:静默组
  2. 组内昵称:Han
  3. 心得体会

计算机表示和保存数据,实际是由0或1的位(bit)组成,8位为一个字节(byte),程序源代码本质上也是一个个byte组成。

书中的hello.c全部由ascii字符组成,ascii字符集中的字符由一个字表示(0到127),共128个字符。
这里要注意的是,除了能看到的见的字符,比如#include,其实还有看不见的字符,比如空格,换行符,都是一个个ASCII字符,空格是32,换行符是10。

hello.c的内容是高级C语言,是文本文件,大家都能看懂,需要翻译成低级机器语言指令,才能被系统执行。

> gcc hello.c -o hello
> file hello hello.c                                                                                                                                                 
hello:   Mach-O 64-bit executable x86_64
hello.c: c program text, ASCII text

可以看到hello.c是文本,hello是可执行文件(executable)。

从代码的内容和最终目标,可以记忆编译的四个阶段:

  1. 预处理;代码最上面include,需要把指定的内容插入到程序文本中;
  2. 编译;中间状态 - 汇编语言(高级机器语言);
  3. 汇编;汇编语言需要翻译成机器语言;
  4. 链接;把外部调用的函数或者多个.o文件合并成最终的可执行文件

疑问:为什么需要有个中间汇编语言,而不是直接编译成机器语言?

大型C程序编译的时候,会有各种各样的依赖(内部或者外部),了解编译的过程,可以在遇到问题的时候,更快速的找到问题根源,其实在Linux中使用源代码编译安装一些库(比如openssl)的时候就很容易遇到这样的问题;

  • 编译的时候缺头文件;
  • 链接的时候版本依赖问题,依赖循环等;

一个可执行文件的运行,其实就会涉及到系统的各个硬件部件:

  • i/o设备;键盘 输入字符串hello;执行完的结果,显示在显示器屏幕上;
  • 主存;shell程序把字符放入寄存器(CPU),然后放入内存
  • 磁盘;加载可执行文件hello,从磁盘复制到主存中;使用DMA技术,不通过CPU直接从磁盘到内存;(I/O总线和内存总线)
  • CPU:执行程序中的机器语言指令;
  • 总线;数据在上面各个部件间传输,都是通过总线来完成的;

系统硬件层面的缓存(L1,L2,L3),我们在日常开发过程基本上接触不到,但其实应用层面的各种缓存概念大家还是经常接触到的,它的核心思想是一致的:把经常访问的数据放到更高速的存储设备(系统)中来提高程序的性能;

应用程序对硬件的使用都必须要通过操作系统(Linux Kernel),系统调用、内核态、内核模块、驱动程序、用户态这些概念都是跟硬件的使用有关联的。

进程的上下文切换是平时日常中比较常接触到的名词,一般讲到网络编程中,设计高并发处理的时候,进程或线程过多,上下文切换的开销就会拖垮系统的性能,最终导致应用的并发处理能力下降。

虚拟内存空间的各个部分,其实最常接触的就是栈和堆。

  • 栈在顶部,从高地址向低地址增长;
  • 堆在底部,从低地址向高地址增长;

这个可以通过打印变量的地址查看。实际开发中,遇到内存越界类的bug,常见现象是程序莫名其妙就crash了,原因就是不小心越界写了其它内存地址,导致看到dump里面crash的位置跟实际出问题的地方完全没有关系,这时候可以通过查看dump中上下内存中的内容来推断是哪里越界了写过来的数据。

并发和并行,实际工作中,接触到应用层的并发概念会更多一些,每秒并发用户数(concurrent user per second),虽然跟系统层面的并发完全不同,但概念相近,一个是同时处理多个用户,一个是同时处理多个任务(process或thread)。并行计算的概念,实际工作中,更多是跟分布式任务结合起来,类似pipeline,比如把一个顺序执行序列的任务,拆分成多个不依赖的子任务并行执行,最后合并结果。

字长(word size)是指针数据的大小,经常讲的32位系统和64位系统,其实就是字长是32位和64位,指针的大小也决定了能够表示的内存空间大小,地址能表示的空间就那么大,所以32位的CPU和操作系统最多只能支持2^32也就是4GB的内存。

字节存储的顺序,大端和小端,下面的程序打印出来的从低到高的字节值,可以看出来就是小端了,大端机器/系统相对少见,我们平常使用的PC机基本都是小端。

#include <stdio.h>

int main() {
    int a = 0x01020304;
    printf("0x%.8x\n", a);

    int *pa = &a;
    unsigned char *p = (unsigned char *) pa;

    printf("0x%.2x ", *p);
    printf("0x%.2x ", *(p+1));
    printf("0x%.2x ", *(p+2));
    printf("0x%.2x", *(p+3));

    return 0;
}

Output: 
0x01020304
0x04 0x03 0x02 0x01
1赞

深入理解计算机系统

  1. 所在小组:第一组
  2. 组内昵称:nigel
  3. 心得体会

一、hello world 生命周期

#include <stdio.h>
int main
{
	printf("hello world");
	return 0;
}

编译: gcc -o hello hello.c 会车后,通过编译系统,就会生成一个可执行程序hello

编译过程:
1.预处理(cpp)
个人理解:编译器会找到“#include”开头的代码,并将文件内容导入到一个新的文件中,并生成新文件(hello.i)
2.编译(ccl)
个人理解:编译器通过对hello.i的词法分析,语法分析,代码生成,优化等,最终生成hello.s文件
3.汇编(as)
个人理解:汇编器根据指令集,将hello.s文件翻译成机器指令,并打包,生成二进制hello.o文件。
4.链接(ld)
个人理解:hello.o虽然是二进制文件,但还不能执行,因为要需要链接。printf函数是标准c函数,链接器把hello.o和printf.o进行合并。最终得到可执行文件hello。然后加载到内存中执行。

二、个人总结:为什么要理解这个复杂的过程

1.理解敲回车后,程序到底做了什么,比如在编译(ccl)阶段居然还优化了性能。
2.还有一些问题,比如while循环比for效率高吗,switch语句是否比if-else效率高。为什么?这些奇葩问题,不搞懂编译过程,很难回答出来
3.有时候会遇到一些奇葩的错误。比如静态变量和全局变量。有一些链接错误。
4.安全漏洞,比如内存泄漏,缓冲区溢出,要编写出安全的程序,有必要深入学习以上知识。

三、对大端小端的理解
0x1122

  • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后,即以0x2211形式储存。

既然小端违反人类常识,首先,为什么会有小端字节序?
答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
参考:Errata Security: How to teach endian

四、cpu的高速缓存
未学习之前,一点不知道cpu中还有缓存的概念。
其中一些个人体会:
1.为什么会有cpu的高速缓存?
答:因为cpu运行速度特别快,一般都是ns级别,而内存的访问速度是ms级别,更别说硬盘啦,反正理解就是cpu很强,欲求不满,那么cpu就想了一个办法。
2.大概方案?
答:我每次从内存或硬盘,取到的数据留下一点,放到我的高速缓存中(动态扩容的规则),以备我下次访问。
3.多核之间呢?
答:L1,L2缓存属于各自独立cpu,L3级缓存可以多核之间共享。L1速度>L2速度>L3速度。
4.举个实用例子?
答:个人理解,在nginx配置中,可以通过worker_cpu_affinity指定对cpu和进程之间绑定关系。最大好处就是可以充分利用到cpu的高速缓存。不然进程之间来回切换,出了切换的开销避免不了,缓存也作废了。

五、其他
对于一些内存,磁盘,寄存器,cpu,总线等相关知识书中还未深入开始,在此备注。

  1. 所在小组:第五组
  2. 组内昵称:肖思成
  3. 心得体会
  • 计算机系统的组成是分为硬件和软件两部分
  • 软件
    • 一个程序的生命周期是从源程序开始的,而所谓的源程序实际上是一个由0和1组成的位(比特)序列,8个位被组成一组,称为字节
    • 程序的执行过程,我的理解就是将代码转换成机器可以识别的二进制文件,即转化为低级机器语言指令。一个C语言程序的转化过程可以分为四个阶段:预处理器编译器汇编器链接器,四者共同组成了整个编译系统
  • 硬件:
    • 总线是贯穿整个系统的,携带信息直接并负责在各个部件之间传递的。这里我的理解是,总线只能传递固定长度的字节块,根据字节的字长,用来区分不用的系统,即4字节(32位),8字节(64位),因为对硬件不是很熟悉,所以这是否就是我们平时口中所谓的操作系统的32位和64位区分?
    • 处理器:指令集架构描述的是每条机器代码指令的效果,微体系结构描述的是处理器实际上如何实现的
  • 高速缓存至关重要:
    • 以前确实很少考虑这部分的问题,为什么要用缓存,为什么就能提高性能,书中有句话给了很深的印象,复制就是开销,减慢了程序的工作。一个程序在执行过程中,不考虑优化,只从最基础的流程去说,要经过从磁盘到主存,再到处理器,然后再到显示设备。而利用高速缓存的局部性原理,使得程序的性能大大提高。
  1. 所在小组:第六组
  2. 组内昵称:黄永平
  3. 心得体会
  • 计算机系统抽象的重要性
    抽象的目的是通过隐藏实现的复杂性,来应对变化,对外提供统一的一致性。文件、虚拟内存、进程、虚拟机是抽象概念的高层次,API接口、网络ISO模型、类的封装与接口等是更细节的抽象,其中的共性应该都是隐藏实现,维持外部视角的稳定或一致性。从这个角度,我们的生活中各个方面都体现了抽象。比如机动车,不论内部结构怎么变化或改造,基本的驾驶操作是不会变化的。这里汽车对外提供的驾驶操作接口也可以理解为抽象吧。

  • 信息的表示与处理
    第二章节的内容很多,足有超过80页,占全书的10%多。最开始读的时候,有点不理解,不就是讲二进制及其运算,搞得这么复杂呀。读到后面的内容,尤其是原理和证明,发现自己的肤浅认识。信息的表示是计算机的实现根基,由于计算机实现是受限的,整数及运算和我们常识中的表示和运算是有差异的,而这一点点的差异会带来软件程序的崩溃,甚至系统的灾难。

  • 整数的转换
    整数的转换有两种方式:一种是在位模式不变的前提下进行转换,比如从有符号数强转为无符号数,虽然(物理的)位模式不变,但是转换解析后的数值确发生变化。另一种是通过扩展字长,在数值不变的前提下,比如从short扩展为int。虽然扩展后的(物理的)位模式变化了,但是转换后的数字并不改变。由于转换的差异,这里很容易产生各种意想不到的问题。

1赞
  1. 所在小组:第六组
  2. 组内昵称:吴彬
  3. 心得体会

1.1 信息就是位+上下文

信息的存储是按位(0和1)存储,8位组成一个字节。从存储介质上,有磁盘文件、内存、网络传输等。区分数据的对象的唯一方法就是读取数据的上下文,不同的上下文对同一个字节序列可能表示不同的信息

1.2 程序被其他程序翻译成不同的格式

翻译流程:Hello.c->预处理器->hello.i->编译器->hello.s->汇编器->hello.o->链接器->hello(可执行程序)

预处理器: include 替换为文件内容

编译器: 输出汇编语言

汇编器: 输出机器指令,输出可重定位目标程序

链接器: 理解不深

1.3 了解编译系统如何工作的有好处

有助于优化程序性能、理解链接时错误、避免安全漏洞

1.7 操作系统管理硬件

操作系统管理硬件,而不是程序直接和硬件打交道,两点考虑:统一的接口、防止滥用硬件

进程感觉自己独占计算机,并发执行由操作系统解决,屏蔽应用程序的复杂性

虚拟内存也是给进程提供一种假象,仿佛自己独占内存,降低应用程序编程复杂性

1.8 系统利用网络通信

1.9 重要主题

Amdahl 定律: 当我们对系统的某个部件加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度

并发和并行: 并发,时间跨度上有重合,同一时间只有一个运行,并行:真正的并行

抽象的重要性:
抽象保证接口的稳定,这样内部逻辑的修改,不会影响依赖方的修改。
我们想象下,如果操作硬件、资源没有统一接口,操作系统一升级,我们所有的应用程序都不能用了,这是灾难啊

  1. 所在小组:第六组
  2. 组内昵称:杨凯伟
  3. 心得体会
  • 操作系统的重要性
    计算机系统由硬件和软件组成。操作系统是介于普通软件和硬件之间的一个特殊的软件,所有其他软件对硬件的操作都必须通过操作系统。操作系统通过几个基本的抽象概念(进程、虚拟内存、文件)来管理硬件和服务其他软件。有了操作系统这一层,其他的软件就不用面对硬件的复杂性了,大大降低了软件开发门槛,提高了生产效率。

  • 信息的表示与处理
    信息都用比特位表示,一个字节 8 个比特。
    字节是最小的寻址单位,意味着不可以访问内存中单独的位。给内存中的所有字节都给定一个唯一的数字标识,然后用 标识 直接访问对应的地址。所有可能地址的集合就称为 虚拟地址空间

  • 表示字符串
    因为计算机只存储0和1,表示字符串就需要字符码将数字和字符映射起来。如 ASCII 字符码。

  • 整数的编码

    • 无符号数的编码
      向量 x 中的每一个元素表示一个二进制位,其中每一位取值为0或1。即我们理解的二进制转十进制的方法。
      $$
      B2U_w(x) = \sum^{w-1}{i=0} x_i2_i = x{w-1} \cdot 2^{w-1} + x_{w-2} \cdot 2^{w-2} + …+ x_0 \cdot 2^0
      $$

    • 有符号数的编码
      有符号数的最高有效位即为符号位,它的权重为 $-2^{w-1}$, 1时为负,0时为非负。由于 0 是非负数,这就导致能表示的整数比负数少一个。
      $$
      B2U_w(x) = - x_{w-1}2^{w-1} + \sum^{w-1}{i=0} x_i2_i = - x{w-1}2^{w-1} + x_{w-2} \cdot 2^{w-2} + x_{w-2} \cdot 2^{w-2} + …+ x_0 \cdot 2^0
      $$
      如:
      $B2T_4([1111]) = -1 \cdot 2^3 + 1 \cdot 2^2 + 1 \cdot 2^1 + 1 \cdot 2^0 = -8 + 4 + 2+ 1 = -1$
      值得注意的是 -1 都是补码表示都是一个全1的串,然而这无符号数的最大值的表示一致。

  1. 所在小组:第三组
  2. 组内昵称:Hongxu
  3. 心得体会

知识点部分

  • 关于文本文件的定义

Files such as hello.c that consist exclusively of ASCII characters are known as text files. All other files are known as binary files.

这里的表述不够严谨——仅包含ASCII码的文件固然是文本文件,但是仅包含unicode等编码的文件同样是文本文件。个人觉得wikipedia对text file的解释更到位:“a kind of computer file that is structured as a sequence of lines of electronic text”。对于经常跟编码打交道的程序员来说,我觉得讲清楚更好。

  • DMA技术可以让disk中的内容直接传输到memory中。

  • Amdahl法则。很久以前看到过这一章但是对这个法则没有一点点印象;从数学公式上确实平白无奇,不过这次读这节是感觉这条法则对性能分析时还蛮重要的。

  • 作者对并发(Concurrency)和并行(Parallelism)的解说简明扼要:并发指系统中的多个、同一时间段进行的行为,并行指利用并发使系统更快执行。提到了三类并发:Thread-Level Concurrency,Instruction-Level Parallelism,SIMD。其中Thread-Level这块对hyperthreading多了一分理解:就是CPU的一个核心执行一个线程的时候,通常会有部分处理单元闲置;超线程就是同时运行更多的线程,来把闲置的处理单元利用上。这比传统的uniprocessor上整体CPU进行上下文切换的开销要小很多。具体细节还不太明白,需要进一步理解。hyperthreading在操作系统层上往往是不可见的;一般来说,Linux上可以通过echo 0 > /sys/devices/system/cpu/cpu7/online 的方式禁用后N/2个逻辑CPU从而禁用hyperthreading。

  • 作者着重提出了抽象对于计算机系统的重要性。比如对文件是对读写字节设备的抽象;编程语言提供了对二进制指令,而后者是对处理器硬件的抽象。不由得让人想到

“Any problem in computer science can be solved by anther layer of indirection”

计算机系统很复杂,经过一层又一层的抽象才能使得人类和机器更好的交互,不容易。当然,如何抽象、抽象带来的性能问题,又是另一回事了。

对写作的启发

本章是综述,很多实际内容作者都提到了要参考后面的章节。作者只列了一些基本概念,我觉得是可以自洽的。内容可以很朴实:讲解了一段hello.c到执行的核心流程,很符合程序员口味。作者是抱着让读者看懂的想法来写的,这点上比好些计算机教材要好很多。。。

1赞
  1. 所在小组:第二组
  2. 组内昵称:Joey
  3. 心得体会
  • 指令集抽象于处理器实际架构,处理器为了支持指令集的功能,实际上还有非常多的细节设计(Cache、流水线等),只是都被隐藏了而已,同样的,汇编语言抽象于指令集,C系语言抽象于汇编语言。
  • 操作系统也提供了一些精彩的抽象:进程让一个可执行程序误认为自己是CPU唯一的执行流、虚拟内存让可执行程序误认为自己是内存的唯一访问者。
  • 根据不同处理器的设计,L1/L2可能是核心独有,L3是共享的,Cache是完全由CPU自己管理的,汇编都无法自己操作,所以要写出高效的C/C++ code不仅要考虑会使用什么样的指令集(比如SIMD),可能还要根据不同的处理器,量化的考虑程序的Cache友好度。
  • 缓存理念不仅存在于单机的存储器分层中,在网络分布式的计算系统中也是非常重要的设计理念,在整个计算机系统体系中缓存都是非常重要的方法。
  1. 所在小组:第一组
  2. 组内昵称:slowly
  3. 心得体会:
  • 进程与线程、并行与并发都是常见的面试题,在以前的认识中进程就是一个运行的应用程序,但是实际上它是操作系统对一个正在运行程序的抽象,而线程并不是一个操作系统的抽象,它是一个运行在上下文中的逻辑流;并发与并行以前也仅仅停留在了最顶层的抽象中,也从最基础的单核CPU到现在的多核CPU,超线程等概念,并行的理解也深入到了指令集并行、单指令 多数据并行,渐渐地揭开系统的抽象,透过这些抽象去看到操作系统本身的一些实际操作。抽象是为了让使用者更好的使用,无须关心内部实现,但是我们使用好这种抽象后,再开始揭开抽象的面纱,看到最底层的东西。我们需要看到操作系统是怎么做到的?为什么要这样做? 这便是后面看书需要带着的一个疑问。
  1. 所在小组:第一组
  2. 组内昵称:sadame
  3. 心得体会:
  • 程序计数器永远指向主存中下一条指令的位置,其和一系列大小为一个字的寄存器文件,算术/逻辑单元ALU共同完成CPU工作

  • 进程上下文切换是在内核态下发生的,应用程序通过系统调用指令把控制权交给内核(内核是操作系统代码常驻主存的部分)

  • 进程,虚拟内存,文件都是操作系统的抽象模型,让进程以为自己是独立地在使用cpu,主存和I/O设备

  • 每个计算机都有一个字长,其决定了寄存器文件的单元大小和虚拟地址空间的大小,如64位机器,那么一个寄存器单元就是八个字节,其虚拟地址有2^64个字节

  • 虚拟内存中通常字节序列由十六进制字节串来表示,一个字节两位十六进制数,每个8个16进制数组成的地址(32位)储存两个16进制数

  • 异或是相同为0,分清位级运算和逻辑运算,他们是完全不同的,逻辑运算只返回0或1

  • 涉及负数的移位运算,有符号数使用算术右移,无符号数使用逻辑右移

  1. 所在小组:静默组
  2. 组内昵称:Bean
  3. 心得体会
  • 存储的多层次结构:操作系统其实也是一个程序,和业务程序没有本质区别,所以这多层次的缓存结构,在平时业务代码中也可以借鉴

计算机系统中的一个重大主题就是提供不同层次的抽象,来隐藏实际实现的复杂性

  • 第一章还有一句话特别深刻:抽象本身是一件特别困难的事情,通过学习操作系统的抽象历程来考虑,为什么会选择这样抽象也是一个特别有趣的角度
  • 之前一直对浮点数的理解不是很到位,还踩过坑(在计算两个经纬度点的距离时,由于使用浮点计算,当距离非常近的时候,由于浮点丢失,cos()函数可能报错),之后对于浮点的使用会更加小心拉
  1. 所在小组:静默组
  2. 组内昵称:Tang_D
  3. 心得体会
  • 一份程序源码从编译到运行需要首先预处理将include的文件直接插入程序文本,然后编译为汇编文件,最后将其翻译为二进制文件,使得计算机可以运行这个文件,最后经过链接阶段,使我们使用到的一些预编译的函数可以合并到我们的.o文件中。
  • 整个计算机主要可以分为抽象和缓存,进程是对CPU的抽象,文件是对I/O,各类存储系统,网络的抽象,而缓存则是为了解决CPU和存储设备速度不匹配的问题,通过在其中加上一块缓存,加快计算机的速度。
  • Amdahl定律给我们的启示是,我们选择优化一个系统的某个模块时,我们需要关注这个模块在系统中的占比,以及可以优化到什么程度,在这两个指标中做取舍,哪怕一个模块很重要,但是在系统中运行时间的占比较小,这时候对他进行优化,取得的收益并不会像想象中那么大。
  • 大端法,小端法,在之前一直没有在意过这个东西,针对数字而言,将其二进制转化为16进制后,低位在前为小端法,和我们的正常书写反过来,而大端法和我们正常书写保持一致,当两个系统网络通信时,如果采用的是不同的编码格式需要注意。
  • 逻辑右移,最高位补0,算术右移,最高位补符号位。

1.所在小组:第七组
2.组内昵称:吴奇驴
3.心得体会

  • Amdahl定律:感觉只是一个现象总结,系统中的某一部分的重要程度、加速程度在第一次进行优化主观判断的比例比较大,真正确定是调试了几次后的。
  • 并发与并行:并行是并发来使系统运行得更快。
  • 抽象:书里讲了计算机软件和硬件通过抽象进行解偶,想起了orm,Model不需要进行修改,但是后端接的数据库是可以不同的。
  • 信息的表示与处理:第二章对数的表示和运算给了很多推导和说明,里面用比较有趣的角度看补码,还有一些技巧:a&&5/a 不会零除,p&&*P++不会导致间接引用空指针。
  • 这本书:第二章自己读起来感觉很难懂,即使跟着视频看也是这样子,后来买了纸质的书翻,感觉真得和视频里说的一样,要读上三遍。
  1. 所在小组:第三组

  2. 组内昵称:hy

  3. 心得体会

  • 如今,计算机系统的基本抽象非常类似的,遵循相同的接口规范(POSIX),对这些底层抽象的理解,有助于提升自己的内力,知其所以然;

  • hello.c 源文件本质就是值 0 和 1 的 bit 组成的序列,8 个 bit 组织成一个 byte(字节),表示文本中的一个字符。8 个 bit 可以对应 2^8 个不同的字符,在 ASCII(美国标准信息交换代码 American Standard Code for Information Interchange) 编码下,取其中 128 个表示 128 个字符,包括大小写字母、数字、标点符号、非打印字符(换行符、制表符等)、控制字符(退格、响铃等);

  • 每个信息都是一个比特串,其代表的意思完全在于上下文中如何解释,例如这 n 个 bit 是整数、浮点数、字符串或者机器指令,都是使用不同的规则(rule)解释出来的,数字有数字的规则,字符串有不同的编码规则;

  • 程序到机器码的过程经过多层次的处理,预处理、编译、汇编、链接,像一个流水线一样,对代码进行处理,每层只做好一件事,得到最终的可执行程序;

  • 系统的硬件提供了软件的载体,在操作系统中,软件可以说是硬件的抽象,总线、I/O设备、内存、处理器,通过对几个核心硬件的了解,我们可以更深入地了解操作系统如何运转,如何变得更快,性能更高。

  • 缓存的重要性毋庸置疑,提到性能优化必提到缓存,在开发中我们尽量使用上层的缓存,利用好局部性原理,把高级缓存的性能利用到极致,像数据库的索引结构 B+ Tree,也是对磁盘和缓存的高效利用,在存储的层次上利用到极致;

  • 进程、虚拟内存、文件这三大抽象,以前只是会使用,通过这次的机会,希望能深入研究下这些抽象实现的原理。抽象是计算机科学最重要的一个概念,各种大佬都在不停地强调这个词:抽象;

  • Amdahl 定律说白了就是木桶原理嘛,整个系统的提升受限制于最短的木板;

  • 并发、并行、锁机制是高性能系统的热点问题,这里从底层的控制流上阐述并发机制,还是有些启发的;

  • 位运算、逻辑运算、移位运算都是以前理解的知识,没啥重点,网络传输统一都是大端表示法,其实不用记,知道这个概念,写程序的时候如果发现顺序反了,改一下就OK。

下面再介绍下 UTF-8 这个目前使用最广泛的编码:

UTF-8 编码

ASCII 是针对英语设计的,一个 byte 表示一个字符是够的,但是汉语有几万个字符,使用一个 byte 是远远不够的,咋办?多用 1 个字节嘛,为此发明了 UNICODE 使用 2 个字节来表示字符,兼容 ASCII,当属于 ASCII 字符时,前一个字节全为 0。

但是,有了新的问题,对可以使用 ASCII 表示的字符使用 UNICODE 表示并不高效,每个字符都浪费了一个字节,存储空间大了一倍,假设你的源码里只有一个汉字,为了显示这个汉字,所有的英文字母都要使用 2 个字节存储,文件大小翻倍了

为了解决这个问题,发明了一些中间格式的字符集,称为通用转换格式,即 UTF(Unicode Transformation Format),常见的有 UTF-8、UTF-16、UTF-32。

UTF-8 怎么编码字符呢?

8 表示使用 8 bit 的块表示字符,可以使用 1 - 4 个块表示。

UTF-8 完全兼容 ASCII,使用一个 byte 表示 ASCII,怎么表示呢,其实很简单:

系统读取到的 UTF-8 编码文件是 0 和 1 的字节流,当取出一个字节(8 bit)时:

  • 如果第一位为 0,表示是一个独立的 ASCII 字符

  • 如果第一位是 1,第二位为 0,表示这个字节为多字节表示字符中的一个组成字节;

  • 如果前两位是 1,第三位为 0,表示当前字节为两个字节表示字符的第一个字节

  • 如果前三位是 1,第四位为 0,表示当前字节为三个字节表示字符的第一个字节

  • 如果前四位是 1,第五位为 0,表示当前字节为四个字节表示字符的第一个字节

因此,根据前两位,是不是 10,可以判断该字节是不是字符编码的第一个字节;如果前两位都是 11 根据前四位,可以确定该字节为字符编码的第一个字节,并且可以判断由几个字节表示。

| 1st Byte | 2nd Byte | 3rd Byte | 4th Byte | Number of Free Bits | Maxinum Expressible Unicode Value |

| — | — | — | — | — | — |

| 0xxxxxxx | | | | 7 | 007F hex(127) |

| 110xxxxx | 10xxxxxx | | | (5 + 6) = 11 | 07FF hex(2047) |

| 1110xxxx | 10xxxxxx | 10xxxxxx | | (4 + 6 + 6) = 16 | FFFF hex(65535) |

| 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | (3 + 6 + 6 + 6) = 21 | 10FFFF hex(1114111) |

可以看到,非首歌 byte 都是使用 10 开头的,除了用作判断规则的 bit,其他 bit 都能用来表示字符,加在一起表示全世界的所有字符还绰绰有余

1.所在小组:静默组
2.组内昵称:维钢、
3.心得体会
操作系统一直以来掌握的不是很好,借着这次机会,让自己操作系统的知识更加完善和体系化。本周主要看了本书的第一章和第二章,第一章主要概括讲了操作系统各个概念和知识,利用一个hello world小程序让我们对操作系统有了整体的印象。第二章讲的是信息的表示和处理,其中让我们掌握了很多细节,同时这章的学习颇有难度,尤其是大小端字节序,整数有无符号的表示、转换和运算。虽然有些难度,但收获颇丰,继续努力,继续坚持。
笔记:

  1. 只由ASCII字符构成的文件称为文本文件,所有其他的文件都称为二进制文件。
  2. 从物理上来说,主存是由一组动态随机存取存储器(DRAM)组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(即数组索引),这些地址是从0开始的。
  3. 最低有效字节在最前面的方式,称为小端法(little endian)。最高有效字节在最前面的方式,称为大端法(big endian)。

1.所在小组:一组
2.组内昵称:郝立鹏(组长)
3.心得体会
数学公式的推导和证明我没有看,不是说不重要,而是现在看不懂(数学换给老师了),工作后弄公式证明感觉有点鸡肋。读第一章和第二章时,虽然某些知识点是以前看过的,但是依旧有以下的感悟

一、抽象思想(文件)

Linux抽象出了进程,文件和虚拟内存。我重点说下对文件的理解

文件是对IO设备的抽象,拥有读和写操作的都可能被抽象成文件

比如普通文件是文件,目录文件也是文件,套接字也是文件,字符设备和块设备(如鼠标、键盘、磁盘、外接设备等)也是文件,管道也是文件。

抽象成文件后,对文件的权限操作就是对文件关联的设备的权限操作,简化了操作流程。

对应用层来说,无需关心不同的设备调用什么api来进行读写操作,简化了软件开发流程;

对于内核来说,像英语中“完形填空”一样把具体实现函数注册进操作系统即可,因为所有“文件”都提供的统一的接口(函数)来操作的。

我细想了想,抽像的思想一直伴随着我们的生活和工作。

在工作中,将复杂业务逻辑抽象成不同概念,构建出各种不同模型,将业务逻辑化繁为简,大大降低了开发人员和产品人员的心智负担。这是个很牛逼的能力(我一直也在练习和琢磨,据说做到头是业务架构师)。比如我们公司将流量分析分为抓包、识别、分析、合并、压缩、存储,现在想想,这么设计的人真是人才!

1.所在小组:三组
2.组内昵称:kippa(组长)、wyhqaq、Hongxu、hy、hhhhhhe、MrTrans、晴天、hector、uucloud
3.心得体会


from : kippa

一、CPU 架构

  • 以前在学习计算机系统时,考试有一道题就是手绘 8086 CPU 结构图,重新学习这门课,再徒手画一次,也算是一件有仪式感的事情吧

二、存储器层次结构图

  • 主要思想是上层存储器作为低一层存储器的告诉缓存。上层的存储器更小更快更贵,下层的存储器更大更慢更便宜。
  • 但是缺少一个定量的直观比较。这里补充一下 Jeff dean 说过的 Numbers Everyone Should Know
操作 速度
L1 cache reference 读取CPU的一级缓存 0.5 ns
L1 cache reference 读取CPU的一级缓存 0.5 ns
Branch mispredict(转移、分支预测) 5 ns
L2 cache reference 读取CPU的二级缓存 7 ns
Mutex lock/unlock 互斥锁\解锁 100 ns
Main memory reference 读取内存数据 100 ns
Compress 1K bytes with Zippy 1k字节压缩 10,000 ns/0.01ms
Send 2K bytes over 1 Gbps network 在1Gbps的网络上发送2k字节 20,000 ns/0.02ms
Read 1 MB sequentially from memory 从内存顺序读取1MB 250,000 ns/0.25ms
Round trip within same datacenter 从一个数据中心往返一次,ping一下 500,000 ns/0.5ms
Disk seek 磁盘搜索/寻道 10,000,000 ns /10ms
Read 1 MB sequentially from network 从网络上顺序读取1兆的数据 10,000,000 ns/10ms
Read 1 MB sequentially from disk 从磁盘里面读出1MB 30,000,000 ns /30ms
Send packet CA->Netherlands->CA 一个包的一次远程访问 150,000,000 ns/150ms

三、虚拟内存

  • 虚拟内存是对主存和磁盘的抽象,为进程提供了一个假象,使得每个进程看到的内存都是一致的,仿佛是在独占地使用主存。

四、计算机系统中的抽象

  • 学习操作系统,主要有三种抽象:文件是对 I/O 设备的抽象,虚拟内存是对程序存储器的抽象,进程是对一个正在运行的程序的的抽象

五、信息存储

  • 这一部分内容都是以前的基础知识,大端小端表示法的不同没有细看,感觉花费太多时间在这里可能不是太划算。

from:wyhqaq

  • 系统的硬件组成:
    • 一条总线贯穿整个系统。硬件通过实现通用的对应的适配器/控制器连接到总线上,各个部件对数据传输率的不同要求,总线又分成I/O总线,内存总线,系统总线等,用不同层次的总线进行互连,以适应各自的特性与需求。
      (软件系统可以抽象成这种结构吗?假如微服务采用这种结构,首先消息队列代替总线,数据库假设成硬盘,然后各种服务解耦成不同的设备,那设备控制器呢?这层的功能应该是为了适应不同厂商的设备提供一个统一的接入口,但是服务能这样再抽象出来吗?比如存储服务控制器,下面包含数据库存储、ceph存储等, 这种结构很像k8s了。所以软件和硬件设计有相似之处,某些方面是可以相互借鉴学习的)
  • 高速缓存至关重要:
    • 那是不是代表着没缓存就系统就无法work?我理解缓存应该在能预料的范围里再上。为什么,第一增加系统复杂性;第二增加维护成本;第三数据一致性。
    • 进程切换:
      • 进程切换需要保存当前进程的上下文(状态信息),这部分需要内核代码来参与,涉及到很多寄存器数据改变,开销很大。操作系统对进程最大数一般有限制(cat /proc/sys/kernel/pid_max && sysctl -a |grep kernel.pid_max)。超线程实际上部分解决这个问题,通过多加套pc和寄存器,减少这种切换。
    • Amdahl定律:
      • 要想显著加速整个系统,必须提升全系统中相当大的部分的速度。木桶原理是说要注意最短板,最短板就是耗时最久,最拖累系统整体的部分,所以两者宏观上表达的都是要抓住问题核心,从核心入手。
    • 计算机系统中抽象的重要性:
      • 不言而喻啊~~自己也要提高抽象能力,多观察一些优美的设计,想想为什么它要这么做,好处坏处。
    • 寻址和字节顺序:
      • 为什么要分小端模式和大端模式?
        参考了这两篇
        why-are-both-little-and-big-endian-in-use 1
        理解字节序
        计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
        但是也有不同观点,所以不用纠结了,就是个历史原因。

from :Hongxu

知识点部分

  • 关于文本文件的定义

Files such as hello.c that consist exclusively of ASCII characters are known as text files. All other files are known as binary files.

这里的表述不够严谨——仅包含ASCII码的文件固然是文本文件,但是仅包含unicode等编码的文件同样是文本文件。个人觉得wikipedia对text file的解释更到位:“a kind of computer file that is structured as a sequence of lines of electronic text”。对于经常跟编码打交道的程序员来说,我觉得讲清楚更好。

  • DMA技术可以让disk中的内容直接传输到memory中。

  • Amdahl法则。很久以前看到过这一章但是对这个法则没有一点点印象;从数学公式上确实平白无奇,不过这次读这节是感觉这条法则对性能分析时还蛮重要的。

  • 作者对并发(Concurrency)和并行(Parallelism)的解说简明扼要:并发指系统中的多个、同一时间段进行的行为,并行指利用并发使系统更快执行。提到了三类并发:Thread-Level Concurrency,Instruction-Level Parallelism,SIMD。其中Thread-Level这块对hyperthreading多了一分理解:就是CPU的一个核心执行一个线程的时候,通常会有部分处理单元闲置;超线程就是同时运行更多的线程,来把闲置的处理单元利用上。这比传统的uniprocessor上整体CPU进行上下文切换的开销要小很多。具体细节还不太明白,需要进一步理解。hyperthreading在操作系统层上往往是不可见的;一般来说,Linux上可以通过echo 0 > /sys/devices/system/cpu/cpu7/online 的方式禁用后N/2个逻辑CPU从而禁用hyperthreading。

  • 作者着重提出了抽象对于计算机系统的重要性。比如对文件是对读写字节设备的抽象;编程语言提供了对二进制指令,而后者是对处理器硬件的抽象。不由得让人想到

“Any problem in computer science can be solved by anther layer of indirection”

计算机系统很复杂,经过一层又一层的抽象才能使得人类和机器更好的交互,不容易。当然,如何抽象、抽象带来的性能问题,又是另一回事了。

对写作的启发

本章是综述,很多实际内容作者都提到了要参考后面的章节。作者只列了一些基本概念,我觉得是可以自洽的。内容可以很朴实:讲解了一段hello.c到执行的核心流程,很符合程序员口味。作者是抱着让读者看懂的想法来写的,这点上比好些计算机教材要好很多。。。


from: hy

  • 如今,计算机系统的基本抽象非常类似的,遵循相同的接口规范(POSIX),对这些底层抽象的理解,有助于提升自己的内力,知其所以然;

  • hello.c 源文件本质就是值 0 和 1 的 bit 组成的序列,8 个 bit 组织成一个 byte(字节),表示文本中的一个字符。8 个 bit 可以对应 2^8 个不同的字符,在 ASCII(美国标准信息交换代码 American Standard Code for Information Interchange) 编码下,取其中 128 个表示 128 个字符,包括大小写字母、数字、标点符号、非打印字符(换行符、制表符等)、控制字符(退格、响铃等);

  • 每个信息都是一个比特串,其代表的意思完全在于上下文中如何解释,例如这 n 个 bit 是整数、浮点数、字符串或者机器指令,都是使用不同的规则(rule)解释出来的,数字有数字的规则,字符串有不同的编码规则;

  • 程序到机器码的过程经过多层次的处理,预处理、编译、汇编、链接,像一个流水线一样,对代码进行处理,每层只做好一件事,得到最终的可执行程序;

  • 系统的硬件提供了软件的载体,在操作系统中,软件可以说是硬件的抽象,总线、I/O设备、内存、处理器,通过对几个核心硬件的了解,我们可以更深入地了解操作系统如何运转,如何变得更快,性能更高。

  • 缓存的重要性毋庸置疑,提到性能优化必提到缓存,在开发中我们尽量使用上层的缓存,利用好局部性原理,把高级缓存的性能利用到极致,像数据库的索引结构 B+ Tree,也是对磁盘和缓存的高效利用,在存储的层次上利用到极致;

  • 进程、虚拟内存、文件这三大抽象,以前只是会使用,通过这次的机会,希望能深入研究下这些抽象实现的原理。抽象是计算机科学最重要的一个概念,各种大佬都在不停地强调这个词:抽象;

  • Amdahl 定律说白了就是木桶原理嘛,整个系统的提升受限制于最短的木板;

  • 并发、并行、锁机制是高性能系统的热点问题,这里从底层的控制流上阐述并发机制,还是有些启发的;

  • 位运算、逻辑运算、移位运算都是以前理解的知识,没啥重点,网络传输统一都是大端表示法,其实不用记,知道这个概念,写程序的时候如果发现顺序反了,改一下就OK。

下面再介绍下 UTF-8 这个目前使用最广泛的编码:

UTF-8 编码

ASCII 是针对英语设计的,一个 byte 表示一个字符是够的,但是汉语有几万个字符,使用一个 byte 是远远不够的,咋办?多用 1 个字节嘛,为此发明了 UNICODE 使用 2 个字节来表示字符,兼容 ASCII,当属于 ASCII 字符时,前一个字节全为 0。

但是,有了新的问题,对可以使用 ASCII 表示的字符使用 UNICODE 表示并不高效,每个字符都浪费了一个字节,存储空间大了一倍,假设你的源码里只有一个汉字,为了显示这个汉字,所有的英文字母都要使用 2 个字节存储,文件大小翻倍了。

为了解决这个问题,发明了一些中间格式的字符集,称为通用转换格式,即 UTF(Unicode Transformation Format),常见的有 UTF-8、UTF-16、UTF-32。

UTF-8 怎么编码字符呢?

8 表示使用 8 bit 的块表示字符,可以使用 1 - 4 个块表示。

UTF-8 完全兼容 ASCII,使用一个 byte 表示 ASCII,怎么表示呢,其实很简单:

系统读取到的 UTF-8 编码文件是 0 和 1 的字节流,当取出一个字节(8 bit)时:

如果第一位为 0,表示是一个独立的 ASCII 字符;

  • 如果第一位是 1,第二位为 0,表示这个字节为多字节表示字符中的一个组成字节;

  • 如果前两位是 1,第三位为 0,表示当前字节为两个字节表示字符的第一个字节;

  • 如果前三位是 1,第四位为 0,表示当前字节为三个字节表示字符的第一个字节;

  • 如果前四位是 1,第五位为 0,表示当前字节为四个字节表示字符的第一个字节;

因此,根据前两位,是不是 10,可以判断该字节是不是字符编码的第一个字节;如果前两位都是 11 根据前四位,可以确定该字节为字符编码的第一个字节,并且可以判断由几个字节表示。

| 1st Byte | 2nd Byte | 3rd Byte | 4th Byte | Number of Free Bits | Maxinum Expressible Unicode Value |

| 0xxxxxxx | | | | 7 | 007F hex(127) |

| 110xxxxx | 10xxxxxx | | | (5 + 6) = 11 | 07FF hex(2047) |

| 1110xxxx | 10xxxxxx | 10xxxxxx | | (4 + 6 + 6) = 16 | FFFF hex(65535) |

| 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | (3 + 6 + 6 + 6) = 21 | 10FFFF hex(1114111) |

可以看到,非首歌 byte 都是使用 10 开头的,除了用作判断规则的 bit,其他 bit 都能用来表示字符,加在一起表示全世界的所有字符还绰绰有余。


from: hhhhhhe

  • 操作系统如何编译一个hello.c文件?
  1. shell接收到用户输入gcc -o hello hello.c
  2. shell首先检查开头的第一个英文单词是否为shell内置命令,如果不是则尝试将其作为可执行文件加载到内存中。shell如何尝试找到gcc这条命令的呢?答案是通过环境变量PATH,通常Linux下可执行文件会存放在/usr/local/sbin,/usr/local/bin,/usr/sbin,/usr/bin等目录中,shell会查找这些目录下是否有gcc命令。如果有,shell找到该命令文件对应的inode号,获得文件的inode信息,inode信息包括文件在磁盘的数据块位置。找到文件所在的磁盘位置之后,shell开始从磁盘的指定块中读取文件内容(shell会如何读取文件内容?),加载到内存中开始执行(文件如何从磁盘加载到内存?)。
  3. gcc编译器开始执行后,分为四个步骤。1、预处理阶段:将#开头的头文件内容和原文件内容一起写入到一个新的以.i结尾的文件中;2、编译阶段:将高级语言指令翻译成汇编,为什么不直接翻译成机器语言?;3、汇编阶段:将汇编翻译成机器指令,将这些指令打包到hello.o文件;4、链接阶段:链接器ld将编译好的hello程序与所用到的库函数链接起来。
  4. 至此hello.c文件备编译成了一个可执行程序hello
  • 操作系统如何执行hello文件?
  1. shell接收输入./hello
  2. 尝试运行加载hello文件,将文件从磁盘复制到内存中
  3. CPU开始执行内存中的hello程序
  4. 执行完成的结果发送到显示器上最终显示出来
  • CPU缓存为什么重要?
  1. 首先要明白缓存中存放的是什么?答案是CPU最近使用到的数据(曾经执行过的命令)或将会使用到的数据(预读文件内容)
  2. 然后需要知道CPU的运算速度和内存数据的读取速度之间差距非常大
  3. CPU需要一个地方存放曾经使用过的数据或将来会用到的数据,内存显然不行,速度差距太大。这时CPU缓存就上场了。通常CPU有L1、L2、L3级缓存,其中L1级缓存的读取速度与CPU运算速度最相近。
  4. 有了CPU缓存,CPU的运算效率会降低被低速设备影响的概率(通常CPU90%的数据都可以从缓存中获取),能更高效的执行。
  • 抽象
  1. 文件是对I/O设备的抽象?把所有的I/O设备抽象成文件进行处理
  2. 虚拟内存是对内存和磁盘I/O设备的抽象?把进程所需的内存操作和I/O操作抽象成对虚拟内存操作
  3. 进程是对处理器、内存和I/O设备的抽象?把程序运行所需的CPU时间片、内存空间、I/O设备操作等资源抽象成进程
  • Amdahl定律
  1. 结论就是:想要显著加速整个系统,必须提升全系统中相当大部分的速度。说人话就是别想着只优化一个地方就能获得好性能
  • 信息流处理
  1. 这一章内容细致,没细看。

from:MrTrans

计算机漫游

  • 计算机系统是软硬件组成,软硬件共同工作来运行应用程序,所以懂硬件才能更好的使用软件,才能懂计算机系统。
  • 信息无处不在,但是缺少了上下文就变得毫无意义。ASCII标准表示文本字符,被大多数现代计算机使用。
  • 了解编译系统大有裨益,可以优化程序性能,理解连接出现的错误以及避免安全漏洞。
  • 程序->可执行程序文件,经历预处理,编译器,汇编器和连接器,其中连接器最容易产生bug。
  • 在理解计算机组成中,cpu于内存之间还存在高速缓存l1,2,3,以平衡cpu于内存之间的运行速度差异。
  • 抽象是计算机逐步螺旋上升的哲学理念。文件是io的抽象,虚拟内存是主存和磁盘的抽象,进程则是文件,内存,处理器的抽象。
  • 线程共享内存,优于进程的上下文切换,协程上下文切换更优于线程。
  • 程序运行,虚拟内存为每个进程提供假象,堆向上动态扩展与收缩,栈向下动态扩展与收缩。
  • Amdahl定律,s=1/(1-a)+a/k 最优性能解不是某一单方面的提升,综合公式考虑。
  • cpu超线程技术,也就是同时多线程。降低并发的压力,并行优化。

信息的表示和处理

大多数计算机都使用8位的块或者字节,作为最小的可寻址内存单位,而不是访问内存中单独的位。

重温16进制,10进制,二进制之间的转换。

同一个数值,整型与浮点型表示二进制,有13个位,序列和位会是相同的。

0,1 也能在布尔的思维下成为代数代数运算。& ^ | ~。

^运算 能够在不引入第三个变量,实现两个数字的交换。

位级运算很基础很重要,位移运算设计逻辑与算术运算。整数的表示,uint64_t-> 9223372036854775807


from: 晴天

正如章节标题,漫游计算机系统,第一章从宏观角度讲解了程序从输入到输出的过程中到底发生了什么。有意思的是,作者从程序员的角度,简单明了地说明源程序的编译,以及系统和硬件之间的交互过程。

  • 源程序文件 hello.c 被翻译为可执行的目标文件 hello 经历4个阶段,分别是 预处理 -> 编译器 -> 汇编器 -> 链接器
  • 可执行文件 hello 被存储在磁盘中,当需要时可被读取并加载到主存

为什么需要了解编译系统如何工作?

要执行程序 hello 并将其输出到shell,绕不开硬件。系统硬件的组成有:

  • 总线是贯穿系统中的电子管道(类似于人体中的血管),携带信息字节在各个部件中传递信息。因系统而异(比如32位或者64位),总线被设计成传送定长的字节块,传输4个字节或者8个字节。(这是不是意味着同等配置下,单位时间内传输的字节数越多,处理速率越快?)
  • 主存 是一个临时存储设备,在处理执行程序时,用来存放程序和程序处理的数据。
  • 处理器(CPU)是解释或执行存储在主存中的指令引擎。其核心是一个大小为一个字的存储设备,称为程序计算器。在任何时刻,PC 都指向主存中的某条机器语言指令。从系统通电开始,处理器不断执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。看上去像一个简单的指令执行模型。(所以单核CPU并发只是模拟出来的,通过快速在进程中快速切换实现,本质上CPU只能执行一条命令)

程序被加载时,指令从磁盘复制到主存;当处理器运行程序时,指令又从主存复制到处理器,而 复制就是开销 。 针对处理器与主存之间的差异,使用高速缓存存储器的局部性(作为暂时的集结区域,存放处理器近期会需要的信息)来提高程序的性能。

理解3个抽象,文件是I/O设备的抽象,虚拟内存是对程序存储器的抽象,而进程是对一个正在进行的程序的抽象。


from:hector

第 1 章 计算机系统漫游

第 1 章以 hello world 程序为例介绍了计算机硬件组、软件运行过程、存储器层次结构、操作系统管理硬件等内容。
程序运行:预处理 -> 编译 -> 汇编 -> 链接

  • 系统的硬件组成:总线 + I/O设备 + 主存 + 处理器

  • 缓存主要是利用局部性原理来提高程序性能。存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。

  • 操作系统的主要作用是防止防止硬件资源被应用程序滥用以及向应用程序提供简单一致的机制来访问硬件资源。操作系统主要是通过几个抽象(进程、虚拟内存、文件)概念来实现这些功能。

  • 虚拟内存让每个进程都以为自己在独占地使用主存。每个进程看到的是虚拟地址空间,从低到高主要分为程序代码和数据、堆、共享库、栈、内核虚拟内存。

  • Amdahl 定律:当我们对系统对某个部分进行加速时,其对系统整体虚拟对影响取决于该部分对重要性和加速程度。

  • 并发和并行的三个层次:(1)线程级并发(2)指令级并行(3)单指令、多数据并行

第 2 章 信息的表示和处理

第 2 章主要讲述了如何在计算机上表示信息、处理信息等。

  • 计算机可能没有产生预期等结果,但是至少它是一致的。
  • 二进制、十进制、十六进制的表示及转化。
  • 位运算(~、&、|、^) 与逻辑运算( ||、&&、!)
  • 移位运算:左移、右移(逻辑右移、算术右移)

结束


from:uucloud

第一章 计算机系统漫游

  • c的编译系统由 预处理器编译器汇编器链接器 构成。预处理器会修改原程序,比如将include的文件插入程序文本;预编译器将文本翻译成汇编;汇编器将汇编翻译成机器语言指令,生成叫做可重定位目标程序relocatable object program的格式(就是.o);链接器将库函数中预编译好的目标文件合并到.o文件生成可执行文件。
  • 总线由 系统总线、内存总线、I/O总线。
  • 利用直接存储器存取(DMA),数据可以不通过CPU,可以直接从磁盘到内存。
  • L1几乎和访问寄存器一样快,L2比L1慢5倍,但仍然比访问内存快5~10倍。他们都是SRAM。
  • 进程、虚拟内存、文件是操作系统对硬件设备的抽象,文件是对I/O设备的抽象,虚拟内存是对主存和磁盘I/O设备的抽象,进程是对处理器,贮存和I/O设备的抽象。
  • 有人说Amdahl是木桶效应,但是根据他的定义来看,这个定律更适合用抓大放小来形容。比如一个应用有两个接口,一个耗时特别长但是调用频率很低,一个耗时很短但是频率很高,这时优化耗时很长的接口可能没什么意义,因为他的初始耗时占比太低了,即使优化到极致也很难带来整体的提高。
  • SIMD并行是一种单指令,多数据并行的并行方式。他允许一条指令产生多个可并发执行的操作。有编译器会自动提取,也可以显示的使用特殊向量来编程。感觉这个并行特性对视频编解码的作用会非常大。

第二章 信息的表示和处理

  • 依赖默认的字数据大小会带来可移植性的问题,这种依赖的情况可以用int32_t或者int64_t,准确指出来。
  • 没在工作里遇到需要关注大小端的,不过前一段时间有看到一篇文章,作者debug proto消息的时候,需要知道varint是小端存储。 https://mp.weixin.qq.com/s/D-hip9Slg-n9-zxI8ygHYg
  • 在c中强制类型转换是很危险的,必须完全清楚类型的底层存储形式。对于像Go这种强类型的语言,是不太需要关心这个的,但是如果使用了unsafe.pointer这样的类型…