《深入理解计算机系统》读书笔记

第一章 计算机系统漫游

     1.程序的编译

       对于一个hello.c程序,从源文件到目标文件的转化是由编译器驱动程序(compiler driver)完成的,翻译过程分为四个阶段完成,执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统。

图1.1 编译系统

      预处理阶段。预处理器(cpp)根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。

       编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。

       汇编阶段。汇编器(as)将hello.s翻译成机器语言指令,并把指令打包成为一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。Hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。

       链接阶段。此时hello程序调用了printf函数。Printf函数存在于一个名为printf.o的单独的预编译目标文件中。链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。最后可执行文件加载到储存器后由系统负责执行。

      2.系统的硬件组成

 

图1.2 一个典型系统的硬件组成

       总线

       贯穿整个系统的一组电子管道,携带信息字节并负责在各个部件见传递。通常总线被设计成传送定长的字节块,各种系统的字节数不相同,例如Intel Pentium系统的字节长为4字节。

       I/O设备

       I/O(输入/输出)设备是系统与外界的联系通道,主要包括:键盘和鼠标,显示器以及用于长期存储数据和程序的磁盘驱动器。每一个I/O设备都是通过一个控制器与适配器与I/O设备连接起来的。控制器是主板上的芯片组,而适配器则是一块插在主板插槽上的卡。

       主存

       主存是由一组DRAM(动态随机访问存储器)芯片组成的。在处理器执行程序时,它被用来存放程序和程序处理的数据。

       处理器

       中央处理单元(CPU)简称处理器,是解释存储在主存中指令的引擎。处理器的核心是程序计数器(PC)的字长大小的存储设备(或寄存器)。PC指向主存中的某条机器语言指令(内含其地址)。

       高速缓存

       之前但系统在执行hello程序时会有大量的拷贝工作,例如把代码和数据从磁盘磁盘拷贝到主存,从主存拷贝到寄存器堆,再从寄存器堆把文件拷贝到显示设备中。这些拷贝工作会减慢程序的实际工作。因此,为了加快程序运行速度,系统设计者采用了更小更快的存储设备,称为高速缓存存储器,它们被用来作为暂时的集结区域,存放处理器在不久的叫过来可能会需要的信息。

       形成层次结构的存储设备

       存储器分层结构的主要思想是一个层次上的存储器作为下一层次上的存储器的高速缓存。

 

图1.3 一个存储器层次结构的示例


作为一个计算机学生,很多基础知识都不知道或者已经不记得了


2.进程的虚拟地址空间
《深入理解计算机系统》读书笔记_第1张图片

第二章 信息的表示和处理
这章主要讲二进制的数据表示方式,学了4年的计算机,经常出现这些知识,但是还是有经常弄不清楚的知识
1.大端和小端存储
小端存储:
机器在存储器中按照最低有效字节到最高有效字节的顺序存储对象,也就是说最低有效字节在前面,Intel的机器就是这样
大端存储:
机器按照最高有效字节到最低有效字节的顺序存储对象,例如IBM,Sun Microsystems的大多数机器

而IBM的个人计算机使用的是与Intel兼容的处理器,许多微处理器,包括Alpha和Motorolad的PowerPC可以运行在任意存储方式中。

例如:变量x存储与0x100处,int型数值为0x01234567
《深入理解计算机系统》读书笔记_第2张图片

第二章 信息的处理和表示

       这章前面的知识其实以前也学过了些,但这章书介绍的很多细节都是以前没注意的。

       在C#中有三种最重要的数字编码。无符号编码是基于传统的二进制表示法的,表示大于或者等于零的数字。二进制补码编码是表示有符号整数的最常见的方式。浮点数编码是表示实数的科学计数法的以二为基数的版本。计算机的表示法用有限的位数来对一个数字编码,因此,当结果太大以致不能表示时,某些运算就会溢出。例如:当计算机计算表达式200*300*400*500会得出-884901888这个违背整数运算属性的结果。

       另外浮点运算与其他两种运算相比有完全不同的数学属性。虽然溢出会产生特殊的值+∞,但是一组正数的乘积总是正的。另一方面由于表示的精度有限,浮点运算是不可结合的。例如,在大多数机器上,C表达式(3.14+1e20)-1e20求得值会是0.0,而3.14+(1e20-1e20)求得的值会是3.14.

       1.信息存储

       大多数计算机使用8位的块来作为最小的可寻址的存储器单位,而不是访问访问存储器中单独的位。存储器的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合就称为虚拟地址空间。编译器和运行是系统的一个任务就是将这个存储器空间划分为更可管理的单元,来存放不同的程序对象,也就是程序数据、指令和控制信息,然后有各种机制可以用来分配和管理程序不同部分的存储,这种管理其实完全是在虚拟地址空间里完成的。

       1.1十六进制表示法

       由于十进制和二进制表示法对于描述为模式来说都不是非常方便,所以引进了另一种替代的方法,也就是十六进制数来书写位模式。表示方法:以0x或0X开头,数字“0”~“9”和字符“A”~“F”来表示16个可能的值。取值范围:0016~FF16

       编写计算级程序的一个常见任务就是手工地在位模式的十进制、二进制和十六进制表示之间转换。二进制和十六进制之间的转换是简单直接的。例如,假设有一个数字0x173A4C,可以通过展开每个十六进制数字,将它转换位二进制格式,如下所示:

 

      这样就给出了二进制表示000101110011101001001100。

       反过来如果给定一个二进制数字000101110011101001001100,则可以通过首先将它分割为每四位一组来把它转换为十六进制,当位总数不是四的倍数,最左边的一组可以少于四位,前面用零补足。如下所示:

 

       十进制和十六进制表示之间的转换需要使用乘法或者除法来处理一般情况。将一个十进制数字转换为十六进制,可以反复地用16除x,得到一个商q和一个余数r,然后用十六进制数字表示的r作为最低位数字,并且通过q反复进行这个过程得到剩下的数字。例如,考虑十进制314156的转换:

 

      从这里可以读出十六进制表示为0x4CB2C。

      当值x是2的幂时也就是对于某个n,x=2∧n,可以很容易地将x写成十六进制形式,只要知道x的二进制表示就是1后面跟n个零。十六进制数字0代表四个二进制0.所以对于被写成i+4j形式的n来说,其中0≤i≥3,可以把x写成开头的十六进制数字为1(i=0)、2(i=1)、4(i=2)或者8(i=3),然后跟着j个十六进制的0。比如,x=2048=211,有n=11=3+4*2,从而得到十六进制表示0x800。

       反过来将一个十六进制数字表示为十进制数字,可以用相应的16的幂乘以每个十六进制数字。比如,给定数字0x7AF,计算它对应的十进制值为7*162+10*16+15=1967。

第一章 计算机系统漫游

1.信息就是位+上下文
源程序(0、1位序列;8位一组称为字节)–每个字节表示程序中某个文本字符
大部分的现代系统都是用ASCII标准来表示文本字符,这种方式实际上就是用一个 唯一的单字节大小的整数值来表示每个字符
每个文本行都以一个不可见的换行符’\n’来结束的,对应的整数值为10
系统中所有的信息——包括磁盘文件、存储器重的程序、存储器中存放的用户数据以及网络上传送的数据,都是由一串位表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。

2.程序被其他程序翻译成不同的格式
例如,gcc编译器驱动程序的翻译过程可以分为4个过程:
-hello.c源程序(文本)–> 预处理器(cpp)-hello.i被修改的源程序(文本)–> 编译器(ccl)-hello.s汇编程序(文本)–> 汇编器(as)-hello.o可重定位目标程序(二进制)+外部printf.o–> 链接器(ld)-hello可执行目标程序(二进制)–>
汇编语言非常有用,它为不同高级语言的不同编译器提供了 通用的输出语言。

3.了解编译系统如何工作是大有益处的
4.处理器读并解释存储在存储器中的指令
外壳(shell)是一个命令行解释器,输出一个提示符,等待你输入一个命令,然后执行这个命令。
通常总线被设计成传送定长的字节快,就是字(word)。
每个IO设备都通过一个 控制器适配器与IO总线相连。控制器和适配器之间的区别主要在于它们的封装方式。控制器是置于IO设备本身的或者系统的主印制电路板(主板)上的芯片组,而适配器则是一块插在主板插槽上的卡。
主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是一组动态随机存储器(DRAM)芯片组成的;从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(即数组索引),这些地址是从零开始的。
中央处理单元(CPU),简称处理器,是解释在主存中指令的引擎。处理器的核心是一个字长的存储设备(或寄存器),成为程序计数器(PC)。在任何时刻,PC都指向贮存中的某条机器语言指令(即含有该条指令的地址)。
CPU在指令的要求下可能会执行以下操作:
加载:把一个字节或者一个字从主存复制到寄存器,以覆盖寄存器原来的内容
存储:把一个字节或者一个字复制到主存的某个位置,以覆盖这个位置上原来的内容
操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算数操作,并将结果存放到一个寄存器中,以覆盖该寄存器原来的内容
跳转:从指令本身中抽取一个字,并将这个字复制到PC中,以覆盖PC中原来的值
Q:寄存器多大?
一个典型的寄存器文件只存储几百字节的信息,而主存里可以存放几十亿字节。然而,处理器从寄存器文件中读数据比从主存中读取几乎快100倍,差距还在持续增大。

5.高速缓存至关重要
L1、L2高速缓存用一种叫做静态随机访问存储器(SRAM)的硬件技术实现。利用了局部性原理,即程序具有访问局部区域里的数据和代码的趋势。

6.存储设备形成层次结构

7.操作系统管理硬件
(1)进程–是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个程序,这是通过处理器在进程间切换来实现的。OS实现这种交错执行的机制成为 上下文切换
(2)线程–尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
由于网络服务器对并行处理的需求,线程称为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当有多处理器可用的时候,多线程也是一种使程序可以更快运行的方法。
多处理器VS多核。一个cpu上多个核心,多处理器多个cpu。
(3)虚拟存储器–抽象概念,为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的是一致的存储器,称为虚拟地址空间。
地址从低到高:
程序代码和数据
共享库
内核虚拟存储器
(4)文件–字节序列。每个IO设备,包括磁盘、键盘、显示器、甚至网络,都可以视为文件。

8.系统之间利用网络通信

9.重要主题
线程级并发:
多核处理器是将多个CPU(称为“核”)集成到一个集成电路芯片上。
超线程,有时候称为同时多线程,是一项允许一个CPU执行多个控制流的技术。
指令级并行:
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。
单指令、多数据并行:
SIMD并行,在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作。
计算机系统中抽象的重要性:
文件是对IO的抽象,虚拟存储器是对程序存储器的抽象(主存+IO),进程是对一个正在运行的程序的抽象(处理器(指令级结构)+虚拟存储器),虚拟机是对整个计算机的抽象(操作系统、处理器和程序)。

第二章 信息的表示和处理

1.信息存储

大多数计算机使用8位的块,或者 字节,作为最小的可寻址的存储器单元,而不是在存储器中访问单独的位。
程序级程序将存储器视为一个非常大的字节数组,称为 虚拟存储器(virtual memory)
存储器的每个字节都由一个唯一的数字来标识,称为它的的 地址(address),所有可能地址的集合称为 虚拟地址空间(virtual address space)这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现是将随机访问存储器(RAM)、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。
C中的指针有两个方面:值和类型。它的值表示某个对象的位置,而它的类型表示那个位置上所存储对象的类型。
十六进制:在C中,以0x或者0X开头的数字常量被认为是十六进制的值。用十六机制书写,一个字节的值域为00~FF。字符A-F既可以大写,也可以小写,甚至大小写混合。
A-10  B-11  C-12  D-13  E-14  F-15
字:指明整数和指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对已个字长为w位的机器而言,虚拟地址的范围为0-2w-1,程序最多方位2w个字节。
大多数计算机字长32位,这就限定了虚拟地址空间为4千兆字节(4GB),也就是刚刚超过4X10(9)字节。
寻址和字节顺序:对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么?以及在存储器重如何排列这些字节?
在几乎所有的机器上,多字节对象都 被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。eg.int类型的变量x的地址为0×100,则&x的值就为0×100,那么x的4个字节将被存储在存储器的0×100、0×101、0×102和0×103。
某些机器选择在存储器中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。最低有效字节在最前的方式叫做 小端法(little endian),大多数Intel兼容机采用这种规则;最高有效字节在最前面的方式叫做 大端法(big endian),大多数IBM和Sun Microsystems的机器采用。也有混合的双端法,可以进行配置。
反汇编是一种确定可执行程序文件所表示的指令序列的工具。
尽管浮点数和整型数据都是对数值12345编码,但是它们有非常不同的字节模式:整型为0×00003039,浮点数为0x4640E400。一般而言,这两种格式使用不同的编码方法。
表示字符串:C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。
C语言中的移位运算:对于无符号数据,右移必须是逻辑的,对于有符号数据,算数的或者逻辑的右移都可以。算数右移时,若最高位是1,填充的就是1。

第三章 程序的机器级表示 

用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的。
2.程序编码
unix> gcc -o1 -o p p1.c p2.c
-o1:告诉编译器使用第一级优化;通常,提高优化级别会使最终程序运行得更快,但是编译时间可能会变长,用调试工具对代码进行调试会更困难。正如我们会看到的,使用更高级别的优化产生的代码会严重改变形式,以至于产生的机器代码和出事源代码之间的关系非常难以理解。因此我们会使用第一级优化作为学习工具,然后当我们增加优化级别时,在看会发生什么。实际中,从得到的程序性能方面考虑,第二级优化被认为是较好的选择。
机器级代码:
对于机器级编程来说,其中两种抽象很重要,第一种是 机器级程序的格式和行为,定义为 指令集体系结构(Instruction set architecture,ISA),它定义了 处理器状态、指令的格式,以及每条指令对状态的影响。第二种是 机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
一些通常对C语言程序员隐藏的处理器状态是可见的:
程序计数器指示将要执行的下一条指令在存储器中的地址;
整数寄存器文件包含8个命名的位置,分别存储32位的值,这些存储器可以存储地址(对应于C的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其他则用来保存临时数据,例如过程的局部变量和函数的返回值;
条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如用来实现if和while语句;
一组浮点寄存器存放浮点数据。
程序存储器(program memory)包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器快(比如用malloc库函数分配的)。程序存储器用虚拟地址来寻址,操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址。
int accum = 0;
2
int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}
unix> gcc -O2 -S code.c 
C语言编译器产生的汇编代码
sum:
pushl %ebp
movl %esp,%ebp
movl 12(%ebp),%eax
addl 8(%ebp),%eax
addl %eax,accum
movl %ebp,%esp
popl %ebp
ret
unix> gcc -O2 -c code.c–GCC会编译并汇编该代码产生目标代码文件code.o,二进制形式,无法直接查看。选17个字节序列,十六进制表示:
……55 89 e5 8b 45 0c 03 45 08 01 05 00 00 00 00 89 ec 5d c3……
(gdb) x/19xb sum
如何找到程序的字节表示?
要查看目标代码文件的内容,最优价值的是反汇编器。这些程序根据目标代码产生一种类似汇编代码的格式。
反汇编只是基于机器代码文件中的字节序列来确定汇编代码,不需要访问程序的源代码或汇编代码;
设计指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令。例如,只有指令pushl %ebp是以字节值55开头的。
生成实际可执行的代码需要对一组目标代码文件运行链接器,链接器将代码的地址移到了一段不同的地址范围中,并确定了存储全局变量的地址。P109
3.数据格式
4.访问信息
一个IA32中央处理单元(CPU)包含一组8个存储32位值的寄存器。
如图,字节操作指令可以独立地读或者写前四个寄存器的2个低位字节。
操作数指示符
源数据值可以以常数的形式给出,或是从寄存器或存储器中读出,结果可以存放在寄存器或存储器中。
因此,各种不同的操作数的可能性被分为三种类型:
1)立即数(immediate),也就是常数值。用$后面跟一个标准C表示法表示的整数,例如$-377或$0x1F。任何能放入32位的字里的数值都可以用作立即数, 不过汇编器在可能时会使用一个或两个字节的编码。(会拆开存在不同的寄存器中?)
2)寄存器(regiser),表示某个寄存器的内容。Ea表示任意寄存器a,Reg[Ea]表示它的值,这是将寄存器集合看成一个数组Reg,用寄存器标识符作为索引。
3)存储器(memory)引用,它会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。Mb[Addr]表示对存储在存储器中从地址Addr开始的b个字节值的引用。
寻址模式
The scaling factor s must be either 1, 2, 4, or 8.

Assume initially that %dh = 8D, %eax = 98765432
1 movb %dh,%al %eax = 9876548D
2 movsbl %dh,%eax %eax = FFFFFF8D
3 movzbl %dh,%eax %eax = 0000008D
例子中都是将寄存器%eax的低位字节设置成%edx的第二个字节。
movb指令不改变其他三个字节,movsbl指令将其他三个字节设为全1或全0,movzbl指令无论如何都是将其他三个字节设为全0.

栈操作说明,根据惯例,我们的栈是倒过来画的,因而栈“顶”在底部,IA32的栈向低地址方向增长,所以压栈是减小栈指针(%esp)的值,并将数据存放到存储器中,而出战是从存储器中读,并增加栈指针的值。
程序栈存放在存储器中的某个区域。栈指针 %esp保存着栈顶元素的地址。
int exchange(int *xp,int y)
{
   int x = *xp;
   *xp = y;
   return x;
}
xp at %ebp+8,y at %ebp+12
1. movl 8(%ebp),%edx     Get xp
By copying to %eax below,x becomes the return value
2. movl (%edx),%eax      Get x at xp
3. movl 12(%ebp),%ecx    Get y
4. movl %ecx,(%edx)      Store y at xp
5.算术和逻辑操作
1)加载有效地址(load effective address)
指令leal实际上是movl指令的变形。它的指令形式是从 存储器读数据到 寄存器,但实际上它根本就没有引用存储器。
它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。
leal S ,  D   D <–&S  Load Effective Address
For example, if register %edx contains value x, then the instruction leal 7(%edx,%edx,4), %eax will set register %eax to 5x + 7. The destination operand must be a register.
AT&T语法,base(offset, index, i),就是 base+offset+index*i
2)一元操作和二元操作
一元操作只有一个操作数,既是源又是目的;
二元操作,源操作数是第一个,目的操作数是第二个,subl %eax,%edx是使寄存器%edx的值减去%eax中的值。第一个操作数可以是立即数、寄存器或是存储器位置,第二个操作数可以是寄存器或是存储器位置,不过同movl指令一样,两个操作数不能同时是存储器位置。
3)移位操作
先给出移位量,然后第二项给出的是要移位的数值。它可以进行算术和逻辑右移。 移位量用单个字节编码,因为只允许进行0到31位的移位(只考虑移位量的低5位)。移位量可以是一个立即数,或者存放在单字节寄存器元素%cl中。(这些指令很特别,因为只允许以这个特定的寄存器作为操作数)。
左移指令:SAL和SHL,两者效果是一样的,都是将右边填上0.
右移指令:SAR执行算术移位(填上符号位)>>A,SHR执行逻辑移位>>L(填上0)。
6.控制
条件码(condition code)寄存器描述了最近的算术或逻辑操作的属性,可以检测这些寄存器来执行条件分支指令。
最常用的条件码有:
CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数的溢出。
ZF:零标志。最近的操作得出的结果为0.
SF:符号标志。最近的操作得到的结果为负数。
OF:溢出标志。最近的操作导致一个补码溢出——正溢出或负溢出。
比较和测试指令CMP和TEST不修改任何寄存器的值,只设置条件码。
jmp指令是无条件跳转,它可以是直接跳转,即跳转目标是作为指令的一部分编码的;也可以是间接跳转,即跳转目标是从寄存器或存储器位置中读出的。汇编语言中,直接跳转是给出一个标号作为跳转目标的,例如”jmp .L1″。间接跳转的写法是”*”后面跟一个操作数指示符,例如jmp *%eax用寄存器%eax中的值作为跳转目标,而指令jmp *(%eax)以%eax中的值作为读地址,从存储器中读出跳转目标。
其他跳转指令都是有条件的——根据条件码的某个组合,或者跳转,或者继续执行代码序列中的下一条指令。
循环
C语言中提供了多种循环结构,即do-while、while和for,汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。
do-while循环:
do
     body-statement
     while(test-expr)
=============
loop:
     body-statement
     t=test-expr;
     if(t)
          goto loop;
while循环:
while ( test-expr )
body-statement
============转换为do-while
if (!test-expr)
goto done;
do
body-statement
while (test-expr);
done:
=============直接翻译成goto代码
t = test-expr;
if (!t)
goto done;
loop:
body-statement
t = test-expr;
if (t)
goto loop;
done:

for循环:
for ( init-expr ;  test-expr ;  update-expr )
body-statement
===========================while
init-expr;
while (test-expr) body-statement
update-expr;
}
=========================do-while
init-expr;
if (!test-expr)
goto done;
do body-statement
update-expr;
g while (test-expr);
done:
===============================goto
init-expr;
t = test-expr;
if (!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if (t)
goto loop;
done:
swith语句:
可以根据一个整数索引值进行多重分支,处理具有多种可能结果的测试时,这种语句特别有用。
它们不仅提高了C代码的可读性,而且通过使用 跳转表这种数据结构使得实现更加高效。跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值i时程序应该采取的动作。程序代码用开关索引值来执行一个跳转表内的数组引用,确定跳转指令的目标。 和使用一组很长的if-else语句相比,使用跳转表的优点是执行开关语句的时间与开关情况的数量无关。GCC根据开关情况的数量和开关情况值的稀少程度来翻译开关语句,当开关情况数量比较多,并且值的范围跨度比较小时,就会使用跳转表。

7.过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。
它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。
大多数机器,只提供转移控制到过程和从过程中转移出控制这种简单的指令。数据传递、局部变量的分配和释放通过操纵程序栈来实现。
栈帧结构:
机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。
为单个过程分配的那部分栈称为栈帧(stack frame)。
Figure 3.16: Stack Frame Structure. The stack is used for passing arguments, for storing return information,
for saving registers, and for local storage.

过程P调用过程Q,则Q的参数放在P的栈帧中。另外,当P调用Q时,P中的返回地址被压入栈中,形成P的栈帧的末尾。返回地址就是当程序从Q返回时应该继续执行的地方。Q的栈帧从保存的帧指针的值(如%ebp)开始,后面是保存的其他寄存器的值。
过程Q也用栈来保存其他不能存放在寄存器中的局部变量,原因:
1.没有足够多的寄存器存放所有的局部变量
2.有些局部变量是数组或结构,因此必须通过数组或结构引用来访问
3。要对一个局部变量使用地址操作符&,我们必须为它生成一个地址
另外,Q也会用栈帧来存放它调用的其他过程的参数。

call的指令效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址,这样当被调用过程返回时,执行会从此处继续。
ret指令从栈中弹出地址,并跳转到这个位置,正确使用这条指令,可以使栈做好准备–leave,栈指针要指向前面call指令存储返回地址的位置。
寄存器使用惯例:
程序寄存器组是唯一能被所有过程共享的资源。
需要保证被调用者不会覆盖某个调用者稍后会使用的寄存器的值,所以使用惯例,所有的程序必须遵守。
寄存器 %eax,%edx和%ecx被划分为 调用者保存寄存器。当P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P所需要的数据。
寄存器 %ebx,%esi和%edi被划分为 被调用者保存寄存器。Q必须在覆盖这些寄存器的值之前,先把它们保存到栈中,并在返回前恢复它们。
此外,必须保持寄存器%ebp和%esp。
1 int P(int x)
2 {
3 int y = x*x;
4 int z = Q(y);
5
6 return y + z;
7 }
Procedure P computes y before calling Q, but it must also ensure that the value of y is available after returns. It can do this by one of two means:
_ Store the value of y in its own stack frame before calling Q. When Q returns, it can then retrieve the value of y from the stack.
_ Store the value of y in a callee save register. If Q, or any procedure called by Q, wants to use this register, it must save the register value in its stack frame and restore the value before it returns. Thus,when Q returns to P, the value of y will be in the callee save register, either because the register was never altered or because it was saved and restored.
1 int swap_add(int *xp, int *yp)
2 {
3 int x = *xp;
4 int y = *yp;
5
6 *xp = y;
7 *yp = x;
8 return x + y;
9 }
10
11 int caller()
12 {
13 int arg1 = 534;
14 int arg2 = 1057;
15 int sum = swap_add(&arg1, &arg2);
16 int diff = arg1 – arg2;
17
18 return sum * diff;
19 }

GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍,包括保存%ebp值的4个字节和返回值的4个字节。采用这个规则是为了保证访问数据的严格对齐。

为什么popl相当于恢复寄存器的值?P154

GCC产生的代码有时候会使用leave指令来释放栈帧(leave指令在ret前使用,既重置了栈指针,又重置了帧指针),而有时会使用一个或两个popl指令。
可以用push指令或是从栈指针减去偏移量来在栈上分配空间。在返回前,函数必须将栈恢复大原始条件,可以恢复所有被调用者保存寄存器和%ebp,并且重置%esp使其指向返回地址。


8.数组分配和访问
T A[N];
–在存储器中分配一个L*N字节的连续区域;用XA表示起始位置。
–引入了标识符A;可以用A作为指向数组开头的指针,这个指针的值就是XA。可以用0-N-1之间的整数索引来访问数组元素,数组元素i会被存放在地址为XA+L*i的地方。


10.综合理解指针
–每个指针都对应一个类型。如果对象类型T,那么指针的类型为*T。特殊的*void类型代表通用指针,比如malloc函数返回一个通用指针,然后通过显式强制类型转换或者赋值操作那样的隐式强制类型转换,将它转换成一个有类型的指针。指针不是机器代码中的一部分,是C语言提供的一种抽象,帮助程序员避免寻址错误。
–每个指针都有一个值。这个值是某个指定类型对象的地址。特殊的NULL(0)值表示该指针没有指向任何地方。
–指针用&运算符创建。可以出现在赋值语句左边的表达式。这样的例子包括变量以及结构、联合和数组的元素。leal指令设计用来计算存储器引用的地址,&运算符的机器代码实现常常用这条指令来计算表达式的值。
–操作符用于指针的间接引用。间接引用是通过存储器引用来实现的,要么是存储到一个指定的地址,要么是从指定的地址读取。其结果是一个值,它的类型与该指针的类型相关。
–数组与指针紧密联系。一个数组的名字可以像一个指针变量一样引用(但不能修改)。数组引用a[3]与指针运算和间接引用*(a+3)有一样的效果。
–将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。
–指针也可以指向函数。这提供了一个很强大的存储和向代码传递引用的功能,这些引用可以被程序的某个其他部分调用。函数指针的值是该函数机器代码表示中第一条指令的地址。
int (*f)(int*)
要从里往外读。因此,我们看到(*f)表明,f是一个指针;而(*f)(int*)表明f是一个指向函数的指针,这个函数以int*作为参数。最后,它是以int*为参数并返回int的函数的指针。
int *f(int*)->
(int *) f(int*)
函数原型,声明了一个函数f,它以一个int*作为参数并返回一个int*。













 第四章 处理器体系结构

1.Y86指令集体系结构

Y86程序用虚拟地址来引用存储器位置。
硬件和操作系统软件联合起来将虚拟地址翻译成实际或物理地址,指明数据实际保存在存储器中哪个地方。
RISC-精简指令集
CISC-复杂指令集
2.逻辑设计和硬件控制语言HCL
存储器和时钟:
时钟寄存器(简称寄存器):存储单个位或字。时钟信号控制寄存器加载输入值。
随机访问存储器(简称存储器):存储多个字。用地址来选择该读或写哪个字,随机访问存储器的例子:1)处理器的虚拟存储器系统,硬件和操作系统软件结合起来使处理器可以在一个很大的地址空间内访问任意的字;2)寄存器文件,再此,寄存器标识符作为地址。
3.Y86的顺序实现
通常处理一条指令包括很多操作。将它们组织成某个特殊的阶段系列,即使指令的动作差异很大,但所有的指令都遵循统一的序列。每一步的具体处理取决于正在执行的指令。
取指(fecth):取指阶段从存储器读取指令字节,地址为程序计数器的值。从指令中抽取出指令指示符字节的两个四位部分,称为icode(指令代码)和ifun(指令功能)。它可能取出一个寄存器指示符字节,指明一个或两个寄存器操作数指示符rA和rB。它还可能取出一个四字节常数字valC。它按顺序方式计算当前指令的下一条指令的地址valP。也就是说,valP等于PC的值加上已取出指令的长度。
译码(decode):译码阶段从寄存器文件读入最多两个操作数,得到值valA和/或valB。通常,它读入指令rA和rB字段指明的寄存器,不过有些指令是读寄存器%esp的。
执行(execute):算术/逻辑单元ALU要么执行指令指明的操作,计算存储器引用的有效地址,要么增加或减少栈指针。得到的值我们成为valE。再次,也可能设置条件码。对于一条跳转指令来说,这个阶段会检验条件码和分支条件,看是不是该选择分支。
访存(memory):访存阶段可以将数据写入存储器,或者从存储器读出数据。
写回(write back):最多可以写两个结果到寄存器文件。
更新PC(PC update):设置成下一条指令的地址。
4.流水线的通用原理
流水线化的一个重要特征就是增加了系统的吞吐量,也就是单位时间内服务的顾客总数,不过它也会轻微地增加延迟,也就是服务一个用户所需要的时间。
深入理解计算机系统读书笔记
//============================================
2010.07.05
深入理解计算机系统
(1) 对于一个无符号数字x,截断它到k位的结果就相当于计算x mod 2^k.
(2) 在大多数的机器上,整数乘法指令相当地慢,需要12或者更多的始终周期,然而其他整数运算-例如加法、减法、位移运算和移位-只需要1个时钟周期.因此,编译器使用的一项重要的优化就是试着使用移位和加法运算的组合来代替乘以常数因子的乘法.
(3) 在大多数的机器上,整数除法要比整数乘法更慢-需要30或者更多的始终周期.除以2的幂也可以用移位运算来实现,只不过我们用的是右移,而不是左移.对于无符号和二进制补码数,分别使用逻辑移位和算术移位来达到目的.


//============================================
2010.07.08
深入理解计算机系统
(1) 反汇编器一些特性说明:
1) IA32指令长度从1~15个字节不等.指令编码被设计成使常用的指令以及操作较少的指令所需的字节数少,二那些不太常用或操作数较多的指令所需字节数较多.
2) 指令格式是按照这样一种方式设计的,从某个给定位置开始,可以将字节唯一地解码成机器指令.例如,只有指令pushl %ebp是以字节值55开头的.
3) 反汇编器只是根据目标文件中的字节序列来确定汇编代码的.它不需要访问程序的源代码或汇编代码.
4) 反汇编器使用的指令命名规则与GAS(Gnu ASembler)使用的有些细微的差别.
5) 与code.s中的汇编代码相比,我们发现结尾多了一条nop指令.这条指令根本不会被执行(它在过程返回指令之后),即使执行了也不会有任何影响(所以称之为nop,是"no operation"的简写,同城读作"no op").编译器插入这样的指令是为了填充存储该过程的空间.
(2) IA32加了一条限制,传送指令的两个操作数不能都指向存储器位置.将一个值从一个存储器位置拷到另一个存储器位置需要两条指令-第一条指令将源值加载到寄存器值写入目的位置.
(4) 根据惯例,所有返回真书或指针值的函数都是通过将结果放在寄存器%eax中来达到目的的.
(5) 加载有效地址(Load effective address)指令leal实际上是movl指令的变形.它的指令形式是从存储器读取数据到寄存器,但实际上它根本就没有引用存储器.它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数(如寄存器).
(6) 一元操作,只有一个操作数,既作源,也作目的.这个操作数可以是一个寄存器,也可以是一个存储器位置.比如说incl(%esp)会是栈顶元素加1.这种语法让人想起C中的加1运算符(++)和减1(--).
(7) 二元操作,第二个操作数既是源又是目的.这种语法让人想起C中向+=这样的赋值运算符.不过要注意,源操作数是第一个,目的操作数是第二个,这是不可交换操作特有的.例如,指令subl %eax, %edx使寄存器%edx的值减去%eax中的值.第一个操作数可以是立即数、寄存器或存储器位置.第二个操作数可以是寄存器或是存储器位置.不过同 movl指令一样,两个操作数不能同时都是存储器位置.
(8) divi指令执行无符号除法,通常会事先将寄存器%edx设置为0.


//============================================
2010.07.09
深入理解计算机系统
(1) 汇编语言中,直接跳转时给出一个标号作为跳转目标地;间接跳转的写法是"*"后面跟一个操作数指示符.如
jmp *%eax    表示用寄存器%eax中的值作为跳转目标;
jmp *(%eax)    表示已%eax中的值作为读地址,从存储器中读出跳转目标;
(2) call指令有一个目标,指明被调用过程起始的指令地址. 同跳转一样,调用可以是直接的,也可以是间接的.在汇编代码中,直接调用的目标是一个标号,而间接调用的目标是*后面跟一个操作数指示符,其语法与movel指令的操作数的语法相同.
(3) call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处.返回地址是紧跟在程序中call后面的那条指令的地址.这样当被调用过程返回时,执行会从此继续.ret指令从栈中弹出地址,并跳转到那个位置.要正确使用这条指令,就要使栈准备好,栈指针要指向前面call指令存储返回地址的位置.leave指令可以用来使栈做好返回的准备.它等价于下面的代码序列:
movl %ebp, %esp    //set stack pointer to beginning of frame
popl %ebp // restore saved %ebp and set stack ptr to end of call's frame
另外这样准备工作也可以通过直接使用传送和弹出操作来完成.
寄存器%eax可以用来返回值,如果函数要返回整数或指针的话.
(4) 根据惯例,寄存器%eax, %edx, 和%ecx被划分为调用者保存(caller save)寄存器.当过程p调用Q时,Q可以覆盖这些寄存器,而不会被破坏任何P所需要的数据.
另外,寄存器%ebx, %esi和%edi被划分为被调用者保存(callee save)寄存器.这意味着Q必须在覆盖他们之前,将这些寄存器的值保存到栈中,并在返回前恢复他们,应为P(或某个更高层次的过程)可能会在今后的计算中需要这些值.此外,根据这里描述的惯例,必须保持寄存器%ebp和%esp.
(5) 单操作数的操作符&和*可以产生指针和间接引用指针.也就是,对于一个表示某个对象的表达式Expr,&Expr表示一个地址.对于表示一个地址的表达式Addr-Expr,*Addr-Expr表示该地址中的值.因此,表达式Expr与*&Expr是等价的.
可以对数组和指针应用数组下标操作,如数组引用A[i]与表达式*(A+i)是一样的.它计算第i个数组元素的地址,然后访问这个存储器位置.
(6) 数组元素在存储器中是按照"行优先"的顺序排列的,这就意味着先是行0的所有元素,后面是行1的所有元素,以此类推.
(7) 一个联合的总的大小等于它最大域的大小.
(8) 无论数据是否对齐,IA32硬件都能正确工作.不过,Intel还是建议要对齐数据以提高存储器系统的性能.Linux沿用的对齐策略是2字节数据类型 (例如short)的地址必须是2的倍数,而较大的数据烈性(例如int, int*, float和double)的地址必须是4的倍数.注意,这个要求就意味着一个short类型对象的地址最低位必须等于0.类似地,任何int类型的对象或指针的地址的最低两位必须都是0.

//============================================
2010.07.13
深入理解计算机系统
(1) 编写高效程序需要两类活动:
第一、我们必须选择一组最好的算法和数据结构;
第二、我们必须编写出编译器能够有效优化以转换成高效可执行代码的源代码.
(2) 优化程序性能的基本策略:
1) 高级设计.为手边的问题悬着适当的算法和数据结构.要特别警觉,避免使用会渐进的产生糟糕性能的算法或编码技术.
2) 基本编码原则. 避免限制优化的因素,这样编译器就能产生高效的代码.
消除连续的函数调用.在可能时,将计算移到循环之外.考虑有选择的妥协程序的模块性以获得更大的效率.
消除不必要的存储器引用.引入临时变量来保存中间结果.只有在最后的值计算出来时,才将结果存放到数组或全局变量中.
3) 低级优化.
尝试各种与数组代码相对的指针形式.
通过展开循环降低循环开销.
通过诸如迭代分隔之类的技术,找到使用流水线的功能单元的方法.
最后的忠告,要小心避免花费精力在令人位家的结果上.一项有用的技术是.在优化代码时使用检查代码(checking code)来测试代码的每个版本,以确保在这一过程中没有引入错误,检查代码将一系列测试应用到程序上,确保它得到期望的结果.
(3) 量化评价一个程序中局限性的简单原则:
重复引用同一个变量的程序有良好的时间局限性.
对于具有步长为k的引用模式的程序,步长越小,空间局限性越好.具有步长为1的引用模式的程序有很好的空间局限性.在存储器中以大步长跳来跳去的程序空间局限性会很差.
对于取指令来说,循环有好的时间和空间局限性.循环体越小,循环迭代次数越多,局部性越好.
(4) 推荐以下技术:
将你的注意力集中在内部循环上,大部分计算和存储器访问都发生在这里.
通过按照数据对象存储在存储器中的顺序来读取数据数据,从而使得你的程序中的空间局部性最大.
记住,不明中率只是确定你代码性能的一个因素(虽然是重要的).存储器访问数量也 扮演着重要角色,有时需要在两者之间做一下折中.


//============================================
2010.07.19
深入理解计算机系统
(1) 学习链接只是的原因:
理解连及其将帮助你构造大型程序.
理解连接将帮助你避免一些危险的编程错误.
理解连将帮助你理解其他重要的系统概念.
理解链接将使你能够开发共享库.

(2) 一个典型的ELF可重定位目标文件包含下面几个节:
.text:以编译程序的机器代码.
.rodata:只读数据,如printf语句中的格式串和开关(switch)语句的跳转表.
.data:以初始化的全局c变量.局部C变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss节中.
.bss:未初始化的全局变量.在目标文件中这个节不占据实际的空间,它仅仅是一个占位符.目标文件格式区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间.
.symtab:一个符号表(symbol table),他存放在程序中被定义和引用的函数和全局变量的信息.
.rel.text:当连接器把目标文件和其他文件结合时,.text节中的许多位置都需要修改.一般而言,任何调用外部函数或者引用全局变量的指令都需要修改.另一方面,调用本地函数的指令则不需要修改.注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示连接器包含这些信息.
.rel.data:被模块定义或引用的任何全局变量的信息.一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改.
.debug:一个调试符号表,其中有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源代码.只有以-g选项调用编译驱动程序时,才会得到这张表.
.line:原始C源程序中的行号和.text节中机器指令之间的映射.只有以-g选项调用编译驱动程序时才会得到这张表.
.strtab:一个字符串表,其内容包括.systab和.debug节中的符号表,以及节头部中的节名字.字符串表就是以null结尾的字符串序列.

(3) 利用static属性隐藏变量和函数的名字.
C程序员使用static属性在模块内部隐藏变量和函数声明,就像你在jave和c++中使用public和private声明一样.C源代码文件扮演模块的角色.任何声明带有static属性的全局变量或者函数都是模块私有的.类似的,任何声明为不带static属性的全局变量和函数都是公开的,可以被其他模块访问.尽可能用static书香来保护你的变量和函数时很好的编程习惯.

(4) 函数和已经初始化的全局变量是强符号,未初始化的全局变量是弱符号.
(5) 根据强弱符号的定义,unix连接器使用下面的规则来处理多出定义的符号:
规则1:不允许有多个强符号.
规则2:如果有一个强符号和多个弱符号,那么选择强符号.
规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个.


//============================================
2010.07.20
深入理解计算机系统
(1) 共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物.
共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序连接起来,这个过程称为动态链接(dynamic linking),是由一个叫动态连接器(dynamic linker)的程序来执行的.
(2) 共享库的"共享"在两个方面有所不同.首先,在任何给定的文件系统中,对于一个库只有一个.so文件.所有引用该库的可执行目标文件共享这个.so文件中的代码与数据,而不像静态库的内容那样被拷贝和嵌入到引用它们的可执行的文件中.其次,在存储器中,一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享.
(3) java定义了一个标准调用规则,叫做jave本地接口(jave native interface, JNI),它允许java程序调用"本地的"c和C++函数,JNI的基本思想是将本地的C函数,比如说foo,编译到共享库中,如foo.so.当一个长在运行的java程序试图调用函数foo时,java解释程序利用dlopen接口(或者某个类似于此的东西)动态链接和加载foo.so,然后再调用函数foo.
(4) 理解ECF很重要的原因:
理解ECF将帮助你理解重要的系统概念.
理解ECF将帮助你理解应用程序是如何与操作系统交互的.
理解ECF将帮助你编写有趣的新应用程序.
理解ECF将帮助你理解软件异常如何工作.
(5) 当异常处理程序完成处理后,根据引起一场的事件的类型,会发生一下三种情况中的一种:
1. 处理程序将控制返回给当前指令Icurr(当事件发生时正在执行的指令).
2. 处理程序将控制返回给Inext(如果没有发生异常将会执行的下一条指令).
3. 处理程序终止被中断的程序.
(6) 异常的类型有:中断(interrupt), 陷阱(trap), 故障(fault)和终止(abort).
(7) 任何逻辑流在时间上和另外的逻辑流重叠的进程被称为并发进程(concurrent process).
而这两个进程就被称为并发运行.
进程和其他进程轮换运行的概念称为多任务(multitasking).一个进程执行它的控制流的一部分的每一时间段叫做时间片(time slice).因此,多任务也叫做时间分片(time slicing).
(8) 进程的三种状态:
运行.进程要么在CPU上执行,要么在等待被执行且最终会被调度.
暂停.进程的执行被挂起(suspended),且不会被调度.
终止.进程永远地停止.进程因为三种原因终止:收到一个信号,该信号的默认行为是终止进程;从主程序返回;调用exit函数.
(9) 一个终止了但还未被回收的进程成为僵死进程(zombie).


//============================================
2010.07.21
深入理解计算机系统
(1) 任意时刻,虚拟页面的集合都分为三个不相交的子集:
未分配的: VM系统还未分配(或者创建)的页.未分配的块没有任何数据和他们相关联,因此也就不占用任何磁盘空间.
缓存的: 当前缓存在物理存储器中的已分配页.
未缓存的: 没有缓存在物理存储器中的已分配页.

(2) 显示分配器的一些相当严格的约束条件:
处理任意请求序列.
立即响应请求.
只使用堆.
对齐块(对齐要求).
不修改已分配的块.

(3) 与套接字限定相关的流限定:
限定一:输入函数跟在输出函数之后.如果中间没有插入对fflush, fseek, fsetpos或者rewind的调用,一个输入函数不能跟在一个输出函数之后.fflush函数清空与流相关的缓冲区.
限定二:输入函数跟在输入函数之后. 如果中间么有插入对fflush, fseek, fsetpos或者rewind的调用, 一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个文件结束.
对I/O流的第一个限定能够通过采用在每个输入操作前刷新缓冲区这样的规则来保证实现.
保证实现第二个限定的唯一办法是,对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写;
FILE *fpin, *fpout;
fpint = fopen(sockfd, "r");
fpout = fopen(sockfd, "w");
但是这样做也有问题,因为它要求应用程序在两个流上都要调用fclose,这样才能释放与每个流相关联的存储器资源,避免存储器xie lou(bd太龌龊了).
fclose(fpin);
fclose(fpout);


//============================================
2010.08.05
深入理解计算机系统
(1) 四类线程不安全函数.
第一类: 不保护共享变量的函数;
第二类: 保持跨越多个调用的状态的函数;
第三类: 返回指向静态变量的指针的函数;
第四类: 调用线程不安全函数的函数;

(2) 死锁.
程序员使用p和v操作顺序不当,以至两个信号量的禁止区域(forbidden region)重叠.
重叠的禁止区域引起了一组称为死锁区域(deadlock region)的状态.
死锁是一个相当困难的问题,因为它不总是可预测的.

(3) 使用简单而有效的规则来避免死锁.
互斥锁加锁顺序规则: 如果对于程序中每对互斥锁(s, t), 每个既包含s也包含t的线程都按照相同的顺序同时对他们加锁,那么这个程序就是无死锁的. 














你可能感兴趣的:(计算机系统网络知识)