进程同步及避免死锁经典问题

Linux多进程实现读写者问题:读优先、写优先、公平排队实现 1. 任务简介 一个数据文件或记录可被多个进程共享,我们把只要求读该文件的进程称为 reader进稈,其他进程称为writer进程。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但不允许一个writer进程和其他reader进程或writer进程同时访回共享对象,因为这种访问将会引起混乱。所谓读者-写者问题(reader-writer problem)是指保证一个writer进程必须与其他进程互斥地访问共享对象的向步问题。读者-写者问题常被用手测试新同步原语。 读者-写者问题被提出后,就一直 阅读详情

定义PV操作的含义:PV操作是由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:
    P(S):将信号量S的值减1,即S=S-1;如果S>0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
    V(S):将信号量S的值加1,即S=S+1;如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。
P操作相当于申请资源,而V操作相当于释放资源。


一、读者-写者问题

计算机系统中的数据(文件、记录)常被多个进程共享,但其中某些进程可能只要求读数据(称为读者Reader),另一些进程则要求修改数据(称为写者Writer)。就共享数据而言,Reader和Writer是两组并发进程共享一组数据区,要求:

(1)允许多个读者同时执行读操作;
(2)不允许读者、写者同时操作;
(3)不允许多个写者同时操作。
Reader和Writer的同步问题分为读者优先、弱写者优先(公平竞争)和强写者优先三种情况,它们的处理方式不同:

1、读者优先

int rc = 0; //记录有多少个读者在读
semaphore rc_semaphore = 1; //对rc互斥访问的信号量
semaphore write_semaphore = 1; //保证读写互斥的信号量

void reader()
{
	do 
	{
		P(rc_semaphore);
		rc++;
		if(1==rc)
			P(write_semaphore);
		V(rc_semaphore);

		read operating.....

	    P(rc_semaphore);
		rc--;
		if(0==rc)
			V(write_semaphore);
		v(rc_semaphore);
	} while (true);
}

void writer()
{
	do 
	{
		P(write_semaphore);
		write operating.....
		V(write_semaphore);
	} while (true);
}
缺点:当不断有读者来读取时,写者就会陷入无限的等待。

2、写者优先(弱优先性)

int rc = 0; //记录有多少个读者在读
semaphore rc_semaphore = 1; //对rc互斥访问的信号量
semaphore write_semaphore = 1; //保证读写互斥的信号量
semaphore read_semaphore = 1;

void reader()
{
	do 
	{
		P(read_semaphore);
		P(rc_semaphore);
		rc++;
		if(1==rc)
			P(write_semaphore);
		V(rc_semaphore);
		V(read_semaphore);

		Reading the file....

		P(rc_semaphore);
		rc--;
		if(0==rc)
			V(write_semaphore);
		V(rc_semaphore);

	} while (true);
}

void writer()
{
	do 
	{
		P(read_semaphore);
		P(write_semaphore);
		writing the file.....
		V(write_semaphore);
		V(read_semaphore);
	} while (true);
}
缺点:这个是按照先来先服务的原则进行读写的,比如当一个进程在写时,来了N个进程读,读者们都在等写者结束,此时又来了个写者,第二个写者就要等前面N个读者读完后才能写。

3、写者优先(强优先性)

int rc = 0;
int wc = 0;
semaphore write_semaphore = 1;
semaphore rc_semaphore =1;
semaphore wc_semaphore = 1;

void reader()
{
	do 
	{
		P(read_semaphore);
		P(rc_semaphore);
		rc++;
		if(1==rc)
			P(write_semaphore);
		V(rc_semaphore);
		V(read_semaphore);

		reading the file.....

		P(rc_semaphore);
		rc--;
		if(0==rc)
			V(write_semaphore);
		V(rc_semaphore);
	} while (true);
}

void writer()
{
	do 
	{
		P(wc_semaphore);
		wc++;
		if(1==wc)
			P(read_semaphore);
		V(wc_semaphore);

		P(write_semaphore);
		writing the file......
		V(write_semaphore);

		P(wc_semaphore);
		wc--;
		if(0==wc)
			V(read_semaphore);
		V(wc_semaphore);
	} while (true);
}

保证了写者优先,只要有写者在写,后来的读者写者中优先执行写者。

二、生产者-消费者

生产者-消费者问题指的是在若干个生产者生产产品,放入共享的缓冲区,若干消费者从缓冲区中取出产品消费;生产者和消费者是互斥访问共享的缓冲区的,若缓冲区没有空位生产者要等待,若缓冲区没有产品消费者要等待,以下通过代码来演示这个过程。

注:以下代码修改自http://blog.sina.com.cn/s/blog_58069bd20100as5w.html

#include <windows.h>
#include <iostream>

using namespace std;

const unsigned short SIZE_OF_BUFFER = 10; //缓冲区长度
unsigned short ProductID = 0; //产品号
unsigned short ConsumeID = 0; //将被消耗的产品号
unsigned short in = 0; //产品进缓冲区时的缓冲区下标
unsigned short out = 0; //产品出缓冲区时的缓冲区下标
int g_buffer[SIZE_OF_BUFFER]; //缓冲区是个循环队列

HANDLE g_hMutex; //用于线程间的互斥
HANDLE g_hFullSemaphore; //当缓冲区满时迫使生产者等待
HANDLE g_hEmptySemaphore; //当缓冲区空时迫使消费者等待
DWORD WINAPI Producer(LPVOID); //生产者线程
DWORD WINAPI Consumer(LPVOID); //消费者线程

int main()
{
	//创建各个互斥信号
	g_hMutex = CreateMutex(NULL,FALSE,NULL);
	g_hFullSemaphore = CreateSemaphore(NULL,0,SIZE_OF_BUFFER,NULL);
	g_hEmptySemaphore = CreateSemaphore(NULL,SIZE_OF_BUFFER,SIZE_OF_BUFFER,NULL);
	
	//调整下面的数值,可以发现,当生产者个数多于消费者个数时,
	//生产速度快,生产者经常等待消费者;反之,消费者经常等待
	const unsigned short PRODUCERS_COUNT = 3; //生产者的个数
	const unsigned short CONSUMERS_COUNT = 2; //消费者的个数
	//总的线程数
	const unsigned short THREADS_COUNT = PRODUCERS_COUNT+CONSUMERS_COUNT;
	HANDLE hThreads[THREADS_COUNT]; //各线程的handle
	DWORD producerID[PRODUCERS_COUNT]={1,2,3}; //生产者线程的标识符
	DWORD consumerID[CONSUMERS_COUNT]={1,2}; //消费者线程的标识符
	//创建生产者线程
	for (int i=0;i<PRODUCERS_COUNT;++i){
		hThreads[i]=CreateThread(NULL,0,Producer,&producerID[i],0,NULL);
		if (hThreads[i]==NULL) return -1;
	}
	//创建消费者线程
	for (int i=0;i<CONSUMERS_COUNT;++i){
		hThreads[PRODUCERS_COUNT+i]=CreateThread(NULL,0,Consumer,&consumerID[i],0,NULL);
		if (hThreads[i]==NULL) return -1;
	}

	while(true){
		Sleep(10000);
	}

	return 0;
}
//生产一个产品。简单模拟了一下,仅输出新产品的ID号
void Produce()
{
	std::cerr << "Producing " << ++ProductID << " ... ";
	std::cerr << "Succeed" << std::endl;
}
//把新生产的产品放入缓冲区
void Append()
{
	std::cerr << "Appending a product ... ";
	g_buffer[in] = ProductID;
	in = (in+1)%SIZE_OF_BUFFER;
	std::cerr << "Succeed" << std::endl;
	//输出缓冲区当前的状态
	for (int i=0;i<SIZE_OF_BUFFER;++i){
		std::cout << i <<": " << g_buffer[i];
		if (i==in) std::cout << " <-- 生产";
		if (i==out) std::cout << " <-- 消费";
		std::cout << std::endl;
	}
}
//从缓冲区中取出一个产品
void Take()
{
	std::cerr << "Taking a product ... ";
	ConsumeID = g_buffer[out];
	g_buffer[out] = 0;
	out = (out+1)%SIZE_OF_BUFFER;
	std::cerr << "Succeed" << std::endl;
	//输出缓冲区当前的状态
	for (int i=0;i<SIZE_OF_BUFFER;++i){
		std::cout << i <<": " << g_buffer[i];
		if (i==in) std::cout << " <-- 生产";
		if (i==out) std::cout << " <-- 消费";
		std::cout << std::endl;
	}
}
//消耗一个产品
void Consume()
{
	std::cerr << "Consuming " << ConsumeID << " ... ";
	std::cerr << "Succeed" << std::endl;
}
//生产者
DWORD WINAPI Producer(LPVOID lpPara)
{
	while(true){
		WaitForSingleObject(g_hEmptySemaphore,INFINITE);
		WaitForSingleObject(g_hMutex,INFINITE);
		Produce();
		Append();
		Sleep(3000);
		ReleaseMutex(g_hMutex);
		ReleaseSemaphore(g_hFullSemaphore,1,NULL);
	}
	return 0;
}
//消费者
DWORD WINAPI Consumer(LPVOID lpPara)
{
	while(true){
		WaitForSingleObject(g_hFullSemaphore,INFINITE);
		WaitForSingleObject(g_hMutex,INFINITE);
		Take();
		Consume();
		Sleep(3000);
		ReleaseMutex(g_hMutex);
		ReleaseSemaphore(g_hEmptySemaphore,1,NULL);
	}
	return 0;
}

三、哲学家进餐

问题描述:

五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘通心粉。由于通心粉很滑,所以需要两把叉子才能夹住。相邻两个盘子之间放有一把叉子,哲学家的生活中有两种交替活动时段:即吃饭和思考。当一个哲学家觉得饿了时,他就试图分两次去取其左边和右边的叉子,每次拿一把,但不分次序。如果成功地得到了两把叉子,就开始吃饭,吃完后放下叉子继续思考。关键问题是:能为每一个哲学家写一段描述其行为的程序,且决不会死锁吗(最好能达到最大的并行程度)?

解法思路:

1、为每个哲学家分配一个semaphore[i]初始值为0,最大值为1

2、当一个哲学家试图进餐时先通过互斥量获得对哲学家们的状态state的独占性访问         

3、改变i哲学家的状态为HUNGRY,测试其左右哲学家是否在用餐,若左右哲学家都不在用餐,则i哲学家可以用餐,改变state[i]=EATING,释放其semaphore[i],释放state,拿起餐插成功进餐;若左右哲学家有人在进餐,释放state,i哲学家等待

4、哲学家用餐完毕,获取对state的独占性访问,改变state[i]=THINKING,此时i哲学家释放了两个餐插,测试其左右哲学家是否在HUNGRY状态

5、重复2-4步骤直到达到最大进餐次数


参考如下代码:(以下代码修改自http://blog.163.com/diaoshuo_1/blog/static/318902012009418115121514/)

/*
Item       The Dining Philosophers Problem
具体要求
1) 5个哲学家,先吃饭,后思考。
2) 每个哲学家都需要吃9顿饭,思考9次,然后结束。
3)吃饭时间为3~7秒的随机时间
4)思考时间为3~9秒的随机时间
*/
#include <windows.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>

#define N       5            //哲学家数目
#define MAXNUM 9     //进餐思考次数
#define LEFT (i+N-1)%N            //i为左邻居编号
#define RIGHT      (i+N+1)%N           //i为右邻居编号
#define THINKING       0                          //哲学家在思考
#define HUNGRY  1                                 //哲学家视图拿起叉子
#define EATING    2                                 //哲学家进餐

///////线程参数
typedef struct _THREAD_ARGS{
	int id;//哲学家编号
	int maxnum; //进餐思考最大次数
}THREAD_ARGS, *PTHREAD_ARGS;

int state[N];//用来记录每位哲学家的状态
int g_philobuf_fork_num;//缓冲区中叉子数目
char mutex_g_buf[]="PS_IPC_PHILO_G_BUF";
HANDLE mutex_g_buf_h;  //为了互斥访问state[N]
//Semaphores' Name
char* sem_perphilo_name[N] = {"PS_IPC_PHILO_SEM_PERPHILO_0","PS_IPC_PHILO_SEM_PERPHILO_1",
"PS_IPC_PHILO_SEM_PERPHILO_2","PS_IPC_PHILO_SEM_PERPHILO_3",
"PS_IPC_PHILO_SEM_PERPHILO_4"};

HANDLE sem_perphilo_h[N];   //每个哲学家一个semaphore,初始值为0,最大值为1

DWORD WINAPI philosopher(void *pArgs);
void take_forks(int i);
void put_forks(int i);
void test(int i);

//线程函数
DWORD WINAPI philosopher(void *pArgs)
{
	PTHREAD_ARGS pTHREAD_ARGS;
	pTHREAD_ARGS=(PTHREAD_ARGS)pArgs;
	int i=pTHREAD_ARGS->id;
	int totalNum = pTHREAD_ARGS->maxnum;

	for(int count=0;count<totalNum;count++)
	{
		take_forks(i);
		srand ((DWORD)time(NULL));
		int eat_t=rand()%4000 + 3000;
		Sleep(eat_t);
		printf("Philosopher %d finishes eating!this is the %d(th) eating time! eat_t:%d\n",i,count+1,eat_t);
		put_forks(i);

		srand ((DWORD)time(NULL));
		int think_t= rand()%6000 + 3000;
		Sleep(think_t);
		printf("Philosopher %d finishes thinking!this is the %d(th) thinking time! think_t:%d\n",i,count+1,think_t);
	}

	printf("Philosopher %d has finished all the eating and thinking activity\n!",i);

	return 0;
}

void take_forks(int i)
{
	WaitForSingleObject(mutex_g_buf_h,INFINITE);
	state[i]=HUNGRY;
	printf("*************************take_forks start***************************\n");
	printf("Philosopher %d is hungry!  Try to take the forks\n",i);
	printf("*************************take_forks end***************************\n\n");
	test(i);
	ReleaseMutex(mutex_g_buf_h);

	WaitForSingleObject(sem_perphilo_h[i],INFINITE);
}

void put_forks(int i)
{
	WaitForSingleObject(mutex_g_buf_h,INFINITE);
	state[i]=THINKING;
	g_philobuf_fork_num=g_philobuf_fork_num-2;//放下了两把叉子
	printf("*************************put_forks start***************************\n");
	printf("Philosopher %d puts down the fork and starts thinking! current forks num:%d\n",i,g_philobuf_fork_num);
	printf("*************************put_forks end***************************\n\n");
	test(LEFT);
	test(RIGHT);
	ReleaseMutex(mutex_g_buf_h);
}
void test(int i)
{
	LONG oldSemValue;
	if(state[i]==HUNGRY&&state[LEFT]!=EATING&&state[RIGHT]!=EATING)
	{
		state[i]=EATING;
		g_philobuf_fork_num=g_philobuf_fork_num+2;//拿起了两把叉子
		printf("*************************test start***************************\n");	
		printf("After testing,Philosopher %d can starts eating! current forks num:%d\n",i,g_philobuf_fork_num);
		printf("*************************test end***************************\n\n");
		ReleaseSemaphore(sem_perphilo_h[i],1,&oldSemValue);
	}
}

int main(int argc,char*argv[])
{
	time_t launch=time(NULL);

	printf("****************************************************\n");
	printf("开始运行时间:%s",ctime(&launch));
	printf("哲学家就餐问题\n");
	printf("哲学家数量:%d\n",N);
	printf("进餐思考次数:%d\n",MAXNUM);      
	printf("****************************************************\n");

	DWORD Status;
	DWORD *ThId_Philo=new DWORD[N];
	HANDLE *philo_h=new HANDLE[N];
	THREAD_ARGS *ThArgs_Philo=new THREAD_ARGS[N];

	//当前拿起的叉子数目
	g_philobuf_fork_num=0;

	mutex_g_buf_h=CreateMutex(NULL,FALSE,mutex_g_buf);
	int nCounter=0;

	for(nCounter=0;nCounter<N;nCounter++)
	{
		sem_perphilo_h[nCounter]=(HANDLE)CreateSemaphore(NULL,0,1,
			sem_perphilo_name[nCounter]);
	}
	//CreateThread
	for(nCounter=0;nCounter<N;nCounter++)
	{
		ThArgs_Philo[nCounter].id=nCounter;
		ThArgs_Philo[nCounter].maxnum=MAXNUM;
		philo_h[nCounter]=(HANDLE)CreateThread(NULL,0,philosopher,
			&ThArgs_Philo[nCounter],0,&ThId_Philo[nCounter]);
	}

	/* w
操作系统-PV原语(三) 一、哲学家进餐问题 由Dijkstra提出并解决的哲学家进餐问题(The Dinning Philosophers Problem)是典型的同步问题。 该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。 1.利用记录型信号量解决哲学家解决问题 经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许 阅读详情

相关推荐

操作系统之经典进程同步问题

这里介绍三中进程同步问题:         1.生产者-消费者问题         2.读者-写者问题         3.哲学家进餐问题 一、生产者-消费者问题      1.问题描述:生产者-消费者模型描述的是有一群生产者进程在生产产品,并将这些产品提供给消费者进程并发进行,具备并发程序的典型特征。PCM为使生产者进程和消费者进程并发进行,在它们之间设置一个具有多个缓冲区的缓

honeysyt.的博客 1万+

进程同步之熟睡的理发师问题

熟睡的理发师问题(The Sleeping-Baber Problem)是操作系统中关于进程同步的一个经典问题,它涉及了到临界区保护、锁、信号量等方面的知识。在这篇博客中,我将具体讲解这个问题并用pthread库编码解决,相关pthread库的使用可查阅IEEE官方文档,我是传送门~~~。 问题描述:  某个理发店有个瞌睡虫理发师,只要没有顾客,他就会去睡觉;而整个理发店由两个区域组成:有N把...

August的博客 8064

经典进程同步与互斥问题

经典进程同步与互斥问题 文章目录经典进程同步与互斥问题一、生产者与消费者问题1.1 问题概述1.2 解决方法二、读者与写者问题2.1 问题概述2.2 解决方法三、哲学家进餐过程3.1 问题概述3.2 解决方法 一、生产者与消费者问题 1.1 问题概述 生产者与消费者互斥使用一个缓冲池 当缓冲池为空时,生产者才能写入数据 当缓冲池不为空时,消费者才能读取数据 1.2 解决方法 设置3个信号量:m...

qlzy的博客 2753

睡觉理发师问题——进程同步与死锁

     前些时看了一下操作系统原理,当我看到进程同步这里时有一个问题引发了我的思考:睡觉理发师问题。     具体如下:一个理发店里有一个理发师,一张理发椅子,多张等候的椅子,当没有顾客的时候,理发师就睡觉。当一个顾客来到店里时,如果理发师在睡觉,则叫醒理发师;否则便坐着等;如果等待的椅子都满了就离开。      我觉得这个问题可以引发许多关于操作系统的问题。比如:进程同步及程序执行

superyinyi的专栏 5824

操作系统信号量PV经典问题:读者写者

操作系统信号量PV经典问题:读者写者,优先级是谁忘了,最经典的那个 C++编写

从零开始掌握RDMA拥塞控制:原理、算法与实战解析

本文深入解析了RDMA拥塞控制的核心原理与实战配置。RDMA依赖无损网络保障高性能,其关键在于PFC与ECN两大技术协同防止丢包。文章重点剖析了主流算法DCQCN的工作机制与参数调优,并简要介绍了HPCC与TIMELY等前沿算法。最后提供了在Linux上配置RoCEv2与DCQCN的详细步骤及性能调试方法,帮助读者从零构建稳定的RDMA网络环境。

weixin_28452161的博客 137

利用ADS设计射频功率放大器

利用ADS设计射频功率放大器 任务: 用飞思卡尔公司的功率放大器芯片ATF27s006NT1设计射频功放,芯片频率范围可达450MHz-3.6GHz。 功放指标: (1) 工作频率:500MHz (2) 增益:G>20dB (3) 电源:+28V (4) 最大输出功率:2W(33dBm) 设计思路 (1)安装DesignKit模型; (2)直流DC扫描; (3)稳定性分析; (4)偏置电路设计及整...

UVM频率 3万+

安卓无障碍服务保活实战:如何用AccessibilityService实现后台常驻(附完整代码)

本文深入解析如何利用Android无障碍服务(AccessibilityService)实现后台服务保活,涵盖多版本系统适配、前台服务集成、开机自启机制等关键技术。通过详细的Kotlin代码示例和合规性指导,帮助开发者构建稳定可靠的后台服务解决方案,同时避免因滥用无障碍服务导致的应用下架风险。

milk5的博客 1074

开发者体验度量:构建DevEx评估体系的方法论与工具

在深入讨论度量方法前,我们需要明确什么是开发者体验(DevEx)。许多人将其简化为"开发工具好不好用",这是一个危险的误解。开发者体验是指开发者在完成工作过程中的整体感受和效率,它由三大核心维度组成:工具与基础设施:开发、测试、部署工具链的效能流程与协作:工作流程的顺畅度和团队协作的效率文化与成长:支持实验、学习和专业发展的环境这三个维度相互作用,共同决定了开发者能否高效且愉悦地完成工作。开发者体验不是一个目的地,而是一段持续的旅程。

2401_88760782的博客 1430

Wireshark时间分析实战:精准调试网络延迟与时间同步问题

本文深入探讨了Wireshark在网络延迟与时间同步问题分析中的核心应用。通过实战案例,详细讲解了如何设置时间显示格式、利用时间参考点量化关键路径延迟,以及运用时间平移功能校正错位时间轴。掌握这些时间分析技巧,能帮助网络工程师精准定位隐藏于数据包时序中的性能瓶颈与故障根源。

weixin_29290963的博客 263

AGV舵轮选型实战:从核心参数到精准计算的完整指南

本文提供了一份详尽的AGV舵轮选型实战指南,强调从经验决策转向数据驱动。文章系统讲解了如何将工况需求转化为核心参数,并通过牵引力、扭矩、功率等关键计算步骤,指导工程师完成精准选型,避免因选型不当导致的运行故障与效率低下。

醉三国 498

大疆司空 2 私有化部署:FH2、Terra、4G 三大服务区别详解

摘要: 司空2私有版包含三大模块:FH2(核心平台,必选)、Terra(建模算力,需NVIDIA显卡)、4G(内网图传)。FH2是基础,支持设备管理与任务调度;Terra适用于测绘建模场景,依赖GPU算力;4G模块保障复杂环境下的图传稳定性,适合纯内网部署。选择逻辑:先部署FH2,再按需添加Terra(需显卡)或4G(需内网支持),避免资源浪费。

2501_93651177的博客 580

SKUA-GOCAD入门教程-第七节 点的创建与编辑3

本文介绍了四种从井数据创建点集的方法

江洋大葱的博客 964

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

上一篇: 关于Blog使用
下一篇: 字符串Hash函数
知行且执行
博客等级 码龄14年 38粉丝 73原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值