XXXIV 11.5.4服务动态内容…………669 12.4.1线程内存模型………696 1l.6综合:TINY Web服务器…671 12.4.2将变量映射到内存…697 11.7小结…678 12.4.3共享变量……698 参考文献说明……… 678 12.5用信号量同步线程 *……**…698 家庭作业 678 12.5.1进度图…701 练习题答案…………… 679 12.5.2信号量…702 12.5.3使用信号量来实现互斥…703 第12章并发编程 681 12.5.4利用信号量来调度共享 12.1基于进程的并发编程………682 资源…………704 12.1.1基于进程的并发服务器…683 12.5.5综合:基于预线程化的 12.1.2进程的优劣…… 684 并发服务器…708 12.2基于I/O多路复用的并发 12.6使用线程提高并行性…710 编程*… 684 12.7其他并发问题…………716 12.2.1基于I/0多路复用的并发 12.7.1线程安全……716 事件驱动服务器…686 12.7.2可重入性 ……717 12.2.21/0多路复用技术的优劣…690 12.7.3在线程化的程序中使用 12.3基于线程的并发编程…691 已存在的库函数………718 12.3.1线程执行模型……… 691 12.7.4竞争… 719 12.3.2 Posix线程 ……………691 12.7.5死锁…… 721 12.3.3创建线程… 692 12.8小结………722 12.3.4终止线程…………… 693 参考文献说明…………………723 12.3.5回收已终止线程的资源 …693 家庭作业 4.0。*4”44*0”404*”*04 723 12.3.6分离线程…………… 694 练习题答案…… 726 12.3.7初始化线程…694 729 12.3.8基于线程的并发 附录A错误处理…………… 服务器…………694 参考文献 ………400.733 12.4多线程程序中的共享变量…696
第1章 计算机系统漫游 计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。虽然系统的 具体实现方式随着时间不断变化,但是系统内在的概念却没有改变。所有计算机系统都有 相似的硬件和软件组件,它们又执行着相似的功能。一些程序员希望深入了解这些组件是 如何工作的以及这些组件是如何影响程序的正确性和性能的,以此来提高自身的技能。本 书便是为这些读者而写的。 现在就要开始一次有趣的漫游历程了。如果你全力投身学习本书中的概念,完全理解底 层计算机系统以及它对应用程序的影响,那么你会步上成为为数不多的“大牛”的道路。 你将会学习一些实践技巧,比如如何避免由计算机表示数字的方式引起的奇怪的数字 错误。你将学会怎样通过一些小窍门来优化自已的C代码,以充分利用现代处理器和存储 器系统的设计。你将了解编译器是如何实现过程调用的,以及如何利用这些知识来避免缓 冲区溢出错误带来的安全漏洞,这些弱点给网络和因特网软件带来了巨大的麻烦。你将学 会如何识别和避免链接时那些令人讨厌的错误,它们困扰着普通的程序员。你将学会如何 编写自己的Unix shell.、自己的动态存储分配包,甚至于自己的Web服务器。你会认识并发 带来的希望和陷阱,这个主题随着单个芯片上集成了多个处理器核变得越来越重要。 在Kernighan和Ritchie的关于C编程语言的经典教材[61]中,他们通过图l-l中所 示的he1lo程序来向读者介绍C。尽管hello程序非常简单,但是为了让它实现运行,系 统的每个主要组成部分都需要协调工作。从某种意义上来说,本书的目的就是要帮助你了 解当你在系统上执行hello程序时,系统发生了什么以及为什么会这样。 code/intro/hello.c #include <stdio.h> 2 3 int main() 5 printf("hello,world\n"); return 0; 7 code/intro/hello.c 图1-1he11o程序(来源:[60]) 我们通过跟踪hello程序的生命周期来开始对系统的学习一从它被程序员创建开始, 到在系统上运行,输出简单的消息,然后终止。我们将沿着这个程序的生命周期,简要地介 绍一些逐步出现的关键概念、专业术语和组成部分。后面的章节将围绕这些内容展开。 1.1信息就是位+上下文 hel1o程序的生命周期是从一个源程序(或者说源文件)开始的,即程序员通过编辑器创 建并保存的文本文件,文件名是he1lo.c。源程序实际上就是一个由值0和1组成的位(又称 为比特)序列,8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符
2第1章计算机系统漫游 大部分的现代计算机系统都使用ASCⅡI标准来表示文本字符,这种方式实际上就是用 一个唯一的单字节大小的整数值e来表示每个字符。比如,图1-2中给出了he1lo.c程序 的ASCII码表示。 # n 1 d SP d 1 35105110 99108117100101 32 60115116100105111 46 h > n n i n t SP m a n ( ) \n 104 62 10 10105110116 32109 97105110 40 41 10 123 n SP SP SP SP p f 1 10 32 32 32 3211211410511011610240 34104101108 0 SP 0 1 d n (n SP 108 111 44 321191111141081009211034 41 59 10 32 SP SP SP r e t n SP 0 n (n 32 32 32114101116117114110 32 48 59 10125 10 图1-2he11o.c的ASCII文本表示 hel1o.c程序是以字节序列的方式储存在文件中的。每个字节都有一个整数值,对应 于某些字符。例如,第一个字节的整数值是35,它对应的就是字符“#”。第二个字节的 整数值为105,它对应的字符是‘1’,依此类推。注意,每个文本行都是以一个看不见的 换行符‘\n’来结束的,它所对应的整数值为10。像hello.c这样只由ASCII字符构成 的文件称为文本文件,所有其他文件都称为二进制文件。 he1lo.c的表示方法说明了一个基本思想:系统中所有的信息一包括磁盘文件、内 存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区 分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。比如,在不同的上下文 中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。 作为程序员,我们需要了解数字的机器表示方式,因为它们与实际的整数和实数是不 同的。它们是对真值的有限近似值,有时候会有意想不到的行为表现。这方面的基本原理 将在第2章中详细描述。 旁注C编程语言的起源 C语言是贝尔实验室的Dennis Ritchie于l969年~l973年间创建的。美国国家标准学 会(American National Standards Institute,ANSI)在l989年颁布了ANSI C的标准,后来C 语言的标准化成了国际标准化组织(International Standards Organization,ISO)的责任。这 些标准定义了C语言和一系列函数库,即所谓的C标准库。Kernighan和Ritchie在他们的 经典著作中描述了ANSI C,这本著作被人们满怀感情地称为“K&R”[6l]。用Ritchie的话 来说[92],C语言是“古怪的、有缺陷的,但同时也是一个巨大的成功”。为什么会成功呢? ·C语言与Unix操作系统关系密切。C从一开始就是作为一种用于Unix系统的程序 语言开发出来的。大部分Uix内核(操作系统的核心部分),以及所有支撑工具和 函数库都是用C语言编写的。20世纪70年代后期到80年代初期,Uix风行于高 等院校,许多人开始接触C语言并喜欢上它。因为Uix几乎全部是用C编写的, 它可以很方便地移植到新的机器上,这种特点为C和Ux赢得了更为广泛的支持。 日有其他编码方式用于表示非英语类语言文本。具体讨论参见2.1,4节的旁注
第1章计算机系统漫游3 ●C语言小而简单。C语言的设计是由一个人而非一个协会掌控的,因此这是一个 简洁明了、没有什么冗赘的设计。K&R这本书用大量的例子和练习描述了完整 的C语言及其标准库,而全书不过261页。C语言的简单使它相对而言易于学 习,也易于移植到不同的计算机上。 ●C语言是为实践目的设计的。C语言是设计用来实现Uix操作系统的。后来, 其他人发现能够用这门语言无障碍地编写他们想要的程序。 C语言是系统级编程的首选,同时它也非常适用于应用级程序的编写。然而,它也 并非适用于所有的程序员和所有的情况。C语言的指针是造成程序员困惑和程序错误的 一个常见原因。同时,C语言还缺乏对非常有用的抽象的显式支持,例如类、对象和异 常。像C++和Java这样针对应用级程序的新程序语言解决了这些问题。 1.2程序被其他程序翻译成不同的格式 hel1o程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读 懂。然而,为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系 列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以 二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件。 在Uix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的: linux>gcc -o hellohello.c 在这里,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行 目标文件he1lo。这个翻译过程可分为四个阶段完成,如图1-3所示。执行这四个阶段的 程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)。 printf.o hello.c 预处理器 hello.i 编译器 hello.s 汇编器 hello.o 链接器 hello 源程序 cpp 修改了的 (cc1) 汇编程序 as 可重定位 (1d) 可执行 (文本) 源程序 (文本) 目标程序 目标程序 (文本) (二进制) (二进制) 图1-3 编译系统 ●预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如 hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件 stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,通常 是以.1作为文件扩展名。 ·编译阶段。编译器(cc1)将文本文件he11o.i翻译成文本文件he11o.s,它包含一 个汇编语言程序。该程序包含函数main的定义,如下所示: 1 main: 2 subq $8,%rsp 3 movl $.LCO,%edi 4 call puts 5 movl $0,,%eax 6 addq $8,%rsp 7 ret 定义中2~7行的每条语句都以一种文本格式描述了一条低级机器语言指令。 汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语
4第1章计算机系统漫游 言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。 ●汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成 一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标 文件he11o.o中。he11o.o文件是一个二进制文件,它包含的17个字节是函数main 的指令编码。如果我们在文本编辑器中打开he1lo.o文件,将看到一堆乱码。 ●链接阶段。请注意,hello程序调用了printf函数,它是每个C编译器都提供的 标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译 好了的目标文件中,而这个文件必须以某种方式合并到我们的he11o.o程序中。链 接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件 (或者简称为可执行文件),可以被加载到内存中,由系统执行。 旁注GNU项目 GCC是GNU(GNU是GNU's Not Unix的缩写)项目开发出来的众多有用工具之 一。GNU项目是l1984年由Richard Stallman发起的一个免税的慈善项目。该项目的目 标非常宏大,就是开发出一个完整的类Uix的系统,其源代码能够不受限制地被修改 和传播。GNU项目已经开发出了一个包含Uix操作系统的所有主要部件的环境,但内 核除外,内核是由Linux项目独立发展而来的。GNU环境包括EMACS编辑器、GCC 编译器、GDB调试器、汇编器、链接器、处理二进制文件的工具以及其他一些部件。 GCC编译器已经发展到支持许多不同的语言,能够为许多不同的机器生成代码。支持 的语言包括C、C++、Fortran、Java、Pascal、.面向对象C语言(Objective-C)和Ada。 GNU项目取得了非凡的成绩,但是却常常被忽略。现代开放源码运动(通常和 Linux联系在一起)的思想起源是GNU项目中自由软件(free software)的概念。(此处的free 为自由言论(free speech)中的“自由”之意,而非免费啤酒(free beer)中的“免费”之意。)而 且,Linux如此受欢迎在很大程度上还要归功于GNU工具,它们给Linux内核提供了环境。 1.3了解编译系统如何工作是大有益处的 对于像hel1o.c这样简单的程序,我们可以依靠编译系统生成正确有效的机器代码。 但是,有一些重要的原因促使程序员必须知道编译系统是如何工作的。 ·优化程序性能。现代编译器都是成熟的工具,通常可以生成很好的代码。作为程序 员,我们无须为了写出高效代码而去了解编译器的内部工作。但是,为了在C程序中 做出好的编码选择,我们确实需要了解一些机器代码以及编译器将不同的C语句转化 为机器代码的方式。比如,一个switch语句是否总是比一系列的if-else语句高效 得多?一个函数调用的开销有多大?whi1e循环比for循环更有效吗?指针引用比数 组索引更有效吗?为什么将循环求和的结果放到一个本地变量中,会比将其放到一个 通过引用传递过来的参数中,运行起来快很多呢?为什么我们只是简单地重新排列一 下算术表达式中的括号就能让函数运行得更快? 在第3章中,我们将介绍x86-64,最近几代Linux、Macintosh和Windows计算机的 机器语言。我们会讲述编译器是怎样把不同的C语言结构翻译成这种机器语言的。在第 5章中,你将学习如何通过简单转换C语言代码,帮助编译器更好地完成工作,从而调 整C程序的性能。在第6章中,你将学习存储器系统的层次结构特性,C语言编译器如 何将数组存放在内存中,以及C程序又是如何能够利用这些知识从而更高效地运行