DOCA UCX
本指南提供关于在 UCX 库之上开发应用程序的说明。
统一通信 X (UCX) 是一个优化的点对点通信框架。
UCX 公开了一组抽象的通信原语,这些原语利用了最佳的可用硬件资源和卸载,例如主动消息、标记发送/接收、远程内存读/写、原子操作以及各种同步例程。 支持的硬件类型包括 RDMA(InfiniBand 和 RoCE)、TCP、GPU 和共享内存。
UCX 通过提供高级 API,屏蔽底层细节,同时保持高性能和可扩展性,从而促进快速开发。
UCX 基于从在世界最大的数据中心和超级计算机上运行的应用程序中获得的累积经验,实现了传输各种大小消息的最佳实践。
UCX 运行时库作为 DOCA 安装的一部分进行安装。
UCX 在主机和 DPU 端的使用方式相同。
系统上任何可用的活动网络设备都可能被 UCX 使用,包括远程对等方可能无法访问的网络设备。
如果无法通过某个网络设备访问其中一个目标(例如,BlueField 无法通过 tmfifo_net0
访问另一个 BlueField),则 UCX 通信可能会失败。
为了解决这个问题,请使用 UCX 环境变量 UCX_NET_DEVICES
来指定 UCX 可以使用的设备。 例如
export UCX_NET_DEVICES=enp3s0f0s0,enp3s0f1s0
或
env UCX_NET_DEVICES=enp3s0f0s0,enp3s0f1s0 <UCX-program>
通过在 BlueField 上使用命令 show_gids
,可以获得 mlx 设备名称和 SF 的端口。 然后,可以使用它来限制 UCX 网络接口并允许 IB。 例如
dpu> show_gids
DEV PORT INDEX GID IPv4 VER DEV
--- ---- ----- --- ------------ --- ---
mlx5_2 1
0
fe80:0000
:0000
:0000
:0052
:72ff:fe63:1651
v2 enp3s0f0s0
mlx5_3 1
0
fe80:0000
:0000
:0000
:0032
:6bff:fe13:f13a v2 enp3s0f1s0
dpu> env UCX_NET_DEVICES=mlx5_2:1
,mlx5_3:1
<UCX-program>
当 RDMACM 不可用时,还需要在 UCX_NET_DEVICES
配置中列出以太网设备,以便它们可以用于基于 TCP 的连接建立。 例如
dpu> env UCX_NET_DEVICES=enp3s0f0s0,enp3s0f1s0,mlx5_2:1
,mlx5_3:1
<UCX-program>
下图描述了 UCX 中间件的软件层。
在上层,各种利用高速通信的应用程序都构建在 UCX 高级 API (UCP) 之上。
UCP 层实现了业务逻辑,以利用、组合和操作不同的传输,从而为不同的用例实现最佳性能。 此逻辑决定了每条消息必须使用哪些传输、要使用哪些类型的基本硬件通信原语、如何分段消息等。
UCT,即传输 API,是一个硬件抽象层,它将不同类型的通信设备统一起来。 UCT API 定义了多个通信原语,但每个传输服务可能只实现其中的一部分——最好是底层硬件本身支持的那些。 UCT 用户(例如,UCP)需要处理 UCT API 定义但传输服务未实现的缺失通信原语。

UCP 对象
本节介绍 UCX 之上编写的大多数应用程序使用的高级通信对象。
UCP 上下文 (ucp_context_h)
上下文是顶层对象,它定义了所有其他 UCX 对象的范围。 可以在同一进程中创建多个上下文,以完全隔离硬件和内存资源。
UCP 工作器 (ucp_worker_h)
工作器代表通信状态及其关联的网络资源。 它负责发送和处理传入消息并处理所有与网络相关的事件。 所有点对点连接都在特定工作器的范围内创建。
工作器对象可以定义为支持来自多个线程的使用。 但是,由于锁争用,当给定工作器大部分时间从一个线程使用时,性能会更好。
工作器通过主动轮询、等待异步事件或两者结合来推进通信。
UCP 端点 (ucp_ep_h)
端点表示从本地工作器到远程工作器的连接。 该远程工作器可以在 UCT 层支持的通信网络之一可到达的任何位置创建。 例如,可以在光纤网络的另一台主机、同一主机、DPU,甚至同一进程中。
UCP 监听器 (ucp_listener_h)
监听器绑定到底层操作系统上的网络端口号,并分派传入的连接请求。 传入的连接请求可用于在服务器(被动)端创建匹配的端点,或被拒绝和释放。
UCP 请求 (ucp_request_h)
请求对象是由非阻塞通信原语之一创建的,用于操作无法立即就地完成的情况。 应用程序应检查请求是否完成,可以通过直接测试请求,或通过将自定义回调与请求关联。
本节介绍用于高速通信的主要 UCX API。 有关完整参考,请参阅 UCX API 规范。
UCX 公开了两种 API:高级 UCP API 和低级 UCT(传输)API。 对于大多数应用程序,建议仅使用 UCP API,因为它减轻了处理每种传输的功能、限制和性能特征的大部分负担。
许多 API 接受带有 field_mask
的结构指针作为参数。 此方法用于提供向后 ABI/API 兼容性:如果引入新的函数参数,它们将作为新字段添加到结构中,因此函数签名不会更改。 此外,field_mask
指定从调用者(用户应用程序)的角度来看哪些结构字段是有效的。 UCX 仅访问此位掩码启用的字段,并对剩余的结构字段使用默认值。
某些 API 需要传递用户定义的回调作为获取特定事件通知的方法。 除非另有说明,否则此类回调是从 ucp_worker_progress()
调用(详见下文)的上下文中调用的,并且预计会快速完成或将其某些任务推迟到另一个线程(以避免超时和来自其他网络事件的处理饥饿)。
UCX 库的 pkg-config(*.pc
文件)名为 ucx
。
以下各节提供了有关库 API 的更多详细信息。
ucs_status_t
一种 enum
类型,用于保存所有 UCX 错误代码。
ucp_init
ucs_status_t ucp_init(const
ucp_params_t *params, const
ucp_config_t *config, ucp_context_h *context_p)
params [in]
– 指向包含可选参数的结构。 除了必须设置的功能外,所有字段都是可选的。config [in]
– 可选,可以为 NULL 以表示默认行为。 可以通过调用 ucp_config_read() 获取配置。注意支持的配置选项可能在 UCX 版本之间有所更改。 可以通过运行 ucx_info CLI 工具获取完整列表
ucx_info -c -f
context_p [out]
– 指向内存中为创建的 UCP 上下文分配的位置的指针
该函数返回由 ucs_status_t
定义的错误代码。
此函数创建一个新的 UCP 顶层上下文,并通过值在 context_p
参数中返回它。
ucp_cleanup
void
ucp_cleanup(ucp_context_h context_p)
context_p [in]
– 一个 UCP 上下文实例
此函数销毁先前创建的上下文。 在调用此函数之前,必须销毁在此上下文中创建的任何其他资源(例如,工作器或端点)。
ucp_worker_create
ucs_status_t ucp_worker_create(ucp_context_h context, const
ucp_worker_params_t *params, ucp_worker_h *worker_p)
context [in]
– 现有的 UCP 上下文params [in]
– 指向包含配置参数的结构。 所有字段都是可选的。 通常,仅使用字段thread_mode
。 可能的thread_mode
值如下UCS_THREAD_MODE_SINGLE
– 只有一个特定的线程(通常是创建工作器的线程)用于访问工作器及其关联的端点。UCS_THREAD_MODE_SERIALIZED
– 多个线程可以访问工作器及其关联的端点,但一次只能访问一个线程。 这意味着应用程序中实现了排除机制(例如,锁定)。 有时,与单线程模式相比,串行化模式需要更昂贵的总线刷新指令。UCS_THREAD_MODE_MULTI
– 多个线程可以在任何给定时间访问工作器。 UCX 在内部处理锁定。 从版本 1.12 开始,它被实现为工作器上的全局锁。
worker_p [out]
– 指向内存中为创建的工作器分配的位置的指针
该函数返回由 ucs_status_t
定义的错误代码。
此函数在先前创建的上下文中创建一个新的 UCP 工作器,并通过值在 worker_p
参数中返回它。
当 ucp_worker_create()
成功时,调用者仍然需要通过调用 ucp_worker_query()
API 来检查工作器创建时的实际线程模式,并在返回的线程模式与预期不符时采取必要的措施(例如,报告错误或回退)。
ucp_worker_destroy
void
ucp_worker_destroy(ucp_worker_h worker)
context_p [in]
– 一个 UCP 工作器实例
此函数销毁先前创建的工作器。 在调用此函数之前,必须销毁所有关联的端点和监听器。
销毁工作器可能会在任何远程对等方上导致通信错误,该远程对等方具有到此工作器的开放端点。 这些错误根据该端点的错误处理配置进行处理(详见 ucp_ep_create
部分)。
ucp_listener_create
ucs_status_t ucp_listener_create(ucp_worker_h worker, const
ucp_listener_params_t *params, ucp_listener_h *listener_p)
worker [in]
– 现有的 UCP 工作器params [in]
– 指向包含配置参数的结构。 字段sockaddr
和conn_handler
是强制性的,但其余字段是可选的。sockaddr
– 指定要监听连接的 IPv4/IPv6 地址。 语义类似于内置的 bind() 函数。INADDR_ANY/INADDR6_ANY
可用于监听所有网络接口。 如果端口号设置为 0,则会选择一个随机的未使用端口。 可以通过调用ucp_listener_query()
API 获取实际端口号。conn_handler
– 用于处理传入连接请求的回调以及关联的用户定义参数。 回调类型定义为void
(*ucp_listener_conn_callback_t) (ucp_conn_request_h conn_request,void
*arg)每当通过此监听器创建远程端点时,都会在监听器端调用此回调,并使用表示传入连接的新
conn_request
对象,以及传递给ucp_listener_create()
的用户定义参数arg
。回调应通过为其创建端点(将
conn_request
作为参数传递给ucp_ep_create
,包括在不同的工作器上)或拒绝并销毁它(调用ucp_listener_reject
)来处理此连接请求。 这不必立即发生。 回调可以将连接请求放在内部应用程序队列中并在以后处理它。
listener_p [out]
– 指向内存中为创建的监听器分配的位置的指针
该函数返回由 ucs_status_t
定义的错误代码。
此函数创建一个新的监听器对象以接受特定网络端口上的传入连接,并通过值在 listener_p
参数中返回它。
ucp_listener_destroy
void
ucp_listener_destroy(ucp_listener_h listener_p)
listener_p [in] – 一个监听器实例
此函数销毁先前创建的监听器。 在调用此函数之前,预计会处理由 conn_handler
报告的任何连接请求。 尚未报告给应用程序的挂起连接请求,或在此函数调用后到达的新连接请求,都将被拒绝。
ucp_ep_create
ucs_status_t ucp_ep_create(ucp_worker_h worker, const
ucp_ep_params_t *params, ucp_ep_h *ep_p)
worker [in]
– 现有的 UCP 工作器params [in]
– 指向包含配置参数的结构。 必须设置创建模式字段。 其他字段是可选的。 常用字段在以下小节中描述。ep_p [in]
– 指向内存中为创建的端点分配的位置的指针
该函数返回由 ucs_status_t
定义的错误代码。
此函数创建一个到远程对等方的新连接,并通过值在 ep_p
参数中返回它。 新端点可以在创建后立即用于通信,尽管某些操作可能会在内部排队并在建立底层连接后发送。
创建模式 (ucp_ep_params_t)
可以通过三种方式创建端点
客户端连接到远程监听器
在这种情况下,sockaddr 字段指定远程 IPv4/IPv6 地址和端口号。 必须启用
flags
字段,并且必须包含UCP_EP_PARAMS_FLAGS_CLIENT_SERVER
标志。 可选地,从 UCX 版本 1.13 开始,可以使用local_sockaddr
字段来指定要绑定的本地源设备地址。服务器由于传入的连接请求而创建端点
在这种情况下,
conn_request
字段必须设置为此连接请求。 这样的端点可以选择在不同的工作器上创建,而不是接受此连接请求的工作器。创建到特定工作器地址的端点
在这种情况下,字段
address
必须设置为指向远程工作器的地址。 该地址(及其长度)必须通过在远程端调用ucp_worker_query(
) 获得,并使用应用程序定义的方法(例如,TCP 套接字或其他现有通信机制)发送。 地址的内部结构是不透明的,并且可能在不同的版本中更改。
用户定义的错误处理 (ucp_ep_params_t)
默认情况下,连接上的意外错误(例如,网络断开或中止的远程进程)会生成致命故障。 为了启用优雅的错误处理,必须在端点创建期间设置几个参数
err_mode 字段必须设置为
UCP_ERR_HANDLING_MODE_PEER
。 这保证了发送请求始终会完成(成功或错误)。 否则,网络错误将被视为致命错误,并中止应用程序,而不会给应用程序执行清理或回退流程的机会。The err_handler.cb
字段必须设置为用户定义的回调,如果在发生连接错误时调用该回调。 错误处理程序定义如下void
(*ucp_err_handler_cb_t)(void
*arg, ucp_ep_h ep, ucs_status_t status)回调参数是用户定义的参数(在 user_data 中传递)、发生错误的端点句柄和错误代码。
在此回调之后,不应在端点上进行更多通信。 应用程序应关闭端点。
The
user_data
字段必须设置为传递给err_handler
回调的用户定义参数
ucs_status_ptr_t
typedef void
* ucs_status_ptr_t;
此函数通常用作非阻塞操作的返回值。
ucs_status_ptr_t
的返回值组合了状态代码和请求指针,请求指针可能是以下之一
一个 NULL 指针,指示操作已就地成功完成。 用户提供的回调(如果有)不会被调用。
一个错误状态,可以通过
UCS_PTR_IS_ERR(status)
宏检测到,并通过UCS_PTR_STATUS(status)
提取。否则,状态是一个请求指针,也可以通过
UCS_PTR_IS_PTR(status)
宏检测到。 这意味着通信操作已启动(或已排队)但尚未完成。 完成情况通过调用用户提供的回调(在ucp_request_param_t
中)或通过调用ucp_request_check_status()
显式检查请求状态来报告。
ucp_ep_close_nbx
ucs_status_ptr_t ucp_ep_close_nbx(ucp_ep_h ep, const
ucp_request_param_t *param)
ep [in]
– 现有的 UCP 端点param [in]
– 指向定义如何执行关闭操作的结构。param
结构的flags
字段指定要用于关闭端点的方法UCP_EP_CLOSE_MODE_FORCE
– 立即关闭端点,而不尝试刷新未完成的操作。 某些已在传输级别完成的请求可能会成功完成,而另一些请求可能会以错误状态完成。 在后一种情况下,不知道它们是否已到达目标进程或在那里完成。以这种方式关闭端点等效于在 TCP 套接字上调用
close()
,并且可能会在远程端生成连接错误。 因此,要使用此模式,本地和远程端点都必须使用设置为UCP_ERR_HANDLING_MODE_PEER
的err_mode
参数创建。UCP_EP_CLOSE_MODE_FLUSH
– 与远程对等方同步并刷新未完成的操作。 某些操作可能会被取消并以状态UCS_ERR_CANCELED
完成。 但是,可以保证它们也没有在远程对等方上完成。
该函数返回一个状态指针以检查操作的状态。 NULL
表示成功。
此函数启动关闭先前创建的端点的过程。 该函数是非阻塞的,返回的值是一个状态指针,用于指示端点何时完全销毁。 有关更多信息,请参阅通信部分。
ucp_request_param_t
struct ucp_request_param_t {
uint32_t op_attr_mask;
uint32_t flags;
union ucp_request_param_t cb;
void
*user_data;
ucp_datatype_t datatype;
/* Some other fields that are rarely used */
…
}
op_attr_mask [in]
– 已启用字段和多个控制标志的掩码。flags [in]
– 特定于操作的标志。 每个 API 方法都为此字段定义了自己的标志集。cb [in]
– 操作完成时的回调。user_data [in]
– 传递给完成回调的用户定义参数。datatype [in]
– 可用于为提供给通信 API 的数据缓冲区(而不是user_data
)指定自定义数据布局。 如果未设置此参数,则数据缓冲区将被视为连续的字节缓冲区。
ucp_request_param_t
的字段指定了几个常用属性和标志,用于控制通信请求的分配和完成方式。 这旨在优化不同的用例。
ucp_worker_progress
unsigned ucp_worker_progress(ucp_worker_h worker)
worker [in]
– 现有的 UCP 工作器
如果任何通信已进行,则该函数返回非零值。 否则,它返回零。
此函数推进工作器上的未完成通信。 这包括轮询硬件和共享内存队列、调用回调、将挂起的操作推送到网络设备、推进复杂协议的状态、推进连接建立过程等等。
尽管某些传输(例如 RDMA)卸载完成了大部分繁重的工作,但通信操作的启动和完成仍然必须由进程显式执行。 UCX 不会生成额外的进度线程。 相反,期望上层应用程序根据需要生成自己的进度线程,以调用 ucp_worker_progress()
。
此函数不能从回调内部使用。
ucp_am_send_nbx
ucs_status_ptr_t ucp_am_send_nbx(ucp_ep_h ep, unsigned id, const
void
*header,
size_t header_length, const
void
*buffer,
size_t count, const
ucp_request_param_t *param)
ep [in]
– 用于发送主动消息的连接。 先前从ucp_ep_create()
返回。id [in]
– 主动消息标识符。 这是一个由应用程序定义的任意 16 位整数值,用于选择要在接收方调用的主动消息回调。 这允许通过不同的回调函数处理不同类型的消息。header [in]
– 指向主动消息的用户定义标头的指针header_length [in]
– 要发送的标头的长度。 通常,标头很小,并且在任何情况下,它都不应大于从ucp_worker_query()
返回的max_am_header
工作器属性。 标头大小可能因可用传输而异,通常预计至少为 256 字节。buffer [in]
– 指向主动消息有效负载的指针count [in]
– 有效负载缓冲区中的元素数。 默认情况下,每个元素都是单个字节,因此这是缓冲区的字节长度。 其他数据布局,例如 IO 向量 (IOV) 列表,可以通过param->datatype
指定。param [in]
– 控制请求完成语义的附加参数。 相关字段仅为flags
,它可以设置为以下标志的组合UCP_AM_SEND_FLAG_REPLY
– 强制将reply_ep
传递给接收方的回调。 这会增加内部标头大小并增加一些开销。UCP_AM_SEND_FLAG_EAGER
– 强制使用 eager 协议(详见下文)。UCP_AM_SEND_FLAG_RNDV
– 强制使用 rendezvous 协议(详见下文)。主动消息可以通过 eager 或 rendezvous 协议发送。 Eager 协议意味着数据缓冲区在回调期间立即在接收方可用,而 rendezvous 协议需要使用对
ucp_am_recv_data_nbx()
的额外调用来获取数据,从而允许将其直接放置到应用程序选择的缓冲区中。 默认情况下,较小的消息通过 eager 协议发送,较大的消息使用 rendezvous 协议。 这可以使用UCP_AM_SEND_FLAG_EAGER
或UCP_AM_SEND_FLAG_RNDV
覆盖。
注意UCP_AM_SEND_FLAG_EAGER 和 UCP_AM_SEND_FLAG_RNDV 是互斥的。
该函数返回一个状态指针以检查操作的状态。 NULL
表示成功。
此函数启动从发起方发送主动消息。 因此,在接收方调用指定的回调(由 ucp_worker_set_am_recv_handler
注册)以处理此消息。 该函数是非阻塞的,因此如果发送操作未立即完成,则会返回请求句柄。
ucp_worker_set_am_recv_handler
ucs_status_t ucp_worker_set_am_recv_handler(ucp_worker_h worker, const
ucp_am_handler_param_t *param)
worker [in]
– 现有的 UCP 工作器。param [in]
– 设置回调配置。 详见下文。
如果任何通信已进行,则该函数返回非零值。 否则,它返回零。
此函数注册一个回调,用于处理给定工作器上的主动消息。
以下是在 param
中设置的强制字段
id
– 要与注册的回调绑定的主动消息标识符。 当接收到具有相同 ID 的传入消息时,将调用回调。arg
– 要传递给主动消息回调的用户定义参数。cb
– 当主动消息到达时要调用的用户定义回调。 回调定义为ucs_status_t (*ucp_am_recv_callback_t)(
void
*arg,const
void
*header, size_t header_length,void
*data, size_t length,const
ucp_am_recv_param_t *param)
以下是从 UCX 传递到回调的参数
arg
– 传递给ucp_worker_set_am_recv_handler
的同一用户定义参数。header
– 指向发送方在发送主动消息时定义的主动消息标头。 标头应由回调使用,因为它在回调返回后无效。header_length
–header
指针的缓冲区的有效大小。data
– 指向数据的指针或不透明句柄,可用于根据字段param->recv_attr
中的UCP_AM_RECV_ATTR_FLAG_RNDV
标志获取数据。 当标志为打开状态时,这是一个不透明句柄。length
– 主动消息数据的长度(即使 data 参数是不透明句柄而不是实际数据)。param
– 指向传入消息的附加参数的指针。 相关字段是recv_attr
– 提供有关传入消息更多信息的标志。reply_ep
– 如果在recv_attr
中设置了UCP_AM_RECV_ATTR_FIELD_REPLY_EP
,则此字段包含一个端点的句柄,该端点可用于向主动消息发送方发送回复。
如果消息数据已被使用或如果在 recv_attr
中设置了 UCP_AM_RECV_ATTR_FLAG_RNDV
,则回调应返回 UCS_OK
。 否则,如果在 recv_attr
中设置了 UCP_AM_RECV_ATTR_FLAG_DATA
,则允许回调保留数据以供以后处理(例如,通过将其添加到内部应用程序队列)。 在这种情况下,回调应返回 UCS_INPROGRESS
,以指示数据应持久存在。
当消息到达时带有 UCP_AM_RECV_ATTR_FLAG_RNDV
标志时,必须使用函数 ucp_am_recv_data_nbx
从发送方获取数据。
ucp_am_recv_data_nbx
ucs_status_ptr_t ucp_am_recv_data_nbx(ucp_worker_h worker, void
*data_desc,
void
*buffer, size_t count,
const
ucp_request_param_t *param)
worker [in]
– 用于启动接收操作的 UCP 工作器对象。注意不需要连接句柄(端点)。
data_desc [in]
– 要接收的数据的句柄。 从主动消息回调的data
参数获得。buffer [in]
– 传入数据的接收缓冲区。count [in]
– 有效负载缓冲区中的元素数。 默认情况下,每个元素都是单个字节,因此这是缓冲区的字节长度。 其他数据布局,例如 IOV 列表,可以通过param->datatype
指定。param [in]
– 控制请求分配和完成报告的附加参数。 此函数不需要特定的标志。
该函数返回一个状态指针以检查操作的状态。 NULL
表示成功。
此函数用于 rendezvous 主动消息。 该函数启动从发送方获取数据到应用程序定义的接收缓冲区的过程。 当使用在 params->recv_attr
字段中设置了 UCP_AM_RECV_ATTR_FLAG_RNDV
标志的主动消息回调时,预计会使用它。
初始化
使用 UCX 的应用程序通常会创建一个全局上下文 (ucp_context_h
),然后创建一个或多个工作器 (ucp_worker_h
)。 每个工作器都会消耗一些内存用于发送/接收缓冲区,因此不建议创建太多工作器。 经验法则是工作器的数量应大致与 CPU 内核/线程的数量相关。
工作器到线程的映射由应用程序的用例定义,例如
单线程应用程序不需要多个工作器
多线程应用程序的简单实现可以在多线程模式下创建一个或多个工作器。 这些工作器可以被任何线程使用。
线程与 CPU 内核之间具有强关联性的多线程应用程序可以为每个线程创建一个专用工作器。 这些工作器可以在单线程模式下创建。
具有许多线程的应用程序可以实现工作器池,并随机使用一个或临时分配一些给线程。
如果有多个工作器,则每个工作器都需要创建自己的端点集,因为每个端点都连接一对特定的工作器。
为了启动通信,应用程序应创建连接到远程对等方的端点 (ucp_ep_h
)。 创建端点有两种主要方法:直接连接到远程工作器的地址,或创建监听器对象 (ucp_listener_h
) 并连接到远程 IP 地址和端口。 这些方法在 ucp_ep_create()
部分中更详细地描述。
通信
初始化 UCP 上下文、工作器和端点后,应用程序可以开始使用端点进行通信。 通常,端点与表示连接的应用程序级对象相关联。
大多数通信操作都遵循类似的模式:非阻塞函数(带有 _nbx
后缀)接收指向 ucp_request_param_t
结构的指针,并返回 ucs_status_ptr_t
。 使用结构指针允许扩展操作,同时保持向后兼容性。
UCP 支持多种通信方法,旨在用于不同类型的应用程序。 对于大多数应用程序,推荐的方法是主动消息,这意味着发起者可以将任意数据发送给响应者,并且响应者调用可以访问此数据的回调。