Linux内核设计与实现 摘录

第1章 Linux内核简介

用户界面是操作系统的外在表象,内核才是操作系统的内在核心。

内核有时候被称作是超级管理者或者是操作系统核心。

应用程序被称为通过系统调用在内核空间运行,而内核被称为运行于进程上下文中。

运行于内核空间,处于进程上下文,代表某个特定的进程执行。

运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。

运行于用户空间,执行用户进程。

Unix内核几乎毫无例外的都是一个不可分割的静态可执行块(文件)。

Linux是一个单内核。Linux内核运行在单独的内核地址空间 ,不过,Linux汲取了微内核的精华:其引以为豪的是模块化设计、抢占式内核、支持内核线程以及动态装载内核模块的能力。

 

第2章 从内核出发

安装内核源代码:

tar xvjf linux-x.y.z.tar.bz2

tar xvzf linux-x.y.z.tar.gz

Makeflie是Makefile内核的基础

make config

make  menuconfig

make xconfig

make gconfig

maek>../some_other_file

make>/dev/null

make modules install

内核编程时必须使用GNU C

内核代码中所用到的C语言扩展中让感兴趣的那部分:内联函数,内联汇编,分支声明

没有内存保护机制

不要轻易在内核中使用浮点数

容积小而固定的栈

同步和并发

可移植性的重要性

内核的确是一头独一无二的猛兽:没有内存保护,没有靠得住的libc,小的堆栈,庞大的源码树。Linux内核遵循它自己的游戏规则,以大人物的架势运行,运行足够长的时间后才停止,打破了惯以为常的习俗。

 

第3章 进程管理

进程是Unix操作系统最基本的抽象之一。

内核调度的对象是线程,而不是进程。

Linux系统的线程实现非常特别——它对线程和进程并不特别区分。对Linux而言,线程只不过是一种特殊的进程罢了。

进程是处于执行期的程序以及它所包含的资源的总称。

fork()和exit()

内核把进程存放在叫做任务队列的双向循环链表中。

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的。

内核通过一个惟一的进程标识符或PID来标识每个进程。

进程描述符中的state域描述了进程的当前状态。

内核经常需要调整某个进程的状态。这时最好使用set_task(task,state)函数。该函数将指定的进程设置为指定的状态。

进程上下文。进程家族树。

传统的fork()系统调用直接把所有的资源复制给新创建的进程。

Linux通过clone()系统调用实现fork()。

vfork()系统调用和fork()的功能相同,除了不拷贝父进程的页表项。

线程机制是现代编程技术中常用的一种抽象。

新建的进程和它的父线程就是流行的所谓线程。

内核经常需要在后台执行一些操作。这种任务可以通过内核线程完成——独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。

一般说来,进程的析构发生在它调用exit()之后,即可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回。

在调用了do_exita()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。当最终需要释放进程描述符时,release_task()会被调用。

如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则的话这些成为孤儿的进程就会在退出时永远处于僵死状态,白白的耗费内存。

 

第4章 进程调度

调度程序是内核的组成部分,它负责选择下一个要运行的进程。

多任务操作系统就是能同时并发地交互执行多个进程的操作系统。

多任务操作系统可以划分为两类:非抢占式多任务和抢占式多任务。

抢占和让步

策略决定调度程序在何时让什么进程运行。

进程可以被分为I/O消耗型和处理器消耗型。

调度算法中最基本的一类就是基于优先级的调度。

Linux内核提供了两组独立的优先级范围:第一种是nice值,第二个范围是实时优先级。

时间片是一个数值,它表明进程在被抢占前所能持续运行的时间。

进程并不是一定非要一次就用完它所有的时间片。

Linux系统是抢占式的。

Linux的调度程序定义于kernel/sched.c中。

调度程序中最基本的数据结构是运行队列。可执行队列定义于kernel/sched.c中。

每个运行队列都有两个优先级数组,一个活跃的和一个过期的。

每个优先级数组都要包含一个这样的位图成员,至少为每个优先级准备一位。

选定一个进程并切换到它去执行是通过schedule()函数实现的。

进程拥有一个初始的优先级,叫做nice值。

休眠(被阻塞)的进程处于一个特殊的不可执行状态。

负载平衡程序由kernel/sched.c中的函数load_balance()来实现。

上下文切换,也就是从一个可执行进程切换到另一个可执行进程,由定义在kernel/sched.c中的context_switch()函数负责处理。

Linux完整地支持内核抢占。

只要没有持有锁,内核就可以进行抢占。

Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR

sched_setscheduler()和sched_getscheduler()分别用于设置和获取进程的调度策略和实时优先级。

Linux调度程序提供强制的处理器绑定机制。

 

第5章 系统调用

 为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求。实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序恣意妄行,惹出大麻烦。

API、POSIX和C库

一般情况下,应用程序通过应用编程接口而不是直接通过系统调用来编程。

在Unix世界中,最流行的应用编程接口是基于POSIX标准的。

提供机制而不是策略。

系统调用通常通过函数进行调用。

每个系统调用被赋予一个系统调用号。

应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行系统调用了。

设计接口的时候要尽量为将来多做考虑。

系统调用必须仔细检查它们所有参数是否合法有效。

copy_to_user()和copy_from_user()都有可能引起阻塞。

内核在执行系统调用的时候处于进程上下文。

系统调用靠C库支持。

 

第6章 中断和中断处理程序

中断使得硬件得以与处理器进行通信。

中断本质上是一种特殊的电信号,由硬件设备发向处理器。处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由OS负责处理这些新到来的数据。

内核随时可能因为新到来的中断而被打断。

不同的对应的中断不同,而每个中断都通过一个惟一的数字标识。

这些中断值通常被称为中断请求线(IRQ)

异常与中断不同,它在产生时必须考虑与处理器时钟同步。实际上,异常也常常称为同步中断。

在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序或中断服务例程。

中断上下文,中断处理程序是上半部。

中断处理程序是管理硬件的驱动程序的组成部分 int request_irq(...)

卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。void free_irq(unsigned int irq,void *dev_id)

当执行一个中断处理程序或下半部时,内核处于中断上下文中。进程上下文是一种内核所处的操作模式。

中断处理系统在Linux中的实现是非常依赖于体系结构的,想必你对此不会感动特别惊讶。

Linux内核提供了一组接口用于操作机器上的中断状态。

 

第7章 下半部和推后执行的工作

下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。

以后仅仅用来强调不是马上而已,理解这一点相当重要。

和上半部分只能通过中断处理程序实现不同,下半部可以通过多种机制实现。

BH,任务队列,软中断。

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。

taskle是利用软中断实现的一种下半部机制。

一般单纯禁止下半部的处理是不够的。为了保证共享数据的安全,更常见的做法是先得到一个锁然后再禁止下半部的处理。

内核同步介绍:共享内存的应用程序必须特别留意保护共享资源,防止共享资源被并发访问。

 

第8章 内核同步介绍

所谓临界区就是访问和操作共享数据的代码段。多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行

避免并发和防止竞争条件被称为同步。

锁的使用是自愿的、非强制性的,它完全属于一种编程者自选的编程手段。

用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度。

在中断处理程序上能避免并发访问的安全代码称作中断安全码

预防死锁的规则:加锁的顺序是关键。防止发生饥饿。不要重复请求一个锁。越复杂的加锁方案有可以会造成死锁--设计应力求简单。

 

第9章 内核同步方法

原子操作可以保证指令以原子的方式执行——执行过程不被打断。

内核提供了两组原子操作接口——一组针对整数进行操作,另一组针对单独的位进行操作。

针对整数的原子操作只能对atomic_t类型的数据进行处理。

使用原子整型操作需要的声明都在<asm/atomic.h>文件中。

原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的。

自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环——旋转——等待锁重新可用。

警告:自旋锁是不可递归的。

自旋可以使用在中断处理程序中。

使用锁的时候一定要对症下药,要有针对性。要知道需要保护的是数据而不是代码。

既然不是对代码加锁,那就一定要用特定的锁来保护自己的共享数据。

spin_lock_init()用来初始化动态创建的自旋锁。

一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。有时把读/写锁叫做共享/排斥锁,或者并发/排斥锁。

Linux中的信号量是一种睡眠锁

信号量不同于自旋锁,它不地禁止内核抢占,所以持有信号量的代码可以被抢占。这意味着信号量不会对调度的等待时间带来负面影响。

信号量的实现是与体系结构相关的,具体实现定义在文件<asm/semaphore.h>中。

与自旋锁一样,信号量也有区分读-写访问的可能。与读-写自旋锁和普通自旋锁之间的关系差不多,读-写信号量也要比普通信号量更具优势。

BKL(大内核锁)是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过渡到细粒度加锁机制。

Seq锁提供了一种很简单的机制,用于读写共享数据。

可以通过preempt_disable()禁止内核抢占。

 

第10章 定时器和时间管理

 时间管理在内核中占有非常重要的地位。

系统定时器是一种可编程硬件芯片,它能以固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负责更新系统时间,还负责执行需要周期性运行的任务。

系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。

事实上内核必须在硬件的帮助下才能计算和管理时间。

系统定时器以某种频率自行触发或射中时钟中断,该频率可以通过编程预定,称作节拍率。

内核知道连续两次时钟中断的间隔时间。这个间隔时间就称为节拍,它等于节拍率分之一。

内核通过控制时钟中断维护实际时间。

系统定时器频率是通过静态预处理定义的,也就是HZ(赫兹)

编写内核代码时,不要认为HZ值是一个固定不变的值。

操作系统并非一定需要固定的时钟中断。

全局变量jiffies用来记录自系统启动以来产生的节拍的总数。

系统运行时间以秒为单位计算,就等于jiffies/HZ

jiffies变量总是无符号长整数,

如果改变内核中HZ的值会给用户空间中某些程序造成异常结果。

实时时钟(RTC)是用来持久存放系统时间的设备,即使系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。

update_one_process()函数的作用是更新进程时间。

当前实际时间(墙上时间)定义在文件kernel/timer.c中。

读写xtime变量需要使用xtime_lock锁,该锁不是普通自旋锁而是一个seqlock锁。

定时器——有时也称为动态定时器或内核定时器——是管理内核时间的基础

因为定时器与当前执行代码是异步的,因此就有可能存在潜在的竞争条件。

内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。

最简单的延迟方法是忙等待(或者说忙循环)

<linux/jiffies.h>中jiffies变量被标记为关键字volatile,关键字volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名来访问。

更理想的延迟执行方法是使用schedule_timeout()函数,该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。

 

第11章 内存管理

内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字,但是,内存管理单元通常以页为单位进行处理。

MMU(内存管理单元)以页大小为单位来管理系统中的页表。

内核用struct_page结构表示系统中的每个物理页,该结构位于<linux/mm.h>中,flag域用来存放页的状态。

Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。

所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>中。最核心的函数是: struct page * alloc_page(unsigned int gfp_mask,unsigned int order)

可以获得填充为0的页。

可以翻译页,有三个函数可用 分别为

void  _free_pages(struct page *page,unsigned int order)

void free_pages(unsigned long addr,unsigned int order)

void free_page(unsigned long addr)

kmalloc()函数一个简单的接口,用它可以获得以字节为单位的一块内核内存。

行为修饰符、区修饰符及类型。

kmalloc()的另外一端就是kfree()

vmalloc()函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址则无需连续。

分配和释放数据结构是所有内核中最普通的操作之一。

当必须创建一个映射而当前上下文又不能睡眠时,内核提供了临时映射

支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是惟一的。

 

第12章 虚拟文件系统

 虚拟文件子系统作为内核子系统,为用户空间程序提供了文件系统相关的接口。系统中所有文件系统不但依赖VFS共存,而且也依靠VFS系统协同工作。

VFS使得用户可以直接使用open(),read(),write()这样的系统调用而无需考虑具体文件系统和实际物理介质。

VFS提供了一个通用文件系统模型,该模型囊括了我们所能想到的文件系统的常用功能和行为。

文件,目录项,索引节点和安装点。

从本质上讲文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。

文件的相关信息,有时被称作为文件的元数据(也就是说,文件的相关数据),被存储在一个单独的数据结构中,该结构被称为索引节点,它其实是index node的缩写,不过近来术语 “inode”使用得更为普遍一些。

VFS其实采用的是面向对象的设计思路,使用一族数据结构来代表通用文件对象。

VFS中有四个主要的对象类型:超级块对象,索引节点对象,目录项对象,文件对象。

各种文件系统都必须实现超级块,该对象用于存储特定文件系统的信息,通常对于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块。

超级块对象由super_block结构体表示,定义在文件<linux/fs.h>。

索引节点对象包含了内核在操作文件或目录时需要的全部信息。索引节点对象由inode结构体表示,定义在文件<linux/fs.h>中。

目录项对象,VFS把目录当作文件对待,所以在路径/bin/vi中,bin和vi都属于文件。

目录项对象有三种有效状态:被使用,未被使用和负状态。

VFS的最后一个主要对象是文件对象。文件对象表示进程已打开的文件。如果我们站在用户空间来看待VFS,文件对象会首先进入我们的视野。

文件对象由file结构体表示,定义在文件<linux/fs.h>中。

文件对象的操作由file_operations结构体表示,定义在文件<linux/fs.h>中。

系统中的每一个进程都有自己的一组打开的文件,像根文件系统、当前工作目录、安装点等等。有三个数据结构将VFS层和系统的进程紧密联系在一起,它们分别是:files_struct、fs_struct和namespace结构体。

Linux支持了相当多种类的文件系统。从本地文件系统,如ext2和ext3,到网络文件系统,如NFS和Coda,Linux在标准内核中已支持的文件系统超过50种。

 

第13章 块I/O层

系统中能够随机访问固定大小数据片的设备被称作块设备,这些数据片就称作块。最常见的块设备就是硬盘。

另一种基本的设备类型是字符设备。

这两种类型的设备的根本区别在于它们是否可以被随机访问——换句话说,就是能否在访问设备时随意地从一个位置跳转到另一个位置。

块设备中最小的可寻址单元是扇区。

描述符用buffer_head结构体表示,被称作缓冲区头,在文件<linux/buffer_head.h>中。

目前内核中块I/O操作的基本容器由bio结构体表示,它定义在文件<linux/bio.h>中。

块设备将它们挂起的块I/O请求保存在请求队列中,该队列由request_queue结构体表示,定义在文件<linux/blkdev.h>中,包含一个双向请求链表以及相关控制信息。

为了优化寻址操作,内核既不会简单地按请求接收次序,也不会立即将其提交给磁盘。相反,它会在提交前,先执行名为合并与排序的预操作。

I/O调度程序将磁盘I/O资源分配给系统中所有挂起的块I/O请求。

 

第14章 进程地址空间

进程的地址空间,也就是系统中每个用户空间进程所看到的内存。

进程地址空间由每个进程中的线性地址区组成,而且更为重要的特点是内核允许进程使用该空间中的地址。

两个不同的进程可以在它们各自地址空间的相同地址存放不同的数据。

内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由mm_struct结构体表示,定义在文件<linux/sched.h>中。

在进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。

内核线程没有进程地址空间,也没有相关的内存描述符。

当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中的active_mm域会被重新,指向新的地址空间。

内存区域由vm_area_struct结构体描述,定义在文件<linux/mm.h>中,内存区域在内核中也经常被称做虚拟内存区域或VMA。

VMA标志是一种位标志,其定义见<linux/mm.h>。

内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确。

在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能。

虽然应用程序操作的对象是映射到物理内存之上的虚拟内存。

 

第15章 页高速缓存和页回写

页高速缓存是Linux内核实现的一种主要磁盘缓存。它用来减少对磁盘的I/O操作。

页高速缓存是由RAM中的物理页组成的,缓存中每一页都对应着磁盘中的多个块。每当内核开始执行一个页I/O操作时,首先会检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免访问磁盘。

页高速缓存缓存的是页面。

一个物理页可能由多个不连续的物理磁盘块组成。

Linux页高速缓存使用address_space结构体描述页高速缓存中的页面。该结构体定义在文件<linux/fs.h>中。

其中i_mmap字段是一个优先搜索树。

页高速缓存通过两个参数——address_space对象和一个偏移量——进行搜索。每个address_space对象都有惟一的基树,它保存在page_tree结构体中。

膝上型电脑模式是一种特殊的页回写策略,该策略主要意图是将硬盘转动的机械行为最小化,允许硬盘尽可能长时间的停滞。

pdflush线程的工作是分别由bdflush和kupdated两个线程共同完成。

 

第16章 模块

尽管Linux是“单块内核”的操作系统——也是说整个系统内核都运行于一个单独的保护域中。但是Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。

模块的所有初始化函数必须符合下面的形式: int my_init(void);

由于采用了新的"kbuild"构建系统,现在构建模块相比从前更加容易。

编译后的模块将被装入到目录/lib/modules/version/kernel/下。

Linux模块之间存在依赖性,也就是说钓鱼模块依赖于鱼饵模块,那么当载入钓鱼模块时,鱼饵模块会被自动载入。

载入模块最简单的方法是通过insmod命令。

所谓存在的Kconfig文件可能是drivers/char/Kconfig.

定义一个模块参数可通过宏module_param()完成。

模块被载入后,就会动态连接到了内核。

 

第17章 kobject和sysfs

统一设备模型,设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构。

设备模型的核心部分就是kobject,它由struct kobject结构体表示,定义于头文件<linux/kobject.h>中。

kobject对象被关联到一种特殊的类型,即ktype.

kset是kobject对象的集合体。把它看成是一个容器,可将所有相关的kobject对象置于同一位置。

subsystem在内核中代表高层概念,它是一个或多个ksets的大集合。

kobject的主要功能之一就是为我们提供了一个统一的引用计数功能。

sysfs文件系统中一个处于内存中的虚拟文件系统,它为我们提供了kobject对象层次结构的视图。

内核事件层实现了内核到用户的消息通知系统,就是建立在上文一直讨论的kobject基础之上。

 

第18章 调试

调试工作艰苦是内核级的开发区别于用户级开发的一个显著特点。

如果你没办法让bug重现出来,下面要讲的这些步骤就毫无意义。

内核提供的打印函数printk(),和C库提供的printf()函数功能几乎相同。

健壮性是printk()函数最容易让人们接受的一个物质。

printk()和printf()一个最主要的区别就是前者可以指定一个记录级别。

 内核将最重要的记录等KERN_EMERG定为“<0>”,将无关紧要的记录等级“KERN_DEBUG”定为“<7>”.

 怎样给你的调用的printk()赋记录等级完全取决于你自己。

在标准的Linux系统上,用户空间的守护进程klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将它们保存在系统日志文件中。

oops是内核告知用户有不幸发生的最常用的方式。

oops的产生有很多可能原因,其中包括内存访问越界或者非法的指令等。

ksymoops一般会随Linux发行提供。

托内核抢占的福,内核提供了一个原子操作计数器。

一些内核调用可以用来方便标记bug,提供断言并输出信息。

神奇系统请求键是另外一根救命稻草,该功能可以通过定义CONFIG_MAGIC_SYSRQ配置选项来启用。

可以使用标准的GNU调试器对正运行的内核进行查看。

gdb vmlinux /proc/kcore

gdb kgdb kdb

 

第19章 可移植性

Linux是一个可移植性非常好的操作系统,它广泛支持了许多不同体系结构的计算机。可移植性是指代码从一种体系结构移植到另外一种体系结构上的方便程度。

有些操作系统在设计时把可移植性作为头等大事之一。

能够由机器一次就完成处理的数据被称为字。

有些操作系统和处理器不把它们标准字长称作字,相反,出于历史原因和某种主页的命名习惯,它们用字来代表一些固定长度的数据类型。

不透明数据类型隐藏了它们内部格式或结构。在C语言中,它们就像黑盒一样。支持它们的语言不是很多。作为替代,开发者们利用typedef声明一个类型,把它叫做不透明类型。

另外一个不透明数据类型的例子是atomic_t。

C标准表示char类型可以带符号也可以不带符号,由具体的编译器、处理器或由它们两者共同决定到底char是带符号合适还是不带符号合适。

一些体系结构对对齐的要求非常严格。通常像基于RISC的系统,载入未对齐的数据会导致处理器陷入。

绝对不要假定时钟中断发生的频率,也就是每秒产生的jiffies数目。

 

第20章 补丁、开发和社区

subscribe linux-kernel your@mail.address to majordomo@vger.kernel.org

Linux命名规范:缩进,括号,每行代码的长度,命名规范,函数,注释,typedef,

  

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值