设为首页 收藏本站
查看: 2013|回复: 0

[经验分享] 使用异步 I/O 大大提高应用程序的性能(来自IBM)

[复制链接]

尚未签到

发表于 2017-5-27 10:37:25 | 显示全部楼层 |阅读模式

AIO简介


Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强。它是 2.6 版本内核的一个标准特性,但是我们在 2.4 版本内核的补丁中也可以找到它。AIO 背后的基本思想是允许进程发起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍后或在接收到 I/O 操作完成的通知时,进程就可以检索 I/O 操作的结果


I/O模型


在深入介绍 AIO API 之前,让我们先来探索一下 Linux 上可以使用的不同 I/O 模型。这并不是一个详尽的介绍,但是我们将试图介绍最常用的一些模型来解释它们与异步 I/O 之间的区别。图 1 给出了同步和异步模型,以及阻塞和非阻塞的模型。

DSC0000.gif

  图1 同步&异步模型、阻塞&非阻塞模型



每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。本节将简要对其一一进行介绍。


同步阻塞 I/O



I/O 密集型与 CPU 密集型进程的比较



I/O 密集型进程所执行的 I/O 操作比执行的处理操作更多。CPU 密集型的进程所执行的处理操作比 I/O 操作更多。Linux 2.6 的调度器实际上更加偏爱 I/O 密集型的进程,因为它们通常会发起一个 I/O 操作,然后进行阻塞,这就意味着其他工作都可以在两者之间有效地交错进行。







最常用的一个模型是同步阻塞 I/O 模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再占用 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。


图 2 给出了传统的阻塞 I/O 模型,这也是目前应用程序中最为常用的一种模型。其行为非常容易理解,其用法对于典型的应用程序来说都非常有效。在调用read 系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被移动到用户空间的缓冲区中。然后应用程序就会解除阻塞(read 调用返回)。



图 2. 同步阻塞 I/O 模型的典型流程
DSC0001.gif




从应用程序的角度来说,read 调用会延续很长时间。实际上,在内核执行读操作和其他工作时,应用程序的确会被阻塞。


同步非阻塞 I/O


同步阻塞 I/O 的一种效率稍低的变种是同步非阻塞 I/O。在这种模型中,设备是以非阻塞的形式打开的。这意味着 I/O 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或EWOULDBLOCK),如图 3 所示。



图 3. 同步非阻塞 I/O 模型的典型流程

DSC0002.gif


非阻塞的实现是 I/O 命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。正如图 3 所示的一样,这个方法可以引入 I/O 操作的延时,因为数据在内核中变为可用到用户调用read 返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。


异步阻塞 I/O


另外一个阻塞解决方案是带有阻塞通知的非阻塞 I/O。在这种模型中,配置的是非阻塞 I/O,然后使用阻塞
select 系统调用来确定一个 I/O 描述符何时有操作。使 select 调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。对于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。



图 4. 异步阻塞 I/O 模型的典型流程 (select)

DSC0003.gif


select 调用的主要问题是它的效率不是非常高。尽管这是异步通知使用的一种方便模型,但是对于高性能的 I/O 操作来说不建议使用。


异步非阻塞 I/O(AIO)


最后,异步非阻塞 I/O 模型是一种处理与 I/O 重叠进行的模型。读请求会立即返回,说明
read 请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当 read 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。



图 5. 异步非阻塞 I/O 模型的典型流程

DSC0004.gif


在一个进程中为了执行多个 I/O 请求而对计算操作和 I/O 处理进行重叠处理的能力利用了处理速度与 I/O 速度之间的差异。当一个或多个 I/O 请求挂起时,CPU 可以执行其他任务;或者更为常见的是,在发起其他 I/O 的同时对已经完成的 I/O 进行操作。


下一节将深入介绍这种模型,探索这种模型使用的 API,然后展示几个命令。





回页首


异步 I/O 的动机


从前面 I/O 模型的分类中,我们可以看出 AIO 的动机。同步阻塞IO模型需要在 I/O 操作开始时阻塞应用程序,这意味着不可能同时重叠进行处理和 I/O 操作;同步非阻塞IO模型允许处理和 I/O 操作重叠进行,但是这需要应用程序根据重现的规则来检查 I/O 操作的状态;这样就剩下异步非阻塞 I/O模型 了,它允许处理和 I/O 操作重叠进行,包括 I/O 操作完成的通知。


除了需要阻塞之外,select 函数所提供的功能(异步阻塞 I/O)与 AIO 类似。不过,它是对通知事件进行阻塞,而不是对 I/O 调用进行阻塞。





回页首


Linux 上的 AIO 简介


本节将探索 Linux 的异步 I/O 模型,从而帮助我们理解如何在应用程序中使用这种技术。


在传统的 I/O 模型中,有一个使用惟一句柄标识的 I/O 通道。在 UNIX® 中,这些句柄是文件描述符(这对等同于文件、管道、套接字等等)。在阻塞 I/O 中,我们发起了一次传输操作,当传输操作完成或发生错误时,系统调用就会返回。



Linux 上的 AIO



AIO 在 2.5 版本的内核中首次出现,现在已经是 2.6 版本的产品内核的一个标准特性了。







在异步非阻塞 I/O 中,我们可以同时发起多个传输操作。这需要每个传输操作都有惟一的上下文,这样我们才能在它们完成时区分到底是哪个传输操作完成了。在 AIO 中,这是一个aiocb(AIO I/O Control Block)结构。这个结构包含了有关传输的所有信息,包括为数据准备的用户缓冲区。在产生 I/O (称为完成)通知时,aiocb 结构就被用来惟一标识所完成的 I/O 操作。这个 API 的展示显示了如何使用它。





回页首


AIO API


AIO 接口的 API 非常简单,但是它为数据传输提供了必需的功能,并给出了两个不同的通知模型。表 1 给出了 AIO 的接口函数,本节稍后会更详细进行介绍。



表 1. AIO 接口 API

API 函数说明


aio_read


请求异步读操作




aio_error


检查异步请求的状态




aio_return


获得完成的异步请求的返回状态




aio_write


请求异步写操作




aio_suspend


挂起调用进程,直到一个或多个异步请求已经完成(或失败)




aio_cancel


取消异步 I/O 请求




lio_listio


发起一系列 I/O 操作



每个 API 函数都使用 aiocb 结构开始或检查。这个结构有很多元素,但是清单 1 仅仅给出了需要(或可以)使用的元素。



清单 1. aiocb 结构中相关的域


  
struct aiocb {
int aio_fildes;               // File Descriptor
int aio_lio_opcode;           // Valid only for lio_listio (r/w/nop)
volatile void *aio_buf;       // Data Buffer
size_t aio_nbytes;            // Number of Bytes in Data Buffer
struct sigevent aio_sigevent; // Notification Structure
/* Internal fields */
...
};



sigevent 结构告诉 AIO 在 I/O 操作完成时应该执行什么操作。我们将在 AIO 的展示中对这个结构进行探索。现在我们将展示各个 AIO 的 API 函数是如何工作的,以及我们应该如何使用它们。


aio_read


aio_read 函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。aio_read 函数的原型如下:



int aio_read( struct aiocb *aiocbp );





aio_read 函数在请求进行排队之后会立即返回。如果执行成功,返回值就为 0;如果出现错误,返回值就为 -1,并设置errno 的值。


要执行读操作,应用程序必须对 aiocb 结构进行初始化。下面这个简短的例子就展示了如何填充aiocb 请求结构,并使用aio_read来执行异步读请求(现在暂时忽略通知)操作。它还展示了
aio_error 的用法,不过我们将稍后再作解释。



清单 2. 使用 aio_read 进行异步读操作的例子


  
#include <aio.h>
...
int fd, ret;
struct aiocb my_aiocb;
fd = open( "file.txt", O_RDONLY );
if (fd < 0) perror("open");
/* Zero out the aiocb structure (recommended) */
bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
/* Allocate a data buffer for the aiocb request */
my_aiocb.aio_buf = malloc(BUFSIZE+1);
if (!my_aiocb.aio_buf) perror("malloc");
/* Initialize the necessary fields in the aiocb */
my_aiocb.aio_fildes = fd;
my_aiocb.aio_nbytes = BUFSIZE;
my_aiocb.aio_offset = 0;
ret = aio_read( &my_aiocb );
if (ret < 0) perror("aio_read");
while ( aio_error( &my_aiocb ) == EINPROGRESS ) ;
if ((ret = aio_return( &my_iocb )) > 0) {
/* got ret bytes on the read */
} else {
/* read failed, consult errno */
}





在清单 2 中,在打开要从中读取数据的文件之后,我们就清空了 aiocb 结构,然后分配一个数据缓冲区。并将对这个数据缓冲区的引用放到aio_buf 中。然后,我们将aio_nbytes 初始化成缓冲区的大小。并将
aio_offset 设置成 0(该文件中的第一个偏移量)。我们将aio_fildes 设置为从中读取数据的文件描述符。在设置这些域之后,就调用aio_read 请求进行读操作。我们然后可以调用aio_error 来确定aio_read 的状态。只要状态是EINPROGRESS,就一直忙碌等待,直到状态发生变化为止。现在,请求可能成功,也可能失败。



使用 AIO 接口来编译程序



我们可以在 aio.h 头文件中找到函数原型和其他需要的符号。在编译使用这种接口的程序时,我们必须使用 POSIX 实时扩展库(librt)。







注意使用这个 API 与标准的库函数从文件中读取内容是非常相似的。除了 aio_read 的一些异步特性之外,另外一个区别是读操作偏移量的设置。在传统的read 调用中,偏移量是在文件描述符上下文中进行维护的。对于每个读操作来说,偏移量都需要进行更新,这样后续的读操作才能对下一块数据进行寻址。对于异步 I/O 操作来说这是不可能的,因为我们可以同时执行很多读请求,因此必须为每个特定的读请求都指定偏移量。


aio_error


aio_error 函数被用来确定请求的状态。其原型如下:



int aio_error( struct aiocb *aiocbp );





这个函数可以返回以下内容:




  • EINPROGRESS,说明请求尚未完成

  • ECANCELLED,说明请求被应用程序取消了

  • -1,说明发生了错误,具体错误原因可以查阅 errno


aio_return


异步 I/O 和标准块 I/O 之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在read 调用上。在标准的read 调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中,我们要使用aio_return 函数。这个函数的原型如下:



ssize_t aio_return( struct aiocb *aiocbp );





只有在 aio_error 调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。aio_return 的返回值就等价于同步情况中read 或write 系统调用的返回值(所传输的字节数,如果发生错误,返回值就为
-1)。


aio_write


aio_write 函数用来请求一个异步写操作。其函数原型如下:



int aio_write( struct aiocb *aiocbp );





aio_write 函数会立即返回,说明请求已经进行排队(成功时返回值为
0,失败时返回值为 -1,并相应地设置 errno)。


这与 read 系统调用类似,但是有一点不一样的行为需要注意。回想一下对于
read 调用来说,要使用的偏移量是非常重要的。然而,对于 write 来说,这个偏移量只有在没有设置 O_APPEND 选项的文件上下文中才会非常重要。如果设置了O_APPEND,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset 域就确定了数据在要写入的文件中的偏移量。


aio_suspend


我们可以使用 aio_suspend 函数来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个aiocb 引用列表,其中任何一个完成都会导致aio_suspend 返回。
aio_suspend 的函数原型如下:



int aio_suspend( const struct aiocb *const cblist[],
int n, const struct timespec *timeout );





aio_suspend 的使用非常简单。我们要提供一个 aiocb 引用列表。如果任何一个完成了,这个调用就会返回0。否则就会返回-1,说明发生了错误。请参看清单 3。



清单 3. 使用 aio_suspend 函数阻塞异步 I/O


  
struct aioct *cblist[MAX_LIST]
/* Clear the list. */
bzero( (char *)cblist, sizeof(cblist) );
/* Load one or more references into the list */
cblist[0] = &my_aiocb;
ret = aio_read( &my_aiocb );
ret = aio_suspend( cblist, MAX_LIST, NULL );





注意,aio_suspend 的第二个参数是 cblist 中元素的个数,而不是aiocb 引用的个数。cblist 中任何NULL 元素都会被
aio_suspend忽略。


如果为 aio_suspend 提供了超时,而超时情况的确发生了,那么它就会返回-1,errno 中会包含EAGAIN。


aio_cancel


aio_cancel 函数允许我们取消对某个文件描述符执行的一个或所有 I/O 请求。其原型如下:



int aio_cancel( int fd, struct aiocb *aiocbp );





要取消一个请求,我们需要提供文件描述符和 aiocb 引用。如果这个请求被成功取消了,那么这个函数就会返回AIO_CANCELED。如果请求完成了,这个函数就会返回AIO_NOTCANCELED。


要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对 aiocbp 的NULL 引用。如果所有的请求都取消了,这个函数就会返回AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返回AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回AIO_ALLDONE。我们然后可以使用aio_error
来验证每个 AIO 请求。如果这个请求已经被取消了,那么aio_error 就会返回 -1,并且
errno 会被设置为 ECANCELED。


lio_listio


最后,AIO 提供了一种方法使用 lio_listio API 函数同时发起多个传输。这个函数非常重要,因为这意味着我们可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作。从性能的角度来看,这非常重要,因此值得我们花点时间探索一下。lio_listioAPI 函数的原型如下:



int lio_listio( int mode, struct aiocb *list[], int nent,
struct sigevent *sig );





mode 参数可以是 LIO_WAIT 或
LIO_NOWAIT。LIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止。在操作进行排队之后,LIO_NOWAIT 就会返回。list 是一个aiocb 引用的列表,最大元素的个数是由nent 定义的。注意
list 的元素可以为NULL,lio_listio 会将其忽略。sigevent 引用定义了在所有 I/O 操作都完成时产生信号的方法。


对于 lio_listio 的请求与传统的 read 或write 请求在必须指定的操作方面稍有不同,如清单 4 所示。



清单 4. 使用 lio_listio 函数发起一系列请求


  
struct aiocb aiocb1, aiocb2;
struct aiocb *list[MAX_LIST];
...
/* Prepare the first aiocb */
aiocb1.aio_fildes = fd;
aiocb1.aio_buf = malloc( BUFSIZE+1 );
aiocb1.aio_nbytes = BUFSIZE;
aiocb1.aio_offset = next_offset;
aiocb1.aio_lio_opcode = LIO_READ;
...
bzero( (char *)list, sizeof(list) );
list[0] = &aiocb1;
list[1] = &aiocb2;
ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );





对于读操作来说,aio_lio_opcode 域的值为 LIO_READ。对于写操作来说,我们要使用LIO_WRITE,不过LIO_NOP 对于不执行操作来说也是有效的。





回页首


AIO 通知


现在我们已经看过了可用的 AIO 函数,本节将深入介绍对异步通知可以使用的方法。我们将通过信号和函数回调来探索异步函数的通知机制。


使用信号进行异步通知


使用信号进行进程间通信(IPC)是 UNIX 中的一种传统机制,AIO 也可以支持这种机制。在这种范例中,应用程序需要定义信号处理程序,在产生指定的信号时就会调用这个处理程序。应用程序然后配置一个异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,特定的aiocb 请求被提供用来记录多个可能会出现的请求。清单 5 展示了这种通知方法。



清单 5. 使用信号作为 AIO 请求的通知


  
void setup_io( ... )
{
int fd;
struct sigaction sig_act;
struct aiocb my_aiocb;
...
/* Set up the signal handler */
sigemptyset(&sig_act.sa_mask);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = aio_completion_handler;

/* Set up the AIO request */
bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
my_aiocb.aio_fildes = fd;
my_aiocb.aio_buf = malloc(BUF_SIZE+1);
my_aiocb.aio_nbytes = BUF_SIZE;
my_aiocb.aio_offset = next_offset;
/* Link the AIO request with the Signal Handler */
my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
my_aiocb.aio_sigevent.sigev_signo = SIGIO;
my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
/* Map the Signal to the Signal Handler */
ret = sigaction( SIGIO, &sig_act, NULL );
...
ret = aio_read( &my_aiocb );
}

void aio_completion_handler( int signo, siginfo_t *info, void *context )
{
struct aiocb *req;

/* Ensure it's our signal */
if (info->si_signo == SIGIO) {
req = (struct aiocb *)info->si_value.sival_ptr;
/* Did the request complete? */
if (aio_error( req ) == 0) {
/* Request completed successfully, get the return status */
ret = aio_return( req );
}
}
return;
}





在清单 5 中,我们在 aio_completion_handler 函数中设置信号处理程序来捕获SIGIO 信号。然后初始化aio_sigevent 结构产生SIGIO 信号来进行通知(这是通过sigev_notify 中的SIGEV_SIGNAL 定义来指定的)。当读操作完成时,信号处理程序就从该信号的si_value
结构中提取出aiocb,并检查错误状态和返回状态来确定 I/O 操作是否完成。


对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行 I/O 操作的理想地方。采用这种方式,在一次数据传输完成时,我们就可以立即开始下一次数据传输操作。


使用回调函数进行异步通知


另外一种通知方式是系统回调函数。这种机制不会为通知而产生一个信号,而是会调用用户空间的一个函数来实现通知功能。我们在sigevent 结构中设置了对aiocb 的引用,从而可以惟一标识正在完成的特定请求。请参看清单 6。



清单 6. 对 AIO 请求使用线程回调通知


  
void setup_io( ... )
{
int fd;
struct aiocb my_aiocb;
...
/* Set up the AIO request */
bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
my_aiocb.aio_fildes = fd;
my_aiocb.aio_buf = malloc(BUF_SIZE+1);
my_aiocb.aio_nbytes = BUF_SIZE;
my_aiocb.aio_offset = next_offset;
/* Link the AIO request with a thread callback */
my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
my_aiocb.aio_sigevent.notify_function = aio_completion_handler;
my_aiocb.aio_sigevent.notify_attributes = NULL;
my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
...
ret = aio_read( &my_aiocb );
}

void aio_completion_handler( sigval_t sigval )
{
struct aiocb *req;
req = (struct aiocb *)sigval.sival_ptr;
/* Did the request complete? */
if (aio_error( req ) == 0) {
/* Request completed successfully, get the return status */
ret = aio_return( req );
}
return;
}





在清单 6 中,在创建自己的 aiocb 请求之后,我们使用 SIGEV_THREAD 请求了一个线程回调函数来作为通知方法。然后我们将指定特定的通知处理程序,并将要传输的上下文加载到处理程序中(在这种情况中,是个对aiocb 请求自己的引用)。在这个处理程序中,我们简单地引用到达的sigval 指针并使用 AIO 函数来验证请求已经完成。





回页首


对 AIO 进行系统优化


proc 文件系统包含了两个虚拟文件,它们可以用来对异步 I/O 的性能进行优化:




  • /proc/sys/fs/aio-nr 文件提供了系统范围异步 I/O 请求现在的数目。

  • /proc/sys/fs/aio-max-nr 文件是所允许的并发请求的最大个数。最大个数通常是 64KB,这对于大部分应用程序来说都已经足够了。




回页首


结束语


使用异步 I/O 可以帮助我们构建 I/O 速度更快、效率更高的应用程序。如果我们的应用程序可以对处理和 I/O 操作重叠进行,那么 AIO 就可以帮助我们构建可以更高效地使用可用 CPU 资源的应用程序。尽管这种 I/O 模型与在大部分 Linux 应用程序中使用的传统阻塞模式都不同,但是异步通知模型在概念上来说却非常简单,可以简化我们的设计。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-381677-1-1.html 上篇帖子: 瞎想:IBM的cognos应该收购FusionCharts产品,加强图形效果展示 下篇帖子: com.ibm.ejs.cm.portability.TableDoesNotExistException: ORA-00942: table or view
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表