处理器调度程序性能概述
线程支持
线程可看作开销低的进程。它是一个可分派实体,创建它需要的资源比创建一个进程需要的资源少。
进程由一个或多个线程组成。事实上,操作系统的早期发行版中负载的直接迁移就是继续创建和管理进程。每个新进程在创建时只带有单一的线程,该线程具有其父进程的优先级并与其它进程中的线程争用处理器。进程在执行时拥有它所使用的资源,而线程仅仅拥有它的当前状态。
当新的或修改的应用程序利用操作系统的线程支持创建额外的线程时,那些线程在该进程的上下文中创建。它们共享进程的私有段和其它资源。
进程中的一个用户线程有一个特定的争用作用域。如果争用作用域是全局的,则该线程与系统中所有其它线程一起争用处理器时间。在进程创建时产生的线程具有全局争用作用域。如果争用作用域本地的,则该线程与进程中的其它线程竞争以成为进程共享的处理器时间的接收方。
决定接下来应该运行哪个线程的算法叫调度策略。
进程和线程
进程是系统中的一个活动,它由某个命令、shell 程序或另一进程启动。
进程的属性如下:
pid
pgid
uid
gid
环境
cwd
文件描述符
信号操作
进程统计信息
nice
线程的属性如下:
堆栈
调度策略
调度优先级
暂挂信号
阻塞信号
线程特定的数据
每个进程由一个或多个线程组成。线程是一个单独的控制序列流。多个控制线程允许应用程序进行重叠操作,例如读取终端和写文件。
多个控制线程也允许应用程序同时为来自多个用户的请求服务。线程提供了这些能力而不需多个进程那样的额外开销,例如要通过 fork() 系统调用创建多个进程。
AIX 4.3.1 中引入了一个快速的 fork 例程 f_fork()。该例程对多线程应用程序非常有用,它们将立刻调用 exec() 子例程,前提是之前应先调用 fork() 子例程。fork() 子例程运行起来较慢,因为在实际派生及让其子例程运行全部子处理程序来初始化所有锁之前,它必须调用 fork 处理程序获得所有的库锁。f_fork() 子例程忽略这些处理程序并直接调用 kfork() 系统调用。Web 服务器是一个可以使用 f_fork() 子例程的很好的应用程序示例。
进程和线程的优先级
优先级管理工具处理进程的优先级。在 AIX V4 中,进程优先级只是线程优先级的前驱。当调用 fork() 子例程时,会创建一个进程和一个要在其中运行的线程。线程的优先级归结于进程。
内核为每个线程维护一个优先级值(有时称为调度优先级)。优先级值是一个正整数且与关联线程的重要性的变化方向相反。也就是说,较小的优先级值表示一个相对重要的线程。当调度程序寻找线程进行分派时,它选择具有较小优先级值的可分派线程。
线程可以有固定的优先级或不固定的优先级。优先级固定的线程的优先级值是一个常量,而优先级不固定的线程的优先级值根据用户线程最小优先级级别(常量 40)、线程的 nice 值(缺省值是 20,可随意由 nice 或 renice 命令进行设置)和其处理器使用的损失而变化。
线程的优先级可以固定成某个值,如果用 setpri() 子例程设置(固定)它们的优先级的话,它们可以具有小于 40 的优先级值。这些线程不会受到调度程序重算算法的影响。如果它们的优先级值固定且小于 40,这些线程将在可以运行所有用户线程之前运行和完成。例如,一个具有固定值 10 的线程将在具有固定值 15 的线程之前运行。
用户可以应用 nice 命令使线程的不固定优先级变低。系统管理员可将一个负的 nice 值应用给线程,这样就给了它较好的优先级。
下图显示了一些可以更改优先级值的方法。
图 6. 如何确定优先级值. 插图显示了如何能在执行过程中或应用了 nice 命令之后更改线程调度优先级值。优先级值越小,线程优先级越高。开始时,nice 值缺省为 20 而基本优先级缺省为 40。在执行一些操作及处理器损失后,nice 的值仍为 20 且基本优先级仍为 40。在运行 renice --5 命令后及使用和以前相同的处理器的情况下,nice 值现在是 15 而基本优先级仍为 40。在以 50 的值发出子例程 setpri() 之后,固定优先级现在是 50 而 nice 值和处理器的使用无关。
线程的 nice 值在创建线程时设置并且在线程的整个生命期中都是常量,除非用户通过 renice 命令或 setpri()、setpriority()、thread_setsched() 或 nice() 系统调用明确更改了它的值。
处理器损失是一个整数,它通过线程最近的处理器使用来计算。如果每次在一个 10 ms 的时钟滴答结束时线程受处理器控制,则最近的处理器使用值近似加 1,直到达到最大值 120。每个滴答的实际优先级损失随着 nice 的值增加。所有线程的最近处理器使用值每秒重算一次。
结果如下:
不固定优先级的线程的优先级随着其最近处理器使用的增加而变低,反之亦然。这暗示一般来讲,某线程最近被分配的时间片越多,则它被分配下一个时间片的可能性越小。
不固定优先级的线程的优先级随着其 nice 值的增加而变低,反之亦然。
注:
使用多处理器运行队列及其负载平衡机制以后,nice 或 renice 的值对线程的优先级可能没有预期的影响,因为较低优先级的运行时间可能等于或大于较高优先级的运行时间。要求 nice 或 renice 产生预期效果的线程应该放在全局运行队列中。
可以使用命令 ps 显示进程的优先级值、nice 值和短期的处理器使用值。
请参阅『处理器的控制争用』中对使用 nice 和 renice 命令的更详细的讨论。
请参阅『调谐线程优先级值的计算』,里面有处理器损失计算和最近处理器使用值衰减的详细信息。
优先级机制也用于 AIX 工作负载管理器中来加强处理器资源管理。因为在工作负载管理器下分类的线程具有的优先级由工作负载管理器管理,它们可能与没有在工作负载管理器下分类的线程具有不同的优先级行为。
线程的调度策略
下面是线程调度策略的可能值:
SCHED_FIFO
这种策略的线程被调度后,它会一直运行到结束,除非被阻塞或有一个较高优先级的线程可分派,它将自愿服从处理器的控制。只有固定优先级的线程才能有 SCHED_FIFO 调度策略。
SCHED_RR
当一个 SCHED_RR 线程在时间片的末尾有控制权时,它将移动到和它具有相同优先级的可分派线程队列的尾部。只有固定优先级的线程才能有 SCHED_RR 的调度策略。
SCHED_OTHER
这个策略在“POSIX 标准 1003.4a”中作为定义的执行程序进行定义。在每个时钟中断时重算运行线程的优先级值意味着该线程可能失去控制权,因为它的优先级值已经超过了另一可分派线程的优先级值。
SCHED_FIFO2
该策略和 SCHED_FIFO 相同,只是它允许一个仅睡眠了很短时间的线程在被唤醒时可放置在其运行队列的头部。这个时间周期是相似性限制(可用 schedtune -a 进行调节)。该策略仅可用于 AIX 4.3.3 及其后续版本。
SCHED_FIFO3
调度策略设置成 SCHED_FIFO3 的线程总是放置在运行队列的头部。为了防止属于 SCHED_FIFO2 调度策略的线程放置在 SCHED_FIFO3 之前,当 SCHED_FIFO3 线程入队列时更改运行队列参数,这样属于 SCHED_FIFO2 的线程就不满足使其能够加入运行队列头部时必须满足的标准。该策略仅可用于 AIX 4.3.3 及其后续版本。
SCHED_FIFO4
只要优先级值相差 1,较高优先级的 SCHED_FIFO4 调度类线程就不会抢占当前正运行的低优先级线程。缺省行为是当前运行于某给定 CPU 的低优先级线程被有资格在同一 CPU 上运行的高优先级线程抢占。该策略仅可用于 AIX 5L V5100-01 + APAR IY22854 及其后续版本。
调度策略可用系统调用 thread_setsched() 进行设置并且仅对调用线程有效。然而,通过指定进程标识发出 setpri() 调用可将线程设置成 SCHED_RR 调度策略;setpri() 的调用者和 setpri() 的目标不必匹配。
只有那些具有 root 权限的进程可以发出 setpri() 系统调用。只有那些具有 root 权限的线程可将调度策略更改成任何 SCHED_FIFO 选项或 SCHED_RR。如果调度策略是 SCHED_OTHER,则优先级参数被 thread_setsched()子例程忽略。
线程的主要优点是适用于当前由多个异步进程组成的应用程序。这些应用程序可通过转变成多线程结构使得系统中有较轻的负载。
调度程序运行队列
调度程序维护一个所有就绪等待分派的线程的运行队列。
给定优先级的所有可分派线程在运行队列中占有一定的位置。
调度程序的基本可分派实体是线程。AIX 5.1 维护 256 个运行队列(128 个在 AIX 4.3 及以前的发行版中)。在 AIX 5.1 中,运行队列与每个线程优先级字段可能值的范围(从 0 到 255)直接相关。这个方法使调度程序更容易确定哪个线程最先运行。调度程序无需搜索一个完整的运行队列,只需要考虑一个掩码,该掩码的某一位启用后可表示在相应的运行队列中存在就绪等待运行的线程。
线程的优先级值快速而频繁地变更。持续的变动归因于调度程序重算优先级的方法。然而,这并不适用于固定优先级的线程。
从 AIX 4.3.3 开始,每个处理器都有自己的运行队列。性能工具中报告的运行队列值将是每个运行队列中所有线程的总和。让每个处理器都有自己的运行队列可节省分派锁的开销并改善总体的处理器相似性。线程通常会更加趋向于留在同一处理器中。如果因为另一处理器上的事件使某线程变得可运行且有空闲的处理器的话,即使不同于最近可运行线程曾经运行过的处理器,该线程也只会立即被分派。在可以检查处理器状态(例如在该线程的处理器上的中断)之前不会出现抢占。
在具有多个运行队列的多处理器系统中,可能出现瞬间的优先级倒置。在任何一个时间点都可能出现这种情况:某个运行队列能使若干线程具有的优先级比另一运行队列更有利。AIX 有一些机制可以随着时间的推移来进行优先级平衡,但是如果要求严格的优先级(例如,对于实时应用程序)可用一个叫做 RT_GRQ 的环境变量,如果将它设置成 ON,将导致该线程位于一个全局运行队列中。在那种情况下,将搜索全局运行队列来察看哪个线程具有最佳优先级。这可以改善中断驱动线程的性能。如果将 schedtune -F 设置成 1,以固定优先级运行的线程就放置在全局运行队列中。
运行队列中的线程平均数可在命令 vmstat 输出的第一列中看到。如果用处理器数去除这个数,结果是每个处理器上可运行线程的平均数。如果这个值大于 1,这些线程必须等待轮到它们使用处理器(这个数越大,性能延迟可能越明显)。
当某线程移到运行队列的末端时(例如,当线程在时间片的末尾拥有控制权时),它会移动到具有相同优先级值的队列中最后一个线程之后的位置上。
调度程序处理器时间片
处理器时间片是调度程序转换到另一个具有相同优先级的线程之前,一个 SCHED_RR 线程能获得的时间的总和。可以使用命令 schedtune 的选项 -t 在时间片上以 10 毫秒的增量来增加时钟滴答数(参阅『用 schedtune 命令修改调度程序时间片』)。
注:
时间片并不是保证的处理器时间量。它是一个线程在面临由另一线程取代的可能性之前可以受控的最长时间。在控制时间达到完整时间片之前有很多方法可使线程失去处理器的控制。
方式转换
用户进程在需要访问系统资源时会经历一个方式转换。这通过系统调用接口或诸如缺页故障这样的中断来实现。有两种方式:
用户方式
内核方式
花在用户方式(应用程序和共享库)下的处理器时间作为用户时间在一些命令的输出中反映出来,例如,vmstat、iostat 和 sar 命令。花在内核方式下的处理器时间作为系统时间在这些命令的输出中反映出来。
用户方式
在用户保护域中执行的程序是用户进程。在这种保护域中执行的代码以用户执行方式执行,且具有下列访问:
读/写访问进程专用区域中的用户数据
读访问用户文本和共享文本区域
使用共享内存功能访问共享数据区域
在用户保护域中执行的程序不能访问内核或内核数据段,除非通过使用系统调用间接访问。在该保护域中的程序只能影响自身的执行环境并在进程或非特权状态下执行。
内核方式
在内核保护域中执行的程序包含中断处理程序、内核进程、基内核和内核扩展(设备驱动程序、系统调用和文件系统)。这个保护域暗示以内核执行方式执行代码,具有下列访问:
读/写访问全局内核地址空间
在进程中执行时读/写访问进程区域中的内核数据
内核服务必须用来访问进程地址空间中的用户数据。
在该保护域中执行的程序会影响所有程序的执行环境,因为它们具有下列特征:
它们可访问全局系统数据
它们可使用内核服务
它们免受所有安全性约束
它们执行于处理器特权状态下。
方式转换
用户方式的进程使用的系统调用允许通过用户方式调用内核函数。直接或间接地调用系统调用来访问的函数一般由程序设计库提供,它们提供对操作系统函数的访问。
方式转换应该不同于在命令 vmstat(cs 列)和 sar(cswch/s)的输出中所看到的上下文转换。在当前运行的线程不同于该处理器上先前运行的线程时会出现上下文转换。
当下列任一情况出现时调度程序执行上下文转换:
线程必须等候某个资源(自愿),比如磁盘 I/O、网络 I/O、睡眠或锁
一个较高优先级线程被唤醒(非自愿)
线程已经用完了它的时间片(通常是 10 ms)。
上下文转换的时间、系统调用、设备中断、NFS I/O 和内核中任何其它活动都看作系统时间。
2004-8-23 19:48:39 鲜花(0) 鸡蛋(0)
snappyboy
等级:论坛游民
文章:123
积分:283
注册:2003-10-20
第2楼
多处理介绍
无论何时,单处理器芯片的运行速度都存在着技术上的限制。如果单处理器无法令人满意地处理系统的工作负载,一种响应是使用多处理器来解决这个问题。
这种响应是否成功不仅仅取决于系统设计者的技术熟练程度,还取决于工作负载是否服从多处理控制。就人的任务而言,如果任务是应答一个免费电话号码的呼叫,增加人员也许不失为一个好主意,但是假如任务是开车的话,这种做法是否有效就值得怀疑了。
如果建议从一个单处理器系统迁移到一个多处理器系统的目标是为了改进性能,则下列条件必须成立:
工作负载受处理器限制并且已经使得它的单处理器系统饱和。
工作负载包含多种处理器密集的元素,例如事务或者复杂计算,这些操作可以同时并且各自独立地执行。
现有的单处理器不能升级,也不能由另一个能量充足的单处理器代替。
虽然正常情况下不变的单线程应用程序在某个多处理器环境中能正确运行,但它们的性能常常会有意外的变化。迁移到多处理器可以改善系统的吞吐量,并能改进复杂的多线程应用程序的执行时间,但是很少能改进个别的单线程命令的响应时间。
要从一个多处理器系统获得最佳性能,需要对多处理器系统独有的操作系统和硬件执行动态有所了解
对称多处理器(SMP)概念和体系结构
对于增加系统复杂性的任何变化,为了获得令人满意的操作和性能,使用多处理器产生了一些设计时必须引起注意的事项。额外的复杂性使得软/硬件权衡的作用域更大,并且比在单处理器系统中更需要软/硬件的密切配合。设计响应和权衡的不同组合使得多处理器系统的体系结构更加多样化。
这一节描述了多处理器系统的主要设计注意事项和这些事项的硬件响应。
多处理的类型
有几种多处理(MP)系统,如下所述:
非共享 MP(纯群集)
每个处理器都是一个完全独立的机器,运行操作系统的一个副本。处理器之间没有共享的部分(每一个都有自己的内存,高速缓存和磁盘),但是它们是互联的。通过 LAN 连接时,处理器之间是松散耦合的。而通过转换器连接时,处理器之间是紧密耦合的。处理器之间的通信是通过消息传送来实现的。
这样一个系统的优点是它具有很好的可伸缩性和高可用性。而缺点则是该系统是一个不为人熟悉的编程模型(消息传送)。
共享磁盘 MP
处理器拥有自身的内存和高速缓存。处理器并行运行并共享磁盘。每个处理器都运行操作系统的一份副本,并且处理器之间是松散耦合的(通过 LAN 连接)。处理器之间的通信是通过信息传送实现的。
共享磁盘的优点是保留了熟悉的编程模型的一部分(磁盘数据是可寻址和连续的,而内存则不是),而且与共享内存的系统相比,这种系统更容易实现高可用性。缺点是由于在对共享数据进行物理和逻辑访问时存在瓶颈,它的可伸缩性受到限制。
共享内存群集(SMC)
一个共享内存群集中的所有处理器有自己的资源(主存储器、磁盘和 I/0),并且每个处理器运行一份操作系统的副本。处理器之间是紧密耦合的(通过一个转换器连接)。处理器之间的通信是通过共享内存实现的。
共享内存 MP
所有处理器通过一条高速总线或者一个转换器在同一机器中紧密耦合。处理器共享同样的全局内存、磁盘和 I/0 设备。只有一份操作系统的副本跨所有处理器运行,并且操作系统必须设计为能利用这种体系结构(多线程操作系统)。
SMP 有几个优点:
它们是增加吞吐量的一种划算的方法。
由于操作系统由所有处理器共享,它们提供了一个单独的系统映像(易于管理)。
它们对一个单独的问题应用多处理器(并行编程)。
负载平衡是由操作系统实现的。
这种单处理器(UP)编程模型可用于一个 SMP 中。
对于共享数据来说,它们是可伸缩的。
所有数据可由所有处理器寻址,并且由硬件监视逻辑保持连续性。
由于通信经由全局共享内存执行,在处理器之间通信不必使用消息传送库。
更多能量的需求可通过向系统添加更多处理器来解决。然而,在一个 SMP 系统里添加更多处理器时,您必须设置关于性能增强的现实期望值。
现在越来越多的应用程序和工具都可以使用。大多数 UP 应用程序可以在 SMP 体系结构中运行或者被移植到 SMP 体系结构中。
SMP 系统有一些局限性,如下所述:
由于高速缓存相关性、锁定机制、共享对象和其它问题,可伸缩性受到限制。
需要新技术来利用多处理器,例如线程编程和设备驱动程序编程。
并行化应用程序
有两种方法可以在一个 SMP 中使应用程序并行化,如下所述:
传统方法是把应用程序分解为多个进程。这些进程使用进程间通信(IPC)方法进行通信,例如管道、信号量或者共享内存。必须能够阻塞进程使其等待事件的发生(例如来自其它进程的消息),并且进程必须用类似锁的东西协调对共享对象的访问。
另一种方法是使用面向 UNIX(POSIX)线程的可移植操作系统接口。线程和进程一样存在协调的问题,并有类似的处理机制。因此一个单独的进程可以同时有很多线程运行在不同的处理器上。协调这些线程并且使得对共享数据的访问序列化是开发者的责任。
在并行化一个应用程序的时候,考虑线程和进程两者各自的优势并且决定使用哪种方法。线程可能比进程快,并且它对内存的共享也比较容易。另一方面,进程的实现更容易分布到多个机器或者群集中。如果一个应用程序需要创建或者删除新实例,则线程会更快(在派生进程中开销更大)。就其它功能而言,线程的开销和进程差不多。
数据序列化
任何可由多个线程读或写的存储元素在程序运行中都可能改变。通常,这对多程序设计环境以及多处理环境都是成立的,但是多处理器的出现以两种方式增加了这种注意事项的作用域和重要性。
多处理器和线程的支持使得编写在线程中共享数据的应用程序具有吸引力和更容易。
内核再也不能通过简单地禁用中断来解决序列化问题。
注:
为了避免产生严重问题,共享数据的程序必须安排好,以对数据进行串行访问,而不是并行访问。在一个程序更新一个共享数据项之前,必须确保没有其它程序(包括它本身在另一个线程里运行的另一副本)会改变该项。通常读操作可以并行地执行。
用来避免程序互相干扰的主要机制是锁。锁是一种抽象概念,它代表对访问一个或多个数据项的许可。锁定和解锁的请求是原子级的;也就是说,它们的实现方式为:其结果既不受中断也不受多处理器访问的影响。所有访问一个共享数据项的程序在处理它之前必须先获得与它相关的锁。如果这个锁已经由另一个程序(或者另一个运行同一程序的线程)占有,则请求的程序必须推迟访问,直到锁变得可用。
除了等待锁所花的时间之外,序列化也增加了一个线程成为不可分派线程所花的时间。当线程不可分派时,其它线程很可能会使这个不可分派线程的高速缓存线路被替换,这将导致线程最后获得锁并被分派时内存等待时间成本增加。
操作系统的内核包含很多共享的数据项,所以它必须在内部进行序列化。因此序列化延迟甚至可能在一个不与其它程序共享数据的应用程序中发生,因为由该程序使用的内核服务必须序列化共享的内核数据。
锁的类型
开放软件基金会/1(OSF/1)1.1 的锁定方法被作为一个 AIX 的多处理器锁定功能模型使用。然而,由于系统是可抢占和可调页的,对 OSF/1 1.1 锁定模型增加了一些特征。简单和复杂的锁都是可抢占的。一个线程在尝试获得一个忙状态的简单锁时也可以睡眠,如果锁的所有者当前并不在运行的话。另外,当一个处理器在一个简单锁上自旋一段时间(这段时间是一个全系统的变量)以后,这个简单锁会变成睡眠锁。
锁粒度
一个在多处理器环境中工作的程序员必须决定对共享数据一定要创建多少单独的锁。如果只有一个锁来序列化整个共享数据项的集合,则相比之下很可能出现锁争用。广泛使用锁的存在给系统吞吐量加了上限。
如果每一个不同的数据项都有自己的锁,则两个线程争用这个锁的概率相对来说就比较低。然而,每一个附加的锁定和解锁调用都会消耗处理器时间,并且多个锁的存在使得可能发生死锁。最简单的死锁情况如下图所示,其中线程 1 拥有锁 A 并且正在等待锁 B。同时,线程 2 拥有锁 B 并且正在等待锁 A。这两个程序都永远用不上会打破死锁的 unlock() 调用。通常对死锁的预防措施是建立一个协议,根据该协议,所有使用一个指定的锁集合的程序必须始终按照完全相同的顺序获得它们。
根据排队理论,一个资源闲置得越少,要得到它的平均等待时间就越长。这种关系是非线性的;如果锁的个数翻倍,平均等待这个锁的时间就比原来的两倍还要多。
减少对锁的等待时间的最有效方法是减少这个锁所保护的范围大小。下面是一些准则:
减少对任何锁的请求频率。
只锁定访问共享数据的代码,而不是一个组件的所有代码(这将减少锁的持有时间)。
只锁定特定的数据项或结构,而不是整个例程。
始终将锁和特定的数据项或结构关联起来,而不是和例程关联。
对于大的数据结构,为结构的每一元素选择一个锁,而不是为整个结构选择一个锁。
当持有一个锁时,从不执行同步 I/O 或者任何其它阻塞活动。
如果您对您组件中的同一数据有多个访问,请试着把它们移到一起,以便它们可以包含在一个锁定 — 解锁操作中。
避免双唤醒的情况。如果您在一个锁下修改了一些数据,并且不得不通知某人您做了这件事,则在公布唤醒之前请释放该锁。
如果必须同时持有两个锁,则最后请求那个最忙的锁。
另一方面,过细粒度将增加对锁的请求和释放的频率,因而会增加额外的指令。您必须在过细和过粗粒度之间找到平衡。最佳粒度不得不通过试验和错误找到,这也是一个 MP 系统中的最大挑战之一。
锁定开销
请求锁,等待锁和释放锁在几方面增加了处理开销:
一个支持多处理的程序总是进行相同的锁定和解锁处理,即使它是在一个单处理器里运行或者是一个多处理器系统里对于这个锁的唯一使用者。
当一个线程请求一个由另一线程持有的锁时,发出请求的线程可能会自旋一会或者置于睡眠状态,如果可能的话,会分派另一个线程。这会消耗处理器时间。
广泛使用锁的存在给系统吞吐量加了一个上限。例如,如果一个给定的程序花 20% 的执行时间来持有一个互斥锁,这个程序最多只有五个实例能同时运行,不管系统里有多少个处理器。事实上,即使只有五个实例,它们也很可能永远不会精确同步,以免互相等待。(参阅『多处理器吞吐量可伸缩性』)。
等待锁
当一个线程需要另一个线程已拥有的锁时,该线程被阻塞并且必须等到锁变得可用为止。有两种不同的等待方式:
对于只被持有很短时间的锁来说,自旋锁是很适合的。它允许等待中的线程保持其处理器重复检查某个死循环(自旋)里的锁定位,直到锁变得可用。自旋导致 CPU 时间(内核或内核扩展锁定的时间)增加。
睡眠锁适合于可能会被持有较长时间的锁。线程会睡眠到锁可用为止,当锁变得可用后,它会被放回到运行队列里。睡眠导致更多的闲置时间。
等待总会降低系统性能。如果使用自旋锁,处理器是繁忙的,但是它不是在做有用功(不是在为吞吐量出力)。如果使用睡眠锁,会导致上下文切换和分派的开销以及随之而来的高速缓存未命中的增加。
操作系统开发者们可以在两种类型的锁之间选择:在等待锁变得可用时允许进程自旋和睡眠的互斥简单锁,和在等待锁变得可用时可以自旋和阻塞进程的复杂读写锁。
一些约定管理着使用锁的规则。不管是硬件还是软件都没有实施或校验的机制。尽管使用锁已经使得 AIX V4 是“MP 安全”的,开发者们还是有责任定义和实现一个合适的锁定策略来保护他们自己的全局数据。
高速缓存一致性
在设计多处理器时,工程师们对保证高速缓存的一致性给予了相当多的注意。他们取得了成功;但是高速缓存一致性是以性能为代价的。我们需要理解这个遭受攻击的问题:
如果每个处理器都有一个反映内存不同部分状态的高速缓存,就可能会有两个或更多高速缓存拥有相同线路的副本。也有可能是一个给定的线路会包含不止一个可锁定的数据项。如果两个线程对那些数据项作了适当的序列化更改,结果可能是两个高速缓存都以不同的,错误版本的内存线路而告终。换句话说,系统的状态不再一致,因为系统包含了应该是一个特定内存区域的内容的两个不同版本。
对高速缓存一致性问题的解决方案通常包括在线路修改之后,除了一条线路以外,使所有重复线路都失效。尽管硬件使用监视逻辑使线路失效,没有任何软件干预的话,任何高速缓存线路已经失效的处理器由于随之而来的延迟,将会在下一次寻址到该线路时出现高速缓存未命中。
监视是用来解决高速缓存一致性问题的逻辑。处理器中的监视逻辑每次修改了其高速缓存中的一个字后,会在总线上广播一条消息。监视逻辑也在总线上监视,寻找来自其它处理器的这种消息。
当一个处理器检测到另一个处理器已经更改了存在于它本身高速缓存内的一个地址的值时,监视逻辑会使得它自己的高速缓存中的该项失效。这被称为交叉式失效。交叉式失效提醒处理器高速缓存中的值已经无效了,处理器必须在别处(内存或其它高速缓存)寻找正确的值。由于交叉式失效增加了高速缓存未命中率,而监视协议增加了总线流量,因而解决高速缓存的一致性问题会降低所有 SMP 的性能和可伸缩性。
处理器相似性和绑定
如果一个线程中断后又重新分派到同一个处理器中,该处理器的高速缓存也许仍含有属于该线程的线路。如果该线程被分派到不同的处理器,它将很可能经历一系列高速缓存未命中,直到它的高速缓存工作集从 RAM 或其它处理器的高速缓存中检索到。另一方面,如果一个可分派的线程必须等到它先前在其中运行的处理器可用,该线程也许会经历一个更长的延迟。
处理器相似性是指将一个线程分派到先前运行它的处理器之上的概率。对处理器相似性的强调程度应随线程的高速缓存工作集大小直接变化,而随自它上一次分派以来的时间长短反向变化。AIX V4 分派器强制对处理器的相似性,因此相似性是由操作系统暗中完成的。
最高程度的处理器相似性是把一个线程绑定到一个特定处理器上。绑定意味着线程将只分派到该处理器,不管其它处理器是否可用。bindprocessor 命令和 bindprocessor() 子例程将一个特定进程的线程绑定到一个特殊的处理器(参阅 bindprocessor 命令)上。显式绑定是通过 fork() 和 exec() 系统调用继承而来的。
绑定对于 CPU 密集的很少经历中断的程序是有用的。有时,它对一般的程序可能会有反作用,因为它也许会在一个 I/O 之后延迟对一个线程的重新分派,直到线程所绑定的处理器变得可用。如果线程已阻塞了一个 I/O 操作的持续时间,它的处理上下文中的大部分不太可能还保留在它所绑定的处理器的高速缓存中。如果该线程被分派到下一个可用的处理器中,它很可能会得到更好的服务。
内存和总线争用
在一个单处理器中,一些内部资源(例如内存条和 I/O 或者内存总线)的争用通常是组件使用时间的一小部分。在一个多处理器中,这些影响会变得更重要,特别是如果高速缓存一致性算法增加了对 RAM 的访问数量
SMP 性能问题
为了有效使用 SMP,当您尝试提高性能时请考虑以下问题:
工作负载并行性
SMP 系统特有的主要性能问题是工作负载的并行性,这个问题可以这样表达:“现在我们有 n 个处理器,我们如何保持它们全都有效地工作”?如果在任何指定时间,一个四路的多处理器系统中只有一个处理器在做有用功,则它比一个单处理器好不了多少。由于用来避免处理器间干扰的额外代码,它可能会更糟。
工作负载并行性是序列化的补充。在系统软件或应用程序工作负载(或者是这两者之间的交互作用)要求序列化这一点上,工作负载并行性就得遭受损失。
工作负载并行性也可以通过增加处理器相似性来更像期望的那样下降。从处理器相似性得来的提高的高速缓存效率可能会使得程序更快的完成。工作负载并行性是降低了(除非有更多可分派的线程处于可用状态),但是响应时间得到了改善。
工作负载并行性的一个组成部分,进程并行性,是指一个多线程进程在任何时候都拥有多个可分派线程的程度。
吞吐量
一个 SMP 系统的吞吐量主要由以下因素决定:
一直处于高级别的工作负载并行性。处理器在特定时间里拥有更多的可分派线程并不能补偿一些处理器在其它时间闲置的情况。
锁争用的数量。
处理器相似性的程度。
响应时间
一个处于 SMP 系统中的特定程序的响应时间取决于:
该程序的进程并行性级别。如果该程序一直拥有两个或更多可分派线程,它的响应时间很可能会在 SMP 环境里得到改善。如果程序只包含一个单独的线程,它的响应时间最多也就是和一个处于相同速度单处理器中的程序相当。
与程序其它实例或者其它使用相同锁的程序之间的锁争用的数量。
程序对处理器的相似性程度。如果程序每次都被分派到不同的处理器中,该处理器中没有它的任何高速缓存线,则该程序可能会比在一个相当的单处理器中运行得更慢。
工作负载多处理
在快速计算机上运行繁重工作负载的多程序设计操作系统给人的感觉印象是有几件事情在同时发生。事实上,很多费力的工作负载在任意给定时刻并没有大量的可分派线程,即使是当它运行在一个序列化相对来说不是大问题的单处理器系统中时。除非至少总是有与处理器一样多的可分派线程,要不然总有一个或多个处理器在一部分时间里闲置。
可分派线程的数量是系统中线程的总数
减去正在等待 I/O 的线程数,
减去正在等待共享资源的线程数,
减去正在等待另一个线程结果的线程数,
减去正对它们自己的请求睡眠的线程数。
工作负载据说是可以多处理的,从这一点来说,它不论何时都显示出与系统中的处理器数一样多的可分派线程数。请注意,这并不只意味着可分派线程的平均数量和处理器一样多。如果可分派线程数在一半时间里为零,剩余时间里是处理器计数的两倍,则可分派线程的平均数将等于处理器数,但是系统里任一给定的处理器只能在一半时间里工作。
增加工作负载的多处理性涉及到以下的一个或两个方面:
确认并解决引起线程等待的任何瓶颈
增加系统中的线程总数
这些解决方案不是独立的。假如有一个单独的、主要的系统瓶颈,增加现有的通过该瓶颈的工作负载的线程数将只会仅仅增加线程等待的比例。假如目前没有瓶颈,增加线程数可能会创建一个瓶颈。
多处理器吞吐量可伸缩性
实际工作负载并不能在 SMP 系统中极佳的伸缩。一些禁止极佳伸缩的因素如下所述:
当处理器的数量增加时,总线/开关的争用也增加。
内存争用增加(所有内存都为所有处理器共享)
随着内存不断消耗,高速缓存未命中的成本增加
高速缓存交叉式失效和读取另一个高速缓存以保持高速缓存一致性
由于更高分派率而引起的增加的高速缓存未命中(更多的进程/线程需要在系统中分派)
增加的同步指令成本
由于更大的操作系统和应用程序数据结构而增加的高速缓存未命中
为锁定/解锁而增加的操作系统和应用程序路径长度
由于等待锁而增加的操作系统和应用程序路径长度
所有这些因素都对称为工作负载的可伸缩性起作用。可伸缩性是工作负载吞吐量受益于其它处理器可用性的程度。它通常表示为一个多处理器的工作负载吞吐量由一个相当的单处理器的吞吐量所除得到的商。例如,如果一个单处理器在给定的工作负载下每秒获得 20 个请求,而一个四处理器的系统每秒获得 58 个请求,则比例因子将是 2.9。这个工作负载是高度可伸缩的。一个专门由长期运行、计算机密集的程序组成的工作负载,如果其 I/O 或其它内核活动是可忽略的,并且没有共享数据,则可以在一个四路系统中达到 3.2 到 3.9 的比例因子。然而,现实中大多数工作负载不能达到这个水平。由于可伸缩性是很难估计的,可伸缩性的假设应基于真实工作负载的评估值。
在多处理器上,两个处理器处理程序执行,但是仍然只有一个锁。为简单起见,显示了所有影响处理器 B 的锁争用。在所示的时间段里,多处理器处理 14 个命令。因此比例因子为 1.83。我们只讨论两个处理器,因为更多处理器的情况不会有什么变化。现在锁在 100% 的时间里都处于使用状态。在一个四路的多处理器中,比例因子可能是 1.83 或更小。
实际程序很少会像插图中的命令那样对称。另外,我们仅仅考虑了争用的一个尺度:锁定。如果我们把高速缓存一致性和处理器相似性的影响包括进来,无疑比例因子几乎会更小。
该示例说明了工作负载通常不能通过简单添加处理器来使它更快运行。确定和最小化线程之间的争用源也是必要的。
伸缩是与工作负载相关的。一些公布的基准程序暗示高水平的可伸缩性是容易获得的。大多数这样的基准程序是通过运行小型的 CPU 密集程序的组合而构造出来的,这些 CPU 密集程序几乎不用什么内核服务。这些基准程序的结果代表了可伸缩性的上限,而不是现实期望。
基准程序的另一个值得注意的有趣观点是通常情况下,一个单路 SMP 的运行速度会比运行操作系统的 UP 版本的同等单处理器慢(大约 5%-15%)。
多处理器响应时间
一个多处理器只能把一个独立程序的执行时间改进到让该程序可以多线程方式运行的程度。有几种方法可以让一个单独程序的某些部分实现并行执行:
显式调用 libpthreads.a 子例程(或者,在老式程序里调用 fork() 子例程)来创建多个同时运行的线程。
用一个并行化的编译器或者预处理器处理程序,该编译器或预处理器会检测到可同时执行的代码序列,并生成多个线程来并行运行这些代码。
使用一个本身是多线程的软件包。
除非使用这些技术的一种或多种,程序在一个多处理器系统中不会比在一个相当的单处理器中运行得快。事实上,由于程序会经历更多的锁定开销和在不同时间分派到不同处理器而产生的延迟,它有可能会更慢。
即使所有可用的技术都用到了,最大限度的改进也受到一个称为“Amdahl 定律”规则的限制。
举例来说,如果一个程序的 50% 的处理必须顺序执行,50% 可以并行执行,则最大的响应时间改进小于因子 2(在另一个闲置的 4 路多处理器中,该值至多为 1.6)。
线程支持
线程可看作开销低的进程。它是一个可分派实体,创建它需要的资源比创建一个进程需要的资源少。
进程由一个或多个线程组成。事实上,操作系统的早期发行版中负载的直接迁移就是继续创建和管理进程。每个新进程在创建时只带有单一的线程,该线程具有其父进程的优先级并与其它进程中的线程争用处理器。进程在执行时拥有它所使用的资源,而线程仅仅拥有它的当前状态。
当新的或修改的应用程序利用操作系统的线程支持创建额外的线程时,那些线程在该进程的上下文中创建。它们共享进程的私有段和其它资源。
进程中的一个用户线程有一个特定的争用作用域。如果争用作用域是全局的,则该线程与系统中所有其它线程一起争用处理器时间。在进程创建时产生的线程具有全局争用作用域。如果争用作用域本地的,则该线程与进程中的其它线程竞争以成为进程共享的处理器时间的接收方。
决定接下来应该运行哪个线程的算法叫调度策略。
进程和线程
进程是系统中的一个活动,它由某个命令、shell 程序或另一进程启动。
进程的属性如下:
pid
pgid
uid
gid
环境
cwd
文件描述符
信号操作
进程统计信息
nice
线程的属性如下:
堆栈
调度策略
调度优先级
暂挂信号
阻塞信号
线程特定的数据
每个进程由一个或多个线程组成。线程是一个单独的控制序列流。多个控制线程允许应用程序进行重叠操作,例如读取终端和写文件。
多个控制线程也允许应用程序同时为来自多个用户的请求服务。线程提供了这些能力而不需多个进程那样的额外开销,例如要通过 fork() 系统调用创建多个进程。
AIX 4.3.1 中引入了一个快速的 fork 例程 f_fork()。该例程对多线程应用程序非常有用,它们将立刻调用 exec() 子例程,前提是之前应先调用 fork() 子例程。fork() 子例程运行起来较慢,因为在实际派生及让其子例程运行全部子处理程序来初始化所有锁之前,它必须调用 fork 处理程序获得所有的库锁。f_fork() 子例程忽略这些处理程序并直接调用 kfork() 系统调用。Web 服务器是一个可以使用 f_fork() 子例程的很好的应用程序示例。
进程和线程的优先级
优先级管理工具处理进程的优先级。在 AIX V4 中,进程优先级只是线程优先级的前驱。当调用 fork() 子例程时,会创建一个进程和一个要在其中运行的线程。线程的优先级归结于进程。
内核为每个线程维护一个优先级值(有时称为调度优先级)。优先级值是一个正整数且与关联线程的重要性的变化方向相反。也就是说,较小的优先级值表示一个相对重要的线程。当调度程序寻找线程进行分派时,它选择具有较小优先级值的可分派线程。
线程可以有固定的优先级或不固定的优先级。优先级固定的线程的优先级值是一个常量,而优先级不固定的线程的优先级值根据用户线程最小优先级级别(常量 40)、线程的 nice 值(缺省值是 20,可随意由 nice 或 renice 命令进行设置)和其处理器使用的损失而变化。
线程的优先级可以固定成某个值,如果用 setpri() 子例程设置(固定)它们的优先级的话,它们可以具有小于 40 的优先级值。这些线程不会受到调度程序重算算法的影响。如果它们的优先级值固定且小于 40,这些线程将在可以运行所有用户线程之前运行和完成。例如,一个具有固定值 10 的线程将在具有固定值 15 的线程之前运行。
用户可以应用 nice 命令使线程的不固定优先级变低。系统管理员可将一个负的 nice 值应用给线程,这样就给了它较好的优先级。
下图显示了一些可以更改优先级值的方法。
图 6. 如何确定优先级值. 插图显示了如何能在执行过程中或应用了 nice 命令之后更改线程调度优先级值。优先级值越小,线程优先级越高。开始时,nice 值缺省为 20 而基本优先级缺省为 40。在执行一些操作及处理器损失后,nice 的值仍为 20 且基本优先级仍为 40。在运行 renice --5 命令后及使用和以前相同的处理器的情况下,nice 值现在是 15 而基本优先级仍为 40。在以 50 的值发出子例程 setpri() 之后,固定优先级现在是 50 而 nice 值和处理器的使用无关。
线程的 nice 值在创建线程时设置并且在线程的整个生命期中都是常量,除非用户通过 renice 命令或 setpri()、setpriority()、thread_setsched() 或 nice() 系统调用明确更改了它的值。
处理器损失是一个整数,它通过线程最近的处理器使用来计算。如果每次在一个 10 ms 的时钟滴答结束时线程受处理器控制,则最近的处理器使用值近似加 1,直到达到最大值 120。每个滴答的实际优先级损失随着 nice 的值增加。所有线程的最近处理器使用值每秒重算一次。
结果如下:
不固定优先级的线程的优先级随着其最近处理器使用的增加而变低,反之亦然。这暗示一般来讲,某线程最近被分配的时间片越多,则它被分配下一个时间片的可能性越小。
不固定优先级的线程的优先级随着其 nice 值的增加而变低,反之亦然。
注:
使用多处理器运行队列及其负载平衡机制以后,nice 或 renice 的值对线程的优先级可能没有预期的影响,因为较低优先级的运行时间可能等于或大于较高优先级的运行时间。要求 nice 或 renice 产生预期效果的线程应该放在全局运行队列中。
可以使用命令 ps 显示进程的优先级值、nice 值和短期的处理器使用值。
请参阅『处理器的控制争用』中对使用 nice 和 renice 命令的更详细的讨论。
请参阅『调谐线程优先级值的计算』,里面有处理器损失计算和最近处理器使用值衰减的详细信息。
优先级机制也用于 AIX 工作负载管理器中来加强处理器资源管理。因为在工作负载管理器下分类的线程具有的优先级由工作负载管理器管理,它们可能与没有在工作负载管理器下分类的线程具有不同的优先级行为。
线程的调度策略
下面是线程调度策略的可能值:
SCHED_FIFO
这种策略的线程被调度后,它会一直运行到结束,除非被阻塞或有一个较高优先级的线程可分派,它将自愿服从处理器的控制。只有固定优先级的线程才能有 SCHED_FIFO 调度策略。
SCHED_RR
当一个 SCHED_RR 线程在时间片的末尾有控制权时,它将移动到和它具有相同优先级的可分派线程队列的尾部。只有固定优先级的线程才能有 SCHED_RR 的调度策略。
SCHED_OTHER
这个策略在“POSIX 标准 1003.4a”中作为定义的执行程序进行定义。在每个时钟中断时重算运行线程的优先级值意味着该线程可能失去控制权,因为它的优先级值已经超过了另一可分派线程的优先级值。
SCHED_FIFO2
该策略和 SCHED_FIFO 相同,只是它允许一个仅睡眠了很短时间的线程在被唤醒时可放置在其运行队列的头部。这个时间周期是相似性限制(可用 schedtune -a 进行调节)。该策略仅可用于 AIX 4.3.3 及其后续版本。
SCHED_FIFO3
调度策略设置成 SCHED_FIFO3 的线程总是放置在运行队列的头部。为了防止属于 SCHED_FIFO2 调度策略的线程放置在 SCHED_FIFO3 之前,当 SCHED_FIFO3 线程入队列时更改运行队列参数,这样属于 SCHED_FIFO2 的线程就不满足使其能够加入运行队列头部时必须满足的标准。该策略仅可用于 AIX 4.3.3 及其后续版本。
SCHED_FIFO4
只要优先级值相差 1,较高优先级的 SCHED_FIFO4 调度类线程就不会抢占当前正运行的低优先级线程。缺省行为是当前运行于某给定 CPU 的低优先级线程被有资格在同一 CPU 上运行的高优先级线程抢占。该策略仅可用于 AIX 5L V5100-01 + APAR IY22854 及其后续版本。
调度策略可用系统调用 thread_setsched() 进行设置并且仅对调用线程有效。然而,通过指定进程标识发出 setpri() 调用可将线程设置成 SCHED_RR 调度策略;setpri() 的调用者和 setpri() 的目标不必匹配。
只有那些具有 root 权限的进程可以发出 setpri() 系统调用。只有那些具有 root 权限的线程可将调度策略更改成任何 SCHED_FIFO 选项或 SCHED_RR。如果调度策略是 SCHED_OTHER,则优先级参数被 thread_setsched()子例程忽略。
线程的主要优点是适用于当前由多个异步进程组成的应用程序。这些应用程序可通过转变成多线程结构使得系统中有较轻的负载。
调度程序运行队列
调度程序维护一个所有就绪等待分派的线程的运行队列。
给定优先级的所有可分派线程在运行队列中占有一定的位置。
调度程序的基本可分派实体是线程。AIX 5.1 维护 256 个运行队列(128 个在 AIX 4.3 及以前的发行版中)。在 AIX 5.1 中,运行队列与每个线程优先级字段可能值的范围(从 0 到 255)直接相关。这个方法使调度程序更容易确定哪个线程最先运行。调度程序无需搜索一个完整的运行队列,只需要考虑一个掩码,该掩码的某一位启用后可表示在相应的运行队列中存在就绪等待运行的线程。
线程的优先级值快速而频繁地变更。持续的变动归因于调度程序重算优先级的方法。然而,这并不适用于固定优先级的线程。
从 AIX 4.3.3 开始,每个处理器都有自己的运行队列。性能工具中报告的运行队列值将是每个运行队列中所有线程的总和。让每个处理器都有自己的运行队列可节省分派锁的开销并改善总体的处理器相似性。线程通常会更加趋向于留在同一处理器中。如果因为另一处理器上的事件使某线程变得可运行且有空闲的处理器的话,即使不同于最近可运行线程曾经运行过的处理器,该线程也只会立即被分派。在可以检查处理器状态(例如在该线程的处理器上的中断)之前不会出现抢占。
在具有多个运行队列的多处理器系统中,可能出现瞬间的优先级倒置。在任何一个时间点都可能出现这种情况:某个运行队列能使若干线程具有的优先级比另一运行队列更有利。AIX 有一些机制可以随着时间的推移来进行优先级平衡,但是如果要求严格的优先级(例如,对于实时应用程序)可用一个叫做 RT_GRQ 的环境变量,如果将它设置成 ON,将导致该线程位于一个全局运行队列中。在那种情况下,将搜索全局运行队列来察看哪个线程具有最佳优先级。这可以改善中断驱动线程的性能。如果将 schedtune -F 设置成 1,以固定优先级运行的线程就放置在全局运行队列中。
运行队列中的线程平均数可在命令 vmstat 输出的第一列中看到。如果用处理器数去除这个数,结果是每个处理器上可运行线程的平均数。如果这个值大于 1,这些线程必须等待轮到它们使用处理器(这个数越大,性能延迟可能越明显)。
当某线程移到运行队列的末端时(例如,当线程在时间片的末尾拥有控制权时),它会移动到具有相同优先级值的队列中最后一个线程之后的位置上。
调度程序处理器时间片
处理器时间片是调度程序转换到另一个具有相同优先级的线程之前,一个 SCHED_RR 线程能获得的时间的总和。可以使用命令 schedtune 的选项 -t 在时间片上以 10 毫秒的增量来增加时钟滴答数(参阅『用 schedtune 命令修改调度程序时间片』)。
注:
时间片并不是保证的处理器时间量。它是一个线程在面临由另一线程取代的可能性之前可以受控的最长时间。在控制时间达到完整时间片之前有很多方法可使线程失去处理器的控制。
方式转换
用户进程在需要访问系统资源时会经历一个方式转换。这通过系统调用接口或诸如缺页故障这样的中断来实现。有两种方式:
用户方式
内核方式
花在用户方式(应用程序和共享库)下的处理器时间作为用户时间在一些命令的输出中反映出来,例如,vmstat、iostat 和 sar 命令。花在内核方式下的处理器时间作为系统时间在这些命令的输出中反映出来。
用户方式
在用户保护域中执行的程序是用户进程。在这种保护域中执行的代码以用户执行方式执行,且具有下列访问:
读/写访问进程专用区域中的用户数据
读访问用户文本和共享文本区域
使用共享内存功能访问共享数据区域
在用户保护域中执行的程序不能访问内核或内核数据段,除非通过使用系统调用间接访问。在该保护域中的程序只能影响自身的执行环境并在进程或非特权状态下执行。
内核方式
在内核保护域中执行的程序包含中断处理程序、内核进程、基内核和内核扩展(设备驱动程序、系统调用和文件系统)。这个保护域暗示以内核执行方式执行代码,具有下列访问:
读/写访问全局内核地址空间
在进程中执行时读/写访问进程区域中的内核数据
内核服务必须用来访问进程地址空间中的用户数据。
在该保护域中执行的程序会影响所有程序的执行环境,因为它们具有下列特征:
它们可访问全局系统数据
它们可使用内核服务
它们免受所有安全性约束
它们执行于处理器特权状态下。
方式转换
用户方式的进程使用的系统调用允许通过用户方式调用内核函数。直接或间接地调用系统调用来访问的函数一般由程序设计库提供,它们提供对操作系统函数的访问。
方式转换应该不同于在命令 vmstat(cs 列)和 sar(cswch/s)的输出中所看到的上下文转换。在当前运行的线程不同于该处理器上先前运行的线程时会出现上下文转换。
当下列任一情况出现时调度程序执行上下文转换:
线程必须等候某个资源(自愿),比如磁盘 I/O、网络 I/O、睡眠或锁
一个较高优先级线程被唤醒(非自愿)
线程已经用完了它的时间片(通常是 10 ms)。
上下文转换的时间、系统调用、设备中断、NFS I/O 和内核中任何其它活动都看作系统时间。
2004-8-23 19:48:39 鲜花(0) 鸡蛋(0)
snappyboy
等级:论坛游民
文章:123
积分:283
注册:2003-10-20
第2楼
多处理介绍
无论何时,单处理器芯片的运行速度都存在着技术上的限制。如果单处理器无法令人满意地处理系统的工作负载,一种响应是使用多处理器来解决这个问题。
这种响应是否成功不仅仅取决于系统设计者的技术熟练程度,还取决于工作负载是否服从多处理控制。就人的任务而言,如果任务是应答一个免费电话号码的呼叫,增加人员也许不失为一个好主意,但是假如任务是开车的话,这种做法是否有效就值得怀疑了。
如果建议从一个单处理器系统迁移到一个多处理器系统的目标是为了改进性能,则下列条件必须成立:
工作负载受处理器限制并且已经使得它的单处理器系统饱和。
工作负载包含多种处理器密集的元素,例如事务或者复杂计算,这些操作可以同时并且各自独立地执行。
现有的单处理器不能升级,也不能由另一个能量充足的单处理器代替。
虽然正常情况下不变的单线程应用程序在某个多处理器环境中能正确运行,但它们的性能常常会有意外的变化。迁移到多处理器可以改善系统的吞吐量,并能改进复杂的多线程应用程序的执行时间,但是很少能改进个别的单线程命令的响应时间。
要从一个多处理器系统获得最佳性能,需要对多处理器系统独有的操作系统和硬件执行动态有所了解
对称多处理器(SMP)概念和体系结构
对于增加系统复杂性的任何变化,为了获得令人满意的操作和性能,使用多处理器产生了一些设计时必须引起注意的事项。额外的复杂性使得软/硬件权衡的作用域更大,并且比在单处理器系统中更需要软/硬件的密切配合。设计响应和权衡的不同组合使得多处理器系统的体系结构更加多样化。
这一节描述了多处理器系统的主要设计注意事项和这些事项的硬件响应。
多处理的类型
有几种多处理(MP)系统,如下所述:
非共享 MP(纯群集)
每个处理器都是一个完全独立的机器,运行操作系统的一个副本。处理器之间没有共享的部分(每一个都有自己的内存,高速缓存和磁盘),但是它们是互联的。通过 LAN 连接时,处理器之间是松散耦合的。而通过转换器连接时,处理器之间是紧密耦合的。处理器之间的通信是通过消息传送来实现的。
这样一个系统的优点是它具有很好的可伸缩性和高可用性。而缺点则是该系统是一个不为人熟悉的编程模型(消息传送)。
共享磁盘 MP
处理器拥有自身的内存和高速缓存。处理器并行运行并共享磁盘。每个处理器都运行操作系统的一份副本,并且处理器之间是松散耦合的(通过 LAN 连接)。处理器之间的通信是通过信息传送实现的。
共享磁盘的优点是保留了熟悉的编程模型的一部分(磁盘数据是可寻址和连续的,而内存则不是),而且与共享内存的系统相比,这种系统更容易实现高可用性。缺点是由于在对共享数据进行物理和逻辑访问时存在瓶颈,它的可伸缩性受到限制。
共享内存群集(SMC)
一个共享内存群集中的所有处理器有自己的资源(主存储器、磁盘和 I/0),并且每个处理器运行一份操作系统的副本。处理器之间是紧密耦合的(通过一个转换器连接)。处理器之间的通信是通过共享内存实现的。
共享内存 MP
所有处理器通过一条高速总线或者一个转换器在同一机器中紧密耦合。处理器共享同样的全局内存、磁盘和 I/0 设备。只有一份操作系统的副本跨所有处理器运行,并且操作系统必须设计为能利用这种体系结构(多线程操作系统)。
SMP 有几个优点:
它们是增加吞吐量的一种划算的方法。
由于操作系统由所有处理器共享,它们提供了一个单独的系统映像(易于管理)。
它们对一个单独的问题应用多处理器(并行编程)。
负载平衡是由操作系统实现的。
这种单处理器(UP)编程模型可用于一个 SMP 中。
对于共享数据来说,它们是可伸缩的。
所有数据可由所有处理器寻址,并且由硬件监视逻辑保持连续性。
由于通信经由全局共享内存执行,在处理器之间通信不必使用消息传送库。
更多能量的需求可通过向系统添加更多处理器来解决。然而,在一个 SMP 系统里添加更多处理器时,您必须设置关于性能增强的现实期望值。
现在越来越多的应用程序和工具都可以使用。大多数 UP 应用程序可以在 SMP 体系结构中运行或者被移植到 SMP 体系结构中。
SMP 系统有一些局限性,如下所述:
由于高速缓存相关性、锁定机制、共享对象和其它问题,可伸缩性受到限制。
需要新技术来利用多处理器,例如线程编程和设备驱动程序编程。
并行化应用程序
有两种方法可以在一个 SMP 中使应用程序并行化,如下所述:
传统方法是把应用程序分解为多个进程。这些进程使用进程间通信(IPC)方法进行通信,例如管道、信号量或者共享内存。必须能够阻塞进程使其等待事件的发生(例如来自其它进程的消息),并且进程必须用类似锁的东西协调对共享对象的访问。
另一种方法是使用面向 UNIX(POSIX)线程的可移植操作系统接口。线程和进程一样存在协调的问题,并有类似的处理机制。因此一个单独的进程可以同时有很多线程运行在不同的处理器上。协调这些线程并且使得对共享数据的访问序列化是开发者的责任。
在并行化一个应用程序的时候,考虑线程和进程两者各自的优势并且决定使用哪种方法。线程可能比进程快,并且它对内存的共享也比较容易。另一方面,进程的实现更容易分布到多个机器或者群集中。如果一个应用程序需要创建或者删除新实例,则线程会更快(在派生进程中开销更大)。就其它功能而言,线程的开销和进程差不多。
数据序列化
任何可由多个线程读或写的存储元素在程序运行中都可能改变。通常,这对多程序设计环境以及多处理环境都是成立的,但是多处理器的出现以两种方式增加了这种注意事项的作用域和重要性。
多处理器和线程的支持使得编写在线程中共享数据的应用程序具有吸引力和更容易。
内核再也不能通过简单地禁用中断来解决序列化问题。
注:
为了避免产生严重问题,共享数据的程序必须安排好,以对数据进行串行访问,而不是并行访问。在一个程序更新一个共享数据项之前,必须确保没有其它程序(包括它本身在另一个线程里运行的另一副本)会改变该项。通常读操作可以并行地执行。
用来避免程序互相干扰的主要机制是锁。锁是一种抽象概念,它代表对访问一个或多个数据项的许可。锁定和解锁的请求是原子级的;也就是说,它们的实现方式为:其结果既不受中断也不受多处理器访问的影响。所有访问一个共享数据项的程序在处理它之前必须先获得与它相关的锁。如果这个锁已经由另一个程序(或者另一个运行同一程序的线程)占有,则请求的程序必须推迟访问,直到锁变得可用。
除了等待锁所花的时间之外,序列化也增加了一个线程成为不可分派线程所花的时间。当线程不可分派时,其它线程很可能会使这个不可分派线程的高速缓存线路被替换,这将导致线程最后获得锁并被分派时内存等待时间成本增加。
操作系统的内核包含很多共享的数据项,所以它必须在内部进行序列化。因此序列化延迟甚至可能在一个不与其它程序共享数据的应用程序中发生,因为由该程序使用的内核服务必须序列化共享的内核数据。
锁的类型
开放软件基金会/1(OSF/1)1.1 的锁定方法被作为一个 AIX 的多处理器锁定功能模型使用。然而,由于系统是可抢占和可调页的,对 OSF/1 1.1 锁定模型增加了一些特征。简单和复杂的锁都是可抢占的。一个线程在尝试获得一个忙状态的简单锁时也可以睡眠,如果锁的所有者当前并不在运行的话。另外,当一个处理器在一个简单锁上自旋一段时间(这段时间是一个全系统的变量)以后,这个简单锁会变成睡眠锁。
锁粒度
一个在多处理器环境中工作的程序员必须决定对共享数据一定要创建多少单独的锁。如果只有一个锁来序列化整个共享数据项的集合,则相比之下很可能出现锁争用。广泛使用锁的存在给系统吞吐量加了上限。
如果每一个不同的数据项都有自己的锁,则两个线程争用这个锁的概率相对来说就比较低。然而,每一个附加的锁定和解锁调用都会消耗处理器时间,并且多个锁的存在使得可能发生死锁。最简单的死锁情况如下图所示,其中线程 1 拥有锁 A 并且正在等待锁 B。同时,线程 2 拥有锁 B 并且正在等待锁 A。这两个程序都永远用不上会打破死锁的 unlock() 调用。通常对死锁的预防措施是建立一个协议,根据该协议,所有使用一个指定的锁集合的程序必须始终按照完全相同的顺序获得它们。
根据排队理论,一个资源闲置得越少,要得到它的平均等待时间就越长。这种关系是非线性的;如果锁的个数翻倍,平均等待这个锁的时间就比原来的两倍还要多。
减少对锁的等待时间的最有效方法是减少这个锁所保护的范围大小。下面是一些准则:
减少对任何锁的请求频率。
只锁定访问共享数据的代码,而不是一个组件的所有代码(这将减少锁的持有时间)。
只锁定特定的数据项或结构,而不是整个例程。
始终将锁和特定的数据项或结构关联起来,而不是和例程关联。
对于大的数据结构,为结构的每一元素选择一个锁,而不是为整个结构选择一个锁。
当持有一个锁时,从不执行同步 I/O 或者任何其它阻塞活动。
如果您对您组件中的同一数据有多个访问,请试着把它们移到一起,以便它们可以包含在一个锁定 — 解锁操作中。
避免双唤醒的情况。如果您在一个锁下修改了一些数据,并且不得不通知某人您做了这件事,则在公布唤醒之前请释放该锁。
如果必须同时持有两个锁,则最后请求那个最忙的锁。
另一方面,过细粒度将增加对锁的请求和释放的频率,因而会增加额外的指令。您必须在过细和过粗粒度之间找到平衡。最佳粒度不得不通过试验和错误找到,这也是一个 MP 系统中的最大挑战之一。
锁定开销
请求锁,等待锁和释放锁在几方面增加了处理开销:
一个支持多处理的程序总是进行相同的锁定和解锁处理,即使它是在一个单处理器里运行或者是一个多处理器系统里对于这个锁的唯一使用者。
当一个线程请求一个由另一线程持有的锁时,发出请求的线程可能会自旋一会或者置于睡眠状态,如果可能的话,会分派另一个线程。这会消耗处理器时间。
广泛使用锁的存在给系统吞吐量加了一个上限。例如,如果一个给定的程序花 20% 的执行时间来持有一个互斥锁,这个程序最多只有五个实例能同时运行,不管系统里有多少个处理器。事实上,即使只有五个实例,它们也很可能永远不会精确同步,以免互相等待。(参阅『多处理器吞吐量可伸缩性』)。
等待锁
当一个线程需要另一个线程已拥有的锁时,该线程被阻塞并且必须等到锁变得可用为止。有两种不同的等待方式:
对于只被持有很短时间的锁来说,自旋锁是很适合的。它允许等待中的线程保持其处理器重复检查某个死循环(自旋)里的锁定位,直到锁变得可用。自旋导致 CPU 时间(内核或内核扩展锁定的时间)增加。
睡眠锁适合于可能会被持有较长时间的锁。线程会睡眠到锁可用为止,当锁变得可用后,它会被放回到运行队列里。睡眠导致更多的闲置时间。
等待总会降低系统性能。如果使用自旋锁,处理器是繁忙的,但是它不是在做有用功(不是在为吞吐量出力)。如果使用睡眠锁,会导致上下文切换和分派的开销以及随之而来的高速缓存未命中的增加。
操作系统开发者们可以在两种类型的锁之间选择:在等待锁变得可用时允许进程自旋和睡眠的互斥简单锁,和在等待锁变得可用时可以自旋和阻塞进程的复杂读写锁。
一些约定管理着使用锁的规则。不管是硬件还是软件都没有实施或校验的机制。尽管使用锁已经使得 AIX V4 是“MP 安全”的,开发者们还是有责任定义和实现一个合适的锁定策略来保护他们自己的全局数据。
高速缓存一致性
在设计多处理器时,工程师们对保证高速缓存的一致性给予了相当多的注意。他们取得了成功;但是高速缓存一致性是以性能为代价的。我们需要理解这个遭受攻击的问题:
如果每个处理器都有一个反映内存不同部分状态的高速缓存,就可能会有两个或更多高速缓存拥有相同线路的副本。也有可能是一个给定的线路会包含不止一个可锁定的数据项。如果两个线程对那些数据项作了适当的序列化更改,结果可能是两个高速缓存都以不同的,错误版本的内存线路而告终。换句话说,系统的状态不再一致,因为系统包含了应该是一个特定内存区域的内容的两个不同版本。
对高速缓存一致性问题的解决方案通常包括在线路修改之后,除了一条线路以外,使所有重复线路都失效。尽管硬件使用监视逻辑使线路失效,没有任何软件干预的话,任何高速缓存线路已经失效的处理器由于随之而来的延迟,将会在下一次寻址到该线路时出现高速缓存未命中。
监视是用来解决高速缓存一致性问题的逻辑。处理器中的监视逻辑每次修改了其高速缓存中的一个字后,会在总线上广播一条消息。监视逻辑也在总线上监视,寻找来自其它处理器的这种消息。
当一个处理器检测到另一个处理器已经更改了存在于它本身高速缓存内的一个地址的值时,监视逻辑会使得它自己的高速缓存中的该项失效。这被称为交叉式失效。交叉式失效提醒处理器高速缓存中的值已经无效了,处理器必须在别处(内存或其它高速缓存)寻找正确的值。由于交叉式失效增加了高速缓存未命中率,而监视协议增加了总线流量,因而解决高速缓存的一致性问题会降低所有 SMP 的性能和可伸缩性。
处理器相似性和绑定
如果一个线程中断后又重新分派到同一个处理器中,该处理器的高速缓存也许仍含有属于该线程的线路。如果该线程被分派到不同的处理器,它将很可能经历一系列高速缓存未命中,直到它的高速缓存工作集从 RAM 或其它处理器的高速缓存中检索到。另一方面,如果一个可分派的线程必须等到它先前在其中运行的处理器可用,该线程也许会经历一个更长的延迟。
处理器相似性是指将一个线程分派到先前运行它的处理器之上的概率。对处理器相似性的强调程度应随线程的高速缓存工作集大小直接变化,而随自它上一次分派以来的时间长短反向变化。AIX V4 分派器强制对处理器的相似性,因此相似性是由操作系统暗中完成的。
最高程度的处理器相似性是把一个线程绑定到一个特定处理器上。绑定意味着线程将只分派到该处理器,不管其它处理器是否可用。bindprocessor 命令和 bindprocessor() 子例程将一个特定进程的线程绑定到一个特殊的处理器(参阅 bindprocessor 命令)上。显式绑定是通过 fork() 和 exec() 系统调用继承而来的。
绑定对于 CPU 密集的很少经历中断的程序是有用的。有时,它对一般的程序可能会有反作用,因为它也许会在一个 I/O 之后延迟对一个线程的重新分派,直到线程所绑定的处理器变得可用。如果线程已阻塞了一个 I/O 操作的持续时间,它的处理上下文中的大部分不太可能还保留在它所绑定的处理器的高速缓存中。如果该线程被分派到下一个可用的处理器中,它很可能会得到更好的服务。
内存和总线争用
在一个单处理器中,一些内部资源(例如内存条和 I/O 或者内存总线)的争用通常是组件使用时间的一小部分。在一个多处理器中,这些影响会变得更重要,特别是如果高速缓存一致性算法增加了对 RAM 的访问数量
SMP 性能问题
为了有效使用 SMP,当您尝试提高性能时请考虑以下问题:
工作负载并行性
SMP 系统特有的主要性能问题是工作负载的并行性,这个问题可以这样表达:“现在我们有 n 个处理器,我们如何保持它们全都有效地工作”?如果在任何指定时间,一个四路的多处理器系统中只有一个处理器在做有用功,则它比一个单处理器好不了多少。由于用来避免处理器间干扰的额外代码,它可能会更糟。
工作负载并行性是序列化的补充。在系统软件或应用程序工作负载(或者是这两者之间的交互作用)要求序列化这一点上,工作负载并行性就得遭受损失。
工作负载并行性也可以通过增加处理器相似性来更像期望的那样下降。从处理器相似性得来的提高的高速缓存效率可能会使得程序更快的完成。工作负载并行性是降低了(除非有更多可分派的线程处于可用状态),但是响应时间得到了改善。
工作负载并行性的一个组成部分,进程并行性,是指一个多线程进程在任何时候都拥有多个可分派线程的程度。
吞吐量
一个 SMP 系统的吞吐量主要由以下因素决定:
一直处于高级别的工作负载并行性。处理器在特定时间里拥有更多的可分派线程并不能补偿一些处理器在其它时间闲置的情况。
锁争用的数量。
处理器相似性的程度。
响应时间
一个处于 SMP 系统中的特定程序的响应时间取决于:
该程序的进程并行性级别。如果该程序一直拥有两个或更多可分派线程,它的响应时间很可能会在 SMP 环境里得到改善。如果程序只包含一个单独的线程,它的响应时间最多也就是和一个处于相同速度单处理器中的程序相当。
与程序其它实例或者其它使用相同锁的程序之间的锁争用的数量。
程序对处理器的相似性程度。如果程序每次都被分派到不同的处理器中,该处理器中没有它的任何高速缓存线,则该程序可能会比在一个相当的单处理器中运行得更慢。
工作负载多处理
在快速计算机上运行繁重工作负载的多程序设计操作系统给人的感觉印象是有几件事情在同时发生。事实上,很多费力的工作负载在任意给定时刻并没有大量的可分派线程,即使是当它运行在一个序列化相对来说不是大问题的单处理器系统中时。除非至少总是有与处理器一样多的可分派线程,要不然总有一个或多个处理器在一部分时间里闲置。
可分派线程的数量是系统中线程的总数
减去正在等待 I/O 的线程数,
减去正在等待共享资源的线程数,
减去正在等待另一个线程结果的线程数,
减去正对它们自己的请求睡眠的线程数。
工作负载据说是可以多处理的,从这一点来说,它不论何时都显示出与系统中的处理器数一样多的可分派线程数。请注意,这并不只意味着可分派线程的平均数量和处理器一样多。如果可分派线程数在一半时间里为零,剩余时间里是处理器计数的两倍,则可分派线程的平均数将等于处理器数,但是系统里任一给定的处理器只能在一半时间里工作。
增加工作负载的多处理性涉及到以下的一个或两个方面:
确认并解决引起线程等待的任何瓶颈
增加系统中的线程总数
这些解决方案不是独立的。假如有一个单独的、主要的系统瓶颈,增加现有的通过该瓶颈的工作负载的线程数将只会仅仅增加线程等待的比例。假如目前没有瓶颈,增加线程数可能会创建一个瓶颈。
多处理器吞吐量可伸缩性
实际工作负载并不能在 SMP 系统中极佳的伸缩。一些禁止极佳伸缩的因素如下所述:
当处理器的数量增加时,总线/开关的争用也增加。
内存争用增加(所有内存都为所有处理器共享)
随着内存不断消耗,高速缓存未命中的成本增加
高速缓存交叉式失效和读取另一个高速缓存以保持高速缓存一致性
由于更高分派率而引起的增加的高速缓存未命中(更多的进程/线程需要在系统中分派)
增加的同步指令成本
由于更大的操作系统和应用程序数据结构而增加的高速缓存未命中
为锁定/解锁而增加的操作系统和应用程序路径长度
由于等待锁而增加的操作系统和应用程序路径长度
所有这些因素都对称为工作负载的可伸缩性起作用。可伸缩性是工作负载吞吐量受益于其它处理器可用性的程度。它通常表示为一个多处理器的工作负载吞吐量由一个相当的单处理器的吞吐量所除得到的商。例如,如果一个单处理器在给定的工作负载下每秒获得 20 个请求,而一个四处理器的系统每秒获得 58 个请求,则比例因子将是 2.9。这个工作负载是高度可伸缩的。一个专门由长期运行、计算机密集的程序组成的工作负载,如果其 I/O 或其它内核活动是可忽略的,并且没有共享数据,则可以在一个四路系统中达到 3.2 到 3.9 的比例因子。然而,现实中大多数工作负载不能达到这个水平。由于可伸缩性是很难估计的,可伸缩性的假设应基于真实工作负载的评估值。
在多处理器上,两个处理器处理程序执行,但是仍然只有一个锁。为简单起见,显示了所有影响处理器 B 的锁争用。在所示的时间段里,多处理器处理 14 个命令。因此比例因子为 1.83。我们只讨论两个处理器,因为更多处理器的情况不会有什么变化。现在锁在 100% 的时间里都处于使用状态。在一个四路的多处理器中,比例因子可能是 1.83 或更小。
实际程序很少会像插图中的命令那样对称。另外,我们仅仅考虑了争用的一个尺度:锁定。如果我们把高速缓存一致性和处理器相似性的影响包括进来,无疑比例因子几乎会更小。
该示例说明了工作负载通常不能通过简单添加处理器来使它更快运行。确定和最小化线程之间的争用源也是必要的。
伸缩是与工作负载相关的。一些公布的基准程序暗示高水平的可伸缩性是容易获得的。大多数这样的基准程序是通过运行小型的 CPU 密集程序的组合而构造出来的,这些 CPU 密集程序几乎不用什么内核服务。这些基准程序的结果代表了可伸缩性的上限,而不是现实期望。
基准程序的另一个值得注意的有趣观点是通常情况下,一个单路 SMP 的运行速度会比运行操作系统的 UP 版本的同等单处理器慢(大约 5%-15%)。
多处理器响应时间
一个多处理器只能把一个独立程序的执行时间改进到让该程序可以多线程方式运行的程度。有几种方法可以让一个单独程序的某些部分实现并行执行:
显式调用 libpthreads.a 子例程(或者,在老式程序里调用 fork() 子例程)来创建多个同时运行的线程。
用一个并行化的编译器或者预处理器处理程序,该编译器或预处理器会检测到可同时执行的代码序列,并生成多个线程来并行运行这些代码。
使用一个本身是多线程的软件包。
除非使用这些技术的一种或多种,程序在一个多处理器系统中不会比在一个相当的单处理器中运行得快。事实上,由于程序会经历更多的锁定开销和在不同时间分派到不同处理器而产生的延迟,它有可能会更慢。
即使所有可用的技术都用到了,最大限度的改进也受到一个称为“Amdahl 定律”规则的限制。
举例来说,如果一个程序的 50% 的处理必须顺序执行,50% 可以并行执行,则最大的响应时间改进小于因子 2(在另一个闲置的 4 路多处理器中,该值至多为 1.6)。
延伸阅读
文章来源于领测软件测试网 https://www.ltesting.net/