前言
本书是从程序员的角度来写的,讲述应用程序员如何能够利用系统知识来编写出更好的程序。
我们的目标是以一种你会立刻发现有用的方式呈现这些基本概念。同时,你也要做好更深入探究的准备,研究像编译器、计算机体系结构、操作系统、嵌入式系统和网络互联这样的题目。
事实上,我们相信,学习系统的唯一方法就是做(do)系统,即在真正的系统上解决具体的问题,或是编写和运行程序。
本书由12章组成,旨在阐述计算机系统的核心概念。
第1章:计算机系统漫游
第一部分 程序结构和执行
第2章:信息的表示和处理
第3章:程序的机器级表示
第4章:处理器体系结构
第5章:优化程序性能
第6章:存储器层次结构
第二部分 在系统上运行程序
第7章:链接
第8章:异常流控制
第9章:虚拟存储器
第三部分 程序间的交互和通信
第10章:系统级I/O
第11章:网络编程
第12章:并发编程
第1章:计算机系统漫游
信息就是位+上下文
系统中所有信息都是由一串位表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
程序被其它程序翻译成不同的格式
GCC编译器将源文件编译成目标文件的过程如上图所示,分为四个阶段:预处理、编译、汇编和链接。
预处理阶段:根据以字符“#”开头的命令,修改源码。例如将头文件中内容直接插入程序文本中,得到一个以.i作为扩展名的文本。
编译阶段:将.i文本翻译成.s文本,它包含汇编语言程序。
汇编阶段:将.s文本翻译成机器语言指令,并将这些指令打包成可重定位目标程序的格式,将结果保存到目标文件.o中。
链接阶段:以某种方式合并目标文件,得到可执行目标文件。
了解编译系统如何工作是大有益处的
优化程序性能、理解链接时出现的错误、避免安全漏洞。
高速缓存至关重要
可执行文件hello最初是存放在硬盘上的,当程序加载时,它被复制到主存;当处理器运行程序时,指令又从主存复制到处理器。相似地,程序中打印的数据串“hello,world\n”初始存放在硬盘上,然后复制到主存,最后从主存复制到显示设备。从程序员的角度,这些复制就是开销,减缓了程序“真正”的工作。因此,系统设计者的一个主要目标就是使这些复制操作尽可能快地完成。
针对处理器与主存之间的差异,系统设计者采用了更小、更快的存储设备,即高速缓存,作为暂时的集结区域,用来存放处理器近期可能会需要的信息。
系统可以获得一个很大的存储器,同时访问速度也很快,原因是利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据的方法,大部分的存储器操作都能在快速的高速缓存中完成。
本书得出的重要结论之一,就是:意识到高速缓存存在的应用程序员,可以利用高速缓存将他们的程序的性能提高一个数量级。
存储设备形成层次结构
在处理器和一个又大又慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速缓存)的想法,已经成为了一个普遍的观念。每个计算机系统中的存储设备都被组织成一个存储器层次结构。
在上图的结构层次中,从上至下,设备访问速度越来越慢、容量越来越大,并且每字节的造价越来越便宜。
存储器层次结构的主要思想是一层上的存储器作为低一层存储器的高速缓存。
操作系统管理硬件
操作系统有两个基本功能:防止硬件被失控的应用程序滥用;向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备。
操作系统通过几个基本的抽象概念(进程、虚拟存储器和文件)来实现这两个功能。
如上图所示,文件是对I/O设备的抽象表示,虚拟存储器是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。
进程是操作系统对一个正在运行的程序的一种抽象。操作系统实现的交错执行的机制称为上下文切换。操作系统保持跟踪进程运行所需的所有状态,这种状态,也就是上下文,包括许多信息。
一个进程可以有多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。多线程之间比多进程之间更容易共享数据。
重要主题
1. 并发与并行
并发是个通用概念,是指同时具有多个活动的系统;
并行是指用并发使一个系统运行得更快。
并行可以在计算机系统的多个抽象层次上运用。在此,我们按照系统层次结构中由高到低的顺序重点强调三个层次。
a. 线程级并发
当构建一个但操作系统内核控制的多处理器组成的系统时,我们就得到一个多处理器系统。随着多核处理器和超线程(hyperthreading)的出现,这种系统变得常见。
超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。它涉及CPU某些硬件有多个备份,比如程序计数器和寄存器文件;而其他的硬件部分只有一份,比如执行浮点算术运算的单元。
b. 指令级并行
在较低抽象层次上,现代处理器可以同时执行多条指令的属性,称为指令级并行。例如流水线的使用,将指令划分成不同的阶段,这些阶段可以并行地执行,用来处理不同指令的不同阶段。
如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量处理器。大多数现代处理器都支持超标量操作。
c. 单指令、多少数据并行
在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称之为单指令、多数据,即SIMD并行。
提供这些SIMD指令多是为了提高处理影像、声音和视频数据应用的执行速度。虽然有些编译器试图从C程序中自动抽取SIMD并行性,但是更为可靠地方法是使用编译器支持的特殊向量数据类型来书写,例如GCC就支持向量数据类型。
2. 计算机系统中抽象的重要性
计算机系统中一个重大主题就是:提供不同层次的抽象表示,来隐藏实际实现的复杂性。
前面介绍到三个抽象:文件是对I/O的抽象;虚拟存储器是对程序存储器的抽象;进程是对一个正在运行的程序的抽象。我们再增加一个新的抽象:虚拟机,它提供了对整个计算机(操作系统、处理器和程序)的抽象。
第5章 优化程序性能
编写高效程序需要几类活动:
第一,我们必须选择一组合适的算法和数据结构。
第二,我们必须写出编译器能够有效优化以转换成高效可执行代码的源代码。对于第二点,理解优化编译器的能力和局限性是很重要的。C语言的有些特性,例如指针运算和强制类型转换的能力,使得编译器很难对它进行优化。程序员经常能够以一种使编译器更容易产生高效代码的方式来编写他们的程序。
第三,针对处理运算量特别大的计算,将一个任务分成多个部分,使得这些部分可以在多核和多处理器的某种组合上并行地计算。即使是要利用并行性,每个并行地线程都以最高性能执行也是非常重要的。
通常,程序员必须在实现和维护程序的简单性与它的运行速度之间做出权衡。
即使是最好的编译器,也受到妨碍优化的因素(optimization blocker)的阻碍,妨碍优化的因素就是程序行为中那些严重依赖于执行环境的方面。程序员必须编写容易优化的代码,以帮组编译器。
程序优化的第一步,就是消除不必要的内容,让代码尽可能有效地执行它期望的工作。这包括消除不必要的函数调用、条件测试和存储器引用。
程序优化的第二步,利用处理器提供的指令级并行(instruction-level parallelism)能力,同时执行多条命令。
研究程序的汇编代码表示,是理解编译器,以及产生的代码如何运行的最有效的手段之一。仔细研究内循环的代码是一个很好的开端,确认降低性能的属性,例如过多的存储器引用和对寄存器使用不当。从汇编代码开始,还可以预测什么操作会并行执行,以及它们如何使用处理器资源。通常通过确认关键路径(critical path)来决定执行一个循环所需要的时间(或者说,至少是一个时间下限)。所谓关键路径,是指在循环的反复执行过程中形成的数据相关链。
一个很有用的策略是,只重写程序,到编译器由此能够产生有效代码所需要的程度就好了。这样,能尽量避免损害代码的可读性、模块性和可移植性,就好像我们使用的是具有最低能力的编译器。
优化编译器的能力和局限性
编译器必须很小心地对程序只使用安全的优化,保证优化前后的程序和为优化的版本有一样的行为。程序员必须花费时间写出程序使得编译器能够将之转换成有效机器代码。
第一个妨碍优化的因素:存储器别名使用(memory aliasing)。所谓存储器别名使用是指两个指针可能指向同一存储器位置的情况。在只进行安全的优化中,编译器必须假设不同的指针可能会指向存储器中同一位置。
第二个妨碍优化的因素:函数调用。大多数编译器不会试图判断一个函数是否没有副作用,相反,编译器会假设最糟糕的情况,并保持所有的函数调用不变。
消除循环的低效率,减少过程调用
识别需要多次执行但是计算结果不会改变的计算,将其移动到代码前面不会被多次求值的部分,这类优化称为代码移动(code motion)。优化编译器会试着进行代码移动,但是对于函数调用等,编译器会非常小心。它们不能可靠发现一个函数是否具有副作用,因而假设函数会有副作用。
过程调用会带来相当大的开销,而且妨碍大多数形式的程序优化。对于性能至关重要的应用来说,为了速度,经常必须要损害一些模块性和抽象性。为了防止以后要修改代码,添加一些文档是很明智的,说明采用了哪些变换,以及导致进行这些变换的假设。
消除不必要的存储器引用
通过引入中间变量等,减少对存储器的引用次数。
理解现代处理器
到目前为止,我们运用的优化都不依赖于目标机器的任何特性。这些优化只是简单地降低了过程调用的开销,以及消除了一些重大的“妨碍优化的因素”,这些因素会给优化编译器造成困难。随着试图进一步提高性能,我们必须考虑充分利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。
在代码级上看,似乎是一次执行一条指令,在实际处理器中,是同时对多条指令求值,这个现象称为指令级并行。现代微处理器了不起的功绩之一是:采用复杂而奇异的微处理器结构,其中,多条指令可以并行地执行,同时又呈现一种简单地顺序执行指令的表象。
两种下界描述了程序的最大性能。当一系列操作必须按照严格顺序执行时,就会遇到延迟界限(latency bound),因为在下一条指令开始之前,这条指令必须结束。当代吗中的数据相关限制了处理器利用指令级并行地能力时,延迟界限能够限定程序性能。吞吐量界限(throughout bound)刻画了处理器功能单元的原始计算能力。这个界限是程序性能的终极限制。
循环展开
循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环迭代的次数。循环展开能够从两个方面改进程序的性能。第一,它减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支。其次,它提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。
循环展开只能改进整数加法和乘法的性能,并不能改进浮点运算的性能。因为浮点运算的关键路径是循环没有展开代码的性能制约因素,然而它任然是肩带循环展开代码的性能制约因素。
编译器可以很容易地执行循环展开。只要优化级别足够高,许多编译器都能例行公事地做到这一点。用命令行选项'-funroll-loops'调用GCC,会执行循环展开。
提高并行性
执行加法和乘法的功能单元是完全流水线化的,这意味着它们可以每个时钟周期开始一个新操作。在存在顺序相关性的代码中,必须考虑打破这种顺序相关,得到比延迟界限更好性能的方法。
并行地累积和重新结合变换是两种打破顺序相关性的方法。总的来说,重新结合变换能够减少计算中关键路径上操作的数量,通过更好地利用功能单元的流水线能力得到更好的性能。大多数编译器不会尝试对浮点运算做重新结合,因为这些运算不保证是可结合的。当前的GCC版本会对整数运算执行重新结合,但是不是总有很好的效果。通常,我们发现,循环展开和并行地累积在多个值中,是提高程序性能的更可靠的方法。
一些限制因素
一个程序的关键路径指明了执行该程序所需时间的一个基本下界。功能单元的吞吐量界限也是程序执行时间的下界。
循环并行性的好处受到描述计算的汇编代码的能力限制。如果我们的并行度超过了可用的寄存器数,那么编译器会诉诸溢出,将某些临时值存放在栈中。一旦出现这种情况,性能会急剧下降。
当分支预测逻辑不能正确预测一个分支是否要跳转的时候,条件分支可能会招致严重的预测错误处罚。如果编译器能够产生使用条件数据传送而不是使用条件控制转移的代码,可以极大地提高程序的性能。这不是C语言程序可以直接控制的,但是有些表达条件行为的方法能够更直接地被翻译成条件传送,而不是其它操作。
我们发现GCC能够为以一种更“功能式”风格书写的代码产生条件传送,在这种风格的代码中,我们用条件操作来计算值,然后用这些只来更新程序状态,这种风格对立于一种更“命令式”的风格。
理解存储器性能
一个包含加载操作的程序的性能,既依赖于流水线的能力,也依赖于加载单元的延迟。由于加载单元每个时钟周期只能启动一条加载操作,所以CPE不可能小于1.00。
与加载操作一样,在大多数情况下,存储操作能够在完全流水线的模式中工作,每个周期开始一条新的存储。存储操作并不影响任何寄存器值,因此,一系列的存储操作不会产生数据相关。只有加载操作是受存储操作结果影响的。
一个存储器读的结果依赖于一个最近的存储器写,这种现象称之为读/写相关(read/write dependency)。读/写相关导致处理速度的下降。
不同的源和目的地址,加载和存储操作可以独立地进行。
源地址和目的地址相同,指令间的数据相关使得关键路径的形成包括了存储、加载等其它相关操作。
应用:性能提高技术
优化程序性能的基本策略:
1)高级设计
为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用那些会渐进地产生糟糕性能的算法或编码技术。
2)基本编码原则
避免限制优化的因素。
>消除连续的函数调用;
>消除不必要的存储器引用。
3)低级优化
>展开循环,降低开销,使进一步优化成为可能;
>通过使用多个累积变量和重新结合等技术,提高指令级并行;
>用功能风格重写条件操作,使得编译采用条件数据传送。
确认和消除性能瓶颈
在处理大型程序时,连知道应该优化什么地方都是很难的。本节描述如何使用代码剖析程序(code profiler),这是在程序执行时
收集性能数据的分析工具。
Unix系统提供了一个
剖析程序GPROF。这个程序提供两种形式的信息。
首先,它确定程序中每个函数花费了多少CPU时间。
其次,它计算每个函数调用的次数,以执行调用的函数来分类。
用GPROF进行剖析的步骤如下:
-
// 为剖析而编译和链接
-
> gcc -O1 -pg prog.c -o prog
-
-
// 运行程序
-
> ./prog file.txt
-
-
// 调用GPROF分析数据
-
> gprof prog
GPROF有些属性值得注意:
>
计时不是很准确。对于运行时间较长的程序,相对准确。
>
调用信息相当可靠。
> 默认情况下,
不显示库函数的调用。相反地,库函数的时间会被计算到调用它们的函数的时间中。
Amdal定律
其中,阿尔法代表某个部分所占时间比重,而我们将它的性能提高k倍数。根据Amdal定律,要想大幅度提高整个系统的速度,我们
必须提高系统很大一部分的速度。这就是Amdal定律的主要观点。
考虑k为穷时,即我们能够将它优化到运行时间可以忽略不计的程序。那么
第七章 链接
链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。
链接可以执行于编译时,即源代码被翻译成机器代码时;也可执行于加载时,即在程序被加载器加载到存储器并执行时;甚至执行于运行时,由应用程序来执行。
连接器在软件开发中扮演一个关键角色,因为它使得分离编译成为可能。
学习编译的知识并理解编译器:
帮助你构造大型程序;
帮助你避免危险的编程错误;
帮助你理解语言的作用域是如何实现的;
帮助你理解其它重要的系统概念;
帮助你利用共享库。
这一章提供了关于链接各方面的彻底的讨论,从传统的静态链接,到加载时的共享库的动态链接,以及到运行时的共享库的动态链接。
编译器驱动程序
大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
GNU编译系统编译源码
首先,运行C预处理器(cpp),将.c文件翻译成.i文件;
接着,运行C编译器(cc1),将.i文件翻译成ASCII汇编语言文件.s文件;
然后,运行汇编器(as),将.s文件翻译成可重定位目标文件.o文件;
最后,运行链接器(ld),将各.o文件组合起来,创建一个可执行目标文件。
shell调用操作系统中的一个叫加载器的函数,它拷贝可执行文件的代码和数据到存储器,然后将控制权转移到这个程序的开头。
为了构造可执行文件,链接器必须完成两个主要任务:
符号解析(siymbol resolution):符号解析的目的是将每个符号引用刚好和一个符号定义联系起来。
重定位(relocation):链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有这些符号的引用,使得它们指向这个存储器的位置,从而重定位这些节。
目标文件有三种形式:
可重定位目标文件。可以在编译时与其它可重定位目标文件合并起来,创建一个可执行目标文件。
可执行目标文件。可被直接拷贝到存储器并执行。
共享目标文件。在加载或运行时被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。
现代Unix系统使用可执行和可链接格式(ELF)。
可重定位目标文件
一个典型的可重定位目标文件包含下面几个节:
.text:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局C变量。局部C变量在运行时保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局C变量。
第8章 异常控制流
现代系统通过使控制流发生突变来对系统的状态变化做出反应。一般而言,我们将这些突变称为异常控制流(Exceptional Control Flow, ECF)。
异常控制流发生在计算机系统的各个层次。
> 在硬件层,硬件检测到的事件会触发控制流突然转移到异常处理程序;
> 在操作系统层,内核通过上下文切换,将控制从一个用户进程转移到另一个用户进程;
> 在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其它函数中任意位置的非本地跳转来对错误做出反应。
作为程序员,理解ECF很重要,这有很多原因:
> 帮助理解重要的系统概念 ECF是操作系统用来实现I/O、进程和虚拟存储器的基本机制。
> 帮助理解应用程序如何与操作系统交互 应用程序通过一种称之为系统调用(system call)的ECF形式,向操作系统请求服务。
> 帮助编写有趣的应用程序 操作系统为应用程序提供了强大的ECF机制,用来创建进程、等待进程终止、通知其它进程系统中的异常事件,以及检测和响应这些时间。
> 帮助理解并发 ECF是计算机系统中实现并发的基本机制。
> 帮助理解软件异常如何工作 像C++和Java这样的语言,通过try、catch以及throw语句来提供软件异常机制。软件异常允许程序进行非本地跳转(违反通常的调用/返回栈规则的跳转)来响应错误的情况。非本地跳转是一种应用层ECF,在C中通过setjmp和longjmp函数提供的。
这一章的重要性在于,将开始学习应用是如何与操作系统交互的。本章将描述:
硬件和操作系统交界的部分的异常;
应用程序到操作系统的入口点的异常(系统调用);
位于应用程序和操作系统的交界之处的异常(进程和信号);
非本地跳转,ECF的一种应用层形式。
异常
异常(exception)就是控制流的突变,用来响应处理器状态中的某些变化。状态变化称为事件(event)。
在任何情况下,当处理器检测到有事件时,通过一张叫做异常表(exceptional table)的跳转表,进行一个间接地过程调用(异常),到一个专门设计用来处理这类时间的操作系统子程序(异常处理程序(exceptional handler))。
在系统启动时,操作系统分配和初始化一张称之为异常表的跳转表,使得条目k包含异常k的处理程序的地址。
异常可以分为4类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
陷阱是有意的异常,是执行一条指令的结果。陷阱的最重要用途是执行系统调用。
故障是错误引起的,它可能被故障处理程序修正。一个经典的故障实例是缺页异常(异常14)。
终止是不可恢复的致命错误的结果,通常是一些硬件错误。
Linux/IA32故障和终止
除法错误(异常0) Linux外壳通常会把除法错误报告为“浮点异常”(Floating exception)。
一般保护故障(异常13) 许多原因会导致不为人知的一般保护故障,通常是因为一个程序引用了一个未定义的虚拟存储器区域,或者是因为试图写一个只读的文本段。Linux不会试图恢复这类故障。Linux外壳通常将此报告为“段错误”(Segmentation fault)。
缺页(异常14) 会重新执行产生故障的指令的一个异常实例。
机器检查(异常18) 在导致故障的指令执行中检测到致命的硬件错误时发生的。机器检查处理程序从不返回控制给应用程序。
Linux/IA32系统调用
每个Linux系统调用,都有一个唯一的整数号,对应一个到内核中跳转表的偏移量。
历史上,系统调用通过异常128(0x80)提供的。
C程序可以直接用syscall函数直接调用任何系统调用,然而,实际中机会没必要这么没做。对于大多数系统调用,标准C库提供了一组方便的包装函数。这些包装函数将参数打包到一起,以合适的系统调用号陷入内核,然后将系统调用的返回状态传递回调用程序。
所有的到Linux系统调用的参数都是通过寄存器而不是栈传递的。
注意:异常作为通用的术语,在必要时才区别异步异常(中断)和同步异常(陷阱、故障和终止)。
进程
进程的经典定义就是一个执行中的程序的实例。
进程提供给应用程序的关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统。
一个逻辑流的执行在时间上与另一个流重叠,称为并发流(concurrent flow),这两个流称之为并发地执行。多个流并发地执行的一般现象称为并发(concurrency)。
注意,并发的思想与流运行的处理器核数或者计算机数无关。如果两个流在时间上重叠,那它们就是并发的。
如果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它们为并行流(parallel flow)。
Linux提供了一种聪明的机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构。2.6版本的Linux内核引入/sys文件系统,它输出关于系统总线和设备的额外的底层信息。
系统调用错误处理
当Unix系统级函数遇到错误时,它们典型地返回-1,并设置全局整数变量errno来表示什么出错了。
strerror函数返回一个文本串,描述了某个error值相关联的错误。
进程控制
每个进程都有一个唯一的正数进程ID(PID)。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝。子进程还继承了父进程所有的打开文件。父进程和新创建的子进程之间最大的区别是他们有不同的PID。
当main开始在一个32位Linux进程中执行时,用户栈有如下图所示的组织结构。
在栈的顶部是main函数的三个参数:1)envp,它指向envp[]数组;2)argv,它指向argv[]数组;3)argc,它给出argv[]中非空指针的数量。
信号
对于只捕获一个信号并终止的程序来说,信号处理是简单直接的。然而当一个程序要捕获多个信号时,一些细微的问题就产生了。
> 待处理信号不会排队等待。任意类型至多只有一个待处理信号。如果有两个类型为k的信号传送到一个目的进程,而目的进程当前正在执行信号k的处理程序,所以信号k是阻塞的,那么第二个信号就被简单地丢弃,它不会排队等待。
> 系统调用可以被中断。像read、write和accept这样的系统调用,潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将errno设置为EINTR。
注:由于信号可以被阻塞和不会排队等待,所以不可以用信号来对其它进程中发生的事件计数。
非本地跳转
C语言提供了一种用户级的异常控制流形式,称为非本地跳转(nonlocal jump),它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要正常的调用-返回序列。非本地跳转通过setjmp和longjmp函数来提供的。
操作进程的工具
Linux系统提供了大量的监控和操作进程的有用工具。
STRACE:打印一个正在运行程序和它的子进程调用的每个系统调用的轨迹。
PMAP:显示进程的存储器映射。
阅读(10748) | 评论(0) | 转发(0) |