高级MPI并行程序设计
这一部分为了提高MPI程序的性能、通用性、移植性等而引入的MPI高级特性,包括非阻塞通信、组通信、不连续的数据的传送以及虚拟进程拓扑等。
非阻塞通信的MPI程序设计
非阻塞通信主要用于实现计算与通信的重叠。所有阻塞通信的形式都有相应的非阻塞通 信的形式。
非阻塞标准发送和接收
MPI_ISEND(buf, count, datatype, dest, tag, comm, request)
MPI_IRECV(buf, count, datatype, source, tag, comm, request)
MPI_ISend和MPI_IRecv启动操作后立即返回,和阻塞发送和接收调用相比,它多了一个接受返回的参数request。这一参数是非阻塞通信对象。通过对这一对象的查询,就可以知道与之相应的非阻塞发送和接收是否完成。
int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request)
int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
非阻塞通信与其他三种通信模式的组合
MPI使用与阻塞通信一样的命名,约定前缀B,S,R分别表示缓存通信模式,同步通信模式和就绪通信模式,前缀I(immediate) 表示这个调用是非阻塞的。
①MPI_ISSEND开始一个同步模式的非阻塞发送 它的返回只是意味着相应的接收操作已经启动,并不表示消息发送的完成。
int MPI_Issend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
②MPI_IBSEND开始一个缓存模式的非阻塞发送 与阻塞发送一样 它也需要程序员主动 为该发送操作提供发送缓冲区。
int MPI_Ibsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
③MPI_IRSEND开始一个接收就绪通信模式非阻塞发送 与阻塞通信一样,它也要求当这 一调用启动之前 相应的接收操作必须已经启动,否则回出错。
int MPI_Irsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
非阻塞通信的完成
不管非阻塞通信是什么样的形式 对于完成调用是不加区分的。
单个非阻塞调用的完成
MPI有两个接口来实现对非阻塞通信是否完成的监控。
MPI_WAIT(request, status)
输入输出: request,非阻塞通信对象
输出: status,返回的状态
MPI_WAIT以非阻塞通信对象为参数,一直等到与该非阻塞通信对象相应的非阻塞通信完成后才返回,同时释放该阻塞通信对象 ,不需要再显式释放该对象。与该非阻塞通信完成有关的信息放在返回的状态参数status中。
int MPI_Wait(MPI_Request *request, MPI_Status *status)
MPI_TEST(request, flag, status)
输入输出:request,非阻塞通信对象
输出: flag,操作是否完成的标志
输出: status,返回的状态
这个接口与wait的区别是,他不必等待非阻塞通信对象所相应的通信结束才返回。若在调用MPI_TEST时,该非阻塞通信已经结束,则完成标志flag=true。若非阻塞通信还没有完成,则它不必等待该非阻塞通信的完成,可以直 接返回,但是完成标志flag=false,同时也不释放相应的非阻塞通信对象。
int MPI_Wait(MPI_Request *request, MPI_Status *status)
多个非阻塞调用的完成
除了一次完成一个非阻塞通信的调用外,MPI还提供其它的调用,可以一次完成多个已经启动的非阻塞通信调用。
*MPI_WAITANY(count, array_of_requests, index, status) *
输入: count,非阻塞通信对象的个数
输入输出:arrary_of_requests,非阻塞通信完成对象数组(句柄数组)
输出: index,完成对象对应的句柄索引
输出:status,返回的状态
MPI_WAITANY用于等待非阻塞通信对象表中任何一个非阻塞通信对象的完成,释放已完成的非阻塞通信对象,然后返回 MPI_WAITANY返回后index=i,即MPI_WAITANY完成的是非阻塞通信对象表中的第i个对象对应的非阻塞通信,则其效果等价于调用了MPI_WAIT(array_of_requests[I],status)。
int MPI_Waitany(int count, MPI_Request *array_of_requests, int *index, MPI_Status *status)
MPI_WAITALL( count, array_of_requests, array_of_statuses)
输入: count,非阻塞通信对象的个数
输入输出:arrary_of_requests,非阻塞通信完成对象数组(句柄数组)
输出:array_of_statuses,状态数组
MPI_WAITALL必须等到非阻塞通信对象表中所有的非阻塞通信对象相应的非阻塞操作都完成后才返回。
int MPI_Waitall(int count, MPI_Request *array_of_requests, MPI_Status *array_of_statuses)
MPI_WAITSOME(incount,array_of_requests,outcount,array_of_indices,array_of_statuses)
IN incount 非阻塞通信对象的个数(整型)
INOUT array_of_requests 非阻塞通信对象数组(句柄数组)
OUT outcount 已完成对象的数目(整型)
OUT array_of_indices 已完成对象的下标数组(整型数组)
OUT array_of_statuses 已完成对象的状态数组(状态数组)
MPI_WAITSOME只要有一个或多个非阻塞通信完成,则该调用就返回。完成非阻塞通信的对象的个数记录在outcount中,相应下标记录在数组array_of_indices中,完成对象的状态记录在状态数组array_of_statuses。
int MPI_Waitsome(int incount,MPI_Request *array_of_request, int *outcount, int *array_of_indices, MPI_Status *array_of_statuses)
MPI_TESTANY(count, array_of_requests, index, flag, status)
IN count 非阻塞通信对象的个数(整型)
INOUT array_of_requests 非阻塞通信对象数组(句柄数组)
OUT index 非阻塞通信对象的索引或MPI_UNDEFINED (整型)
OUT flag 是否有对象完成(逻辑型)
OUT status 状态(状态类型)
MPI_TESTANY用于测试非阻塞通信对象表中是否有任何一个对象已经完成,用flag来记录有则true,无则false返回。
int MPI_Testany(int count, MPI_Request *array_of_requests, int *index, int *flag, MPI_Status *status)
MPI_TESTALL(count, array_of_requests, flag, array_of_statuses)
IN count 非阻塞通信对象的个数(整型)
INOUT array_of_requests 非阻塞通信对象数组(句柄数组)
OUT flag 所有非阻塞通信对象是否都完成(逻辑型)
OUT array_of_statuses 状态数组(状态数组)
MPI_TESTALL只有当所有的非阻塞通信对象都完成时,才使得flag=true返回,并且释放所有的查询对象,只要有一个非阻塞通信对象没有完成 ,则令flag=false立即返回。
int MPI_Testall(int count, MPI_Request *array_of_requests, int *flag, MPI_Status *array_of_statuses)
MPI_TESTSOME(incount,array_of_requests,outcount,array_of_indices,array_of_statuses)
IN incount 非阻塞通信对象的个数(整型)
INOUT array_of_requests 非阻塞通信对象数组(句柄数组)
OUT outcount 已完成对象的数目(整型)
OUT array_of_indices 已完成对象的下标数组(整型数组)
OUT array_of_statuses 已完成对象的状态数组(状态数组)
MPI_TESTSOME和MPI_WAITSOME类似 只不过它可以立即返回 ,outcount记录完成的个数。
int MPI_Testsome(int incount,MPI_Request *array_of_request, int *outcount, int *array_of_indices, MPI_Status *array_of_statuses)
非阻塞通信对象
非阻塞通信对象是MPI内部的对象,通过一个句柄存取,使用非阻塞通信对象可以识别非阻塞通信操作的各种特性,例如发送模式和它联结的通信缓冲区,通信上下文,用于发送的标识和目的参数,或用于接收的标识和源参数, 此外非阻塞通信对象还存储关于这个挂起通信操作状态的信息。
取消非阻塞通信对象
MPI_CANCEL(request)
IN request 非阻塞通信对象(句柄)
取消调用并不意味着相应的通信一定会被取消。若取消操作调用时相应的非阻塞通信已经开始 ,则它会正常完成 。若取消操作调用时相应的非阻塞通信还没有开始,则可以释放通信占用的资源,取消该非阻塞通信,对于非阻塞通信,即使调用了取消操作,也必须调用非阻塞通信的完成操作或查询对象的释放操作来释放查询对象。如果一个非阻塞通信已经被执行了取消操作,则该通信的MPI_WAIT或MPI_TEST将释放取消通信的非阻塞通信对象,并且在返回结果status中指明该通信已经被取消。
这是一个立即返回的调用。
int MPI_Cancel(MPI_Request *request)
MPI_TEST_CANCELLED(status,flag)
IN status 状态 (状态类型)
OUT flag 是否取消标志(逻辑类型)
该接口用于测试一个通信操作是否被取消,flag=true则取消成功。
int MPI_Test_cancelled(MPI_Status status, int *flag)
释放非阻塞通信对象
除了调用非阻塞对象的完成操作可以取消非阻塞对象,还提供了一个接口用于取消该对象。
MPI_REQUEST_FREE(request)
INOUT request 非阻塞通信对象
一旦执行了释放操作,非阻塞通信对象就无法再通过其它任何的调用访问。request变为MPI_REQUEST_NULL 。如果与该非阻塞通信对象相联系的通信还没有完成,则该对象的资源并不会立即释放,它将等到该非阻塞通信结束后再释放。
int MPI_Request_free(MPI_Request * request)
消息到达的检查
在执行接收操作先,可以先调用接口检查相应发送的消息是否到达。
MPI_PROBE (source,tag,cmm,status)
IN source 源进程标识或任意进程标识MPI_ANY_SOURCE(整型)
IN tag 特定tag值或任意tag值MPI_ANY_TAG 整型
IN comm 通信域 句柄
OUT status 返回的状态 状态类型
该接口为阻塞调用。
int MPI_Probe(int source,int tag,MPI_Comm comm,MPI_Status *status)
MPI_IPROBE(source,tag,comm,flag,status)
IN source 源进程标识或任意进程标识MPI_ANY_SOURCE 整型
IN tag 特定tag值或任意tag值MPI_ANY_TAG 整型
IN comm 通信域 句柄
OUT flag 是否有消息到达标志 逻辑
OUT status 返回的状态 状态类型
相应的非阻塞调用,flag记录是否有相应信封的消息到达。若flag=true,MPI_IPROBE的status和MPI_RECV的status的返回值相同,否则status为空。MPI_IPROBE的source参数可以是MPI_ANY_SOURCE tag参数,可以是MPI_ANY_TAG ,以便用户可以检查来自不确定的源source以及不确定的标识tag 。
int MPI_Iprobe(int source,int tag,MPI_Comm comm,int *flag, MPI_Status *status)
注意:非阻塞通信和阻塞通信相同,符合有序接收的语义约束。
重复非阻塞通信
重复非阻塞通信需要如下步骤 :
1 通信的初始化 比如MPI_SEND_INIT
2 启动通信 MPI_START
( 重复 )
3 完成通信 MPI_WAIT
4 释放查询对象 MPI_REQUEST_FREE
当不需要再进行通信时,必须通过显式的语句MPI_REQUEST_FREE将非阻塞通信对象释放掉。只有当该对象成为非活动状态时才可以被取消。
根据通信模式的不同,重复非阻塞通信也有四种不同的形式 即标准模式 同步模式 缓存模式和就绪模式。
标准模式:
int MPI_Send_init(void* buf, int count, MPI_Data type,int dest, int tag, MPI_Comm comm, MPI_Request *request)
同步模式:
int MPI_Bsend_init(void* buf,int count,MPI_Datatype datatype,int dest, int tag, MPI_Comm comm,MPI_Request *request)
缓存模式:
int MPI_Ssend_init(void* buf,int count,MPI_Datatype datatype,int dest, int tag, MPI_Comm comm,MPI_Request *request)
就绪模式:
int MPI_Rsend_init(void* buf,int count,MPI_Datatype datatype,int dest, int tag, MPI_Comm comm,MPI_Request *request)
创建一个标准模式的重复非阻塞通信的接收操作:
MPI_RECV_INIT(buf,count,datatype,source,tag,comm,request)
OUT buf 接收缓冲区初始地址(可选数据类型)
IN count 接收数据的最大个数(整型)
IN datatype 接收数据的数据类型(句柄)
IN source 发送进程的标识或任意进程MPI_ANY_SOURCE(整型)
IN tag 消息标识或任意标识MPI_ANY_TAG(整型)
IN comm 通信域(句柄)
OUT request 非阻塞通信对象(句柄)
int MPI_Recv_init(void* buf,int count,MPI_Datatype datatype,int source, int tag, MPI_Comm comm,MPI_Request *request)
重复非阻塞通信的激活操作:
MPI_START(request)
INOUT request 非阻塞通信对象(句柄)
request参数是一个由初始化非阻塞重复调用返回的句柄 。一个用MPI_SEND_INIT创建的非阻塞重复通信对象来调用MPI_START产生的通信和用 MPI_ISEND调用产生的通信效果一样 。其他同理。
int MPI_Start(MPI_Request *request)
*MPI_STARTALL(count,array_of_requests) *
IN count 开始非阻塞通信对象的个数 (整型)
IN array_of_requests 非阻塞通信对象数组(句柄队列)
一个用MPI_START 初始化的发送操作可以被任何接收操作匹配,类似地,一个用 MPI_START初始化的接收操作可以接收任何发送操作产生的消息.
int MPI_Startall(int count, MPI_Request *array_of_requests)
组通信MPI程序设计
组通信一般实现三个功能,通信、同步和计算 。对于组通信,按通信的方向的不同,又可以分为以下三种 :一对多通信、多对一通信和多对多通信。
MPI组通信的计算功能是分三步实现的,首先是通信的功能,即消息根据要求发送到目的进程,目的进程也已经接收到了各自所需要的消息,然后是对消息的处理,即计算部分,MPI组通信有计算功能的调用都指定了计算操作,用给定的计算操作对接收到的数据进行处理,最后一步是将处理结果放入指定的接收缓冲区。
广播
MPI_BCAST(buffer,count,datatype,root,comm)
IN/OUT buffer 通信消息缓冲区的起始地址(可选数据类型)
IN count 将广播出去/或接收的数据个数(整型)
IN datatype 广播/接收数据的数据类型(句柄)
IN root 广播数据的根进程的标识号(整型)
IN comm 通信域
一对多通信:对于广播操作的调用,不管是广播进程还是接收进程,在调用形式上完全一致。其执行结 果是将根进程通信消息缓冲区中的消息拷贝到其他所有进程中去。
int MPI_Bcast(void* buffer,int count,MPI_Datatype datatype,int root, MPI_Comm comm)
收集
MPI_GATHER(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root , comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
IN sendcount 发送消息缓冲区中的数据个数(整型)
IN sendtype 发送消息缓冲区中的数据类型(句柄)
OUT recvbuf 接收消息缓冲区的起始地址(可选数据类型)
IN recvcount 待接收的元素个数(整型,仅对于根进程有意义)
IN recvtype 接收元素的数据类型(句柄,仅对于根进程有意义)
IN root 接收进程的序列号(整型)
IN comm 通信域(句柄)
多对一通信:每个调用这个接口的函数都执行发送操作,但接收消息仅对于根进程有效。其结果就象一个进程组中的N个进程,包括根进程在内,都执行了一个发送调用,同时根进程执行了N次接收调用 。根进程根据发送进程的进程标识的序号即进程的rank值,将它们各自的消息依次存放到自已的消息缓冲区中 。从各个进程收集到的数据的个数必须相同。
int MPI_Gather(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm)
MPI_GATHERV(sendbuf, sendcount, sendtype, recvbuf, recvcounts, displs,recvtype, root, comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
IN sendcount 发送消息缓冲区中的数据个数(整型)
IN sendtype 发送消息缓冲区中的数据类型(句柄)
OUT recvbuf 接收消息缓冲区的起始地址(可选数据类型,仅对于根进程有意义)
IN recvcounts 整型数组(长度为组的大小), 其值为从每个进程接收的数据个数
IN displs 整数数组,每个入口表示相对于recvbuf的位移
IN recvtype 接收消息缓冲区中数据类型 (句柄)
IN root 接收进程的标识号(句柄)
IN comm 通信域(句柄)
这个接口同样为多对一通信的收集调用,与MPI_GATHER不同的是,根从每一个进程接收的数据元素的个数可以不同,但是发送和接收的个数必须一致。除此之外,它还为每一个接收消息在接收缓冲区的位置提供了一个位置偏移displs数组,用户可以将接收的数据存放到根进程消息缓冲区的任意位置 。
int MPI_Gatherv(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int *recvcounts, int *displs, MPI_Datatype recvtype, int root, MPI_Comm comm)
散发
MPI_SCATTER(sendbuf,sendcount,sendtype,recvbuf,recvcount,recvtype, root,comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
IN sendcount 发送到各个进程的数据个数(整型)
IN sendtype 发送消息缓冲区中的数据类型(句柄)
OUT recvbuf 接收消息缓冲区的起始地址(可选数据类型)
IN recvcount 待接收的元素个数(整型)
IN recvtype 接收元素的数据类型(句柄)
IN root 发送进程的序列号(整型)
IN comm 通信域(句柄)
MPI_SCATTER是一对多的组通信调用,和广播不同,ROOT向各个进程发送的数据可以是不同的。根进程发送元素个数指的是发送给每一个进程的数据元素的个数,而不是总的数据个数 。MPI_SCATTER和MPI_GATHER的效果正好相反,两者互为逆操作。
MPI_SCATTERV(sendbuf, sendcounts, displs, sendtype, recvbuf, recvcount, recvtype, root, comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
IN sendcounts 发送数据的个数整数数组 (整型)
IN displs 发送数据偏移 整数数组(整型)
IN sendtype 发送消息缓冲区中元素类型(句柄)
OUT recvbuf 接收消息缓冲区的起始地址(可变)
IN recvcount 接收消息缓冲区中数据的个数(整型)
IN recvtype 接收消息缓冲区中元素的类型(句柄)
IN root 发送进程的标识号(句柄)
IN comm 通信域(句柄)
MPI_SCATTERV对MPI_SCATTER的功能进行了扩展,它允许ROOT向各个进程发送个数不等的数据。因此要求sendcounts是一个数组,同时还提供一个新的参数displs指明根进程发往其它不同进程数据在根发送缓冲区中的偏移位置。
int MPI_Scatterv(void* sendbuf, int *sendcounts, int *displs, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm)
组收集
MPI_ALLGATHER(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype,comm)
参数类型与MPI_GATHER完全一致。
MPI_GATHER是将数据收集到ROOT进程,而MPI_ALLGATHER相当于每一个进程都作为ROOT执行了一次MPI_GATHER调用 ,即每一个进程都收集到了其它所有进程的数据。
MPI_ALLGATHERV(sendbuf, sendcount, sendtype, recvbuf, recvcounts, displs, recvtype, comm)
参数类型与MPI_GATHERV完全一致。
MPI_ALLGATHERV也是所有的进程都将接收结果,而不是只有根进程接收结果。从每个进程发送的第j块数据将被每个进程接收,然后存放在各个进程接收消息缓冲区recvbuf的第j块,进程j的sendcount和sendtype的类型必须和其他所有进程的recvcounts[j]和recvtype相同。
全互换
MPI_ALLTOALL(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
IN sendcount 发送到每个进程的数据个数(整型)
IN sendtype 发送消息缓冲区中的数据类型(句柄)
OUT recvbuf 接收消息缓冲区的起始地址(可选数据类型)
IN recvcount 从每个进程中接收的元素个数(整型)
IN recvtype 接收消息缓冲区的数据类型(句柄)
IN comm 通信域(句柄)
MPI_ALLTOALL是组内进程之间完全的消息交换,其中每一个进程都相其它所有的进程发送消息,同时每一个进程都从其它所有的进程接收消息。每个进程和根进程之间,发送的数据量必须和接收的数据量相等。调用MPI_ALLTOALL相当于每个进程依次将它的发送缓冲区的第i块数据发送给第i个进程,同时每个进程又都依次从第j个进程接收数据放到各自接收缓冲区的第j块数据区的位置。
int MPI_Alltoall(void* sendbuf, int sendcount, MPI_Datatype sendtype,void* recvbuf,int recvcount, MPI_Datatype recvtype,MPI_Comm comm)
MPI_ALLTOALLV(sendbuf, sendcounts, sdispls, sendtype, recvbuf, recvcounts, rdispls, recvtype, comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
IN sendcounts 向每个进程发送的数据个数(整型数组)
IN sdispls 向每个进程发送数据的位移 整型数组
IN sendtype 发送数据的数据类型(句柄)
OUT recvbuf 接收消息缓冲区的起始地址(可选数据类型)
IN recvcounts 从每个进程中接收的数据个数(整型数组)
IN rdispls 从每个进程接收的数据在接收缓冲区的位移 整型数组
IN recvtype 接收数据的数据类型(句柄)
IN comm 通信域(句柄)
MPI_ALLTOALLV 在 MPI_ALLTOALL的基础上进一步增加了灵活性。它可以由sdispls指定待发送数据的位置,在接收方则由rdispls指定接收的数据存放在缓冲区的偏移量 。
MPI_ALLTOALL和MPI_ALLTOALLV可以实现n次独立的点对点通信 但也有限制 :
1) 所有数据必须是同一类型
2)所有的消息必须按顺序进行散发和收集
同步
- MPI_BARRIER(comm)
MPI_BARRIER阻塞所有的调用者直到所有的组成员都调用了它,各个进程中这个调用才可以返回 。
int MPI_Barrier(MPI_Comm comm)
规约
MPI_REDUCE(sendbuf,recvbuf,count,datatype,op,root,comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
OUT recvbuf 接收消息缓冲区中的地址(可选数据类型)
IN count 发送消息缓冲区中的数据个数(整型)
IN datatype 发送消息缓冲区的元素类型(句柄)
IN op 归约操作符(句柄)
IN root 根进程序列号(整型)
IN comm 通信域(句柄)
MPI_REDUCE将组内每个进程输入缓冲区中的数据按给定的操作op进行运算,并将其结果返回到序列号为root的进程的输出缓冲区中。要求发送和接收的元素数目和类型都必须相同。
int MPI_Reduce(void* sendbuf, void* recvbuf, int count, PI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm)
- *MPI_ALLREDUCE(sendbuf, recvbuf, count, datatype, op, comm) *
组规约:组归约MPI_ALLREDUCE就,相当于组中每一个进程都作为ROOT分别进行了一次归约操作。即归约的结果不只是某一个 进程拥有 而是所有的进程都拥有。
- MPI_REDUCE_SCATTER(sendbuf, recvbuf, recvcounts, datatype, op, comm)
规约并发散:参数设置与规约操作相同。MPI_REDUCE_SCATTER操作可以认为是MPI对每个归约操作的变形,它将归约结果分 散到组内的所有进程中去,而不是仅仅归约到ROOT进程。
扫描
MPI_SCAN(sendbuf, recvbuf, count, datatype, op, comm)
IN sendbuf 发送消息缓冲区的起始地址(可选数据类型)
OUT recvbuf 接收消息缓冲区的起始地址(可选数据类型)
IN count 输入缓冲区中元素的个数(整型)
IN datatype 输入缓冲区中元素的类型(句柄)
IN op 操作(句柄)
IN comm 通信域(句柄)
可以将扫描看作是一种特殊的归约,即每一个进程都对排在它前面的进程进行归约操作。MPI_SCAN调用的结果是 对于每一个进程i,它对进程0,…,i的发送缓冲区的数据进行指定的归约操作 结果存入进程i的接收缓冲区 。