_IOW的意思

本文深入探讨Linux驱动程序的接口及其实现细节,包括系统调用、设备驱动程序的四层接口、file_operations数据结构、PCI字符设备驱动程序设计、设备启动与检测、中断处理、特殊控制函数ioctl()的使用、调用Linux内核函数等关键概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

§1. Linux驱动程序接口

系统调用是操作系统内核与应用程序之间的接口,设备驱动程序则是操作系统内核与机器硬件的接口。几乎所有的系统操作最终映射到物理设备,除了CPU、内存 和少数其它设备,所有的设备控制操作都由该设备特殊的可执行代码实现,此代码就是设备驱动程序。操作系统内核需要访问两类主要设备:字符设备和块设备。与 此相关主要有两类设备驱动程序,字符设备驱动程序和块设备驱动程序。Linux(也是所有UNIX)的基本原理之一是:系统试图使它对所有各类设备的输 入、输出看起来就好象对普通文件的输入、输出一样。设备驱动程序本身具有文件的外部特征,它们都能使用象
open(),close(),read(),write()等系统调用。为使设备的存取能象文件一样处理,所有设备在目录中应有对应的文件名称,才可使用有关系统调用。
通常Linux驱动程序接口分为如下四层:
1).应用程序进程与内核的接口;
2).内核与文件系统的接口;
3).文件系统与设备驱动程序的接口;
4).设备驱动程序与硬件设备的接口。

§2. 驱动程序文件操作数据结构

每个驱动程序都有一个file-operation的数据结构,包含指向驱动程序内部函数的指针。file-operation的数据结构为:
struct file-operation {
int (*lseek)();
int (*read)();
int (*write)();
int (*readdir)();
int (*select)();
int (*ioctl)();
int (*mmap)();
int (*open)();
int (*close)();
int (*release)();
int (*fsync)();
int (*fasync)();
int (*check-media-change)();
int (*revalidate)();
}
内核中有两个表,一个用于字符设备驱动程序,一个用于块设备驱动程序。这两个表用于保存指向file-operation数据结构的指针,驱动程序内部函 数的地址保存在这一结构。内核用主设备号作为索引访问file-operation结构,可以访问驱动程序子程序地址。SBS617设备采用了PCI总线 字符设备的驱动程序实现方式。完成了设备驱动程序,经GNU软件编译,链接,产生一可加载模块,可以用于动态装入Linux操作系统内核,也可以在需要时 从内核中卸除。

§3. file_operations介绍
在结构file_operations里,指出了设备驱动程序所提供的入口点位置,分别是:
(1) lseek,移动文件指针的位置,显然只能用于可以随机存取的设备。
(2) read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误,否则返回实际读取的字节 数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode->i_blksize的的倍数。
(3) write,进行写操作,与read类似。
(4) readdir,取得下一个目录入口点,只有与文件系统相关的设备驱动程序才使用。
(5) selec,进行选择操作,如果驱动程序没有提供select入口,select操作将会认为设备已经准备好进行任何的I/O操作。
(6) ioctl,驱动程序特殊控制入口点,进行读、写以外的其它操作,参数cmd为自定义的命令。 这是很有意思的部分,之后我会详尽介绍;
(7) mmap,用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。
(8) open,打开设备准备进行I/O操作。返回0表示打开成功,返回负数表
示失败。如果驱动程序没有提供open入口,则只要/dev/driver文件存
在就认为打开成功。
(9) release,即close操作。
设备驱动程序所提供的入口点,在设备驱动程序初始化的时候向系统进行登记,以便系统在适当的时候调用。

§4 PCI字符设备驱动程序

要设计PCI设备驱动程序,必须进一步结合硬件设备和PCI总线的特性。设计PCI设备驱动程序的重要任务是找寻相应的硬件并实现对它的访问。作为外围设 备的硬件必须响应三种地址空间的访问,即内存,IO,寄存器地址空间。前两种地址空间可以为PCI总线上的所有设备共享。寄存器空间占用物理地址,可以通 过特殊的函数来访问配置寄存器。一旦可以访问配置寄存器,设备驱动程序就可以访问硬件了。每个设备的PCI配置寄存器均由256Bytes构成,其中 64Bytes是标准化的,4Bytes标识了一个唯一的函数ID,通过这个ID驱动程序就可以定位该设备。
存取系统中的字符设备和存取系统文件一样。应用程序使用标准的系统调用来打开、读写和关闭设备,就像使用一个文件-样。当字符设备初始化时,通过向 chrdevs数组中添加一个入口,设备驱动程序在系统内核中注册。 chrdevs数组由device_struct数据结构组成。设备的主设备号用来作为此chrdevs的索引,因为一个设备的主设备号是固定的。

LINUX系统里,通过调用register_chrdev向系统注册字符型设备驱动程序。register_chrdev定义为:
#include linux/fs.h
#include linux/errno.h
int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops);
其中,major是为设备驱动程序向系统申请的主设备号,如果为0则系统为此驱动程序动态地分配一个主设备号。name是设备名。fops就是前面所说的 对各个调用的入口点的说明。此函数返回0表示成功。返回-EINVAL表示申请的主设备号非法,一般来说是主设备号大于系统所允许的最大设备号。返回- EBUSY表示所申请的主设备号正在被其它设备驱动程序使用。如果是动态分配主设备号成功,此函数将返回所分配的主设备号。

§5 PCI设备启动与检测

PC主板BIOS在系统启动时,可以自动检测PCI设备并配置设备的每一地址区。当驱动程序访问设备时,它的内存、I/O地址空间已经映射到进程的地址空 间了。在驱动程序init_module()中,通过调用函数pcibios_find_device()函数返回设备在总线上的位置及函数指针,其中的 包含文件及函数原型为:

#include Linux/pci.h
#include Linux/config.h
#include Linux/bios32.h
int pcibios_find_device(unsigned short vendor,
unsigned short id, unsigned short index,
unsigned char *bus, unsigned short *function)

§6 地址空间访问

在设备驱动程序检测到设备之后,通常要从三个地址空间读写数据,其中寄存器空间的读写尤为重要,因为只有通过它驱动程序才可能找到设备内存和I/O空间的映射地址。设备驱动程序通过调用以下函数实现寄存器空间的访问,其中的包含文件及函数原型为:

#include Linux/bios32.h
int pcibios_read_config_byte( unsigned char bus,
unsigned char function,
unsigned char where,
unsigned char b*ptr)

int pcibios_write_config_byte(unsigned char bus,
unsigned char function,
unsigned char where,
unsigned char b*ptr)

类似的还有:
pcibios_read_config_word (), pcibios_write_config_word () ,
pcibios_read_config_dword () ,pcibios_write_config_dword() 调用。
PCI设备最多有6个地址区,类型可以为内存区或I/O区。接口板可以通过配置寄存器的PCI_BASE_ADDRESS_0 到PCI_BASE_ADDRESS_5来报告各地址区的实际地址位置。内存、IO空间的访问通过inb(),memcpy()等调用。当然可以通过 pcibios_read_config_byte(),pcibios_write_config_byte() 来访问配置寄存器的相应基地址值。

§7 中断处理


对中断的处理是属于系统核心的部分, PC主板BIOS为多数设备分配了一个唯一的中断号,在配置寄存器中保存, 设备驱动程序通过pcibios_read_config_byte() 函数读取相应的值,格式为:

xxx_irq=pcibios_read_config_byte(pci_bus,pci_device_fn,
PCI_INTERRUPT_LINE,
&pci_cofig->int_line)

操作系统中有中断寄存器,将特定的中断请求与中断处理函数联系在一起,当中断发生时调用相应的中断处理函数处理。Linux操作系统下可用request_irq(),free_irq( )实现中断的请求,释放,其中包含文件及形式为:

#include Linux/sched.h
int request_irq(unsigned int irq,
void (*handler)(int irq,void dev_id,struct pt_regs *regs),
unsigned long flags,
const char *device,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
参数irq表示所要申请的硬件中断号。handler为向系统登记的中断处理子程序,中断产生时由系统来调用,调用时所带参数irq为中断号, dev_id为申请时告诉系统的设备标识,regs为中断发生时寄存器内容。device为设备名,将会出现在/proc/interrupts文件里。
flag是申请时的选项,它决定中断处理 程序的一些特性,有两种方式写中断方式设备驱动程序:即快中断方式和定时等待方式。采取快中断方式需要将request_irq()的第三个type类型 参数设为SA_INTERRUPT。 正常中断与快中断的区别在于: 从正常中断返回时,内核可以利用机会调度更优先的进程执行; 而快中断不进行调度立即恢复被中断程序的执行.
在LINUX系统中,中断可以被不同的中断处理程序共享,这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,这些处理程 序之间以dev_id来区分。如果中断由某个处理程序独占,则dev_id 可以为NULL。request_irq返回0表示成功,返回-INVAL表示irq>15或 handler==NULL,返回-EBUSY表示中断已经被占用且不能共享。

中断处理函数形式为:

void xxx_irq_handler(int xxx_irq,void *dev_id,
struct pt_regs *regs)

§8 特殊控制函数ioctl()

ioctl()具有设备特殊性,不同于read() , write(),在于它允许应用程序访问、配置设备,并进入可能的操作模式。 通常的read()、write()不能使用这些控制操作。ioctl()可以控制I/O通道。设备驱动的一个特点是要与其它设备硬件交换读/写的数据并 需要同步控制。
多数的ioctl()由一系列的switch语句组成, ioctl()命令及操作选择考虑到硬件的特性和实际要实现的功能。写ioctl()程序之前,应选择相应的命令,不应该简单使用1-N的数字。选择ioctl()的命令有以下的考虑:
·首先命令码在系统中应该唯一,以避免与其它设备冲突,每个命令码应由多个比特域构成。
· 参考两个文件来帮助选择ioctl()的命令,include/asm/ioctl.h及Documentation/ioctl_number.txt 有如下定义:
命令码有四个8比特组,其相应取值的宏定义及含义如下表:

命令码取值宏定义及含义
比特组名称 取值宏定义 含义
type _IOC_TYEBITS 表示每个驱动程序唯一的类型标识
number _IOC_NRBITS 表示序列号
direction _IOC_NONE, _IOC_READ,
_IOC_WRITE,_IOC_READ|WRITE 表示数据传输的方向
size _IOC_SIZEBITS 表示传输数据的大小

在头文件< asm/ioctl.h >中定义了设置命令码的一些有用的宏:
_IO(type,nr);
_IOR(type,nr,size);
_IOW(type,nr,size);
_IOC_DIR(nr);
_IOC_TYPE(nr);
_IOC_NR(nr);
_IOC_SIZE(nr);

这些设置与具体的硬件功能有关,可以参考有关的硬件手册。通过以上方式可以设置命令、获得设备参数及实现控制操作, 完成设备驱动程序的重要功能。
对于设备驱动程序,ioctl() 函数非常重要,用户可以通过它来控制设备函数,获取状态信息,进行数据的读写。
ioctl()在用户空间的形式为:

int (*ioctl)(struct inode *inode , struct file *file ,
unsigned int cmd , unsigned long arg)

其中cmd相当于一个选择码,取决于使用的特殊控制命令,cmd命令通常在头文件中声明。直接的调用的格式为:

temp = ioctl( fd, XX_xxxx, param* );

\"fd\"是设备文件句柄。XX_xxxx 是控制码。Param是一个参数结构的指针,当调用 ioctl()时,需要理解一些特殊参数结构 ,可以参考下面的四个表格。返回值0表示成功,-1失败。

§9.调用Linux内核函数

Linux有许多内核函数可以调用。例如;

1)memcpy_fromfs( *toptr, *fromptr, sizeof());
// 用于从文件系统传输数据
2) memcpy_tofs ( *toptr, *fromptr, sizeof());
// 用于将数据传输到文件系统
#include asm/segment.h
void memcpy_fromfs(void * toptr,const void * fromptr,unsigned long n);
void memcpy_tofs(void * toptr,const void * fromptr,unsigned long n);
在用户程序调用read 、write时,因为进程的运行状态由用户态变为核心 态,地址空间也变为核心地址空间。而read、write中参数buf是指向用户程 序的私有地址空间的,所以不能直接访问,必须通过上述两个系统函数来访问用户程序的私有地址空间。memcpy_fromfs由用户程序地址空间往核心地 址空间复制,memcpy_tofs则反之。参数toptr为复制的目的指针,fromptr为源指针,n 为要复制的字节数。

3) ptr = vmalloc( sizeof() );// 动态分配内存
4) vfree( ptr ); // 动态释放内存
5) vremap( xxx_mapping[ chn ].pci_addr,
xxx_mapping[ chn ].len );
// 映射PCI地址,
chn = current_map_chn.

6)作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用malloc
和free,而调用kmalloc和kfree,定义为:
#include linux/kernel.h
void * kmalloc(unsigned int len, int priority);
void kfree(void * ptr);
参数len为希望申请的字节数,ptr为要释放的内存指针。priority为分配内存操作的优先级,即在没有足够空闲内存时如何操作,一般用GFP_KERNEL。
7)与中断和内存不同,使用一个没有申请的I/O端口不会使CPU产生异常, 也
就不会导致诸如\"segmentation fault\"一类的错误发生。任何进程都可以访问
任何一个I/O端口。此时系统无法保证对I/O端口的操作不会发生冲突,甚至会因此而使系统崩溃。因此,在使用I/O端口前,也应该检查此I/O端口是否已有 别的程序在使用,若没有,再把此端口标记为正在使用,在使用完以后释放它。
这样需要用到如下几个函数:
int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from, unsigned int extent,
const char *name);
void release_region(unsigned int from, unsigned int extent);
调用这些函数时的参数为:from表示所申请的I/O端口的起始地址;
extent为所要申请的从from开始的端口数;name为设备名,将会出现在
/proc/ioports文件里。check_region返回0表示I/O端口空闲,否则为正在
被使用。
在申请了I/O端口之后,就可以如下几个函数来访问I/O端口:
#include asm/io.h
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);
其中inb_p和outb_p插入了一定的延时以适应某些慢的I/O端口。
9)在设备驱动程序里,一般都需要用到计时机制。在LINUX系统中,时钟是
由系统接管,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:
#include asm/param.h
#include linux/timer.h
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
inline void init_timer(struct timer_list * timer);
struct timer_list的定义为:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long d);
};
其中expires是要执行function的时间。系统核心有一个全局变量JIFFIES
表示当前时间,一般在调用add_timer时jiffies=JIFFIES+num,表示在num个
系统最小时间间隔后执行function。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数HZ表示一秒内最小时间间隔的数目,则num*HZ 表示num 秒。系统计时到预定时间就调用function,并把此子程序从定时队列里删除,因此如果想要每隔一定时间间隔执行一次的话,就必须在function里 再一次调用add_timer。function的参数d即为timer里面的data项。
10)在设备驱动程序里,还可能会用到如下的一些系统函数:
#include asm/system.h
#define cli() __asm__ __volatile__ (\"cli\"::)
#define sti() __asm__ __volatile__ (\"sti\"::)
这两个函数负责打开和关闭中断允许。
11)在设备驱动程序里,可以调用printk来打印一些调试信息,用法与printf 类似。
printk打印的信息不仅出现在屏幕上,同时还记录在文件syslog里。

 

<think>好的,我现在要详细解释Linux内核中的_IOW宏,包括其构造、用法和示例。首先,我需要回顾用户的问题,用户之前询问了关于ioctl的实现,现在想深入了解_IOW宏的具体细节。用户还提供了两个引用,但看起来主要与/bin目录下的命令和slab分配器有关,可能与当前问题关联不大,但可能需要留意是否有间接联系。 首先,我需要确认_IOW宏的作用。_IOW用于生成ioctl命令码,属于Linux内核中设备驱动开发的一部分。用户之前的问题涉及ioctl的实现,所以需要保持连贯性,确保现在的回答补充之前的解释。 接下来,我需要拆解_IOW宏的构造。根据之前的回答,命令码的组成结构是类型、序号和方向。_IOW宏属于_IOC宏家族,用于定义有数据写入的ioctl命令。需要详细说明每个参数的意义,比如类型(type)、序号(nr)和数据类型(size)。这里需要确认参数的具体含义,例如size是否指数据类型的大小,或者用户空间传递的数据结构的大小。 然后,用法部分需要说明在驱动程序中如何定义和使用这些命令码。例如,使用_IOW('k', 2, int)来定义一个设置配置的命令。同时,用户空间如何调用ioctl时使用相同的命令码。需要强调数据传递的方向,即从用户空间到内核空间,因此需要使用copy_from_user函数。 示例部分需要结合代码,展示宏的定义、驱动中的处理以及用户空间的调用。要确保代码示例正确,包括错误检查,比如copy_from_user的返回值处理,以及权限检查(如capable(CAP_SYS_ADMIN))。 此外,需要提到兼容性问题,比如64位内核处理32位应用时可能需要compat_ioctl,但这是扩展内容,用户可能不需要深入,但可以简要提及。 还要注意安全性,如用户空间指针的有效性检查和权限验证,这在之前的回答中已经提到,需要再次强调。 可能会遇到的问题包括命令码冲突,因此需要解释如何选择唯一的类型字符和序号。另外,数据类型的大小变化可能导致问题,例如如果传递结构体,sizeof需要正确。 最后,确保回答结构清晰,分点说明构造、用法、示例,并附上相关问题。需要参考用户之前的提问,确保信息连贯,不重复之前的内容,而是深入细节。 现在需要验证_IOW宏的具体定义。根据内核源码,_IOW(type,nr,size)宏的定义应该是将方向设置为_IOC_WRITE,然后组合类型、序号和大小到命令码中。命令码的构造公式为: $$ \text{命令码} = (\text{类型} \ll 8) | (\text{序号} \ll 0) | (\text{方向} \ll 30) $$ 方向由_IOC_READ或_IOC_WRITE决定,这里_IOW对应_IOC_WRITE。类型是一个8位的字符,序号是8位,方向是2位,因此组合后的命令码是32位整数。 在用法中,驱动需要处理对应的命令,比如MY_IOCTL_SET_CONFIG,使用copy_from_user从用户空间获取数据。用户空间调用ioctl时传递指向数据的指针。 可能还需要提到其他类似的宏,如_IOR、_IO、_IOWR,以帮助用户区分不同方向的数据传输。 总结回答的结构: 1. 宏的作用和定义 2. 构造分解(类型、序号、方向、大小) 3. 使用步骤(定义命令码、驱动处理、用户空间调用) 4. 示例代码(驱动和用户空间) 5. 注意事项(兼容性、安全性、命令码设计) 6. 相关问题 需要确保所有技术细节准确,比如参数顺序,宏的参数是否正确,以及错误处理是否恰当。同时,使用中文解释,保持语言口语化,但技术术语准确。</think>### Linux内核中`_IOW`宏详解 #### 一、宏定义与作用 `_IOW`是Linux内核提供的**ioctl命令构造宏**,用于生成具有**数据写入方向**(用户空间→内核空间)的控制命令码。其完整定义为: ```c #define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size))) ``` 该宏生成的命令码包含**四要素**: 1. **方向标识**:`_IOC_WRITE`(数据写入) 2. **设备类型**:ASCII字符(唯一性) 3. **命令序号**:8位整数(0-255) 4. **数据尺寸**:通过`sizeof()`自动推导数据类型大小[^1] #### 二、命令码结构解析 命令码通过位运算组合,其二进制结构为: $$ \text{命令码} = \underbrace{\text{方向}}_{2\text{bit}} \ll 30 \quad | \quad \underbrace{\text{类型}}_{8\text{bit}} \ll 8 \quad | \quad \underbrace{\text{序号}}_{8\text{bit}} \ll 0 \quad | \quad \underbrace{\text{数据尺寸}}_{14\text{bit}} \ll 16 $$ 例如,`_IOW('k', 2, int)`生成的命令码将包含: - 方向位:`0x40000000`(表示写入) - 类型值:`'k'`(ASCII码0x6B) - 序号值:2 - 数据尺寸:`sizeof(int)`(通常为4字节) #### 三、使用步骤与示例 ##### 1. 驱动端定义命令 ```c // 定义类型标识(必须唯一) #define MYDEV_TYPE 0x6B // 'k'的ASCII码 // 构造ioctl命令 #define MYDEV_SET_CONFIG _IOW(MYDEV_TYPE, 1, int) // 写入int型数据 #define MYDEV_SET_BUFFER _IOW(MYDEV_TYPE, 2, struct my_data) // 写入结构体 ``` ##### 2. 驱动处理逻辑 ```c static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { void __user *argp = (void __user *)arg; switch (cmd) { case MYDEV_SET_CONFIG: { int config_val; // 安全检查:权限验证 if (!capable(CAP_SYS_ADMIN)) return -EPERM; // 数据拷贝 if (copy_from_user(&config_val, argp, sizeof(config_val))) return -EFAULT; // 处理逻辑 device_config = config_val; break; } case MYDEV_SET_BUFFER: { struct my_data data; if (copy_from_user(&data, argp, sizeof(data))) return -EFAULT; // 处理数据结构 break; } default: return -ENOTTY; } return 0; } ``` ##### 3. 用户空间调用 ```c #include <sys/ioctl.h> int fd = open("/dev/mydev", O_RDWR); // 设置整型配置 int config = 42; ioctl(fd, MYDEV_SET_CONFIG, &config); // 设置结构体数据 struct my_data buf = {.id = 1, .value = 3.14}; ioctl(fd, MYDEV_SET_BUFFER, &buf); ``` #### 四、关键注意事项 1. **类型唯一性** 设备类型字符应通过`Documentation/ioctl/ioctl-number.txt`注册,避免冲突。例如: ``` 0x6B 'k' linux/mydev.h My Custom Device ``` 2. **数据尺寸自适应** 当使用结构体时,`_IOW`会自动计算数据大小: ```c struct my_data { int id; double value; }; #define MYDEV_CMD _IOW('k', 3, struct my_data) // 尺寸=16字节 ``` 3. **64/32位兼容** 对于跨架构支持,需实现`compat_ioctl`: ```c static long mydev_compat_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { // 处理32位用户空间指针转换 } ``` 4. **安全规范** - 必须验证用户指针:`access_ok(VERIFY_READ/WRITE)` - 敏感操作需权限检查:`capable(CAP_SYS_ADMIN)` #### 五、相关宏对比 | 宏名称 | 方向 | 典型应用场景 | |---------|--------------------|----------------------| | `_IO` | 无数据传输 | 简单控制指令 | | `_IOR` | 内核→用户空间 | 读取设备状态 | | `_IOW` | 用户空间→内核 | 配置设备参数 | | `_IOWR` | 双向传输 | 复杂数据交换 | --- ### 相关问题 1. 如何避免不同驱动间的ioctl命令码冲突? 2. 为什么`copy_from_user`必须配合`access_ok`检查使用? 3. 在ioctl命令中传递结构体指针需要注意哪些内存对齐问题? [^1]: Linux内核源码:`include/uapi/asm-generic/ioctl.h` [^2]: 《Linux设备驱动程序》第三章
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值