深入理解计算机系统 小结

一、编译系统:执行预处理、编译、汇编、链接四个阶段的程序一起构成了编译系统。

1、预处理阶段:预处理器读取#include中的头文件的内容,将其插入到程序文本中,形成.i文件。

2、编译阶段:编译器将.i文件翻译成包含一个汇编语言程序的.s文件。汇编语言程序每条语句都描述了一条低级机器语言指令。汇编语言为不同高级语言的不同编译器提供了通用的输出语言。比如c编译器和Fortran编译器产生的输出文件用一样的汇编语言。

3、汇编阶段:汇编器将.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,将其存在二进制文件——.o文件中。

4、链接阶段:链接器将hello.o文件和预编译好的库函数文件(比如printf.o)链接成可执行文件。可执行文件可以被加载到内存,由系统执行。

 

二、信息的处理和表示

1、位运算、逻辑运算和移位运算

1.1位运算:&|^~

不用额外空间交换ab的值的方法:

1.1.1 //有可能会出现溢出

a = a + b;

b = a - b;

A = a - b;

1.1.2 //通过位运算不用担心溢出

a = a ^ b;

b = b ^ a;

a = b ^ a;

1.2逻辑运算:&&||、!

1.3移位运算:a<<x不溢出情况就得到a * Pow(2,x)1<<31 = -Pow(2,31);1 << 32 = 0;

2、整数和浮点数的表示:

2.1整型的表示(int):

2.1.1范围:4byte-Pow(2,31)~Pow(2,31)-1

2.1.2最大值加一会变成最小值;

2.1.3负数是相反数取反加一,这样的话1后面加310就不会表示-0,而是-Pow(2,31),而且可以直接用来计算;int型数字相加(不论正负)都按位相加就好,结果是对的;

2.2浮点数(float):

2.2.1范围:四个字节:范围-10^38~10^38//注意32位最多有Pow(2,32)种排列情况,最多能表示32个不一样的数,浮点数表示范围扩大了,但有的数无法表示,只能表示近似值。越靠近原点,数据密度越大,远离原点,数据稀疏。阶码位数越多,能表示的数范围越大,但也越稀疏

2.2.2表示:(-1)^s * (1.frac)^(exp-127);//s一位,frac二十三位,exp八位;

2.2.4特殊值:

0exp = 0b00000000,frac = 00000000000000000000000(23).s = 1时表示-0

正无穷大:exp = 0b11111111,frac = 00...0(23),s = 0;

NaN(Not a Number)exp = 0b11111111,frac != 0,s = 0 s =1;

最大规格化数:exp = 0b11111110,frac = 11...1(23),s = 0.接近2 * Pow(2,127)

最小规格化正数:exp = 0b00000001,frac = 00...0(23),s = 0.接近Pow(2,-126)

 

三、缓存:基于缓存的存储器层次结构之所以有效是因为局部性原理:时间局部性和空间局部性。

1、存储器的层次结构:从上往下依次是:寄存器、三个级别的高速缓存(SRAM)、主存(DRAM)、本地二级存储(本地磁盘)、远程二级存储(分布式文件系统、web服务器)。越往上存储空间越小,速度越快,价格越高。每一级都可以做下一级的缓存。

2、块:第k+1层的存储器被划分成连续的数据对象片称为块。块是数据的传输单元,在第k层和第k+1层之间传递,相邻层次之间块大小是一致的。一般越往下,层次对之间的块大小越大。通常块大小是固定的,但也可以不固定,比如存储在web服务器上的远程HTML文件。

3、命中与不命中

3.1缓存命中:程序需要第k+1层的对象恰好缓存在第k层。

3.2缓存不命中:程序需要第k+1层的对象未缓存在第k层。

3.2.1冷不命中:缓存是空的(冷缓存)。冷不命中只是暂时的。

3.2.2冲突不命中:

放置策略:将k+1层某个块限制在第k层块的一个小子集中。

冲突不命中:缓存足够大,但是因为这些对象会映射到同一个缓存块,发生不命中。

3.3.3容量不命中:工作集大小大于缓存大小。

4、高速缓存存储器

4.1地址结构:地址一共m位,所以可以有M = Pow(2,m)个不同的地址。m = t + s + b.地址从前往后依次是t,s,b.

4.1.1 t位表示标记位:标记位可以标识组中该行是否存储了所要对象的信息。

4.1.2 s位用于标识块所在的组。所以可以有S = Pow(2,s)个不同的组。

4.1.3 b位存储块偏移信息。所以每行能存储空间为B = Pow(2,b)的数据块。找到组和行之后根据块偏移定位到块在缓存中的起始地址。

4.2缓存中块的组织结构

4.2.1(S):先将缓存分成不同的组。S = Pow(2,s)。全相联映射只有一个组。此时地址中没有s位。

4.2.2(E):每个组里可以有不同的行。行里有一个有效位用于标识此行是否包含有意义信息。E表示每组里有多少行。直接映射中每组一行;组相联映射中每组多行。

4.2.3(B):每行B = Pow(2,b)位用于存储块。假如块大小为x,则B / x能表示每行存储多少块。

4.3在缓存中索引块w

4.3.1组选择:根据ws个组索引位找到相应的组。全相联映射因只有一个组,不用此步。

4.3.2行匹配:找到有效位标记为有效的行,如果标记为匹配,则实现行匹配。

4.3.2.1直接映射中因为组里只有一行,所以查看该行是否(有效&&标记位匹配)。

4.3.2.2组相联映射中:组中任一行都可以包含映射到这个组的块。所以要遍历组中每一行。

4.3.2.3全相联映射与组相联映射类似。

4.3.3字抽取:b位的块偏移给出所找对象的起始字节。

4.4替换策略(组相联映射中未命中会涉及):最不常使用、最近最少使用。会有开销,但是越往存储器层次结构下面走,远离CPU,不命中的开销越大,所以需要用更好的替换策略。

 

四、动态存储器分配:使用的重要原因是经常在程序运行时才知道某些数据结构大小。

1、c语言内存模型(从低地址到高地址依次是):程序文本、初始化全局变量(初始化的静态局部变量也在这里,下同)、未初始化全局变量、堆、栈。栈里的变量地址越来越小,但是数组是特殊的,比如数组a[10]里,a[1]地址比a[0]大。堆里面的变量地址越来越大。有一个指针brk,指向堆顶。

函数void * sbrk(intptr_t incr)brk增加incr。如果成功就返回brk旧值。

函数void* malloc(size_t size)返回大小至少size字节的存储块。因为需要做双子边界对齐,所以可能比size大。注意字符串末尾是有\0的。如果函数出现问题就返回NULLmalloc不初始化空闲块。calloc初始化空闲块为0.realloc能改变以前分配空闲块的大小。注意:全局变量和静态变量都自动初始化为0,局部变量初始化为随机值。

函数void* free(void ptr)释放ptr所指向的(malloc或者callocrealloc申请的)内存块。

2、分配器风格:

2.1隐式分配器:也叫垃圾收集器,自动释放不再使用的块。

2.2显式分配器:要求应用显式地释放任何已分配的块。

3、碎片:

3.1内部碎片:即分配的没有利用的部分。比如用于双子边界对齐的部分。

3.2外部碎片:空闲存储器合计足够满足一个分配请求,但是没有单独空闲块满足分配请求。

4、分配器设计

4.1堆块组织:由三部分组成:头部、有效载荷和填充(可选)组成。

4.1.1隐式空闲链表:头部有32位,表示块大小。经过双子边界对齐,块大小是8的倍数,因此后三位总是0,所有用来存储其它信息。最后一位用于标识空闲块是否分配,1表示分配,0表示空闲。

4.1.2使用边界标记的堆块:当释放堆块时需要通过读取前后两个堆块头部信息检查前后两个堆块是否有空闲的,如果有就进行合并。很容易找到后面堆块的头部,但前面堆块头部难以定位。因此堆块组织时在末尾增加和头部一样的尾部就容易了。

4.1.3显式空闲链表:隐式空闲链表里块分配与堆块总数呈线性关系。把空闲块组织成显式数据结构使块分配与空闲块数量呈线性关系。比如可以将堆块组织处双向空闲链表,前驱指针和后继指针指向相邻的空闲块。

空闲块释放时可以用后进先出(LIFO)方式:新释放的块放在链表开始处。如果配合首次适配的放置策略和尾部标记,释放和合并都在常数时间完成。

也可以按照地址顺序组织空闲链表,配合基于地址的首次适配策略可以使空闲块利用率解决最佳适配。但是释放所用时间是log(n)的。

4.1.4分离空闲链表:维护多个空闲链表,每个链表的块的大小大致相等。

4.1.4.1分离适配:

组织:分配器维护一个空闲链表的数组,每个空闲链表和一个大小类相关联,并且被组织成某种类型的显式或隐式链表。

分配:先确定大小类的大小,对适当的空闲链表做首次适配。如果找不到合适的块(比如此大小类的链表里的块都被分配了),就找下一个更大的大小类。如果分配时没有找到合适的块,就向操作系统请求额外的堆存储器。从这个额外的堆存储器分配一个块,将其余部分放置合适的大小类中。

分割:分割后将剩余部分插入到适当的空闲链表中。

合并:释放后将合并(或相邻块都被分配而不能合并)的块放到合适的大小类中

优点:搜索被限制在堆的某个部分而不是整个堆,所以速度变快了;对分离空闲链表进行简单首次适配搜索,存储器利用率接近最佳适配搜索。

4.1.4.2伙伴系统:分离适配的一种特例:每个大小类都是2的幂。

优点:快速搜索、快速合并;缺点:要求块大小为2的幂容易出现内部碎片。

 

五、并发编程

1、基于进程的并发编程:服务器为每个请求的客户端创建进程。共享文件表但不共享用户地址空间。

1.1优点:比较安全,一个进程不可能覆盖另一个进程的虚拟存储器。

1.2缺点:进程共享信息变得困难,需要通过IPC(进程间通信机制)。进程控制和IPC开销很高。

2、基于I/O多路复用的并发事件驱动编程:事件驱动程序中将逻辑流模型化为自动机。对新客户端的请求,服务器会创建一个状态机。

2.1优点:比基于进程的设计给程序员更多对程序行为的控制;

运行在单一进程上下文中,因此每个逻辑流都能访问该进程全部地址空间。

2.2缺点:编码复杂。

3、基于线程的并发编程:运行在进程上下文的逻辑流。

3.1.1线程存储器模型:每个线程有它自己的线程上下文,包括线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。线程由内核自动调度,多个线程运行在单一进程上下文中,所以共享进程虚拟地址空间的整个内容,包括代码、数据、堆、共享库和打开的文件。

3.1.2一个线程无法读另一个线程寄存器的值;任何线程都可以访问共享虚拟存储器的任意位置。如果线程修改存一个存储器位置,其它每个线程都能在读这个位置时发现变化。寄存器不是共享的,虚拟内存是共享的。

3.3线程执行模型:每个进程开始生命周期都是单一线程,称为主线程,他可以创建对等线程。

3.4线程的与进程的区别:

3.4.1线程上下文比进程上下文小得多,是轻量级的,切换代价小。

3.4.2线程不像进程按照严格父子层次组织,和一个进程相关的线程组成一个对等(线程)池,独立于其它线程创建的线程。每个对等线程都能读写相同的共享数据。一个线程可以杀死它的任意对等线程,或者等待它的任意对等线程终止。主线程和其它线程区别仅在于它是进程第一个运行的线程。

3.5回收线程:线程默认被创建为可结合的

3.5.1可结合的线程:能被其它线程回收资源和杀死。为了避免内存泄露,可结合的线程要么被其它线程显式回收,要么通过函数pthread_detach分离。

3.5.2分离的线程:不能被其它进程回收或杀死,它的存储器资源在终止时由系统自动释放。

3.6用信号量同步线程:共享变量是方便的,但也引入了同步错误的可能性。可以通过信号量、进度图加以分析、限制。多线程竞争多个资源会出现死锁问题,规定获取信号量的顺序可以避免。

3.7可重入函数:当被多个线程调用时不会引用任何数据共享的函数。是线程安全函数的一种。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值