从ramfs分析文件系统的设计和实现

作者:杨红刚,兰州大学

邮箱:eagle.rtlinux@gmail.com / rtlinux@163.com

--------------------------------------------------------------------


REF:
1. linux-3.6.9/fs/ramfs
2. Documentation/filesystems/ramfs-rootfs-initramfs.txt


目录:

0. 文件系统各个数据结构之间的联系图

1. 分析了一个最简单的Linux文件系统ramfs的实现

   分析了最简单的文件系统ramfs的实现。

2. 最小文件系统设计小结

    总结对ramfs的分析,得出最小文件系统的设计实现关键。

3. 最小文件系统实例

    该部分基本上复制了ramfs的实现,通过该过程和修改相关的函数实现,可以研究

     每个函数的作用。加深对fs的理解。


-------------------------------------------------------------------------------------------------------------------

第零章 文件系统各个数据结构之间的联系图

在开始之前以ext4为例子先给出整个文件系统的数据结构之间的关系图。通过以后的

循序渐进的理解,最后就能理解整个图的含义了。高清图下载




第一章 分析了一个最简单的Linux文件系统ramfs的实现

ramfs展示了怎样设计一个虚拟文件系统(virtual filesystem)。
它实现了一个符合POSIX规范的文件系统的所有的逻辑。
该文件系统没有实现独立的数据结构,仅仅利用了VFS中已经定义
结构。

ramfs将Linux磁盘缓冲导出为一个可动态调整大小的基于RAM的文件
系统。ramfs没有后备存储源。向ramfs中进行的文件写操作也会
分配目录项和页缓存,但是数据并不写回到任何其他存储介质上。
这意味着涉及的内存页不会被标记为“干净”状态,这样VM就不会
回收分配给ramfs的内存。

由于ramfs的实现完全基于已经存在的Linux缓冲机制,所以其代码
很少(不超过634行)。


首先,通过查看Makefile文件检查ramfs依赖的模块。
obj-y += ramfs.o

file-mmu-y := file-nommu.o
file-mmu-$(CONFIG_MMU) := file-mmu.o
ramfs-objs += inode.o $(file-mmu-y)

可见ramfs-objs由inode.c,如果CONFIG_MMU配置为Y,那么
还包括file-mmu.c,否则,包含file-nommu.c。
下面的讨论假定CONFIG_MMU为Y。

--- include/linux/ramfs.h -----
该文件中为ramfs模块对其他模块的接口。包括:

    函数
    ramfs_get_inode()
    ramfs_mount()
    init_rootfs()
    ramfs_fill_super()

    数据结构
    const struct file_operations ramfs_file_operations
    vm_operations_struct generic_file_vm_ops


------------------------------
---- fs/ramfs/file-mmu.c -----
------------------------------

该文件中为ramfs_file_operations、ramfs_aops和ramfs_file_inode_operations
的具体初始化。

const struct address_space_operations ramfs_aops = {
    .readpage   = simple_readpage,
    .write_begin    = simple_write_begin,
    .write_end  = simple_write_end,
    .set_page_dirty = __set_page_dirty_no_writeback,
};
该结构中为ramfs的“地址空间”结构。"地址空间"结构,
将缓存数据与其来源之间建立关联。该结构用于ramfs向内存模块申请内存页,并提供
从后备数据来源读取数据并填充缓冲区以及将缓冲区中的数据写入后备设备等操作。


const struct file_operations ramfs_file_operations = {
    .read       = do_sync_read,
    .aio_read   = generic_file_aio_read,
    .write      = do_sync_write,
    .aio_write  = generic_file_aio_write,
    .mmap       = generic_file_mmap,
    .fsync      = noop_fsync,
    .splice_read    = generic_file_splice_read,
    .splice_write   = generic_file_splice_write,
    .llseek     = generic_file_llseek,
};
该结构为ramfs中文件的通用文件操作函数集合。

const struct inode_operations ramfs_file_inode_operations = {
    .setattr    = simple_setattr,
    .getattr    = simple_getattr,
};
尬结构包括ramfs的文件节点操作函数集合。


-------------------------
---- fs/ramfs/inode.c ---
-------------------------

ramfs的模块初始化函数:
static int __init init_ramfs_fs(void);
调用内核函数register_filesystem()注册ramfs。
关于ramfs的描述结构rootfs_fs_type的初始化如下:
static struct file_system_type ramfs_fs_type = {
    .name       = "ramfs",
    .mount      = ramfs_mount,
    .kill_sb    = ramfs_kill_sb,
};
"ramfs"为文件系统的名字。
ramfs_mount为ramfs的挂载操作。
ramfs_kill_sb为不再需要ramfs文件系统时执行相关清理工作。


------------------------------------------------------
作用:初始化ramfs的超级块。
注释:包括挂载参数,根inode和根目录项等的初始化。
int ramfs_fill_super(struct super_block *sb, void *data, int silent);

 [1] 将文件系统的挂载参数保存到sb.s_options字段。该值用于generic_show_options()
函数对文件系统挂载参数的显示操作。
 [2] 分配ramfs特有的ramfs_fs_info结构。其原型为:

struct ramfs_mount_opts {
    umode_t mode;
};

struct ramfs_fs_info {
    struct ramfs_mount_opts mount_opts;
};
 [3] 指向ramfs_fs_info结构的指针存放在ramfs的超级块的私有字段s_fs_info中。
 [4] 调用ramfs_parse_options()对挂载参数进行解析。将挂载参数“mode=XX”中的
   XX保存在ramfs_mount_opts的mode字段。默认值为0755。其他挂载参数都被忽略。
 [5] 初始化超级块的s_maxbytes为MAX_LFS_FILESIZE。该字段表示最大的文件长度。
   初始化超级块的s_blocksize字段为PAGE_CACHE_SIZE。该字段表示文件系统块的长度,
   单位为字节。另一个字段为s_blocksize_bits字段,该字段也表示文件系统块的长度,
   只是它为s_blocksize取以2为底的对数。
     初始化超级块的s_magic字段,该字段为超级块的魔数,用于检查超级块的损坏。
   初始化超级块的s_op字段为ramfs_ops。该结构包含了用于处理超级块的相关操作。
 
static const struct super_operations ramfs_ops = {
    .statfs     = simple_statfs, //给出文件系统的统计信息,例如使用和未使用的数据块的数目,或者文件件名的最大长度。  
    .drop_inode = generic_delete_inode, //当inode的引用计数降为0时,将inode删除。
    .show_options   = generic_show_options, //用以显示文件系统装载的选项。
};

     初始化超级块的s_time_gran字段。它表示文件系统支持的各种时间戳的最大可能的粒度。单位为ns。
 [6] 调用ramfs_get_inode()为ramfs超级块在生成一个代表根节点inode。并将inode各个字段进行初始化。最后,返回
   指向该根inode的指针。(具体操作详见下文分析。)
 [7] 调用d_make_root()为根inode创建并初始化一个目录项。

------------------------------------------------------
作用:查找或者创建一个VFS超级块super_block结构。执行ramfs_fill_supper()对ramfs的超级
   块进行初始化。然后,对根目录的目录项引用计数增一。
struct dentry *ramfs_mount(struct file_system_type *fs_type,
        int flags, const char *dev_name, void *data);

-----------------------------------------------

static void ramfs_kill_sb(struct super_block *sb)

[1] 释放ramfs私有的数据结构ramfs_fs_info。
[2] 调用kill_litter_super()执行对超级块的清理操作。


------------------------------------------------------
struct inode *ramfs_get_inode(struct super_block *sb,
                const struct inode *dir, umode_t mode, dev_t dev)

[1] 调用new_inode()创建一个符合ramfs类型的inode结构。
[2] 初始化inode的编号。
  初始化inode的uid, gid, mode字段。
  初始化inode的“地址空间”的操作集合为ramfs_aops,该操作集合和ramfs文件内容读写相关。
    初始化inode的后备存储设备的有关信息为ramfs_backing_dev_info。后备存储设备是与“地址空间”相关的外部设备,是数据的来源和去向。

    为inode的flags字段添加GFP_HIGHUSER标记。flags字段标志集主要用于保存映射页所来自的GFP内存区的有关信息。该字段也可以
  用来保存异步传输期间发生的错误信息,在异步传输期间错误无法直接传递给调用者。AS_EIO表示一般性I/O错误,AS_ENOSPC表示没有足够的空间
  来完成一个异步写操作。GFP_HIGHUSER表示保存内存页的物理内存优先从高端内存区域获取。
    
    为inode的flags添加AS_UNEVICTABLE标志。这样ramfs涉及的内存页就会放入unevictable_list中,这些内存页不会再被回收。
    
    初始化inode的i_atime, i_mtime, i_ctime为当前时间。

[3] 根据mode的设置为inode设置四种不同操作集:
    - REG//普通文件
        设置inode的i_op为ramfs_file_inode_operations操作集。
    设置inode的i_fop为ramfs_file_operations操作集。    
    - DIR//文件夹
        设置inode的i_op为ramfs_dir_inode_operations操作集。
    设置inode的i_fop为simple_dir_operations操作集。
    - LINK//链接    
        设置i_op为page_symlink_inode_operations操作集。
[4] 将初始化好的inode的指针返回。

--------- 地址空间操作 -------------------
const struct address_space_operations ramfs_aops = {
    .readpage   = simple_readpage,
    .write_begin    = simple_write_begin,
    .write_end  = simple_write_end,
    .set_page_dirty = __set_page_dirty_no_writeback,
};
simple_readpage: 用于从后备存储器将一页数据读入页框。对于ramfs没有后备存储器,只有RAM。

simple_write_begin:
  根据文件的读写位置pos计算出文件的页偏移值。
    根据索引查找或者分配一个page结构。
  将页中数据初始化为“0”。

simple_write_end: 在对page执行写入操作后,执行相关更新操作。

__set_page_dirty_no_writeback: 将某个页标记为“脏”。但是并不执行写回操作,
  因为ramfs不需要写回到磁盘。这也是为什么ramfs重写挂载后,之前写入的数据丢失的原因。


------------ 后备存储器 -----------------
static struct backing_dev_info ramfs_backing_dev_info = {
    .name       = "ramfs",
    .ra_pages   = 0,    /* No readahead */
    .capabilities   = BDI_CAP_NO_ACCT_AND_WRITEBACK |
              BDI_CAP_MAP_DIRECT | BDI_CAP_MAP_COPY |
              BDI_CAP_READ_MAP | BDI_CAP_WRITE_MAP | BDI_CAP_EXEC_MAP,
};
该结构描述了作为数据来源和去向的后备存储设备的相关描述信息。

ra_pages: 设置了最大预读数量。
BDI_CAP_NO_ACCT_AND_WRITEBACK 不需要写回、不执行脏页统计、不自动计算回写页统计。
BDI_CAP_MAP_DIRECT 可以直接映射。
BDI_CAP_MAP_COPY 拷贝可以被映射。
BDI_CAP_READ_MAP 可以映射读取。
BDI_CAP_WRITE_MAP 可以映射写。
BDI_CAP_EXEC_MAP 可以被映射执行代码。

----------- 文件节点操作集 ------------

const struct inode_operations ramfs_file_inode_operations = {
    .setattr    = simple_setattr,
    .getattr    = simple_getattr,
};

simple_setattr: 一个简单的属性设置函数。仅仅针对内存文件系统或者特殊的文件系统。
  如果需要在文件大小改变时也对磁盘上的元数据进行修改那么需要文件系统提供相关
  的修改方法。
simple_getattr: 获取文件的相关属性信息,如ino,mode,nlink, uid, gid 等等。

------------- 文件操作集 ----------------

const struct file_operations ramfs_file_operations = {
    .read       = do_sync_read,
    .aio_read   = generic_file_aio_read,
    .write      = do_sync_write,
    .aio_write  = generic_file_aio_write,
    .mmap       = generic_file_mmap,
    .fsync      = noop_fsync,
    .splice_read    = generic_file_splice_read,
    .splice_write   = generic_file_splice_write,
    .llseek     = generic_file_llseek,
};

全部为VFS层提供的通用的文件操作方法。

------------ 文件夹节点操作集 -------

static const struct inode_operations ramfs_dir_inode_operations = {
    .create     = ramfs_create,
    .lookup     = simple_lookup,
    .link       = simple_link,
    .unlink     = simple_unlink,
    .symlink    = ramfs_symlink,
    .mkdir      = ramfs_mkdir,
    .rmdir      = simple_rmdir,
    .mknod      = ramfs_mknod,
    .rename     = simple_rename,
};  

static int ramfs_create(struct inode *dir, struct dentry *dentry, umode_t mode, bool excl)
        :在目录@dir下,创建一个普通文件,并在@dir下创建一个和文件关联的目录项@dentry。

simple_lookup:通用的查找操作。根据文件系统对象的名字(字符串)查找对应的inode实例。???
   为目录项设置d_delete字段为simple_delete_dentry()。
   并将目录项添加到目录项的hash表上,方便快速查找。
int simple_link(struct dentry *old_dentry, struct inode *dir, struct dentry *dentry)
   创建硬链接。创建从@dir下的@dentry目录项到@old_dentry目录项管理的inode的硬链接。
simple_unlink: 为ssimple_link()的逆操作。

static int ramfs_symlink(struct inode * dir, struct dentry *dentry, const char * symname)
  在目录@dir下新建并初始化一个链接inode,链接的路径名为@symname。

static int ramfs_mkdir(struct inode * dir, struct dentry * dentry, umode_t mode)
  在目录@dir下,创建一个目录项inode并和@dentry相关联。
simple_rmdir: ramfs_mkdir的逆操作。

ramfs_mknod: 在@dir下分配和初始化一个设备inode,并和@dentry相关联。 


------------ simple_dir_operations ----------
const struct file_operations simple_dir_operations = {
    .open       = dcache_dir_open,
    .release    = dcache_dir_close,
    .llseek     = dcache_dir_lseek,
    .read       = generic_read_dir,
    .readdir    = dcache_readdir,
    .fsync      = noop_fsync,
};  

ramfs文件夹操作直接使用了simple_dir_operations。


第二章 最小文件系统设计小结




********** 小结:最小文件系统设计

1. 需要的操作

   - 文件系统注册
     使用register_filesystem()向内核注册文件系统。
   - 文件系统卸载
     使用unregister_filesystem()来卸载文件系统。

2. 需要提供的数据结构

   在使用register_filesystem()向内核注册文件系统时,需要提供一个[1] struct file_system_type的实例,
   该实例描述了文件系统的基本信息。主要包括@name,文件系统的名字。@mount文件系统的挂载相关的操作,
   挂载时需要提供初始化超级块(super_block)的操作。@kill_sb提供了对超级块的清理操作。
 
   在初始化文件系统超级块时需要指定超级块相关的操作集合。这需要填充一个[2] struct super_operations结构。
   主要有@statfs,用于给出文件系统的统计信息,例如未使用的数据块的数目、文件名的最大长度等。
   @drop_inode,用于当inode的引用计数降为0时,将inode删除。
   @show_options,用于显示文件系统的装载选项。

   在分配新的inode时,需要初始化inode的相关操作。这需要初始化一个[3] struct address_space_operations。
   该结构为“地址空间”的操作集。“地址空间”建立了内存中数据和其数据来源之间的关联。当需要更多的物理
   内存时,负责向内存模块申请内存。并提供从后备数据来源读取数据并填充内存缓冲区以及将更新后的数据
   写回到后备设备等操作。其中@readpage用于将一页数据读取页框。通常初始化为内核的标准函数mpage_readpage。
   在ramfs中初始化为simple_readpage。@write_begin函数被通用的缓冲区写函数调用,用来告知文件系统
   准备好向文件的特定偏移处写若干个字节的操作。文件系统根据需要分配相关的内存空间等等。
   在成功地进行了@readpage和数据拷贝后,必须调用@write_end。
   @set_page_dirty被VM调用,来将一个页标记为脏。应该设置页的PageDirty标记和基树的PAGECACHE_TAG_DIRTY的标记。

   [4] struct backing_dev_info包含了与“地址空间”相关的后备存储器的有关信息。后备存储器是指与地址空间
   相关的外部设备,用作地址空间中数据的来源,通常为块设备。@name是后备存储设备的名字。@ra_pages为最大预读
   的数量,单位为PaGE_CACHE_SIZE。@capabilities中最重要的信息是数据页是否可以回写。比如,BDI_CAP_NO_ACCT_AND_WRITEBACK
   表示不需要回写、不执行脏页统计、不自动计算回写页统计。

   [5] struct inode_operations 为文件夹节点相关的操作集。主要包括在指定目录下创建/删除普通文件、设备文件、目录文件、
   链接文件和查找以及重命名等相关操作。
   [6] struct file_operations 文件夹相关的操作集合。

   [7] struct inode_operations 为文件节点相关的操作集。
   [8] struct file_operations 文件相关的操作集。


第三章 最小文件系统实例


*********** 最小文件系统实例


wendyfs是为了方便读者快速掌握Linux文件系统的设计而从ramfs中抽取出来的一个基于RAM的文件系统。具体实现
参见wendyfs.c。使用方法相见README。
在实现中注释掉了wendyfs_dir_inode_operations中的.rename操作,这样wendyfs中就无法使用重命名操作。比如,
mv fileA fileB。这样可以研究每一个方法的作用。

//wendyfs.c

/*
   * 本模块代码大部分拷贝自fs/ramfs/inode.c和fs/ramfs/file-mmu.c。
   * 主要实现了一个RAM文件系统的基本功能(除了文件重命名 mv fileA fileB)
   * 本模块可以帮助我们学习Linux文件系统。 
*/


#include <linux/fs.h> /* super_operations */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/pagemap.h> /*mapping_set_unevictable() */
#include <linux/time.h> /* CURRENT_TIME */ 
#include <linux/backing-dev.h> 



//should be defined in the heaeder files
#define WENDYFS_MAGIC     0xa5a5a5a5


struct inode *wendyfs_get_inode(struct super_block *sb,
                const struct inode *dir, umode_t mode, dev_t dev);
int myset_page_dirty_no_writeback(struct page *page);


/* Wendyfs's address space operations */
const struct address_space_operations wendyfs_aops = {
		.readpage	= simple_readpage,
		.write_begin	= simple_write_begin,
		.write_end		= simple_write_end,
		.set_page_dirty	= myset_page_dirty_no_writeback,
};

static struct backing_dev_info wendyfs_backing_dev_info = { 
    .name       = "wendyfs",
    .ra_pages   = 0,    /* No readahead */
    .capabilities   = BDI_CAP_NO_ACCT_AND_WRITEBACK |
              BDI_CAP_MAP_DIRECT | BDI_CAP_MAP_COPY |
              BDI_CAP_READ_MAP | BDI_CAP_WRITE_MAP | BDI_CAP_EXEC_MAP,
};


/*
 * For address_spaces which do not use buffers nor write back.
 */
int myset_page_dirty_no_writeback(struct page *page)
{
    if (!PageDirty(page))
        return !TestSetPageDirty(page);
    return 0;
}


/* File creation */
static int wendyfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
	int error = -ENOSPC;
	struct inode* inode = wendyfs_get_inode(dir->i_sb, dir, mode, dev);

	if (inode) {
		d_instantiate(dentry, inode);
		dget(dentry);   /* Extra count - pin the dentry in core */
		error = 0;
		dir->i_mtime = dir->i_ctime = CURRENT_TIME;
	}

	return error;
}

static int wendyfs_mkdir(struct inode * dir, struct dentry * dentry, umode_t mode)
{
	int retval = wendyfs_mknod(dir, dentry, mode | S_IFDIR, 0); // DIR inode
  	if (!retval)
			inc_nlink(dir);

	return retval;
}


static int wendyfs_create(struct inode *dir, struct dentry *dentry, umode_t mode, bool excl)
{
    return wendyfs_mknod(dir, dentry, mode | S_IFREG, 0); 
}


static const struct inode_operations wendyfs_dir_inode_operations = {
	.create		= wendyfs_create,
	.lookup		= simple_lookup,
	.mkdir		= wendyfs_mkdir,
	.rmdir		= simple_rmdir,
	.link		= simple_link,
	.unlink		= simple_unlink,
//	.rename		= simple_rename, 
};


const struct inode_operations wendyfs_file_inode_operations = {
	.setattr	= simple_setattr,
	.getattr	= simple_getattr,
};


const struct file_operations wendyfs_file_operations = {
    .read       = do_sync_read,
    .aio_read   = generic_file_aio_read,
    .write      = do_sync_write,
    .aio_write  = generic_file_aio_write,
    .fsync      = noop_fsync,
    .llseek     = generic_file_llseek,
};

struct inode *wendyfs_get_inode(struct super_block *sb,
                const struct inode *dir, umode_t mode, dev_t dev) 
{
	/* Allocate one inode */
    struct inode * inode = new_inode(sb); 

	/* Init the inode */
    if (inode) {
		inode->i_ino = get_next_ino();

		/* Init uid,gid,mode for new inode according to posix standards */
	    inode_init_owner(inode, dir, mode);
		
		/* Set the address space operation set */
		inode->i_mapping->a_ops = &wendyfs_aops;
		
		/* Set the backing device info */
		inode->i_mapping->backing_dev_info = &wendyfs_backing_dev_info;
		
		/* The pages wendyfs covered will be placed on unevictable_list. So
		   these pages will not be reclaimed. */
		mapping_set_unevictable(inode->i_mapping);
		inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
		
		/* Set inode and file operation sets */
		switch (mode & S_IFMT) {
		default: 
				init_special_inode(inode, mode, dev);
				break;
		case S_IFDIR:
				/* dir inode operation set */
				inode->i_op = &wendyfs_dir_inode_operations;
				/* dir operation set */
				inode->i_fop = &simple_dir_operations;
				inc_nlink(inode);
				break;
		case S_IFREG:
				/* regular file inode operation set */
				inode->i_op = &wendyfs_file_inode_operations;
				/* regular file operation set */
				inode->i_fop = &wendyfs_file_operations;
				break;
		}
	}

	return inode;
}

/* Super Block related operations */
static const struct super_operations wendyfs_ops = { 
    .statfs     = simple_statfs,
    .drop_inode = generic_delete_inode,
    .show_options   = generic_show_options,
};


int wendyfs_fill_super(struct super_block* sb, void* data, int silent)
{
	struct inode *inode;

	sb->s_maxbytes		= 4096;
	sb->s_blocksize		= 4096;
	sb->s_blocksize_bits	= 12;
	sb->s_magic			= WENDYFS_MAGIC;
	/* Set super block operations */
	sb->s_op			= &wendyfs_ops;
	sb->s_time_gran		= 1;

	/* Create and initialize the root inode */
	inode = wendyfs_get_inode(sb, NULL, S_IFDIR, 0);
	sb->s_root = d_make_root(inode);
	if (!sb->s_root)
			return -ENOMEM;

	return 0;
}



struct dentry* wendyfs_mount(struct file_system_type *fs_type,
				int flags, const char* dev_name, void* data)
{
	return mount_nodev(fs_type, flags, data, wendyfs_fill_super);
}

static void wendyfs_kill_sb(struct super_block* sb)
{
	kill_litter_super(sb);
}

static struct file_system_type	wendy_fs_type = {
		.name		= "wendyfs",
		.mount		= wendyfs_mount,
		.kill_sb	= wendyfs_kill_sb,
};


static int __init init_wendy_fs(void)
{
		return register_filesystem(&wendy_fs_type);
}

static void __exit exit_wendy_fs(void)
{
	unregister_filesystem(&wendy_fs_type);
}


module_init(init_wendy_fs)
module_exit(exit_wendy_fs)

MODULE_AUTHOR("Yang Honggang, <eagle.rtlinux@gmail.com>");
MODULE_LICENSE("GPL");

//README

Howto use:
0. Compile wendyfs module
   $make

1. Insert wendyfs module
	#insmod wendyfs.ko
2. Check the filesystem list
    #cat /proc/filesystems
    ...
	nodev	wendyfs
3. Mount the wendyfs 
    #mount -t wendyfs none /mnt
4. Create a dir in the mount point
    #cd /mnt
    #mkdir hello
5. Delete the hello dir
    #rm -rf hello
6. Create a file and rename it
   #echo "hello" > halo
   #cat halo
   #mv halo hello
   mv: cannot move `halo' to `hello': Operation not permitted
   ...


Makefile

obj-m := wendyfs.o
default:
	make -C /lib/modules/`uname -r`/build M=`pwd` modules	
clean:
	rm modules.order Module.symvers *.ko *.mod.* *.o .tmp_versions/ .*cmd -rf



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值