Linux图形显示系统之DRM

目录

1. Overview

2. Software architecture

2.1 API

2.2 DRM-Master 和 DRM-Auth

2.3 GEM

2.4 Translation Table Maps

2.5 DMA Buffer Sharing and PRIME

2.6 Kernel Mode Setting

2.7 KMS device model

2.8 Atomic Display

2.9 Render nodes

3.  Hardware support

4. Development


1. Overview

        Direct Rendering Manager(DRM)是linux内核子系统,负责与显卡交互。 DRM提供一组API,用户空间程序可以使用该API将命令和数据发送到GPU并执行诸如配置显示器的模式设置之类的操作。DRM最初是作为X server Direct Rendering基础结构的内核空间组件开发的,但从那以后它被其他图形系统(例如Wayland)所使用。用户空间程序可以使用DRM API命令GPU执行硬件加速的3D渲染和视频解码以及GPU计算。

        Linux内核已经有一个fbdev的API,用于管理图形适配器的帧缓存区,但不能用于满足基于3D加速的现代基于GPU的视频硬件的需求。这些设备通常需要在自己的内存中设置和管理命令队列,以将命令分派给GPU,并且还需要管理该内存中的缓冲区和可用空间。最初,用户空间程序(例如X server)直接管理这些资源,但通常它们的行为就像它们是唯一可以访问GPU的程序一样。当两个或多个程序试图同时控制相同的硬件,并以自己的方式设置每个资源时,大多数情况下,它们都是灾难性。

        DRM的创建是为了允许多个程序同时使用视频硬件资源。DRM获得对GPU的独占访问权,并负责初始化和维护命令队列、内存和任何其他资源。希望使用GPU的程序将请求发送到DRM,DRM充当仲裁程序,并注意规避可能的冲突。

        多年来,DRM的范围已得到扩展,以涵盖以前由用户空间程序处理的多重功能,例如framebuffer管理和模式设置,内存共享对象和内存同步。这些扩展中的某些被赋予了特定的名称,例如图形执行管理器(GEM)或内核模式设置(KMS),并且当特别提及它们提供的功能时,该术语将占上风,但它们仍然属于整个内核DRM子系统的一部分。

2. Software architecture

        DRM驻留在内核空间中,因此用户空间程序必须使用内核系统调用来请求其服务。但是DRM没有定义自己的自定义调用。相反,它遵循Unix原则“一切皆文件”,使用/dev层次结构下的设备文件通过文件系统名称空间公开GPU。DRM检测到的每个GPU都称为DRM设备,并创建了一个设备文件/dev/dri/cardX(X是一个序列号)与之连接,并使用ioctl调用与DRM进行通信。不同的ioctl对应于DRM API的不同功能。

        创建了一个名为libdrm的库,以方便用户空间程序与DRM子系统的接口。该库只是一个包装器,它为DRM API的每个ioctl提供用C编写的函数,以及常量、结构和其他辅助元素。使用libdrm不仅避免将内核接口直接暴露给应用程序,而且还提供了在程序之间重用和共享代码的优点。

        DRM由两部分组成:通用“DRM core”和每种受支持的特定部分(“DRM Driver”)。DRM core提供了可以注册不同DRM驱动程序的基本框架,还为用户空间提供了具有通用的,独立于硬件的,功能的最少ioctl集。另一方面,DRM Driver实现API的硬件相关部分,具体取决于它所支持的GPU类型,它应提供DRM核心未涵盖的其余ioctl的实现。

       DRM驱动也可以扩展API,提供特定GPU上可用的具有附加功能的附加ioctl。当特定的DRM驱动程序提供增强的API时,用户空间libdrm也将通过一个额外的库libdrm-driver扩展,这个扩展库可以被用户空间用来调用其他ioctl接口。

2.1 API

        DRM Core将几个接口导出到用户空间应用程序,让相应的libdrm包装成函数后来使用。

        DRM Driver通过ioctl和sysfs文件导出设备专用的接口,供用户空间驱动程序和支持设备的应用程序使用。外部接口包括:内存映射,上下文管理,DMA操作,AGP管理,vblank控制,fence管理,内存管理和输出管理

2.2 DRM-Master 和 DRM-Auth

        DRM API 中有几个 ioctl 由于并发问题仅限于用户空间单个进程使用。为了实现这种限制,将 DRM 设备分为 Master 和 Auth。上述的那些 ioctl 只能被 DRM-Master 的进程调用。打开了 /dev/dri/cardX 的进程的文件句柄会被标志为 master,特别是第一个调用该 SET_MASTER ioctl 的进程。如果不是 DRM-Master 的进程在使用这些限制 ioctl 的时候会返回错误。进程也可以通过 DROP_MASTER ioctl 放弃 Master 角色,来让其他进程变成 Master。

        X Server 或者其他的 Display Server 通常会是他们所管理的 DRM 设备的 DRM-Master 进程。当 DRM 设备启动的时候,这些 Display Server 打开设备节点,获取 DRM-Master 权限,直到关闭设备。

        对于其他的用户空间进程,还有一种办法可以获得 DRM 设备的这些受限权限,这就是 DRM-Auth。它是一种针对 DRM 设备的验证方式,用来证明该进程已经获得了 DRM-Master 对于他们去访问受限 ioctls 的许可。

步骤:

        (1)DRM Client 使用 GET_MAGIC ioctl 从 DRM 设备获取一个 32bit 整型的 token。并通过任何方式(通常是 IPC)传递给 DRM-Master。

        (2)DRM-Master 进程使用 AUTH-MAGIC ioctl 返回 token 给 DRM 设备。

        (3)设备将 DRM-Master 所给的 token 和 Auth 的进行对比。通过的话就赋予进程文件句柄特殊的权限。

2.3 GEM

        Graphics Execution Manager(GEM)是一种内存管理方法。由于视频存储器的大小增加以及诸如OpenGL之类的图形API的日益复杂性,从性能角度看,在每个上下文切换处重新初始化图形卡状态的策略过于低效。另外,现代linux桌面还需要一种最佳方式与合成管理器(compositing manager)共享屏幕外缓冲区。这些要求诞生开发了用于管理内核内部图形缓冲区的新方法,图形执行管理方法(GEM)是其中一种。

        GEM为API提供了显式的内存管理原语。通过GEM,用户空间程序可以创建,处理和销魂GPU视频内存中的内存对象(Bo)。从用户空间程序的角度看,这些被称为“GEM对象”的对象是持久性的,不需要在程序每次重新获得对GPU的控制时对重新加载它们。当用户空间程序需要大量的视频内存(以存储framebuffer,纹理或者GPU所需的任何其他数据)时,它将使用GEM API请求分配给DRM驱动程序。DRM驱动程序会跟踪已使用的视频内存,如果有可用的空闲内存,则能够满足请求,将“句柄”返回给用户空间,以在以后的操作中进一步引用分配的内存。GEM API还提供了一些操作,以填充缓冲区并在不再需要时释放缓冲区。当用户空间进程有意或由于终止而关闭了DRM设备文件描述符时,未释放的GEM句柄中的内存将恢复。

        GEM还允许使用同一DRM设备(因此使用相同的DRM驱动程序)的两个或多个用户空间进程共享GEM对象。GEM句柄时是某个进程唯一的局部32位整数,但在其他进程中可重复,因此不适合共享。所需要的是一个全局命名空间,GEM通过使用称为GEM names的全局句柄来提供一个命名空间GEM名称是指使用相同的DRM驱动程序在同一个DRM设备中通过使用唯一的32位整数创建的一个GEM对象,并且仅是一个GEM对象。GEM提供了一个操作flink来从GEM句柄获取GEM名称。然后,该进程可以使用任何可用的IPC机制将该GEM名称(32位整数)传递给一个进程。接收方进程可以通过GEM名称来获取原始GEM对象的本地GEM句柄。

        不幸的是,使用GEM名称共享缓冲区并不安全。一个恶意的第三方进程访问同一DRM设备可以会试图通过探查32位整数来猜测其他两个进程共享的缓冲区的GEM名称。一旦找到GEM名称,就可以访问和修改其内容,从而违反了缓冲区信息的机密性和完整性。后来通过在DRM中引入了DMA-BUF支持来克服此缺点,因为DMA-BUF将用户空间中的缓冲区表示为文件描述符,可以安全的共享它们。

        除了管理视频内存空间外,任何视频内存管理系统的另一个重要任务是处理GPU和CPU之间的内存同步。当前的存储器架构非常复杂,并且通常涉及用于系统存储器以及有时也用于视频存储器的各种级别的高速缓存。因此,视频内存管理器还应处理缓存一致性,以确保CPU和GPU之间共享的数据一致。这意味着视频内存管理内部结构通常高度依赖于GPU和存储体系结构的硬件细节,因此取决于驱动程序。

        GEM最初是由英特尔工程师开发的,旨在为其i915驱动程序提供视频存储管理器。英特尔GMA 9xx家族是具有统一内存架构(UMA)的集成GPU,其中GPU和CPU共享物理内存,并且没有专用的VRAM。GEM定义了用于内存同步的“内存域”,尽管这些内存域与GPU无关,但它们在设计时特别考虑了UMA内存架构,这使其不适用与其他内存架构,例如具有单独VRAM的内存架构。因此,其他的DRM驱动程序已决定向用户空间程序公开GEM API,但在内部它们实现了更适合其特定硬件和内存体系结构的其他内存管理器。

        GEM API还提供了用于控制执行流(命令缓冲区)的ioctl,但它们是Intel特定的,可与Intel i915和更高版本的GPU一起使用。除了特定于内存管理的ioctl,没有DRM驱动程序试图去实现GEM API的其他任何部分。

2.4 Translation Table Maps

        Translation Table Maps(TTM)是在GEM之前开发的用于GPU的通用内存管理器名称。它专门用于管理GPU可能访问的不同类型的内存,包括专用的Video RAM(通常安装在显卡中)与可通过称为Graphics Address Remapping Table(GART)的I/O内存管理单元访问的系统内存。考虑到用户空间图形应用程序通常会处理大量视频数据,TTM还应处理CPU无法直接寻址的Video RAM部分,并以最佳性能来实现。另一个重要的事情是保持所涉及的不同内存和缓存之间的一致性。

        TTM的主要概念是“缓冲对象”,即在某些时刻GPU是可寻址的视频内存。当用户空间图形应用程序想要访问某个缓冲对象(通常是用内容填充它)时,TTM可能需要将其重新定位为CPU可以寻址的一种存储器。当GPU需要访问的缓冲区对象还不在GPU的地址空间中时,可能会发生进一步的重定位或GART映射操作。这些重定位操作中的每一个都必须处理任何相关的数据和缓存一致性问题。

        TTM的另一个重要概念是围栏(fences)。围栏本质上是一种管理CPU和GPU之间并发性的机制。围栏跟踪GPU何时不再使用缓冲区对象,通常用于通知任何具有访问权限的用户空间进程。

        TTM试图以合适的方式管理所有类型的内存体系结构,包括有和没有专用VRAM的体系结构,并在内存管理器中提供所有可想到的功能,以便与任何类型的硬件一起使用,这导致了过于复杂的情况。API远远超出所需的解决方案。一些DRM开发人员认为它不适用于任何特定的驱动程序,尤其是API。当GEM成为一种更简单的内存管理器时,它的API优于TTM。但是一些驱动程序开发人员认为TTM所采用的方法更适合于具有专用Video RAM和IOMMU的分立显卡,因此他们决定在内部使用TTM,同时将其缓冲区对象公开为GEM对象,从而支持GEM API。当前使用TTM作为内部内存管理器但提供GEM API的驱动程序实例包括AMD显卡的radeon驱动程序和NVIDIA显卡的nouveau驱动程序。

2.5 DMA Buffer Sharing and PRIME

         DMA缓冲区共享API(通常缩写为DMA-BUF)是一种Linux内核内部API,旨在提供一种通用机制来在多个设备之间共享DMA缓冲区,并可能由不同类型的设备驱动进行管理。例如,Vdieo4Linux设备和图形适配器设备可以通过DMA-BUF共享 缓冲区,以实现前者产生并由后者消耗的视频流数据的零复制。任何Linux设备驱动程序都可以将此API实现为导出器、用户(消费者)或者二者兼而有之。

        在DRM中首次利用此功能来实现PRIME(擎天柱,显卡交火),这是一种用于GPU卸载的的解决方案,它使用DMA-BUF在独立显卡和集成显卡的DRM驱动程序之间共享生成的framebuffer。DMA-BUF的一项重要功能是为了将共享缓冲区作为文件描述符提供给用户空间。为了开发PRIME,在DRM API中添加了两个新的ioctl,一个将本地GEM句柄转换为DMA-BUF文件描述符,另一个用于相反的操作。

       后来这两个新的ioctl被用作解决GEM缓冲区共享固有的不安全性的一种方法。与GEM名称不同,无法猜测到文件描述符(它们不是全局命名空间),并且Unix操作系统提供了一种安全的方法来传递Unix域套接字,通过使用SCM_RIGHTS语义。一个想要与另一个进程共享GEM对象的进程可以将本地GEM句柄转换为DMA-BUF文件描述符,然后将其传递给接收者,后者可以从接收的文件描述符中获得自己的GEM句柄。DRI3使用这种方法在客户端和X Server或者Wayland之间共享缓冲区。

2.6 Kernel Mode Setting

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

         在早期,希望使用图形framebuffer的用户空间程序还负责提供模式设置操作,因此它们需要在对视频硬件进行特权访问的情况下运行。在Unix类型的操作系统中,X Server是最突出的示例,其模式设置实现存在于每种特定类型的显卡的DDX驱动程序中。这种方法(以后称为用户空间模式设置或UMS)带来了几个问题。这不仅打破了操作系统应在程序和硬件之间提供的隔离性,从而提高稳定性和安全性,而如果两个或多个用户空间程序尝试在操作系统上进行模式设置,可能会使图形硬件处于不一致的状态。同时,为了避免这种冲突,实际上X Server成为了唯一执行模式设置操作的用户空间程序。其余的用户空间程序依靠X Server来设置适当的模式并处理涉及模式设置的任何其他操作。最初,模式设置仅在X Server启动过程中执行,但是后来X Server可以在运行时进行设置。XFree86 3.1.2中引入了XFree-VidModeExtension扩展,以允许X-Client请求对X Server的modeline(分辨率)更改。VidModeExtension后来被更加通用的XRandR扩展取代。

        然而,这不是在linux系统中进行模式设置的唯一代码。在系统引导过程中,Linux内核必须为虚拟控制台设置最小文本模式(基于VESA BIOS扩展定义的标准模式)。Linux内核帧缓冲驱动程序还包含用于配置帧缓冲设备的模式代码。为了避免模式设置冲突,XFree86 Server(以及后来的X.Org Server)在用户从图形环境切换到文本虚拟控制台时保存模式设置状态,并在切回到X图形环境时还原它。此过程在过渡中引发了闪烁现象,并且还可能失败,从而导致输出显示损坏或无法使用。

用户空间模式设置方法还引起了其他问题:

        Ø 挂起/恢复过程必须依靠用户空间工具来恢复以前的模式。由于模式集配置错误,这些程序之一的单个故障或崩溃可能会使系统无法正常显示,因此无法使用。

        Ø 当屏幕处于图形模式时(例如运行X),内核也不可能显示错误或调试信息,因为内核知道的唯一模式是VESA_BIOS标准文本模式。

        Ø 更为紧迫的问题是绕过X Server的图形应用程序的激增以及X的其他图形栈替代方案的出现,从而进一步扩展了模式设置代码在整个系统中的重复。

        为了解决这些问题,将模式设置代码移至内核中的单个位置,放到了现有的DRM模块中。然后每个进程(包括X Server)都应该能够命令内核执行模式设置操作,并且内核将确保并发操作不会导致不一致的状态。添加到DRM模块以执行这些模式设置操作的新内核API和代码称为Kernel Mode-Setting(KMS)。

        Kernel Mode-Setting(KMS)有几个好处。最直接是从内核(Linux控制台,fbdev)和用户空间(X Server DDX驱动程序)中删除重复的模式设置代码。KMS还使编写替代图形系统变得更加容易,而这些图形系统现在不需要实现自己的模式设置代码。通过提供集中的模式管理,KMS解决了在控制台和X之间以及X的不同实例之间切换引起的闪屏问题。由于它在内核中可用,因此也可以在引导过程开始时使用它,从而避免了由于这些早期阶段的模式更改而导致的闪烁。

        Kernel Mode-Setting(KMS)是内核的一部分,这一事实使KMS可以使用仅在内核空间可用的资源(例如中断)。例如,挂起/恢复过程后的模式恢复通过由内核本身进行管理,大大简化了操作,并附带地提高了安全性(不再需要具有root权限的用户空间工具)。内核还可以轻松地热拔插新的显示设备,从而解决了一个长期存在的问题。模式设置也与内存管理密切相关,因为framebuffer基本上是内存缓冲区,因此强烈建议与图形内存管理器紧密集成。这就是为什么将内核模式设置代码合并到DRM中而不是作为一个单独的子系统的主要原因。

        为避免破坏DRM API的向后兼容性,内核模式设置作为某些DRM驱动程序的附件驱动程序功能提供。任何DRM驱动程序在向DRM内核注册时都可以选择提供DRIVER_MODESET标志,以指示它支持KMS API。那些实现内核模式设置的驱动程序通常被称为KMS驱动程序,以将它们与传统的(无KMS)DRM驱动。

        KMS驱动已经被接受适配到了这种程度,某些缺少3D加速(或者硬件供应商不希望公开或者实现它)的驱动程序在没有DRM其余API的情况下实现了KMS API。

2.7 KMS device model

         KMS将输出设备建模和管理为一系列抽象硬件模块,这些模块通常分布在显示控制器的显示输出管道上。这些模块是:

         Ø CRTC:每个CRTC(来自CRT控制器)代表显示控制器的扫描引擎,指向扫描缓冲区(framebuffer)。CRTC的目的是读取当前在扫描缓冲区中的像素数据,并借助PLL电路从中生成视频模式时序信号。可用的CRTC的数量决定了硬件可以同时处理多少个独立的输出设备,因此,要使用屏配置,每个显示设备至少需要一个独立的CRTC。两个或更多个CRTC也可以工作在克隆模式下,它们从相同的framebuffer中扫描出来,将相同的图像数据发送到多个输出设备。

        Ø Connector:连接器代表显示控制器从扫描输出中发送视频信号到显示的操作。通常,KMS连接器的概念对应于输出设备(显示器,笔记本电脑面板等)硬件中的物理连接器(VGA,DVI,FPD-Link,HDMI,DisplayPort,S-Video等),它是永久的,也可以是临时的。与当前物理连接的输出设备有关的信息(例如连接状态,EDID数据,DPMS状态或受支持的视频模式)也存储在连接器内。

         Ø Encoder:显示控制器必须对来自于CRTC视频模式数据编码为适合的符合预期的连接器的格式,以适配连接器。编码器代表能够执行这些编码之一的硬件模块。用于数字输出的编码示例为TMDS和LVDS;对于VGA和TV输出等模拟输出,通常使用特定的DAC模块。一个连接器一次只能接受一个编码器的信号,并且每种类型的连接器仅支持某些编码。可能还会存在其他物理限制,即并非每个CRTC都连接到每个可用的编码器,从而限制了CRTC-encoder-connector的可能组合。

        Ø Plane:Plane不是硬件块,而是一个包含向CRTC发送数据的缓存块的内存对象。拥有framebuffer的Plane称为主平面(primary plane),每个CRTC必须关联一个平面,因为它是CRTC决定采用哪种视频模式的根据—显示分辨率(宽度和高度),像素大小,像素格式,刷新率等。如果显示控制器支持硬件光标覆盖,则CRTC可能还与光标平面相关联;如果显示控制器能够从其他硬件覆盖中扫描,并“即使”合成或混合发送到输出设备的最终图像,则CRTC可能与之关联。

2.8 Atomic Display

         近年来,人们一直在努力使与KMS API有关的某些常规操作具有原子性,尤其是与模式设置和页面翻转操作有关。这种增强的KMS API就是所谓的“原子显示”(以前称为“原子模式设置”和“原子或原子页面翻转”)。

         原子模式设置的目的是通过避免可能导致视频状态不一致或无效的中间步骤,确保在具有多个限制的复杂配置中正确更改模式;在必须撤消失败的模式设置过程(“回滚”)时,它还避免了危险的视频状态。原子模式设置通过提供模式测试功能,可以事先知道某些特定的模式配置是否合适。当测试原子模式并确认其有效性时,可以通过单个不可分割的(原子)提交操作来应用它。测试和提交操作均由具有不同标志的同一新ioctl提供。

        另一方面,原子页面翻转允许在同一输出上更新多个平面(例如主平面,光标平面以及可能的某些叠加或辅助平面),这些平面均在同一VBLANK间隔内同步,从而确保正确的显示而不会撕裂。这项要求特别适用于移动和嵌入式显示控制器,它们倾向于使用多个平面/叠层以节省功耗。

        新的原子API是在旧的KMS API的基础上构建的。 它使用相同的模型和对象(CRTC,Encoder,Connector,Plane等),但是可以修改的对象属性越来越多。原子过程基于更改相关属性以构建我们要测试或提交的状态。 我们要修改的属性取决于我们是否要进行模式设置(主要是CRTC,编码器和连接器属性)或页面翻转(通常是平面属性)。这两种情况的ioctl相同,不同之处在于每次传递的属性列表。

2.9 Render nodes

         在原始DRM API中,DRM设备“/dev/dri/cardX”用于特权(模式设置,其他显示控件)和非特权(渲染,GPGPU计算)操作。 出于安全原因,打开关联的DRM设备文件需要“等同于root特权”的特殊特权。这导致了一种体系结构,其中只有一些可靠的用户空间程序(X服务器,图形合成器等)可以完全访问DRM API,包括特权部分(如模式集API)。 想要渲染或进行GPGPU计算的其余用户空间应用程序应由DRM设备的所有者(“DRM主设备”)通过使用特殊的身份验证界面来授予。然后,经过身份验证的应用程序可以使用DRM API的受限版本来呈现或进行计算,而无需特权操作。这种设计施加了严格的约束:必须始终有运行中的图形服务器(X服务器,Wayland合成器,...)充当DRM设备的DRM主设备,以便可以授予其他用户空间程序使用DRM设备的权限,即使在不涉及任何图形显示(如GPGPU计算)的情况下。

         “渲染节点(Render nodes)”概念试图通过将DRM用户空间API分成两个接口(一个特权和一个非特权)并为每个接口使用单独的设备文件(或“节点”)来解决这些情况。对于找到的每个GPU,其对应的DRM驱动程序(如果它支持渲染节点功能)除了主节点/dev /dri/cardX之外,还会创建一个设备文件/dev/dri/renderDX,称为渲染节点。使用直接渲染模型的客户端和想要利用GPU的计算功能的应用程序可以通过简单地打开任何现有的渲染节点并使用DRM API支持的有限子集来分派GPU操作,而无需其他特权即可做到这一点--前提是它们具有打开设备文件的文件系统权限。 显示服务器,合成器和任何其他请求模式API或任何其他特权操作的程序,都必须打开授予对完整DRM API的访问权限的标准主节点,并像往常一样使用它。渲染节点显式禁止GEM flink操作,以防止使用不安全的GEM全局名称共享缓冲区。 只能通过使用PRIME(DMA-BUF)文件描述符的方式,与包括图形服务器在内的另一个客户端共享缓冲区。

3.  Hardware support

        如上图所示,DRM将由用户模式图形设备程序使用,例如Mesa 3D。用户空间程序使用Linux系统调用访问DRM,DRM通过自身的系统调用来响应Linux的系统调用。

4. Development

        Direct Rendering Manager是在Linux内核中开发的,其源代码位于Linux源代码的/ drivers/gpu/drm目录中。子系统维护者是Dave Airlie,其他维护者则负责特定的驱动程序。 与Linux内核开发一样,DRM子维护者和贡献者将具有新功能和错误修复的补丁程序发送给主要DRM维护者,后者将它们集成到自己的Linux存储库中。DRM维护者依次将所有这些修补程序提交给Linus Torvalds,这些修补程序随时准备发布到Linux新版本中。 作为整个内核的顶级维护者,Torvalds保留了有关补丁是否适合包含在内核中的最后决定。

        由于历史原因,libdrm库的源代码保留在Mesa项目之下。

libdrm下载链接:Index of /libdrm

参考:LIBDRM使用_linux_dafei的博客-CSDN博客

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值