Linux系统编程(六)线程同步、互斥机制


  

前述:同步机制的引入及概念

为了更全面的学习,在学习本章之前,请查看《线程的创建与使用》这篇文章。
   进程中的多线程之间使用的是共享的数据,那么如果多个线程同时操作同一个共享资源,那么岂不是很混乱了。为了解决这个问题,引入了线程间的同步机制,即每次对共享资源进行操作时,只能有一个线程操作,其他线程必须等待其操作完毕后再进行操作。
   线程的同步机制是指在多线程编程中,用来协调线程之间的操作,以确保它们按预期顺序执行,避免竞态条件、死锁和其他并发问题。同步机制的主要目的是管理对共享资源的访问,确保数据一致性和程序的正确性。

序号机制描述
1互斥锁互斥锁的主要作用是同步,确保同一时刻只有一个线程能够进入临界区,从而避免竞态条件。例如,多个线程需要访问或修改同一个共享变量时,可以用互斥锁保护这个变量。
2条件变量条件变量用于线程等待某个条件成立,通常与互斥锁结合使用。
3信号量/灯信号量是一个计数器,用于控制对资源的访问,允许多个线程同时访问一定数量的资源。

一、互斥锁

1. 定义

   在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。mutex 是一种简单的加锁的方法来控制对共享资源的存取。这个互斥锁只有两种状态(上锁和解锁)。

问题:为什么需要对共享资源加锁?
   答:因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,以实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程才能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。

   注意:要使用互斥锁时,一定要避免死锁的出现!死锁是指两个或多个线程相互等待对方释放资源,从而导致所有线程都无法继续执行。避免死锁是多线程编程中的一个重要问题。且不应该在信号处理函数中使用互斥锁。

2. 互斥锁常用方法

    互斥锁类型: ( 1)快速锁(普通锁),(2)嵌套锁(递归锁),(3)检错锁。 这里我们使用最多的就是快速锁,即普通锁。

   互斥锁有两种创建(初始化)方法:静态方式(使用宏)和动态方式(用函数创建)。我们常使用动态创建。

3. 相关函数

(1)头文件

#include <pthread.h>

(2)创建互斥锁

   互斥锁需要在线程创建之前初始化好,以确保在任何线程尝试使用锁之前,锁已经准备就绪。这样做是为了防止竞争条件和潜在的未定义行为。

int pthread_mutex_init(pthread_mutex_t *mutex,  const pthread_mutexattr_t *mutexattr)
//pthread_mutex_t *mutex :传入互斥锁句柄的地址。
//const pthread_mutexattr_t *mutexattr :填NULL,表示该锁为普通锁。

(3)销毁互斥锁

   销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。

int pthread_mutex_destroy(pthread_mutex_t *mutex);
//pthread_mutex_t *mutex :传入互斥锁的句柄地址。

(4)加锁

   对共享资源进行操作前,需要进行加锁操作。

int pthread_mutex_lock(pthread_mutex_t *mutex)
//pthread_mutex_t *mutex :传入互斥锁的句柄地址。

(5)解锁

   对共享资源操作完毕后,需要进行解锁操作,以便其他线程使用这个共享资源。

int pthread_mutex_unlock(pthread_mutex_t *mutex)
//pthread_mutex_t *mutex :传入互斥锁的句柄地址。

4. 使用例程

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h> 

pthread_mutex_t mutex;       //互斥锁句柄
int Sum_tick= 10000000;      //假如:总票数。(公共资源)利用互斥锁保护。

void *task(void *arg)
{    
	int index= *(int *)arg;    //用index接收arg的值,区别第几个线程。
	free(arg); //释放内存
	
	int *slef_tick =(int *)malloc(sizeof(int *));         
	*slef_tick =0;   //本窗口售出票数统计
	//避免死锁的出现,必须要有解锁动作
	while(1)
	{
		//如果该线程上锁,则其他线程需要等待解锁才能继续执行。
		pthread_mutex_lock(&mutex);         //上锁:对公共资源使用前
		if(Sum_tick > 0)                        //还有票 
		{   
			 Sum_tick--;
			 pthread_mutex_unlock(&mutex);  //解锁:对公共资源操作完后
			 (*slef_tick)++;
		} 
		else 
		{
			pthread_mutex_unlock(&mutex);    //解锁: 防止死锁出现,当tick < 0时也需要解锁。
			break;                           //下班
		}
	}
	printf("%d号窗口今日售出%d张票。\n",index, *slef_tick);
//3. 退出线程并返回slef的数值
	pthread_exit((void *)slef_tick);
}


int main(int argc, char **argv)
{
	int ret;
	pthread_t thread[10];  //线程句柄
	int *thread_return;   
	int sum=0;             //sum记录所有子线程返回值的和。
	
//1. 在线程创建之前创建好互斥锁。  NULL为普通锁
	ret=pthread_mutex_init(&mutex, NULL);
	if(ret <0)
	{
		perror("pthread_mutex_init error!\n");
		return -1;
	}
	
//2. 创建10个线程:
	for(int i=0;i<10;i++)
	{
	    int *index=(int *)malloc(sizeof(int *));
	    *index=i;
		ret=pthread_create(&thread[i], NULL, task, (void *)index);  //把i的值传给函数func1的形参
		if(ret!=0)
		{
			perror("pthread_create error!\n");
			return -1;
		}
	}
	
//4. 等待线程退出,并释放线程资源
	for(int i=0;i<10;i++)
	{
		pthread_join(thread[i], (void **)&thread_return);
		sum = sum+ (*thread_return);
	}
	
	printf("今日景点总售出%d张票。\n",sum);
//5. 释放内存
	free(thread_return);
//6. 销毁互斥锁
	pthread_mutex_destroy(&mutex);
	return 0;
}

为什么每个线程售出的票数都不一样呢?
   答:这是因为线程间也存在竞争,谁抢到CPU的使用权谁就开始执行。我们可以查看CPU的核心数来确定每次可以同时有多少个线程同时运行。
在这里插入图片描述

二、条件变量

   条件变量是利用线程间共享的全局变量进行同步的一种机制。目的是为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。使用条件变量必定要使用互斥锁(普通锁)!
主要包括两个动作:①线程等待条件变量的条件成立而挂起。②线程使条件成立(给出条件成立信号)。

1. 相关函数

(1)创建条件变量

int pthread_cond_init(pthread_cond_t *cond,  pthread_condattr_t *cond_attr);
//pthread_cond_t *cond :条件变量的句柄。
//pthread_condattr_t *cond_attr :属性,通常位NULL。

(2)注销条件变量

   只有在没有线程在该条件变量上等待的时候才能注销这个条件变量。因为 Linux 实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。

int pthread_cond_destroy(pthread_cond_t *cond);
//pthread_cond_t *cond :条件变量的句柄。

(3)等待条件变量成立

   等待方式有两种:无条件等待和计时等待。我们主要使用无条件等待函数。
   无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求 pthread_cond_wait()(或 pthread_cond_timedwait())的竞争条件。

●无条件等待

int pthread_cond_wait(pthread_cond_t *cond,  pthread_mutex_t *mutex);
//pthread_cond_t *cond :要等待的条件变量句柄。
// pthread_mutex_t *mutex :相对应的互斥锁句柄。

   特别注意: 在使用等待函数前,必须加上互斥锁。因为对共享资源的修改都是在条件变量成立后进行修改,因为不确定何时条件会成立。所以要确保在等待条件的过程中,互斥锁已经被锁定,保护共享资源。这里虽然在等待函数前加上了互斥锁,但是等待函数有个特性,就是在条件不成立时,会自动解开互斥锁并挂起(阻塞)等待条件成立,以免影响其他线程使用共享资源。当条件成立后,会解除阻塞并自动加上互斥锁(不需要手动上锁),然后继续执行。使用如下:

//步骤1:先上锁,再等待条件成立。
pthread_mutex_lock(&mutex);		

//步骤2:等待条件成立,当不成立时,自动解开步骤1的锁,挂起等待。当条件成立后,自动上锁。
pthread_cond_wait(&cond,&mutex);  

//步骤3:对共享资源进行修改
data++;  

步骤4:解锁,这里解的锁不是步骤1的锁,而是步骤2条件成立后自动上的锁。
pthread_mutex_unlock(&mutex);      

(4)条件变量激发(使条件变量成立)

激发条件有两种形式:
【1】激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个。

int pthread_cond_signal(pthread_cond_t *cond); 
//pthread_cond_t *cond :要激活的条件变量句柄。

【2】激活所有等待线程。

int pthread_cond_broadcast(pthread_cond_t *cond);

2. 使用注意

   不应该在同一个线程中既等待条件变量的成立,又激活该条件变量。这通常是两个不同角色的线程所执行的任务。等待条件变量的线程(等待者)和激活条件变量的线程(信号者)应该是不同的,以避免逻辑错误和死锁的情况。

3. 使用例程

   功能:模拟有2个水果摊卖水果,一共有5箱水果。还有一个水果仓库用来供货。当水果摊的水果卖完后,发送条件变量激活信号给水果仓库,水果仓库供货。

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

pthread_mutex_t mutex;                         //互斥锁的句柄
pthread_cond_t  cond;                          //条件变量的句柄

int Sum_fruits= 10;                                  //假如:总箱数。(公共资源)利用互斥锁保护。

//------------------------------------函数体1--------------------------------------------
void *func1 (void *arg)
{
   int index=*(int *)arg;                   //用num接收arg的值,也就是i的值。
   free(arg);
   int *slef_num = (int *)malloc(sizeof(int *));
   *slef_num=0;                            //卖出水果箱数统计
  
  while(1)
  	{
  	  pthread_mutex_lock(&mutex);                      //上锁:对公共资源使用前
  	  if(Sum_fruits > 0)                                     //还有货 
	  	{   Sum_fruits--;
	        if(Sum_fruits ==0)  pthread_cond_signal(&cond);   //激发条件。激发一个没有等待指令的条件变量无意义。
	        pthread_mutex_unlock(&mutex);              //解锁:对公共资源操作完后
	        (*slef_num) ++;	   
	    } 
      else 
	  	{
	  	  pthread_mutex_unlock(&mutex);         //解锁: 防止死锁出现,当tick < 0时也需要解锁。
	   	  break;                                //下班
      	}
	  printf("%d号摊位售出一箱水果,剩余%d箱\n",index, Sum_fruits);
	  sleep(1);
    }  
     
    printf("%d号摊位今日售出%d箱水果,下班\n",index, *slef_num );
    //退出线程并返回slef的数值
    pthread_exit((void *)slef_num);
}
//------------------------------------函数体2-------------------------------------------
void *func2 (void *arg)                   //仓库等待激发指令
{
    while(1)                       
		{
		   pthread_mutex_lock(&mutex);		    //上锁。
		   pthread_cond_wait(&cond,&mutex);     //等待条件成立。   
		   Sum_fruits =10;                              //送货
		   printf("送10箱水果到水果摊\n");
		   /*这里的解锁不是和第51行的上锁配对!!而是解开56行的等待条件成立后的锁*/
		   pthread_mutex_unlock(&mutex);         //解锁。
        }
}
//----------------------------------主函数----------------------------------------
int main(int argc, char **argv)
{
   int ret;
   pthread_t thread[2];                  //创建 2个 水果摊 线程
   pthread_t thread2;                    //创建 1个 水果仓库 线程
   int *thread_result;                         //用于接收返回值
   int sum=0;                      //用n来接收子线程的返回值slef。sum记录所有子线程返回值的和。
   
//1. 在线程创建之前创建好互斥锁 。NULL为普通锁。
   ret=pthread_mutex_init(&mutex, NULL);
   if(ret<0)
		{
	      perror("pthread_mutex_init error!\n");
		  return -1;
		}
//2. 在线程创建之前创建好条件变量。
   ret=pthread_cond_init(&cond,NULL);
    if(ret<0)
		{
	      perror("pthread_cond_init error!\n");
		  return -1;
		}
//3. 创建线程。2个卖水果的摊位。
  for(int i=0;i<2;i++)
  	{
  	  int *index=(int *)malloc(sizeof(int *));
  	  *index=i;
	  ret=pthread_create(&thread[i], NULL, func1, (void *)index);  //把i的值传给函数func1的形参
	  if(ret!=0)
		{
	      perror("pthread_create error!\n");
		  return -1;
		}
  	}
  
//4. 创建1个线程,用于水果仓库供货
   ret=pthread_create(&thread2, NULL, func2, NULL);  
	  if(ret!=0)
		{
	      perror("pthread_create error!\n");
		  return -1;
		}
//5. 等待子线程退出,并释放资源。 
	for(int i=0;i<2;i++)
	{
		pthread_join(thread[i], (void **)&thread_result);
		sum = sum+ (*thread_result);
		printf("sum=%d\n",sum);	  
	}
	free(thread_result); //释放malloc申请的内存资源。
	//等待水果仓库 子线程退出,并释放其占用的资源,不接收返回值
	pthread_join(thread2, NULL);
	
//销毁互斥锁
	pthread_mutex_destroy(&mutex);
//销毁条件变量
	pthread_cond_destroy(&cond);
	return 0;
}

三、信号灯

1. 分类

   二元信号灯:也叫互斥量,只能取0和1两个值。它用于实现互斥访问,即一次只允许一个线程访问共享资源。
   计数信号灯:计数范围为0到N,允许多个线程访问有限数量的共享资源。

2. 信号灯操作

  等待(P操作):检查信号灯的值。如果信号灯的值大于0,则将其减1并继续执行;如果信号灯的值为0,则阻塞等待,直到信号灯的值大于0。
  释放(V操作):将信号灯的值加1,并唤醒等待的线程(如果有)。

3. 相关函数

(0)头文件

#include <semaphore.h>

(1)创建信号灯
成功返回 0,失败返回负数。

int sem_init(sem_t *sem, int pshared, unsigned int value);
/*
sem_t *sem:要操作的信号灯句柄。
int pshared:是否为多进程共享而不仅仅是用于一个进程之间的多线程共享。通常为0,表示线程间。 1 - 进程同步。
unsigned int value:为信号灯的初值。
*/

(2)注销灯
  被注销的信号灯 sem 要求已没有线程在等待该信号灯,否则返回-1,且置 errno 为EBUSY。除此之外,Linux Threads 的信号灯注销函数不做其他动作。

int sem_destroy(sem_t * sem);
//sem_t *sem:要操作的信号灯句柄。

(3)点灯
  点灯操作将信号灯值原子地加 1,表示增加一个可访问的资源。只有信号灯值大于 0,才能访问公共资源。 当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞。

int sem_post(sem_t * sem); //相当于解锁
//sem_t * sem:要操作的信号灯句柄。

(4)灭灯
  sem_wait 主要被用来阻塞当前线程直到信号量 sem 的值大于 0,解除阻塞后将 sem 的值减一,表明公共资源经使用后减少。等待灯亮(信号灯值大于 0),然后将信号灯原子地减 1,并返回。

int sem_wait(sem_t * sem); //相当于加锁
//sem_t * sem:要操作的信号灯句柄。

(5)获取灯值

int sem_getvalue(sem_t * sem, int * sval);
//sem_t * sem:要操作的信号灯句柄。
//int * sval:将值存放在这个指针所致的地址下。

4. 使用例程

   功能:模拟有2个水果摊卖水果,一共有5箱水果。还有一个水果仓库用来供货。当水果摊的水果卖完后,发送条件变量激活信号给水果仓库,水果仓库供货。(使用信号灯)

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>

pthread_mutex_t mutex;         //互斥锁的句柄
sem_t  sem;                    //信号灯句柄

int Sum_fruits= 10;                                  //假如:总箱数。(公共资源)利用互斥锁保护。

//------------------------------------函数体1--------------------------------------------
void *func1 (void *arg)
{
   int index=*(int *)arg;                   //用num接收arg的值,也就是i的值。
   free(arg);
   int *slef_num = (int *)malloc(sizeof(int *));
   *slef_num=0;                            //卖出水果箱数统计
  
  while(1)
  	{
  	  pthread_mutex_lock(&mutex);                      //上锁:对公共资源使用前
  	  if(Sum_fruits > 0)                                     //还有货 
	  	{   Sum_fruits--;
	        if(Sum_fruits ==0)   sem_post(&sem);    //点灯:+1   
	        pthread_mutex_unlock(&mutex);           //解锁:对公共资源操作完后
	        (*slef_num) ++;	   
	    } 
      else 
	  	{
	  	  pthread_mutex_unlock(&mutex);         //解锁: 防止死锁出现,当tick < 0时也需要解锁。
	   	  break;                                //下班
      	}
	  printf("%d号摊位售出一箱水果,剩余%d箱\n",index, Sum_fruits);
	  sleep(1);
    }  
     
    printf("%d号摊位今日售出%d箱水果,下班\n",index, *slef_num );
    //退出线程并返回slef的数值
    pthread_exit((void *)slef_num);
}
//------------------------------------函数体2-------------------------------------------
void *func2 (void *arg)                   //仓库等待激发指令
{
    while(1)                       
		{
		   sem_wait(&sem);	                     //灭灯: -1
		   Sum_fruits=10;                              //送货	
		   printf("送10箱水果到水果摊\n");
        }
}
//----------------------------------主函数----------------------------------------
int main(int argc, char **argv)
{
   int ret;
   pthread_t thread[2];                  //创建 2个 水果摊 线程
   pthread_t thread2;                    //创建 1个 水果仓库 线程
   int *thread_result;                         //用于接收返回值
   int sum=0;                      //用n来接收子线程的返回值slef。sum记录所有子线程返回值的和。
   
//1. 在线程创建之前创建好互斥锁 。NULL为普通锁。
   ret=pthread_mutex_init(&mutex, NULL);
   if(ret<0)
		{
	      perror("pthread_mutex_init error!\n");
		  return -1;
		}
//2. 在线程创建之前创建好信号灯      。
	ret=sem_init(&sem, 0, 0);
	if(ret<0)
	{
		perror("sem_init error!\n");
		return -1;
	}
		
//3. 创建线程。2个卖水果的摊位。
  for(int i=0;i<2;i++)
  	{
  	  int *index=(int *)malloc(sizeof(int *));
  	  *index=i;
	  ret=pthread_create(&thread[i], NULL, func1, (void *)index);  //把i的值传给函数func1的形参
	  if(ret!=0)
		{
	      perror("pthread_create error!\n");
		  return -1;
		}
  	}
  
//4. 创建1个线程,用于水果仓库供货
   ret=pthread_create(&thread2, NULL, func2, NULL);  
	  if(ret!=0)
		{
	      perror("pthread_create error!\n");
		  return -1;
		}
//5. 等待子线程退出,并释放资源。 
	for(int i=0;i<2;i++)
	{
		pthread_join(thread[i], (void **)&thread_result);
		sum = sum+ (*thread_result);
		printf("sum=%d\n",sum);	  
	}
	free(thread_result); //释放malloc申请的内存资源。
	//等待水果仓库 子线程退出,并释放其占用的资源,不接收返回值
	pthread_join(thread2, NULL);
	
   //销毁信号灯
	sem_destroy(&sem);
   //销毁互斥锁
	pthread_mutex_destroy(&mutex);
	return 0;
}

在这里插入图片描述

四、原子操作(内核层)

   原子操作(atomic operations)是指对变量进行不可分割的操作。这些操作在多处理器环境中非常重要,因为它们保证了对共享变量的操作不会被中断,从而避免数据竞争和不一致性。

1. 优势

   避免数据竞争:在多处理器或多线程环境中,多个处理器或线程可能同时访问和修改共享变量。原子操作确保这些操作是不可分割的,不会被其他操作中断,从而避免数据竞争。
   简化代码:使用原子操作可以避免使用复杂的锁机制,使代码更加简洁和高效。

2. 常用的原子操作函数(内核层函数)

头文件:#include <linux/atomic.h>
●对于32位原子变量:atomic_t num; 。下列函数调用一次,只执行一次。

atomic_inc(&num);//对原子变量 v 进行递增操作。
atomic_dec(&num);//对原子变量 v 进行递减操作。
atomic_add(i, &num);//将 i 加到原子变量 v 上。
atomic_sub(i, &num);//从原子变量 v 中减去 i。
atomic_set(&num, i);//将原子变量 v 设置为 i。
atomic_read(&num);//读取原子变量 v 的值。

●对于64位原子变量:atomic64_t num;。下列函数调用一次,只执行一次。

atomic64_inc(&num);//对原子变量 v 进行递增操作。
atomic64_dec(&num);//对原子变量 v 进行递减操作。
atomic64_add(i, &num);//将 i 加到原子变量 v 上。
atomic64_sub(i, &num);//从原子变量 v 中减去 i。
atomic64_set(&num, i);//将原子变量 v 设置为 i。
atomic64_read(&num);//读取原子变量 v 的值。

3. 示例代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/atomic.h>

static atomic_t my_counter = ATOMIC_INIT(0);  //将原子值初始化为0

static int my_module_init(void) {
    printk(KERN_INFO "My Kernel Module: Initialization\n");

    // 原子地递增计数器
    atomic_inc(&my_counter);
    printk(KERN_INFO "Counter Value: %d\n", atomic_read(&my_counter));

    return 0;  // 成功返回0
}

static void my_module_exit(void) {
    printk(KERN_INFO "My Kernel Module: Exit\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");

五、习题

题目1:创建4个线程,线程按顺序执行并打印0~9。即第一个线程打印完0-9后,第二个线程开始执行打印…

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

// 信号量数组,用于控制线程的顺序
sem_t semaphores[4];

// 线程函数
void* print_numbers(void* arg) {
    int thread_index = *((int*)arg);
    
    // 等待前一个线程完成
    sem_wait(&semaphores[thread_index]);
    
    // 打印0~9
    for (int i = 0; i < 10; ++i) {
        printf("Thread %d: %d\n", thread_index, i);
    }
    
    // 释放下一个线程
    sem_post(&semaphores[thread_index + 1]);
    
    return NULL;
}

int main() {
    pthread_t threads[4];
    int thread_indices[4];

    // 初始化信号量
    for (int i = 0; i < 4; ++i) {
        sem_init(&semaphores[i], 0, 0);
    }
    // 第一个信号量设为1,允许第一个线程执行
    sem_post(&semaphores[0]);

    // 创建并启动4个线程
    for (int i = 0; i < 4; ++i) {
        thread_indices[i] = i;
        pthread_create(&threads[i], NULL, print_numbers, &thread_indices[i]);
    }

    // 等待所有线程完成
    for (int i = 0; i < 4; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 销毁信号量
    for (int i = 0; i < 4; ++i) {
        sem_destroy(&semaphores[i]);
    }

    return 0;
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值