sendfile:Linux中的"零拷贝"

使用Flask的send_file方法发送文件 Flask是一个流行的Python Web框架,它提供了许多有用的功能来构建Web应用程序。send_file方法可以将文件作为响应发送给客户端,使用户能够下载或查看文件。这是一个简单的示例,演示了如何使用Flask的send_file方法发送文件。运行上述代码后,Flask应用程序将在本地主机上的默认端口(通常是5000)上运行。要使用send_file方法,首先需要安装Flask。在函数中,我们指定了要发送的文件的路径,可以根据实际情况修改。安装完成后,可以开始编写Flask应用程序。 阅读详情

如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中,将从用户空间应用程序的角度来阐述这个问题,因此有意忽略了复杂的内核实现。

什么是”零拷贝”

为了更好的理解问题的解决法,我们首先需要理解问题本身。首先我们以一个网络服务守护进程为例,考虑它在将存储在文件中的信息通过网络传送给客户这样的简单过程中,所涉及的操作。下面是其中的部分简单代阿:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

看起来不能更简单了。你也许认为执行这两个系统调用并未产生多少开销。实际上,这简直错的一塌糊涂。在执行这两个系统调用的过程中,目标数据至少被复制了4次,同时发生了同样多次数的用户/内核空间的切换(实际上该过程远比此处描述的要复杂,但是我希望以简单的方式描述之,以更好的理解本文的主题)。


为了更好的理解这两句代码所涉及的操作,请看图1。图的上半部展示了上下文切换,而下半部展示了复制操作。

图1. Copying in Two Sample System Calls

步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。

步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。

步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。

步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生,这与我们的代码的执行是独立且异步发生的。你可能会疑惑:“为何要说是独立、异步?难道不是在write系统调用返回前数据已经被传送了?write系统调用的返回,并不意味着传输成功——它甚至无法保证传输的开始。调用的返回,只是表明以太网驱动程序在其传输队列中有空位,并已经接受我们的数据用于传输。可能有众多的数据排在我们的数据之前。除非驱动程序或硬件采用优先级队列的方法,各组数据是依照FIFO的次序被传输的(图1中叉状的DMA copy表明这最后一次复制可以被延后)。

正如你所看到的,上面的过程中存在很多的数据冗余。某些冗余可以被消除,以减少开销、提高性能。作为一名驱动程序开发人员,我的工作围绕着拥有先进特性的硬件展开。某些硬件支持完全绕开内存,将数据直接传送给其他设备的特性。这一特性消除了系统内存中的数据副本,因此是一种很好的选择,但并不是所有的硬件都支持。此外,来自于硬盘的数据必须重新打包(地址连续)才能用于网络传输,这也引入了某些复杂性。为了减少开销,我们可以从消除内核缓冲区与用户缓冲区之间的复制入手。

消除复制的一种方法是将read系统调用,改为mmap系统调用,例如:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

为了更好的理解这其中设计的操作,请看图2。上下文切换部分与图1保持一致。

图2. Calling mmap

步骤一:mmap系统调用导致文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。

步骤二:write系统调用导致内核将数据从内核缓冲区复制到与socket相关联的内核缓冲区中。

步骤三:DMA模块将数据由socket的缓冲区传递给协议引擎时,第3次复制发生。

通过调用mmap而不是read,我们已经将内核需要执行的复制操作减半。当有大量数据要进行传输是,这将有相当良好的效果。然而,性能的改进需要付出代价的;是用mmap与write这种组合方法,存在着一些隐藏的陷阱。例如,考虑一下在内存中对文件进行映射后调用write,与此同时另外一个进程将同一文件截断的情形。此时write系统调用会被进程接收到的SIGBUS信号中断,因为当前进程访问了非法内存地址。对SIGBUS信号的默认处理是杀死当前进程并生成dump core文件——而这对于网络服务器程序而言不是最期望的操作。

有两种方式可用于解决该问题:

第一种方式是为SIGBUS信号设置信号处理程序,并在处理程序中简单的执行return语句。在这样处理方式下,write系统调用返回被信号中断前已写的字节数,并将errno全局变量设置为成功。必须指出,这并不是个好的解决方式——治标不治本。由于收到SIGBUS信号意味着进程发生了严重错误,我不鼓励采取这种解决方式。

第二种方式应用了文件租借(在Microsoft Windows系统中被称为“机会锁”)。这才是解劝前面问题的正确方式。通过对文件描述符执行租借,可以同内核就某个特定文件达成租约。从内核可以获得读/写租约。当另外一个进程试图将你正在传输的文件截断时,内核会向你的进程发送实时信号——RT_SIGNAL_LEASE。该信号通知你的进程,内核即将终止在该文件上你曾获得的租约。这样,在write调用访问非法内存地址、并被随后接收到的SIGBUS信号杀死之前,write系统调用就被RT_SIGNAL_LEASE信号中断了。write的返回值是在被中断前已写的字节数,全局变量errno设置为成功。下面是一段展示如何从内核获得租约的示例代码。

if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
}

/* l_type can be F_RDLCK F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}

Sendfile

sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个本地文件之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数。使用方法如下:

sendfile(socket, file, len);

为了更好的理解所涉及的操作,请看图3

图3. Replacing Read and Write with Sendfile

步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到某个内核缓冲区,之后再被复制到与socket相关联的缓冲区内。

步骤二:当DMA模块将位于socket相关联缓冲区中的数据传递给协议引擎时,执行第3次复制。

你可能会在想,我们在调用sendfile发送数据的期间,如果另外一个进程将文件截断的话,会发生什么事情?如果进程没有为SIGBUS注册任何信号处理函数的话,sendfile系统调用返回被信号中断前已发送的字节数,并将全局变量errno置为成功。

然而,如果在调用sendfile之前,从内核获得了文件租约,那么类似的,在sendfile调用返回前会收到RT_SIGNAL_LEASE。

到此为止,我们已经能够避免内核进行多次复制,然而我们还存在一分多余的副本。这份副本也可以消除吗?当然,在硬件提供的一些帮助下是可以的。为了消除内核产生的素有数据冗余,需要网络适配器支持聚合操作特性。该特性意味着待发送的数据不要求存放在地址连续的内存空间中;相反,可以是分散在各个内存位置。在内核版本2.4中,socket缓冲区描述符结构发生了改动,以适应聚合操作的要求——这就是Linux中所谓的"零拷贝“。这种方式不仅减少了多个上下文切换,而且消除了数据冗余。从用户层应用程序的角度来开,没有发生任何改动,所有代码仍然是类似下面的形式:

sendfile(socket, file, len);

为了更好的理解所涉及的操作,请看图4

Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.

步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中。

步骤二:数据并未被复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。

由于数据实际上仍然由磁盘复制到内存,再由内存复制到发送设备,有人可能会声称这并不是真正的"零拷贝"。然而,从操作系统的角度来看,这就是"零拷贝",因为内核空间内不存在冗余数据。应用"零拷贝"特性,出了避免复制之外,还能获得其他性能优势,例如更少的上下文切换,更少的CPU cache污染以及没有CPU必要计算校验和。

现在我们明白了什么是"零拷贝",让我们将理论付诸实践,编写一些代码。你可以从www.xalien.org/articles/source/sfl-src.tgz处下载完整的源码。执行"tar -zxvf sfl-src.tgz"将源码解压。运行make命令,编译源码,并创建随机数据文件data.bin

从头文件开始介绍代码:

/* sfl.c sendfile example program
Dragan Stancevic <
header name function / variable
-------------------------------------------------*/
#include <stdio.h> /* printf, perror */
#include <fcntl.h> /* open */
#include <unistd.h> /* close */
#include <errno.h> /* errno */
#include <string.h> /* memset */
#include <sys/socket.h> /* socket */
#include <netinet/in.h> /* sockaddr_in */
#include <sys/sendfile.h> /* sendfile */
#include <arpa/inet.h> /* inet_addr */
#define BUFF_SIZE (10*1024) /* size of the tmp buffer */

除了基本socket操作所需要的 <sys/socket.h> 和<netinet/in.h>头文件外,我们还需要包含sendfile系统调用的原型定义,这可以在<sys/sendfile.h>头文件中找到。

服务器标志:

/* are we sending or receiving */
if(argv[1][0] == 's') is_server++;
/* open descriptors */
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);

该程序既能以服务端/发送方,也能以客户端/接收方的身份运行。我们需要检查命令行参数中的一项,然后相应的设置is_server标志。程序中大开了一个地址族为PF_INET的流套接字;作为服务端运行时需要向客户发送数据,因此要打开某个数据文件。由于程序中是用sendfile系统调用来发送数据,因此不需要读取文件内容并存储在程序的缓冲区内。

接下来是服务器地址:

/* clear the memory */
memset(&sa, 0, sizeof(struct sockaddr_in));
/* initialize structure */
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);


将服务端地址结构清零后设置协议族、端口和IP地址。服务端的IP地址作为命令行参数传递给程序。端口号硬编码为1033,选择该端口是因为它在要求root权限的端口范围之上。

下面是服务端的分支代码:

if(is_server){
int client; /* new client socket */
printf("Server binding to [%s]\n", argv[2]);
if(bind(sd, (struct sockaddr *)&sa,sizeof(sa)) < 0){
perror("bind");
exit(errno);
}

作为服务端,需要为socket描述符分配一个地址,这是通过系统调用bind完成的,它将服务器地址(sa)分配给socket描述符(sd).

if(listen(sd,1) < 0){
perror("listen");
exit(errno);
}

由于使用流套接字,必须对内核声明接受外来连接请求的意愿,并设置连接队列的尺寸。此处将队列长度设为1,但是通常会将该值设的高一些,用于接受已建立的连接。在老版本的内核中,该队列被用于阻止SYN flood攻击。由于listen系统调用之改为设定已建立连接的数量,该特性已被listen调用遗弃。内核参数tcp_max_syn_backlog承担了保护系统不受SYN flood攻击的功能。

if((client = accept(sd, NULL, NULL)) < 0){
perror("accept");
exit(errno);
}

accept系统调用从待处理的已连接队列中选取第一个连接请求,为之建立一个新的socket。accept调用的返回值是新建立连接的描述符;新的socket可以用于read、write和poll/select系统调用。

if((cnt = sendfile(client,fd,&off, BUFF_SIZE)) < 0){
perror("sendfile");
exit(errno);
}

printf("Server sent %d bytes.\n", cnt);
close(client);

在客户socket描述符上已经建立好连接,因此可以开始将数据传输至远端系统——这时通过调用sendfile系统调用来完成。该调用在Linux中的原型为如下形式:

extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset, size_t __count) __THROW;

前两个参数为文件描述符,第三个参数表示sendfile开始传输数据的偏移量。第四个参数是打算传输的字节数。为了sendfile可以使用"零拷贝“特性,网卡需要支持聚合操作,此外还应具备校验和计算能力。如果你的NIC不具备这些特性,仍可以是用sendfile来发送数据,区别是内核在传输前会将所有缓冲区的内容合并。

移植性问题

sendfile系统调用的问题之一,总体上来看,是缺少标准化的实现,这与open系统调用类些。sendfile在Linux、Solaris或HP-UX中的实现有很大的不同。这给希望在网络传输代码中利用"零拷贝"的开发者带来了问题。

这些实现差异中的一点在于Linux提供的sendfile,是定义为用于两个文件描述符之间和文件到socket之间的传输接口。另一方面,HP-UX和Solaris中,sendfile只能用于文件到socket的传输。

第二点差异,是Linux没有实现向量化传输。Solaris和HP-UX 中的sendfile系统调用包含额外的参数,用于消除为待传输数据添加头部的开销。

展望

Linux中“零拷贝”的实现还远未结束,并很可能在不久的未来发生变化。更多的功能将会被添加,例如,现在的sendfile不支持向量化传输,而诸如Samba和Apache这样的服务器不得不是用TCP_COKR标志来执行多个sendfile调用。该标志告知系统还有数据要在下一个sendfile调用中到达。TCP_CORK和TCP_NODELAY不兼容,后者在我们希望为数据添加头部时使用。这也正是一个完美的例子,用于说明支持向量化的sendfile将在那些情况下,消除目前实现所强制产生的多个sendfile调用和延迟。

当前sendfile一个相当令人不愉快的限制是它无法用户传输大于2GB的文件。如此尺寸大小的文件,在今天并非十分罕见,不得不复制数据是十分令人失望的。由于这种情况下sendfile和mmap都是不可用的,在未来内核版本中提供sendfile64,将会提供很大的帮助。

结论

尽管有一些缺点,"零拷贝"sendfile是一个很有用的特性。我希望读者认为本文提供了足够的信息以开始在程序中使用sendfile。如果你对这个主题有更深层次的兴趣,敬请期待我的第二篇文章——"Zero Copy II: Kernel Perspective",在其中将更深一步的讲述"零拷贝"的内核内部实现。


http://t.matong.me/2011/03/29/zero-copy-linux.html

Python实现文件传输功能——sendfile 在网络应用中,文件传输功能是非常常用的,那么在Python中如何实现这一功能呢?首先需要创建一个发送端,用于将要传输的文件发送出去。在发送端中,需要指定文件的路径和ip地址、端口号。接着需要创建接收端,用于接收发送端发送的文件。同样,在接收端中,需要指定ip地址和端口号。通过以上步骤,我们就实现了Python中文件传输功能的基本实现。host = ‘127.0.0.1’ # 定义主机ip地址。host = ‘127.0.0.1’ # 定义主机ip地址。port = 12345 # 定义端口号。 阅读详情

相关推荐

Flask send_file使用详解

通过使用Flask的send_file函数,您可以方便地在Web应用程序中向客户端发送文件。在Flask框架中,send_file函数是一个非常有用的方法,它可以用于在Web应用程序中向客户端发送文件。在Flask应用程序中,我们需要定义路由和相应的处理函数。在这个例子中,我们将创建一个路由,当用户访问该路由时,将发送一个文件给客户端。最后,我们需要运行Flask应用程序,以便能够在Web浏览器中访问它。在这个例子中,我们将发送一个PDF文件,并设置。首先,我们需要导入Flask和send_file函数。

HkPostscript的博客 1061

flask 返回json_轻松掌握文件下载,详解flask中的send_file

在flask中的文件下载使用后端实现中,我们经常会让用户访问一个URL时下载一个文件. 在flask中直接使用send_file即可.最简单的send_file如下我们看下效果可以看到,直接send_file时flask直接返回了文件内容,效果和我们自己的用open+read读出内容并返回一样,浏览器没有识别成一个文件下载.要浏览器识别为文件下载的话,需要加上as_attachment=True这...

weixin_39907157的博客 7573

【AI赋能】Sora + CapCut 打造爆款短视频:从脚本到发布的自动化工作流

本文详细介绍了如何利用Sora、Grok和CapCut等AI工具构建自动化短视频生产工作流。通过结构化Prompt生成专业脚本,借助Sora将文字转化为高质量视频素材,并利用CapCut进行高效剪辑与合成,实现从创意到发布的十倍效率提升,助力个人及团队打造爆款内容。

g8f9d0s1a2的博客 493

零拷贝——sendfile

一、DMA io读写有两种方式: 中断 DMA 用户进程发起数据读取请求 系统调度为该进程分配cpu cpu向io控制器(ide,scsi)发送io请求 用户进程等待io完成,让出cpu 系统调度cpu执行其他任务 数据写入至io控制器的缓冲寄存器 缓冲寄存器满了向cpu发出中断信号 cpu读取数据至内存 通过中断,cpu需要拷贝数据。 2、DMA 用户进程发起...

赶路人儿 4289

vue项目基于WebRTC实现一对一音视频通话

vue项目基于WebRTC实现一对一音视频通话。完整代码gitee地址: https://gitee.com/wade-nian/wdn-webrtc.git。

qq_920189602的博客 6646

从零到一:Bioconductor新手入门与R包安装实战

本文详细介绍了Bioconductor在生物信息学中的重要性及其R包安装实战指南。从环境准备到BiocManager的使用,再到常见问题排查,帮助新手快速掌握这一强大工具。特别推荐org.Hs.eg.db等核心工具包,助力基因注释和功能分析,提升研究效率。

weixin_29061041的博客 418

sendfile: Linux中的"零拷贝"

译文:http://blog.youkuaiyun.com/caianye/article/details/7576198 原文:http://www.linuxjournal.com/article/6345 如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中

Sunshine的专栏 2658

VSCode搭配Keil开发STM32:从环境配置到代码跳转全流程(避坑指南)

本文详细介绍了如何使用VSCode搭配Keil进行STM32开发,从环境配置到代码跳转的全流程指南。通过安装必备插件和正确配置项目文件,开发者可以在VSCode中享受现代编辑器的便捷功能,同时保留Keil的编译调试能力,提升嵌入式开发效率。

weixin_28832121的博客 253

sendfile:Linux中的“零拷贝“

如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中,将从用户空间应用程序的角度来阐述这个问题,因此有意忽略了复杂的内核实现。 什么是”零拷贝” 为了更好的理解问题的解决法,我们首先需要理解问题本身。首先我们以一个网络服务守护进程为例,考虑它在将存储在文件中的信息通过网络传送给客户这样的简单过程中,所涉及的操作。下面是其中的部分简单代阿: read(file, tmp_bu

行者的博客 300

【专栏导引贴】100+智能优化算法:从背景原理到代码实战(持续更新)

本专栏系统整理100+种现代优化算法,涵盖粒子群优化(PSO)、遗传算法(GA)、鲸鱼优化(WOA)等经典方法,以及白鲸优化(BWO)、非洲秃鹫优化(AVOA)等新兴算法。每篇详解包含生物启发原理、数学模型、代码实现及工程应用,采用统一编程风格,提供可直接运行的实例代码。专栏持续更新,旨在为研究者提供系统化的智能优化算法参考资料,实现从理论到实践的完整知识链。目前已收录23种算法,包括蚁群系统(ACS)、蝙蝠算法(BA)、天牛须搜索(BAS)等自然启发式优化方法。

qq_42212808的博客 3642

qcc304x笔记之手机端显示电量(九)

默认的sdk有些安卓手机不支持电量显示,所以需要加电量显示的补丁 流程分析 1. 首先新建一个task,并将task注册给电池模块 appBatteryRegister(batteryRegistrationForm) 实现handler处理函数,当battery模块电池电量更新后,会将电池百分比值发送到本模块,然后我们就可以在handler中将实时电量同步到手机端了。 hfpAtCmdInit atCmdTask.task.handler = appAtCmdHandleM...

在云巅的博客 2199

Linux "零拷贝" sendfile函数

Sendfile函数说明 #include ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); sendfile()是作用于数据拷贝在两个文件描述符之间的操作函数.这个拷贝操作是内核中操作的,所以称为"零拷贝".sendfile函数比起read和write函数高效得多,因为read和write是要把

chunlovenan的专栏 5249

【WRFDA教程第四期】雷达数据同化(Radar Data Assimilation)

雷达数据同化(Radar Data Assimilation)

WW、forever的博客 1758

linux独有的sendfile系统调用--“零拷贝,高效”

参考:http://blog.youkuaiyun.com/caianye/article/details/7576198 如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中,将从用户空间应用程序的角度来阐述这个问题,因此有意忽略了复杂的内核实现。 ...

azi8860的博客 261

通用抓取算法AnyGrasp(Graspnet)——本地部署并测试自定义输入数据

背景:GraspNet在数据采集方面,首先将相机固定到机械臂末端,对同一个场景拍摄256个不同的角度,同时场景中的每个物体都会提前采集好3D模型,之后将其投射到仿真环境中,在仿真环境中,对抓取位姿进行打标签,如此完成数据的采集。在运行官方的测试示例时,使用的是路径下的输入数据,可以分析下。color.png(输入):rgb图像(输出):输出的生成抓取位姿的图像depth.png(输入):和当前rgb图像对应的深度图像(保存格式是单通道uint16)meta.mat。

boss-dog 1324

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Linux "零拷贝" sendfile函数中文说明及实际操作分析

Sendfile函数说明 #include ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); sendfile()是作用于数据拷贝在两个文件描述符之间的操作函数.这个拷贝操作是内核中操作的,所以称为"零拷贝".sendfile函数比起read和write函数高效得多,因为read和write是要

amao992的专栏 1023

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

上一篇: DMA技术
下一篇: Linux 中的零拷贝技术,第 1 部分
xiaofei0859
博客等级 码龄18年 454粉丝 55原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值