直接渲染管理器(Direct Rendering Manager, DRM)

这是一篇来至于wiki的译文,希望对学习DRM的同学有帮助。

目录

概述

软件架构

API

DRM-Master and DRM-Auth

图形执行管理器(Graphics Execution Manager)

Translation Table Maps

DMA Buffer共享和PRIME

KMS(Kernel Mode Setting)

KMS device model

原子显示(Atomic Display)

渲染节点(Render node)

Direct Rendering Manager(DRM)是一个Linux内核子系统,负责为显卡GPU提供软件驱动接口(译者注:早期是用来驱动GPU的,后期加入了KMS)。 DRM向用户空间导出API接口,用户空间程序可以通过API接口,将命令和数据发送给GPU,执行各类操作(诸如配置显示模式等)。 DRM最初是作为X Server Direct Rendering Infrastructure(DRI)的内核组件来开发的。但从那以后,其他的图形协议框架(例如Wayland)也使用了DRM。

用户空间程序可以使用DRM API发生命令,让GPU执行硬件3D渲染、视频解码和GPGPU计算。

概述

虽然Linux内核已经有了fbdev,用于管理图形适配器的framebuffer,但是这不能满足带有3D加速功能和GPU的现代视频硬件的需求。这些设备通常需要在自己的内存中设置和管理命令队列,以便将命令分发给GPU,并且还需要管理buffer的申请和释放。早前,用户空间程序(如X Server)直接管理这些资源,但是他们的软件行为表现得好像它们是上层唯一访问这些资源的程序。当多个上层程序在同一时间,按各自的方式设置和控制同一视频硬件资源时,会出现灾难性后果。

Direct Rendering Manager出现的目的,是为了允许多个程序协同地使用视频硬件资源。DRM以独占的方式访问GPU,并负责初始化和维护命令队列、内存和其他硬件资源。希望使用GPU的程序都得向DRM发送请求,DRM充当仲裁并避免可能的并发冲突问题。

多年来,DRM涵盖的功能范围不断扩大,涵盖了更多以前由用户空间程序处理的功能,如framebuffer管理、mode setting、内存共享和同步。这些扩展中的某些部分后来被取了特定的名称,比如Graphics Execution Manager(GEM)或kernel mode-setting(KMS),在它们的功能被明确界定前,术语的成分更大一些。但它们实际上都是内核DRM子系统的一部分。

在一台计算机中包含两个GPU(一个独立GPU和一个集成GPU)已成为一种趋势,这也引出了新的课题,例如需要在DRM层解决GPU切换。对照Nvidia Optimus 技术,DRM提供了GPU卸载功能(译者注:多GPU间切换功能),称为PRIME。

Without DRM
With DRM

软件架构

The Direct Rendering Manager在内核空间中,因此用户空间程序必须使用系统调用去请求其服务。然而,DRM并没有定义个性化的系统调用。相反,它遵循了Unix“一切都是文件”的原则,通过文件系统命名空间,在设备文件/dev目录下导出了GPU设备。每个被DRM检测到GPU都被称为DRM设备,并创建一个设备文件/dev/dri/cardX(其中X是一个序列号)与之对应。要访问GPU的用户空间程序必须打开此文件并通过ioctl调用与DRM通信,不同的ioctl对应了DRM不同的功能。

libdrm库方便了用户空间程序与DRM子系统的交互。这个库仅仅是一个wrapper,它为DRM的每个ioctl  API都实现了一个C 函数。libdrm的使用不仅避免了将内核接口直接暴露给应用程序,而且还表现出在程序之间重用和共享代码的常见优势。

DRM由两部分组成:一个通用的“DRM core”和一个特定的“DRM driver”(用于支持某种特定类型的硬件)。DRM core提供了一个基础框架,DRM driver可以向其注册;core也向用户空间提供了一组最小集的ioctl,这些ioctl具有通过性功能的ioctl,独立于任何特定硬件。而DRM driver,要去实现依赖于特定硬件的 DRM API部分,这和driver所支持的特定GPU硬件相关;而且,它应该提供DRM core没有实现的剩余部分的ioctl。DRM driver也可以扩展API,提供额外的ioctl(只用于它支持的这个特定硬件)。当某DRM driver提供了扩展API时,用户空间的libdrm可通过一个额外的libdrm-driver来对应扩展,用户空间程序可以用这个libdrm-driver来与扩展的ioctl接口交互。

 

DRM Architecture

API

DRM core向用户空间应用程序导出接口,应用程序通常通过相应的libdrm包装函数来使用。另外,DRM driver暴露到用户空间的device-specific接口,用户空间程序可以通过ioctl和sysfs来使用。device-specific接口包括:内存映射、上下文管理、DMA操作、AGP管理、vblank控制、fence管理、内存管理和输出管理。

DRM-Master and DRM-Auth

出于安全和并发的考虑,DRM API中有几个操作(ioctls)必须加以限制:由单一用户空间进程使用。为了实现这一目的,DRM限制:只有被认为是DRM设备“主”进程才能够调用这些受限ioctl,这个“主”进程通常称为DRM-Master。在所有打开设备节点/dev/dri/cardX的进程中,只有一个进程的文件句柄被标记为master,即第一个调用SET_MASTER ioctl的进程。非DRM-Master进程调用这些受限ioctl都将返回一个错误。DRM-Master进程也可以通过 DROP_MASTER ioctl放弃它的master角色,这样别的进程就可以请求master角色。

X Server(或者其他的display server)通常就是获取DRM Master状态的进程,通常它在启动期间打开相应的设备节点,并在整个图形会话期间持有这些特权,直到程序终止。

对于其他非DRM Master的进程,有另一种被称为DRM-Auth方法,去获得使用受限ioctl操作的权限。它基本上就是一种针对DRM设备进行身份验证的方法,目的是向DRM设备证明该进程已获得了DRM-Master的批准,获得了使用的特权。

1. client端可以使用GET_MAGIC ioctl从DRM device获取一个唯一的令牌(32位整数),并用任何可能的方式(通常是某种IPC)将其传递给DRM主进程(例如,在DRI2中,任何X client都可以向X server发送DRI2身份验证请求)。

2. DRM主进程通过调用AUTH_MAGIC ioctl将令牌发送回DRM device。

3. 如果DRM device从DRM-Master接收到的令牌与该进程的令牌匹配,那么DRM device就会向该进程授予特殊权限。

图形执行管理器(Graphics Execution Manager)

由于视频内存的不断增大和graphics API(如OpenGL)的日益复杂,在每个上下文切换处重新初始化显卡状态的策略代价变得太高,性能也不理想。此外,现代Linux桌面需要一种最佳的方式来与合成管理器共享off-screen buffer。为满足这些需求,需要在内核中开发一种新的管理图形缓冲区方法。图形执行管理器(Graphics Execution Manager,GEM)作为这些方法之一应运而生。

GEM提供了一组显式内存管理(译者注:相对于隐式)的API原语。用户空间程序可以通过GEM创建、处理和销毁GPU视频内存中的memory objects。从用户空间程序的角度来看,这些被称为“GEM objects”的memory objects,是持久存在的,不需要每次在程序重新控制GPU时都重新加载。当用户空间程序需要一块视频内存(用于存储framebuffer、纹理或GPU所需的任何其他数据)时,它会使用GEM API向DRM driver请求分配。DRM driver会跟踪所使用的视频内存空间,在有可用内存的情况下尽可能地去满足请求,并向用户空间返回一个“handle”,后面 DRM-Master和DRM-Auth程序会执行 Graphics Execution Manager操作用此“handle”做进一步的内存分配。GEM API还提供了填充buffer的操作,和在不需要buffer时的释放操作。当用户空间进程因为结束或者故障而关闭DRM device文件描述符时,未释放的GEM handles内存将被回收。

GEM还允许多个使用同一DRM device(因此使用相同的DRM driver)的用户空间进程去共享一个GEM object。对于一个进程来说,GEM句柄是本地唯一的32位整数,但是在其他别的进程中,可能是重复的32位整数,因此不适合来做共享。所以需要一个全局命名空间,GEM通过被称为GEM names的全局handles来实现这一点。一个GEM name指向一个GEM object:在同一DRM driver驱动的同一DRM device中,有且仅有一个用32位整数创建的GEM object。GEM提供了一个flink操作来从GEM handle中获取GEM name。然后,一个进程可以使用任何可用的IPC机制将这个GEM name(32位整数)传递给另一个进程。接受GEM name的进程,可以用GEM name来获得一个指向原始GEM object的本地GEM handle。

不幸的是,使用GEM names来做共享buffer是不安全的。一个访问相同DRM device的第三方恶意程序,可以通过探测32位整数,来猜测其他两个进程共享buffer的GEM name。一旦找到GEM name,就可以访问和修改它的内容,这就违反了buffer信息的机密性和完整性。DRM通过引入DMA-BUF机制来解决这个问题,因为DMA-BUF在用户空间用文件描述符来表征buffer ,文件描述符是共享安全的。

对于任何视频内存管理系统来说,除了管理视频内存空间外,另一个重要任务是处理GPU和CPU之间的内存同步。现在系统的存储架构非常复杂,通常涉及到不同级别的cache,对于视频内存空间也是如此。因此,视频内存管理器还应该处理cache一致性,以确保CPU和GPU之间共享数据是一致的。这就意味着:视频内存管理内部通常高度依赖于GPU和存储架构的硬件细节,因此,一般DRM driver都是定制化的驱动程序。

GEM最初由英特尔工程师为i915驱动提供一个视频内存管理器而开发的。Intel GMA 9xx系列是具有统一内存架构(UMA)的集成GPU,其中GPU和CPU是共享物理内存的,并没有专用的VRAM。GEM为内存同步定义了“内存域”,虽然这些“内存域”是和GPU不相关的,但是在设计GEM时,专门考虑了UMA内存架构,这使得GEM不太适合其他内存体系架构,比如那些带有独立VRAM的内存架构。出于这个原因,其他DRM driver决定向用户空间程序导出GEM API,但API内部实现了一个更适合其特定硬件和内存架构的不同内存管理器(译者注:GEM API是一样的,底层驱动实现不一样,有的基于原始的GEM,有的可能基于TTM)。

GEM API还提供ioctl来控制执行流(command buffer),但是它们专用于Intel硬件,用于i915和Intel更高版本的GPU。除了内存管理的ioctl外,其他DRM driver没有去尝试实现GEM API的其他部分。

Translation Table Maps

在GEM之前,Translation Table Maps(TTM) 作为一个GPU通用内存管理器已被开发出来。它专门设计用于管理GPU可能访问到的不同类型的内存,包括专用的Video RAM(通常集成在显卡中)和可以通过I/O内存管理单元访问的系统内存(Graphics Address Remapping Table,GART)。考虑到用户空间图形应用程序通常会处理大量视频数据,TTM还应该去处理CPU不能直接寻址的Video RAM部分空间,并尽可能以最佳性能来处理数据。另一个重要的是,保持不同存储和cache之间的一致性。

TTM的主要概念是“buffer object”,即在某个时间点必须由GPU寻址的视频内存区域。当用户空间图形应用程序想要访问某个buffer objects(通常是用内容填充它)时,TTM可能需要将其重新定位到CPU可寻址的内存。当GPU需要访问的buffer object并不在GPU的地址空间时,可能会发生进一步重新定位或GART映射操作。每个重定位操作都必须处理相关数据和cache一致性问题。

fence是TTM另一个重要的概念,fence本质上是一种管理CPU和GPU之间并发的机制(译者注:共享buffer的同步机制)。当一个buffer object不再被GPU使用时,fence会跟踪到并通常会通知用户空间进程访问这个buffer。

TTM试图以一种合适的方式管理所有类型的内存架构,包括有专用VRAM和没有专用VRAM的内存架构,并在内存管理器中提供所有可能的功能,以驱动任何类型的硬件,这一事实导致了一个过于复杂的解决方案,其API远远大于需求。一些DRM开发人员认为:TTM,尤其是其API,不适合一些特定的driver。当GEM作为一个简单的内存管理器出现时,它的API比TTM更受欢迎。但是,一些驱动程序开发人员认为TTM采用的方法更适合于具有专用video memory和IOMMU的独立显卡,因此他们决定在驱动内部使用TTM,同时将其buffer object封装为GEM对象向外暴露,从而支持GEM API。当前使用TTM作为内存管理器但向外提供GEM API的driver的例子有:用于AMD显卡的radeon driver和用于NVIDIA显卡的nouveau driver。

DMA Buffer共享和PRIME

The DMA Buffer Sharing API(通常缩写为DMA-BUF)是一种Linux内核API,旨在提供一种在多个设备间共享DMA buffer的通用机制,buffer可能由不同类型的设备驱动程序来管理。例如,V4L2设备和图形适配器设备可以通过DMA-BUF共享buffers来实现视频流数据的零拷贝,前者生产视频数据,后者消费视频数据。

DRM首次利用DMA-BUF来实现PRIME技术。PRIME技术是一个GPU卸载解决方案,它使用DMA-BUF在独立GPU和集成GPU DRM driver间共享framebuffer。DMA-BUF一个重要特性是,共享buffer在用户空间表征为文件描述符。为了开发PRIME,DRM API添加了两个新的ioctl,一个用于将本地GEM handle转换为DMA-BUF文件描述符,另一个则是完全相反的操作。

这两个新的ioctl后来被重新启用,用来解决GEM buffer共享机制中固有的安全问题。不同于GEM name,文件描述符是无法猜测的(它们不是全局命名空间),并且Unix操作系统提供了一种安全的方法来访问他们,即使用SCM_RIGHTS语义的Unix域套接字。希望与另一个进程共享GEM object的进程,可以将其本地GEM handle转换为DMA BUF文件描述符并将其传递给接收者,接收者又可以从接收到的文件描述符中获取自己的GEM handle。DRI3使用此方法在client和X Server之间共享buffer,而且Wayland也一样。

KMS(Kernel Mode Setting)

为了工作正常,显卡或者图形适配器必须要设置一个模式(一组屏幕分辨率、颜色深度和刷新率的组合),该模式必须要在显卡自身和其所连接的显示屏幕能够支持的值范围内。这种设置操作称为模式设置,通常它需要对图形硬件进行访问,即要修改显卡的某些寄存器值。在开始使用framebuffer之前,以及当应用程序或用户需要更改模式时,都要执行模式设置操作。

在早期,使用graphical framebuffer的用户空间程序还负责提供模式设置操作,因此它们需要运行在特权模式来访问视频硬件。在类Unix操作系统中,X Server是最明显的例子,它的模式设置都实现在每个特定显卡的DDX驱动里面。后来,这种设置方式被称为User space Mode-Setting or UMS,同时它也引出了多个问题。它不仅打破了操作系统在程序和硬件之间提供的隔离(这种隔离提高了系统稳定性和安全性),而且如果有两个或多个用户空间程序试图同时进行模式设置时,也可能使图形硬件处于不一致的状态。为了避免这些冲突,X Server实际上成为能够执行模式设置操作的唯一用户空间程序,其余的用户空间程序都要依赖X Server来设置想要模式和处理与模式设置相关的操作。最初,模式设置是在X Server启动过程中以独占方式执行的,但后来X Server在运行时也可以进行模式设置了。XFree86 3.1.2版本中引入了XFree86-VidModeExtension扩展,它允许X client向X Server请求modeline(resolution)  changes。VidMode扩展后来被更通用的XRandR扩展所取代。

但是,在Linux系统中,这不是做mode-setting的唯一代码。在系统引导过程中,Linux内核必须为虚拟控制台(基于VESA BIOS扩展定义的标准模式)设置最小文本模式。Linux fb driver同样包含配置framebuffer 的mode-setting代码。为了避免mode-setting冲突,当用户从图形环境切换到文本虚拟控制台时,XFree86 Server(以及后来的X.Org Server)通过保存自己的模式设置状态,以便在用户切换回图形环境时恢复状态,来应对这种情况。但是,这个过程会引起令人烦恼的闪屏现象,甚至可能由于配置失败而导致显示器无法正常显示。

"DRM Master" in user space

这种用户空间模式设置的方法也引起了其他问题:

1. suspend/resume进程必须依赖用户空间工具来恢复之前的模式。这些进程中任何一个由于模式配置错误而导致的失败和crash,都可能让系统不能显示,甚至不能工作。

2. 当屏幕处于图形模式(例如X正在运行时)时,内核是不可能显示错误或调试消息,因为内核只知道VESA BIOS标准文本模式。

3.一个更紧迫的问题是:绕过X Server的图形应用程序激增,以及替代X的图像协议的不断涌现,使得mode-setting code在整个系统中重复出现的地方不断增多。

为了解决这些问题,mode-setting code被移进内核中的单一地方,即现在的DRM模块中。然后,包括X Server在内的每个应用进程都应该通过内核来执行mode-setting操作,内核将确保并发操作不会引起硬件状态的不一致。为了执行这些mode-setting 操作,在内核DRM模块中增加了新的内核API和代码,这些内核部分被称为Kernel Mode-Setting(KMS)。

Kernel Mode-Setting有多个好处。 当然,最直接的好处是,内核(Linux console, fbdev)和用户空间(X Server DDX drivers)中重复的mode-setting 代码可以移除了。 另外,KMS还使得开发新graphics systems更加容易,因为现在不需要这些图形系统实现自己的mode-setting代码。通过这种集中的mode-setting管理方式,KMS解决了在console和X之间(或者不同X实现间)进行切换时出现的闪屏问题。由于它是在内核中的,因此引导阶段也可以使用它,从而避免了在早期阶段更改mode而出现的闪屏问题。

由于KMS是内核的一部分,因此它可以使用仅在内核态才可使用的资源(如中断)。这样,像suspend/resume进程可以通过内核自己的管理来恢复mode,这就大大简化了操作,并且刚好也提高了安全性(不再需要具有root权限的用户空间工具);另外,KMS也轻松解决了内核不支持显示设备hotplug 这个长期问题。Mode-setting与内存管理密切相关,因为framebuffer基本上就是memory buffer,因此将KMS和图形内存管理器紧密集成在一起的呼声很高。 这也是为什么将KMS合并到DRM中而不是作为一个独立子系统的主要原因。

为了避免破坏DRM API向后兼容性,KMS被当作DRM driver的附加驱动feature 而存在。在向DRM core注册时,DRM driver可以选择DRIVER_MODESET flag,以表示其支持KMS API。那些实现Kernel Mode-Setting的driver通常被称为KMS driver,以区别于传统的(无KMS)DRM driver。

也许是因为硬件不支持,某些driver并不包含3D加速的功能,但是这些驱动也是实现了KMS API,虽然它没有实现DRM API其他部分(译者注:除了KMS API的其他部分)。

KMS device model

KMS将输出设备模型化为一系列硬件抽象模块来就行管理,这些模块通常在display controller的显示输出管道上能够找到。 这些块是:

1. CRTC:每个CRTC代表一个display controller的扫描引擎,指向scanout buffer (framebuffer)。CRTC读取当前scanout buffer中的像素数据,并在PLL电路的帮助下,生成视频模式timing信号。 CRTC的个数决定了硬件可以同时处理多少个独立的输出设备,因此,要进行多屏显示,每个显示设备至少需要一个CRTC。两个或多个CRTC也可以工作在克隆模式下,即从相同的framebuffer中扫描出相同的图像数据发送到多个输出设备。

2. connector: 连接器代表display controller发送视频信号的物理接口, 而视频信号来源于为显示而做的扫描操作。 通常,KMS中的connector 概念对应于硬件物理连接器(VGA,DVI,FPD-Link,HDMI,DisplayPort,S-Video等),输出设备(显示器,面板等 )可以永久性地或者临时性地连接到这些接口。 与输出设备相关的信息(例如连接状态,EDID数据,DPMS状态或者支持的视频模式)都存储connector中。

3. encoder: display controller 必须使用一种适合connector的编码格式,对来自于CRTC的视频timing信号进行编码。encoder代表能够执行这种编码的硬件模块。 像TMDS和LVDS就是数字输出的编码; 而VGA和TV out 就是模拟输出,它俩通常还要使用特定的DAC模块。 一个连接器在一个时间点只能接收一个编码器的信号,并且每种类型的连接器仅支持某些编码。 可能还会存在其他的物理限制,即并非每个CRTC都会被连接到一个编码器,这些都限制了CRTC-encoder-connector的可能组合。

4. plane:plane不是一个硬件模块(译者注:有材料表示为“硬件图层”,用于多图层的硬件叠图),而是包含buffer的内存对象,其中的framebuffer数据会被喂到scan-out engine(CRTC)。 拥有framebuffer的plane称为主plane(primary plane),并且每个CRTC都必须关联一个plane,因为plane决定了CRTC视频模式--显示分辨率(宽度和高度),像素大小,像素格式和刷新率等等。 如果显示控制器支持硬件光标叠层的话,CRTC应该还与一个光标plane相关联;如果显示控制器有能力,从其他硬件图层获取数据,并“立即”合成到待显示输出的图像上的话,它应该还会关联辅助图层。

原子显示(Atomic Display)

近年来,人们一直在努力将原子操作带入KMS API的某些常规操作中,特别是像mode setting和page flipping操作, 以便来优化KMS 的“原子显示” API(以前称为“原子模式设置”和“原子页面翻转”)。

atomic mode-setting的目的是,通过回避可能导致视频状态不一致或者无效的中间步骤,确保在多限制下的复杂配置,视频状态也能够正确修改;另外,当要撤消一个错误的模式设置(“回滚”)时,它也可以避免了那些具有风险的视频状态。通过提供模式测试功能,atomic mode-setting还可以预先知道某个特定的模式配置是否恰当。 当一个模式被测试并确认是合法的,就可以通过单个不可分割的原子提交操作来设置这个模式,使其生效。 这些测试和提交操作,都可以通过不同flags的ioctl操作来完成。

另一方面,atomic page flip允许在同一输出上更新多个平面(例如:primary plane,cursor plane以及可能的硬件叠层或辅助图层),这些plane均在同一VBLANK间隔内同步,从而确保图像正确显示而不会出现撕裂。移动和嵌入式显示控制器特别需要这种叠层技术,它们更倾向于使用这种叠层技术去降低功耗。

新atomic API是在旧KMS API基础上构建的。 它使用了相同的模型和object(CRTC,encoder,connector,plane等),不同的是,property的数量越来越多(译者注:property是DRM架构里面的一种object)。原子操纵通过更改相关property来构建我们要测试或提交的状态。 我们要修改的属性取决于我们是否要进行mode-setting(主要是CRTC,encoder和connector 的property)或page flipping(通常是plane属性)。 在两种情况下,ioctl都是相同的,区别在于每次传递的属性列表。

渲染节点(Render node)

对于原始的DRM API,上层程序有特权(mode-setting, 其他的显示控制)和非特权(渲染,GPGPU计算)两种方式来操作DRM设备(/dev/dri/cardX)。出于安全原因,打开DRM设备相关文件都需要具有“等同于root权限”的特殊权限。这就导致产生了一种架构,架构中只有一些可靠的用户空间程序(X server,图形合成器等)可以完全访问DRM API,包括特权部分API(如mode-setting API)。其余的应用程序,如果想要渲染或者进行GPGPU计算,要通过特定的身份验证接口从"DRM Master"程序那里来获得权限。通过身份验证的应用程序可以使用受限版本的DRM API(而非特权API)来进行渲染和或GPGPU计算。这种设计产生一个严重的约束:必须始终有图形服务器(X Server,Wayland compositor,...)在运行,以便充当DRM-Master,来授权其他应用程序使用DRM设备的权限,即使是在不涉及任何图形显示的情况下。

"render nodes" 的概念就是试图解决上面的情况:它将DRM用户空间API分为两类接口:一类特权接口和一个非特权接口,并为每类接口提供独立的设备文件节点(node)。对于GPU硬件,它的DRM driver(如果driver支持渲染节点功能)除了创建/dev/dri/cardX主节点外,还会创建一个渲染节点,即设备文件/dev/dri/renderDX。使用直接渲染模型的client和想要利用GPU来计算的应用程序,都可以简单地通过打开渲染节点,并使用DRM API支持的有限子集,来向GPU发起操作,而整个过程,无需什么特权,只要系统让程序有打开渲染节点的权限。Display servers, compositors和其他需要mode-set API或特权操作的程序,都必须要打开标准主节点,主节点被授予DRM API的完整访问权限。 渲染节点明确禁止GEM flink操作,以防止使用不安全的GEM global name来共享buffer;仅PRIME(DMA-BUF)文件描述符可用于共享buffer。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值