Solaris 操作系统联网 -- 揭秘Sunay Tripathi(高级工程师,Solaris 核心技术组),2006 年 1 月 摘要:本文讨论了 Solaris 10 操作系统联网改进以及以前发行版中联网的演变过程。主题包括 TCP、UDP、IP、设备驱动程序框架以及性能调节。 目录
1.0 背景知识Solaris 1.x 操作系统联网堆栈是 BSD 衍生版本,它与 BSD Reno 实现非常相似。BSD 堆栈非常适于低端计算机,但 Sun 希望满足低端客户以及企业客户的需要,因而将 Solaris 操作系统迁移到 AT&T SVR4 体系结构,它最终变为 Solaris 2.x 平台。 对于 Solaris 2.x 操作系统,联网堆栈发生了很大改变,由 BSD 风格的堆栈转变为基于 STREAMS 的堆栈。STREAMS 框架提供了一种简便的消息传送接口,使 STREAMS 模块之间可以灵活地进行交互。通过使用 STREAMS 的内部和外部边界,模块编写者可以提供互斥功能,而不会使实现变得复杂。建立 STREAM 的成本很高,但每秒建立的连接数并不是一个重要的判别标准,连接通常会长时间有效。如果连接的有效期更长(NFS、FTP 等),则建立新流的成本就可以在连接周期内进行平摊。 在 90 年代末,服务器非常依赖 SMP,并需要运行大量的 CPU。随着中到高端计算机变得越来越以 NUMA 为中心,在 CPU 之间切换处理的成本变得很高。由于 STREAMS 在设计上并不具备任何 CPU 相似性,因而特定连接的数据包将发送到不同的 CPU。很明显,Solaris 产品需要弃用 STREAMS 体系结构。 在 90 年代末,万维网也实现了飞速发展。处理能力的增强意味着可以建立大量有效期较短的连接,从而使连接建立时间变得同样重要。通过 Solaris 10 平台,联网堆栈经历了又一次转变,其中核心组件(如套接字层、TCP、UPD、IP 和设备驱动程序)使用 IP 分类器和串行化队列来缩短连接建立时间、提高可伸缩性以及降低数据包处理成本。仍然使用 STREAMS 体系结构来提供 ISV 实现附加功能所需的灵活性。 2.0 Solaris 10 操作系统的堆栈让我们看一下新框架及其关键组件的工作方式。 在 Solaris 10 操作系统之前,堆栈使用 STREAMS 边界和内核适应性互斥锁进行多线程处理。TCP 使用 STREAMS QPAIR 边界;UDP 使用 STREAMS QPAIR 和 PUTSHARED;IP 使用 PERMOD 边界、PUTSHARED 以及受互斥锁保护的各种 TCP、UDP 和 IP 全局数据结构。此堆栈是由用户端线程(执行各种系统调用、网络设备驱动程序读取端中断或设备驱动程序工作线程)和 STREAMS 框架工作线程执行的。当前边界提供了针对每个模块、每个协议的堆栈层或水平边界。在 Solaris 9 和以前版本中,基于 STREAMS 的堆栈在单个协议层周围提供水平边界,这通常会导致在多个 CPU 上处理数据包,并且由排列在协议层之间的多个线程进行处理。这导致了大量的上下文切换,特定于连接的数据结构的数据局域性较差。 "FireEngine" 方法是将所有协议层合并为一个完全多线程的 STREAMS 模块。在合并的模块内,没有使用针对每个数据结构的锁,而是使用针对每个 CPU 的同步机制(称为“垂直边界”)。“垂直边界”是使用称为 "squeue" 的串行化队列抽象层实现的。每个 squeue 绑定到一个 CPU 上,而每个连接又绑定到一个 squeue 上,这就可以提供特定于连接的数据结构所需的任何同步和互斥功能。 只要有数据包到达 IP,就会立即使用 IP 连接分类器在边界外部完成连接(或上下文)的查找,以获取入站数据包。连接结构是基于分类来识别的。由于查找是在边界外部进行的,因此,我们可以在初始化连接后将连接绑定到垂直边界或 "squeue" 的实例上,并在连接所绑定到的 squeue 上处理该连接的所有数据包,从而保持更好的缓存局域性。后面部分将更详细地介绍垂直边界和分类器。分类器也会变为数据库,用于存储所有入站和出站数据包所需的一系列函数调用。这样,Solaris 联网堆栈即可由当前的消息传送接口更改为 BSD 风格的函数调用接口。为处理连接数据包而动态创建的函数字符串(事件列表)是最终新框架的基础,其他模块和第三方高性能模块也可以参与到此框架中。
Squeue 可确保在任何给定时间只有一个线程能够处理给定连接,从而在合并的 TCP/IP 模块中串行化多个线程对 TCP 连接结构的访问(从读取和写入端)。这与 STREAMS QPAIR 边界很类似,但它保护从 IP 到 垂直边界或 squeue 本身只为数据结构提供数据包串行化和互斥功能,但通过创建针对每个 CPU 的边界并将连接绑定到 CPU 处理中断所附加的实例上,我们可以提供更好的数据局域性。 我们可以在创建针对每个连接的边界和针对每个 CPU 的边界之间进行选择,即每个连接或每个 CPU 一个实例。针对每个连接的边界和线程争用所产生的开销会使性能降低,因此我们选择针对每个 CPU 的实例。对于针对每个 CPU 的实例,我们可以选择将连接结构排入队列以进行处理,或者只将数据包本身排入队列,而将连接结构指针存储在数据包本身中。前一种方法导致出现一些明显的资源匮乏现象,连接数据包源源不断地到达。为了防止出现这种情况,所产生的开销导致了性能下降。数据包排队可以保护这种顺序,并且此过程已得到大大简化,这就是我们在 FireEngine 中采用的方法。 正如前面提到的一样,每个连接实例将被分配给单个 squeue,因而仅在垂直边界内部对其进行处理。由于每次由单个线程对 squeue 进行处理,因此,不需要进行额外的锁定,即可访问用于从边界内部处理给定连接的所有数据结构。这可改进连接元数据、数据包元数据以及数据包有效负荷数据访问的 CPU 和线程上下文数据区域性。此外,这还允许删除针对每个设备的驱动程序工作线程方案(在解决系统范围内的资源问题时会出现问题),以及实现额外的策略算法以便基于网络接口吞吐量和系统吞吐量以最佳方式处理给定的网络接口(例如,将针对每个连接的数据包处理分摊到一组 CPU)。进入 squeue 的线程可以立即处理数据包,也可以将其排入队列,以便稍后由另一个线程或工作线程对其进行处理。这种选择取决于 squeue 入口点和 squeue 的状态。仅当没有其他线程进入相同 squeue 时,才能立即进行处理。squeue 使用以下抽象层表示:
typedef struct squeue_s {
int_t sq_flag; /* Flags tells squeue status */
kmutex_t sq_lock; /* Lock to protect the flag etc */
mblk_t *sq_first; /* First Packet */
mblk_t *sq_last; /* Last Packet */
thread_t sq_worker; /* the worker thread for squeue */
} squeue_t;
一定要注意,squeue 是基于每个硬件执行管道创建的,即内核、超线程等。串行化队列(和硬件执行管道)的堆栈处理仅限于每次处理一个线程,但这实际会提高性能,因为新堆栈可确保不会出现任何资源(如内存或垂直边界内部的锁定)等待。此外,与只允许一个线程不间断地运行相比,允许多个内核线程分享硬件执行管道时间所产生的开销会更多。
始终允许工作线程清空整个队列。选择正确清空模型的过程非常复杂。可选的模型如下所示:
可以单独将这些选项应用于读取线程和写入线程。 通常,中断线程执行的清空操作应始终是受时间限制的“清空和处理”;而写入线程可以在“处理自己的数据包”和“受时间限制的处理和清空”之间进行选择。对于 Solaris 10 发行版,可以使用缺省设置“处理您自己的数据包”来调整写入线程行为;而读取端则固定为“受时间限制的处理和清空”。 发信号通知工作线程是另一个值得探索的选项。如果数据包到达率很低,并且强制线程将其数据包排入队列,则应允许在进入线程处理完 squeue 时立即运行工作线程(如果有要完成的工作)。 另一方面,如果数据包到达率较高,则可能希望延迟工作线程的唤醒时间,并希望中断随后到达,以完成清空。如果在数据包到达率较高时立即唤醒工作线程,则会导致在工作线程和中断线程之间产生不必要的争用。 Solaris 10 操作系统的缺省设置是延迟唤醒工作线程。最初对可用服务器进行的试验表明,在经过 10 分钟延迟后唤醒工作线程可获得最佳效果。 要向 squeue 发出请求,您需要使用针对每个 squeue 的锁定以保护队列状态,但这并不会产生可伸缩性问题,因为它是在 CPU 之间分配的,并且仅保持很短一段时间。我们还利用了优化功能,它可避免上下文切换,同时仍保留 squeue 处理的单线程语义。我们在系统中为每个 CPU 创建一个 squeue 实例,并将工作线程绑定到该 CPU 上。随后,将每个连接绑定到特定 squeue 上,因而也将其绑定到特定 CPU 上。 可以更改 squeue 到 CPU 的绑定,但由于 squeue 保护语义的原因,连接到 squeue 的绑定从来不会发生变化。对于合并的 TCP/IP,垂直边界保护每个连接的 TCP 状态。对于出站连接,将在“打开”、“绑定”或“连接”时选择每个连接使用的 squeue 实例;对于入站连接,将在“预备连接创建时间”选择每个连接使用的 squeue 实例。 选择的 squeue 实例取决于系统中 CPU 和 NIC 的相对速度。共有以下两种情况:
对于 Solaris 10 操作系统,确定 NIC 比 CPU 快或慢是由系统管理员通过调整全局变量
squeue_t *squeue_create(squeue_t *, uint32_t, processorid_t, void (*)(), \
void *, clock_t, pri_t);
void squeue_bind(squeue_t *, processorid_t);
void squeue_unbind(squeue_t *);
void squeue_enter(squeue_t *, mblk_t *, void (*)(), void *);
void squeue_fill(squeue_t *, mblk_t *, void (*)(), void *);
IP 连接扇出机制包含三个散列表:
在查找过程中,将返回连接结构(所有连接信息的超集)。此连接结构称为
typedef struct conn_s {
kmutex_t conn_lock; /* Lock for conn_ref */
uint32_t conn_ref; /* Reference counter */
uint32_t conn_flags; /* Flags */
struct ill_s *conn_ill; /* The ill packets are coming on */
struct ire_s *conn_ire; /* ire cache for outbound packets */
tcp_t *conn_tcp; /* Pointer to tcp struct */
void *conn_ulp /* Pointer for upper layer*/
edesc_pf conn_send; /* Function to call on read side */
edesc_pf conn_recv; /* Function to call on write side */
squeue_t *conn_sqp; /* Squeue for processing */
/* Address and Ports */
struct {
in6_addr_t connua_laddr; /* Local address */
in6_addr_t connua_faddr; /* Remote address. */
} connua_v6addr;
#define conn_src V4_PART_OF_V6(connua_v6addr.connua_laddr)
#define conn_rem V4_PART_OF_V6(connua_v6addr.connua_faddr)
#define conn_srcv6 connua_v6addr.connua_laddr
#define conn_remv6 connua_v6addr.connua_faddr
union {
/* Used for classifier match performance */
uint32_t conn_ports2;
struct {
in_port_t tcpu_fport; /* Remote port */
in_port_t tcpu_lport; /* Local port */
} tcpu_ports;
} u_port;
#define conn_fport u_port.tcpu_ports.tcpu_fport
#define conn_lport u_port.tcpu_ports.tcpu_lport
#define conn_ports u_port.conn_ports2
uint8_t conn_protocol; /* protocol type */
kcondvar_t conn_cv;
} conn_t;
一个值得注意的有趣成员是指向 squeue 或垂直边界的指针。查找是在边界外部进行的,数据包是在它所附加的 squeue 连接上进行处理/排队的。此外,
此外,连接扇出机制具有用于支持通配符侦听器的置备,例如 IP 分类器 API 类似于以下内容:
conn_t *ipcl_conn_create(uint32_t type, int sleep);
void ipcl_conn_destroy(conn_t *connp);
int ipcl_proto_insert(conn_t *connp, uint8_t protocol);
int ipcl_proto_insert_v6(conn_t *connp, uint8_t protocol);
conn_t *ipcl_proto_classify(uint8_t protocol);
int *ipcl_bind_insert(conn_t *connp, uint8_t protocol, ipaddr_t src,
uint16_t lport);
int *ipcl_bind_insert_v6(conn_t *connp, uint8_t protocol,
const in6_addr_t * src, uint16_t lport);
int *ipcl_conn_insert(conn_t *connp, uint8_t protocol, ipaddr_t src,
ipaddr_t dst, uint32_t ports);
int *ipcl_conn_insert_v6(conn_t *connp, uint8_t protocol,
in6_addr_t *src, in6_addr_t *dst, uint32_t ports);
void ipcl_hash_remove(conn_t *connp);
conn_t *ipcl_classify_v4(mblk_t *mp);
conn_t *ipcl_classify_v6(mblk_t *mp);
conn_t *ipcl_classify(mblk_t *mp);
函数名称在很大程度上是自说明性的。
由于堆栈是完全多线程的(除非垂直边界强制实现针对每个 CPU 的串行化),因此,它使用基于引用的方案来确保连接实例在需要时可用。引用计数是由 对于建立的 TCP 连接,可确保对其进行三种引用。每个协议层具有对实例的引用(TCP 和 IP 各有一个引用),分类器本身也具有引用,因为它是建立的连接。每次数据包到达连接并且分类器查找连接实例时,都会设置一个额外的引用;当协议层处理完该数据包后,将会删除此引用。类似地,在连接实例上运行的任何定时器也具有引用,以确保只要触发定时器就会调用该实例。在删除最后一个实例后,将会释放与连接实例关联的内存。 3.0 TCP
Solaris 10 操作系统提供了与以前发行版相同的 TCP 视图,即 TCP 显示为克隆设备,但它实际上是一个复合设备,将 TCP 和 IP 代码合并为单个 D_MP STREAMS 模块。合并 TCP/IP 模块的打开和关闭 STREAMS 入口点与 IP 入口点相同,即
TCP 的操作部分完全由通过
图 1 供垂直边界使用的 TCP 入口点: tcp_input - 所有入站数据包和控制消息 tcp_output - 所有出站数据包和控制消息 tcp_close_output - 在用户关闭时 tcp_timewait_output - 时间等待到期 tcp_rsrv_input - 在读取端释放流控制 tcp_timer - 所有 tcp 定时器
在控制和数据路径上,FireEngine 将 TCP 和 IP 之间的接口由基于 STREAMS 的现有消息传送接口更改为基于函数调用的接口。在出站端,TCP 通过调用
类似地,控制消息也作为函数参数直接传递。 基本协议处理代码保持不变。让我们看一下通用套接字调用,以及它们如何与框架进行交互。
TCP 的套接字打开或
tcp_connect 中的更改类似于 tcp_bind。完整的 bind() 请求是作为 TPI 消息准备的,并作为函数参数传递给 ip_bind_v{4, 6}。IP 调用分类器并在连接的散列表中插入连接。不再使用 TCP 中的 conn_ hash 表。
此路径是 tcp_bind 的一部分。tcp_bind 准备本地绑定 TPI 消息,并将其作为 ip_bind_v{4, 6} 函数参数进行传递。IP 调用分类器并在绑定散列表中插入连接。TCP 的侦听散列表不再存在。
在 Solaris 10 之前的发行版中,accept 实现在侦听器上下文中执行大量的连接建立处理。三路信号握手是在侦听器的边界中完成的,并在侦听器 STREAM 中向上发送连接指示。将在侦听器流中向下发送执行接受所需的消息,从将 T_CONN_RES 消息发送到 TCP 时起,直至 sockfs 收到确认之前,侦听器都是单线程的。在 Solaris 10 之前的发行版中,如果连接到达率很高,堆栈接受新连接的能力就会大大下降。
另外还会产生一些额外的 TCP 开销,这会导致接受速度变慢。当
当 SYN 数据包到达时,FireEngine 模型就会立即在其边界上建立一个“预备”连接(在接受完成之前,传入连接称为“预备”连接),从而确保数据包始终到达正确的连接。因此,可以完全消除 TCP 全局队列。仍然在侦听器 STREAM 中将连接指示发送到侦听器,但接受是在新创建的接受器 STREAM 中完成的(因此,不需要为此 STREAM 分配数据结构),并且可以在接受器 STREAM 中发送确认。因此,在接受处理过程中的任何时候, 新模型是周密实现的,这是由于存在新的传入连接(预备)仅仅是因为它有侦听器,在接受处理过程中,预备连接和侦听器可能会由于预备连接接收到重置或侦听器关闭而随时消失。
预备连接从对侦听器进行引用入手,以使预备连接对侦听器的引用始终有效,即便侦听器可能已经关闭。在完成三路信号握手后需要发送连接指示时,预备连接将对其自身进行引用,以使其能够在接收到重置后关闭,但对它的任何引用仍然有效。预备连接将指向其自身的指针作为连接指示消息的一部分进行发送,此消息是在检查侦听器尚未关闭后通过侦听器 STREAM 发送的。在新创建的接受器流 STREAM 中向下传送 TCP 中的关闭处理不再需要等到引用计数降为零,因为对关闭队列的引用和对 TCP 的引用现已分离。释放对关闭队列的所有引用后,关闭就会立即返回。在大多数情况下,TCP 数据结构本身可能会继续作为分离的 TCP 而保留下来。释放对 TCP 的最后一个引用后,将会释放 TCP 数据结构。
用户启动的关闭仅关闭流。可能会继续保留基础 TCP 结构。在传输所有用户数据并且数据处于 在最常见的情况下,如果 TCP 可以访问 IRE,则无需调用 IP 即可传送出站数据包。使用合并的 TCP/IP 具有如下优势:能够访问缓存 IRE 以获取连接,并且 TCP 随后可以根据 IRE 中的信息将数据直接放入链路层驱动程序。FireEngine 完全按上述方式工作。
在 Solaris 10 操作系统中,TCP Fusion 是一个用于回送 TCP 连接的无协议数据路径。两个本地 TCP 端点的熔合是在连接建立时完成的。缺省情况下,所有回送 TCP 连接都将被熔合。可通过将系统范围内的可调整
如果熔合失败,我们将恢复使用通常的 TCP 数据路径;如果成功,两个端点将继续使用
如果启用了同步 STREAMS,则会采用后一种路径。如果
TCP Fusion 中的锁定是由 squeue 和互斥锁 在同步流模式下,TCP Fusion 的小型写入流控制的速率限制是通过检查接收缓冲区大小和数据块数(两者均设置为不同的限制)实现的。这不同于通常的 STREAMS 流控制,对于后者,累积大小检查优于数据块计数检查(STREAMS 队列高水印通常表示字节)。每次排队都会触发发往接收进程的通知;数据块的阻塞表明接收者速度下降,应该及早阻止或通知发送者,而不要继续浪费系统资源。实际上,这相当于动态限制未完成的片段数。
缺省情况下,允许排队的数据块的最小数量为 8,可通过系统范围内的可调整 4.0 UDP除对框架的改进外,Solaris 10 操作系统还对 UDP 数据包在堆栈中的移动过程进行了其他更改。项目的内部代码名称为 "Yosemite"。在 Solaris 10 发行版之前,UDP 处理成本平均分摊到每个数据包的处理成本以及每个字节的处理成本。数据包处理成本通常是由于 STREAMS、流标头处理以及堆栈和驱动程序中的数据包丢失造成的。每个字节的处理成本是由于缺少硬件校验和以及整个网络堆栈中的未优化代码分支造成的。 尽管 UDP 并不可靠,但局域网已变得非常可靠,应用程序倾向于假设 LAN 环境中没有数据包丢失。这种假设在很大程度上是正确的,但在 Solaris 10 之前的版本中,堆栈在处理 UDP 过载方面并不是非常有效,往往会在堆栈本身中丢失数据包。而在入站时,会在整个接收路径的多个层中丢失数据包。对于 UDP,最常见和最明显的位置是 IP 层,这是由于缺少对数据包进行排队所需的资源。另一个很重要、但并不明显的数据包丢失位置是网络适配器层。当计算机以较高速度处理传入数据包时,通常会发生这种数据包丢失情况。
UDP
这是一个完全多线程的 UDP 模块,它运行在与 IP 相同的保护域中。此模块允许传输 (UDP) 层与其上面和下面的层进行更紧密地集成。这允许
当执行修改端点状态的函数时,UDP 需要对每个端点执行独占操作。 Solaris 10 模型使用与 STREAMS 无关的内部边界来实现上述同步,下面对此进行了说明:
必须使用
为支持此项功能,新 UDP 模型使用两个操作模式,即 UDP MT HOT 模式和 UDP SQUEUE 模式。在 UDP MT HOT 模式下,多个线程可以同时进入 UDP 端点。这用于发送或接收常规数据,它类似于 putshared STREAMS 入口点。控制操作和其他特殊情况调用
在稳定模式下,UDP 跟踪在端点上运行的线程数。 尽管 UDP 和 IP 在相同的保护域中运行,但它们仍是单独的 STREAMS 模块。因此,STREAMS 检测保持不变,UDP 模块实例始终被推送到 IP 上面。虽然这会导致为每个 UDP 端点执行额外的打开和关闭操作,但这为依靠此类检测几何执行某些操作的一些应用程序提供向后兼容性,如在流上发出 I POP 以获取对 IP9 的直接访问。 实际的 UDP 处理是在 IP 实例内完成的。UDP 模块实例并不拥有任何有关端点的状态,而仅作为伪模块,这种模块的存在是为了使 STREAMS 检测外观保持不变。 Solaris 10 平台允许使用以下检测模式:
这些模式意味着,我们不支持 IP 和 UDP 之间的任何中间模块;事实上,Solaris 技术过去从未支持此类方案,因为 IP 和传输模块层之间的通信语义是专用的。
在
如果传输模块直接位于 同步 STREAMS 是传统 STREAMS 接口的扩展,用于进行消息传送和处理。它最初是作为合并的复制以及校验和操作的一部分添加的。它提供了一种方法,以同步方式调用模块或驱动程序的入口点以响应用户 I/O 请求。在传统 STREAMS 中,流标头是此类请求的同步障碍。同步 STREAMS 提供了一种机制,将此障碍从流标头向下移动到下面的模块。
在 Solaris 10 之前的发行版中,由于很多方面的因素,同步 STREAMS 的 TCP 实现非常复杂。主要因素是合并的校验和以及
与之相反,在 Solaris 10 操作系统中,TCP 在
每次数据到达时,传输模块将安排应用程序对其进行检索。如果当前在读取操作过程中阻止了应用程序(休眠),将解除阻止该应用程序以使其能够恢复执行。这是通过在流上调用
也可以在 作为读取操作的一部分,传输模块通过将数据从其读取端同步 STREAMS 入口点返回以向应用程序提供数据。对于回送 TCP,同步 STREAMS 读取入口点将其接收队列的全部内容(字节流)返回到流标头;任何其余数据都将在流标头重新排列,等待下一次读取。对于 UDP,读取入口点每次仅返回一条消息(数据报)。
缺省情况下,当
(请注意,不允许在套接字端点上执行 I INSERT 或 I REMOVE
如果需要使用回退, 5.0 IP正如前面所提到的一样,所有传输层已合并到一个 IP 模块中,此模块完全是多线程的,并作为伪设备驱动程序以及 STREAMS 模块。IP 中的重要变化是删除了 IP 客户机功能以及入站数据包流的多路复用功能。新的 IP 分类器(仍然是 IP 模块的一部分)负责对入站数据包进行分类以传送到正确的连接实例。IP 模块仍负责网络层协议处理以及检测和管理网络接口。 让我们简单了解一下,网络接口检测、多路径以及多址传播在新堆栈中的工作方式。
检测是很长的一系列操作,其中包含 IP、ARP 和设备驱动程序之间的消息交换。在检测操作中,通常会包含大多数设置 IOCTL。一个自然模型是串行化这些 IOCTL,每个 ILL 一个(IP 较低级别)。例如, 另外一种可能性是使用一种更精细的方法,并串行化每个 IPIF 的操作,而不是串行化每个 ILL 的操作。只有在 ILL 上提供了很多 IPIF 并且不同 IPIF 的操作不会产生任何相互干扰时,这种方法才是有益的。 另一种可能性是,使用标准 Solaris MT 技术将所有 IOCTL 完全多线程化。但是,这会产生不必要的复杂性,并且不会带来多大好处。很难在整个检测系列操作中都保持锁定,这涉及等待以及与驱动程序或其他模块之间的消息交换。同时允许 IPIF 上有多个设置 IOCTL 并不会显著提高性能或功能,因为这些是完全非重复性的控制操作。广播 IRE 是针对每个 ILL 创建的,而不是针对每个 IPIF。因此,如果尝试在 ILL 上同时打开多个 IPIF,则会在广播 IRE 创建逻辑中产生额外的复杂性。另一方面,串行化每个 ILL 的检测操作有助于方便地利用现有 IP 代码库。在检测过程中,IP 与设备驱动程序和 ARP 交换消息。从基础设备驱动程序中接收的消息也是在 IP 中以独占方式处理的。这是很方便的,因为我们不能在 putnext 中保持标准互斥锁,以便尝试在写入端和读取端活动之间提供互斥功能。可通过使用针对每个 ILL 的串行化队列,而不是使用完全独占性的 PERMOD syncq 轻松地达到这样的效果。 5.2 IP 网络多路径 (IP Network Multipathing, IPMP)
IPMP 操作均基于 IPMP 组概念。故障转移和回退操作是在两个 ILL 之间执行的,通常作为相同 IPMP 组的一部分。IPIF 和 ILM 将在两个 ILL 之间移动。这涉及关闭源 ILL,并且可能涉及打开目标 ILL。关闭或打开 ILL 会影响广播 IRE。需要针对每个 IPMP 组对广播 IRE 进行分组,以抑制所接收的重复广播数据包。因此,广播 IRE 处理影响 IPMP 组的所有成员。设置
多址传播合并是在 ILG 和 ILM 结构上完成的。对于在 IPC(套接字)上运行并尝试执行多址传播合并的多个线程,在 ILG 上运行时需要对其进行同步。如果多个线程可能在不同的 IPC(套接字端点)运行并尝试进行多址传播合并,它们最终可能会尝试同时处理 ILM,并且在访问 ILM 时需要对其进行同步。它们均采用标准 Solaris MT 技术。考虑到所有上述情况(即检测、IPMP 和多址传播),它们的共同特征是针对每个 IPMP 组串行化所有独占操作。如果未启用 IPMP,则以 6.0 Solaris 10 设备驱动程序框架让我们简单了解一下,在 Solaris 10 操作系统之前,网络设备驱动程序是如何实现的,以及在新的 Solaris 10 堆栈中为什么需要对其进行更改。 6.1 GLDv2 和单一 DLPI 驱动程序(在 Solaris 9 和早期发行版中) 在 Solaris 10 之前的发行版中,网络堆栈是在 DLPI1 提供程序上中继的,这些提供程序通常是使用两种方法之一实现的。下图(图 2)显示了基于所谓的单一数据链路提供程序接口 (Data Link Provider Interface, DLPI) 驱动程序的堆栈,并且还显示了基于使用通用 LAN 驱动程序 (GLDv2) 模块的驱动程序的堆栈。
图 2 GLDv2 模块的实际功能相当于库。客户机仍然与绑定到设备上的驱动程序实例进行通信,但 DLPI 协议处理是通过调用 GLDv2 模块来完成的,该模块随后回调驱动程序以访问硬件。使用 GLD 模块具有比较明显的优势,因为驱动程序编写者不需要重新实现大量多半是通用的 DLPI 协议处理。也可以在 GLD 模块中集中实现第二层(数据链路)功能,如 802.1q 虚拟 LAN (Virtual LAN, VLAN),以使所有驱动程序都能够利用这些功能。但是,当体系结构实现 802.3ad 链路聚合(也称为端口汇聚)等功能时,仍然会出现问题,此时,网络接口和设备之间的一一对应关系将被破坏。 GLDv2 和单一驱动程序依赖于 DLPI 消息,并通过 STREAMS 框架与上面的层进行通信。这种机制对于链路聚合或 10Gb NIC 并不十分有效。对于新的堆栈,需要使用一种更好的机制以确保数据区域性,并使堆栈能够以更精细的方式控制设备驱动程序来处理中断问题。 Solaris 10 软件引入了一个称为 GLDv3(内部名称为 "project Nemo")的新设备驱动程序框架以及新的堆栈。大多数主要设备驱动程序都移植到此框架上,所有将来的设备驱动程序和 10Gb 设备驱动程序均基于此框架。此框架还提供了一个基于 STREAMS 的 DLPI 层以保持向后兼容性(以允许外部非 IP 模块能够继续正常使用)。 GLDv3 体系结构虚拟化网络堆栈的第二层。网络接口和设备之间的一一对应关系不复存在。下图(图 3)显示了在 MAC 服务模块 (MAC) 中注册的多个设备。它还显示了两个客户机:一个是传统客户机,通过 DLPI 与数据链路驱动程序 (Data-Link Driver, DLD) 进行通信,另一个是基于内核的客户机,它只对数据链路服务模块 (Data-Link Service, DLS) 进行直接函数调用。
图 3 6.2.1 GLDv3 驱动程序
GLDv3 驱动程序类似于 GLD 驱动程序。必须依赖于 misc/mac. 和 misc/dld 来链接该驱动程序。它必须使用指向以下结构实例的指针来调用
typedef struct mac {
const char *m_ident;
mac_ext_t *m_extp;
struct mac_impl *m_impl;
void *m_driver;
dev_info_t *m_dip;
uint_t m_port;
mac_info_t m_info;
mac_stat_t m_stat;
mac_start_t m_start;
mac_stop_t m_stop;
mac_promisc_t m_promisc;
mac_multicst_t m_multicst;
mac_unicst_t m_unicst;
mac_resources_t m_resources;
mac_ioctl_t m_ioctl;
mac_tx_t m_tx;
} mac_t;
此结构必须在注册周期内持续存在,例如,直至调用
此
typedef uint64_t (*mac_stat_t)(void *, mac_stat_t);
调用此入口点是为了检索在
typedef int (*mac_start_t)(void *);
调用此入口点是为了使设备退出在注册接口时所处于的重置/停止状态。在进行此调用之前,MAC 模块不会提交任何数据包以进行传输,并且驱动程序不应提交任何数据包以进行接收。如果此函数成功,则应该返回零。如果失败,则应该返回相应的
typedef void (*mac_stop_t)(void *); 此入口点应该将设备停止,并使其处于重置/停止状态,以便可以取消注册接口。在进行此调用后,MAC 不会提交任何数据包以进行传输;在完成调用后,驱动程序不应提交任何数据包以进行接收。
typedef int (*mac_promisc_t)(void *, boolean_t);
此入口点用于设置设备的混杂性。如果第二个参数是
typedef int (*mac_multicst_t)(void *, boolean_t, const uint8_t *);
此入口点用于在设备从中接收数据包的多址传播地址集中添加和删除地址。如果第二个参数是
typedef int (*mac_unicst_t)(void *, const uint8_t *); 此入口点用于设置新的设备单点传送地址。在进行此调用后,除非设备处于混杂模式,否则,只应接收具有新地址和介质广播地址的数据包。
typedef void (*mac_resources_t)(void *, boolean_t); 调用此入口点是为了请求驱动程序注册其各个接收资源或接收环。
typedef mblk_t *(*mac_tx_t)(void *, mblk_t *);
此入口点用于提交设备要传输的数据包。第二个参数指向
typedef struct mac_info {
uint_t mi_media;
uint_t mi_sdu_min;
uint_t mi_sdu_max;
uint32_t mi_cksum;
uint32_t mi_poll;
boolean_t mi_stat[MAC_NSTAT];
uint_t mi_addr_length;
uint8_t mi_unicst_addr[MAXADDRLEN];
uint8_t mi_brdcst_addr[MAXADDRLEN];
} mac_info_t;
typedef enum {
MAC_STAT_IFSPEED = 0,
MAC_STAT_MULTIRCV,
MAC_STAT_BRDCSTRCV,
MAC_STAT_MULTIXMT,
MAC_STAT_BRDCSTXMT,
MAC_STAT_NORCVBUF,
MAC_STAT_IERRORS,
MAC_STAT_UNKNOWNS,
MAC_STAT_NOXMTBUF,
MAC_STAT_OERRORS,
MAC_STAT_COLLISIONS,
MAC_STAT_RBYTES,
MAC_STAT_IPACKETS,
MAC_STAT_OBYTES,
MAC_STAT_OPACKETS,
MAC_STAT_ALIGN_ERRORS,
MAC_STAT_FCS_ERRORS,
MAC_STAT_FIRST_COLLISIONS,
MAC_STAT_MULTI_COLLISIONS,
MAC_STAT_SQE_ERRORS,
MAC_STAT_DEFER_XMTS,
MAC_STAT_TX_LATE_COLLISIONS,
MAC_STAT_EX_COLLISIONS,
MAC_STAT_MACXMT_ERRORS,
MAC_STAT_CARRIER_ERRORS,
MAC_STAT_TOOLONG_ERRORS,
MAC_STAT_MACRCV_ERRORS,
MAC_STAT_XCVR_ADDR,
MAC_STAT_XCVR_ID,
MAC_STAT_XVCR_INUSE,
MAC_STAT_CAP_1000FDX,
MAC_STAT_CAP_1000HDX,
MAC_STAT_CAP_100FDX,
MAC_STAT_CAP_100HDX,
MAC_STAT_CAP_10FDX,
MAC_STAT_CAP_10HDX,
MAC_STAT_CAP_ASMPAUSE,
MAC_STAT_CAP_PAUSE,
MAC_STAT_CAP_AUTONEG,
MAC_STAT_ADV_CAP_1000FDX,
MAC_STAT_ADV_CAP_1000HDX,
MAC_STAT_ADV_CAP_100FDX,
MAC_STAT_ADV_CAP_100HDX,
MAC_STAT_ADV_CAP_10FDX,
MAC_STAT_ADV_CAP_10HDX,
MAC_STAT_ADV_CAP_ASMPAUSE,
MAC_STAT_ADV_CAP_PAUSE,
MAC_STAT_ADV_CAP_AUTONEG,
MAC_STAT_LP_CAP_1000FDX,
MAC_STAT_LP_CAP_1000HDX,
MAC_STAT_LP_CAP_100FDX,
MAC_STAT_LP_CAP_100HDX,
MAC_STAT_LP_CAP_10FDX,
MAC_STAT_LP_CAP_10HDX,
MAC_STAT_LP_CAP_ASMPAUSE,
MAC_STAT_LP_CAP_PAUSE,
MAC_STAT_LP_CAP_AUTONEG,
MAC_STAT_LINK_ASMPAUSE,
MAC_STAT_LINK_PAUSE,
MAC_STAT_LINK_AUTONEG,
MAC_STAT_LINK_DUPLEX,
MAC_STAT_LINK_STATE,
MAC_NSTAT /* must be the last entry */
} mac_stat_t;
提供 6.2.2 MAC 服务 (MAC) 模块 一些重要的驱动程序支持函数包括:
extern mac_resource_handle_t mac_resource_add(mac_t *, mac_resource_t *); 各个成员定义如下:
typedef void (*mac_blank_t)(void *, time_t, uint_t);
typedef mblk_t *(*mac_poll_t)(void *, uint_t);
typedef enum {
MAC_RX_FIFO = 1
} mac_resource_type_t;
typedef struct mac_rx_fifo_s {
mac_resource_type_t mrf_type; /* MAC_RX_FIFO */
mac_blank_t mrf_blank;
mac_poll_t mrf_poll;
void *mrf_arg;
time_t mrf_normal_blank_time;
uint_t mrf_normal_pkt_cnt;
} mac_rx_fifo_t;
typedef union mac_resource_u {
mac_resource_type_t mr_type;
mac_rx_fifo_t mr_fifo;
} mac_resource_t;
应该从
此
其他两个字段(
中断率由上面的层通过使用不同参数调用
extern void mac_resource_update(mac_t *); 在可用资源发生变化时由驱动程序调用。
extern void mac_rx(mac_t *, mac_resource_handle_t, mblk_t *);
应该调用此函数以提供要接收的数据包链(包含在 6.2.3 数据链路服务 (Data-Link Service, DLS) 模块 DLS 模块提供了类似于 DLPI 的数据链路服务接口。DLS 接口是内核级函数接口,而不是由 DLPI 指定的基于 STREAMS 消息的接口。此模块为上面的层提供了创建和销毁数据链路服务所需的接口;它还提供了检测和取消检测 NIC 所需的接口。与旧 GLDv2 或单一 DLPI 设备驱动程序相比,基于 GLDv3 的设备驱动程序的 NIC 检测和取消检测没有发生变化。主要变化是数据路径,这些路径允许进行直接调用、使用数据包链以及对 NIC 进行较精细的控制。 6.2.4 数据链路驱动程序 (Data-Link Driver, DLD) 数据链路驱动程序使用 DLS 和 MAC 模块所提供的接口来提供 DLPI。此驱动程序是使用传递给控制节点的 IOCTL 进行配置的。这些 IOCTL 创建并销毁单独的 DLPI 提供程序节点。此模块处理检测/取消检测 NIC 所需的 DLPI 消息,并通过不支持 GLDv3 的客户机的 STREAMS 为数据路径提供向后兼容性。 GLDv3 框架按照 IEEE 802.3ad 定义提供链路聚合支持。此功能的主要设计原理如下:
GLDv3 链路聚合是通过名为
图 4
GLDv3 Solaris 10 平台进一步改进了硬件校验和卸载功能,以便提高大多数应用程序的总体性能。Solaris 软件中的 16 位应用程序的补码校验和卸载框架已存在一段时间。它最初是应 Solaris 2.6 操作系统中的零复制 TCP/IP 要求而添加的,但直至最近才对其进行扩展以处理其他协议。Solaris 技术定义了以下两类校验和卸载:
对于传送和接收而言,为非分段 IPv4 方案(单点传送或多址传播)添加支持的作用微乎其微,因为大多数新型网络适配器支持这两类校验和卸载,而接口的差异很小。IPv6 的情况就没这么简单,因为只有为数不多的完整校验和网络适配器能够处理通过 IPv4/IPv6 传送的 TCP/UDP 数据包的校验和计算。 分段 IP 方案具有类似的限制。在传送时,校验和适用于未分段的数据报。为使适配器支持校验和卸载,它必须能够在最终计算校验和并通过线路发送片段之前缓冲所有 IP 片段(或在硬件中执行分段);在此之前,无法对出站 IP 片段执行校验和卸载。另一方面,接收片段重新组装方案更灵活一些,因为大多数完整校验和(以及所有部分校验和)网络适配器都能够计算校验和并向网络堆栈提供该值。在片段重新组装阶段,网络堆栈可以通过将这些值合并来获取未分段的数据报的校验和状态。 IP 存在选项时,无需进行校验和卸载,从而操作得以简化。对于部分校验和卸载,某些适配器将开始偏移限制为简单 IP 数据包足以处理的宽度。当协议标头长度超过此类限制(由于存在选项)时,开始偏移将回绕,从而导致不正确的计算。对于完整校验和卸载,没有合适的适配器能够正确处理 IPV4 源路由选项。 当发生传送校验和卸载时,网络堆栈会将合格的数据包与驱动程序所需的辅助信息相关联,以将校验和计算卸载到硬件上。 对于入站的情况,驱动程序可以完全控制与硬件计算的校验和值相关联的数据包。在驱动程序通过 DL CAPAB HCKSUM 宣布其功能后,网络堆栈将接受 IPv4 和 IPv6 数据包的完整和/或部分校验和信息。未分段和分段有效负荷都会执行此过程。 分段数据包首先需要执行重新组装过程,因为完全重新组装的数据报将会进行校验和验证。在重新组装过程中,网络堆栈将合并每个片段的由硬件计算的校验和值。
经过一段时间后,
聚合密钥必须是介于 1 和 65535 之间的整数值。某些设备并不支持可配置的数据链路或聚合。可以使用 在配置聚合时,GLDv3 框架允许用户通过不同的聚合成员选择出站负载平衡策略。此策略指定使用哪个 dev 对象来发送数据包。策略由一个或多个层说明符的列表(用逗号分隔)组成。层说明符为以下内容之一:
例如,要使用上层协议信息,可以使用以下策略: -P L4 要使用源和目标 MAC 地址以及源和目标 IP 地址,可以使用以下策略: -P L2,L3
框架还支持将链路聚合控制协议 (Link Aggregation Control Protocol, LACP) 用于基于 GLDv3 的聚合,可以由 在系统中插入新设备时,将在重新配置引导或 DR 过程中为设备创建缺省的非 VLAN 数据链路。在重新引导过程中,将保留所有对象的配置。
将来,我们打算使用 7.0 性能调节
Solaris 10 堆栈优化旨在方便快捷地提供一流的性能,而与所使用的硬件无关。秘诀就在于采用了一些技术,如在中断和轮询模式之间进行动态切换,这在通过如下方式管理负载时提供了很好的延迟功能:允许 NIC 中断每个数据包,并在负载很高时切换到轮询模式以获取更好的吞吐量和明确限制的延迟。缺省设置也是基于硬件配置仔细选取的。例如,在 Solaris 10 之前的发行版中, 尽管具有这些功能,有时仍需要调整一些可优化函数来处理极端情况或特定工作负载。在下面的部分中,我们讨论了一些用于控制堆栈行为的可优化函数。请务必仔细了解这些函数所产生的影响,否则,系统可能会变得不稳定。一定要注意,对于大量应用程序和工作负载,缺省设置便可提供最佳效果。
set ip:ip_squeue_fanout=1
set ip:ip_squeue_bind=0
缺省值为 2,可以添加以下行以便通过 set ip:tcp_squeue_wput=1 当 CPU 数量远多于活动 NIC 数量时,应该将此值设置为 1;当应用程序线程执行 squeue 清空和被锁定的可能性较高时,平台内在具有较高的内存延迟。
ip:ip_squeue_wait=0
此外,某些协议级调节(如更改 8.0 前景根据预计,Solaris 联网堆栈将继续建立在较好的层间垂直集成的基础之上,这可进一步提高区域性和性能。随着芯片多线程化和多内核 CPU 的出现,预计并行执行管道的数量将继续增加,甚至在低端系统上也是如此。目前,典型的双 CPU 计算机是双内核的,可提供四个执行管道,并且还可能具有超线程功能。 NIC 也变得越来越先进,可通过 MSI-X 提供多个中断;小型分类功能;多个 DMA 通道以及各种无状态卸载(如大片段卸载)等。 将来的工作预计会继续迎合这些硬件趋势,其中包括 TCP 卸载引擎、远程直接内存存取 (Remote Direct Memory Access, RDMA) 和 iSCSI 支持。其他具体的工作领域如下所示:
9.0 致谢我们在此对参与本文部分内容编写工作的 Thirumalai Srinivasan、Adi Masputra、Nicolas Droux 和 Eric Cheng 深表谢意。另外,还要感谢 Solaris 联网社区的所有成员的大力帮助。 除非另行颁发许可,否则此处所有技术手册中的代码(包括文章、常见问题解答和样例)只能在本许可下使用。 |
| |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
| ||||||||||||