可以借鉴!
原文地址:CS:APP笔记+每章总结作者:唳天飞鹰
CS:APP(Computer Systems:A Programmmer’s Perspective),中文译名:深入理解计算机系统,可谓是一本非常经典的书了,有人甚至说是程序员必看书籍之一。
由于前辈们(尤其是嘉哥,在嘉哥的相关技术书籍里发现的这本书)对这本书的赞誉不断,所以我也找了本看看,只有一个感觉:太厚了!在读第一章的时候,还是很容易入戏的,可是越到后来越是难以入戏啊,各种硬件知识有木有,都出来处理器设计了(书中仿照IA32体系,也就是我们平时所说的x86,自己搞了个Y86指令集,而且用各种组合电路来实现这个指令集,晕死,有木有,有木有!),总的来说,书中确实涉及到不少方面的知识,而且很多知识对于我们理解程序和硬件的结合是特别有帮助的!
前言:计算机系统漫游
这个是我唯一读的比较轻松的地方,嘿嘿!写一下我作的笔记吧!
1。1信息就是位+上下文
程序源文件就是由bit组成的。
1。2程序被其他程序翻译成不同的格式
源文件--目标文件---可执行文件
gcc还可以将c程序翻译成汇编代码gcc -s test test.c
gcc还可以对程序进行一定的编译优化:gcc -O2 -o test test.c
1。3了解编译系统如何工作的好处
优化程序的性能、理解链接时出现的各种错误、避免安全漏洞
1。4处理器读并解释存储在存储器中的指令
硬件组成:
cpu: pc 、寄存器堆、ALU、总线接口
总线:传送定长的字节块,被称为字
I/o设备:控制器(一般在主板上)+适配器
主存:由DRAM芯片组成
1。5高速缓存
L1和L2
1。6层次结构存储设备
寄存器--》L1--》L2--》DRAM--》本地二级存储--》远程二级存储
1。7操作系统管理硬件
[转载]CS:APP笔记+每章总结
进程、线程、虚拟存储器、文件(将一切看作文件)
进程的虚拟地址空间:
[转载]CS:APP笔记+每章总结
前言部分总结:
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据不同的上下文又有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文本。
处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量的时间在存储器、I/O设备和CPU寄存器之间拷贝数据,所以系统中的存储设备就被按层次排列,CPU寄存器在顶部,接着是多层硬件高速缓存存储器、DRAM主存储器和磁盘存储器。在层次模型中位于更高层的存储设备比低层的存储设备要快,单位比特造价也更高。程序员通过理解和运用这种存储层次结构的知识,可以优化他们C程序性能。
操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象概念:文件是对I/O设备的抽象概念;虚拟存储器是对主存和磁盘的抽象概念;进程是处理器、主存和I/O设备的抽象概念。
最后,网络提供了计算机系统之间通信的手段。从某个系统的角度来看,网络就是一种I/O设备。
第一部分:程序结构和执行
我们对计算机系统的探索是从学习计算机本身开始的,它由处理器和存储器子系统组成。在核心部分,我们需要方法来表示基本数据类型,比如整数和实数去处的近似值。然后,我们考虑机器级指令如何操作这样的数据,编译器如何将C程序翻译成这样的指令。接下来,我们研究几种实现处理器的方法,来更好地了解如何使用硬件资源来执行指令。一旦我们理解了编译器和机器级代码,我们就能通过编写可以高效理解编译的源代码,来分析如何最大化程序的性能。我们以存储器子系统的设计来结束本部分,这是现代计算机系统最复杂的部分之一。
第2章:信息的表示和处理
2。1信息的存储
大多数计算机使用8位的块,或叫做字节,来作为最小的可寻址的存储器单位,而不是访问存储器中单独的位,这就产生了虚拟存储器。学过操作系统的同学,应该还记得虚拟地址是怎么和物理地址对应起来的。
信息是按二进制的位存储的,但是我们还应该掌握二进制、十进制和十六进制的转换。
还要注意,不同的硬件系统的,信息的存储也是有一些差别的,比如我们所熟悉的大端存储和小端存储。小端法指最低有效字节在最前面的低位,大端法指最高有效字节在最前面的低位。
在信息系统中,还有一个特殊的运算体系,那就是布尔代数体系,并且由此产生了布尔环(主要用于高质量的CD等,比如CD被磨损了,仍能正常寻址,都是布尔环的功劳)。
2。2整数表示
C支持多种整型数据类型--表示有限范围的整数。
无符号数和二进制补码编码表示。
有符号数和无符号数的转换:存储的位不变,不过编译器解释不同了。
2。3整数运算
无符号数的加法。
二进制补码的加法。
无符号的乘法。
乘以2的幂。移位实现,左移
除以2的幂,移位实现,右称,只需要一个时钟周期。
浮点数:具体想想计算机组成原理那块吧,规格化和非规格化、溢出
第2章小结:
计算机将信息编码为位(比特),通常组织成字节序列。有不同的编码方式用来表示整数、实数和字符串。不同的计算机模型在编码数字和多字节数据中的字节顺序上使用不同的约定。
C语言被设计成包容多种不同字长和数字编码的实现。虽然高端机器逐渐开始使用64位字长,但是目前大多数机器仍使用32位字长。大多数机器对整数使用二进制补码编码,而对浮点数使用IEEE编码。在位级上理解这些编码,并且理解算术运算的数学特性,对于编写能在全部数值上正确运算的程序来说,是很重要的的。
C语言的标准规定在无符号和有符号整数之间进行强制类型转换时,基本的位模型不应改变。C语言中隐式的强制类型转换会得到许多程序员无法预计的结果,常常导致程序错误。
由于编码长度有限,计算机运算与传统整数和实数相比,具有非常不同的属性。当超出表示范围时,有限长度能够引起数值溢出。当浮点数非常接近0.0时,从而转换成0时,浮点数也会下溢。
和大多数其他程序一样,C语言实现的有限整数运算和真实的整数运算相比有一些特殊的属性。例如,由于溢出,表达式xx有可能会得到一个负数。但是,无符号数和二进制补码的运算都能满足环的属性。这就允许编译器做很多的优化。例如,用(x<<3)-x取代表示7x时,我们就利用了结合性、交换性和分配性,还利用了移位和乘以2的幂之间的关系。
我们已经看到了几种使用位级运算和算术运算组合的聪明方法。
浮点数表示通过将数字编码为x*2^y的形式来近似的表示实数。最常见的浮点数表示是由IEEE标准754定义的。必须非常小心地使用浮点数,因为浮点数运算的范围和精度非常有限,而且浮点数并不遵守普遍的算术属性,比如结合性。
第3章程序的机器级表示
3.1历史观点
这一节简要地回顾了Intel的体系结构。Intel处理器从1978年那个想法简单的16位处理器发展而来,现在已经成为桌面计算机的主流机器。随着新特性的加入,体系结构也在相应地成长,从16位体系结构转变成了支持32位(甚至64位和128位)数据和地址的结构。
3.2程序编码
假如我们写一个C程序,有两个文件p1.c和p2.c,然后我们用Unix命令行编译这些代码:
unix> gcc -O2 -o p p1.c p2.c
命令gcc表明的就是GNU C编译器GCC。编译选项-O2告诉编译器使用第二级优化。通常,提高优化级别会使最终程序运行得更快,但是编译时间可能会变长,对代码进行调试会更困难。第二级优化是性能优化和使用方便之间的一种很好的妥协。
这个命令实际上调用了一系列程序,将源代码转化为可执行代码。首先,C预处理器会扩展源代码,插入所有#include命令指定的文件,并扩展所有宏。其次,编译器产生两个源文件的汇编代码,名为p1.s和p2.s。接下来,汇编器会将汇编代码转化成二进制目标代码文件p1.o和p2.o。最后,链接器将两个目标文件与实现标准Unix库函数的代码合并,并产生最终的可执行文件。
3.3数据格式
由于是从16位体系结构扩展成32位的,Intel使用术语“字”表示16位数据类型。因此,称32位数为“双字”,称64位为“四字”。
普通整数和长整数都是双字的,无论它们是否有符号。
字符类型是单字节。
浮点数有三种形式:单精度(4字节)为float,双精度(8字节)为double。
3.4访问信息
一个IA32中央处理器单元(CPU)包含一组八个存储32位值的寄存器,这些寄存器用来存储整数数据和指针。字操作指令可以独立地读或者写前四个寄存器的两个低位字节。
操作数指示符:大多数指令有一个或多个操作数,指示出执行一个操作中要引用的源数据值,以及放置结果的目的位置。各种操作数的可能性被划分为三种类型:立即数、寄存器、存储器引用。
有多种不同的寻址模式,允许不同形式的存储器引用。这类寻址方式比较多,参考:http://baike.baidu.com/view/889427.htm
3.5算术和逻辑操作
加载有效地址指令:leal,实际上是movl指令的变形,它从存储器读数据到寄存器。
一元和二元操作符:一元操作,只有一个操作数,既作源,也作目的。二元操作,第二个操作数既是源又是目的。
移位操作:先给出移位量,然后是待移位的值。
3.6控制
程序执行的一个很重要的部分就是控制被执行操作的顺序。
条件码:CF、ZF、SF、OF等。
一些跳转指令根据这些条件码进行跳转。
3.7过程
一个过程调用包括将数据和控制从代码的一部分传递到另一部分。
IA32使用栈帧结构来支持过程调用。栈用来传递过程参数、存储返回信息、保存寄存器以供以后恢复之用,以及用于要地存储。为单个过程分配的那部分栈称为栈帧。
3.8数组分配和访问
C中数组是一种将标题型数据聚焦成更大数据类型的方式。C用来实现数组的方式非常简单。C的一个不同寻常的特点是可以对数组中的元素产生指针,并对这些指针进行运算。
动态分配的数组:C只支持大小在编译时就能知道的多维数组,在许多应用程序中,我们需要代码能够对动态分配的任意大小的数组进行操作,在C中用malloc和calloc两个函数。
3.9异类的数据结构
C提供了两种将不同的类型的对象结合到一直来创建数据类型的机制:结构和联合。
C的结构声明创建一个数据类型,将可能不同类型的对象聚合到一个对象中,结构的各个组成部分是用名字来引用的。结构的实现类似于数组的实现,因为结构的所有组成部分都存放在存储器中连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器保存关于每个结构类型的信息,指示每个域的字节偏移。它以这些偏移作为存储器引用指令字节中的位移,从而产生对结构元素的引用。
联合提供了一种方式,能够规避C的类型系统,允许以多种类型来引用一个对象。联合声明的语法与结构的语法一样,只不过语义相差较大。它们不是用不同的域来引用不同的存储器块,而是引用同一存储器块。
3.10对齐
许多计算机系统对基本数据类型的可允许地址做出了一些限制,要求某种类型的对象的地址必须是某个值K(通常是2、4、8)的倍数。这种对齐限制简化了处理器和存储器系统之间接口的硬件设计。
3.11综合:理解指针
指针是C语言的一个重要特色。它们提供一种统一方式,能够远程访问数据结构。
1)每个指针都有一个值。这个值是指定类型对象的地址。特殊的NULL(0)值表示该指针没有指向任何地方。
2)指针是用&运算符创建的。
3)*操作符用于指针的间接引用。
4)数组与指针是紧密联系的。
5)指针也可以指向函数。
3.12现实生活:使用GDB调试器
GNU的调试器GDB提供了许多有用的特性来支持对机器级程序的运行时评估的分析。
GDB常用命令:
开始和停止:quit run kill
断点:break sum(sum is a function) break *0x80483c3(0x80483c3是地址) delete 1(删除断点1) delete(删除所有断点)
执行:stepi (执行一步) stepi 4(执行4步) nexti (与stepi相似,但是会不会进入函数调用)
continue(继续执行) finish(运行直到当前函数返回)
检查代码:disas(检查当前代码) disas sum(检查sum函数)
检查数据:print $eax(打印寄存器)
3.13.存储器的越界引用和缓冲区溢出
C对于数组引用不进行任何边界检查,而且局部变量和状态信息都存放在栈中。这两种情况结合到一起,就能导致严重的程序错误,一个对越界数组元素的写操作破坏了存储在栈中的状态信息。
另一种特别常见的状态破坏称为缓冲区溢出。通常,在栈中分配某个字节数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。
3.14小结
在本章中,我们窥视了高级语言提供的抽象层下面的东西,以了解机器级编程。通过让编译器产生机器级的汇编代码表示,我们了解了编译器和它的优化能力,以及机器代码、它的数据类型和它的指令集。
汇编语言和C代码差别很大。在汇编语言程序中,各种数据类型之间的差别很小。程序是以指令序列来表示的,每条指令都完成一个单独的操作。部分程序状态,如寄存器和运行时栈,对程序员来说是直接可见的。仅提供了低级操作来支持数据处理和程序控制。编译器必须用多条指令来产生和操作各种数据结构,来实现像条件、循环和过程这样的控制结构。我们讲述了C和如何编译C的许多不同之处。我们看到C缺乏边界检查,使得许多程序容易出现缓冲区溢出,而这已经使许多系统容易受到入侵者的恶意攻击。
我们只分析了C到IA32的映射,但是我们讲的大多数内容对其他语言和机器组合来说也是类似的。例如编译C++与编译C就非常相似,实际上,C++的早期实现就只是简单的执行了从C++到C的源到源的转换,并对结果运行C编译器,产生目标代码。C++的对象用结构来表示,类似于C的struct。C++的方法是用指向实现方法的代码的指针来表示的。相比而言,java的实现方式完全不同。java的目标代码是一种特殊的二进制表示称为java字节码。这种代码可以看成是虚拟机的机器级程序。正如它的名字暗示的那样,这种机器并不是直接用硬件实现的。相反,软件解释器处理字节码,模拟虚拟机的行为。这种方法的优点是相同的java字节码可以在许多不同的机器上执行。
第四章:处理器体系结构
本章依照X86指令集自己造了个Y86指令集,并根据这个指令集设计了一个CPU。
由于本章偏向硬件,所以在此笔记做的比较粗糙。
4.1Y86指令集
大体意思是说Y86是X86的精简版。
4.2逻辑设计和硬件控制语言HCL
在这节中讲到了逻辑门、组合电路和HCL布尔表达式、字级的组合电路和HCL整数表达式、集合关系、存储器和时钟控制
4.3Y86的顺序实现
本节主要讲了将处理组织成阶段、SEQ的硬件结构、SEQ的时序、SEQ+:重新安排计算阶段
4.4流水线的通用原理
流水线的一个重要特征是增加了吞吐量,也就是单位时间内服务的顾客总数,不过它也会轻微地增加执行时间,也就是服务一个用户需要的时间。
流水线也是存在局限性的:不一致的划分;深度流水线,收益反而下降;带反馈的流水线。
4.5流水线的实现
插入流水线寄存器、对信号进行重新排列和标号、流水线冒险、用暂停来避免数据冒险;用前递来避免数据冒险;加载和使用数据冒险
4.6小结
我们已经看到,指令集体系结构(ISA)在处理器行为(就指令集合及其编码而言)和如何实现处理器之间提供了一层抽象。ISA提供了程序执行的一种顺序说明,也就是一条指令执行完了,下一条指令才会开始。
基于ISA指令集,并且大大简化其数据类型、地址模式和指令编码,我们定义出了Y86指令集,得到的ISA既有RISC指令集的属性,也有CISC指令集的属性。然后,我们将不同指令组织放到六个阶段中处理,在此,根据被执行指令的不同,每个阶段中的操作也不相同。从此,我们构造了SEQ处理器,其中每个时钟周期推进一条指令通过每个阶段。通过重新排列各个阶段,我们创建了SEQ+设计,其中第一个阶段选择程序计数器的值,它被用来取出当前指令。
流水线化通过让不同的阶段并行操作,改进了系统的吞吐量性能。在任意一个给定的时间,多条指令被处理。在引入这种并行性的过程中,我们必须非常小心,以提供与程序的顺序执行相同的用户可见的、程序级行为。我们通过往SEQ+中添加流水线寄存器,并重新安排周期来创建PIPE-流水线,介绍了流水线化。然后,我们添加了前递逻辑,加速了将结果从一条指令发送到另一条指令,从而提高了流水线的性能。有几种特殊情况需要额外的流水线控制逻辑来暂停或取消一些流水线阶段:
在本章中,我们学习了有关处理器设计的几个重要的经验:
1)管理复杂性是首要问题。我们想要优化使用硬件资源,在最小的成本下获得最大的性能。为了实现这个目的,我们创建了一个非常简单而一致的框架,来处理所有不同指令类型。有了这个框架,我们就能够在处理不同指令类型的逻辑中间共享硬件单元。
2)我们不需要直接实现ISA。ISA的直接实现意味着一个顺序的设计。为了获得更高的性能,我们想运用硬件能力以同时执行许多操作,这就导致要使用流水线化的设计。通过仔细的设计和分析,我们能够处理各种流水线冒险,因此运行一个程序的整体效果,同用ISA模型获得的效果完全一致。
3)硬件设计人员必须非常谨慎小心。一旦芯片被制造出来,就几乎不可能改正任何错误了。一开始就使设计正确是非常重要的。意思就是,仔细地分析各种指令类型和组合情况,甚至于那些看上去没有意义的情况,例如弹出栈指针。必须用系统的模拟测试程序彻底地测试设计。
第五章:优化程序性能
编写高效程序需要两类活动:第一,我们必须选择一组最好的算法和数据结构;第二,我们必须编写出编译器效优化以转换成高效可执行代码的源代码。
5.1优化编译器的能力和局限性
编译器优化程序的能力受几个因素的限制,包括:要求它们绝不能改变正确的程序行为;它们对程序行为、对使用它们的环境了解有限;需要很快地完成编译工作。
编译器必须假设不同的指针可能会指向存储器中同一位置。这就造成了一个主要的妨碍化的因素。
5.2表示程序性能
我们需要一种方法来表示程序性能,它能指怃们改进代码。对许多程序都很有用的度量标准是每元素的周期数(CPE)。这种度量标准帮助我们在更详细的级别上理解迭代程序的循环性能。
5.3消除循环中的低效率
在循环中不变的求值运算拿到循环外。这种优化称为代码移动优化。
5.4减少过程调用
过程调用会造成想法大的开销,而且妨碍大多数形式的程序优化。
对于性能至关重要的程序来说,为了速度,经常必须要损害一些模块性的抽象性。
5.5消除不必要的存储器引用
可以把一个在循环中经常要用到的存储器的值用临时变量来代替。
5.6理解现代处理器
这节讲解了PentiumIII处理器一个简单的工作模型。
整个设计分为两个部分:ICU(指令控制单元)和EU(执行单元)。前者负责从存储器中读出指令序列,并根据这些指令序列生成一组针对程序数据的基本的操作;而后者执行这些操作。
ICU从指令高速缓存中读取指令,指令高速缓存是一个特殊的高速缓存存储器,它包含最近访问的指令。不过当程序遇到转移时,程序有两种可能的前进方向:一种是选择转移,控制被传递到转移目标;另一种是不选择转移,控制被传递到指令序列的下一条指令。现代处理器采用了一种称为转移预测的技术,在这种技术中处理器会预测是否选择转移,同时还预测转移的目标地址。使用一种投机执行的技术。
5.7降低循环的开销
本节主要几种不太常用的降低循环开销的方法。比如在一个循环中连续进行几个操作。
5.10提高并行性
循环分割
5.11小结
虽然关于代码优化的大多数论述都描述了编译器是如何能生成高效代码的,但是应用程序员有很多方法来协助编译器完成这项任务。没有任何编译器能用一人好的算法或数据结构代替低效率的算法或数据结构,因此程序设计的这些方面仍然应该是程序员主要关心的。我们还看到妨碍优化的因素,例如存储器别名和过程调用,严重限制了编译器执行大量优化的能力。同样,程序员必须对消除这些妨碍优化的负主要责任。
除些之外,我们还研究了一系列技术,包括循环展开、迭代分割以及指针运算。随着我们对优化的深入,研究汇编代码以及试着理解机器是如何执行计算的变得重要起来。对于现代、乱序处理器上的执行,分析程序是如何在有无限处理资源但是功能单元的执行时间和发射时间与目标处理器相符的机器上执行的,收获良多。为了精练这个分析,我们还应该考虑诸如功能单元数量和类型这样的资源约束。
包含条件转移可与存储器系统复杂交互的程序,比我们首先考虑的简单循环程序,更加难以分析和优化。基本策略是使循环更容易预测,并试着减少存储和加载操作之间的相互影响。
当处理大型程序时,将我们的注意力集中在最耗时的部分变得很重要。代码剖析程序和相关的工具能帮助我们系统地评价和改进程序性能。我们描述了GPROF,一个标准的Unix剖析工具。也还有更加复杂完善的剖析程序可用,例如Intel的VTUNE程序开发系统。这些工具可以在过程级分解执行时间,测量程序每个基本块的性能。基本块是没有条件操作的指令序列。
第六章:存储器的层次结构
6.1存储技术
静态RAM(SRAM):一个又稳态的存储单元
动态RAM(DRAM) : DRAM将每个位存储为对电容充电。
磁盘存储:构造:每个盘片有两个面,表面覆盖着磁性记录材料;磁盘容量:记录密度、磁道密度、面密度;磁盘操作:寻道时间、旋转时间、传送时间;逻辑磁盘块:盘面、磁道、扇区。
6.2局部性
局部性通常有两种形式:时间局部性和空间局部性。
局部性小结:
1)重要引用同一个变量的程序有良好的时间局部性。
2)对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。
3)对于取指令来说,循环有好的时间和空间局部性。循环越小,循环迭代次数越多,局部性越好。
6.3存储器的层次结构
整个计算机体系的存储器层次系统:寄存器-》L1级缓存—》L2级缓存-》DRAM主存-》本地二级缓存-》远程二级缓存。
缓存体系:数据总是块为传送单元。
缓存命中:
缓存不命中:覆盖一个现存的块的过程被称为替换。替换策略:随机替换、LRU(最近最少使用)。
6.4高速缓存存储器
现在系统中,L1级高速缓存一般是位于CPU芯片中,L2高速缓存连接到存储器总线或连接到它自己的高速缓存总线。
缓存与被缓存的映射关系:直接映射、组相联、全相联。
有关写的问题:写命中:直写法、写回法。
写不命中:写分配-》加载相应的存储器到缓存 非写加法:避开高速缓存,直接更新存储器。
6.5小结
基本存储技术包括RAM(随机存储器)、ROM(非易失性存储器)和磁盘。RAM有两种基本类型。SRAM(静态RAM)快一些,但是也贵一些,它既可以用做CPU芯片上的高速缓存,也可以用做芯片外的高速缓存。动态RAM(DRAM)慢一点,也便宜一些,用做主存和图形帧缓冲区。非易失性存储器,也称为只读存储器(ROM),即使是在关电的时候,也能保持它们的信息,它们用来存储固件。磁盘是非易失性存储设备,以每个位很低的成本保存大量的数据。代价是较长的访问时间。
一般而言,较快的存储技术每个位会更贵,而且容量较小。这些技术的价格和性能属性正在动态地以不同的速度变化着。特别地,DRAM和磁盘访问时间滞后于CPU周期时间。系统通过将存储器组织成存储设备的层次经结构来弥补这些差异,在这个层次结构中,较小、较快的设备在顶部,较大、较慢的设备部。因为编写良好的程序有好的局部性,大多数数据都可以从较高层得到服务,结果就是存储系统能以较高层的速度运行,但却有较低层的成本和容量。
程序员可以通过编写良好空间和时间局部性的程序来动态地改进程序的运行时间。利用基于SRAM的高速缓存存储器特别重要,主要从L1高速缓存存取数据的程序能比从存储器取数据的程序运行快过一个数量级。
第二部分:在系统上运行程序
链接器把我们程序的各个部分联合成一个单独的文件,处理器可以将这个文件加载到存储器,并且执行它。现代操作系统与硬件合作,为每个程序提供一种幻像,好像这个程序是在独占地使用处理器和主存,而实际上在任何时文,系统上都有多个程序在运行。这部分,你将很好地理解程序和硬件之间的交互关系。本部分旨在拓宽你对系统的了解,使你牢固地掌握程序和操作系统之间的交互关系。你将学习到如何使用操作系统提供的服务来构建系统级程序。
第7章链接
链接就是将不同部分的代码和数据收集和组合成一个单一文件的过程,这个文件可被加载到存储器并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时,也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至可以执行于运行时,由应用程序来执行。
7.1编译器驱动程序
大多数编译系统提供编译驱动程序,它为用户,根据需求调用语言预处理器、编译器、汇编器和链接器。
我们拿GNU的GCC编译驱动程序来说一下,这里我们有一个文件main.c,首先驱动程序会运行C预处理器,将main.c翻译成一个ASCII码的蹭文件main.i,接下来,驱动程序运行C编译器,将main.i翻译成一个ASCII汇编语言文件main.s,随后驱动程序运行汇编器,将main.s翻译成一个可重定位的目标文件main.o。
在main.c中引用了swap.c,那么驱动程序会经过相同的过程把swap.c翻译成swap.o,最后,驱动程序会运行链接器程序,将main.o和swap.o以及一些必要的系统目标文件组合起来,创建了个可执行的目标文件。
7.2静态链接
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行文件作为输出。
为了创建可执行文件,链接器必须完成两个主要任务:
1)符号解析。目标文件定义和引用符号。符号解析的目的是将每个符号引用和一个符号定义联系起来。
2)重定位。编译器和汇编器生成从地址零开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
7.3目标文件
目标文件有三种形式:
1)可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
2)可执行目标文件。包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
3)共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时,被动态地加载到存储器并链接。
7.4可重定位的目标文件(ELF)
可重定位目标文件
ELF头、.text、.rodata、.data、.bss、.symtab、.rel.text、.rel.data、.debug、.line、.strtab、节头部表。
.text:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局C变量。
.bss:未初始化的全局C变量。
.symtab:一个符号表,它存放在程序中被定义和引用的函数和全局变量的信息。
.rel.text:当链接器把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
.rel.data:被模块定义或引用的全局变量信息。
.debug:一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源文件。只有以-g选项调用编译驱动程序时,才会生成这个表。
.line:原始C源程序中的行号和.text节中机器指令之间的映射。
.strtab:一个字符表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
7.5符号和符号表
每个可重定位的目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
1)由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数以及被定义为不带C的static属性的全局变量。
2)由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于定义在其他模块中的C函数和变量。
3)只被模块m定义和引用的本地符号。有的本地链接器符号对应于带static属性的C函数和全局变量。这些符号在模块m中的任何地方都是可见的,但是不能被其他模块引用。目标文件中对应于模块m的节和相应的源文件的名字也能获得本地符号。
7.6符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位的目标文件符号表中的一个确定的符号定义联系起来。
对本地符号的引用,符号解析非常简单明了。编译器只允许每个模块中的每个要地符号只有一个定义。编译器还确保静态本地变量,它们也会有本地链接器符号,拥有惟一的名字。
对全局符号的引用解析就棘手得多。当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,它会假设该符号是在其他某个模块中定义的,生成一个链接器符号表表目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用的符号,它就会输出一条错误信息并终止。
函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。根据强度弱符号的定义,Unix链接器使用下面的规则来处理多处定义的符号:
1)不允许有多个强符号。
2)如果有一个强符号和多个弱符号,那么选择强符号。
3)如果有多个弱符号,那么从这些弱符号中任意选择一个。
静态链接库、链接器如何使用静态库来解析引用。
7.7重定位
重定位由两步组成:
1)重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
2)重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。为了执行这一步,链接器依赖于称为重定位静止的可重定位目标模块中的数据结构。
7.8可执行目标文件
我们的C程序,开始时是一组ASCII文本,已经被转化为一个二进制文件,且这个二进制文件包含加载程序到存储器并运行它所需的所有信息。
7.9加载可执行目标文件
7.10动态链接共享库
7.11从应用程序中加载和链接共享库
7.12小结
链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的、可执行的和共享的。可重定位的目标文件由静态链接器组合成一个可执行的目标文件,它可以加载到存储器中并执行。共享目标文件是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或者根据需要在程序调用dlopen库的函数时。
链接器的两个主要任务是符号解析和重定位。符号解析将目标文件中的每个全局符号都绑定到一个惟一的定义,而重定位确定每个符号的最终存储器地址,并修改对那些目标的引用。
静态链接器是由像GCC这样的编译器调用的。它们将多个可重定位目标文件组合成一个单独的可执行目标文件。多个目标文件可以定义相同的符号,而链接器用来悄悄地解析这些多处定义的规则可能在用户程序中引入的微妙错误。
多个目标文件可以被连接到一个单独的静态库中。链接器用库来解析其他目标模块中的符号引用。许多链接器通过从左到右的顺序扫描来解析符号引用,这是另一个引起令迷惑的链接时错误的来源。
加载器将可执行文件的内容映射到存储器,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件中有未解析的到定义在共享库的程序和数据的引用。在加载时,加载器将部分链接的可执行文件映射到存储器,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。
被编译为位置无关代码的共享库可以加载到任何地方,也可以在运行时被多个进程共享。为了加载、链接和访问共享库的函数和数据,应用程序还可以在运行时使用动态链接器。
第八章异常控制流
8.1异常
异常是一种形式的异常控制流,它一部分是由硬件实现的,一部分是由操作系统实现的。
在任何情况中,当处理器检测到有事件发生时,它就会通过一张异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序—异常处理程序。
异常的类别:中断、陷阱、故障和终止。
8.2进程
异常提供基本的构造元素,它允许操作系统提供进程的概念,有了异常机制,系统中的每个程序都是运行在某个进程的上下文中的。
进程提供给应用程序的关键抽象:逻辑控制流、私有地址空间。
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器用某个控制寄存器中的一个模式位来提供这种功能的,这个模式位表明了处理器运行在用户模式还是内核模式。
操作系统内核利用一种称为上下文切换的较高级形式的异常控制流来实现多任务。
8.3系统调用和错误处理
Unix系统提供了大量的系统调用,当应用程序想向内核请求服务时,都可以使用这些系统调用。
当系统调用出现错误时,它们典型地会返回-1。
8.4进程控制
从程序员角度看,进程总是处于下面三种状态之一:运行、停止(挂起)、终止。.
进程控制包括:获取进程ID、创建和终止进程、回收子进程、让进程休眠和加载并运行程序
8.5信号
一个信号就是一个消息,它通知进程一个某种类型的事件已经在系统中发生了。
信号术语:发送信号、接收信号、处理信号。
信号处理问题:当一个程序要捕捉多个信号时,一些细微的问题就产生了。
1)待处理信号被阻塞。Unix信号处理程序典型地会阻塞当前处理程序正在处理的类型的待处理信号。假设一个进程捕捉了一个SIGINT信号,并且当前正在运行它的SIGINT处理程序。如果另一个SIGINT信号传递到这个进程,那么这个SIGINT将变成待处理的,但是不会被接收,直到处理程序返回。
2)待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为k的信号传送到一个目的进程,而由于目的进程当前正在执行信号k的处理程序,所以信号k是阻塞的,那么第二个信号就被简单地丢弃,它不会排队等待。关键思想是存在一个待处理的信号仅仅表明至少已经到达了一个信号。
3)系统调用可以被中断。像read、write和accept这样的系统调用潜在地会阻塞进行一段较长的时间,称为之慢速系统调用。在某些系统中,当处理程序捕捉到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并errno设置为EINTR。
8.6非本地跳转
C提供了一种形式的用户级异常控制流,称为非本地跳转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。非本地跳转是通过setjmp和longjmp函数来提供的。
8.7操作进行的工具
Unix系统提供了大量的监控和操作进程的有用工具:
strace:打印一个程序和它的子进程调用的每个系统调用的轨迹。
ps:列出系统中当前的进程。
top:打印出关于当前进行资源使用的信息。
kill:发送一个信号给进程。
/proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容,用户程序可以读取这些内容。
8.8小结
异常控制流发生在计算机系统的各个层次。在硬件层,异常是由处理器的事件触发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。
有四种不同类型的异常:中断、故障、终止和陷阱。当一个外部I/o设备,例如定时器芯片或者一个磁盘控制器,设置了处理器芯片上的中断管脚时,(对于任意指令)中断会异步地发生。控制返到中断指令的下一条指令。执行一条指令可能导致故障和终止的发生。故障处理程序会重新开始故障指令,而终止处理程序从不将控制返回给被中断的流。最后,陷阱就是用来实现系统调用的函数调用,系统调用提供给应用到操作系统代码的受控入口点。
在操作系统层,内核提供关于一个进程的基础性概念。一个进程提供给应用两个重要的抽象:1.逻辑控制流,这提供给每个程序一个假象,好像它是在独占地使用处理器;2.私有地址空间,它提供给每个程序一个假象,好像在独占地使用主存。
在操作系统和应用之间的接口处,应用可以创建子进程,等待它们的子进程暂停或者终止,运行新的程序,并捕捉来自其他进程的信号。信号处理的语义清楚地指定期望的信号处理语义。
最后,在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数转移到另一个函数。
第九章:测量程序执行时间
9.1计算机系统上的时间流
计算机是在两个完全不周的时间尺度上工作的。在微观级别,它们以每个时间周期一条或多条指令的速度执行指令,每个时钟周期大约为1ns;在宏观尺度上,处理器必须响应外部事件,外部事件发生的时间尺度要以ms来度量。
我们希望处理器从一个进程切换到另一个进程,这样用户看上去就好像处理器在同时执行许多程序一样。由于这个原因,计算机有一个外部计时器,它周期性地向处理器发送中断信号。这些中断信号之间的时间被称为间隔时间。
从应用程序的角度看时间,可以把时间流看成两种时间段的交替,一种时间段里程序是活动的,另一种时间段里程序是不活动的。
9.2通过间隔计数来测量时间
操作系统也用计时器来记录每个进程使用的累计时间,这种信息提供的是对程序执行时间不那么准确的测量值。
操作系统维护着每个进程使用的用户时间量和系统时间量的计数值,当计时器中断发生时,操作系统会确定哪个进程是活动的,并且对那个进程的一个计数值增加计时器间隔时间。
9.3周期计数器
为了给计时测量提供更高的精确度,许多处理器还包含一个运行在时钟周期级的计时器。这个计时器是个特殊的寄存器,每个时钟周期它都会加1。可以用特殊的机器指令来读这个计数器的值。不是所有的处理器都有这样的计数器的,而且有这样的计数器的处理器在实现细节上也各不相同。
9.4小结
本章开始时提出一个看似问题:“程序X在机器Y上运行得有多快?”不幸的是,计算机系统用来同时运行多个进程的机制使得很难获得程序性能可靠的测量值。系统活动倾向于在两个不同的时间尺度上进行。在微观级别上,每条指令执行的时间以ns来衡量的。在宏观级别上,输入/输出交互发生的延迟是以ms来衡量的。计算机系统通过不断地从一个任务切换到另一个任务来利用这种差异,一次运行若干ms。
计算机系统有两种完全不同的记录时间流逝的方法。从宏观角度来看,计时器中断发生的频率似乎很快,但是从微观的角度来看却秀慢。通过间隔计数,系统能够获得对程序执行时间非常精力的测量值。这种方法只对长持续时间表用。周期计数器非常快,可以得到在微观尺度上很好的测量值。对于测量绝对时间的周期计数器,上下文切换的影响能够导致很小(在负载很轻的系统上)到很大(在负载很重的系统上)的误差。因此,没有方法是完美的。理解在一个特殊的系统上能够获得的准确度是很重要的。
取决于前面存储器引用和条件转移的历史,高速缓存和转移预测的影响可以导致执行代码的某个片段所需的时间每次都不同。通过事先运行某些将高速缓存设置为可预测状态的代码,我们可以部分地控制引起这种变化的因素,但是在有上下文切换发生时,这些尝试就没有用了。因此,我们必须进行多次测量,分析结果,以确定真实的执行时间。幸运的是,所有引起变化的因素的效果都是增加执行时间,因此只需分析确定测出的时间的最小值是否是在一个准确的测量值。
第十章:虚拟存储器
10.1物理和虚拟地址
计算机系统的主存被组织上由M个连续的字节大小的单元组成的数组。每字节都有一个惟一的物理地址。但是,为通用计算机设计,现代处理器使用是虚拟寻址。
根据虚拟寻址,CPU通过生成一个虚拟地址(VA)来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译。就像异常处理一样,地址翻译需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做MMU(存储器管理单元)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。
10.2地址空间
地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们属性(地址)。
10.3虚拟存储器作为缓存的工具
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
1)未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
2)缓存的:当前缓存在物理存储器中的已分配页。
3)未缓存的:没有缓存在物理存储器(主存)中的已分配页。
DRAM高速缓存的组织结构、页表、页命中、缺页分配页面、局部性再次搭救。
DRAM缓存是全相联的,任何物理页都可以包含任意虚拟页。
10.4虚拟存储器作为存储器管理的工具
VM简化了链接和加载,共享代码和数据,以及对应用分配存储器,此外,虚拟存储器还可以对存储器进行相应的保护。
10.5地址翻译
页面命中时,CPU硬件执行的步骤。
1)处理器生成一个虚拟地址,并把它传送给MMU。
2)MMU生成PTE(page table entry),并从高速缓存/主存请求得到它。
3)高速缓存/主存向MMU返回PTE。
4)MMU构造物理地址,并把它传送给高速缓存/主存。
5)高速缓存/主存返回所请求的数据字给处理器。
页面命中完全是由硬件来处理的。
和页面命中不同,处理缺页要求硬件和操作系统内核协作来完成。
第一步到第三步同上。
第四步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
第五步:缺页处理程序确定出物理存储器中的牺牲页,如果这个页面已经被修改了,则把它页面换出到磁盘。
第六步:缺页处理程序调入新的页面,并更新存储器中的PTE。
第七步:缺页处理程序返回到原来的进程,驱使导致缺页的指令重新启动。CPU将引起缺页的指令重新改善给MMU。因为虚拟页面现在缓存在物理存储器中,所以就会命中。
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便虚拟地址翻译为物理地址。为了减少开销,很多系统在MMU中包括了一个关于PTE的小的缓存,称为TLB(translation lookaside buffer,翻译后备缓冲器)。
多级页表的出现,从两个方面减少了存储器要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就不存在;第二,只有一级页表才需要总是在主存中。
10.6动态分配
一个动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。在大多数的Unix系统中,堆是一个请求二进制的零的区域,它紧接在未初始化的bss区域后开始,并向上生长。
使用动态存储器分配的最重要的原因是它们经常直到程序实际运行时,才知道某此数据结构的大小。
碎片:有两种形式的碎片,内部碎片和外部碎片。内部碎片是在一个已分配块比有效载荷大时发生的;外部碎片是当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。
10.7小结
虚拟存储器是对主存的一个抽象。支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容间由操作系统提供的。
虚拟存储器提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟存储器缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘拷贝到主存缓存。如果必要,将写回被驱逐的页。第二,虚拟存储器简化了存储器管理,进而又简化了链接、在进程间共享数据、进程的存储器分配,以及程序加载。最后,虚拟存储器通过在每条条目中加入保护位,从而简化了存储器保护。
地址翻译的过程必须和系统中任意硬件缓存的操作集成在一起。大多数条目位于L1高速缓存中,但是一个称为TLB的页表条目在芯片上的高速缓存,通常会消除访问在L1上的页表条目的开销。
现代系统通过将虚拟存储器组块和磁盘上的文件组块关联起来,来初始化虚拟存储器,这个过程称为存储器映射。存储器映射为共享数据、创建新的进程以及加载程序,提供了一种高效的机制。应用可以使用mmap函数来手工地和删除虚拟地址空间的区域。然而,大多数程序依赖于动态存储器分配器,例如 malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态存储器分配器是一个有系统级感觉的应用级程序,它直接操作存储器,而无需类型系统的很多帮助。分配器有两种类型:显式分配器要求应用显式地释放它们的存储器块;隐式分配器(垃圾收集器)自动释放任何无用的和不可达的块。
对于C程序员来说,管理和使用虚拟存储器是一件困难和容易出错的任务。常见的错误示例包括:间接引用指针,读取未初始化的存储器,允许栈缓冲区溢出,假设指针和它们指向的对象大小相同,引用指针而不是它所指向的对象,误解指针运算,引用不存在的变量,以及引起存储器泄漏。
第三部分:程序间的交互和通信
现实世界中,应用程序利用操作系统提供的服务来与I/o设备及其他程序通信。
本部分将使你了解Unix操作系统提供的基本I/o服务,以及如何用这些服务来构造应用程序,例如web客户端和服务器,它们是通过Internet彼此通信的。你将学习编写诸如web服务器这样的可以同时为多个客户端提供服务的并发程序
第十一章:系统级I/o
输入/输出(I/o)是在主存和外部设备之间拷贝数据的过程。输入操作是从I/o设备拷贝数据到主存,而输出操作是从主存拷贝数据到I/o设备。
11.1Unix I/o
在Unix中,所有的I/o设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
11.2打开和关闭文件
通过open函数来打开一个已存在的文件或者创建一个新的文件。
通过close函数关闭一个打开的文件。
11.3读和写文件
应用程序是通过分别调用read和write函数来执行输入和输出的。
11.4用Rio钮进行健壮地读和写
Rio包提供了方便、健壮和高效的I/O。Rio包提供了两类不同的函数:无缓冲的输入输出函数和带缓冲的输入函数。
11.5读取文件元数据
应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(有时也称为文件的元数据)。
11.6共享文件
可以用许多不同的方式来共享Unix文件。除非你清楚内核是如何表示打开的文件,否则文件共享的概念相当难懂。内核用三种相关的数据结构来表示打开的文件:
1)描述符表。每个进程都有它独立的描述符表,它的表项是由进程的文件描述符来索引的。每个的描述符表项指向文件表中的一个表项。
2)文件表。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括有当前的文件位置、引用计数即当前指向该表项的描述符表项数,以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
3)v-node表:同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
11.7小结
Unix提供了少量的系统级函数,它们允许应用程序打开、关闭、读写文件,提取文件的元数据,以及执行I/O重定向。Unix的读和写操作会出现不足值,应用程序必须正确地预计和处理这种情况。应用程序不直接调用 Unix I/O函数,而应该使用Rio包,Rio包通过反复执行读写操作,直到传送完所有的请求数据,自动处理不足值。
Unix内核使用三种相关的数据结构来表示打开的文件。描述符表中的表项指向打开文件表中表项,而打开文件表中的表项又指向v-node表中的表项。每个进程都有它自己单独的描述符表,而 所有的进程共享同一打开文件表和v-node表。理解这些结构的一般构成就能使们清楚地理解文件共享和I/O重定向。
标准I/O库是基于Unix I/O实现的,并提供了一组强大的高级I/O例程。对于大多数应用程序而言,标准I/O更简单,是优于Unix I/O的选择。然而,因为对标准I/O和网络文件的一些相互不兼容的限制,Unix I/O比之标准I/O更该适用于网络应用程序。
第十二章:网络编程
12.1客户端-服务器编程模型
客户端-服务器模型中的基本操作是事务。一个客户端-服务器事务由四步组成:
1)当一个客户端需要服务时,它向服务器发送一个请求,引起一个事务。
2)服务器收到请求后,解释它,并以适当的方式操作它的资源。
3)服务器给客户端发送一个响应,并等待下一个请求。
4)客户端收到响应并处理它。
12.2网络
客户端和服务器通过运行在不同的主机上,并且通过计算机网络的硬件和软件资源来通信。
对于主机而言,网络只是又一种I/O设备。
12.3全球的IP因特网
每台因特网主机都运行实现TCP/IP的软件。因特网的客户端和服务器混合使用套接字接口函数和Unix I/O函数来进行通信。套接字函数典型地是作为系统调用来实现的,这些系统会陷入内核,并调用各种内核模式的TCP/IP函数。
IP地址、因特网域名,因特网连接(套接字的概念)。
12.4套接字接口
套接字是连接的商战,每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的。
套接字的地址结构、connect函数、open_clientfd函数、bind函数、listen函数、open_listened函数、accept函数。
12.5web服务器
web基础:http协议、HTML语言、URL。
http事务:http请求、http响应
12.6小结
每个网络应用都是基于客户端-服务器端模型的。根据这个模型,一个应用是由一个服务器和一个或多个客户端组成的。服务器管理资源,以某种方式操作资源,为它的客户端服务。客户端-服务器模型中的基本操作是客户端-服务器事务,它是由客户端请求和跟随的服务器响应组成的。
客户端和服务器通过因特网这个全球网络来通信。从一个程序员的观点来看,我们可以把因特网看成是一个全球范围的主机集合,具有以下几个属性:每个因特网都有一个惟一的32位名字,称为它的IP地址;IP地址的集合映射为一个因特网域名的集合;不同因特网主机上的进程能够通过连接互相通信。
客户端和服务器通过使用套接字接口建立连接。套接字是连接的端点,对应用程序来说,连接是以文件描述符的形式出现的。套接字接口提供了打开和关闭套接字描述符的函数。客户端和服务器通过读写这些描述符来实现彼此间的通信。
web服务器使用HTTP协议和它们的客户端(比如浏览器)彼此通信。浏览器向服务器请求静态或者动态的内容。对静态内容的请求是通过从服务器磁盘取得文件并把它返回给客户端来服务的。
对动态内容的请求是通过在服务器上一个子进程的上下文中运行一个程序并将它的输出返回给客户端来服务的。CGI标准提供了一组规则,来管理客户端如何将程序参数传递给服务器,服务器如何将这些参数以及其他信息传递给子进程,以及子进程如何将它的输出发送回客户端。
第十三章:并发编程
现代操作系统提供了三种基本的构造并发程序的方法:
1)进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显工的进程间通信机制。
2)I/O多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流。逻辑流被模型化为状态机,作为数据到达文件描述符的结果,主程序显式地从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有流都共享同一地址空间。
3)线程。线程是运行在个单一进程上下文中的逻辑流,由内核进行调度的。
线程中有共享变量时,要用相应的方法去同步线程,比如信号量机制。在资源调度中,会出现竞争和列锁的现象。
13.1小结
一个并发进程帅在时间上重叠的一组逻辑流组成的。在这一章中,我们学习了三种不同的构建并发程序的机制:进程、I/O多路复用和线程。
进程是由内核自动调度的,而且因为它们有各自独立的虚拟地址空间,所以要实现共享数据,它们需要显式的IPC(interprocess communictaion,ipc)机制。事件驱动程序创建它们自己的并发逻辑流,这些逻辑流被模型化为状态机,用I/O多路复用来显式地调度这些流。因为程序运行在一个单一进程中,所以在流之间共享数据速度很快而且很容易。线程是这些方法的综合。同基于进程的流一样,线程是内核自动调度的。同基于I/O多路复用的流一样,线程是运行在一个单一进程的上下文中的,因此可以快速而方便地共享数据。
无论哪种并发机制,同步对共享数据的并发访问都是一个困难的问题。提出对信号量的P和V操作就是为了帮助解决这个问题。信号量操作可以用来提供对共享数据的互斥访问,也对诸如生产者-消费者程序中共享缓冲区这样的资源访问进行调度。
写在最后:
这篇文章是我在读完CSAPP后所做的笔记,本来想是通过自己把各个知识点串一下,可真正做的时候才发现自己的火候不够,可以说没有火候,只能像小学时做笔记那样,把我认为重要的知识点在此罗列一下,以便查阅和更进一步的学习。
笔记中,很多地方都写的不是很详细,所以针对的人群是阅读过CSAPP的人,如果没有阅读过的,也可以通过阅读此文,对其中一些不了解的地方,再到网上查找更详细的信息来进一步了解。
总的来说,CSAPP是一本不错的IT技术书,通过本书你会对整个计算机体系,整个程序运行机制,有一个大体的了解,如果你阅读的够深入,肯定会有详细的了解。感觉本书就像是为你以后学习打下了地基,以后学到的知识都可以在这本书里找到支撑点。
这本书实在太厚,有几次都想要放弃阅读,不过还是坚持了下来,感觉读完这本书后的感觉才是最爽的,而真正从这本书得到的知识可能并不像作者期望的那么多,但是确实对我有很大的帮助,尤其是第六章(存储器的层次结构)、第七章(链接)、第十章(虚拟存储器)和第十三章(并发编程),读后感觉股股清风身边拂过,涓涓细流余音袅袅。