linux内核调度器 调度原理(2.6.24笔记整理)

Kevin.Liu             2012/10/20 ~2012/10/27               内核版本:2.6.24

博客:http://janneo.blog.sohu.com

Linux2.6.24内核,调度器章节的笔记

linux调度原理

linux调度器 


 本文只是为了方便今后复习整理的读书笔记,仅仅是将现有的知识用我自己的语言(和图画)进行重新表述,没有什么所谓的原创,都是参考他人的或者参考linux内核源码及文档。

博客中图片丢失,排版也不方便,因此建议下载pdf文档,地址http://download.csdn.net/detail/janneoevans/4699128

主要参考《深入理解Linux内核架构》(Wolfgang Mauerer)一书。

另外还参考了:

对switch_to的理解

http://home.ustc.edu.cn/~hchunhui/linux_sched.html#sec9 和 郭海林 同学的ppt

主调度器执行的时机

http://www.linuxdiyf.com/linux/201107/648.html

进程的状态切换

http://www.ibm.com/developerworks/linux/library/l-task-killable/

虚拟运行时间部分

http://blog.csdn.net/ustc_dylan/article/details/4140245

组调度

http://book.51cto.com/art/201005/200918.htm

 

题外话

此文对linux内核的理解或者表述很有可能出现多处谬误,希望您在读的时候保持怀疑的态度。如果发现错误了希望您能帮我指出,免得此文误导更多人,谢谢!

 注意:文中出现的名字“队列”仅仅是一个普通中文名词,表示具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue.

本文章也是“吃百家饭”而成的,可以任意转载,不要求注明原作者,但是你得保证我在更新了文章中的错误时,你也能够及时更新你转载的副本,否则错误会蔓延下去。 

目录

Linux2.6.24内核,调度器章节的笔记...1

1.     调度器相关的功能结构概述...2

1.1.        运行(Run Queue)队列...2

1.2.        核心调度器...3

1.1.1.       主调度器...3

1.1.2.       周期性调度器...4

1.3.        调度器类...5

2.     就绪进程在队列中如何排序的...7

2.1.        实时进程...7

2.2.        普通进程...8

2.2.1.       模型建立...9

2.2.2.       抽象模型的总结...18

2.2.3.       真实模型概述...18

2.2.4.       对应关系...21

2.2.5.       真实模型详述...22

3.     进程优先权...30

4.     核心调度器...35

4.1.        周期性调度器...36

4.2.        主调度器...42

4.2.1.       代码块1.43

4.2.2.       代码块2.43

4.2.3.       代码块3.44

4.2.4.       代码块4.45

4.2.5.       总结...46

4.2.6.       switch_to.47

5.     进程的状态切换...54

 

Linux2.6.24内核,调度器章节的笔记

 本文只是为了方便今后复习整理的读书笔记,仅仅是将现有的知识用我自己的语言(和图画)进行重新表述,没有什么所谓的原创,都是参考他人的或者参考linux内核源码及文档。

主要参考《深入理解Linux内核架构》(Wolfgang Mauerer)一书。

另外还参考了:

switch_to的理解

http://home.ustc.edu.cn/~hchunhui/linux_sched.html#sec9  郭海林 同学的ppt

主调度器执行的时机

http://www.linuxdiyf.com/linux/201107/648.html 

进程的状态切换

http://www.ibm.com/developerworks/linux/library/l-task-killable/ 

虚拟运行时间部分

http://blog.csdn.net/ustc_dylan/article/details/4140245 

组调度

http://book.51cto.com/art/201005/200918.htm 

 

题外话

此文对linux内核的理解或者表述很有可能出现多处谬误,希望您在读的时候保持怀疑的态度。如果发现错误了希望您能帮我指出,免得此文误导更多人,谢谢!

 注意:文中出现的名字“队列”仅仅是一个普通中文名词,表示具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue.

本文章也是“吃百家饭”而成的,可以任意转载,不要求注明原作者,但是你得保证我在更新了文章中的错误时,你也能够及时更新你转载的副本,否则错误会蔓延下去。 

目录

Linux2.6.24内核,调度器章节的笔记... 1

1.      调度器相关的功能结构概述... 2

1.1.         运行(Run Queue)队列... 2

1.2.         核心调度器... 3

1.1.1.        主调度器... 3

1.1.2.        周期性调度器... 4

1.3.         调度器类... 5

2.      就绪进程在队列中如何排序的... 7

2.1.         实时进程... 7

2.2.         普通进程... 8

2.2.1.        模型建立... 8

2.2.2.        抽象模型的总结... 17

2.2.3.        真实模型概述... 18

2.2.4.        对应关系... 21

2.2.5.        真实模型详述... 22

3.      进程优先权... 30

4.      核心调度器... 34

4.1.         周期性调度器... 36

4.2.         主调度器... 42

4.2.1.        代码块143

4.2.2.        代码块243

4.2.3.        代码块343

4.2.4.        代码块445

4.2.5.        总结... 45

4.2.6.        switch_to47

5.      进程的状态切换... 54

 

1.      调度器相关的功能结构概述

在学习调度器前,先从整体上做一个了解,对整体构成有一个清晰的认识,然后在深入细节,才不会像我一样在学习的过程中迷失在细节里。

1.1. 运行(Run Queue)队列

原理概述

linux内核用结构体rqstruct rq)将处于就绪(ready)状态的进程组织在一起。

rq结构体包含cfsrt成员,分别表示两个就绪队列:cfs就绪队列用于组织就绪的普通进程(这个队列上的进程用完全公平调度器进行调度)rt就绪队列用于用于组织就绪的实时进程(该队列上的进程用实时调度器调度)

在多核系统中,每个CPU对应一个rq结构体。

Figure.就绪队列简要示意图


细节

cfs队列实际上是用红黑树组织的,rt队列是用链表组织的。

在这里只需要知道如下性质就行了:红黑树是一种二叉搜索树,大小关系是:左孩子<父节点<右边,即最左边的节点的值最小(如果对该内容感兴趣可以参考数据结构和算法分析相关书籍)。

有几个CPU就会有几个rq结构体,所有的结构体保存在 一个数组中(即runqueues)

1.2. 核心调度器

原理概述

进程调度与两个调度函数有关:scheduler_tick()schedule(),这两者分别被称作周期性调度器(或周期性调度函数)主调度器(或主调度函数)。两者合在一起被称作通用调度器(或者核心调度器)(这里一定要分清几个名词分别代表什么意思,不要像我第一次读到书中这个部分时一样,搞定一头雾水,完全不知所云)

1.1.1.     主调度器

在我们通常的概念中:调度器就负责将CPU使用权限从一个进程切换到另一个进程。完成这个工作的这其实就是Linux内核中所谓的主调度器


上图中,三种不同颜色的长条分别表示CPU分配给进程ABC的一小段执行时间,执行顺序是:A,B,C。竖直的虚线表示当前时间,也就是说;A已经在CPU上执行完CPU分配给它的时间,马上轮到B执行了。这时主调度器shedule就负责完成相关处理工作然后将CPU的使用权交给进程B

总之,主调度器的工作就是完成进程间的切换。

1.1.2.     周期性调度器

再来看看周期性调度器都干些什么吧。同样是刚才的那幅图,不过现在我们关注的不是从进程A切换到进程B这个过程,而是把ACPU上执行的过程放大后观察细节。

A享用它得到的CPU时间的过程中,系统会定时调用周期性调度器(即定时执行周期性调度函数scheduler_tick())


在此版本的内核中,这个周期为10ms(这个10ms是这样得来的:内中定义了一个宏变量:HZ=100,它表示每秒钟周期性调度器执行的次数,那么时间间隔t=1/HZ=1/100s=10ms10ms是个什么概念呢,我们粗略地计算一下:如果周期性调度程序每次执行100条指令,每秒执行100次,那么一秒钟周期性调度器在CPU上执行的指令就是1万条。如果主频为1GHz的处理器每秒钟执行10亿条指令,就相当于,周期性调度器消耗的CPU只占CPU总处理能力的 1/10亿=10万分之一,微乎其微)。为了方便理解,上图将A获得的时间段分成长度为10ms的小片(注意:只是为了方便讲解,假想成这样的,内核并没有做这样的划分)。

周期性调度器每10ms执行一次,那它都干了些什么呢?它只是更新了一些统计信息。例如:进程A的结构体的成员sum_exec_runtime记录了ACPU上运行的总时间,周期性调度器会更新该时间为:sum_exec_runtime+=10ms(这种说法不准确,细节信息后续内容会讲到)。

我第一次在书中看到周期性调度器的时候,就没法儿理解,它又不负责进程切换,怎么能称之为调度器呢,这未免也太误导读者了吧。要记住一点:它不负责进程切换。

细节

周期性调度器是用中断实现的:系统定时产生一个中断,然后在中断过程中执行scheduler_tick()函数,执行完毕后将CPU使用权限还给A(有可能不会还给A了,细节后续在讨论),下一个时间点到了,系统会再次产生中断,然后去执行scheduler_tick()函数。(中断过程对进程A是透明的,所以A是一个傻子,它以为自己连续享用了自己得到的CPU时间段,其实它中途被scheduler_tick()中断过很多次)

小结:

主调度器负责将CPU的使用权从一个进程切换到另一个进程。周期性调度器只是定时更新调度相关的统计信息。     

1.3. 调度器类

原理概述

内核中定义了很多用于处理不同类型进程(普通进程、实时进程、idle进程)的处理函数,例如:将普通进程放入就绪队列的函数:enqueue_task_fair(),将实时进程放入就绪队列的函数enqueue_task_rt()

调度器需要用到这些函数,如果需要将睡眠的进程重新放入就绪队列,会调用enque_task_XXX()函数。那么,调用哪一个?答:先判断该进程是什么类型的进程,如果是普通进程就调用enqueue_task_fair(),如果是实时进程就调用enqueue_task_rt()(如下图)

 

这固然可行,但是内核不是这样做的。Linux内核把这些用于处理普通进程的函数用结构体实例fair_sched_class(该结构体的成员全是指向函数的指针)组织起来,把用于处理实时进程的函数用结构体实例rt_sched_class组织起来,把用于处理idle进程的函数用idle_sched_class组织起来。

然后将普通进程关联到fair_sched_class(task_A->sched_class= fair_sched_class),实时进程关联到rt_sched_class,idle进程关联到idle_sched_class,那么需要用到相关函数的时候就不需要判断进程是什么类型的了,而是直接调用该进程关联的函数就行了(如:task_A->sched_class->enqueue_task(rq,p, wakeup, head);)。


 

2.      就绪进程在队列中如何排序的

linux内核调度器有了大概的了解,现在我们接着进入细节的学习。

首先看一看各个进程在就绪队列中究竟是怎样进行先后排序的(即,如何决定进程被调度的先后顺序)。

2.1. 实时进程

原理概述

所有就绪的的实时进程都被组织在rq结构体中的rt(struct rt_rq)就绪队列上,它的排序非常简单。

相同优先等级的实时进程被组织在同一个双向链表中。在本版本的内核中,实时进程的优先等级从[0~99],共100个等级,因此就有100个这样的链表。每个链表的表头记录在rt.active.queue中。

activert(struct rt_rq)的一个成员,是struct rt_prio_array类型的:

kernel/sched.c

struct rt_rq {

    struct rt_prio_array active;

...

};

struct rt_prio_array的定义如下:

kernel/sched.c

struct rt_prio_array {

    DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit fordelimiter */

    struct list_head queue[MAX_RT_PRIO];

};

它包含两个成员,一个数组queue,用来存放各个优先级的实时进程的链表头。另一个是一个位图。位图中每一位对应一个实时进程的链表,如果优先级为5的链表上没有进程,那么为图中第5位就为0,反之则为1。如下图:


调度器选择实时进程进程执行时,先在优先级高的链表上查找,如果没有再依次找优先级低的。在同一个优先级中,进程被调度的先后顺序就是它在链表中的先后顺序。

2.2. 普通进程

原理概述

所有就绪的普通进程被组织在rq结构体中的cfs(struct cfs_rq)就绪队列上。这里为了方便把它称作"队列",它实际上是用红黑树进行组织的,红黑树是一种二叉搜索树:左孩子的值小于父节点,右孩子的值大于父节点,即最小的会出现在红黑树最左边。如下图所示:


红黑树的具体细节不做讨论,只需要记住排在最左边进程最先执行就行了。接下来的讨论中我们不关心它如何使使用红黑树进行组织的,只是简单地把它看做是一个具有先后顺序的队列就行了(再次提醒,这里的“队列”仅仅指具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue)。

这个版本的linux内核中用于决定就绪进程在就绪队列中先后顺序的机理很简单,也很巧妙。直接讲它怎么实现的,对于读者来说要听明白应该也是轻而易举的;但是对于表述能力如此差的我来说,要讲明白则太过于勉强了,所以我就从最简单的"模型"开始一步一步构建"真实模型"

 

提示这一部分内容对调度器的理解不是特别重要,在这里却占用了很多的篇幅,如果不感兴趣最好是直接跳过,直接从进程优先权看起,或者先看完后面的内容,再回头看这部分。

以下内容,为了讲解方便,可能运用了类似于时间片的讲述方法,可能会让读者误以为该版本linux内核采用了时间片的管理方式。因此,需要特别声明一下,早期的内核中有时间片的概念,但是该版本的内核已经没有时间片的概念了。

细节

2.2.1.     模型建立

A.   runtime公平模型

原理概述

这个模型的主要目的是要让各个进程尽可能的公平享用CPU时间。其机理如下:

CPU的总时间按就绪的进程数目等分给每个进程,每个进程在就绪队列中的先后顺序由它已享用的CPU时间(runtime)决定,已享用CPU时间短的排在队列的排前面,反之则排在队列后面。

为了好好阐述这个模型是怎么运作的,我们模拟一下进程被调度的过程:调度器每次在所有可运行的进程中挑一个runtime值最小的,让它运行4ms(进程也可以在这4ms还没用完的时候中途释放CPU),刚刚被选中的进程运行完4ms后,调度器再重新挑一个runtime值最小的。规定每次最多执行4ms是为了防止某个进程一直占用CPU不释放而提出的。记住我们这个模型的机理,下面根据一个例子看看具体调度流程什么怎样的:

a)初始的时候,三个进程都没有运行因此,runtime都等于0

进程

A

B

C

runtime(ms)

0

0

0

按runtime进行排序

A B C

         

b)   由于runtime值都等于0,假设初始顺序是A B C。那么将A放到CPU上执行,它执行了4ms后:

进程

A

B

C

runtime(ms)

4

0

0

按runtime进行排序

B  C  A

c)    按进程排列的顺序,接下来该B执行,假设B只执行了2ms

进程

A

B

C

runtime(ms)

4

2

0

按runtime进行排序

C  B  A

d)    假设B执行了2ms的时候产生特殊事件激活了调度器,调度器选择下一个进程执行。进程Cruntime最小,因此选择C

C执行了4ms

进程

A

B

C

runtime(ms)

4

2

4

按runtime进行排序

B  A  C

假设执行情况是这样的:进程在执行的时候需要从就绪队列中取下来,执行完毕后再放入就绪队列中(真实情况差不多上也是这样的,只是有一点小小的不同)。这就可以解释,为什么CA有相同的runtime值,而C却排在A后面了。

e)       进程Bruntime值最小,接下来轮到进程B执行,B执行了4ms

进程

A

B

C

runtime(ms)

4

6

4

按runtime进行排序

A C B

这就是最简单的模型:CPU总是挑已经执行时间最短的那个进程到CPU上运行。最简单的模型往往有很大的漏洞,我们接着往下看。

B.    min_runtime公平模型。

原理概述

这个模型的主要目的是要让各个进程尽可能的公平享用CPU时间。其机理如下:

CPU的总时间按就绪的进程数目等分给每个进程,每个进程在就绪队列中的先后顺序由它已享用的CPU时间(runtime)决定,已享用CPU时间短的排在队列的排前面,反之则排在队列后面。

为了好好阐述这个模型是怎么运作的,我们模拟一下进程被调度的过程:调度器每次在所有可运行的进程中挑一个runtime值最小的,让它运行4ms(进程也可以在这4ms还没用完的时候中途释放CPU),刚刚被选中的进程运行完4ms后,调度器再重新挑一个runtime值最小的。规定每次最多执行4ms是为了防止某个进程一直占用CPU不释放而提出的。记住我们这个模型的机理,下面根据一个例子看看具体调度流程什么怎样的:


a)初始的时候,三个进程都没有运行因此,runtime都等于0

进程

A

B

C

runtime(ms)

0

0

0

按runtime进行排序

A B C

         

b)   由于runtime值都等于0,假设初始顺序是A B C。那么将A放到CPU上执行,它执行了4ms后:

进程

A

B

C

runtime(ms)

4

0

0

按runtime进行排序

B  C  A

c)    按进程排列的顺序,接下来该B执行,假设B只执行了2ms

进程

A

B

C

runtime(ms)

4

2

0

按runtime进行排序

C  B  A

d)    假设B执行了2ms的时候产生特殊事件激活了调度器,调度器选择下一个进程执行。进程Cruntime最小,因此选择C

C执行了4ms

进程

A

B

C

runtime(ms)

4

2

4

按runtime进行排序

B  A  C

假设执行情况是这样的:进程在执行的时候需要从就绪队列中取下来,执行完毕后再放入就绪队列中(真实情况差不多上也是这样的,只是有一点小小的不同)。这就可以解释,为什么CA有相同的runtime值,而C却排在A后面了。

e)       进程Bruntime值最小,接下来轮到进程B执行,B执行了4ms

进程

A

B

C

runtime(ms)

4

6

4

按runtime进行排序

A C B

这就是最简单的模型:CPU总是挑已经执行时间最短的那个进程到CPU上运行。最简单的模型往往有很大的漏洞,我们接着往下看。

B.    min_runtime公平模型。

刚刚讨论的runtime有一个致命的缺陷:

假设系统中只有A,B,C三个进程,并且这三个进程都各自运行了1000ms,那么他们的vruntime为:

进程

A

B

C

runtime(ms)

100

150

200

此时,进程D被创建了,它没有享用过CPU时间,因此它的runtime=0。这就产生大问题了,在接下来的100ms内,调度器总是会让进程DCPU上执行,直到它运行了100ms以上。

如果你觉得这不是问题的话,你考虑一下这个情况:在已经运行了三年的服务器上,进程A,B,Cruntime都等于1年,这时进程D被创建了,那么接下来的一年内,就只有进程D在运行,其他的进程就慢慢等吧!

细节

谈到这里,我们就简单讨论一下什么叫“公平”吧。

假设进程A,B,C是同时创建并加入就绪队列的。

我的理解是:“操作系统应该在‘当前’将时间公平分配给‘当前’系统中的每个进程”。“当前”意味着什么:

a)    进程A,B,C在系统中共同经历了100+150+200=450ms,它们应该公平享用这段时间,即,每个进程应当执行150ms

b)   D进程被创建了,那么从现在起操作系统应该将CPU时间公平分配给这四个进程。

也就是说,从每个进程创建之时起,它就应该受到“不计历史”的公平待遇——无论其他进程之前运行了多长时间,当前的所有进程都应当一视同仁。

在这种情况下就很好办,如果A,B,Cruntime都等于150ms,那么D被创建的时候,也将它的runtime设置成150ms即可,这样在接下来的一段时间内,这四个进程会基本上公平地享用CPU时间。

然而,这只是我们的理想状态,因为第一点 a) 已经被打破了:A,B,C在过去的450ms内已经没有公平享用这段时间了。我们总不能等到A,B也执行了150ms之后才允许进程D被创建吧。

那么,我们要将Druntime设置成多少才能:1.使得从现在起A,B,C,D进程尽可能地受到公平待遇。2.还要体现出进程A,B在前450ms内受到了不公平待遇。

如果将Druntime设置为A,B,Cruntime最大的值显然对进程D不公,如果设置为他们中的最小值,那又对进程A不公。我们姑且先将进程Druntime设置为A,B,C中最小的那个值(即100ms),这个处理方法显然达不到我们的目的,不过,总比将D->runtime设置成0要好多了。(具体怎么处理的,我们留到后面再讲)

这就是我们在runtime公平模型上改进之后得到的min_runtime公平模型。

C.    weight优先级模型

原理概述

刚刚讨论了“公平”的问题,然而,在真实系统中,不同的进程具有不同的重要性。重要的进程我们应该尽量多分配CPU时间,不重要的进程应该少分配CPU时间。

为了达到这个目的,我们引入一个权重(weight)参数。即,每个进程有一个权重值,进程得到的CPU时间和这个权重值成正比

基于刚才的min_runtime公平模型,我们怎样才能引入weight这个参数呢?

刚才的条件不变:

a)    调度器总是选择runtime值最小的进程放到CPU上执行。

b)   每次执行不得超过4ms,执行完4ms后或者因为某些事件中途激活了调度器,再选一个runtime值最小的(如果当前进程的runtime值最小,就还会选中它执行)。

c)    新创建的进程的runtime设置为当前可运行进程中最小的runtime值。

分析

假设进程A,B的权重分别是12,怎样才能使得在一段时间内进程B执行的时间是进程A2倍呢?

在公平模型中我们用runtime来对进程进行排序,runtime小的排在前面:当进程A,B都分别执行了12msruntime都等于12)时,如果轮到进程B运行,它最多再运行4msBruntime变成了16),调度器就会将CPU时间切换给进程A

很容易发现,如果不在runtime上下功夫的话,进程B最多比进程A多执行4ms(除非进程A是个瞌睡虫,经常睡眠;别试图用催眠进程A的方法来解决该问题,这代价太大了。即便代价很小,那进程A醒来之后又总会排在进程B前面)。

换一个角度考虑,我们就很容易发现解决该问题的方法。我们想要达到这样一个效果:A进程执行了N ms后以及B进程执行了2N ms后他们应当具有相同的先后顺序,即他们的runtime值基本相同。

我们用runtime表示进程已经运行的时间,显然和以上表述矛盾,因此我们用另一个参数vruntime(虚拟运行时间)代替它。

我们检验一下是否可行:假设进程A每执行1ms,它的vruntime值就增加1。进程B2ms,它的vruntime才增加1(vruntime仅仅是一个数值,用来作为对进程进行排序的参考,不用来反映进程真实执行时间,别像我一样看书看到这一部分时一样陷入理解误区)

我们仍然用之前的过程模拟一下:


 

a)     初始的时候两个个进程都没有运行,runtime都等于0

进程

A

B

weight

1

  • 17
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
编译Linux 2.6.24内核有以下几个步骤: 1. 下载内核源代码:首先,你需要下载Linux 2.6.24内核源代码。你可以从Linux内核官方网站或者其他镜像网站上找到合适的源代码包。下载完成后,解压源代码到合适的目录。 2. 配置编译选项:进入源代码目录后,你需要进行内核的配置。可以使用make menuconfig或make xconfig等命令来进行交互的配置。可以根据自己的需求选择合适的配置选项,例如硬件支持、文件系统等。 3. 运行编译命令:配置完成后,运行make命令进行编译。这个过程可能会需要一定时间,因为编译内核需要编译源代码大量的文件。你可以使用make -j <n>命令来指定同时编译的进程数量,其<n>为你机上的CPU核心数。 4. 安装内核:编译完成后,你可以使用make install命令将编译好的内核安装到系统。这个过程会将内核映像文件复制到/boot目录下,并更新引导加载程序的配置文件。 5. 配置引导加载程序:安装完内核后,你需要配置引导加载程序以便系统能够启动新编译的内核。具体配置方式会因使用的引导加载程序而有所不同,例如GRUB、LILO等。 6. 重启系统:完成上述步骤后,现在你可以重启系统,选择新编译的内核进行启动。在系统启动时,可以通过/boot/grub/menu.lst或/boot/grub/grub.conf等文件来查看引导加载程序的配置。 希望以上信息对你有所帮助,在进行内核编译时请确保备份重要数据并仔细阅读相关文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值