Java Solaris 加入 SDN 参与讨论 我的社区 注册说明

BigAdmin 系统管理门户网站
Feature Article
BigAdmin 专题文章:Solaris 操作系统联网 -- 揭秘

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 操作系统的堆栈

让我们看一下新框架及其关键组件的工作方式。

2.1 概述

在 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 风格的函数调用接口。为处理连接数据包而动态创建的函数字符串(事件列表)是最终新框架的基础,其他模块和第三方高性能模块也可以参与到此框架中。

2.2 垂直边界

Squeue 可确保在任何给定时间只有一个线程能够处理给定连接,从而在合并的 TCP/IP 模块中串行化多个线程对 TCP 连接结构的访问(从读取和写入端)。这与 STREAMS QPAIR 边界很类似,但它保护从 IP 到 sockfs 的整个连接状态,而不是只保护模块实例。

垂直边界或 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 是基于每个硬件执行管道创建的,即内核、超线程等。串行化队列(和硬件执行管道)的堆栈处理仅限于每次处理一个线程,但这实际会提高性能,因为新堆栈可确保不会出现任何资源(如内存或垂直边界内部的锁定)等待。此外,与只允许一个线程不间断地运行相比,允许多个内核线程分享硬件执行管道时间所产生的开销会更多。

  • 排队模型:对于读取和写入端,队列是严格意义上的 FIFO(first in first out,先进先出),这可确保任何特定连接不会出现资源不足或匮乏的现象。读取或写入端线程将数据包排在链末尾。然后,可允许它基于下面的处理模型处理数据包或发信号通知工作线程。
  • 处理模型:将其数据包排入队列后,如果另一个线程已在处理 squeue,则排队的线程将返回,并随后将基于清空模型清空数据包。如果 squeue 未得到处理,而且没有排队的数据包,线程可以将 squeue 标记为正在处理(由 sq_flag 表示),并对数据包进行处理。在完成数据包处理后,它将删除“正在处理”标志,并释放 squeue 以用于将来处理。
  • 清空模型:线程(能够成功处理其自身的数据包)在处理请求时,也可以清空任何排队的数据包。此外,如果 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 的相对速度。共有以下两种情况:

  • CPU 比 NIC 快:将传入连接分配给中断的 CPU 的“squeue 实例”。对于出站连接,将连接分配给正在运行应用程序的 CPU 的 squeue 实例。
  • NIC 比 CPU 快:单个 CPU 无法满足处理 NIC 的要求。将在所有可用 squeue 上以随机方式绑定连接。

对于 Solaris 10 操作系统,确定 NIC 比 CPU 快或慢是由系统管理员通过调整全局变量 ip_squeue_fanout 的方式完成的。缺省值为 no fanout,即将传入连接分配给中断 CPU 所附加的 squeue。为了使 CPU 脱机,绑定到此 CPU 上的工作线程将删除其绑定,并在 CPU 重新联机时再将其恢复。这样,DR 功能就可以正常实现。当连接的数据包到达多个 NIC(因而中断多个 CPU)时,将始终在原先建立连接的 squeue 上处理这些数据包。在 Solaris 10 操作系统中,基于 TCP 的连接仅为垂直边界提供。在确定这是 TCP 连接后,将在 TCP 和 IP 层中实现垂直边界的接口。Solaris 10 操作系统更新预计会引入通用垂直边界,以用于任何用途。squeue API 类似于以下内容:

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 *);

squeue_create 实例化新的 squeue,并使用 squeue_bind()/squeue_unbind() 将其自身绑定到特定 CPU 上或从中取消绑定。squeue 在创建后将永远不会损坏。squeue_enter() 用于尝试并访问 squeue,进入的线程可以基于前面讨论的模型处理和清空 squeue。squeue_fill() 仅用于将数据包排在 squeue 上,供工作线程或其他线程处理。

2.3 IP 分类器

IP 连接扇出机制包含三个散列表:

  • 五元组散列表 {协议、远程和本地 IP 地址、远程和本地端口},用于保持完全限定的 TCP(已建立)连接
  • 包含协议、本地地址和本地端口的三元组查找,用于保持侦听器
  • 单元组查找,用于协议侦听器

在查找过程中,将返回连接结构(所有连接信息的超集)。此连接结构称为 conn_t,并按如下方式对其进行抽象化。

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 连接上进行处理/排队的。此外,conn_recvconn_send 指向读取端和写入端函数。如果将数据包发往 TCP,则读入端函数可能是 tcp_input

此外,连接扇出机制具有用于支持通配符侦听器的置备,例如 INADDR ANY。目前,连接的表和绑定表仅主要用于 TCP 和 UDP。侦听器条目在 listen() 调用期间创建。该条目在为 TCP 完成三路信号握手后进入连接的表。

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);

函数名称在很大程度上是自说明性的。

2.4 同步机制

由于堆栈是完全多线程的(除非垂直边界强制实现针对每个 CPU 的串行化),因此,它使用基于引用的方案来确保连接实例在需要时可用。引用计数是由 conn_t 成员 conn_ref 执行的,并由 conn_lock 进行保护。锁定的主要用途并不是保护 conn_t 块,而只是保护引用计数。每次某个实体引用数据结构(存储指向数据结构的指针供以后进行处理)时,它通过调用 CONN_INC_REF 宏(基本上是获取 conn_lock)来增加引用计数,增加 conn_ref,然后删除 conn_lock。每次实体删除对连接实例的引用时,它都会使用 CONN_DEC_REF 宏删除其引用。

对于建立的 TCP 连接,可确保对其进行三种引用。每个协议层具有对实例的引用(TCP 和 IP 各有一个引用),分类器本身也具有引用,因为它是建立的连接。每次数据包到达连接并且分类器查找连接实例时,都会设置一个额外的引用;当协议层处理完该数据包后,将会删除此引用。类似地,在连接实例上运行的任何定时器也具有引用,以确保只要触发定时器就会调用该实例。在删除最后一个实例后,将会释放与连接实例关联的内存。


3.0 TCP

Solaris 10 操作系统提供了与以前发行版相同的 TCP 视图,即 TCP 显示为克隆设备,但它实际上是一个复合设备,将 TCP 和 IP 代码合并为单个 D_MP STREAMS 模块。合并 TCP/IP 模块的打开和关闭 STREAMS 入口点与 IP 入口点相同,即 ip_openip_close。基于在打开过程中传递的主编号,IP 确定此打开对应于 TCP 打开还是 IP 打开。TCP 的放置和服务 STREAMS 入口点是 tcp_wputtcp_wsrvtcp_rsrvtcp_wput 入口点仅用作包装例程,并从顶部启用 sockfs 和其他模块以便使用 STREAMS 与 TCP 进行通信。请注意,由于 IP 直接调用 TCP 函数,因此 tcp_rput 缺失。IP 的 STREAMS 入口点保持不变。

TCP 的操作部分完全由通过 squeue_* 语义进入的垂直边界进行保护,如图 1 所示。从顶部流入的数据包通过包装函数 tcp_wput 进入 TCP,在进入相应垂直边界后,此函数随后尝试执行实际的 TCP 输出处理函数 tcp_output。类似地,在进入垂直边界后,来自底部的数据包尝试执行实际的 TCP 输入处理函数 tcp_input。可以在多个入口点通过垂直边界进入 TCP。

图 1

图 1

供垂直边界使用的 TCP 入口点:

tcp_input - 所有入站数据包和控制消息
tcp_output - 所有出站数据包和控制消息
tcp_close_output - 在用户关闭时
tcp_timewait_output - 时间等待到期
tcp_rsrv_input - 在读取端释放流控制
tcp_timer - 所有 tcp 定时器

3.1 TCP 和 IP 之间的接口

在控制和数据路径上,FireEngine 将 TCP 和 IP 之间的接口由基于 STREAMS 的现有消息传送接口更改为基于函数调用的接口。在出站端,TCP 通过调用 ip_output 将完全准备好的数据包直接传递给 IP(在垂直边界内部)。

类似地,控制消息也作为函数参数直接传递。ip_bind_v{4, 6} 将绑定消息作为参数进行接收,执行所需的操作,然后将结果 mp 返回给调用者。TCP 在 connect()bind()listen() 路径中直接调用 ip_bind_v{4, 6}。IP 仍保留其所有 STREAMS 入口点,但 TCP (/dev/tcp) 变为真正的设备驱动程序(即,无法将其推送到其他设备驱动程序上)。

基本协议处理代码保持不变。让我们看一下通用套接字调用,以及它们如何与框架进行交互。

3.2 套接字

TCP 的套接字打开或 /dev/tcp 的打开最终会调用 ip_open。此打开随后调用 IP 连接分类器,并分配针对每个 TCP 端点的控制块(已经与 conn_t 集成在一起)。它选择此连接的 squeue。对于内部打开(即通过接受器流的 sockfs),几乎不执行任何操作,我们将执行有用操作的时间一直延迟到接受时间。

3.3 绑定

tcp_bind 最终需要与 IP 进行通信,以确定传入的地址是否有效。FireEngine TCP 像往常一样,以 TPI 消息的形式准备此请求。但是,此消息将作为函数参数直接传递到 ip_bind_v{4, 6}(它可将结果作为另一条消息返回)。在利用现有代码方面(只对其进行最低限度的更改),使用消息作为参数是非常有帮助的。TCP 验证绑定时所使用的端口散列表仍然保留在 TCP 中,原因是分类器无需使用该列表。

3.4 连接

tcp_connect 中的更改类似于 tcp_bind。完整的 bind() 请求是作为 TPI 消息准备的,并作为函数参数传递给 ip_bind_v{4, 6}。IP 调用分类器并在连接的散列表中插入连接。不再使用 TCP 中的 conn_ hash 表。

3.5 侦听

此路径是 tcp_bind 的一部分。tcp_bind 准备本地绑定 TPI 消息,并将其作为 ip_bind_v{4, 6} 函数参数进行传递。IP 调用分类器并在绑定散列表中插入连接。TCP 的侦听散列表不再存在。

3.6 接受

在 Solaris 10 之前的发行版中,accept 实现在侦听器上下文中执行大量的连接建立处理。三路信号握手是在侦听器的边界中完成的,并在侦听器 STREAM 中向上发送连接指示。将在侦听器流中向下发送执行接受所需的消息,从将 T_CONN_RES 消息发送到 TCP 时起,直至 sockfs 收到确认之前,侦听器都是单线程的。在 Solaris 10 之前的发行版中,如果连接到达率很高,堆栈接受新连接的能力就会大大下降。

另外还会产生一些额外的 TCP 开销,这会导致接受速度变慢。当 sockfs 打开到 TCP 的接受器流以接受新连接时,TCP 并不知道已经分配了新连接所需的数据结构。因此,它会分配新的结构并对其进行初始化。随后,在接受处理过程中,将释放这些结构。在 Solaris 10 之前的发行版中,另一个主要设计问题是新创建连接的数据包到达侦听器的边界。这需要检查每个传入数据包,需要将到达错误边界的数据包发送到其正确边界,这会导致额外的延迟。

当 SYN 数据包到达时,FireEngine 模型就会立即在其边界上建立一个“预备”连接(在接受完成之前,传入连接称为“预备”连接),从而确保数据包始终到达正确的连接。因此,可以完全消除 TCP 全局队列。仍然在侦听器 STREAM 中将连接指示发送到侦听器,但接受是在新创建的接受器 STREAM 中完成的(因此,不需要为此 STREAM 分配数据结构),并且可以在接受器 STREAM 中发送确认。因此,在接受处理过程中的任何时候,sockfs 都不需要变为单线程。

新模型是周密实现的,这是由于存在新的传入连接(预备)仅仅是因为它有侦听器,在接受处理过程中,预备连接和侦听器可能会由于预备连接接收到重置或侦听器关闭而随时消失。

预备连接从对侦听器进行引用入手,以使预备连接对侦听器的引用始终有效,即便侦听器可能已经关闭。在完成三路信号握手后需要发送连接指示时,预备连接将对其自身进行引用,以使其能够在接收到重置后关闭,但对它的任何引用仍然有效。预备连接将指向其自身的指针作为连接指示消息的一部分进行发送,此消息是在检查侦听器尚未关闭后通过侦听器 STREAM 发送的。在新创建的接受器流 STREAM 中向下传送 T_CONN_RES 消息时,我们会再次进入预备连接的边界,并检查预备连接在完成接受处理之前尚未由于接收到重置而关闭。对于基于 TLI/XTI 的应用程序,T_CONN_RES 消息仍然是在侦听器 STREAM 上处理的,并且确认将发回侦听器 STREAMS,因此行为没有发生任何改变。

3.7 关闭

TCP 中的关闭处理不再需要等到引用计数降为零,因为对关闭队列的引用和对 TCP 的引用现已分离。释放对关闭队列的所有引用后,关闭就会立即返回。在大多数情况下,TCP 数据结构本身可能会继续作为分离的 TCP 而保留下来。释放对 TCP 的最后一个引用后,将会释放 TCP 数据结构。

用户启动的关闭仅关闭流。可能会继续保留基础 TCP 结构。在传输所有用户数据并且数据处于 TIME_WAIT 状态后(将保持特定的一段时间),TCP 随后将与对等项进行 FIN/ACK 交换。这称为分离的 TCP。这些分离的 TCP 也需要进行保护,以防止在给定的分离 TCP 上同时进行出站和入站处理。

3.8 数据路径

在最常见的情况下,如果 TCP 可以访问 IRE,则无需调用 IP 即可传送出站数据包。使用合并的 TCP/IP 具有如下优势:能够访问缓存 IRE 以获取连接,并且 TCP 随后可以根据 IRE 中的信息将数据直接放入链路层驱动程序。FireEngine 完全按上述方式工作。

3.9 TCP 回送

在 Solaris 10 操作系统中,TCP Fusion 是一个用于回送 TCP 连接的无协议数据路径。两个本地 TCP 端点的熔合是在连接建立时完成的。缺省情况下,所有回送 TCP 连接都将被熔合。可通过将系统范围内的可调整 do tcp fusion 设置为 0 来改变此行为。要使熔合能够成功完成,需要满足两个端点的各种条件:

  • 它们必须共用相同的 squeue。
  • 它们必须是 TCP,而不是“原始套接字”。
  • 它们不能要求进行协议级处理,即连接不存在 IPsec 或 IPQoS 策略。

如果熔合失败,我们将恢复使用通常的 TCP 数据路径;如果成功,两个端点将继续使用 tcp fuse output() 作为传送路径。tcp fuse output() 将应用程序数据直接排入对等项的接收队列中;不需要进行协议处理。将数据排入队列后,发送者可以执行以下操作之一:

  • 通过调用 putnext(9F),在接收者的读取队列中向上推送数据。
  • 返回并让接收者通过同步 STREAMS 入口点检索排队数据。

如果启用了同步 STREAMS,则会采用后一种路径。如果 sockfs 由于模块插入或删除不再直接位于 TCP 模块上面,则会自动将其禁用。

TCP Fusion 中的锁定是由 squeue 和互斥锁 tcp fuse lock 处理的。要使熔合成功完成,一个要求是两个端点需要使用相同的 squeue。这可确保在一端仍在发送数据时,两个端点都不会消失。在启用同步 STREAMS 后,squeue 本身并不足以保证可以安全地进行访问。原因是,tcp fuse rrw() 并没有进入 squeue,且它对 tcp rcv 列表以及其他熔合相关字段的访问须与发送者同步。为此,应使用 tcp fuse lock

在同步流模式下,TCP Fusion 的小型写入流控制的速率限制是通过检查接收缓冲区大小和数据块数(两者均设置为不同的限制)实现的。这不同于通常的 STREAMS 流控制,对于后者,累积大小检查优于数据块计数检查(STREAMS 队列高水印通常表示字节)。每次排队都会触发发往接收进程的通知;数据块的阻塞表明接收者速度下降,应该及早阻止或通知发送者,而不要继续浪费系统资源。实际上,这相当于动态限制未完成的片段数。

缺省情况下,允许排队的数据块的最小数量为 8,可通过系统范围内的可调整 tcp_fusion_burst_min 将其更改为较高的值或 0(后者可禁用成组检查)。


4.0 UDP

除对框架的改进外,Solaris 10 操作系统还对 UDP 数据包在堆栈中的移动过程进行了其他更改。项目的内部代码名称为 "Yosemite"。在 Solaris 10 发行版之前,UDP 处理成本平均分摊到每个数据包的处理成本以及每个字节的处理成本。数据包处理成本通常是由于 STREAMS、流标头处理以及堆栈和驱动程序中的数据包丢失造成的。每个字节的处理成本是由于缺少硬件校验和以及整个网络堆栈中的未优化代码分支造成的。

4.1 堆栈中的 UDP 数据包丢失

尽管 UDP 并不可靠,但局域网已变得非常可靠,应用程序倾向于假设 LAN 环境中没有数据包丢失。这种假设在很大程度上是正确的,但在 Solaris 10 之前的版本中,堆栈在处理 UDP 过载方面并不是非常有效,往往会在堆栈本身中丢失数据包。

而在入站时,会在整个接收路径的多个层中丢失数据包。对于 UDP,最常见和最明显的位置是 IP 层,这是由于缺少对数据包进行排队所需的资源。另一个很重要、但并不明显的数据包丢失位置是网络适配器层。当计算机以较高速度处理传入数据包时,通常会发生这种数据包丢失情况。

UDP sockfs 扩展 (sockudp) 是 socktpi(用于处理基于套接字的 UDP 应用程序)的替代路径。前者通过去除流标头和 TPI 消息传送接口,在应用程序和网络堆栈之间提供更直接的通道。这允许在整个套接字和传输层中进行直接数据和函数访问。此外,这可使堆栈变得更高效,再加上 UDP 硬件校验和卸载(甚至对分段的 UDP),可确保很少会在堆栈中丢失 UDP 数据包。

4.2 UDP 模块

这是一个完全多线程的 UDP 模块,它运行在与 IP 相同的保护域中。此模块允许传输 (UDP) 层与其上面和下面的层进行更紧密地集成。这允许 socktpi 直接调用 UDP。类似地,UDP 也可以直接调用数据链路层。在出现 GLDv3 之后,数据链路层也可以直接调用传输层。此外,还可以直接调用实用程序函数,而不需要使用基于消息的接口。

当执行修改端点状态的函数时,UDP 需要对每个端点执行独占操作。udp rput other() 使用 IP 选项对数据包进行处理,这种处理导致更新端点的与选项有关的状态。udp wput other() 从顶部开始处理控制操作,例如,connect(3SOCKET)(它需要更新端点状态)。在出现 STREAMS 后,这种同步是通过使用共享内部边界入口点以及使用 qwriter inner() 获取对端点的独占访问来实现的。

Solaris 10 模型使用与 STREAMS 无关的内部边界来实现上述同步,下面对此进行了说明:

  • udp enter():进入 UDP 端点边界。即 udp become writer() 在 UDP 端点上成为专用边界。它指定一个函数,在边界成为专用边界后,将立即或稍后以独占方式调用该函数。
  • udp exit():退出 UDP 端点边界。

必须使用 udp enter() 完成从顶部或底部进入 UDP 的过程。通常情况下,在这些边界中无法保持任何锁定。在结束独占模式后,必须调用 udp exit() 以退出边界。

为支持此项功能,新 UDP 模型使用两个操作模式,即 UDP MT HOT 模式和 UDP SQUEUE 模式。在 UDP MT HOT 模式下,多个线程可以同时进入 UDP 端点。这用于发送或接收常规数据,它类似于 putshared STREAMS 入口点。控制操作和其他特殊情况调用 udp become writer(),以使其在每个端点变为专用的,这会导致转变为 UDP SQUEUE 模式。从定义上,squeue 可串行化对 conn t 的访问。如果 UDP 连接的 squeue 中没有其他暂挂消息,端点将恢复为 MT HOT 模式。在两者之间,如果并非端点的所有 MT 线程都完成,则在端点将消息排入队列,UDP 处于两种瞬态模式之一,即 UDP MT QUEUED 模式或 UDP QUEUED SQUEUE 模式。

在稳定模式下,UDP 跟踪在端点上运行的线程数。udp reader count 变量表示在 UDP MT HOT 模式下作为读取器进入端点的线程数。只有一个读取器时,例如,当此计数器降为 1 时,将会转变为 UDP SQUEUE 模式。类似地,udp squeue count 表示在 UDP SQUEUE 模式下在端点的 squeue 上运行的线程数。在最后一个线程退出端点后,模式将转变为 UDP MT HOT。

尽管 UDP 和 IP 在相同的保护域中运行,但它们仍是单独的 STREAMS 模块。因此,STREAMS 检测保持不变,UDP 模块实例始终被推送到 IP 上面。虽然这会导致为每个 UDP 端点执行额外的打开和关闭操作,但这为依靠此类检测几何执行某些操作的一些应用程序提供向后兼容性,如在流上发出 I POP 以获取对 IP9 的直接访问。

实际的 UDP 处理是在 IP 实例内完成的。UDP 模块实例并不拥有任何有关端点的状态,而仅作为伪模块,这种模块的存在是为了使 STREAMS 检测外观保持不变。

Solaris 10 平台允许使用以下检测模式:

  • 常规:先打开 IP,然后将 UDP 直接推送到其上面。这是在打开 UDP 套接字或设备时执行的缺省操作。
  • SNMP:UDP 被推送到 IP 以外的模块上面。在发生这种情况时,仅支持 SNMP 语义。

这些模式意味着,我们不支持 IP 和 UDP 之间的任何中间模块;事实上,Solaris 技术过去从未支持此类方案,因为 IP 和传输模块层之间的通信语义是专用的。

4.3 UDP 和套接字交互

socket(3SOCKET) 系统调用过程中发生的一个重要事件是,检测与套接字的地址系列和协议类型关联的模块。TCP 或 UDP 套接字很可能会导致 sockfs 直接位于相应传输模块的上面。在 Solaris 10 发行版之前,套接字层使用 STREAMS 原语与 UDP 模块进行通信。Solaris 10 操作系统允许使用一个在功能上可调用的接口,每次从 sockfs 传送到 UDP 时,不再需要使用元数据的 T UNITDATA REQ 消息。相反,可以直接为替代 UDP 入口点提供数据及其附属信息(如远程套接字地址),从而避免了额外的分配成本。

如果传输模块直接位于 sockfs 下面,则允许使用同步 STREAMS。这使传输层能够缓冲传入数据,供应用程序以后在发出读取操作时进行检索(通过同步 STREAMS),从而缩短了接收处理时间。

4.4 同步 STREAMS

同步 STREAMS 是传统 STREAMS 接口的扩展,用于进行消息传送和处理。它最初是作为合并的复制以及校验和操作的一部分添加的。它提供了一种方法,以同步方式调用模块或驱动程序的入口点以响应用户 I/O 请求。在传统 STREAMS 中,流标头是此类请求的同步障碍。同步 STREAMS 提供了一种机制,将此障碍从流标头向下移动到下面的模块。

在 Solaris 10 之前的发行版中,由于很多方面的因素,同步 STREAMS 的 TCP 实现非常复杂。主要因素是合并的校验和以及 copyin/copyout 操作。

与之相反,在 Solaris 10 操作系统中,TCP 在 copyin/copyout 过程中并不依赖于校验和,因此,此机制已大大得到简化,可用于在读取端回送 TCP 和 UDP。在请求过程中,将调用同步 STREAMS 入口点,如 read(2)recv(3SOCKET)。这些模块将数据排入其内部接收队列中,并使发送线程能够更快地返回,而不是使用 putnext(9F) 向上游发送数据。这可避免从发送线程上下文中调用 strrput() 以将数据排在流标头中,因而实现了更好的动态性。通过减少排队和发信号/轮询通知接收应用程序所花的时间,发送线程可以更快地返回以完成其他工作,因而串行化程度不如以前高。

每次数据到达时,传输模块将安排应用程序对其进行检索。如果当前在读取操作过程中阻止了应用程序(休眠),将解除阻止该应用程序以使其能够恢复执行。这是通过在流上调用 STR WAKEUP SET() 实现的。类似地,当应用程序没有其他可用数据时,在下一次读取尝试过程中,传输模块允许通过调用 STR WAKEUP CLEAR() 再次将其阻止。此前到达的任何新数据将覆盖此状态,并导致继续执行后续读取操作。

也可以在 poll(2) 中阻止应用程序,直至发生读取事件,否则,它可能会等待 SIGPOLLSIGIO 信号(如果所使用的套接字不会对其进行阻止)。因此,每次传输模块接收到数据时,将发送事件通知和/或发信号通知应用程序。这是通过在相应流上调用 STR SENDSIG() 实现的。

作为读取操作的一部分,传输模块通过将数据从其读取端同步 STREAMS 入口点返回以向应用程序提供数据。对于回送 TCP,同步 STREAMS 读取入口点将其接收队列的全部内容(字节流)返回到流标头;任何其余数据都将在流标头重新排列,等待下一次读取。对于 UDP,读取入口点每次仅返回一条消息(数据报)。

4.5 STREAMS 回退

缺省情况下,当 sockfs 直接位于相应传输模块上面时,将为所有 UDP 和回送 TCP 套接字启用直接传送和读取端同步 STREAMS 优化。在某些情况下,需要禁用这些功能;当发生这种情况时,必须通过 putnext(9F) 来完成 sockfs 和传输模块之间的消息交换。下面对这些情况进行了介绍:

  • 中间模块:此模块被配置为在打开时通过 autopush(1M) 自动推送到传输模块上面,或者通过 ioctl(2) 将其 I PUSH 到套接字上面。
  • 流转换:虚拟的 sockmod 模块是从套接字中 I POP 的,从而导致将其从套接字端点转换为设备流。

(请注意,不允许在套接字端点上执行 I INSERT 或 I REMOVE ioctl,因而不需要使用回退来解决这一问题。)

如果需要使用回退,sockfs 将通知传输模块禁用了直接模式。sockfs 模块以 ioctl 消息形式向下发送通知,这向传输模块表明现在必须使用 putnext(9F) 向上游传送数据。这允许数据流经中间模块,并提供与设备流语义之间的兼容性。


5.0 IP

正如前面所提到的一样,所有传输层已合并到一个 IP 模块中,此模块完全是多线程的,并作为伪设备驱动程序以及 STREAMS 模块。IP 中的重要变化是删除了 IP 客户机功能以及入站数据包流的多路复用功能。新的 IP 分类器(仍然是 IP 模块的一部分)负责对入站数据包进行分类以传送到正确的连接实例。IP 模块仍负责网络层协议处理以及检测和管理网络接口。

让我们简单了解一下,网络接口检测、多路径以及多址传播在新堆栈中的工作方式。

5.1 检测 NIC

检测是很长的一系列操作,其中包含 IP、ARP 和设备驱动程序之间的消息交换。在检测操作中,通常会包含大多数设置 IOCTL。一个自然模型是串行化这些 IOCTL,每个 ILL 一个(IP 较低级别)。例如,hme0qfe0 检测可并行执行,而不需要进行任何干预。但会串行化 hme0 上的所有各种设置 IOCTL。

另外一种可能性是使用一种更精细的方法,并串行化每个 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 组的所有成员。设置 IFF_FAILEDIFF_STANDBY 会导致评估 IPMP 组中的所有 ILL,并导致对广播 IRE 重新分组。因此,针对每个 IPMP 组串行化 IPMP 操作有助于方便地利用现有代码库。IPMP 组包含 IPv4 和 IPv6 ILL。

5.3 多址传播

多址传播合并是在 ILG 和 ILM 结构上完成的。对于在 IPC(套接字)上运行并尝试执行多址传播合并的多个线程,在 ILG 上运行时需要对其进行同步。如果多个线程可能在不同的 IPC(套接字端点)运行并尝试进行多址传播合并,它们最终可能会尝试同时处理 ILM,并且在访问 ILM 时需要对其进行同步。它们均采用标准 Solaris MT 技术。考虑到所有上述情况(即检测、IPMP 和多址传播),它们的共同特征是针对每个 IPMP 组串行化所有独占操作。如果未启用 IPMP,则以 phyint 为基础。例如,同时采用的 hme0 v4 和 hme0 v6 ILL 共享一个 phyint。在上一示例中,多址传播可能会具有较高的多线程度。但它必须与其他独占操作共存。例如,如果已在执行故障转移操作以尝试在两个 ILL 之间移动 ILM,我们不希望线程创建或删除 ILM。因此,它们最基本的共同特征是,为每个物理接口或每个 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

图 2

GLDv2 模块的实际功能相当于库。客户机仍然与绑定到设备上的驱动程序实例进行通信,但 DLPI 协议处理是通过调用 GLDv2 模块来完成的,该模块随后回调驱动程序以访问硬件。使用 GLD 模块具有比较明显的优势,因为驱动程序编写者不需要重新实现大量多半是通用的 DLPI 协议处理。也可以在 GLD 模块中集中实现第二层(数据链路)功能,如 802.1q 虚拟 LAN (Virtual LAN, VLAN),以使所有驱动程序都能够利用这些功能。但是,当体系结构实现 802.3ad 链路聚合(也称为端口汇聚)等功能时,仍然会出现问题,此时,网络接口和设备之间的一一对应关系将被破坏。

GLDv2 和单一驱动程序依赖于 DLPI 消息,并通过 STREAMS 框架与上面的层进行通信。这种机制对于链路聚合或 10Gb NIC 并不十分有效。对于新的堆栈,需要使用一种更好的机制以确保数据区域性,并使堆栈能够以更精细的方式控制设备驱动程序来处理中断问题。

6.2 GLDv3 -- 新体系结构

Solaris 10 软件引入了一个称为 GLDv3(内部名称为 "project Nemo")的新设备驱动程序框架以及新的堆栈。大多数主要设备驱动程序都移植到此框架上,所有将来的设备驱动程序和 10Gb 设备驱动程序均基于此框架。此框架还提供了一个基于 STREAMS 的 DLPI 层以保持向后兼容性(以允许外部非 IP 模块能够继续正常使用)。

GLDv3 体系结构虚拟化网络堆栈的第二层。网络接口和设备之间的一一对应关系不复存在。下图(图 3)显示了在 MAC 服务模块 (MAC) 中注册的多个设备。它还显示了两个客户机:一个是传统客户机,通过 DLPI 与数据链路驱动程序 (Data-Link Driver, DLD) 进行通信,另一个是基于内核的客户机,它只对数据链路服务模块 (Data-Link Service, DLS) 进行直接函数调用。

network_magic_fig3

图 3

6.2.1 GLDv3 驱动程序

GLDv3 驱动程序类似于 GLD 驱动程序。必须依赖于 misc/mac. 和 misc/dld 来链接该驱动程序。它必须使用指向以下结构实例的指针来调用 mac_register(),以便在 MAC 模块中进行注册:

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;

此结构必须在注册周期内持续存在,例如,直至调用 mac_unregister() 后才能取消其分配。在调用 mod_install(9F) 之前,还需要使用 GLDv3 驱动程序 _init(9E) 入口点来调用 mac_init_ops();从 _fini(9E) 中调用 mod_remove(9F) 后,则需要使用它们来调用 mac_fini_ops()

mac_t 结构的重要成员如下所示:

m_impl - MAC 模块使用它来指向其专用数据。驱动程序不能对其进行读取或修改。

m_driver - 驱动程序应该设置此字段以指向其专用数据。将提供该值作为驱动程序入口点的第一个参数。

m_dip - 必须将此字段设置为调用 mac_register() 的驱动程序实例的 dev_info_t 指针。

m_stat -

typedef uint64_t    (*mac_stat_t)(void *, mac_stat_t);

调用此入口点是为了检索在 mac_stat_t 枚举中定义的某个统计数据的值(如下所示)。应以 64 位无符号整数存储和返回所有值。不会为驱动程序没有显式声明支持的统计数据请求值。

m_start -

typedef    int (*mac_start_t)(void *);

调用此入口点是为了使设备退出在注册接口时所处于的重置/停止状态。在进行此调用之前,MAC 模块不会提交任何数据包以进行传输,并且驱动程序不应提交任何数据包以进行接收。如果此函数成功,则应该返回零。如果失败,则应该返回相应的 errno 值。

m_stop -

typedef void (*mac_stop_t)(void *);

此入口点应该将设备停止,并使其处于重置/停止状态,以便可以取消注册接口。在进行此调用后,MAC 不会提交任何数据包以进行传输;在完成调用后,驱动程序不应提交任何数据包以进行接收。

m_promisc -

typedef int (*mac_promisc_t)(void *, boolean_t);

此入口点用于设置设备的混杂性。如果第二个参数是 B_TRUE,则设备应接收介质上的所有数据包。如果将其设置为 B_FALSE,则只应接收发往设备单点传送地址和介质广播地址的数据包。

m_multicst -

typedef int (*mac_multicst_t)(void *, boolean_t, const uint8_t *);

此入口点用于在设备从中接收数据包的多址传播地址集中添加和删除地址。如果第二个参数是 B_TRUE,则应该将第三个参数所指向的地址添加到集合中。如果第二个参数是 B_FALSE,则应该删除第三个参数所指向的地址。

m_unicst -

typedef int (*mac_unicst_t)(void *, const uint8_t *);

此入口点用于设置新的设备单点传送地址。在进行此调用后,除非设备处于混杂模式,否则,只应接收具有新地址和介质广播地址的数据包。

m_resources -

typedef void (*mac_resources_t)(void *, boolean_t);

调用此入口点是为了请求驱动程序注册其各个接收资源或接收环。

m_tx -

typedef mblk_t *(*mac_tx_t)(void *, mblk_t *);

此入口点用于提交设备要传输的数据包。第二个参数指向 mblk_t 结构中包含的一个或多个数据包。将使用 b_cont 字段将相同数据包的片段链接在一起。单独数据包将通过前导片段中的 b_next 字段进行链接。应该按照在链中的出现顺序来安排数据包传输。应返回无法安排的每个剩余数据包链。如果 m_tx() 返回无法安排的数据包,驱动程序必须在资源可用时调用 mac_tx_update()。如果安排传输所有数据包,则应该返回 NULL

m_info - 这是一个嵌入式结构,其定义如下所示:

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;

mi_media 是一组介质类型;mi_sdu_min 是最小有效负荷大小;mi_sdu_max 是最大有效负荷大小;mi_cksum 详细说明设备 cksum 功能标志;mi_poll 详细说明驱动程序是否支持轮询;mi_addr_length 被设置为介质所使用的地址长度;mi_unicst_addr 被设置为在调用 mac_register() 时的设备单点传送地址;mi_brdcst_addr 被设置为介质的广播地址;mi_stat 是布尔值数组。

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;

提供 MAC_MIB_SET()MAC_ETHER_SET()MAC_MII_SET() 宏是为了将三个组中的所有值分别设置为 B_TRUE

6.2.2 MAC 服务 (MAC) 模块

一些重要的驱动程序支持函数包括:

mac_resource_add -

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;

应该从 m_resources() 入口点中调用此函数,以便在 MAC 模块中注册各个接收资源(通常是 DMA 描述符的环缓冲区)。随后应以 mac_rx() 调用的形式提供返回的 mac_resource_handle_t 值。mac_resource_add() 的第二个参数指定所添加的资源。资源由 mac_resource_t 结构指定。目前,仅支持类型为 MAC_RX_FIFO 的资源。MAC_RX_FIFO 资源由 mac_rx_fifo_t 结构描述。

mac_blank 函数供上面的层使用以控制设备的中断率。第一个参数是设备上下文,它用作 poll_blank 的第一个参数。

其他两个字段(mrf_normal_blank_timemrf_normal_pkt_cnt)分别指定缺省中断间隔和数据包计数阈值。当上面的层要求驱动程序恢复为缺省中断率时,可以将这些参数用作 mac_blank 的第二个和第三个参数。

中断率由上面的层通过使用不同参数调用 poll_blank 来控制。上面的层可通过将这些值的倍数传递给 mac_blank 的最后两个参数来增加或减少中断率。如果将这些值设置为零,则会禁用中断;NIC 被视为处于轮询模式。

mac_poll 是驱动程序提供的函数,上面的层使用它从接收环中检索数据包链(最多为第二个参数指定的最大计数量),它与先前在 mac_resource_add 过程中提供的 mrf_arg 相对应(作为 mac_poll 的第一个参数提供)。

mac_resource_update -

extern void mac_resource_update(mac_t *);

在可用资源发生变化时由驱动程序调用。

mac_rx -

extern void mac_rx(mac_t *, mac_resource_handle_t, mblk_t *);

应该调用此函数以提供要接收的数据包链(包含在 mblk_t 结构中)。应使用 b_cont 字段将相同数据包的片段链接在一起。单独数据包应通过使用前导片段中的 b_next 字段进行链接。如果注册的资源接收了数据包链,则应该将相应的 mac_resource_handle_t 值作为函数的第二个参数提供。尝试在多个 CPU 之间分摊负载时,协议堆栈将使用此值作为提示。假设属于相同流的数据包将始终由相同的资源接收。如果资源未知或未注册,则应该将 NULL 作为第二个参数进行传递。

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 为数据路径提供向后兼容性。

6.3 GLDv3 链路聚合体系结构

GLDv3 框架按照 IEEE 802.3ad 定义提供链路聚合支持。此功能的主要设计原理如下:

  • 允许在不更改代码的情况下聚合 GLDv3 MAC 驱动程序。
  • 保持非聚合设备的性能。
  • 确保聚合设备的性能是每个成员的累积线路速率,例如,由于聚合而产生的最小开销。
  • 支持手动配置和链路聚合控制协议 (Link Aggregation Control Protocol, LACP)。

GLDv3 链路聚合是通过名为 aggr 的伪驱动程序实现的。它在 GLDv3 MAC 层中注册对应于链路聚合组的虚拟端口。它使用 MAC 层提供的客户机接口来控制聚合的 MAC 端口并与其进行通信,如图 4 所示。它还导出一个伪 aggr 设备驱动程序,dladm 命令可使用该驱动程序来配置和控制链路聚合接口。在将 MAC 端口配置为链路聚合组的一部分后,其他 MAC 客户机(如 DLS 层)将无法同时对其进行访问。独占访问是由 MAC 层强制实施的。LACP 实现是由 aggr 驱动程序(它可以访问各个 MAC 端口或链路)完成的。

network_magic_fig4

图 4

GLDv3 aggr 驱动程序用作上面层的常规 MAC 模块,并显示为标准 NIC 接口(使用 dladm 创建后,可以使用 ifconfig 对该接口进行配置和管理)。aggr 模块在上面的层中使用 mac_resource_add 函数注册每个 MAC 端口(它是聚合的一部分),以便上面的层可以单独管理来自每个 MAC 端口的数据路径和中断。聚合的接口是作为单个接口(可能具有一个 IP 地址)进行管理的。数据路径是由唯一的 CPU/squeue 作为单个 NIC 进行管理的,这可实现 Solaris 操作系统聚合功能。这可提供接近于零的开销,并在作为聚合组一部分的 MAC 端口数方面提供线性可伸缩性。

6.4 校验和卸载

Solaris 10 平台进一步改进了硬件校验和卸载功能,以便提高大多数应用程序的总体性能。Solaris 软件中的 16 位应用程序的补码校验和卸载框架已存在一段时间。它最初是应 Solaris 2.6 操作系统中的零复制 TCP/IP 要求而添加的,但直至最近才对其进行扩展以处理其他协议。Solaris 技术定义了以下两类校验和卸载:

  • 完整:硬件中的完整校验和计算,其中包括 TCP 和 UDP 数据包的伪标头校验和计算。假设硬件具有分析协议标头的功能。
  • 部分:基于开始、结束和内容偏移的“哑”应用程序的补码校验和,用于描述校验和数据的范围以及传输校验和字段的位置,硬件中没有伪标头计算功能。

对于传送和接收而言,为非分段 IPv4 方案(单点传送或多址传播)添加支持的作用微乎其微,因为大多数新型网络适配器支持这两类校验和卸载,而接口的差异很小。IPv6 的情况就没这么简单,因为只有为数不多的完整校验和网络适配器能够处理通过 IPv4/IPv6 传送的 TCP/UDP 数据包的校验和计算。

分段 IP 方案具有类似的限制。在传送时,校验和适用于未分段的数据报。为使适配器支持校验和卸载,它必须能够在最终计算校验和并通过线路发送片段之前缓冲所有 IP 片段(或在硬件中执行分段);在此之前,无法对出站 IP 片段执行校验和卸载。另一方面,接收片段重新组装方案更灵活一些,因为大多数完整校验和(以及所有部分校验和)网络适配器都能够计算校验和并向网络堆栈提供该值。在片段重新组装阶段,网络堆栈可以通过将这些值合并来获取未分段的数据报的校验和状态。

IP 存在选项时,无需进行校验和卸载,从而操作得以简化。对于部分校验和卸载,某些适配器将开始偏移限制为简单 IP 数据包足以处理的宽度。当协议标头长度超过此类限制(由于存在选项)时,开始偏移将回绕,从而导致不正确的计算。对于完整校验和卸载,没有合适的适配器能够正确处理 IPV4 源路由选项。

当发生传送校验和卸载时,网络堆栈会将合格的数据包与驱动程序所需的辅助信息相关联,以将校验和计算卸载到硬件上。

对于入站的情况,驱动程序可以完全控制与硬件计算的校验和值相关联的数据包。在驱动程序通过 DL CAPAB HCKSUM 宣布其功能后,网络堆栈将接受 IPv4 和 IPv6 数据包的完整和/或部分校验和信息。未分段和分段有效负荷都会执行此过程。

分段数据包首先需要执行重新组装过程,因为完全重新组装的数据报将会进行校验和验证。在重新组装过程中,网络堆栈将合并每个片段的由硬件计算的校验和值。

6.4.1 dladm -- 用于数据链路管理的新命令

经过一段时间后,ifconfig 命令会因为尝试管理堆栈各层而变得严重过载。Solaris 10 操作系统引入了 dladm 命令来管理数据链路服务,并减轻了 ifconfig 的负担。dladm 命令是针对以下三种对象执行的:

  • link:数据链路(由名称标识)
  • aggr:网络设备聚合(由密钥标识)
  • dev:网络设备(由并置的驱动程序名称和实例编号标识)

聚合密钥必须是介于 1 和 65535 之间的整数值。某些设备并不支持可配置的数据链路或聚合。可以使用 dladm 查看此类设备提供的固定数据链路,但不能对其进行配置。

在配置聚合时,GLDv3 框架允许用户通过不同的聚合成员选择出站负载平衡策略。此策略指定使用哪个 dev 对象来发送数据包。策略由一个或多个层说明符的列表(用逗号分隔)组成。层说明符为以下内容之一:

  • L2:按照数据包的源和目标 MAC 地址选择出站设备。
  • L3:按照数据包的源和目标 IP 地址选择出站设备。
  • L4:按照数据包中包含的上层协议信息选择出站设备。对于 TCP 和 UDP,这包括源和目标端口。对于 IPsec,这包括 SPI(Security Parameters Index,安全参数索引)。

例如,要使用上层协议信息,可以使用以下策略:

-P L4

要使用源和目标 MAC 地址以及源和目标 IP 地址,可以使用以下策略:

-P L2,L3

框架还支持将链路聚合控制协议 (Link Aggregation Control Protocol, LACP) 用于基于 GLDv3 的聚合,可以由 dladm 通过 lacp-modelacp-timer 子命令对其进行控制。可以将 lacp-mode 设置为 offactivepassive

在系统中插入新设备时,将在重新配置引导或 DR 过程中为设备创建缺省的非 VLAN 数据链路。在重新引导过程中,将保留所有对象的配置。

将来,我们打算使用 dladm 及其专用文件(/etc/datalink.conf,其中存储了所有永久性信息)来管理特定于设备的参数,这些参数当前是通过特定于 ndd 驱动程序的配置文件和 /etc/system 进行管理的。


7.0 性能调节

Solaris 10 堆栈优化旨在方便快捷地提供一流的性能,而与所使用的硬件无关。秘诀就在于采用了一些技术,如在中断和轮询模式之间进行动态切换,这在通过如下方式管理负载时提供了很好的延迟功能:允许 NIC 中断每个数据包,并在负载很高时切换到轮询模式以获取更好的吞吐量和明确限制的延迟。缺省设置也是基于硬件配置仔细选取的。例如,在 Solaris 10 之前的发行版中,tcp_conn_hash_size 可优化函数的功能非常有限。缺省值为 512 个散列桶,它是根据支持的最低配置(在内存方面)进行选择的。Solaris 10 操作系统着眼于引导时的可用内存来选择 tcp_conn_hash_size 值。类似地,当“回收”处于时间等待状态的连接时,并不会立即释放与连接实例关联的内存(仍然基于总可用系统内存),而是将其放在 free_list 中。当新连接在给定时间段内到达时,TCP 尝试重新使用 free_list 中的内存;否则,将定期清理 free_list

尽管具有这些功能,有时仍需要调整一些可优化函数来处理极端情况或特定工作负载。在下面的部分中,我们讨论了一些用于控制堆栈行为的可优化函数。请务必仔细了解这些函数所产生的影响,否则,系统可能会变得不稳定。一定要注意,对于大量应用程序和工作负载,缺省设置便可提供最佳效果。

ip_squeue_fanout:控制是否在所有 CPU 中分摊来自一个 NIC 的传入连接的负载。值为 0 表示将传入连接分配给与中断的 CPU 关联的 squeue。值为 1 表示在所有 CPU 中分摊连接的负载。如果 NIC 速度比 CPU 快(如 10Gb NIC),并且需要使用多个 CPU 为 NIC 提供服务,则需要使用后一个值。可以添加以下行以便通过 /etc/system 进行设置:

set ip:ip_squeue_fanout=1

ip_squeue_bind:控制是否将工作线程绑定到特定 CPU 上。绑定(缺省)时,它们可提供更好的区域性。仅当要在系统上创建处理器集合时,才应选择非缺省值(不绑定)。可以添加以下行以便通过 /etc/system 取消设置:

set ip:ip_squeue_bind=0

tcp_squeue_wput:控制写入端 squeue 清空行为。

  1. 尝试处理您自己的数据包,但不要尝试清空 squeue。
  2. 尝试处理您自己的数据包以及任何排队的数据包。

缺省值为 2,可以添加以下行以便通过 /etc/system 进行更改:

set ip:tcp_squeue_wput=1

当 CPU 数量远多于活动 NIC 数量时,应该将此值设置为 1;当应用程序线程执行 squeue 清空和被锁定的可能性较高时,平台内在具有较高的内存延迟。

ip_squeue_wait:控制工作线程在处理排队的数据包之前等待的时间,以 ms(毫秒)为单位(假定中断或写入者线程将处理数据包)。对于具有较高通信量的服务器,使用缺省值 10 毫秒就可以了;但如果计算机(如台式机)具有较多交互性通信而出现延迟问题,则应该添加以下设置,通过 /etc/system 将该值设置为 0:

ip:ip_squeue_wait=0

此外,某些协议级调节(如更改 max_buf、高低水印等)是很有益的,对于大型内存系统尤其如此。


8.0 前景

根据预计,Solaris 联网堆栈将继续建立在较好的层间垂直集成的基础之上,这可进一步提高区域性和性能。随着芯片多线程化和多内核 CPU 的出现,预计并行执行管道的数量将继续增加,甚至在低端系统上也是如此。目前,典型的双 CPU 计算机是双内核的,可提供四个执行管道,并且还可能具有超线程功能。

NIC 也变得越来越先进,可通过 MSI-X 提供多个中断;小型分类功能;多个 DMA 通道以及各种无状态卸载(如大片段卸载)等。

将来的工作预计会继续迎合这些硬件趋势,其中包括 TCP 卸载引擎、远程直接内存存取 (Remote Direct Memory Access, RDMA) 和 iSCSI 支持。其他具体的工作领域如下所示:

  • 网络堆栈虚拟化:在整个行业范围内,目前的趋势是服务器合并以及在相同物理实例上运行多个虚拟计算机,因此,能够对 Solaris 堆栈有效地进行虚拟化非常重要。
  • 硬件资源控制:推动网络虚拟化发展的相同趋势也提出了如下要求:有效地控制相同位置中的各种应用程序和虚拟机的带宽使用量。
  • 第三方高性能模块支持:目前的 Solaris 10 框架仍然专用于 Sun 所开发的模块。基于 STREAMS 的模块是 ISV 的唯一选项;因此,它们无法充分挖掘新框架的潜力。
  • 转发性能:进一步提高 Solaris 操作系统转发性能的工作正在进行当中。
  • 网络安全和性能:世界变得越来越复杂,有很多怀有敌意的攻击者正在伺机蠢蠢欲动。从现在起,在性能和安全两者之间进行选择不太可能。这两者都是必须满足的要求。Solaris 软件一直非常注重安全性,Solaris 10 操作系统在不损害性能的前提下又大大改进了安全性。工作重点预计是继续增强 IP 过滤器性能和功能,并提供全新的方法来检测和应付拒绝服务攻击。

9.0 致谢

我们在此对参与本文部分内容编写工作的 Thirumalai Srinivasan、Adi Masputra、Nicolas Droux 和 Eric Cheng 深表谢意。另外,还要感谢 Solaris 联网社区的所有成员的大力帮助。


除非另行颁发许可,否则此处所有技术手册中的代码(包括文章、常见问题解答和样例)只能在本许可下使用。