hankd@engr.uky.edu
尽管此 HOWTO 已经“重新发布”(v2.0,2004-06-28)以更新作者联系信息,但它有很多失效链接,并且一些信息严重过时。与其仅仅修复链接,不如将本文档进行大量重写,作为我们预计在 2004 年 7 月发布的指南。届时,此 HOWTO 将会过时。新旧文档的首选主页 URL 都是 http://aggregate.org/LDP/
并行处理 指的是通过将程序分成多个片段来加速程序执行的概念,这些片段可以同时在各自的处理器上执行。一个程序在 n 个处理器上执行的速度可能比在单个处理器上快 n 倍。
传统上,多个处理器是在专门设计的“并行计算机”中提供的;沿着这些思路,Linux 现在支持 SMP 系统(通常作为“服务器”出售),其中多个处理器在单个计算机内共享单个内存和总线接口。一组计算机(例如,一组各自运行 Linux 的 PC)也可以通过网络互连以形成并行处理集群。使用 Linux 进行并行计算的第三种选择是使用多媒体指令扩展(即 MMX)来并行处理整数数据向量。最后,也可以将 Linux 系统用作专用附加并行处理计算引擎的“主机”。本文档将详细讨论所有这些方法。
尽管使用多个处理器可以加速许多操作,但大多数应用程序还无法从并行处理中受益。基本上,只有在以下情况下,并行处理才是合适的:
好消息是,如果以上所有条件都成立,你会发现使用 Linux 的并行处理可以为某些执行复杂计算或操作大型数据集的程序产生超级计算机性能。更重要的是,它可以使用廉价的硬件做到这一点……你可能已经拥有了。作为额外的奖励,当并行 Linux 系统不忙于执行并行作业时,也很容易将其用于其他事情。
如果并行处理不是你想要的,但你希望至少在性能上获得适度的提升,你仍然可以做一些事情。例如,你可以通过迁移到更快的处理器、添加内存、用快速宽 SCSI 替换 IDE 磁盘等来提高顺序程序的性能。如果这正是你感兴趣的,请跳转到第 6.2 节;否则,请继续阅读。
尽管并行处理已在许多系统中使用多年,但大多数计算机用户仍然对其有些陌生。因此,在讨论各种替代方案之前,重要的是要熟悉一些常用术语。
SIMD(单指令流,多数据流)指的是一种并行执行模型,其中所有处理器同时执行相同的操作,但每个处理器都可以对其自己的数据进行操作。此模型自然地适合于对数组的每个元素执行相同操作的概念,因此通常与向量或数组操作相关联。由于所有操作本质上都是同步的,因此 SIMD 处理器之间的交互往往易于且高效地实现。
MIMD(多指令流,多数据流)指的是一种并行执行模型,其中每个处理器基本上独立运行。此模型最自然地适合于基于功能分解程序以进行并行执行的概念;例如,一个处理器可能更新数据库文件,而另一个处理器生成新条目的图形显示。这是一种比 SIMD 执行更灵活的模型,但它以调试噩梦的风险为代价,这种噩梦被称为竞争条件,在这种情况下,程序可能会由于定时变化重新排序一个处理器的操作相对于另一个处理器的操作而间歇性地失败。
SPMD(单程序,多数据)是 MIMD 的受限版本,其中所有处理器都运行相同的程序。与 SIMD 不同,执行 SPMD 代码的每个处理器可能会采用通过程序的不同控制流路径。
通信系统的带宽是在单位时间内可以传输的最大数据量……一旦数据传输开始。串行连接的带宽通常以 波特 或 比特/秒 (b/s) 衡量,这通常对应于 1/10 到 1/8 那么多的 字节/秒 (B/s)。例如,一个 1,200 波特的调制解调器传输大约 120 B/s,而一个 155 Mb/s ATM 网络连接快了近 130,000 倍,传输大约 17 MB/s。高带宽允许在处理器之间高效地传输大量数据块。
通信系统的延迟是传输一个对象所需的最小时间,包括任何发送和接收软件开销。延迟在并行处理中非常重要,因为它决定了最小的有用粒度,即一段代码产生并行执行加速所需的最小运行时间。基本上,如果一段代码的运行时间少于传输其结果值所需的时间(即延迟),则在该段代码需要结果值的处理器上串行执行该代码段将比并行执行更快;串行执行将避免通信开销。
消息传递是并行系统中处理器之间交互的模型。通常,消息由一个处理器上的软件构建,并通过互连网络发送到另一个处理器,然后该处理器必须接受消息内容并对其进行处理。尽管处理每条消息的开销(延迟)可能很高,但通常对每条消息可能包含多少信息几乎没有限制。因此,消息传递可以产生高带宽,使其成为将大量数据从一个处理器传输到另一个处理器的非常有效的方法。但是,为了最大限度地减少对昂贵的消息传递操作的需求,并行程序中的数据结构必须分布在处理器上,以便每个处理器引用的大多数数据都在其本地内存中……此任务称为数据布局。
共享内存是并行系统中处理器之间交互的模型。像运行 Linux 的多处理器 Pentium 机器这样的系统物理共享处理器之间的单个内存,因此一个处理器写入共享内存的值可以被任何处理器直接访问。或者,逻辑共享内存可以在每个处理器都有自己的内存的系统中实现,方法是将每个非本地内存引用转换为适当的处理器间通信。共享内存的任何一种实现通常都被认为比消息传递更容易使用。物理共享内存可以同时具有高带宽和低延迟,但前提是多个处理器不同时尝试访问总线;因此,数据布局仍然会严重影响性能,并且缓存效应等会使确定最佳布局变得困难。
在消息传递和共享内存模型中,通信由单个处理器发起;相反,聚合函数通信是一种本质上并行的通信模型,其中整组处理器协同工作。最简单的此类操作是屏障同步,其中每个单独的处理器等待直到组中的每个处理器都到达屏障。通过让每个处理器输出一个数据作为到达屏障的副作用,可以使通信硬件向每个处理器返回一个值,该值是来自所有处理器收集的值的任意函数。例如,返回值可能是问题“是否有任何处理器找到解决方案?”的答案,或者可能是来自每个处理器的值的总和。延迟可能非常低,但每个处理器的带宽也往往很低。传统上,此模型主要用于控制并行执行,而不是分发数据值。
这是聚合函数的另一个名称,最常用于指代使用多个消息传递操作构建的聚合函数。
SMP(对称多处理器)指的是一组处理器作为对等方协同工作的操作系统概念,因此任何工作都可以由任何处理器同样出色地完成。通常,SMP 意味着 MIMD 和共享内存的结合。在 IA32 世界中,SMP 通常意味着符合 MPS(英特尔多处理器规范);未来,它可能意味着“Slot 2”……
SWAR(寄存器内的 SIMD)是通用术语,指将寄存器划分为多个整数字段,并使用寄存器宽度操作跨这些字段执行 SIMD 并行计算的概念。给定一台具有 k 位寄存器、数据路径和功能单元的机器,长期以来人们都知道,普通寄存器操作可以充当多达 n 个、k/n 位字段值的 SIMD 并行操作。尽管可以使用普通整数寄存器和指令来实现这种类型的并行性,但许多高端微处理器最近添加了专用指令来增强此技术在面向多媒体任务方面的性能。除了英特尔/AMD/Cyrix MMX(多媒体扩展)之外,还有:Digital Alpha MAX(多媒体扩展)、惠普 PA-RISC MAX(多媒体加速扩展)、MIPS MDMX(数字媒体扩展,发音为“Mad Max”)和 Sun SPARC V9 VIS(可视指令集)。除了就 MMX 达成一致的三家供应商外,所有这些指令集扩展大致相当,但互不兼容。
附加处理器本质上是连接到主机系统的专用计算机,用于加速特定类型的计算。例如,用于 PC 的许多视频和音频卡都包含附加处理器,分别设计用于加速常见的图形操作和音频 DSP(数字信号处理)。还有各种各样的附加阵列处理器,之所以这样称呼它们是因为它们旨在加速数组上的算术运算。事实上,许多商业超级计算机实际上是带有工作站主机的附加处理器。
RAID(廉价磁盘冗余阵列)是一种用于提高磁盘 I/O 的带宽和可靠性的简单技术。尽管有许多不同的变体,但所有变体都有两个关键概念。首先,每个数据块都条带化分布在一组 n+k 个磁盘驱动器上,这样每个驱动器只需读取或写入 1/n 的数据……从而产生一个驱动器 n 倍的带宽。其次,写入冗余数据,以便在磁盘驱动器发生故障时可以恢复数据;这很重要,因为否则,如果 n+k 个驱动器中的任何一个发生故障,整个文件系统可能会丢失。关于 RAID 的一般概述,请访问 http://www.uni-mainz.de/~neuffer/scsi/what_is_raid.html,关于 Linux 系统的 RAID 选项的信息,请访问 http://linas.org/linux/raid.html。除了专门的 RAID 硬件支持外,Linux 还支持跨单个 Linux 系统托管的多个磁盘的软件 RAID 0、1、4 和 5;有关详细信息,请参阅 Software RAID mini-HOWTO 和 Multi-Disk System Tuning mini-HOWTO。集群中多台机器上的磁盘驱动器上的 RAID 不受直接支持。
IA32(英特尔架构,32 位)实际上与并行处理无关,而是指指令集通常与英特尔 386 的指令集兼容的处理器类别。基本上,286 之后的任何英特尔 x86 处理器都与以 IA32 为特征的 32 位平面内存模型兼容。AMD 和 Cyrix 也制造了大量 IA32 兼容处理器。由于 Linux 主要在 IA32 处理器上发展起来,并且商品市场也集中在那里,因此使用 IA32 来区分这些处理器与 PowerPC、Alpha、PA-RISC、MIPS、SPARC 等处理器是很方便的。即将到来的 IA64(64 位,具有 EPIC,显式并行指令计算)肯定会使情况变得复杂,但第一个 IA64 处理器 Merced 计划在 1999 年之前投产。
自从许多并行超级计算机公司倒闭以来,COTS(商用现货)通常被讨论为并行计算系统的要求。如果纯粹从狂热的角度来看,使用 PC 的唯一 COTS 并行处理技术是 SMP Windows NT 服务器和各种 MMX Windows 应用程序;那样狂热实际上并不划算。COTS 的基本概念实际上是最大限度地减少开发时间和成本。因此,COTS 更实用、更常见的含义是,至少大多数子系统受益于商品营销,但在其他技术有效的地方使用其他技术。最常见的,COTS 并行处理指的是集群,其中节点是商品 PC,但网络接口和软件在某种程度上是定制的……通常运行 Linux 和免费提供的应用程序代码(例如,copyleft 或公共领域),但实际上不是 COTS。
为了更好地理解本 HOWTO 中概述的各种并行编程方法的使用,有一个示例问题很有用。尽管几乎任何简单的并行算法都可以,但通过选择一种已用于演示各种其他并行编程系统的算法,比较和对比各种方法变得更容易一些。M. J. Quinn 的著作《并行计算理论与实践》,第二版,McGraw Hill,纽约,1994 年,使用了一种计算 Pi 值的并行算法来演示各种不同的并行超级计算机编程环境(例如,nCUBE 消息传递,Sequent 共享内存)。在本 HOWTO 中,我们使用了相同的基本算法。
该算法通过对 x 平方下的面积求和来计算 Pi 的近似值。作为一个纯粹的顺序 C 程序,该算法如下所示:
#include <stdlib.h>; #include <stdio.h>; main(int argc, char **argv) { register double width, sum; register int intervals, i; /* get the number of intervals */ intervals = atoi(argv[1]); width = 1.0 / intervals; /* do the computation */ sum = 0; for (i=0; i<intervals; ++i) { register double x = (i + 0.5) * width; sum += 4.0 / (1.0 + x * x); } sum *= width; printf("Estimation of pi is %f\n", sum); return(0); }
但是,这种顺序算法很容易产生“令人尴尬的并行”实现。该区域被细分为间隔,并且任意数量的处理器可以各自独立地对分配给它的间隔求和,而无需处理器之间的交互。一旦计算出局部和,它们就会被加在一起以创建全局和;此步骤需要处理器之间一定程度的协调和通信。最后,此全局和由一个处理器打印为 Pi 的近似值。
在本 HOWTO 中,此算法的各种并行实现在讨论每种不同的编程方法时出现。
本文档的其余部分分为五个部分。第 2、3、4 和 5 节对应于支持使用 Linux 进行并行处理的三种不同类型的硬件配置
本文档的最后一节涵盖了在使用 Linux 进行并行处理时具有普遍意义的方面,而不是特定于上述方法之一。
当你阅读本文档时,请记住我们尚未测试所有内容,并且此处报告的许多内容“仍然具有研究性质”(一种委婉的说法是“不能完全像应有的那样工作”;-)。但是,使用 Linux 的并行处理现在很有用,并且越来越多的团队正在努力使其变得更好。
本 HOWTO 的作者是 Hank Dietz 博士,目前是肯塔基大学电气与计算机工程系的网络教授和 James F. Hardymon 讲席教授,地址为肯塔基州列克星敦市,邮编 40506-0046。根据 Linux 文档项目指南,Dietz 保留本文档的权利。尽管已努力确保本演示文稿的正确性和公正性,但 Dietz 和肯塔基大学均不对任何问题或错误负责,并且肯塔基大学不认可所讨论的任何工作/产品。
本文档简要概述了如何使用 SMP Linux 系统进行并行处理。关于 SMP Linux 的最新信息可能通过 SMP Linux 项目邮件列表获得;发送电子邮件至 majordomo@vger.rutgers.edu,并在正文中输入 subscribe linux-smp
以加入列表。
SMP Linux 真的有效吗?在 1996 年 6 月,我购买了一个全新的(好吧,新的非品牌 ;-) 双处理器 100MHz Pentium 系统。完全组装的系统,包括两个处理器、华硕主板、256K 缓存、32M 内存、1.6G 磁盘、6X CDROM、Stealth 64 和 15 英寸 Acer 监视器,总共花费 1,800 美元。这只比同等的单处理器系统贵几百美元。让 SMP Linux 运行起来非常简单,只需安装“库存”的单处理器 Linux,在 makefile 中取消注释 SMP=1
行重新编译内核(尽管我发现将 SMP
设置为 1
有点讽刺 ;-),并告知 lilo
关于新内核。该系统性能良好,并且足够稳定,可以作为我从那以后的主要工作站。总而言之,SMP Linux 确实有效。
下一个问题是,在 SMP Linux 下编写和执行共享内存并行程序可以获得多少高级支持。在 1996 年初,支持不多。情况已经改变。例如,现在有一个非常完整的 POSIX 线程库。
尽管性能可能低于本机共享内存机制,但 SMP Linux 系统也可以使用最初为使用套接字通信的工作站集群开发的大多数并行处理软件。套接字(参见第 3.3 节)在 SMP Linux 系统内工作,甚至对于联网为集群的多个 SMP 也是如此。但是,套接字意味着 SMP 的许多不必要的开销。大部分开销都在内核或中断处理程序中;这加剧了问题,因为 SMP Linux 通常只允许一个处理器一次进入内核,并且中断控制器设置为仅引导处理器可以处理中断。尽管如此,典型的 SMP 通信硬件比大多数集群网络好得多,以至于集群软件通常在 SMP 上比在其设计的集群上运行得更好。
本节的其余部分讨论了 SMP 硬件,回顾了用于跨并行程序进程共享内存的基本 Linux 机制,对原子性、易变性、锁和缓存行做了一些观察,最后提供了一些指向其他共享内存并行处理资源的指针。
尽管 SMP 系统已经存在多年,但直到最近,每台这样的机器都倾向于以不同的方式实现基本功能,以至于操作系统支持不可移植。改变这种情况的是英特尔的多处理器规范,通常简称为 MPS。MPS 1.4 规范目前以 PDF 文件形式在 http://www.intel.com/design/pro/datashts/242016.htm 上提供,并且在 http://support.intel.com/oem_developer/ial/support/9300.HTM 上有一个 MPS 1.1 的简要概述,但请注意英特尔经常重新排列其 WWW 站点。许多 供应商 正在构建 MPS 兼容系统,支持多达四个处理器,但 MPS 理论上允许更多处理器。
SMP Linux 支持的唯一非 MPS、非 IA32 系统是 Sun4m 多处理器 SPARC 机器。SMP Linux 支持大多数英特尔 MPS 1.1 或 1.4 兼容机器,最多可支持 16 个 486DX、Pentium、Pentium MMX、Pentium Pro 或 Pentium II 处理器。不受支持的 IA32 处理器包括英特尔 386、英特尔 486SX/SLC 处理器(缺少浮点硬件会干扰 SMP 机制)以及 AMD 和 Cyrix 处理器(它们需要不同的 SMP 支持芯片,但在撰写本文时似乎不可用)。
重要的是要理解,MPS 兼容系统的性能可能差异很大。正如预期的那样,性能差异的一个原因是处理器速度:更快的时钟速度往往会产生更快的系统,并且 Pentium Pro 处理器比 Pentium 更快。但是,MPS 并没有真正指定硬件如何实现共享内存,而只是指定了从软件角度来看该实现必须如何工作;这意味着性能还取决于共享内存实现如何与 SMP Linux 和你的特定程序的特性交互。
符合 MPS 的系统之间的主要区别在于它们如何实现对物理共享内存的访问。
一些 MPS Pentium 系统以及所有 MPS Pentium Pro 和 Pentium II 系统都具有独立的 L2 缓存。(L2 缓存封装在 Pentium Pro 或 Pentium II 模块中。)独立的 L2 缓存通常被认为可以最大限度地提高计算性能,但在 Linux 下情况并非如此明显。主要复杂性在于,当前的 SMP Linux 调度程序不会尝试将每个进程保持在同一处理器上,这种概念称为处理器亲和性。这种情况可能很快就会改变;最近在 SMP Linux 开发社区中就此进行了一些讨论,标题为“处理器绑定”。在没有处理器亲和性的情况下,当进程在与上次执行它的处理器不同的处理器上获得时间片时,拥有独立的 L2 缓存可能会引入显着的开销。
许多相对便宜的系统组织成两个 Pentium 处理器共享单个 L2 缓存。坏消息是,这会导致缓存争用,严重降低运行多个独立顺序程序时的性能。好消息是,许多并行程序实际上可能会从共享缓存中受益,因为如果两个处理器都想从共享内存中访问同一行,则只需要一个处理器将其提取到缓存中,并且避免了总线争用。在共享 L2 缓存的情况下,缺少处理器亲和性也会造成较小的损害。因此,对于并行程序,共享 L2 缓存是否像人们预期的那样有害,实际上并不清楚。
我们使用双 Pentium 共享 256K 缓存系统的经验表明,性能范围很广,具体取决于所需的内核活动级别。最坏的情况是,我们只看到大约 1.2 倍的加速。但是,我们也看到了高达 2.1 倍的加速,这表明计算密集型 SPMD 风格的代码确实从“共享获取”效果中获益。
首先要说的是,大多数现代系统将处理器连接到一个或多个 PCI 总线,而 PCI 总线又“桥接”到一个或多个 ISA/EISA 总线。这些桥接器增加了延迟,并且 EISA 和 ISA 通常比 PCI 提供更低的带宽(ISA 最低),因此磁盘驱动器、视频卡和其他高性能设备通常应通过 PCI 总线接口连接。
尽管 MPS 系统即使只有一个 PCI 总线,也可以为许多计算密集型并行程序实现良好的加速,但 I/O 操作的性能不高于单处理器性能……并且可能由于处理器的总线争用而略差。因此,如果你希望加速 I/O,请确保你获得具有多个独立 PCI 总线和 I/O 控制器(例如,多个 SCSI 链)的 MPS 系统。你需要小心确保 SMP Linux 支持你获得的东西。还要记住,当前的 SMP Linux 本质上只允许一个处理器在任何时候进入内核,因此你应该仔细选择你的 I/O 控制器,以选择那些最大限度地减少每个 I/O 操作所需的内核时间的控制器。为了获得真正的高性能,你甚至可以考虑直接从用户进程执行原始设备 I/O,而无需系统调用……这不一定像听起来那么难,并且不需要损害安全性(有关基本技术的描述,请参见第 3.3 节)。
重要的是要注意,在过去几年中,总线速度和处理器时钟频率之间的关系变得非常模糊。尽管大多数系统现在使用相同的 PCI 时钟频率,但将更快的处理器时钟与更慢的总线时钟配对的情况并不少见。这方面的经典例子是 Pentium 133 通常使用比 Pentium 150 更快的总线,在各种基准测试中表现出非常奇怪的性能。这些影响在 SMP 系统中被放大;拥有更快的总线时钟甚至更重要。
内存交织实际上与 MPS 没有任何关系,但你经常会在 MPS 系统中看到它被提及,因为这些系统通常对内存带宽的要求更高。基本上,双向或四向交织组织 RAM,以便使用多个 RAM 库而不是仅使用一个 RAM 库来完成块访问。这提供了更高的内存访问带宽,特别是对于缓存行加载和存储。
然而,关于这一点的情况有点混乱,因为 EDO DRAM 和各种其他内存技术倾向于改进类似的运算。关于 DRAM 技术的出色概述,请参见 http://www.pcguide.com/ref/ram/tech.htm。
因此,例如,拥有 2 路交织 EDO DRAM 还是非交织 SDRAM 更好?这是一个非常好的问题,没有简单的答案,因为交织和异构 DRAM 技术往往都很昂贵。在更普通的内存配置上投入相同的美元通常会给你带来更大的主内存。即使是最慢的 DRAM 也仍然比使用基于磁盘的虚拟内存快得多……
好的,所以你已经决定在 SMP 上进行并行处理是一件很棒的事情……你如何开始入门?嗯,第一步是了解一点共享内存通信的真正工作方式。
听起来你只是让一个处理器将一个值存储到内存中,而另一个处理器加载它;不幸的是,它并没有那么简单。例如,进程和处理器之间的关系非常模糊;但是,如果我们活动进程的数量不超过处理器数量,则这些术语大致可以互换。本节的其余部分简要总结了如果你不了解它们可能会导致严重问题的关键问题:用于确定共享内容的两种不同模型、原子性问题、易变性的概念、硬件锁指令、缓存行效应和 Linux 调度程序问题。
共享内存编程中常用的两种根本不同的模型:共享一切和共享某些内容。这两种模型都允许处理器通过从/向共享内存加载和存储来通信;区别在于,共享一切模型将所有数据结构都放在共享内存中,而共享某些内容模型则要求用户显式指示哪些数据结构可能被共享,哪些数据结构是单个处理器的私有数据结构。
应该使用哪种共享内存模型? 这在很大程度上是一个信仰问题。 很多人喜欢“共享一切”模型,因为他们不需要在声明时就确定哪些数据结构应该共享……你只需在可能冲突的共享对象访问周围放置锁,以确保在任何时刻只有一个进程(或处理器)可以访问。 然而,这实际上并没有那么简单……因此许多人更喜欢相对安全的“共享部分”模型。
共享一切的好处在于,你可以轻松地将现有的顺序程序逐步转换为共享一切的并行程序。 你不必首先确定哪些数据需要被其他处理器访问。
简而言之,共享一切的主要问题是,一个处理器采取的任何操作都可能影响其他处理器。 这个问题以两种方式浮出水面:
errno
的变量中返回错误代码; 如果两个“共享一切”进程执行各种调用,它们会相互干扰,因为它们共享同一个 errno
。 尽管现在有一个库版本修复了 errno
问题,但类似的问题仍然存在于大多数库中。 例如,除非采取特殊的预防措施,否则如果从多个“共享一切”进程进行调用,X 库将无法工作。core
文件,提示你发生了什么。 在“共享一切”并行处理中,很可能 stray access 会导致不是 fault 进程的进程崩溃,使得定位和纠正错误几乎不可能。当使用“共享部分”时,这两种类型的问题都不常见,因为只有显式标记的数据结构才会被共享。 同样显而易见的是,“共享一切”仅在所有处理器执行完全相同的内存映像时才有效; 你不能在多个不同的代码映像之间使用“共享一切”(即,只能使用 SPMD,而不能使用通用的 MIMD)。
最常见的“共享一切”编程支持是线程库。 线程本质上是“轻量级”进程,它们可能不像常规 UNIX 进程那样被调度,最重要的是,它们共享对单个内存映射的访问。 POSIX Pthreads 包一直是许多移植工作的重点; 最大的问题是,在 SMP Linux 下,这些端口中的任何一个实际上是否并行运行程序的线程(理想情况下,每个线程都有一个处理器)。 POSIX API 没有要求这样做,并且像 http://www.aa.net/~mtp/PCthreads.html 这样的版本显然没有实现并行线程执行 - 程序的所有线程都保留在单个 Linux 进程中。
第一个支持 SMP Linux 并行性的线程库是现在有些过时的 bb_threads 库,ftp://caliban.physics.utoronto.ca/pub/linux/,这是一个非常小的库,它使用 Linux clone()
调用来 fork 新的、独立调度的 Linux 进程,所有进程共享一个地址空间。 SMP Linux 机器可以并行运行多个这些“线程”,因为每个“线程”都是一个完整的 Linux 进程; 缺点是你无法获得某些操作系统下的线程库提供的相同的“轻量级”调度控制。 该库使用了一些 C 包装的汇编代码,将新的内存块安装为每个线程的堆栈,并为锁数组(互斥对象)提供原子访问函数。 文档包括一个 README
和一个简短的示例程序。
最近,开发了一个使用 clone()
的 POSIX 线程版本。 这个库 LinuxThreads,显然是在 SMP Linux 下使用的首选“共享一切”库。 POSIX 线程有完善的文档,并且 LinuxThreads README 和 LinuxThreads FAQ 都做得非常好。 现在主要的问题仅仅是 POSIX 线程有很多细节需要正确处理,而 LinuxThreads 仍然是一个正在进行中的工作。 还有一个问题是 POSIX 线程标准已经通过标准化过程发展,因此你需要小心,不要为过时的早期版本的标准编程。
“共享部分”实际上是“只共享需要共享的内容”。 这种方法可以用于通用的 MIMD(不仅仅是 SPMD),前提是小心地将共享对象分配到每个处理器内存映射中的相同位置。 更重要的是,“共享部分”使预测和调整性能、调试代码等变得更容易。 唯一的问题是:
目前,有两种非常相似的机制允许 Linux 进程组拥有独立的内存空间,所有进程仅共享相对较小的内存段。 假设你没有愚蠢地在你配置 Linux 系统时排除“System V IPC”,Linux 支持一种非常可移植的机制,它通常被称为“System V 共享内存”。 另一种选择是内存映射工具,其实现方式在不同的 UNIX 系统中差异很大:mmap()
系统调用。 你可以并且应该从手册页中了解这些调用……但在第 2.5 节和 2.6 节中给出了每个调用的简要概述,以帮助你入门。
无论你使用上述两种模型中的哪一种,结果几乎都是相同的:你获得了一个指向可读/写内存块的指针,该内存块可由你的并行程序中的所有进程访问。 这是否意味着我可以像访问普通本地内存中的共享内存对象一样访问我的并行程序? 嗯,不完全是……
原子性指的是对对象的操作作为一个不可分割、不可中断的序列完成的概念。 不幸的是,共享内存访问并不意味着对共享内存中所有数据的操作都是原子发生的。 除非采取特殊的预防措施,否则只有在单个总线事务中发生的简单加载或存储操作(即,对齐的 8 位、16 位或 32 位操作,但不是未对齐的或 64 位操作)才是原子的。 更糟糕的是,像 GCC 这样的“智能”编译器通常会执行优化,这些优化可能会消除确保其他处理器可以看到此处理器所做操作所需的内存操作。 幸运的是,这两个问题都可以补救……只剩下访问效率和缓存行大小之间的关系让我们担心了。
然而,在讨论这些问题之前,指出所有这些都假设每个处理器的内存引用都按照它们被编码的顺序发生是有用的。 Pentium 就是这样做的,但也指出未来的 Intel 处理器可能不会这样做。 因此,对于未来的处理器,请记住,可能需要用一些指令包围一些共享内存访问,这些指令会导致所有挂起的内存访问完成,从而提供内存访问顺序。 CPUID
指令显然被保留具有这种副作用。
为了防止 GCC 的优化器将共享内存对象的值缓冲在寄存器中,共享内存中的所有对象都应声明为具有 volatile
属性的类型。 如果这样做,所有只需要一次字访问的共享对象读取和写入都将原子地发生。 例如,假设 p 是一个指向整数的指针,其中指针和它将指向的整数都在共享内存中; ANSI C 声明可能是
volatile int * volatile p;
在此代码中,第一个 volatile
指的是 p
最终将指向的 int
; 第二个 volatile
指的是指针本身。 是的,这很烦人,但这是为使 GCC 能够执行一些非常强大的优化而付出的代价。 至少在理论上,GCC 的 -traditional
选项可能足以生成正确的代码,但会牺牲一些优化,因为前 ANSI K&R C 本质上声称所有变量都是 volatile 的,除非显式声明为 register
。 尽管如此,如果你的典型 GCC 编译看起来像 cc -O6 ...
,你真的会希望仅在必要时才显式地将事物标记为 volatile。
有传言说,使用标记为修改所有处理器寄存器的汇编语言锁将导致 GCC 适当地刷新所有变量,从而避免与声明为 volatile
的事物相关的“低效”编译代码。 这种 hack 对于使用 GCC 版本 2.7.0 的静态分配的全局变量似乎有效……但是,ANSI C 标准不要求这种行为。 更糟糕的是,其他只进行读取访问的进程可以将值永远缓冲在寄存器中,从而永远不会注意到共享内存值实际上已更改。 总之,做你想做的,但只有通过 volatile
访问的变量才能保证正常工作。
请注意,你可以通过使用强制 volatile
属性的类型转换来导致对普通变量的 volatile 访问。 例如,普通的 int i;
可以通过 *((volatile int *) &i)
作为 volatile 引用; 因此,你可以仅在关键时刻显式调用 volatile 的“开销”。
如果你认为 ++i;
总是可以工作来将变量 i
在共享内存中加一,那你将有一个令人讨厌的小惊喜:即使编码为单个指令,结果的加载和存储也是单独的内存事务,并且其他处理器可能在这两个事务之间访问 i
。 例如,让两个进程都执行 ++i;
可能只会将 i
递增一,而不是递增二。 根据 Intel Pentium “架构和编程手册”,LOCK
前缀可用于确保以下任何指令相对于它访问的数据内存位置是原子的:
BTS, BTR, BTC mem, reg/imm XCHG reg, mem XCHG mem, reg ADD, OR, ADC, SBB, AND, SUB, XOR mem, reg/imm NOT, NEG, INC, DEC mem CMPXCHG, XADD
但是,使用所有这些操作可能不是一个好主意。 例如,XADD
甚至在 386 中都不存在,因此对其进行编码可能会导致可移植性问题。
XCHG
指令总是断言锁,即使没有 LOCK
前缀,因此显然是构建更高级别原子构造(如信号量和共享队列)的首选原子操作。 当然,你无法仅通过编写 C 代码来让 GCC 生成此指令……相反,你必须使用一些内联汇编代码。 给定一个字大小的 volatile 对象 obj 和一个字大小的寄存器值 reg,GCC 内联汇编代码是
__asm__ __volatile__ ("xchgl %1,%0" :"=r" (reg), "=m" (obj) :"r" (reg), "m" (obj));
使用位操作进行锁定的 GCC 内联汇编代码的示例在 bb_threads 库的源代码中给出。
然而,重要的是要记住,使内存事务原子化是有成本的。 锁定操作会带来相当大的开销,并可能延迟来自其他处理器的内存活动,而普通引用可能会使用本地缓存。 当尽可能少地使用锁定操作时,可以获得最佳性能。 此外,这些 IA32 原子指令显然不可移植到其他系统。
有许多替代方法允许使用普通指令来实现各种同步,包括互斥 - 确保在任何时刻最多只有一个处理器正在更新给定的共享对象。 大多数操作系统教科书至少讨论了这些技术中的一种。 Abraham Silberschatz 和 Peter B. Galvin 的操作系统概念第四版 ISBN 0-201-50480-4 中有一个相当好的讨论。
另一个基本的原子性问题可能会对 SMP 性能产生巨大影响:缓存行大小。 尽管 MPS 标准要求无论使用何种缓存,引用都必须是一致的,但事实是,当一个处理器写入内存的特定行时,旧行的每个缓存副本都必须无效或更新。 这意味着,如果两个或多个处理器都在将数据写入同一行的不同部分,则可能会导致大量的缓存和总线流量,实际上是将该行从缓存传递到缓存。 这个问题被称为伪共享。 解决方案很简单,就是尝试组织数据,以便并行访问的数据倾向于来自每个进程的不同缓存行。
你可能会认为,在使用共享 L2 缓存的系统中,伪共享不是问题,但请记住,仍然存在单独的 L1 缓存。 缓存组织和单独级别的数量都可能有所不同,但 Pentium L1 缓存行大小为 32 字节,典型的外部缓存行大小约为 256 字节。 假设两个项目的地址(物理地址或虚拟地址)分别为 a 和 b,并且每个处理器的最大缓存行大小为 c,我们假设 c 是 2 的幂。 非常精确地说,如果 ((int) a) & ~(c - 1)
等于 ((int) b) & ~(c - 1)
,则两个引用都在同一缓存行中。 一个更简单的规则是,如果并行引用的共享对象至少相隔 c 字节,它们应该映射到不同的缓存行。
尽管使用共享内存进行并行处理的全部意义在于避免 OS 开销,但 OS 开销可能来自通信本身以外的其他方面。 我们已经说过,应该构建的进程数小于或等于机器中的处理器数。 但是你如何准确地决定要创建多少个进程呢?
为了获得最佳性能,你的并行程序中的进程数应等于你的程序进程的预期数量,这些进程可以同时在不同的处理器上运行。 例如,如果一台四处理器 SMP 通常有一个进程为了其他目的(例如,WWW 服务器)而积极运行,那么你的并行程序应该只使用三个进程。 你可以通过查看 uptime
命令引用的“负载平均值”来大致了解系统上有多少其他进程处于活动状态。
或者,你可以使用例如 renice
命令或 nice()
系统调用来提高并行程序中进程的优先级。 你必须具有特权才能提高优先级。 想法很简单,就是将其他进程从处理器中挤出,以便你的程序可以在所有处理器上同时运行。 使用 http://luz.cs.nmt.edu/~rtlinux/ 上的 SMP Linux 原型版本可以更明确地实现这一点,该版本提供实时调度器。
如果你不是唯一将你的 SMP 系统视为并行机器的用户,那么你也可能在两个或多个尝试同时执行的并行程序之间存在冲突。 这种标准解决方案是组调度 - 即,操作调度优先级,以便在任何给定时刻,只有单个并行程序的进程正在运行。 然而,值得回顾的是,使用更多的并行性往往会产生边际效益递减,并且调度器活动会增加开销。 因此,例如,对于一台四处理器机器来说,运行两个每个有两个进程的程序可能比在两个每个有四个进程的程序之间进行组调度更好。
这里还有一个曲折。 假设你正在一台白天使用繁忙的机器上开发程序,但该机器将在晚上完全可用于并行执行。 你需要使用完整数量的进程编写和测试你的代码的正确性,即使你知道你的白天测试运行会很慢。 嗯,如果你有进程忙等待共享内存值被当前未运行(在其他处理器上)的其他进程更改,它们将会非常慢。 如果你在单处理器系统上开发和测试你的代码,也会出现同样的问题。
解决方案是在你的代码中嵌入调用,无论它可能在何处循环等待来自另一个处理器的操作,以便 Linux 将给另一个进程运行的机会。 我使用一个 C 宏,称之为 IDLE_ME
来做到这一点:对于测试运行,使用 cc -DIDLE_ME=usleep(1); ...
编译; 对于“生产”运行,使用 cc -DIDLE_ME={} ...
编译。 usleep(1)
调用请求 1 微秒的睡眠,这具有允许 Linux 调度器选择不同的进程在该处理器上运行的效果。 如果进程数是可用处理器数的两倍以上,则带有 usleep(1)
调用的代码比没有它们的代码运行速度快十倍并不罕见。
bb_threads(“Bare Bones”线程)库,ftp://caliban.physics.utoronto.ca/pub/linux/,是一个非常简单的库,它演示了 Linux clone()
调用的使用。 gzip tar
文件只有 7K 字节! 尽管此库基本上已被第 2.4 节中讨论的 LinuxThreads 库所取代,但 bb_threads 仍然可用,并且它足够小巧和简单,可以很好地作为 Linux 线程支持使用的入门。 当然,阅读此源代码比浏览 LinuxThreads 的源代码要容易得多。 总之,bb_threads 库是一个好的起点,但实际上不适合编码大型项目。
使用 bb_threads 库的基本程序结构是
bb_threads_stacksize(b)
设置为 b 字节。MAX_MUTEXES
,并通过 bb_threads_mutexcreate(i)
初始化锁 i。void
的函数 f,并使用单个参数 arg,你可以执行类似 bb_threads_newthread(f, &arg)
的操作,其中 f 应该声明为类似 void f(void *arg, size_t dummy)
的形式。 如果你需要传递多个参数,请传递指向初始化为保存参数值的结构的指针。bb_threads_lock(n)
和 bb_threads_unlock(n)
,其中 n 是标识要使用的锁的整数。 请注意,此库中的锁定和解锁操作是非常基本的自旋锁,它们使用原子总线锁定指令,这可能会导致过度的内存引用干扰,并且不尝试确保公平性。 与 bb_threads 打包在一起的演示程序没有正确使用锁来防止从函数 fnn
和 main
中同时执行 printf()
……因此,演示并不总是有效。 我不是说要贬低演示,而是要强调这东西非常棘手; 此外,使用 LinuxThreads 也只是稍微容易一些。return
时,它实际上会销毁进程……但本地堆栈内存不会自动释放。 确切地说,Linux 不支持释放,但内存空间不会自动添加回 malloc()
空闲列表。 因此,父进程应该通过 bb_threads_cleanup(wait(NULL))
回收每个死亡子进程的空间。
以下 C 程序使用第 1.3 节中讨论的算法,使用两个 bb_threads 线程计算 Pi 的近似值。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include "bb_threads.h" volatile double pi = 0.0; volatile int intervals; volatile int pids[2]; /* Unix PIDs of threads */ void do_pi(void *data, size_t len) { register double width, localsum; register int i; register int iproc = (getpid() != pids[0]); /* set width */ width = 1.0 / intervals; /* do the local computations */ localsum = 0; for (i=iproc; i<intervals; i+=2) { register double x = (i + 0.5) * width; localsum += 4.0 / (1.0 + x * x); } localsum *= width; /* get permission, update pi, and unlock */ bb_threads_lock(0); pi += localsum; bb_threads_unlock(0); } int main(int argc, char **argv) { /* get the number of intervals */ intervals = atoi(argv[1]); /* set stack size and create lock... */ bb_threads_stacksize(65536); bb_threads_mutexcreate(0); /* make two threads... */ pids[0] = bb_threads_newthread(do_pi, NULL); pids[1] = bb_threads_newthread(do_pi, NULL); /* cleanup after two threads (really a barrier sync) */ bb_threads_cleanup(wait(NULL)); bb_threads_cleanup(wait(NULL)); /* print the result */ printf("Estimation of pi is %f\n", pi); /* check-out */ exit(0); }
LinuxThreads http://pauillac.inria.fr/~xleroy/linuxthreads/ 是对 POSIX 1003.1c 线程标准规定的“共享一切”的一个相当完整和可靠的实现。 与其他 POSIX 线程端口不同,LinuxThreads 使用与 bb_threads 相同的 Linux 内核线程工具 (clone()
)。 POSIX 兼容性意味着相对容易地从其他系统移植相当多的线程应用程序,并且有各种教程材料可用。 简而言之,这绝对是在 Linux 下用于开发大规模线程程序的线程包。
使用 LinuxThreads 库的基本程序结构是
pthread_mutex_t lock
类型的变量。 使用 pthread_mutex_init(&lock,val)
初始化你将需要使用的每个锁。pthread_t
类型的变量来标识每个线程。 要创建运行 f()
的线程 pthread_t thread
,可以调用 pthread_create(&thread,NULL,f,&arg)
。pthread_mutex_lock(&lock)
和 pthread_mutex_unlock(&lock)
。pthread_join(thread,&retval)
在每个线程之后进行清理。-D_REENTRANT
。下面是一个使用 LinuxThreads 进行 Pi 并行计算的示例。 使用了第 1.3 节的算法,并且与 bb_threads 示例一样,两个线程并行执行。
#include <stdio.h> #include <stdlib.h> #include "pthread.h" volatile double pi = 0.0; /* Approximation to pi (shared) */ pthread_mutex_t pi_lock; /* Lock for above */ volatile double intervals; /* How many intervals? */ void * process(void *arg) { register double width, localsum; register int i; register int iproc = (*((char *) arg) - '0'); /* Set width */ width = 1.0 / intervals; /* Do the local computations */ localsum = 0; for (i=iproc; i<intervals; i+=2) { register double x = (i + 0.5) * width; localsum += 4.0 / (1.0 + x * x); } localsum *= width; /* Lock pi for update, update it, and unlock */ pthread_mutex_lock(&pi_lock); pi += localsum; pthread_mutex_unlock(&pi_lock); return(NULL); } int main(int argc, char **argv) { pthread_t thread0, thread1; void * retval; /* Get the number of intervals */ intervals = atoi(argv[1]); /* Initialize the lock on pi */ pthread_mutex_init(&pi_lock, NULL); /* Make the two threads */ if (pthread_create(&thread0, NULL, process, "0") || pthread_create(&thread1, NULL, process, "1")) { fprintf(stderr, "%s: cannot make thread\n", argv[0]); exit(1); } /* Join (collapse) the two threads */ if (pthread_join(thread0, &retval) || pthread_join(thread1, &retval)) { fprintf(stderr, "%s: thread join failed\n", argv[0]); exit(1); } /* Print the result */ printf("Estimation of pi is %f\n", pi); /* Check-out */ exit(0); }
System V IPC(进程间通信)支持由许多系统调用组成,这些系统调用提供消息队列、信号量和共享内存机制。 当然,这些机制最初旨在用于多进程在单处理器系统内进行通信。 然而,这意味着它也应该适用于 SMP Linux 下的进程之间进行通信,无论它们在哪个处理器上运行。
在深入了解如何使用这些调用之前,重要的是要理解,尽管 System V IPC 调用确实存在用于信号量和消息传输等操作,但你可能不应该使用它们。 为什么不呢? 这些函数通常在 SMP Linux 下速度很慢且串行化。 就说这么多。
创建一组进程共享访问共享内存段的基本过程是
shmget()
来创建所需大小的新段。 或者,此调用可用于获取预先存在的共享内存段的 ID。 在任何一种情况下,返回值都是共享内存段 ID 或 -1(表示错误)。 例如,要创建一个 b 字节的共享内存段,调用可能是 shmid = shmget(IPC_PRIVATE, b, (IPC_CREAT | 0666))
。shmat()
调用允许程序员指定段应出现的虚拟地址,但选择的地址必须在页面边界上对齐(即,是 getpagesize()
返回的页面大小的倍数,通常为 4096 字节),并且将覆盖先前位于该地址的任何内存的映射。 因此,我们更喜欢让系统选择地址。 在任何一种情况下,返回值都是指向刚刚映射的段的基虚拟地址的指针。 代码是 shmptr = shmat(shmid, 0, 0)
。 请注意,你可以通过简单地将所有共享变量声明为 struct
类型的成员,并将 shmptr 声明为指向该类型的指针,来将所有静态共享变量分配到此共享内存段中。 使用此技术,共享变量 x 将作为 shmptr->
x 访问。shmctl()
来设置此默认操作。 代码类似于 shmctl(shmid, IPC_RMID, 0)
。fork()
调用来创建所需数量的进程……每个进程都将继承共享内存段。shmdt(shmptr)
完成的。
尽管上述设置确实需要一些系统调用,但一旦建立了共享内存段,一个处理器对该内存中的值所做的任何更改都将自动对所有进程可见。 最重要的是,每个通信操作都将在没有系统调用开销的情况下发生。
以下是一个使用 System V 共享内存段的 C 程序示例。 它使用第 1.3 节中给出的相同算法计算 Pi。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ipc.h> #include <sys/shm.h> volatile struct shared { double pi; int lock; } *shared; inline extern int xchg(register int reg, volatile int * volatile obj) { /* Atomic exchange instruction */ __asm__ __volatile__ ("xchgl %1,%0" :"=r" (reg), "=m" (*obj) :"r" (reg), "m" (*obj)); return(reg); } main(int argc, char **argv) { register double width, localsum; register int intervals, i; register int shmid; register int iproc = 0;; /* Allocate System V shared memory */ shmid = shmget(IPC_PRIVATE, sizeof(struct shared), (IPC_CREAT | 0600)); shared = ((volatile struct shared *) shmat(shmid, 0, 0)); shmctl(shmid, IPC_RMID, 0); /* Initialize... */ shared->pi = 0.0; shared->lock = 0; /* Fork a child */ if (!fork()) ++iproc; /* get the number of intervals */ intervals = atoi(argv[1]); width = 1.0 / intervals; /* do the local computations */ localsum = 0; for (i=iproc; i<intervals; i+=2) { register double x = (i + 0.5) * width; localsum += 4.0 / (1.0 + x * x); } localsum *= width; /* Atomic spin lock, add, unlock... */ while (xchg((iproc + 1), &(shared->lock))) ; shared->pi += localsum; shared->lock = 0; /* Terminate child (barrier sync) */ if (iproc == 0) { wait(NULL); printf("Estimation of pi is %f\n", shared->pi); } /* Check out */ return(0); }
在此示例中,我使用了 IA32 原子交换指令来实现锁定。 为了获得更好的性能和可移植性,请替换一种避免原子总线锁定指令的同步技术(在第 2.2 节中讨论)。
在调试代码时,记住 ipcs
命令将报告当前正在使用的 System V IPC 设施的状态是有用的。
使用系统调用进行文件 I/O 可能非常昂贵; 事实上,这就是为什么存在用户缓冲的文件 I/O 库(getchar()
、fwrite()
等)的原因。 但是,如果多个进程正在访问同一个可写文件,则用户缓冲区不起作用,并且用户缓冲区管理开销很大。 BSD UNIX 对此的修复是添加了一个系统调用,该调用允许将文件的一部分映射到用户内存中,本质上是使用虚拟内存分页机制来导致更新。 来自 Sequent 的系统多年来也使用了相同的机制作为其共享内存并行处理支持的基础。 尽管(相当旧的)手册页中存在一些非常负面的评论,但 Linux 似乎正确地执行了至少一些基本功能,并且它支持此系统调用的退化使用,以映射可以在多个进程之间共享的匿名内存段。
本质上,Linux 的 mmap()
实现是第 2.5 节中概述的 System V 共享内存方案中步骤 2、3 和 4 的插件替代品。 要创建匿名共享内存段
shmptr = mmap(0, /* system assigns address */ b, /* size of shared memory segment */ (PROT_READ | PROT_WRITE), /* access rights, can be rwx */ (MAP_ANON | MAP_SHARED), /* anonymous, shared */ 0, /* file descriptor (not used) */ 0); /* file offset (not used) */
与 System V 共享内存 shmdt()
调用等效的是 munmap()
munmap(shmptr, b);
在我看来,使用 mmap()
而不是 System V 共享内存支持没有任何真正的好处。
本节尝试概述使用 Linux 的集群并行处理。 集群目前是最流行且最多样化的方法,范围从传统的工作站网络 (NOW) 到本质上是定制的并行机器,而这些机器恰好使用 Linux PC 作为处理器节点。 还有相当多的软件支持使用 Linux 机器集群进行并行处理。
集群并行处理提供了几个重要的优势:
好吧,所以集群是免费的或便宜的,而且可以非常大且高度可用……为什么不是每个人都使用集群呢?嗯,也存在一些问题
ps
命令仅报告一台 Linux 系统上运行的进程,而不是跨 Linux 系统集群运行的所有进程。
因此,基本情况是集群提供了巨大的潜力,但对于大多数应用程序来说,实现这种潜力可能非常困难。好消息是有相当多的软件支持可以帮助你在非常适合这种环境的程序中获得良好的性能,并且还有专门设计的网络来扩大可以获得良好性能的程序范围。
计算机网络是一个蓬勃发展的领域……但这你已经知道了。越来越多的网络技术和产品正在被开发出来,并且大多数都可以应用于将一组机器(即每台运行 Linux 的 PC)变成并行处理集群的形式。
不幸的是,没有一种网络技术能够最好地解决所有问题;事实上,方法、成本和性能的范围之广,起初很难让人相信。例如,使用标准的商用硬件,每台联网机器的成本从不到 5 美元到超过 4,000 美元不等。交付的带宽和延迟也各有四个数量级的差异。
在尝试了解特定网络之前,重要的是要认识到这些东西变化很快(请参阅 http://www.linux.org.uk/NetNews.html 以获取 Linux 网络新闻),并且很难获得关于某些网络的准确数据。
在特别不确定的地方,我放置了一个?。我花了很多时间研究这个主题,但我确信我的总结充满了错误,并且遗漏了很多重要的东西。如果你有任何更正或补充,请发送电子邮件至 hankd@engr.uky.edu。
像 http://web.syr.edu/~jmwobus/comfaqs/lan-technology.html 上的 LAN 技术记分卡之类的摘要提供了一些不同类型网络和 LAN 标准的特性。但是,本 HOWTO 中的摘要侧重于与构建 Linux 集群最相关的网络属性。讨论每个网络的部分都以简短的特性列表开始。以下定义了这些条目的含义。
如果答案是否,则含义非常明确。其他答案试图描述用于访问网络的基本程序接口。大多数网络硬件通过内核驱动程序进行接口,通常支持 TCP/UDP 通信。一些其他网络使用更直接的(例如,库)接口,通过绕过内核来减少延迟。
多年前,通过 OS 调用访问浮点单元被认为是完全可以接受的,但现在这显然是荒谬的;在我看来,对于并行程序中处理器之间的每次通信都需要 OS 调用来说,这同样是笨拙的。问题是计算机尚未集成这些通信机制,因此非内核方法往往存在可移植性问题。在不久的将来,你将会听到更多关于这方面的信息,主要以新的 虚拟接口 (VI) 架构,http://www.viarch.org/ 的形式出现,这是一种标准化的方法,用于大多数网络接口操作绕过通常的 OS 调用层。VI 标准得到了 Compaq、Intel 和 Microsoft 的支持,并且肯定会对未来几年的 SAN(系统区域网络)设计产生重大影响。
这是每个人都关心的数字。我通常使用理论上的最佳情况数字;你的实际情况会有所不同。
在我看来,这是每个人都应该比带宽更关心的数字。同样,我使用了不切实际的最佳情况数字,但至少这些数字确实包括了所有延迟来源,包括硬件和软件。在大多数情况下,网络延迟仅为几微秒;更大的数字反映了低效的硬件和软件接口层。
简而言之,这描述了你如何获得这种类型的网络硬件。商品化的东西可以从许多供应商处广泛获得,价格是主要的区别因素。多供应商的东西可以从多个竞争供应商处获得,但存在显着差异和潜在的互操作性问题。单供应商网络让你受制于该供应商(无论它多么仁慈)。公共领域设计意味着即使你找不到人卖给你,你或任何其他人都可以购买零件并自己制造一个。研究原型只是那样;它们通常既不适合外部用户,也无法提供给他们。
如何连接这种网络?性能最高且现在最常见的是 PCI 总线接口卡。还有 EISA、VESA 局部总线(VL 总线)和 ISA 总线卡。ISA 最早出现,并且仍然常用于低性能卡。EISA 仍然作为许多 PCI 机器中的第二条总线而存在,因此有一些卡。如今,你不太可能看到 VL 的东西(尽管 http://www.vesa.org/ 会不同意)。
当然,任何无需打开 PC 机箱即可使用的接口都具有一定的吸引力。IrDA 和 USB 接口正变得越来越普遍。标准并行端口 (SPP) 曾经是你打印机所连接的端口,但最近它作为 ISA 总线的外部扩展得到了广泛应用;IEEE 1284 标准增强了这种新功能,该标准规定了 EPP 和 ECP 的改进。还有旧的、可靠的、缓慢的 RS232 串行端口。我不知道有人使用 VGA 视频连接器、键盘、鼠标或游戏端口连接机器……差不多就是这样了。
总线是一根电线、一组电线或光纤。集线器是一个小盒子,它知道如何连接插入其中的不同电线/光纤;交换集线器允许多个连接同时主动传输数据。
以下是如何使用这些数字。假设不包括网络连接,购买一台 PC 用作集群中的节点需要花费 2,000 美元。添加快速以太网使每个节点的成本增加到约 2,400 美元;添加 Myrinet 则使成本增加到约 3,800 美元。如果你有大约 20,000 美元的预算,这意味着你可以拥有 8 台通过快速以太网连接的机器或 5 台通过 Myrinet 连接的机器。拥有多个网络也非常合理;例如,20,000 美元可以购买 8 台通过快速以太网和 TTL_PAPERS 连接的机器。选择最有可能产生运行你的应用程序最快的集群的网络或网络组合。
当你读到这篇文章时,这些数字将会是错误的……见鬼,它们可能已经错了。可能还会有批量折扣、特别优惠等。尽管如此,这里引用的价格不太可能错到会引导你做出完全不合适的选择。不需要博士学位(虽然我确实有一个 ;-))就能看出,昂贵的网络只有在你的应用程序需要它们的特殊属性或者被集群的 PC 相对昂贵时才有意义。
现在你已经了解了免责声明,继续往下看……
ARCNET 是一种局域网,主要用于嵌入式实时控制系统。与以太网类似,该网络在物理上组织为总线上的抽头或一个或多个集线器,但是,与以太网不同,它使用基于令牌的协议,在逻辑上将网络构建为环。数据包头很小(3 或 4 个字节),消息可以携带少至单个字节的数据。因此,ARCNET 比以太网产生更一致的性能,具有有界延迟等。不幸的是,它比以太网慢且不太流行,因此更昂贵。更多信息可从 ARCNET 贸易协会 http://www.arcnet.com/ 获取。
除非你在过去几年里一直处于昏迷状态,否则你可能已经听说了很多关于 ATM(异步传输模式)是未来的……好吧,有点像。ATM 比 HiPPI 便宜,比快速以太网更快,并且可以在电话公司关心的非常远的距离上使用。ATM 网络协议还旨在提供更低开销的软件接口,并更有效地管理小消息和实时通信(例如,数字音频和视频)。它也是 Linux 当前支持的最高带宽网络之一。坏消息是 ATM 并不便宜,并且供应商之间仍然存在一些兼容性问题。Linux ATM 开发的概述可在 http://lrcwww.epfl.ch/linux-atm/ 上找到。
CAPERS(并行执行和快速同步的电缆适配器)是 PAPERS 项目 http://garage.ecn.purdue.edu/~papers/ 的衍生项目,该项目位于普渡大学电气与计算机工程学院。本质上,它定义了一个软件协议,用于使用普通的“LapLink”SPP 到 SPP 电缆为两台 Linux PC 实现 PAPERS 库。这个想法不具有可扩展性,但你无法比它更划算。与 TTL_PAPERS 一样,为了提高系统安全性,建议但不强制使用次要内核补丁:http://garage.ecn.purdue.edu/~papers/giveioperm.html。
多年来,10 Mb/s 以太网一直是标准网络技术。好的以太网接口卡可以以远低于 50 美元的价格购买,并且相当多的 PC 现在都在主板上内置了以太网控制器。对于轻度使用的网络,以太网连接可以组织成没有集线器的多抽头总线;这种配置可以为多达 200 台机器提供服务,成本极低,但不适合并行处理。添加非交换集线器实际上无助于提高性能。但是,可以为同时连接提供全带宽的交换集线器每个端口仅需约 100 美元。Linux 支持惊人的以太网接口范围,但重要的是要记住,接口硬件的变化可能会导致显着的性能差异。请参阅硬件兼容性 HOWTO,了解有关哪些受支持以及它们的工作效果的评论;另请参阅 http://cesdis1.gsfc.nasa.gov/linux/drivers/。
NASA CESDIS 的 Beowulf 项目 http://cesdis.gsfc.nasa.gov/linux/beowulf/beowulf.html 中完成的 16 台机器 Linux 集群工作提供了一种有趣的性能改进方法。在那里,许多以太网卡驱动程序的作者 Donald Becker 开发了对跨多个相互镜像的以太网网络(即共享相同的网络地址)进行负载共享的支持。这种负载共享内置于标准 Linux 发行版中,并且在套接字操作级别以下不可见地完成。由于集线器成本很高,因此将每台机器连接到两个或多个无集线器或非交换集线器以太网网络可能是一种非常经济高效的性能改进方式。事实上,在网络性能瓶颈是一台机器的情况下,使用镜像网络进行负载共享比使用单个交换集线器网络效果更好。
虽然实际上有许多不同的技术自称为“快速以太网”,但该术语最常指的是基于集线器的 100 Mb/s 以太网,它在某种程度上与较旧的“10 BaseT”10 Mb/s 设备和电缆兼容。正如可能预期的那样,任何称为以太网的东西通常都以批量市场定价,并且这些接口通常只是 155 Mb/s ATM 卡价格的一小部分。问题在于,让一群机器共享单个 100 Mb/s“总线”(使用非交换集线器)的带宽所产生的性能平均而言甚至可能不如使用交换集线器的 10 Mb/s 以太网,后者可以为每台机器的连接提供完整的 10 Mb/s。
可以同时为每台机器提供 100 Mb/s 的交换集线器很昂贵,但价格每天都在下降,并且这些交换机确实比非交换集线器产生更高的总网络带宽。使 ATM 交换机如此昂贵的原因是它们必须为每个(相对较短的)ATM 信元进行交换;一些快速以太网交换机利用预期较低的交换频率,使用可能具有通过交换机的低延迟的技术,但需要多个毫秒才能更改交换路径……如果你的路由模式频繁更改,请避免使用这些交换机。有关各种卡和驱动程序的信息,请参阅 http://cesdis1.gsfc.nasa.gov/linux/drivers/。
另请注意,如以太网所述,NASA 的 Beowulf 项目 http://cesdis.gsfc.nasa.gov/linux/beowulf/beowulf.html 一直在开发支持,通过跨多个快速以太网进行负载共享来提供改进的性能。
我不确定千兆以太网 http://www.gigabit-ethernet.org/ 是否有充分的技术理由被称为以太网……但这个名称确实准确地反映了这样一个事实,即这是一种旨在成为廉价、大众市场、计算机网络技术的网络,并具有对 IP 的原生支持。但是,当前的价格反映了构建 Gb/s 硬件仍然是一件棘手的事情。
与其他以太网技术不同,千兆以太网提供了一定程度的流量控制,这应该使其成为更可靠的网络。FDR 或全双工中继器,只是复用线路,使用缓冲和本地流量控制来提高性能。大多数交换集线器都作为现有千兆位交换矩阵的新接口模块而构建。Switch/FDR 产品已由或已宣布由至少 http://www.acacianet.com/、http://www.baynetworks.com/、http://www.cabletron.com/、http://www.networks.digital.com/、http://www.extremenetworks.com/、http://www.foundrynet.com/、http://www.gigalabs.com/、http://www.packetengines.com/、http://www.plaintree.com/、http://www.prominet.com/、http://www.sun.com/ 和 http://www.xlnt.com/ 提供。
Packet Engines "Yellowfin" G-NIC http://www.packetengines.com/ 有一个 Linux 驱动程序 http://cesdis.gsfc.nasa.gov/linux/drivers/yellowfin.html。在 Linux 下的早期测试中,带宽比最好的 100 Mb/s 快速以太网高约 2.5 倍;对于千兆位网络,仔细调整 PCI 总线的使用是关键因素。毫无疑问,驱动程序改进以及其他 NIC 的 Linux 驱动程序将会随之而来。
FC(光纤通道)的目标是提供高性能块 I/O(一个 FC 帧携带 2,048 字节的数据有效负载),特别是用于共享磁盘和其他存储设备,这些设备可以直接连接到 FC,而不是通过计算机连接。在带宽方面,FC 的规格相对较快,运行速度在 133 到 1,062 Mb/s 之间。如果 FC 成为流行的高端 SCSI 替代品,它可能会很快成为一种廉价技术;目前,它并不便宜,并且 Linux 不支持它。光纤通道协会在 http://www.amdahl.com/ext/CARP/FCA/FCA.html 上维护了一个很好的 FC 参考资料集合
火线,http://www.firewire.org/,IEEE 1394-1995 标准,注定要成为低成本高速数字网络,用于消费电子产品。展示应用是将 DV 数字摄像机连接到计算机,但火线旨在用于从 SCSI 替代品到互连家庭影院组件的应用。它允许在任何拓扑中使用总线和桥接器连接多达 64K 个设备,而不会创建环路,并在添加或移除组件时自动检测配置。支持短(四字节“四字节组”)低延迟消息以及类似 ATM 的等时传输(用于保持多媒体消息同步)。Adaptec 拥有火线产品,允许将多达 63 个设备连接到单个 PCI 接口卡,并且在 http://www.adaptec.com/serialio/ 上也提供了良好的通用火线信息。
虽然火线不会成为可用的最高带宽网络,但消费级市场(应该会推动价格非常低)和低延迟支持可能会使它成为未来一两年内最好的 Linux PC 集群消息传递网络技术之一。
HiPPI(高性能并行接口)最初旨在为超级计算机和另一台机器(超级计算机、帧缓冲区、磁盘阵列等)之间传输海量数据集提供非常高的带宽,并且已成为超级计算机的主要标准。虽然这是一个矛盾修辞法,但 串行 HiPPI 也变得流行起来,通常使用光纤电缆而不是 32 位宽的标准(并行)HiPPI 电缆。在过去几年中,HiPPI 交叉开关变得很常见,价格也大幅下降;不幸的是,串行 HiPPI 仍然很昂贵,而这通常是 PCI 总线接口卡支持的。更糟糕的是,Linux 尚不支持 HiPPI。CERN 在 http://www.cern.ch/HSI/hippi/ 上维护了一个很好的 HiPPI 概述;他们还在 http://www.cern.ch/HSI/hippi/procintf/manufact.htm 上维护了一个相当长的 HiPPI 供应商列表。
IrDA(红外数据协会,http://www.irda.org/)是许多笔记本电脑侧面的小型红外设备。使用此接口本质上很难连接两台以上的机器,因此不太可能用于集群。Donald Becker 对 IrDA 做了一些初步工作。
Myrinet http://www.myri.com/ 是一种局域网 (LAN),旨在也用作“系统区域网络” (SAN),即机柜内充满机器的网络,这些机器连接为并行系统。LAN 和 SAN 版本使用不同的物理介质,并且具有略微不同的特性;通常,SAN 版本将在集群内使用。
Myrinet 在结构上相当传统,但以实施得特别好而闻名。据说 Linux 的驱动程序性能非常好,尽管据报道,对于主机计算机的不同 PCI 总线实现,性能变化惊人地大。
目前,Myrinet 显然是不受“预算挑战”过于严重的集群组的首选网络。如果你对 Linux PC 的看法是具有至少 256 MB RAM 和 SCSI RAID 的高端奔腾 Pro 或奔腾 II,那么 Myrinet 的成本是非常合理的。但是,使用更普通的 PC 配置,你可能会发现你的选择是在 Myrinet 连接的 N 台机器与多个快速以太网和 TTL_PAPERS 连接的 2N 台机器之间。这实际上取决于你的预算是多少以及你最关心哪种类型的计算。
卡尔斯鲁厄大学信息学系的 ParaStation 项目 http://wwwipd.ira.uka.de/parastation 正在构建一个 PVM 兼容的自定义低延迟网络。他们首先使用自定义 EISA 卡接口和运行 BSD UNIX 的 PC 构建了一个双处理器 ParaPC 原型,然后使用 DEC Alpha 构建了更大的集群。自 1997 年 1 月以来,ParaStation 已可用于 Linux。PCI 卡正在与一家名为 Hitex 的公司合作制造(请参阅 http://www.hitex.com:80/parastation/)。Parastation 硬件实现了快速、可靠的消息传输和简单的屏障同步。
仅需一根“LapLink”电缆的成本,PLIP(并行线路接口协议)就允许两台 Linux 机器通过标准并行端口使用基于标准套接字的软件进行通信。就带宽、延迟和可扩展性而言,这不是一种非常重要的网络技术;但是,接近零成本和软件兼容性很有用。该驱动程序是标准 Linux 内核发行版的一部分。
SCI(可扩展相干互连,ANSI/IEEE 1596-1992)的目标本质上是提供一种高性能机制,可以支持跨大量机器的相干共享内存访问,以及各种类型的块消息传输。可以相当肯定地说,与大多数其他网络技术相比,SCI 的设计带宽和延迟都“非常棒”。问题在于 SCI 作为廉价的生产单元并未广泛使用,并且没有任何 Linux 支持。
SCI 主要用于各种专有设计,用于逻辑共享的物理分布式内存机器,例如 HP/Convex Exemplar SPP 和 Sequent NUMA-Q 2000(请参阅 http://www.sequent.com/)。但是,SCI 可以作为 PCI 接口卡和 4 路交换机(通过级联四个 4 路交换机最多可以连接 16 台机器)从 Dolphin http://www.dolphinics.com/ 获得,作为他们的 CluStar 产品线。CERN 在 http://www.cern.ch/HSI/sci/sci.html 上维护了一组很好的 SCI 概述链接。
SCSI(小型计算机系统互连)本质上是一种 I/O 总线,用于磁盘驱动器、CD ROM、图像扫描仪等。有三个单独的标准 SCSI-1、SCSI-2 和 SCSI-3;快速和超高速;以及 8 位、16 位或 32 位的数据路径宽度(SCSI-3 中也提到了与火线兼容性)。这一切都非常令人困惑,但我们都知道好的 SCSI 比 EIDE 快一些,并且可以更有效地处理更多设备。
许多人没有意识到的是,两台计算机共享单个 SCSI 总线相当简单。这种类型的配置对于在机器之间共享磁盘驱动器和实现 故障转移 非常有用 - 当另一台机器发生故障时,让一台机器接管数据库请求。目前,这是 Microsoft 的 PC 集群产品 WolfPack 支持的唯一机制。但是,无法扩展到更大的系统使得共享 SCSI 对于一般的并行处理来说毫无意义。
ServerNet 是来自 Tandem http://www.tandem.com 的高性能网络硬件。特别是在在线事务处理 (OLTP) 领域,Tandem 以其高可靠性系统的领先生产商而闻名,因此他们的网络不仅声称具有高性能,而且还具有“高数据完整性和可靠性”也就不足为奇了。ServerNet 的另一个有趣方面是,它声称能够将数据从任何设备直接传输到任何设备;不仅在处理器之间,而且还在磁盘驱动器等之间,采用类似于第 3.5 节中描述的 MPI 远程内存访问机制的单边样式。关于 ServerNet 的最后一个评论:虽然只有一个供应商,但该供应商足够强大,有可能将 ServerNet 确立为主要标准……Tandem 归 Compaq 所有。
普林斯顿大学计算机科学系的 SHRIMP 项目 http://www.CS.Princeton.EDU/shrimp/ 正在构建一台并行计算机,使用运行 Linux 的 PC 作为处理单元。第一个 SHRIMP(可扩展、高性能、真正廉价的多处理器)是一个简单的双处理器原型,在自定义 EISA 卡接口上使用双端口 RAM。现在有一个原型,它将使用自定义接口卡连接到“集线器”来扩展到更大的配置,“集线器”本质上与 Intel Paragon 中使用的网格路由网络相同(请参阅 http://www.ssd.intel.com/paragon.html)。在开发低开销“虚拟内存映射通信”硬件和支持软件方面投入了大量精力。
虽然 SLIP(串行线路接口协议)牢牢地位于性能频谱的低端,但 SLIP(或 CSLIP 或 PPP)允许两台机器通过普通的 RS232 串行端口执行套接字通信。RS232 端口可以使用零调制解调器 RS232 串行电缆连接,甚至可以通过调制解调器通过拨号连接。在任何情况下,延迟都很高,带宽也很低,因此只有在没有其他替代方案可用时才应使用 SLIP。然而,值得注意的是,大多数 PC 都有两个 RS232 端口,因此可以通过简单地将机器连接为线性阵列或环来联网一组机器。甚至还有称为 EQL 的负载共享软件。
普渡大学电气与计算机工程学院的 PAPERS(用于并行执行和快速同步的普渡适配器)项目 http://garage.ecn.purdue.edu/~papers/ 正在构建可扩展、低延迟、聚合函数通信硬件和软件,这些硬件和软件允许使用未经修改的 PC/工作站作为节点构建并行超级计算机。
已经构建了十几种不同类型的 PAPERS 硬件,这些硬件通过 SPP(标准并行端口)连接到 PC/工作站,大致遵循两条开发路线。称为“PAPERS”的版本以更高的性能为目标,使用任何适当的技术;当前的工作使用 FPGA,并且还在开发高带宽 PCI 总线接口设计。相比之下,称为“TTL_PAPERS”的版本旨在在普渡大学以外的地方轻松复制,并且是非常简单的公共领域设计,可以使用普通的 TTL 逻辑构建。其中一种设计已商业化,http://chelsea.ios.com:80/~hgdietz/sbm4.html。
与其他大学的自定义硬件设计不同,TTL_PAPERS 集群已在美国到韩国的许多大学组装。带宽受到 SPP 连接的严重限制,但 PAPERS 实现了非常低延迟的聚合函数通信;即使是最快的面向消息的系统也无法在这些聚合函数上提供可比的性能。因此,PAPERS 特别适合同步视频墙的显示(将在即将发布的视频墙 HOWTO 中进一步讨论)、调度对高带宽网络的访问、评估遗传搜索中的全局适应度等。尽管 PAPERS 集群已使用 IBM PowerPC AIX、DEC Alpha OSF/1 和 HP PA-RISC HP-UX 机器构建,但基于 Linux 的 PC 是最受支持的平台。
使用 TTL_PAPERS AFAPI 的用户程序可以直接访问 Linux 下的 SPP 硬件端口寄存器,而无需每次访问都进行操作系统调用。为此,AFAPI 首先使用 iopl()
或 ioperm()
获取端口权限。这些调用的问题在于它们都要求用户程序具有特权,从而产生潜在的安全漏洞。解决方案是一个可选的内核补丁,http://garage.ecn.purdue.edu/~papers/giveioperm.html,它允许特权进程控制任何进程的端口权限。
USB(通用串行总线,http://www.usb.org/)是一种热插拔的、传统以太网速度的总线,最多可连接 127 个外围设备,范围从键盘到视频会议摄像头。目前尚不清楚多台计算机如何使用 USB 相互连接。无论如何,USB 端口正迅速成为 PC 主板上的标准配置,就像 RS232 和 SPP 一样,因此如果您购买的下一台 PC 背面潜伏着一两个 USB 端口,请不要感到惊讶。Linux 驱动程序的开发在 http://peloncho.fis.ucm.es/~inaky/USB.html 中进行了讨论。
在某些方面,USB 几乎可以看作是 FireWire 的低性能、零成本版本,您今天就可以购买到。
WAPERS(用于并行执行和快速同步的线与适配器)是 PAPERS 项目(http://garage.ecn.purdue.edu/~papers/)在普渡大学电气与计算机工程学院的一个衍生项目。如果正确实现,SPP 具有四个集电极开路输出位,可以跨机器连接在一起以实现 4 位宽的线与。这种线与在电气上非常敏感,可以以这种方式连接的最大机器数量关键取决于端口的模拟特性(最大灌电流和上拉电阻值);通常,最多可以通过 WAPERS 连接 7 或 8 台机器。虽然成本和延迟非常低,但带宽也很低;WAPERS 更适合作为聚合操作的辅助网络,而不是集群中的唯一网络。与 TTL_PAPERS 一样,为了提高系统安全性,建议但不强制使用一个小的内核补丁:http://garage.ecn.purdue.edu/~papers/giveioperm.html。
在继续讨论并行应用程序的软件支持之前,首先简要介绍一下网络硬件的底层软件接口基础知识是很有用的。实际上只有三种基本选择:套接字、设备驱动程序和用户级库。
到目前为止,最常见的底层网络接口是套接字接口。套接字已经成为 Unix 的一部分十多年了,大多数标准网络硬件都设计为至少支持两种类型的套接字协议:UDP 和 TCP。这两种类型的套接字都允许您将任意大小的数据块从一台机器发送到另一台机器,但它们之间存在几个重要的区别。通常,两者都会产生大约 1,000 微秒的最小延迟,尽管性能可能会因网络流量而变得更差。
这些套接字类型是大多数可移植的、更高级别的并行处理软件的基本网络软件接口;例如,PVM 使用 UDP 和 TCP 的组合,因此了解它们之间的区别将有助于您调整性能。为了获得更好的性能,您还可以在程序中直接使用这些机制。以下只是 UDP 和 TCP 的简单概述;有关详细信息,请参阅手册页和优秀的网络编程书籍。
UDP 是用户数据报协议,但您更容易记住 UDP 的属性,即不可靠数据报处理。换句话说,UDP 允许将每个块作为单个消息发送,但消息可能会在传输过程中丢失。事实上,根据网络流量,UDP 消息可能会丢失、多次到达,或者以与发送顺序不同的顺序到达。UDP 消息的发送者不会自动收到确认,因此检测和补偿这些问题取决于用户编写的代码。幸运的是,UDP 确实确保如果消息到达,则消息内容是完整的(即,您永远不会只收到 UDP 消息的一部分)。
UDP 的优点在于它往往是最快的套接字协议。此外,UDP 是“无连接的”,这意味着每条消息本质上都独立于所有其他消息。一个很好的类比是,每条消息都像一封要邮寄的信件;您可能会向同一地址发送多封信件,但每封信件都独立于其他信件,并且您可以向多少人发送信件没有限制。
与 UDP 不同,TCP 是一种可靠的、基于连接的协议。发送的每个块不被视为消息,而是被视为通过发送者和接收者之间连接传输的连续字节流中的数据块。这与 UDP 消息传递非常不同,因为每个块只是字节流的一部分,用户代码需要弄清楚如何从字节流中提取每个块;没有标记分隔消息。此外,连接对于网络问题更加脆弱,并且每个进程可以同时存在的连接数量有限。由于它是可靠的,TCP 通常意味着比 UDP 显著更高的开销。
然而,关于 TCP 也有一些令人愉快的惊喜。其中之一是,如果通过连接发送多条消息,TCP 能够将它们打包到一个缓冲区中,以更好地匹配网络硬件数据包大小,从而可能为短消息或大小不规则的消息组产生比 UDP 更好的性能。另一个好处是,使用机器之间可靠的直接物理链路构建的网络可以轻松有效地模拟 TCP 连接。例如,ParaStation 的“套接字库”接口软件就是这样做的,它使用用户级调用提供 TCP 语义,这些用户级调用与标准 TCP 操作系统调用的不同之处仅在于每个函数名称都添加了前缀 PSS
。
当涉及到实际将数据推送到网络或从网络拉取数据时,标准的 Unix 软件接口是 Unix 内核的一部分,称为设备驱动程序。UDP 和 TCP 不仅仅传输数据,它们还意味着相当多的套接字管理开销。例如,必须有一些东西来管理多个 TCP 连接可以共享单个物理网络接口的事实。相比之下,专用网络接口的设备驱动程序只需要实现一些简单的数据传输功能。然后,用户程序可以使用 open()
来标识正确的设备,然后使用诸如 read()
和 write()
之类的系统调用在打开的“文件”上调用这些设备驱动程序功能。因此,每个这样的操作都可以在系统调用的开销下传输数据块,这可能快至数十微秒。
为 Linux 编写设备驱动程序并不难……如果您精确知道设备硬件的工作原理。如果您不确定它是如何工作的,请不要猜测。调试设备驱动程序并不有趣,并且错误可能会烧毁硬件。但是,如果这没有吓退您,则可能可以编写设备驱动程序,例如,使用专用以太网卡作为哑但快速的机器到机器直接连接,而无需通常的以太网协议开销。事实上,这与早期的一些 Intel 超级计算机所做的非常相似……请查看设备驱动程序 HOWTO 以获取更多信息。
如果您参加过操作系统课程,用户级访问硬件设备寄存器正是您被教导永远不要做的事情,因为操作系统的主要目的之一是控制设备访问。但是,操作系统调用至少需要数十微秒的开销。对于像 TTL_PAPERS 这样的自定义网络硬件,它可以在短短 3 微秒内执行基本网络操作,这种操作系统调用开销是无法容忍的。避免这种开销的唯一方法是拥有用户级代码 - 用户级库 - 直接访问硬件设备寄存器。因此,问题变成了用户级库如何直接访问硬件,又不损害操作系统对设备访问权限的控制。
在典型的系统上,用户级库直接访问硬件设备寄存器的唯一方法是
mmap()
调用(在第 2.6 节中首次提到)可以用于映射一个特殊文件,该文件表示 I/O 设备的物理内存页地址。或者,编写设备驱动程序来执行此功能相对简单。此外,此设备驱动程序可以通过仅映射包含所需特定设备寄存器的页面来控制访问,从而维护操作系统访问控制。*((char *) 0x1234) = 5;
会将字节值 5 存储到内存位置 1234(十六进制)。幸运的是,对于 Intel 386(和兼容处理器)的 Linux 来说,情况甚至更好
ioperm()
操作系统调用,获取访问与设备寄存器对应的精确 I/O 端口地址的权限。或者,可以使用独立的特权用户进程(即“元操作系统”)使用 giveioperm() 操作系统调用补丁 来管理权限。
第二种解决方案更可取,因为通常多个 I/O 设备的寄存器位于单个页面内,在这种情况下,第一种技术无法防止访问恰好与预期寄存器位于同一页面中的其他设备寄存器。当然,缺点是 386 端口 I/O 指令无法在 C 中编码 - 相反,您需要使用一些汇编代码。用于字节值端口输入的 GCC 包装(可在 C 程序中使用)内联汇编代码函数是
extern inline unsigned char inb(unsigned short port) { unsigned char _v; __asm__ __volatile__ ("inb %w1,%b0" :"=a" (_v) :"d" (port), "0" (0)); return _v; }
类似地,用于字节端口输出的 GCC 包装代码是
extern inline void outb(unsigned char value, unsigned short port) { __asm__ __volatile__ ("outb %b0,%w1" :/* no outputs */ :"a" (value), "d" (port)); }
PVM(并行虚拟机)是一个免费提供的、可移植的消息传递库,通常在套接字之上实现。它已被明确确立为消息传递集群并行计算的事实标准。
PVM 支持单处理器和 SMP Linux 机器,以及通过支持套接字的网络(例如,SLIP、PLIP、以太网、ATM)连接的 Linux 机器集群。事实上,PVM 甚至可以在各种不同类型的处理器、配置和物理网络(异构集群)的机器组之间工作 - 甚至可以将通过 Internet 连接的机器视为并行集群。PVM 还提供跨集群的并行作业控制工具。最重要的是,PVM 长期以来都是免费提供的(目前可从 http://www.epm.ornl.gov/pvm/pvm_home.html 获取),这导致许多编程语言编译器、应用程序库、编程和调试工具等都将其用作“可移植消息传递目标库”。还有一个网络新闻组,comp.parallel.pvm。
然而,重要的是要注意,PVM 消息传递调用通常会给标准套接字操作增加显著的开销,而标准套接字操作已经具有高延迟。此外,消息处理调用本身并不构成特别“友好”的编程模型。
使用第 1.3 节中首次描述的相同 Pi 计算示例,使用 C 和 PVM 库调用的版本是
#include <stdlib.h> #include <stdio.h> #include <pvm3.h> #define NPROC 4 main(int argc, char **argv) { register double lsum, width; double sum; register int intervals, i; int mytid, iproc, msgtag = 4; int tids[NPROC]; /* array of task ids */ /* enroll in pvm */ mytid = pvm_mytid(); /* Join a group and, if I am the first instance, iproc=0, spawn more copies of myself */ iproc = pvm_joingroup("pi"); if (iproc == 0) { tids[0] = pvm_mytid(); pvm_spawn("pvm_pi", &argv[1], 0, NULL, NPROC-1, &tids[1]); } /* make sure all processes are here */ pvm_barrier("pi", NPROC); /* get the number of intervals */ intervals = atoi(argv[1]); width = 1.0 / intervals; lsum = 0.0; for (i = iproc; i<intervals; i+=NPROC) { register double x = (i + 0.5) * width; lsum += 4.0 / (1.0 + x * x); } /* sum across the local results & scale by width */ sum = lsum * width; pvm_reduce(PvmSum, &sum, 1, PVM_DOUBLE, msgtag, "pi", 0); /* have only the console PE print the result */ if (iproc == 0) { printf("Estimation of pi is %f\n", sum); } /* Check program finished, leave group, exit pvm */ pvm_barrier("pi", NPROC); pvm_lvgroup("pi"); pvm_exit(); return(0); }
虽然 PVM 是事实上的标准消息传递库,但 MPI(消息传递接口)是相对较新的官方标准。MPI 标准的主页是 http://www.mcs.anl.gov:80/mpi/,新闻组是 comp.parallel.mpi。
然而,在讨论 MPI 之前,我觉得有必要稍微谈谈过去几年一直在进行的 PVM 与 MPI 的宗教战争。我实际上不站在任何一边。这是我对差异的相对公正的总结尝试
简而言之,PVM 有一个,而 MPI 没有指定如何/是否实现一个。因此,启动 PVM 程序执行之类的操作在任何地方都是相同的,而对于 MPI,则取决于正在使用的实现。
PVM 在工作站周期回收利用的世界中成长起来,因此直接管理机器和操作系统的异构组合。相比之下,MPI 在很大程度上假设目标是 MPP(大规模并行处理器)或几乎相同的专用工作站集群。
PVM 证明了 MPI 2.0 所不具备的用途统一性。新的 MPI 2.0 标准包含许多超出基本消息传递模型的功能 - 例如 RMA(远程内存访问)和并行文件 I/O。这些东西有用吗?当然有用……但学习 MPI 2.0 很像学习一门全新的编程语言。
MPI 是在 PVM 之后设计的,显然从中吸取了教训。MPI 提供更简单、更高效的缓冲区处理和更高级别的抽象,允许在消息中传输用户定义的数据结构。
据我统计,设计为使用 PVM 的东西仍然明显多于设计为使用 MPI 的东西;但是,将它们移植到 MPI 很容易,并且 MPI 以广泛支持的正式标准为后盾这一事实意味着,对于许多机构来说,使用 MPI 是一项政策问题。
结论?嗯,至少有三个独立开发的、免费提供的 MPI 版本可以在 Linux 系统集群上运行(其中一个是我写的)
无论使用这些 MPI 实现中的哪一个(或其他),执行最常见的通信类型都相当简单。
然而,MPI 2.0 融入了几种通信范例,这些范例从根本上来说差异很大,以至于使用其中一种范例的程序员甚至可能无法将另一种编码风格识别为 MPI。因此,与其给出一个示例程序,不如给出 MPI 支持的每种根本不同的通信范例的示例,这将很有用。所有三个程序都实现了相同的基本算法(来自第 1.3 节),该算法在本 HOWTO 中用于计算 Pi 值。
第一个 MPI 程序使用基本的 MPI 消息传递调用,让每个处理器将其部分和发送到处理器 0,处理器 0 对结果进行求和并打印
#include <stdlib.h> #include <stdio.h> #include <mpi.h> main(int argc, char **argv) { register double width; double sum, lsum; register int intervals, i; int nproc, iproc; MPI_Status status; if (MPI_Init(&argc, &argv) != MPI_SUCCESS) exit(1); MPI_Comm_size(MPI_COMM_WORLD, &nproc); MPI_Comm_rank(MPI_COMM_WORLD, &iproc); intervals = atoi(argv[1]); width = 1.0 / intervals; lsum = 0; for (i=iproc; i<intervals; i+=nproc) { register double x = (i + 0.5) * width; lsum += 4.0 / (1.0 + x * x); } lsum *= width; if (iproc != 0) { MPI_Send(&lbuf, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); } else { sum = lsum; for (i=1; i<nproc; ++i) { MPI_Recv(&lbuf, 1, MPI_DOUBLE, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); sum += lsum; } printf("Estimation of pi is %f\n", sum); } MPI_Finalize(); return(0); }
第二个 MPI 版本使用集体通信(对于这个特定的应用程序来说,这显然是最合适的)
#include <stdlib.h> #include <stdio.h> #include <mpi.h> main(int argc, char **argv) { register double width; double sum, lsum; register int intervals, i; int nproc, iproc; if (MPI_Init(&argc, &argv) != MPI_SUCCESS) exit(1); MPI_Comm_size(MPI_COMM_WORLD, &nproc); MPI_Comm_rank(MPI_COMM_WORLD, &iproc); intervals = atoi(argv[1]); width = 1.0 / intervals; lsum = 0; for (i=iproc; i<intervals; i+=nproc) { register double x = (i + 0.5) * width; lsum += 4.0 / (1.0 + x * x); } lsum *= width; MPI_Reduce(&lsum, &sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); if (iproc == 0) { printf("Estimation of pi is %f\n", sum); } MPI_Finalize(); return(0); }
第三个 MPI 版本使用 MPI 2.0 RMA 机制,让每个处理器将其本地 lsum
添加到处理器 0 上的 sum
中
#include <stdlib.h> #include <stdio.h> #include <mpi.h> main(int argc, char **argv) { register double width; double sum = 0, lsum; register int intervals, i; int nproc, iproc; MPI_Win sum_win; if (MPI_Init(&argc, &argv) != MPI_SUCCESS) exit(1); MPI_Comm_size(MPI_COMM_WORLD, &nproc); MPI_Comm_rank(MPI_COMM_WORLD, &iproc); MPI_Win_create(&sum, sizeof(sum), sizeof(sum), 0, MPI_COMM_WORLD, &sum_win); MPI_Win_fence(0, sum_win); intervals = atoi(argv[1]); width = 1.0 / intervals; lsum = 0; for (i=iproc; i<intervals; i+=nproc) { register double x = (i + 0.5) * width; lsum += 4.0 / (1.0 + x * x); } lsum *= width; MPI_Accumulate(&lsum, 1, MPI_DOUBLE, 0, 0, 1, MPI_DOUBLE, MPI_SUM, sum_win); MPI_Win_fence(0, sum_win); if (iproc == 0) { printf("Estimation of pi is %f\n", sum); } MPI_Finalize(); return(0); }
值得注意的是,MPI 2.0 RMA 机制非常巧妙地克服了各种处理器上相应数据结构驻留在不同内存位置的任何潜在问题。这是通过引用一个“窗口”来完成的,该“窗口”暗示了基地址、防止越界访问的保护,甚至地址缩放。高效的实现得益于 RMA 处理可能会延迟到下一个 MPI_Win_fence
。总而言之,RMA 机制可能是分布式共享内存和消息传递之间的一种奇怪的交叉,但它是一个非常干净的接口,可能会产生非常高效的通信。
与 PVM、MPI 等不同,AFAPI(聚合函数应用程序编程接口)的诞生并非试图构建一个分层在现有网络硬件和软件之上的可移植抽象接口。相反,AFAPI 最初是 PAPERS(普渡大学用于并行执行和快速同步的适配器;参见 http://garage.ecn.purdue.edu/~papers/)的非常特定于硬件的底层支持库。
PAPERS 在第 3.2 节中简要讨论过;它是一个公共领域设计的自定义聚合函数网络,可提供低至几微秒的延迟。然而,关于 PAPERS 的重要之处在于,它的开发是为了构建一台超级计算机,与现有超级计算机相比,它将成为编译器技术的更好目标。这与大多数 Linux 集群工作和 PVM/MPI 在质上不同,后者通常专注于尝试使用标准网络来处理相对较少的足够粗粒度的并行应用程序。Linux PC 被用作 PAPERS 系统的组件仅仅是以最具成本效益的方式实现原型的一种人为产物。
在十几个不同的原型实现中需要一个通用的底层软件接口,这使得 PAPERS 库成为 AFAPI 的标准。然而,AFAPI 使用的模型本质上更简单,更适合并行化编译器或为 SIMD 架构编写的代码的更细粒度交互。模型的简单性不仅使 PAPERS 硬件易于构建,而且还为各种其他硬件系统(例如 SMP)提供了令人惊讶的高效的 AFAPI 端口。
AFAPI 目前在通过 TTL_PAPERS、CAPERS 或 WAPERS 连接的 Linux 集群上运行。它还在使用名为 SHMAPERS 的 System V 共享内存库的 SMP 系统上运行(无需操作系统调用甚至总线锁定指令,请参阅第 2.2 节)。一个在传统网络(例如,以太网)上使用 UDP 广播跨 Linux 集群运行的版本正在开发中。所有已发布的版本都可从 http://garage.ecn.purdue.edu/~papers/ 获得。所有版本的 AFAPI 都设计为从 C 或 C++ 调用。
以下示例程序是第 1.3 节中描述的 Pi 计算的 AFAPI 版本。
#include <stdlib.h> #include <stdio.h> #include "afapi.h" main(int argc, char **argv) { register double width, sum; register int intervals, i; if (p_init()) exit(1); intervals = atoi(argv[1]); width = 1.0 / intervals; sum = 0; for (i=IPROC; i<intervals; i+=NPROC) { register double x = (i + 0.5) * width; sum += 4.0 / (1.0 + x * x); } sum = p_reduceAdd64f(sum) * width; if (IPROC == CPROC) { printf("Estimation of pi is %f\n", sum); } p_exit(); return(0); }
除了 PVM、MPI 和 AFAPI 之外,以下库还提供了一些功能,这些功能可能在使用 Linux 系统集群进行并行计算时很有用。本文档对这些系统的处理较为简略,仅仅是因为与 PVM、MPI 和 AFAPI 不同,我几乎没有或根本没有在 Linux 集群上使用这些系统的直接经验。如果您发现这些库或其他库特别有用,请发送电子邮件至 hankd@engr.uky.edu 描述您的发现,我将考虑添加关于该库的扩展章节。
Condor 是一个分布式资源管理系统,可以管理大型异构工作站集群。其设计受到用户的需求的推动,这些用户希望将此类集群的未利用容量用于其长时间运行的计算密集型作业。Condor 在执行机器上保留了很大程度上来自原始机器的环境,即使原始机器和执行机器不共享公共文件系统和/或密码机制也是如此。由单个进程组成的 Condor 作业会自动进行检查点操作,并在工作站之间根据需要进行迁移,以确保最终完成。
Condor 可在 http://www.cs.wisc.edu/condor/ 获得。存在 Linux 端口;更多信息可在 http://www.cs.wisc.edu/condor/linux/linux.html 获得。联系 condor-admin@cs.wisc.edu 了解详情。
DFN-RPC(德国研究网络远程过程调用)工具的开发是为了在工作站和计算服务器或集群之间分发和并行化科学技术应用程序。该接口针对 Fortran 编写的应用程序进行了优化,但 DFN-RPC 也可在 C 环境中使用。它已被移植到 Linux。更多信息请访问 ftp://ftp.uni-stuttgart.de/pub/rus/dfn_rpc/README_dfnrpc.html。
DQS 3.0(分布式排队系统)不是一个严格意义上的库,而是一个在 Linux 下开发和测试的作业排队系统。它旨在允许将异构集群作为一个单一实体进行使用和管理。它可以从 http://www.scri.fsu.edu/~pasko/dqs.html 获得。
还有一个名为 CODINE 4.1.1(分布式网络环境中的计算)的商业版本。有关它的信息可从 http://www.genias.de/genias_welcome.html 获得。
由于集群可以以如此多种不同的方式构建和使用,因此有很多小组做出了有趣的贡献。以下是一些与集群相关的项目的参考资料,这些项目可能具有普遍意义。这包括 Linux 特定的和通用集群参考资料的混合。列表按字母顺序排列。
Beowulf 项目,http://cesdis1.gsfc.nasa.gov/beowulf/,专注于生产软件,用于使用基于商用 PC 级硬件、高带宽集群内部网络和 Linux 操作系统构建的现成集群工作站。
Thomas Sterling 一直是 Beowulf 背后的驱动力,并且仍然是 Linux 集群在科学计算中的雄辩且直言不讳的倡导者。事实上,许多小组现在将其集群称为“Beowulf 类”系统 - 即使该集群实际上与官方 Beowulf 设计并不那么相似。
Don Becker 在支持 Beowulf 项目的工作中,制作了 Linux 通用使用的许多网络驱动程序。其中许多驱动程序甚至已针对 BSD 进行了调整。Don 还负责许多 Linux 网络驱动程序,这些驱动程序允许跨多个并行连接进行负载共享,以在没有昂贵的交换式集线器的情况下实现更高的带宽。这种类型的负载共享是 Beowulf 集群最初的显着特征。
Linux/AP+ 项目,http://cap.anu.edu.au/cap/projects/linux/,不完全是关于 Linux 集群,而是专注于将 Linux 移植到 Fujitsu AP1000+ 并添加适当的并行处理增强功能。AP1000+ 是一种商用 SPARC 架构的并行计算机,它使用具有环形拓扑结构的自定义网络,带宽为 25 MB/s,延迟为 10 微秒……简而言之,它看起来很像 SPARC Linux 集群。
Locust 项目,http://www.ecsl.cs.sunysb.edu/~manish/locust/,正在构建一个分布式虚拟共享内存系统,该系统使用编译时信息来隐藏消息延迟并减少运行时网络流量。Pupa 是 Locust 的底层通信子系统,使用以太网在 FreeBSD 下连接 486 PC 实现。Linux 呢?
Midway,http://www.cs.cmu.edu/afs/cs.cmu.edu/project/midway/WWW/HomePage.html,是一个基于软件的 DSM(分布式共享内存)系统,与 TreadMarks 类似。好消息是它使用编译时辅助而不是相对较慢的页面错误机制,而且它是免费的。坏消息是它不在 Linux 集群上运行。
MOSIX 修改了 BSDI BSD/OS,以在联网的 PC 组之间提供动态负载均衡和抢占式进程迁移。这不仅对于并行处理来说是不错的东西,而且对于通常将集群用作可扩展 SMP 来说也是不错的。会有 Linux 版本吗?请访问 http://www.cs.huji.ac.il/mosix/ 了解更多信息。
伯克利 NOW(工作站网络)项目,http://now.cs.berkeley.edu/,引领了使用工作站网络进行并行计算的潮流。这里正在进行大量工作,所有这些工作都旨在“在未来几年内演示一个实用的 100 处理器系统”。唉,他们不使用 Linux。
使用 Linux 进行并行处理的 WWW 站点,http://aggregate.org/LDP/,是本 HOWTO 和许多相关文档的所在地,包括为期一天的教程的在线幻灯片。除了在 PAPERS 项目上的工作外,普渡大学电气与计算机工程学院通常一直是并行处理领域的领导者;建立该站点的目的是帮助其他人将 Linux PC 应用于并行处理。
自普渡大学的第一个 Linux PC 集群于 1994 年 2 月组装以来,普渡大学已经组装了许多 Linux PC 集群,包括几个带有视频墙的集群。尽管这些集群使用了 386、486 和 Pentium 系统(没有 Pentium Pro 系统),但 Intel 最近向普渡大学捐赠了一笔款项,这将使其能够构建多个大型 Pentium II 系统集群(单个集群计划有多达 165 台机器)。尽管这些集群都具有/将具有 PAPERS 网络,但大多数集群也具有传统网络。
1997 年 4 月 10 日至 11 日,艾姆斯实验室在爱荷华州得梅因市举办了奔腾 Pro 集群研讨会。来自本次研讨会的 WWW 站点,http://www.scl.ameslab.gov/workshops/PPCworkshop.html,包含从所有与会者那里收集的大量 PC 集群信息。
DSM(分布式共享内存)是一种技术,通过该技术,消息传递系统可以表现得像 SMP 一样。有很多这样的系统,其中大多数系统使用操作系统页面错误机制来触发消息传输。TreadMarks,http://www.cs.rice.edu/~willy/TreadMarks/overview.html,是此类系统中效率较高的系统之一,并且可以在 Linux 集群上运行。坏消息是“TreadMarks 正以少量成本分发给大学和非营利机构。” 有关该软件的更多信息,请联系 treadmarks@ece.rice.edu。
康奈尔大学的 U-Net(用户级网络接口架构)项目,http://www2.cs.cornell.edu/U-Net/Default.html,旨在通过虚拟化网络接口来提供低延迟和高带宽,以便应用程序可以在没有操作系统干预的情况下发送和接收消息。U-Net 在使用基于 DECchip DC21140 的快速以太网卡或 Fore Systems PCA-200(不是 PCA-200E)ATM 卡的 Linux PC 上运行。
威斯康星大学在集群相关方面确实有很多工作。威斯康星风洞 (Wisconsin Wind Tunnel, WWT) 项目,http://www.cs.wisc.edu/~wwt/,正在进行各种工作,旨在开发编译器和底层并行硬件之间的“标准”接口。还有威斯康星 COW (工作站集群, Cluster Of Workstations)、协作共享内存和 Tempest、Paradyn 并行性能工具等。遗憾的是,关于 Linux 的内容不多。
寄存器内单指令多数据流 (SIMD) (SWAR) 并不是一个新概念。鉴于机器具有 k 位寄存器、数据路径和功能单元,人们早已知道,普通寄存器操作可以作为 SIMD 并行操作,对 n 个 k/n 位整数域值进行操作。然而,直到最近对多媒体的推动,SWAR 技术提供的 2 到 8 倍的加速才成为主流计算关注的问题。大多数微处理器的 1997 年版本都集成了对 SWAR 的硬件支持
新微处理器提供的硬件支持存在一些不足,诸如仅支持某些字段大小的某些操作之类的怪癖。然而,重要的是要记住,对于许多 SWAR 操作来说,您不需要任何硬件支持即可高效运行。例如,按位操作不受寄存器的逻辑分区的影响。
尽管每个现代处理器都能够以至少某种 SWAR 并行性执行,但可悲的事实是,即使是最好的 SWAR 增强的指令集也不支持非常通用的并行性。事实上,许多人已经注意到,奔腾和“采用 MMX 技术的奔腾”之间的性能差异通常是由于 L1 缓存更大等因素,而 L1 缓存的出现与 MMX 的出现相吻合。因此,实际上,SWAR(或 MMX)有什么用?
x[y]
(其中 y
是索引数组)的成本高昂。这些是严重的限制,但这种类型的并行性出现在许多并行算法中 - 不仅仅是多媒体应用程序。对于合适的算法类型,SWAR 比 SMP 或集群并行性更有效……而且使用它不需要任何成本。
SWAR 的基本概念,即寄存器内单指令多数据流,是字长寄存器上的操作可以用于加速计算,通过对 n 个 k/n 位字段值执行 SIMD 并行操作。然而,使用 SWAR 技术可能很笨拙,并且某些 SWAR 操作实际上比相应的串行操作序列更昂贵,因为它们需要额外的指令来强制执行字段分区。
为了说明这一点,让我们考虑一个大大简化的 SWAR 机制,该机制管理每个 32 位寄存器中的四个 8 位字段。两个寄存器中的值可以表示为
PE3 PE2 PE1 PE0 +-------+-------+-------+-------+ Reg0 | D 7:0 | C 7:0 | B 7:0 | A 7:0 | +-------+-------+-------+-------+ Reg1 | H 7:0 | G 7:0 | F 7:0 | E 7:0 | +-------+-------+-------+-------+
这只是表明每个寄存器基本上被视为四个独立的 8 位整数值的向量。或者,将 A
和 E
视为处理单元 0 (PE0) 的 Reg0 和 Reg1 中的值,将 B
和 F
视为 PE1 寄存器中的值,依此类推。
本文的其余部分简要回顾了这些整数向量上 SIMD 并行操作的基本类别,以及如何实现这些功能。
可以使用普通的 32 位整数运算轻松执行某些 SWAR 操作,而无需考虑该操作实际上旨在并行独立地对这些 8 位字段进行操作。我们称任何此类 SWAR 操作为多态,因为该函数不受字段类型(大小)的影响。
测试任何字段是否为非零是多态的,所有按位逻辑运算也是如此。例如,普通的按位与运算(C 的 &
运算符)执行按位与运算,无论字段大小如何。上述寄存器的简单按位与运算产生
PE3 PE2 PE1 PE0 +---------+---------+---------+---------+ Reg2 | D&H 7:0 | C&G 7:0 | B&F 7:0 | A&E 7:0 | +---------+---------+---------+---------+
由于按位与运算的结果位 k 的值始终仅受操作数位 k 的值的影响,因此使用相同的单条指令支持所有字段大小。
遗憾的是,许多重要的 SWAR 操作不是多态的。算术运算,例如加法、减法、乘法和除法,都受到字段之间进位/借位交互的影响。我们称此类 SWAR 操作为分区操作,因为每个此类操作都必须有效地对操作数和结果进行分区,以防止字段之间发生交互。然而,实际上有三种不同的方法可以用来实现这种效果。
实现分区操作最明显的方法可能是提供对“分区并行指令”的硬件支持,这些指令可以切断字段之间的进位/借位逻辑。这种方法可以产生最高的性能,但它需要更改处理器的指令集,并且通常对字段大小施加许多限制(例如,可能支持 8 位字段,但不支持 12 位字段)。
AMD/Cyrix/Intel MMX、Digital MAX、HP MAX 和 Sun VIS 都实现了分区指令的受限版本。遗憾的是,这些不同的指令集扩展具有显着不同的限制,使得算法在它们之间有些不可移植。例如,考虑以下分区操作示例
Instruction AMD/Cyrix/Intel MMX DEC MAX HP MAX Sun VIS +---------------------+---------------------+---------+--------+---------+ | Absolute Difference | | 8 | | 8 | +---------------------+---------------------+---------+--------+---------+ | Merge Maximum | | 8, 16 | | | +---------------------+---------------------+---------+--------+---------+ | Compare | 8, 16, 32 | | | 16, 32 | +---------------------+---------------------+---------+--------+---------+ | Multiply | 16 | | | 8x16 | +---------------------+---------------------+---------+--------+---------+ | Add | 8, 16, 32 | | 16 | 16, 32 | +---------------------+---------------------+---------+--------+---------+
在表中,数字表示每个操作支持的字段大小(以位为单位)。即使该表省略了许多指令,包括所有更奇特的指令,但很明显,存在许多差异。直接结果是,高级语言 (HLL) 实际上并不是非常有效的编程模型,并且可移植性通常很差。
使用分区指令实现分区操作当然可能是高效的,但是如果您需要的分区操作不受硬件支持,您该怎么办?答案是,您使用一系列普通指令来执行操作,其中进位/借位跨字段,然后校正不需要的字段交互。
这是一种纯软件方法,校正确实会引入开销,但它可以用于完全通用的字段分区。这种方法也是完全通用的,因为它可以用于填补硬件对分区指令支持方面的空白,或者它可以用于为根本没有硬件支持的目标机器提供完整的功能。事实上,通过以 C 语言之类的语言表达代码序列,这种方法允许 SWAR 程序完全可移植。
立即出现的问题是:使用带有校正代码的未分区操作来模拟 SWAR 分区操作究竟有多低效?好吧,这当然是价值 64,000 美元的问题……但许多操作并不像人们想象的那么困难。
考虑使用普通的 32 位运算来实现两个源向量 x
+y
的四元素 8 位整数向量加法。
普通的 32 位加法实际上可能会产生正确的结果,但前提是任何 8 位字段都不会进位到下一个字段。因此,我们的目标只是确保不发生此类进位。由于将两个 k 位字段相加会生成最多 k+1 位的结果,因此我们可以通过简单地“屏蔽掉”每个字段的最高有效位来确保不发生进位。这是通过将每个操作数与 0x7f7f7f7f
进行按位与运算,然后执行普通的 32 位加法来完成的。
t = ((x & 0x7f7f7f7f) + (y & 0x7f7f7f7f));
该结果是正确的……除了每个字段中最重要的位。计算每个字段的正确值仅仅是将 x
和 y
的最高有效位与为 t
计算的 7 位进位结果进行两次 1 位分区加法的问题。幸运的是,1 位分区加法是通过普通的异或运算实现的。因此,结果很简单
(t ^ ((x ^ y) & 0x80808080))
好吧,也许这没那么简单。毕竟,执行四个加法需要六个操作。然而,请注意,操作次数不是字段数量的函数……因此,字段越多,我们获得的加速就越多。事实上,我们可能仍然会获得加速,仅仅因为这些字段是在单个(整数向量)操作中加载和存储的,寄存器可用性可能会提高,并且动态代码调度依赖性更少(因为避免了部分字引用)。
虽然分区操作实现的其他两种方法都侧重于最大程度地利用寄存器的空间,但控制字段值以使字段间进位/借位事件永远不应发生,在计算上可能更有效。例如,如果我们知道所有要相加的字段值都使得不会发生字段溢出,则可以使用普通的加法指令来实现分区加法操作;事实上,给定此约束,普通的加法指令似乎是多态的,并且可用于任何字段大小而无需校正代码。因此,问题变成了如何确保字段值不会导致进位/借位事件。
确保此属性的一种方法是实现可以限制字段值范围的分区指令。Digital MAX 向量最小值和最大值指令可以被视为对裁剪字段值以避免字段间进位/借位的硬件支持。
然而,假设我们没有可以有效地限制字段值范围的分区指令……是否存在可以廉价实施的充分条件来确保进位/借位事件不会干扰相邻字段?答案在于对算术属性的分析。将两个 k 位数字相加会生成最多 k+1 位的数字;因此,k+1 位的字段可以安全地包含此类操作,尽管使用了普通指令。
因此,假设我们前面示例中的 8 位字段现在是 7 位字段,带有 1 位“进位/借位分隔符”
PE3 PE2 PE1 PE0 +----+-------+----+-------+----+-------+----+-------+ Reg0 | D' | D 6:0 | C' | C 6:0 | B' | B 6:0 | A' | A 6:0 | +----+-------+----+-------+----+-------+----+-------+
7 位加法向量的执行方式如下。让我们假设,在任何分区操作开始之前,所有进位分隔符位(A'
、B'
、C'
和 D'
)的值都为 0。通过简单地执行普通的加法运算,所有字段都获得了正确的 7 位值;然而,某些分隔符位值现在可能为 1。我们可以通过再执行一个常规操作(屏蔽掉分隔符位)来纠正这一点。因此,我们的 7 位整数向量加法 x
+y
为
((x + y) & 0x7f7f7f7f)
这只是两条指令执行四个加法,显然可以实现良好的加速。
精明的读者可能已经注意到,将分隔符位设置为 0 不适用于减法运算。然而,校正非常简单。要计算 x
-y
,我们只需确保初始条件,即 x
中的分隔符全部为 1,而 y
中的分隔符全部为 0。在最坏的情况下,我们会得到
(((x | 0x80808080) - y) & 0x7f7f7f7f)
然而,通常可以通过确保生成 x
值的操作使用 | 0x80808080
而不是 & 0x7f7f7f7f
作为最后一步来优化掉额外的按位或运算。
SWAR 分区操作应使用哪种方法?答案很简单:“以产生最佳加速效果的方法为准。”有趣的是,对于在同一机器上运行的同一程序中的不同字段大小,使用的理想方法可能不同。
虽然一些并行计算(包括对图像像素的许多操作)具有向量中的第 i 个值仅是操作数向量中第 i 个位置的值的函数的属性,但通常情况并非如此。例如,即使是像素操作(例如平滑)也需要来自相邻像素的值作为操作数,而像 FFT 这样的变换则需要更复杂(不太局部化)的通信模式。
使用未分区移位操作为 SWAR 高效地实现一维最近邻通信并不困难。例如,要将值从 PE
i 移动到 PE
(*i*+1),简单的移位操作就足够了。如果字段长度为 8 位,我们将使用
(x << 8)
不过,并非总是那么简单。例如,要将值从 PE
i 移动到 PE
(*i*-1),简单的移位操作可能就足够了……但 C 语言没有指定右移是否保留符号位,并且某些机器仅提供有符号右移。因此,在一般情况下,我们必须显式地将可能复制的符号位清零
((x >> 8) & 0x00ffffff)
使用未分区移位添加“环绕连接”也相当高效。例如,要将值从 PE
i 移动到 PE
(*i*+1) 并进行环绕
((x << 8) | ((x >> 24) & 0x000000ff))
真正的问题出现在必须实现更通用的通信模式时。只有 HP MAX 指令集支持使用单条指令任意重排字段,这称为 Permute
。此 Permute
指令实际上名不副实;它不仅可以执行字段的任意排列,而且还允许重复。简而言之,它实现了任意 x[y]
操作。
遗憾的是,没有这样的指令,x[y]
就很难实现。代码序列通常既长又低效;事实上,它是顺序代码。这非常令人失望。x[y]
操作在 MasPar MP1/MP2 和 Thinking Machines CM1/CM2/CM200 SIMD 超级计算机中的相对高速是这些机器性能良好的关键原因之一。然而,即使在这些超级计算机上,x[y]
也始终比最近邻通信慢,因此许多算法的设计目的是最大限度地减少对 x[y]
操作的需求。简而言之,在没有硬件支持的情况下,最好开发 SWAR 算法,就好像 x[y]
是不合法的……或者至少不便宜。
循环是一种计算,其中正在计算的值之间存在明显的顺序关系。然而,如果这些循环涉及结合律运算,则可以使用树形结构并行算法重新编码计算。
最常见的可并行化循环类型可能是称为结合律归约的类别。例如,要计算向量值的总和,人们通常编写纯粹的顺序 C 代码,例如
t = 0; for (i=0; i<MAX; ++i) t += x[i];
然而,加法的顺序很少重要。如果更改加法的顺序,浮点和饱和运算可能会产生不同的答案,但普通的环绕整数加法将产生与加法顺序无关的相同结果。因此,我们可以将此序列重写为树形结构并行求和,其中我们首先将值对相加,然后将这些部分和对相加,依此类推,直到产生单个最终和结果。对于四个 8 位值的向量,仅需要两个加法步骤;第一步执行两个 8 位加法,产生两个 16 位结果字段(每个字段包含一个 9 位结果)
t = ((x & 0x00ff00ff) + ((x >> 8) & 0x00ff00ff));
第二步在 16 位字段中将这两个 9 位值相加,产生一个 10 位结果
((t + (t >> 16)) & 0x000003ff)
实际上,第二步执行了两个 16 位字段加法……但顶部 16 位加法没有意义,这就是为什么结果被屏蔽为单个 10 位结果值。
扫描,也称为“并行前缀”操作,实现高效性有点困难。这是因为,与归约不同,扫描会产生分区结果。因此,可以使用相当明显的分区操作序列来实现扫描。
对于 Linux,IA32 处理器是我们主要关注的对象。好消息是,AMD、Cyrix 和 Intel 都实现了相同的 MMX 指令。然而,MMX 性能各不相同;例如,K6 只有一个 MMX 流水线 - 采用 MMX 技术的奔腾有两个。唯一真正糟糕的消息是,Intel 仍在播放那些愚蠢的 MMX 商业广告…… ;-)
实际上有三种使用 MMX 进行 SWAR 的方法
总而言之,MMX SWAR 仍然很难使用。然而,只需稍加努力,上面给出的第二种方法现在就可以使用。以下是基本知识
inline extern int mmx_init(void) { int mmx_available; __asm__ __volatile__ ( /* Get CPU version information */ "movl $1, %%eax\n\t" "cpuid\n\t" "andl $0x800000, %%edx\n\t" "movl %%edx, %0" : "=q" (mmx_available) : /* no input */ ); return mmx_available; }
unsigned long long
类型的数据之一。因此,这种类型的基于内存的变量成为 MMX 模块和调用它们的 C 程序之间的通信机制。或者,您可以将 MMX 数据声明为任何 64 位对齐的数据结构(通过将数据类型声明为带有 unsigned long long
字段的 union
来确保 64 位对齐非常方便)。.byte
汇编器指令编写 MMX 代码以编码每条指令。手工完成很痛苦,但对于编译器来说生成并不困难。例如,MMX 指令 PADDB MM0,MM1
可以编码为 GCC 内联汇编代码__asm__ __volatile__ (".byte 0x0f, 0xfc, 0xc1\n\t");
EMMS
指令退出 MMX 代码,该指令可以编码为__asm__ __volatile__ (".byte 0x0f, 0x77\n\t");
如果以上内容看起来非常笨拙和粗糙,那确实如此。然而,MMX 还很年轻……本文的未来版本将提供更好的 MMX SWAR 编程方法。
尽管这种方法最近不再流行,但对于其他并行处理方法来说,几乎不可能通过使用 Linux 系统来托管附加的并行计算系统来达到如此低的成本和如此高的性能。问题是可用的软件支持非常少;您几乎只能靠自己。
一般来说,附加的并行处理器往往专门用于执行特定类型的功能。
在因您在某种程度上只能靠自己而感到沮丧之前,了解 Linux PC 是少数几个非常适合这种用途的平台之一是有用的。
PC 成为优秀主机有两个主要原因。第一个是廉价且易于扩展的功能;诸如更多内存、磁盘、网络等资源可以轻松添加到 PC。第二个是易于接口。ISA 和 PCI 总线原型卡不仅广泛可用,而且并行端口在完全非侵入式接口中提供合理的性能。IA32 单独的 I/O 空间也通过提供硬件 I/O 地址保护来促进接口,保护级别达到各个 I/O 端口地址的级别。
Linux 也是一个优秀的宿主操作系统。完整源代码的免费可用性以及大量的“黑客”指南显然是巨大的帮助。然而,Linux 还提供良好的近实时调度,甚至在 http://luz.cs.nmt.edu/~rtlinux/ 上有一个真正的实时版本的 Linux。也许更重要的是,在提供完整的 UNIX 环境的同时,Linux 可以支持为在 Microsoft DOS 和/或 Windows 下运行而编写的开发工具。MSDOS 程序可以使用 dosemu
在 Linux 进程中执行,以提供可以实际运行 MSDOS 的受保护虚拟机。Linux 对 Windows 3.xx 程序的支持更加直接:诸如 wine
之类的免费软件,http://www.linpro.no/wine/,很好地模拟了 Windows 3.11,足以让大多数程序在 UNIX/X 环境中正确高效地执行。
以下两节给出了我希望在 Linux 下获得支持的附加并行系统的示例……
高性能 DSP(数字信号处理)处理器市场蓬勃发展。尽管这些芯片通常设计为嵌入在特定于应用程序的系统中,但它们也是优秀的附加并行计算机。为什么?
尽管一些声卡和调制解调器包含 Linux 驱动程序可以访问的 DSP 处理器,但巨大的回报来自使用具有四个或更多 DSP 处理器的附加并行系统。
由于德州仪器 TMS320 系列,http://www.ti.com/sc/docs/dsps/dsphome.htm,长期以来一直非常受欢迎,并且构建基于 TMS320 的并行处理器非常简单,因此有相当多的此类系统可用。TMS320 有仅整数和浮点版本;旧设计使用了一种有点不寻常的单精度浮点格式,但新模型支持 IEEE 格式。旧的 TMS320C4x(又名“C4x”)使用 TI 特定的单精度浮点格式实现了高达 80 MFLOPS 的性能;相比之下,单个 'C67x 将为 IEEE 浮点计算提供高达 1 GFLOPS 单精度或 420 MFLOPS 双精度性能,使用称为 VelociTI 的基于 VLIW 的芯片架构。不仅很容易将一组这些芯片配置为多处理器,而且在单个芯片中,'C8x 多处理器将提供 100 MFLOPS IEEE 浮点 RISC 主处理器以及两个或四个整数从 DSP。
最近在更多附加并行系统中使用的另一个 DSP 处理器系列是来自模拟器件 http://www.analog.com/ 的 SHARC(又名 ADSP-2106x)。这些芯片可以配置为 6 处理器共享内存多处理器,无需外部胶合逻辑,并且也可以使用每个芯片六个 4 位链路配置更大的系统。大多数大型系统似乎都面向军事应用,而且有点贵。然而,Integrated Computing Engines, Inc., http://www.iced.com/, 制造了一款有趣的小型双板 PCI 卡套装,名为 GreenICE。该单元包含 16 个 SHARC 处理器的阵列,能够使用单精度 IEEE 格式提供约 1.9 GFLOPS 的峰值速度。GreenICE 的成本低于 5,000 美元。
在我看来,附加的并行 DSP 确实应该得到 Linux 并行处理社区的更多关注……
如果并行处理的全部意义在于获得最高的加速,那么为什么不构建定制硬件呢?好吧,我们都知道答案;成本太高,开发时间太长,即使我们稍微更改算法也会变得无用等等。然而,可电重新编程 FPGA(现场可编程门阵列)的最新进展已使大多数异议无效。现在,门密度足够高,以至于可以在单个 FPGA 中构建一个完整的简单处理器,并且重新配置(重新编程)FPGA 的时间也已降至一个合理的水平,即使从算法的一个阶段移动到下一个阶段,重新配置也是合理的。
这不适合胆小者:您必须使用 VHDL 等硬件描述语言进行 FPGA 配置,以及编写低级代码以与 Linux 主机系统上的程序进行接口。然而,FPGA 的成本很低,特别是对于在低精度整数数据上运行的算法(实际上是 SWAR 擅长的东西的一个小超集),FPGA 可以以几乎与您向其提供数据的速度一样快的速度执行复杂操作。例如,简单的基于 FPGA 的系统在搜索基因数据库方面取得了优于超级计算机的时间。
还有其他公司制造合适的基于 FPGA 的硬件,但以下两家公司代表了一个很好的样本。
Virtual Computer Company 提供各种使用动态可重配置的基于 SRAM 的 Xilinx FPGA 的产品。他们的 8/16 位“Virtual ISA Proto Board”http://www.vcc.com/products/isa.html 价格低于 2,000 美元。
Altera ARC-PCI(Altera 可重配置计算机,PCI 总线),http://www.altera.com/html/new/pressrel/pr_arc-pci.html,是一种类似类型的卡,但使用 Altera FPGA 和 PCI 总线接口而不是 ISA。
许多设计工具、硬件描述语言、编译器、路由器、映射器等都以仅在 Windows 和/或 DOS 下运行的目标代码形式提供。您可以简单地在主机 PC 上保留一个带有 DOS/Windows 的磁盘分区,并在需要使用它们时重新启动,但是,许多这些软件包可能可以在 Linux 下使用 dosemu
或像 wine
这样的 Windows 模拟器运行。
本节涵盖的材料适用于 Linux 的所有四种并行处理模型。
我主要以编译器研究员而闻名,因此我希望能够说有很多非常出色的编译器自动为 Linux 系统生成高效的并行代码。遗憾的是,事实是,很难击败通过在由 GCC 编译的 C 代码中使用各种显式通信和其他并行操作来表达您的并行程序所获得的性能。
以下语言/编译器项目代表了从高级语言生成合理高效代码的一些最佳努力。一般来说,对于它针对的编程任务类型,它相当有效,但没有哪一个是强大的通用语言和编译器系统,可以让您永远停止编写 C 程序以使用 GCC 编译……这很好。按照预期用途使用这些语言和编译器,您将获得更短的开发时间、更轻松的调试和维护等好处。
除了此处列出的语言和编译器之外,还有很多语言和编译器(按字母顺序排列)。免费提供的编译器列表(其中大多数与 Linux 并行处理无关)位于 http://www.idiom.com/free-compilers/。
至少在科学计算领域,永远都会有 Fortran。当然,现在的 Fortran 与 1966 年 ANSI 标准中的含义不同。基本上,Fortran 66 非常简单。Fortran 77 添加了大量功能,其中最引人注目的是对字符数据的改进支持和 DO
循环语义的更改。PCF(并行计算论坛, Parallel Computing Forum)Fortran 试图向 77 添加各种并行处理支持功能。Fortran 90 是一种功能齐全的现代语言,本质上是在 77 语言中添加了类似 C++ 的面向对象编程功能和并行数组语法。HPF(高性能 Fortran, High-Performance Fortran,http://www.crpc.rice.edu/HPFF/home.html)本身经历了两个版本(HPF-1 和 HPF-2),本质上是我们许多人过去所知的 CM Fortran、MasPar Fortran 或 Fortran D 的增强标准化版本;它使用各种并行处理增强功能扩展了 Fortran 90,主要侧重于指定数据布局。Fortran 95 代表了对 90 的相对较小的增强和改进。
通常适用于 C 的方法也适用于 f2c
、g77
(http://linux.uni-regensburg.de/psi_linux/gcc/html_g77/g77_91.html 上有一个不错的特定于 Linux 的概述)或来自 http://extweb.nag.co.uk/nagware/NCNJNKNM.html 的商业 Fortran 90/95 产品。这是因为所有这些编译器最终都归结为 GCC 后端中使用的相同代码生成。
可以为 SMP 生成代码的商业 Fortran 并行化器可从 http://www.kai.com/ 和 http://www.psrv.com/vast/vast_parallel.html 获得。尚不清楚这些编译器是否适用于 SMP Linux,但这应该是可能的,因为标准 POSIX 线程(即 LinuxThreads)可以在 SMP Linux 下工作。
Portland Group,http://www.pgroup.com/,拥有商业并行化 HPF Fortran(以及 C、C++)编译器,这些编译器为 SMP Linux 生成代码;它们还有一个针对使用 MPI 或 PVM 的集群的版本。位于 http://www.apri.com/ 的 FORGE/spf/xHPF 产品也可能对 SMP 或集群有用。
可能适用于并行 Linux 系统的免费并行化 Fortran 包括
我确信我遗漏了许多可能对各种 Fortran 方言有用的编译器,但是编译器太多了,很难跟踪。将来,我更愿意只列出已知可以在 Linux 上工作的编译器。请发送电子邮件评论和/或更正至 hankd@engr.uky.edu。
GLU (Granular Lucid) 是一个非常高级的编程系统,它基于混合编程模型,该模型结合了内涵式 (Lucid) 和命令式模型。它支持 PVM 和 TCP 套接字。它能在 Linux 下运行吗?更多信息请访问 http://www.csl.sri.com/GLU.html。
Jade 是一种并行编程语言,它扩展了 C 语言,以利用顺序、命令式程序中的粗粒度并发性。它假设一个分布式共享内存模型,该模型由 SAM 为使用 PVM 的工作站集群实现。更多信息请访问 http://suif.stanford.edu/~scales/sam.html。
Mentat 是一个面向对象的并行处理系统,它与工作站集群一起工作,并且已经被移植到 Linux。Mentat 编程语言 (MPL) 是一种基于 C++ 的面向对象编程语言。Mentat 运行时系统使用一些模糊类似于非阻塞远程过程调用的东西。更多信息请访问 http://www.cs.virginia.edu/~mentat/。
Legion http://www.cs.virginia.edu/~legion/ 构建在 Mentat 之上,在广域网络机器上提供单个虚拟机的外观。
不要与 Mentat 的 MPL 混淆,这种语言最初是作为 MasPar SIMD 超级计算机的本机并行 C 方言开发的。好吧,MasPar 实际上已经不在那个业务中了(他们现在是 NeoVista Solutions,http://www.neovista.com,一家数据挖掘公司),但他们的 MPL 编译器是使用 GCC 构建的,因此它仍然可以免费获得。在阿拉巴马大学亨茨维尔分校和普渡大学的共同努力下,MasPar 的 MPL 已经被重新定向以生成带有 AFAPI 调用的 C 代码(见第 3.6 节),因此可以在 Linux SMP 和集群上运行。然而,该编译器有些错误... 请参阅 http://www.math.luc.edu/~laufer/mspls/papers/cohen.ps。
Myrias 是一家销售名为 PAMS (并行应用程序管理系统) 软件产品的公司。PAMS 为虚拟共享内存并行处理提供非常简单的指令。尚不支持 Linux 机器网络。有关更多信息,请访问 http://www.myrias.com/。
Parallaxis-III 是一种结构化编程语言,它使用“虚拟处理器和连接”扩展了 Modula-2,用于数据并行性(SIMD 模型)。Parallaxis 软件包括用于顺序和并行计算机系统的编译器、调试器(gdb 和 xgbd 调试器的扩展)以及来自不同领域(尤其是图像处理)的大量示例算法。这可以在顺序 Linux 系统上运行... 旧版本支持各种并行目标,新版本也将支持(例如,针对 PVM 集群)。更多信息请访问 http://www.informatik.uni-stuttgart.de/ipvr/bv/p3/p3.html。
pC++/Sage++ 是 C++ 的语言扩展,它允许使用来自某些基本“元素”类的“对象集合”进行数据并行样式操作。它是一个预处理器,生成可以在 PVM 下运行的 C++ 代码。它能在 Linux 下运行吗?更多信息请访问 http://www.extreme.indiana.edu/sage/。
SR (同步资源) 是一种并发编程语言,其中资源封装了进程和它们共享的变量;操作提供了进程交互的主要机制。SR 为调用和服务操作的机制提供了新颖的集成。因此,支持本地和远程过程调用、会合、消息传递、动态进程创建、多播和信号量。SR 还支持共享全局变量和操作。
它已被移植到 Linux,但不清楚它可以执行什么样的并行性。更多信息请访问 http://www.cs.arizona.edu/sr/www/index.html。
ZPL 是一种基于数组的编程语言,旨在支持工程和科学应用。它生成对名为 IronMan 的简单消息传递接口的调用,并且构成该接口的少量函数可以使用几乎任何消息传递系统轻松实现。但是,它主要针对工作站集群上的 PVM 和 MPI,并且支持 Linux。更多信息请访问 http://www.cs.washington.edu/research/projects/orca3/zpl/www/。
很多人花费大量时间对特定的主板、网卡等进行基准测试,试图确定哪个是最好的。这种方法的问题在于,当你能够对某些东西进行基准测试时,它已经不再是最好的可用产品了;它甚至可能已经从市场上撤下,并被具有完全不同属性的修订型号所取代。
购买 PC 硬件就像购买橙汁。通常,无论标签上是什么公司名称,它都是用非常好的东西制成的。很少有人知道或关心组件(或浓缩橙汁)来自哪里。也就是说,您应该注意一些硬件差异。我的建议很简单,您只需了解您可以从 Linux 下的硬件获得什么期望,然后将注意力集中在获得快速交付、优惠的价格和合理的退货政策上。
在 http://www.pcguide.com/ref/cpu/fam/ 中给出了不同 PC 处理器的出色概述;事实上,整个 WWW 网站 http://www.pcguide.com/ 充满了关于 PC 硬件的优秀技术概述。了解特定硬件配置的性能也很有用,Linux 基准测试 HOWTO http://sunsite.unc.edu/LDP/HOWTO/Benchmarking-HOWTO.html 是一个很好的起点。
Intel IA32 处理器有许多特殊寄存器,可用于非常详细地测量运行系统的性能。Intel VTune,http://developer.intel.com/design/perftool/vtune/,在一个非常完整的代码调优系统中使用性能寄存器,但不幸的是它不能在 Linux 下运行。用于访问 Pentium 性能寄存器的可加载模块设备驱动程序和库例程可从 http://www.cs.umd.edu/users/akinlar/driver.html 获得。请记住,这些性能寄存器在不同的 IA32 处理器上是不同的;此代码仅适用于 Pentium,不适用于 486、Pentium Pro、Pentium II、K6 等。
关于性能的另一个评论是合适的,特别是对于那些想要建造大型集群并将它们放置在狭小空间中的人来说。至少一些现代处理器包含热传感器和电路,如果工作温度过高,这些传感器和电路用于降低内部时钟频率(试图降低热输出并提高可靠性)。我不是建议每个人都应该购买珀尔帖器件(热泵)来冷却每个 CPU,但您应该意识到,高工作温度不仅会缩短组件寿命,还会直接降低系统性能。不要以阻碍气流、将热量滞留在封闭区域等物理配置来布置您的计算机。
最后,性能不仅仅是速度,还包括可靠性和可用性。高可靠性意味着您的系统几乎永远不会崩溃,即使组件发生故障也是如此... 这通常需要冗余电源和热插拔主板等特殊功能。这通常并不便宜。高可用性是指您的系统几乎始终可用的概念... 系统可能会在组件发生故障时崩溃,但系统会很快修复和重启。有一个高可用性 HOWTO 讨论了许多基本问题。但是,特别是对于集群而言,高可用性可以通过拥有一些备件来简单地实现。我建议至少一个备件,并且对于大型集群中的每 16 台机器,我更喜欢至少有一个备件。丢弃故障硬件并用备件替换它可以比维护合同产生更高的可用性和更低的成本。
那么,有人在使用 Linux 进行并行处理吗?是的!
不久前,很多人都在怀疑许多并行处理超级计算机公司的倒闭是否意味着并行处理即将消亡。我当时并不认为它已经死了(有关我认为真正发生的事情的有趣概述,请参阅 http://dynamo.ecn.purdue.edu/~hankd/Opinions/pardead.html),现在看来并行处理再次兴起已经非常清楚了。即使是最近停止制造并行超级计算机的英特尔,也对其 MMX 和即将推出的 IA64 EPIC(显式并行指令计算机)等技术中的并行处理支持感到自豪。
如果您使用您最喜欢的搜索引擎搜索“Linux”和“并行”,您会发现很多地方都在使用 Linux 进行并行处理。特别是,Linux PC 集群似乎随处可见。Linux 的适用性,结合 PC 硬件的低成本和高性能,使得使用 Linux 进行并行处理成为预算受限的小型团体和资金充足的大型国家研究实验室进行超级计算的流行方法。
本文档中其他地方列出的各种项目维护着具有类似并行 Linux 配置的“同类”研究站点的列表。但是,在 http://yara.ecn.purdue.edu/~pplinux/Sites/,有一个超文本文件,旨在为所有使用 Linux 系统进行并行处理的各种站点提供照片、描述和联系信息。要让您的站点信息发布在那里
当前列表中有 14 个集群,但我们知道全球至少有几十个 Linux 集群。当然,列出并不意味着任何背书等;我们的希望只是提高人们对使用 Linux 进行并行处理的认识、研究和协作。