关于select、poll和epoll

1.什么是IO多路复用?

​ 在过去,服务器在处理各个链接IO的时候,为了使recv()在阻塞等待客户端数据的时候,不影响其它的链接IO,往往采用的是一链接一线程。这样我们每个连接,都在一个独立的线程中处理。这样虽然做的了不阻塞,但面对如今的百万并发的情况。一个连接一个线程就不适合了,因为线程的创建会销毁计算机的资源。

​ 那我们能不能在一个线程之中,能不能完成不阻塞不影响其它链接的操作呢?答案是可以的,这就是今天要说的IO多路复用。

​ IO多路复用是一种同步IO模型,允许单个进程/线程同时处理多个IO请求。其核心思想在于,通过监视多个文件描述符(如套接字)的状态,当某个文件描述符就绪(即可读或可写)时,能够通知应用程序进行相应的读写操作。这种机制显著提高了系统的并发性和响应能力,减少了系统资源的浪费。

​ 常见的IO多路复用技术包括select、poll和epoll等。这些技术的主要区别在于底层实现和性能。例如,select使用数组来存储文件描述符,存在最大连接数的限制;而poll使用链表,无最大连接数的限制,因此在处理大量文件描述符时可能更有效率。

2.select

2.1 select介绍和使用

首先我们来看一下第一种,"select"下面是select的函数原型。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要监控的最大文件描述符加1。
  • readfds:指向fd_set结构的指针,这个集合中包含要监控的可读类型的文件描述符。
  • writefds:指向fd_set结构的指针,这个集合中包含要监控的可写类型的文件描述符。
  • exceptfds:指向fd_set结构的指针,这个集合中包含要监控是否有异常条件出现的文件描述符。
  • timeout:select函数的超时时间。这个参数可以是NULL(表示无限等待),也可以是一个具体的时间值。如果是非零值,则select会在指定的时间内阻塞,等待文件描述符就绪;如果超时时间到达,即使文件描述符没有就绪,select也会返回。

在此之外,还有几个操作宏

 此外还几个接口:
void FD_CLR(int fd, fd_set *set);      
int  FD_ISSET(int fd, fd_set *set);   
void FD_SET(int fd, fd_set *set);    
void FD_ZERO(fd_set *set);          

  1. FD_ZERO(fd_set *set):这个宏用于清除一个文件描述符集合,将set指向的fd_set结构中的所有位设置为0。
  2. FD_SET(int fd, fd_set *set):这个宏用于将一个文件描述符添加到文件描述符集合中。它将set指向的fd_set结构中对应于文件描述符fd的位设置为1。
  3. FD_CLR(int fd, fd_set *set):这个宏用于从文件描述符集合中删除一个文件描述符。它将set指向的fd_set结构中对应于文件描述符fd的位设置为0。
  4. FD_ISSET(int fd, fd_set *set):这个宏用于检查一个文件描述符是否存在于文件描述符集合中。如果set指向的fd_set结构中对应于文件描述符fd的位为1,则返回非零值(真);否则返回0(假)。

对于他们都会用到一个结构体“fd_set”,我们来看一下这个结构体的原型。

typedef struct {  
    unsigned long fds_bits[FD_SETSIZE/(8*sizeof(unsigned long))];  
} fd_set;

fd_set结构体对于文件描述符的表示是用位图来记录的,关于位图的概念如下。

位图(Bitmap)是一种数据结构,主要用于高效地管理和操作大量的、固定大小的项,通常用于表示哪些项是设置的(或激活的),哪些项是未设置的(或未激活的)。在位图中,每一位(bit)通常代表一个项的状态,其中“1”可能表示该项是激活的,而“0”表示未激活。

具体来说,位图使用连续的二进制位来表示数据。由于每一位只有两种状态(0或1),位图非常节省空间。例如,如果要表示一个包含1000个项的集合,每个项只有两种状态(开/关),那么使用位图只需要大约125字节(1000位 / 8位/字节)的内存空间。如果使用其他数据结构(如数组或链表),则可能需要更多的空间。

位图的主要优点包括:

  1. 空间效率:如上所述,位图使用极少的空间来表示大量的项。
  2. 快速访问:由于位图是连续的,因此可以通过简单的位运算来快速检查和修改项的状态。
  3. 固定大小:位图的大小是固定的,因此很容易进行内存管理。

位图在操作系统、数据库系统、网络编程和许多其他领域都有广泛的应用。例如,在操作系统中,位图经常用于管理内存页、文件描述符或其他系统资源;在数据库系统中,位图索引用于加速某些类型的查询;在网络编程中,位图可用于跟踪哪些套接字是活动的或需要处理。

也就是说,如果一个文件描述符fd的值是5,如果把他添加到fd_set这个结构体的集合中,那么他对应的表示状态就是第5位的0和1.也就是他只能表示两个状态。再此,我们就显现出了select的一个弊端。那就是,他的集合对文件描述符的数量和大小都是有限制的,取决于FD_SETSIZE这个值。不同的平台,FD_SETSIZE的值是不一样的,在编写快平台程序的时候,这一点我们要注意.下面我们用一端完整的代码,来掩饰一些select的建立流程。我们用注释对代码进行讲解。

2.2 实现一个select的使用

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>

int main()
{

//###############################建立一个监听本地端口用的文件描述符#######################################
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
	servaddr.sin_port = htons(2000); // 0-1023, 

	if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
		printf("bind failed: %s\n", strerror(errno));
	}

	listen(sockfd, 10);
	printf("listen finshed: %d\n", sockfd); // 3 

	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);
//****************************************************************************************************

//############################################实现select###############################################
    
    fd_set rfds,rset;/*这里我们创建了两个集合。rfds集合用来保存我们要监测的文件描述符的集合,rset用来记录在rfds这个集合中
                      哪些文件描述符可读了*/
    
	FD_ZERO(&rfds);//清空rfds这个集合,用来初始化
	FD_SET(sockfd, &rfds);//将监听用的文件描述符sockfd添加进这个集合中。

	int maxfd = sockfd;//设置当前文件描述符为集合中最大描述符,因为当前只有一个文件描述符,所以他一定是最大的。

	while (1) {
		rset=rfds;//每次都要重新赋值一遍,因为rset经过select其值会被改变
		int nready = select(maxfd+1, &rset, NULL, NULL, NULL);/*调用select,再此我们设置null,此时如果rset集合之中
                                                              */如果有可读就会立刻返回
		
		
		if (FD_ISSET(sockfd, &rset)) { // accept
            //如果是有新的链接建立了,那我们就把他存入到带监测集合rfds中
			int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept finshed: %d\n", clientfd);

			FD_SET(clientfd, &rfds); 
			
			if (clientfd > maxfd) maxfd = clientfd;
		}

		// recv
		int i = 0;
		//因为sockfd是最先建立的,所以他一定是集合中最小的文件描述符。
		for (i = sockfd+1; i <= maxfd;i ++) { 

			if (FD_ISSET(i, &rset)) {
				char buffer[1024] = {0};
				
				int count = recv(i, buffer, 1024, 0);//读取其内容
				if (count == 0) { // disconnect
					printf("client disconnect: %d\n", i);
					close(i);
					FD_CLR(i, &rfds);//如果是0说明客户端断开了连接,那我们就把他从待检测集合中删除,
					
					continue;
				}

				printf("RECV: %s\n", buffer);

				count = send(i, buffer, count, 0);
				printf("SEND: %d\n", count);

			}

		}
		
	}
//**********************************************************************************************************

}

上面是一个简单的select的实际用法,再次我们补充一个知识点,就是在linux系统中,创建的文件描述符是当前可用的文件描述符中最小的,所以我们也是利用了这一点确定了sockfd这个文件描述符是最小的,因为他是最先建立的。只有当我们调用close()函数的时候,系统才会回收这个文件描述符,要注意的是,回收并不是立刻触发的。也就是说,当我们close了3这个文件描述符的时候,我们再次建立一个新的文件描述符的时候不一定就是3.因为系统可能还没有把这个3回收。文件描述符的回收机制,一般是等待60S左右,这个时间是可以更改的,具体如何更改我们就不展开说了。

2.3 select的优缺点以及使用场景。

优点

  1. 跨平台性select 在大多数 Unix-like 系统上都是可用的,因此使用 select 编写的代码具有较好的跨平台性。
  2. 简单性select 的 API 相对简单,易于理解和使用。它只需要一个 fd_set 结构体来表示文件描述符的集合,以及几个简单的宏来操作这个集合。
  3. 适用性:对于中小规模的并发连接,select 通常能够胜任。在许多常见的网络编程场景中,select 能够提供足够的性能和灵活性。

缺点

  1. 性能问题:当监视的文件描述符数量非常大时,select 的性能会显著下降。这是因为 select 使用轮询的方式来检查文件描述符的状态,当文件描述符数量增多时,轮询的开销会变得很大。
  2. 文件描述符限制select 有一个固定的文件描述符上限,通常是 FD_SETSIZE。这个限制可能不适用于需要处理大量并发连接的高性能服务器。
  3. 精度问题select 的超时参数是以秒和微秒为单位的,这对于需要高精度计时的应用来说可能不够精确。
  4. 信号干扰select 在等待期间可能会被信号中断,这可能会导致一些复杂性和错误处理的问题。
  5. 不可移植性:尽管 select 在许多 Unix-like 系统上可用,但在某些非 Unix 系统(如 Windows)上可能没有直接的等价物,这可能导致跨平台开发的复杂性。

适用场景

select适用于一些并发量很低的场景,而且selcet缺点很明显,他跟我们后面要说的poll的机制类似。但poll相比于select来说,他没有一个固定的文件描述上限。所以他完全可以被poll替代。也就是说,select适合于并发量很小,且该系统上没有poll的场景。

3.poll

3.1 poll的介绍

下面是poll的函数原型。

#include <poll.h>  
int poll(struct pollfd *fds, nfds_t nfds, int timeout);	

其中,fds是一个指向pollfd结构体数组的指针,每个pollfd结构体描述了一个文件描述符及其关注的事件;nfdsfds数组中的元素个数,即要监视的文件描述符的数量;timeout是等待事件发生的超时时间,单位是毫秒(ms)。

pollfd结构体定义如下:

	struct pollfd{
	int fd;          这是要监控的描述符;
	short events;    针对这个描述符要监控的事件;    常用:POLLIN-可读;  POLLINT-可写(其都是些比特位)
	short revents;   监控调用返回后这个描述符实际就绪的事件;
};

下面是events值的相关描述

  • POLLIN:普通或优先级带数据可读。

  • POLLOUT:普通或优先级带数据可写。

  • POLLPRI:高优先级带数据可读。

  • POLLERR:发生错误。

  • POLLHUP:发生挂起(hung up)。

  • POLLNVAL:无效的文件描述符。

  • revents成员是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。它将是之前events中所请求的事件和当前实际发生事件的交集。

当调用poll函数时,它会阻塞进程直到以下情况之一发生:

  • 至少有一个文件描述符就绪,并且相关的事件在revents中设置。
  • 超时时间到期(如果timeout不是-1)。
  • 调用被信号中断(此时errno被设置为EINTR)。

返回值说明:

  • 如果函数调用成功,则返回所有事件就绪的文件描述符个数。
  • 如果超时时间到期且没有任何文件描述符就绪,返回0。
  • 如果调用失败,返回-1,并设置全局变量errno以指示错误。

3.2 poll的实现

对比上述select我们发现,poll并没有对文件描述符的大小和数量的限制,这也就是他对比于select的优势所在。下面我们用代码展示一下,poll函数的适用,并且我们用注释来对其做一个解说.

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>

int main()
{
//##########################################创建监听文件描述符################################################
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
	servaddr.sin_port = htons(2000); // 0-1023, 

	if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
		printf("bind failed: %s\n", strerror(errno));
	}

	listen(sockfd, 10);
	printf("listen finshed: %d\n", sockfd); // 3 

	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);
//**********************************************************************************************************

//###############################################实现poll####################################################
struct pollfd fds[1024] = {0};//创建一个pollfd结构体的数组,每个pollfd结构体将记录一个文件描述符标志和其状态
	fds[sockfd].fd = sockfd;//我们把sockfd存入进去
	fds[sockfd].events = POLLIN;//这里是我们要关注的事件,

	int maxfd = sockfd;//设置最大文件描述的值,方便我们后续遍历数组

	while (1) {

		int nready = poll(fds, maxfd+1, -1);//调用poll

		if (fds[sockfd].revents & POLLIN) {
       //如果当前sockfd有可读事件
			int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept finshed: %d\n", clientfd);
          //我们把当前新接收的文件描述符 记录进去
			fds[clientfd].fd = clientfd;
			fds[clientfd].events = POLLIN;
			
			if (clientfd > maxfd) maxfd = clientfd;

		}
	
		int i = 0;
		for (i = sockfd+1; i <= maxfd;i ++) { // i fd

			if (fds[i].revents & POLLIN) {

				char buffer[1024] = {0};
				
				int count = recv(i, buffer, 1024, 0);
				if (count == 0) { // disconnect
					printf("client disconnect: %d\n", i);
					close(i);

					fds[i].fd = -1;
					fds[i].events = 0;
					
					continue;
				}

				printf("RECV: %s\n", buffer);

				count = send(i, buffer, count, 0);
				printf("SEND: %d\n", count);

			}
		
		}

	}

//**********************************************************************************************************


}

3.3 poll的优缺点以及适用场景

poll函数作为I/O多路复用机制的一种,与select类似,但在某些方面有所改进。以下是poll的主要优缺点:

优点:

  1. 无文件描述符数量限制:相较于select,poll不受文件描述符数量的限制。这意味着它可以处理更多的并发连接,尤其适用于需要支持大量文件描述符的场景。
  2. 输入输出参数分离:在poll中,输入和输出参数是分开的,这使得代码更加清晰,也避免了每次调用时都需要重新设置文件描述符集合的麻烦。
  3. 更好的可移植性:在某些系统中,select可能不受支持,而poll则具有更好的可移植性,可以在更多的Unix-like系统上使用。

缺点:

  1. 轮询机制效率问题:类似于select,poll使用轮询的方式来检查每个文件描述符的状态。当文件描述符的数量很大时,这种轮询机制会导致不必要的开销,特别是在只有少数文件描述符处于就绪状态的情况下。
  2. 效率随描述符数量增长而下降:随着监视的文件描述符数量的增长,poll的效率会线性下降。这是因为每次调用poll时,它都需要遍历整个文件描述符集合来查找就绪的描述符。
  3. 不支持优先级:与select一样,poll也不支持基于优先级的文件描述符处理。所有文件描述符都被平等对待,无法根据优先级进行差异化处理。

综上所述,poll在处理大量文件描述符时相较于select具有更好的可伸缩性,但由于其轮询机制,在处理大量文件描述符时效率仍然可能受到影响。因此,在选择使用poll还是其他I/O多路复用机制时,需要根据具体的应用场景和需求进行权衡。对于需要处理大量并发连接且对性能有较高要求的场景,通常推荐使用更高效的机制,如Linux中的epoll。

适用场景

poll随着并发量的增大,他的轮询遍历效率将会很低,所以他适合处理并发量小的场景下适用,作为select的替代。

4.epoll

4.1 epoll的介绍

epoll是一个非常强大的工具,在epoll出现之前,linux并没有被定义为服务器系统的一哥,直到epoll的诞生,linux成为了服务器系统的标签。

下面是epoll的相关函数

#include <sys/epoll.h>
int epoll_create(int size);

功能:创建一个epoll对象,并返回该对象的文件描述符。此文件描述符将被用于后续的epoll操作。

参数:size自Linux内核2.6.8版本起就被忽略,只要求size大于0即可。

返回值:成功时返回epoll文件描述符,失败时返回-1并设置errno

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能:对指定的epoll文件描述符执行控制操作,如添加、修改或删除一个文件描述符的监听。

参数:

  • epfd:由epoll_create返回的epoll文件描述符。
  • op:要执行的操作,可以是EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(删除)。
  • fd:要添加、修改或删除的文件描述符。
  • event:指向一个epoll_event结构体的指针,用于描述对fd感兴趣的事件。

返回值:成功时返回0,失败时返回-1并设置errno

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能:等待注册在epoll实例上的事件。如果有事件发生,或者超时,该函数将返回。

参数:

  • epfd:由epoll_create返回的epoll文件描述符。
  • events:指向一个epoll_event结构体的数组,用于存储返回的事件。
  • maxevents:告知内核这个events的大小,即这个数组一共可以保存多少epoll_event结构体。
  • timeout:超时时间(毫秒),-1表示永远等待。

返回值:成功时返回发生事件的文件描述符个数,超时返回0,失败返回-1并设置errno

struct epoll_event {  
    __uint32_t events;      /* Epoll events */  
    epoll_data_t data;      /* User data variable */  
};

其中events可以是以下的一个或多个宏的按位或:

  • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
  • EPOLLOUT:表示对应的文件描述符可以写。
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里并未使用这一特性,故不做过多解释)。
  • EPOLLERR:表示对应的文件描述符发生错误。
  • EPOLLHUP:表示对应的文件描述符被挂起。
  • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

下面是一个epoll的实现代码.

4.2 epoll的代码实现

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>

int main()
{
//##########################################创建监听文件描述符################################################
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
	servaddr.sin_port = htons(2000); // 0-1023, 

	if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
		printf("bind failed: %s\n", strerror(errno));
	}

	listen(sockfd, 10);
	printf("listen finshed: %d\n", sockfd); // 3 

	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);
//**********************************************************************************************************

//############################################实现epoll######################################################

	int epfd = epoll_create(1);//创建一个epoll的文件描述符

	struct epoll_event ev;//创建一个epoll_event事件结构体
	ev.events = EPOLLIN;//设置我们要关注可读事件
	ev.data.fd = sockfd;//设置该事件绑定的文件描述符为sockfd
	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);//我们将该事件添加到epfd中

	while (1) {

		struct epoll_event events[1024] = {0};//创建一个用来接收epoll_wait返回的事件数组
		int nready = epoll_wait(epfd, events, 1024, -1);//调用epoll

		int i = 0;
		for (i = 0;i < nready;i ++) {

			int connfd = events[i].data.fd;

			if (connfd == sockfd) {//如果该文件描述符是sockfd说明有新的连接建立
				int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
				printf("accept finshed: %d\n", clientfd);

				ev.events = EPOLLIN;
				ev.data.fd = clientfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);//我们把新的连接建立的文件描述符添加进去epoll中
				
			} else if (events[i].events & EPOLLIN) {//如果是其它的可读事件

				char buffer[1024] = {0};
				
				int count = recv(connfd, buffer, 1024, 0);
				if (count == 0) { // disconnect
					printf("client disconnect: %d\n", connfd);
					close(connfd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);//如果客户端断开连接,我们把该文件描述符从epoll取出
					
					continue;
				}

				printf("RECV: %s\n", buffer);

				count = send(connfd, buffer, count, 0);
				printf("SEND: %d\n", count);

			}

		}

	}
	
//**********************************************************************************************************




}

4.3 epoll的优缺点

优点

  1. 性能卓越:epoll在处理大量连接时性能出色,这主要得益于其事件驱动机制。当文件描述符就绪时,epoll会立即通知应用程序,避免了不必要的轮询,从而提高了效率。
  2. 高可扩展性:随着连接数的增加,epoll的性能下降相对较慢。这使得它非常适合处理大规模并发连接的场景。
  3. 支持边缘触发模式:epoll不仅支持传统的水平触发模式,还支持边缘触发模式。边缘触发模式可以减少不必要的通知,进一步提高应用程序的效率。
  4. 内存使用效率高:epoll通过内核与用户空间之间的mmap内存映射来加速消息传递,减少了数据拷贝的次数,从而提高了内存使用效率。

缺点:

  1. 平台限制:epoll是Linux特有的机制,因此在其他操作系统上无法使用。这使得跨平台开发时可能需要考虑其他I/O多路复用机制。
  2. 兼容性问题:在某些较旧的Linux发行版中,epoll的支持可能不够完善或存在已知的问题。因此,在使用epoll时需要注意目标平台的兼容性问题。

适用场景:

epoll其实是一个万金油,对于现在的网络服务器来说几乎大部分都在用epoll作为底层的IO管理。首先,在高并发网络通信中,epoll能够高效地处理大量并发连接,显著提高网络通信的效率。这得益于其事件驱动的特性,只在文件描述符状态发生变化时通知程序,避免了不必要的轮询,从而降低了系统资源的消耗。

其次,对于实时性要求高的应用,如游戏、语音聊天等,epoll同样表现出色。这些应用需要高效率的I/O操作来保证实时性,而epoll通过其高效的I/O事件通知机制,能够确保及时响应和处理各种I/O事件。

最后,在大规模分布式系统中,epoll通过其稳定性和可靠性,可以实现系统的高效运行。通过使用epoll技术,可以确保系统在处理大量连接和复杂操作时,仍然能够保持稳定的性能和响应速度。

其实epoll的底层实现,采用的是一个红黑树的数据结构,他的实现原理其实是一个很具有研究价值的东西。后续我将会对epoll进行更深入的研究。

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值