级别: 中级 |
内核黑客,Linux on Cell 的内核维护者, IBM Deutschland Entwicklung GmbH
2005 年 6 月 25 日
对于 Linux on the Cell 的基本平台支持早已搭建好了,目前正努力加入主流的 Linux 内核树。阅读本文可以了解 Cell 这种独一无二的体系结构,以及可以运行 Linux 的 SPU 文件系统接口。
本文改编自 LinuxTag 2005 上发表的 The Cell processor programming model 一文;要获得更多详细信息,请参阅 参考资料 一节。
Cell 处理器是由 Sony、Toshiba 和 IBM® 共同设计的,它是今年 CPU 市场上最值得期待的新品。据说它采用了一种全新的体系结构,在消费和工作站市场上具有前所未有的性能。它采用了一个 64 位的 PowerPC® 核心,将多个独立的向量处理器(称为协处理部件,Synergistic Processing Unit,SPU)组合成单个微处理器。
与现有的 SMP 系统或其他多核心的处理器实现不同,在 Cell 中,只有通用的 PowerPC 核心才可以在一个通用的操作系统上运行,而 SPU 则是专门用来运行一些计算任务的。将 Linux™ 移植到 Cell 的 PowerPC 核心上是一个相当简单的任务,因为它与现有的一些平台非常类似,例如 IBM pSeries® 或 Apple Power Macintosh,但是仅仅这样并不能使用 SPU 的那些功能强大的计算能力。
只有内核才可以直接与 SPU 进行通信,因此需要将硬件接口抽象为系统调用或设备驱动程序。用户接口中最重要的一些功能包括将一个程序的二进制文件加载到 SPU 中,在 SPU 程序和 Linux 用户空间的应用程序中传输内存的内容,并对程序的执行情况进行同步。其他挑战还有 SPU 程序执行与现有工具(例如 GDB 和 OProfile)的集成。
Sony、IBM 和 Toshiba 在德克萨斯州奥斯汀成立了一个联合小组,他们负责实现 Linux 内核移植的一些基础工作。当前的内核补丁基于最新的 2.6.xx 内核,它是由位于德国 Bblingen 的 IBM LTC(Linux Technology Center)小组进行维护的。他们希望将这些补丁大部分都集成到 2.6.13 版本的内核中,这样就可以成为将来各个发行版本的一部分。
Cell 处理器
PowerPC Processing Element
Cell 处理器有一个 PowerPC 处理部件(PowerPC Processing Element,PPE),它采用的是 64 位的 PowerPC AS 体系结构,这与 PowerPC 970 CPU(也称为 G5)和所有最新的 IBM POWER™ 处理器使用的体系结构都完全相同。与 970 类似,它可以使用 VMX(AltiVec)向量指令并行执行算术运算。
而且,Cell 处理器可以使用与 IBM POWER5™ 处理器或 Intel® 的 Pentium 4 处理器中超线程技术类似的一种同步多线程(simultaneous multithreading,SMT)技术。
IBM LTC 有一个在 PPE 上运行的标准 Linux 发行版,它只需要添加少量的内核补丁来增加对与现有目标平台有所区别的硬件特性的支持即可。具体来说,Cell 处理器包括一个中断控制器和一个 IOMMU 的实现,它们与早期的内核版本的支持都是不兼容的。
我们在 LTC 运行的硬件是一个基于 Cell 处理器的刀片服务器样机,它具有两个 Cell 处理器,用作一个对称多处理(SMP)系统,目前配备了 512MB 内存。它设计用于一个 IBM BladeCenter™ 机架中。
在下一个内核发行版中集成对 PPE 的支持之后,将可以为现有的所有 64 位 PowerPC 机器使用一个内核二进制文件,包括 Cell、Apple Power Mac 和 IBM pSeries。
尽管还没有计划要在 Cell 上支持 32 位的 Linux 内核,但是我们可以在 Cell 平台上使用 PowerPC 64 的内核,并且借助 ELF32 二进制格式的支持来运行 32 位和 64 位的发行版本。注意,所有的 32 位 PowerPC 应用程序应该不加任何修改都可以在这种平台上运行。
Synergistic Processing Element
Synergistic Processing Element(SPE)是 Cell 处理器中最关键的一个特性,也正是它无与伦比的处理能力的源泉。一个芯片中封装了 8 个 SPE,每个 SPE 中具有一个 SPU、一个内存流控制器(Memory Flow Controller,MFC) 以及 256KB SRAM 用作本地内存。
SPU 本身使用向量操作,每个时钟周期可以执行多达 8 条浮点指令。
总线接口
Cell 处理器具有 3 个高速总线接口,一个用于内存之间的连接,另外两个用于 I/O 或 SMP 的连接。内存接口连接 XDRAM 芯片,它是目前速度最快的一种内存技术,速度远比目前的 DDR 和 DDR2 接口更快。
与内存接口类似,其他两个接口也是基于 Rambus 技术的。这两个接口中有一个专门用来连接 I/O 设备,通常是为 FlexIO 协议采用一个南桥或北桥芯片。另外一个接口也可以用来连接 I/O 设备,或者用来连接多个 Cell 处理器,形成一个 SMP 系统。
基本的 SPU 设计
SPU 就像是简单的 CPU 设计与数字信号处理器之间的交叉。它使用相同的指令来实现 32 位或 128 位的向量处理。它具有一个 18 位的地址空间,可以访问 256KB 的本地存储器,后者是芯片本身的一个部分。这既不需要使用内存管理单元,也不需要使用指令或数据缓存。相反,SPU 可以以 L1 缓存的速度来访问本地存储器中的任何 128 位的字。
内存流控制器
MFC 是本地存储内存与系统内存之间的主要通信工具。正如前面介绍的一样,在每个 SPE 中都有一个 MFC。它具有一个集成的内存管理单元,通常使用与 PPE 类似的页表查询机制来提供对某个进程地址空间的访问。
DMA 请求通常会涉及数据在 SPE 本地存储与 PPE 端虚拟地址空间之间进行移动。DMA 请求的类型包括对齐读写操作,就像是可以使用单字的原子更新操作一样,例如实现一个可以在 SPE 和用户进程之间进行共享的自旋锁。
SPE 和 PPE 都可以发起 DMA 传输。PPE 可以通过在内核模式中使用内存映射寄存器来发起 DMA 传输,而SPE 则可以使用在 SPU 上运行的代码来写入 DMA 通道。
MFC 可以具有对同一个地址空间的多个并发的 DMA 请求,这些请求可以来自于 PPE 和 SPU。每个 MFC 都可以访问一个单独的地址空间。
指令集
在 SPU 内部运行的程序需要非常简单,而且是自包含的,因此在 SPU 中并不需要复杂的访问保护或不同的优先级模式。结果是指令集中包含大部分算术操作和转移操作,但是并不包含 PPE 中的那种内核模式指令。
而且,执行代码产生的异常结果也不会向 SPU 进行汇报。如果发生了一个非常严重的错误,例如一个无效的操作码,那么 SPU 就会停止,并向 PPE 发送一个中断。有些常见的异常源在 SPU 上根本就不可能出现。例如,根本就不存在寻址异常,因为所有的指针都是对齐的,并且在视图访问某处内存时都根据本地存储的大小进行了截断。
算法向量操作与 PPE 的 VMX 操作非常类似,您可以使用这种操作进行高度优化的视频、图像处理或科学应用。
SPU 与 Cell 处理器的其他部件之间的主要通信方法由许多“通道”来定义。每个通道都是一个预定义的函数,它可能是一个读通道,也可能是一个写通道。
例如,邮箱机制就是 SPE 和 PPE 之间常用的一种基本通信方法。SPU 有一个读通道来从邮箱接收单个数据字,有两个写通道来发送数据字(更详细的介绍请参看下文)。这两个写通道中有一个定义用来在数据可用时对 CPU 产生外部中断,另外一个则不具有通知机制。
当 SPU 视图从一个空的邮箱中读取信息时,它会停止执行,直到有值写入自己的内存映射寄存器中为止。
当 PPE 希望访问邮箱时,它需要能够访问内存映射的寄存器空间,后者通常只对于内核空间来说是可用的。每个 SPU 具有三个邮箱寄存器,每个都可以访问这三个 SPU 邮箱通道。
内存映射寄存器是由 PPE 用来控制对 SPE 的特定属性进行控制的,但是 SPU 代码本身则不能对它进行访问。例如,一个 PPE 端的邮箱寄存器会作为一个只写的物理内存位置出现。当 PPE 将一个数据字写入这个地址时,SPU 就可以从对应的邮箱读通道中读取这个数据字的内容。
其他通道用来访问与 PPE 上的用户上下文关联在一起的虚拟内存。通过向 DMA 通道写入数据,SPE 可以发起一个内存传输操作,这可以与 SPU 代码和 PPE 控制流并行执行。例如,只有在由于所访问的页面已经被交换到磁盘上而发生页面失效的情况时,PPE 才会接收到一个中断。
可能的编程模型
字符设备
在 Linux 程序中使用 SPU 需要使用一部分内核代码,因为控制寄存器只能从 PPE 中使用特权模式进行访问。让用户空间的程序访问硬件资源最简单的方法是通过一个字符设备驱动程序,它可以通过 read、write 和 ioctl 系统调用进行访问。
这对于很多简单设备来说都是适用的,在有些地方可以用来对处理器的功能进行测试,但是这种方法具有很多问题。最为重要的是,如果每个 SPU 都是由一个字符设备来表示的,那么程序就很难发现一个还没有被其他程序使用的 SPU。还有,这个接口不允许按照健全的方式对一个多用户系统中的 SPU 进行可视化。
系统调用
另外一种使用 SPU 的方法是用来定义一组系统调用。这使得在从 SPU 上运行的进程进行抽象的底层单元时,替换物理上的 SPU 成为可能。SPU 处理器可以由内核进行调度,所有的用户不用彼此进行交互就可以直接创建它们。从底层来说,这也意味着要复制一些内核的基础设施,同时解决可能出现的大量新系统调用,从而提供所有必要的功能。
例如,如果现有的 Linux 进程 ID 之后是一个新的线程 ID 空间,那么就需要对这个 PID 真正修改所有的系统调用(kill
、getpriority
、ptrace
等等),或者提供新版本的系统调用。而这两种方法都不是跨平台观点所倡导的。
SPU 文件系统
虚拟文件系统
LTC 小组最终采用的解决方案是创建一个虚拟文件系统来使 SPU 具体化。现在存在很多类似的文件系统,例如 procfs、sysfs 或 mqueue。与具有设备基础的文件系统不同,这几种文件系统并不需要使用一个分区来存储数据,而是会将所有的资源都保存在内存中,同时可以使用一些常见的系统调用,例如 open
、read
和 getdents
,从而在用户空间和内核空间之间进行通信。
我们将这种文件系统称为“spufs”,通常将其挂载到 /spu 上,不过要是挂载到其他地方上也是可以的。
硬件资源的映射
spufs 中的每个目录都是指一个逻辑的 SPU 上下文。这个 SPU 上下文会被当成一个类似的物理 SPU 对待,当前的实现可以在它们之间强制进行直接映射。将来,我们计划要对此进行修改,使其可以保存逻辑上下文而不只是物理 SPU 上下文,并且可以采用一些内核开关来切换它们。
当文件系统挂载到系统中之后,它最初是空的,在其根目录中唯一有效的操作就是使用 mkdir
系统调用创建一个新的目录。
每个上下文目录中都包含一组固定的文件,它们是在建立这个上下文时自动创建的。最重要的有:
使用 SPU 上下文
要在一个进程中使用 SPU,用户需要具有对 spufs 的挂载点具有写权限,并要为这个新的 SPU 上下文选择一个尚未使用的名字。mkdir
系统调用用来创建这个上下文,用户进程然后可以打开这个目录中相关的文件。
|
程序的文本和数据段现在需要写入到 mem 文件中,这可以使用 write
系统调用,或者通过将该文件映射到该进程的地址空间中实现。通常,不需要对内存重新进行分配,因为 SPU 程序是静态链接的。
由于每个执行 SPU_RUN ioctl 的线程在进行 ioctl 系统调用时都会阻塞,因此不能同时与其他任何系统资源进行交互,包括其他的 SPU 上下文或属于执行上下文的文件。一个进程可以使用多个 SPU 上下文,但是要在每个给定的时间点上在多个 SPU 上运行,这个进程需要包含至少一个每个正在运行的 SPU 上下文所使用的线程。
同理,如果这个程序与使用邮箱访问的 SPU 代码进行通信,它就需要创建一个新的线程,例如通过调用 fork
或 pthread_create
。其中一个线程然后会对运行文件调用 SPU_RUN ioctl 系统调用,而其他线程则可以对邮箱文件和其他可能的文件描述符执行一个事件循环。
在运行时不需要与 SPU 代码进行通信的程序可以只有单个执行线程,它可以是用户空间或 SPU 上运行的一个进程。
当使用 SPU 上下文的程序执行完之后,就必须关闭所有在这个上下文目录中打开的文件描述符,然后使用 rmdir
系统调用删除这个目录。
mkdir
创建一个完整的目录,而 rmdir
则删除这个目录及其中包含的所有文件。
信号处理
我们可能需要将信号发送给正在执行 SPU 代码的线程。通常这是由 SPU 代码本身调用的。在发生这种情况时,SPU 就会停止,ioctl 调用也会被中断。
如果这导致在用户空间中调用一个信号处理程序,那么就会创建一个新的堆栈帧,在其中会更新这个线程和 ioctl 的参数,从而在执行信号处理程序之前刷新当前指令的指针。通常,信号处理程序将会返回,并进入上次离开时 ioctl 的位置处,这样 SPU 程序就可以继续执行了。
SPU 库抽象
库接口
我们已经在底层编程模型的基础上构建了一个可移植的库接口。这个库接口并不依赖于文件系统的实现细节,但是也可以在其他内核接口稍有不同的操作系统上使用。
这个接口并不是提供一个逻辑 SPU 的抽象,而是面向线程的,它的工作方式与 pthread 库类似。在创建一个 SPU 线程时,这个库就创建一个新线程,它负责管理与主线程异步的 SPU 上下文。
从 SPU 中使用库调用
当 SPU 需要执行任何标准的库调用时,例如 printf
或 exit
,它都需要回调主线程。它是通过执行一个具有标准参数值的特殊停止和信号汇编指令实现的。这个值是从 ioctl 调用中返回的,用户线程必须对此进行响应。这通常意味着从 SPE 的本地存储中拷贝参数,在用户线程中运行各自的库函数,并通过再次调用 ioctl 继续执行。
从 SPU 中直接进行系统调用
我们正在考虑为 spufs 增加一个直接的系统调用,在这种情况中,停止并产生信号的指令不能跟踪到用户空间中,而是让内核从本地存储中读取系统调用参数,并直接进入系统调用。
由于这发生在一个用户进程的 ioctl 系统调用之内,因此 SPU 系统调用的任何指针参数都假设是指向该进程的地址空间,这个 SPU 程序需要使用 DMA 来访问它们。
工具链支持
编译器,binutils
由于 Cell 的 PPE 使用了与 PowerPC 970 CPU 相同的指令集,因此不需要对编译器和 binutils 进行任何修改。然而,如果使用专门为 CPU 的流水线结构进行优化的编译器时,编译后的代码可以更有效地运行。您可以找到 GCC 的一个补丁,它用来添加 PPE 的流水线定义,这样就可以创建优化的代码。
由于 SPU 指令集并不与现有的 CPU 架构直接相关,因此我们为 GCC 和 binutils 编写了一个新的后端程序。SPU 代码与 PPC 代码分开进行编译,并在运行时进行加载。
GCC 对 PPE 优化的修改和 CPU 后端都希望可以作为 Linux 发行版本的一部分发布。
编译器引入了一些新的特性来实现 DMA 传输和其他邮箱访问,这是因为这些并不是 C 语言标准的部分。还有,这些新特性以及一个新的数据类型使用了向量指令进行并行化的计算。这与 VMX/AltiVec 或 SSE 向量指令的功能类似。
未来的 GCC 版本应该可以使用自动向量化技术,自动创建向量代码,但是现在还不具有这种功能。这就像是显式的向量指令通常都比编译器所生成的代码更加有效一样。
调试器
调试 SPU 程序会出现一些新的问题。虽然为 SPU 自己创建一个新的 GDB 目标非常简单,但是大部分用户都需要对 PPE 和 SPE 之间的交互进行调试。我们此处讨论的方法是要启用 GDB 在一个二进制文件中支持对多个目标程序的支持,并修改 PowerPC 目标程序,使其知晓 spufs 的存在。当 PowerPC 调试器发现一个程序运行 SPU_RUN ioctl 调用时,它就切换到 SPU 后端代码上,并使用 SPU 上下文,而不是主程序的上下文。
分析器
虽然现在还没有用于 spufs 程序的分析器,但是我们计划对 OProfile 进行扩展,使其可以包含 PPE/SPE 程序。这需要修改 OProfile 的内核代码来周期性地对 SPU 指令指针进行采样。
在用户空间的 OProfile 中,从 spufs 文件到需要加载的实际 ELF 文件需要一个额外的间接级别。
结束语
在目前的 Linux on Cell 中,您可以编写专用的程序在这种原型系统上运行,同时使用这种芯片的全部性能。虽然大部分程序都不能立即在 Cell 平台上更好地运行,但是很有可能将一些性能关键的程序移植到使用在 SPU 上运行的库代码的程序,以实现更好的性能。
基本的平台支持目前正在设法进入主流的 Linux 内核中,SPU 文件系统接口也正在逐步稳定地通往包含到主发行商的内核版本的征途上。