Linux 文件与I/O管理

目录

一、VFS

二、I/O 体系结构

三、块设备I/O

 四、页高速缓存

五、访问文件


一、VFS

        VFS是指Virtual File System,即虚拟文件系统,又称 Virtual Filesystem Switch,即虚拟文件系统转换。VFS是Linux对多个文件系统做的一层抽象,方便Linux实现多种文件系统共存,对上层应用程序屏蔽文件系统类型,提供统一的调用接口,并且VFS提供了目录项高速缓存,索引节点高速缓存和页高速缓存等磁盘高速缓存来提升文件读取性能。VFS支持的文件系统类型大概分为以下三种:

  1. 网络文件系统,允许轻松访问属于其他网络计算机的文件的文件系统,如 nfs、cifs、ncp等;
  2. 磁盘文件系统,管理类似磁盘的存储设备的文件系统,如Linux的ext4、ext3,windows的nfs,fat32,unix的ufs等;
  3. 特殊文件系统,不管理本地或者远程磁盘空间的文件系统,如 proc、sysfs、ramfs、tmpfs 等。 特殊文件系统不限于物理块设备,但是内核给每个特殊文件系统分配一个虚拟的块设备,其主设备号为0,次设备号为任意值。

可以通过cat /proc/filesystems命令查看Linux已经注册支持的文件系统。Linux的根目录包含在根文件系统中,通常为ext2或者ext3,其他所有的文件系统可以挂载到根目录下的某个子目录,挂载的目录称为安装点。同一个文件系统可以被安装多次,对应多个不同的安装点,但是对应的超级块对象只有一个。根文件系统的安装在系统初始化阶段完成。需要VFS支持一个新的文件系统时,需要基于该文件系统实现VFS制定的标准接口,将接口实现代码放入内核镜像或者作为一个模块动态装入,然后注册该文件系统即可。

       VFS引入了一个通用的文件模型,包含四种对象类型,其中超级块对象和索引节点对象在磁盘上都有对应的映像,对应的数据结构中都有一个字段标识当前对象是否是脏的,如果是则需要更新磁盘对应的映像:

  1. 超级块对象 (superblock object):存放一个已安装的文件系统的相关信息,数据结构为super_block,通过文件系统对象file_system_type的get_sb方法获取。
  2. 索引节点对象 (inode object):存放关于具体文件或者文件目录的相关信息,数据结构为inode,每个索引节点对应都有一个唯一的索引节点号,来唯一的标识文件系统中的文件。索引节点不随文件的文件名的更改而更改,随文件的存在而存在。Linux使用索引节点高速缓存来提供索引节点的查找效率。
  3. 目录项对象 (dentry object) :存放目录项与对应文件进行链接的相关信息,数据结构为dentry,按指定路径查找文件时,内核为每一级目录创建一个目录项对象,每个目录项对象都会关联一个索引节点对象,通过它来查找当前目录下包含的文件。Linux使用基于散列表的目录项高速缓存来提高目录项查找的效率,减少频繁创建目录项的性能损耗,同时使用LRU链表维护未使用的目录项对象,方便内核缩减高速缓存时释放最近未使用的目录项。
  4. 文件对象 (file object) :存放打开文件与进程之间进行交互的有关信息,数据结构为file,文件被打开时由内核创建,多个文件对象可指向一个文件。

      VFS将不同文件系统转换成这四种对象时都遵循一个原则, 所有与该文件系统或者文件的操作函数都由文件系统本身实现,所有的系统调用最终都会转换成对特定文件系统的操作函数的调用。如文件对象中包含一个字段f_opt,该字段包含了对该文件的读写操作的函数指针,当执行read()系统调用时,对应的系统调用服务例程sys_read()会调用file对象中f_opt字段的read()方法。

    参考:Linux虚拟文件系统VFS解决

二、I/O 体系结构

    CPU与I/O设备之间的数据通路通常称为I/O总线,任何I/O设备只能连接到一条总线上,连接需要3中硬件元素参与:I/O端口,I/O接口和设备控制器。

    CPU同外部设备交互是通过设备控制器上的寄存器来完成,按功能来分,一共有3种寄存器:控制寄存器,用于接收操作指令;状态寄存器,用于获取设备状态;输入/输出寄存器,又称数据寄存器,用于接收指令的参数和输出操作结果。CPU为了访问这些外设寄存器必须对其分配地址,不同架构CPU的分配方式不一样:

1、x86架构的CPU采用独立编址方式分配地址,CPU为外设寄存器实现了一个单独的地址空间,称为I/O空间,与常规的内存地址空间独立,32位下大小为64k,在I/O空间下给寄存器分配的地址称为I/O端口。CPU有专门的指令读写I/O端口,如in,out指令,比常规的读写内存指令更短,执行速度更快。Linux下驱动程序加载时首先需要调用request_resource( )方法给外设分配I/O端口,Linux用resouce数据结构表示一个外设拥有的I/O端口范围,所有的resouce以树形结构保存,子节点的I/O端口范围是父节点的子范围。Linux访问I/O端口有两种方式,一种是利用汇编指令直接访问,一种是将其映射到内核地址空间内,然后使用跟访问I/O内存一样函数访问I/O端口,底层实现是根据地址范围判断是否是I/O端口,如果是则依然使用汇编指令访问。

2、arm架构的CPU采用统一编址的方式,在常规的内存地址空间内划分一段,给外设的寄存器分配地址,把外设的寄存器当做普通的物理内存来使用,使用常规的内存读写指令操作寄存器,这部分特殊的内存称为I/O内存。Linux驱动程序加载时需要调用request_mem_region()给外设在内核空间分配一段地址空间,访问I/O内存只有一种方式,通过ioread8、ioread16、ioread32等函数访问。

     I/O接口是处于一组寄存器和对应设备之间的硬件电路,主要起翻译器的作用,把寄存器中的值翻译成对应设备所需的命令和数据,分为两种:

1、专用I/O接口,专门用于一个特定的设备,通常与设备控制器位于同一个芯片上,如键盘接口,鼠标接口,磁盘接口等。

2、通用I/O接口,通用的连接其他外部设备的接口,如常见的USB接口,用于连接显示屏,投影仪等的SCSI接口接口

   设备控制器也是起到翻译器的作用,将从I/O接口接收到的高级指令转换成适当的电信号并驱动设备完成特定的操作,将从设备接收到的电信号转换成寄存器中的数据,并通过中断通知CPU及时取走并处理数据。

   整体结构如下图:

    参考:Linux系统对IO端口和IO内存的管理

三、块设备I/O

      块设备的主要特点是CPU和总线读写数据的速度远大于硬件设备的速度,Linux设计了多种机制来提高块设备的I/O效率。

      磁盘高速缓存是内核利用内存提供的磁盘数据的缓存,提高不同进程读写同一份数据的效率,包含索引节点高速缓存,目录项高速缓存和页高速缓存等。现在磁盘通常使用RAID卡来提高磁盘的可用性和冗余能力,RAID卡通常都提供了硬件层面的读写缓存,并自带电池保证断电的情况下缓存数据不丢失。

      扇区是块设备传递数据的最小单位,不允许传送少于一个扇区的数据,通常一个扇区的大小为512bytes。

      是VFS和文件系统传送数据的基本单位,块的大小必须是扇区的整数倍,最大不能超过一个页框,每个块在内核都有对应的块缓冲区,两者大小一致,读取数据时会将硬件读取的值来填充对应的块缓冲区,写入数据时从块缓冲区中的实际值来更新硬件对应的扇区。

      是ELF文件中的概念,同一个段的数据在磁盘上是连续的,但是对应的页框不一定是连续的,这种将相邻扇区的数据传递到非连续内存区的方式称为分散-聚合DMA传送。如果多个段的页框是相邻的且对应的数据块也是相邻的,则通用块层会将其合并成一个更大的内存区,称为物理段。一次分散-聚合DMA传送可能会涉及多个段的数据读取,块设备驱动程序必须支持。

     通用块层是一个内核组件,他处理所有来自内核的对块设备的读写请求,其核心数据结构是bio,代表一个块,每启动一次新的I/O操作时,就会通过bio_alloc()函数由slab分配器分配一个新的bio结构,I/O操作时bio中的部分字段会实时更新。其提供的功能如下:

  • 将数据缓冲区放在高端内存区,仅当CPU访问其中的数据时,才将其映射到内核空间,访问结束后取消映射
  • 通过零-复制模式将数据缓冲区直接映射到进程用户态地址空间内,避免磁盘数据从内核空间拷贝到用户空间
  • 管理逻辑卷和磁盘分区,发挥大部分新磁盘控制器的高级特性

通用块层收到IO请求后会将其对某个磁盘分区的请求转换成对整块磁盘的部分扇区的请求而忽略磁盘分区的存在,然后将IO请求传给I/O调度程序做适当的合并处理。 

    I/O调度层也是一个内核组件,用于对通用块层发出的I/O请求做适当的合并处理,即对相邻的几个扇区的数据请求合并成一个请求,减少读写数据过程中磁头移动的耗时,从而提高磁盘I/O效率。块设备待处理的I/O请求用request数据结构表示,每个请求可能包含一个或者多个bio结构,I/O调度层的合并操作就是把bio结构加入一个已经存在的合适的request结构中,块设备驱动程序处理request时会逐一遍历处理所有的bio结构。每个块设备都维护自己的I/O请求队列,I/O调度针对单个I/O请求队列。请求队列的数据结构为request_queue,实质上就是一个双向链表,元素就是request结构,元素排序的顺序由I/O调度算法确定。每个请求队列都有一个允许处理的最大请求数,默认情况下最多有128个读请求和128个写请求,如果请求队列满了则添加I/O请求的进程会被阻塞。块设备驱动程序会周期的(通常是3ms)从请求队列中取出一个待处理的request结构,然后发送合适的命令给设备控制器完成数据读写请求。

     内核提供了四种I/O调度算法,预期算法,最后期限算法,CFQ完全公平队列算法和Noop算法,默认情况为预期算法,可通过内核参数elevator进行再设置。系统管理员也可通过sysfs特殊文件系统动态的调整某个块设备的I/O调度算法。预期算法是最复杂的,演变自最后期限算法,该算法维护四个队列,两个最后期限排序队列和两个扇区排序队列,两种队列包含的读写请求是一样的,默认情况是优先处理扇区排序队列,当最后期限队列中某个请求超时了则优先处理该请求,从请求被传给I/O调度算法开始计时,读请求默认超时时间是125ms,写请求默认超时时间是250ms,这样处理是为了避免扇区排序的请求因排序靠后而饿死。

     块设备驱动程序是Linux块子系统中的最底层组件,从I/O调度中获取待处理请求,然后发送合适的命令给磁盘控制器完成数据读写请求。一个块设备驱动程序可能处理多个块设备,每个磁盘分区都被看做是一个单独的块设备,块设备用block_device结构表示。当内核收到一个打开内核块设备文件的请求时,会通过bdev特殊文件系统判断内存中是否存在对应的块设备描述符,如果存在则更新,否则创建一个新的描述符。   

    以read()系统调用为例,从应用程序执行系统调用到底层磁盘读取数据执行的过程如下图:

   

1、read系统调用的服务例程调用适合的VFS函数

2、VFS函数判断请求的数据是否在磁盘高速缓存中,如果不存在则调用所属的文件系统对应的函数

3、文件系统根据文件路径和文件名找到该文件所属的磁盘分区,具体的磁盘块和权限信息,权限校验通过后,如果该文件属于块设备则将磁盘块的读取请求传给通用块层

4、通用块层将磁盘分区和磁盘块这些信息转换成整个磁盘对应的扇区,将对扇区的读取请求传给I/O调度层

5、I/O调度层对不同进程的I/O请求做适当的合并处理,将合并后的I/O请求传给块设备驱动程序

6、块设备驱动程序找到对应的块设备,发送命令给对应的设备控制器,完成数据读写请求。

参考:Linux通用块层

           Linux IO Scheduler(Linux IO 调度器)

 四、页高速缓存

       页高速缓存是内核使用的主要磁盘高速缓存,绝大多数情况下,内核读写磁盘都要使用高速缓存,除非使用O_DIRECT标志,此时I/O操作会绕过高速缓存而使用用户态进程地址空间的缓冲区,如数据库为了使用自己的磁盘高速缓存算法会利用此标志。 读取数据时如果数据页不在缓存中则从磁盘读取并加到缓存中,写入数据时如果数据页不在缓存中则在缓存中添加一个新页,用待写入的数据填充新页。

      页高速缓存中的每个页所包含的数据肯定属于某个文件,这个文件称为页的所有者。一个数据页对应的磁盘块不一定是物理相邻的,所以不能用设备号和块号来识别,只能通过页所属的索引节点和页在文件中偏移量来识别。

     文件用索引节点i_node表示,该文件对应的页高速缓存用address_space表示,对应于i_node的i_data字段,address_space中的host字段指向其所属的i_node对象,a_ops字段包含了所有操作页的函数;页描述符中的mapping字段指向该页所属的索引节点的address_space,index字段表示在磁盘映像以页大小为单位的偏移量,即页索引,通过这两字段在页高速缓存中查找对应的页。属于同一个address_space的所有高速缓存页以一颗搜索树的形式组织,以加快按页索引查找高速缓存页的效率,其page_tree字段表示搜索树的根,可以通过根节点往下查找到所有属于address_space的高速缓存页。

      搜索树的根节点用radix_tree_root数据结构表示,其中rnode指向第一层子节点,height表示当前树的深度(不包含叶子节点);子节点用radix_tree_node数据结构表示,其中slots表示一个大小为64的指针数组,count表示slots数组中非空节点的数量。最底层的子节点存放指向页描述符的指针(叶子节点),而上层子节点存放指向其他子节点的指针。搜索树的深度会根据加入到树中的页描述符动态调整,如果页描述符对应的页索引超过了当前搜索树的最大索引则增加深度,32位下最大不能超过6。按页索引来查找对应的页描述符时,页索引相当于虚拟地址,查找方式类似于虚拟地址到物理地址的转换,以树的深度为2为例,此时最大索引为64*64即4096,将页索引的低12位中,高的6位表示第一层子节点中的数组索引,低6位表示第二层子节点的数组索引。搜索树的结构如下图:

    

       VFS和文件系统是以块为基本单位读写磁盘数据,每个块都有对应的块缓冲区,且一个块的大小最大不能超过一个页的大小。内核从2.4.10开始,将块缓冲区对应的页放到页高速缓存中管理来提高管理效率,存放块缓冲区的页称为缓冲区页,一个缓冲区页内所有的块缓冲区的大小必须相同。块缓冲区用bufffer_head数据结构表示,称为缓冲区首部描述符,该描述符包含了内核必须了解的如何处理块的所有信息,如块大小,逻辑块号,所属块设备,块缓冲区的位置等。一个缓冲区页下所有块缓冲区对应的块的缓冲区首部都通过一个单向循环链表关联起来,缓冲区页的页描述符的private字段指向第一个块缓冲区首部,块缓冲区首部的b_this_page指向下一个缓冲区首部的地址,b_page字段指向关联的缓冲区页描述符的地址,b_data字段指向块缓冲区在缓冲区页中的位置,其关联关系如下图:

      当内核操作一个块时,会根据缓冲区首部判断对应的块缓冲区是否在页高速缓存中,具体来说根据块设备获取对应的address_space对象,根据块大小和逻辑块号计算页索引,然后查找页索引对应的页是否存在,如果不在则分配一个新的缓存区页并加入到对应块设备的高速缓存页搜索树中。当内核想要获得更多的空闲内存的时候,会尝试回收未被引用的未被改写的缓冲区页。

      当进程向某个文件写入数据时,实际写入的是对应文件的某个块缓冲区,对应的被修改的缓冲区页被标记为脏页,即页描述符的PG_dirty标志被置位,块缓冲区写入结束后执行写的函数立即返回,由内核线程pdflush负责周期性的将脏页或者脏页中已经修改的块缓冲区刷新到磁盘文件中,pdflush线程的数量由内核动态调整,最少2个,最多8个,这种方式称为延迟写策略,从而将进程的多次写入合并成一次缓慢的物理更新,提高磁盘的写入效率。其存在的问题是:

  •    如果发生硬件错误或者断电,则尚未刷新到磁盘的脏页中的修改会丢失
  •    页高速缓存会因为块设备的增大而增大,占用较多内存空间

 因此在下列条件下执行脏页刷新:

  • 页高速缓存占用的内存空间达到限制
  • 脏页的数量过多或者变成脏页的时间太长
  • 进程调用sync(),fsync()等系统调用主动刷新脏页

五、访问文件

访问文件有多种,主要有以下几种:

1、规范模式

     规范模式下打开文件后,标志O_SYNC和O_DIRECT清0,读数据通过read()系统调用完成,会阻塞进程直到数据被拷贝进用户态地址空间内;写数据通过write()系统调用完成,在数据被写入对应的高速缓存页中就立即返回。

2、同步模式

    同步模式下打开文件,标志O_SYNC置1,读数据同规范模式,写数据时会阻塞进程直到修改的数据被正确写入到磁盘中。

3、内存映射模式

   内存映射模式下文件被打开后会执行mmap()系统调用将文件映射至内存,对文件的读写变成对内存的读写,不需要正常的read(),write()等文件读写系统调用。内存映射采用请求调页机制分配页框,即刚创建的内存映射对应的线性区是没有分配任何页框的,进程访问页框中的数据触发缺页异常,由缺页异常处理程序负责分配页框并从磁盘读取对应文件内容填充页框。

4、直接I/O模式

   直接I/O模式下文件被打开后标志O_DIRECT被置1,任何读写操作都将数据在用户态地址空间和磁盘之间传递而绕过页高速缓冲。

5、异步模式

   异步模式下文件被打开后主进程不会被阻塞,而是另起一个子进程执行规模模式下的文件读写,读写过程中数据缓存在位于用户态地址空间内的AIO环形内存缓冲区,最后将结果通知给父进程,可通过POSIX API库函数或者Linux 特有的系统调用实现。

   内核读文件是以页为单位的,当进程执行read()系统调用读取部分字节时,内核计算出被请求数据所处的页索引,此时如果有预读的页则开始预读,然后在页高速缓存中查找是否存在对应的页,如果不存在则分配一个新的页框并将其加入到页高速缓存中,接着调用address_page对象中的readpage方法完成磁盘文件到新页的I/O数据传输。这个过程是以页为单位循环进行的,直到所有待读取的数据页都已经在页高速缓存中,最后为出于内核空间的高速缓存页建立永久内核映射并把页中的数据拷贝到用户态地址空间中,拷贝完成解除永久内核映射,主要流程由do_generic_file_read()函数实现。

    执行readpage()方法表示进入了通用块层,会根据块大小和页大小计算待读取页中包含的所有块及其文件块号,对每一块调用文件系统的get_block方法得到逻辑块号,即相对于磁盘或者分区开始位置的块索引,然后分配一个新的bio描述符并初始化,将其提交到I/O调度程序然后返回,读取数据完成时I/O调度程序会回调mpage_end_io_read()方法,更新被填充页的标志位,唤醒相关的睡眠进程。如果读取的块在磁盘上是连续的则使用一个bio.,一次读取,如果不是连续的则使用多个bio,逐一多次读取。注意如果打开的文件是bdev特殊文件系统中的块设备文件时则对块的操作会转换成对缓冲区首部的操作。

    文件预读是指进程请求某个数据页时,内核会自动将其相邻的多个数据页读入页高速缓存中的技术。在顺序访问的前提下,预读可以极大提高磁盘的效率,一次磁盘交互会读取一大组相邻的扇区,同时因为程序接下来可能要读取的文件因为预读已经在内存中了可以提高系统的响应能力。在随机访问下预读是有害的,因为预读会导致无用的数据浪费了页高速缓存的内存空间。当访问给定文件时,预读算法使用两个页面集,当前窗和预读窗,各对应于文件的一个连续区域。当前窗内的页是进程请求的页和内核预读的页,位于页高速缓存中,预读窗内的页紧挨着当前窗的页,是内核正在预读的页,预读窗内的页都不是进程请求的而是内核假定进程会很快请求的页。当内核认为是顺序访问且请求的第一页就在当前窗内就检查是否建立了预读窗,如果没有则创建一个预读窗并处罚相应页的读操作。理想情况下,进程从当前窗请求页时,预读窗的页正在传送。文件的最大预读量默认是32页,可以通过系统调用posix_fadvise()来改变。

    内核写文件时会先获取对应文件的索引节点的信号量,借此确保一次只能有一个进程对某个文件发出write()系统调用,然后调用_generic_file_aio_write_nolock()函数将相关的页标记为脏页,标记完成释放信号量,此时如果文件设置了O_SYNC标志则阻塞调用进程直到脏页全部刷新到磁盘,主要流程在genaric_file_write()函数中。

    _generic_file_aio_write_nolock()函数会检查文件大小,确保修改后的文件大小不超过最大文件大小,更新文件的修改时间,然后循环更新涉及到的所有相关页。每次循环时,会检查待写入的页是否在页高速缓存中,如果不在则分配一个新页并在页高速缓存中插入此页,接着将待写入的数据拷贝到页中,最后将该页标记为脏页,标记完成会检查此时页高速缓存中脏页的比例是否超过一个固定的阈值,默认为40%,如果是则刷新脏页到磁盘上。

    把脏页刷新到磁盘上通过address_space对象的writepages方法实现,最终调用mpage_writepages()方法,该方法会在页高速缓存中查找所有的脏页,然后循环处理所有的脏页。每次循环时,会先锁定该页,接着将页对应的文件块号转换成逻辑块号,如果不存在该文件对应的bio则分配一个bio,接着将该bio提交到I/O调度程序中,I/O写入完成则唤醒等待页传输结束的进程并清除bio描述符。

   内存映射有两种类型,由进程调用mmap()方法时指定的MAP_SHARED或者MAP_PRIVATE标志来决定:

  •  共享型:在线性区的页的写操作都会修改磁盘文件对应的数据页,且修改结果对所有对文件做了共享映射的进程都可见。共享内存映射的页都在页高速缓存中。
  •  私有型:当进程创建的映射只是为读文件而不是写文件时使用,进程可以修改线性区的页,但是修改只对修改进程可见,且不会修改磁盘文件对应的数据页,进程未修改的页会随着其他进程对文件的修改而更新。私有内存映射下未修改的页在页高速缓存中,一旦进程修改了页,内核就将其复制一份,在进程页表中用复制页替换原来的页并在复制页上完成修改,复制页不插入到页高速缓存中,即典型的写时复制技术的应用。

   创建内存映射的核心流程由文件对象的mmap实现,该属性对应的方法通常是generic_file_map()函数实现,该函数有两个参数,文件对象地址和线性区描述符地址。创建内存映射时会检查文件对象的mmap字段是否为NULL,如果是则表示该文件不支持内存映射。

      内存映射的请求调页逻辑由do_no_page()方法实现,核心由nopage()方法实现,通常是根据请求数据的虚拟地址确定对应数据在文件中的偏移量,接着在页高速缓存中查找对应的页是否存在,如果不存在则分配一个新页框,将其加入到页高速缓存,如果已存在,则锁定该页;最后调用readpage方法从磁盘读入或者更新该页内容。读磁盘的时候如果标记是顺序读则会触发预读。

     将内存映射的脏页刷新到磁盘可以通过msync()系统调用主动触发,也可由pdflush内核线程周期性的触发。msync()系统调用会扫描线性区地址区间对应的页表项,将其标记为脏页,并刷新TLB,如果MS_ASYNC标志置位则返回,如果是MS_SYNC标志置位则需要调用mpage_writepages()方法将脏页刷新到磁盘上,调用进程会一直睡眠知道I/O传输完成。

   上面描述的内存映射都是线性内存映射,即内存映射的是文件的顺序页,Linux内核还提供了非线性内存页,即内存映射的是文件的随机页。使用时先调用mmap()创建一个常规的内存映射,接着调用remap_file_pages()来重新映射指定的页,重新映射主要是改变对应页的页表项。两者的内存页都是按照相对于文件开始处的页索引存放在页高速缓存中,刷新脏页到磁盘上的方式一样。

    直接I/O传送是Linux提供的绕过页高速缓存,直接在用户态地址空间的页与磁盘之间传输数据的文件访问方式,内核保证当调用进程被调度切换了而I/O传输还在进行中,此时相关的高速缓存页不会被交换到磁盘上。具体读写过程跟常规模式相比就是磁盘文件数据传送的目的地不一样,其他的相同,需要将操作的数据通过通用块层转换成对应的块,然后提交到I/O调度程序。
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值