Linux设备驱动编程模型之上层容器篇

2.6内核增加了一个引人注目的新特性——统一设备模型(device model)。设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,从而使得系统具有以下优点:
l        代码重复最小化。
l        提供诸如引用计数这样的统一机制。
l        可以列举系统中所有的设备,观察它们的状态,并且查看它们连接的总线。
l        可以将系统中的全部设备结构以树的形式完整、有效的展 现出来——包括所有的总线和内部连接。
l        可以将设备和其对应的驱动联系起来,反之亦然。
l        可以将设备按照类型加以归类,比如分类为输入设备,而无需理解物理设备的拓扑结构。
l        可以沿设备树的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源。
   最后一点是实现设备模型的最初动机。若想在内核中实现智能的电源管理,就需要来建立表示系统中设备拓扑关系的树结构。当在树上端的设备关闭电源时,内核必须首先关闭该设备节点以下的(处于叶子上的)设备电源。比如内核需要先关闭一个USB鼠标,然后才可关闭USB控制器;同样内核也必须在关闭PCI总线前先关闭USB控制器。简而言之,若要准确而又高效的完成上述电源管理目标,内核无疑需要一颗设备树。

一、原理

虽然设备模型的初衷是为了方便电源管理而提供出的一种设备拓扑结构,但是为了方便调试,设备模型的开发者决定将设备结构树导出为一个文件系统,这就是sysfs文件系统,它可以帮助用户能以一个简单文件系统的方式来观察系统中各种设备的拓扑结构。这个举措很快被证明是非常明智的,首先sysfs代替了先前处于/proc下的设备相关文件;另外它为系统对象提供了一个很有效的视图。实际上,sysfs起初被称为driverfs。最终sysfs使得我们认识到一个全新的对象模型非常有利于系统。今天所有2.6内核的系统都拥有sysfs文件系统,而且几乎都毫无例外的将其挂载。
             图一是挂载于/sys目录下的sysfs文件系统的局部视图。
    /sys
     |—block/
     |   |–fd0
     |   |–hda
     |   |–dev
     |   |–device->../../devices/pci0000:00/0000:00:1f.1/ide0/0.0
     |    …
     |–bus/
     |–class/
     |–devices/
     |–firmware/
     |–power/
     |–module/
Sysfs的根目录下包含了七个目录:blockbusclass,devicesfirmware, ,module和power。block目录下的每个子目录都对应着系统中的一个块设备。反过来,每个目录下又都包含了该块设备的所有分区。bus目录提供了一个系统总线视图。class目录包含了以高层功能逻辑组织起来的系统设备视图。devices目录是系统中设备拓扑结构视图,它直接映射出了内核中设备结构体的组织层次。firmware目录包含了一些诸如ACPI,EDD,EFI等低层子系统的特殊树。power目录包含了系统范围的电源管理数据。
其中最重要的目录是devices,该目录将设备模型导出到用户空间。目录结构就是系统中实际的设备拓扑。其它目录中的很多数据都是将devices目录下的数据加以转换加工而得。比如,/sys/class/net/目录是以注册网络接口这一高层概念来组织设备关系的,在这个目中可能会有目录eth0,它里面包含的devices文件其实就是一个指回到devices下实际设备目录的符号连接。
   随便看看任何你可访问到的Linux系统,这种系统设备视图相当准确和漂亮,而且可以看到class中的高层概念与devices中的低层物理设备,以及bus中的实际驱动程序之间互相联络是非常广泛的。

Sysfs文件系统的目标就是要展现设备驱动模型组件之间的层次关系。在Linux中,sysfs文件系统被安装于/sys目录下:

mount -t sysfs sysfs /sys
/sys
     |—block/
     |   |–fd0
     |   |–hda
     |   |–dev
     |   |–device->../../devices/pci0000:00/0000:00:1f.1/ide0/0.0
     |    …
     |–bus/
     |–class/
     |–devices/
     |–firmware/
     |–power/
     |–module/

那么,在这样的目录树中,哪些目录是驱动模型要关注的对象?
bus-系统中用于连接设备的总线,在内核中对应的结构体为 struct bus_type { …  };
device-内核所识别的所有设备,依照连接它们的总线对其进行组织, 对应的结构体为struct device { …  };
class-系统中设备的类型(声卡,网卡,显卡,输入设备等),同一类中包含的设备可能连接到不同的总线, 对应的结构体为 struct class { …  };
为什么不对Power进行单独描述?实际上,Power与device有关,它只是device中的一个字段。
除此之外,立马闪现在我们脑子里的对象还有:
driver-在内核中注册的设备驱动程序, 对应的结构体为
struct device_driver{ …  };
以上bus,device,class,driver,是可以感受到的对象,在内核中都用相应的结构体来描述。而实际上,按照面向对象的思想,我们需要抽象出一个最基本的对象,这就是设备模型的核心对象kobject.
   Kobject Linux 2.6引入的新的设备管理机制,在内核中就是一个struct kobject结构体。有了这个数据结构,内核中所有设备在底层都具有统一的接口,kobject提供基本的对象管理,是构成Linux2.6设备模型的核心结构,它与sysfs文件系统紧密关联,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。Kobject是组成设备模型的基本结构。类似于C++中的基类,它嵌入于更大的对象中,即所谓的容器,如上面提到的bus,classdevices, drivers 都是典型的容器,它们是描述设备模型的组件。

二、主要对象介绍

1,核心对象kobject

话说kobject是驱动模型的核心对象,但在sysfs文件系统中似乎并没有对应的项,而这种看似“无”,实际上蕴藏着“有”。
这“有”从何说起。回想文件系统中的核心对象“索引节点(indoe)”和目录项“dentry
Inode与文件系统中的一个文件相对应(而实际上,只有文件被访问时,才在内存创建索引节点)。
Dentry-每个路径中的一个分量,例如路径/bin/ls,其中/ bin ls三个都是目录项,只是前两个是目录,而最后一个是普通文件。也就是说,目录项目录项或者是一子目录,或者是一个文件。
从上面的定义可以看出,indoedentry谁的包容性更大?当然是dentry
那么,kobjectdentry有何关系?由此我们可以推想,把dentry作为kobject中的一个字段,恍然间,kobject变得强大起来了。何谓“强大“,因为这样以来,就可以方便地将kobject映射到一个dentry上,也就是说,kobject/sys下的任何一个目录或文件相对应了,进一步说,把kobject导出形成文件系统就变得如同在内存中构建目录项一样简单。由此可知,kobject其实已经形成一棵树了。这就是以隐藏在背后的对象模型为桥梁,将驱动模型和sysfs文件系统全然联系起来。由于kobject被映射到目录项,同时对象模型层次结构也已经在内存形成一个树,因此sysfs的形成就水到渠成了。
既然kobject要形成一颗树,那么其中的字段就要有parent,以表示树的层次关系;另外,kobject不能是无名氏,得有name字段,按说,目录或文件名并不会很长,但是,sysfs文件系统为了表示对象之间复杂的关系,需要通过软链接达到,而软链接常常有较长的名字

/*设备驱动程序模型的核心数据结构,每个kobject
数据结构对应于sysfs文件系统中的一个目录*/
struct kobject {
	const char		*name;/*指向含有容器名称的字符*/
	struct list_head	entry;/*用于kobject所插入的链表的指针*/
	struct kobject		*parent;/*指向父kobject*/
	struct kset		*kset;/*指向包含的kset*/
	struct kobj_type	*ktype;/*指向kobject的类型的描述符*/
	struct sysfs_dirent	*sd;/*用同一种 struct sysfs_dirent 来统一设备模型中的 kset/kobject/attr/attr_group.*/
	struct kref		kref;/*容器的引用计数器*/
	unsigned int state_initialized:1;/*指明kobject是否被初始化*/
	unsigned int state_in_sysfs:1;/*指明是否使用了sysfs*/
	unsigned int state_add_uevent_sent:1;/*热插拔加载事件*/
	unsigned int state_remove_uevent_sent:1;/*热插拔卸载事件*/
	unsigned int uevent_suppress:1;/*如果设置了uevent_suppress字段,说明不希望产生事件,忽略事件正确返回*/
};

2.    引用计数kref  
kobject的主要功能之一就是为我们提供了一个统一的引用计数系统,为什么说它具有“统一”的能力?那是因为kobject是“基”对象,就像大厦的基地,其他对象(如devic,bus,class,device_driver等容器)都将其包含,以后,其他对象的引用技术继承或封装kobject的引用技术就可以了。
初始化时,kobject的引用计数设置为1。只要引用计数不为零,那么该对象就会继续保留在内存中,也可以说是被“钉住”了。任何包含对象引用的代码首先要增加该对象的引用计数,当代码结束后则减少它的引用计数。增加引用计数称为获得(getting)对象的引用,减少引用计数称为释放(putting)对象的引用。当引用计数跌到零时,对象便可以被销毁,同时相关内存也都被释放。
增加一个引用计数可通过koject_get()函数完成:
struct kobject * kobject_get(struct kobject *kobj);该函数正常情况下将返回一个指向kobject的指针,如果失败则返回NULL指针;
减少引用计数通过kobject_put()完成:
void kobject_put(struct kobject *kobj);如果对应的kobject的引用计数减少到零,则与该kobject关联的ktype中的析构函数将被调用。 我们深入到引用计数系统的内部去看,会发现kobject的引用计数是通过kref结构体实现的,该结构体定义在头文件<linux/kref.h>中:
struct kref {        atomic_t refcount;};

其中唯一的字段是用来存放引用计数的原子变量。那为什么采用结构体?这是为了便于进行类型检测。在使用kref前,必须先通过kref_init()函数来初始化它:

void kref_init(struct kref *kref){        atomic_set(&kref->refcount, 1);}

正如你所看到的,这个函数简单的将原子变量置1,所以kref一但被初始化,它表示的引用计数便固定为1
    开发者现在不必在内核代码中利用atmoic_t类型来实现其自己的引用计数。对开发者而言,在内核代码中最好的方法是利用kref类型和它相应的辅助函数,为自己提供一个通用的、正确的引用计数机制。
上述的所有函数定义与声明分别在在文件lib/kref.c和文件<linux/kref.h>中。

3.    共同特性的ktype

如上所述,kobject是一个抽象而基本的对象。对于一族具有共同特性的kobject,就是用ktype来描述:

struct kobj_type {
	void (*release)(struct kobject *kobj);
	struct sysfs_ops *sysfs_ops;
	struct attribute **default_attrs;
};

定义于头文件<linux/kobject.h>中。 release指针指向在kobject引用计数减至零时要被调用的析构函数。该函数负责释放所有kobject使用的内存和其它相关清理工作。
 sysfs_ops变量指向sysfs_ops结构体,其中包含两个函数,也就是对属性进行操作的读写函数show()和store()。
最后,default_attrs指向一个attribute结构体数组。这些结构体定义了kobject相关的默认属性。属性描述了给定对象的特征,其实,属性就是对应/sys树形结构中的叶子结点,也就是文件。

4.    对象集合体kset 
   Kset,顾名思义就是kobject对象的集合体,可以把它看成是一个容器,可将所有相关的kobject对象聚集起来,比如“全部的块设备”就是一个kset。听起来ksetktypes非常类似,好像没有多少实质内容。那么“为什么会需要这两个类似的东西呢”。ksets可把kobject集中到一个集合中,而ktype描述相关类型kobject所共有的特性,它们之间的重要区别在于:具有相同ktypekobject可以被分组到不同的ksets
       kobjectkset指针指向相应的kset集合。kset集合由kset结构体表示,定义于头文件<linux/kobject.h>中:

struct kset {
	struct list_head list;/*包含在kset中的kobject类型的描述符*/
	spinlock_t list_lock;
	/*嵌入的kobject结构,位于kset中的kobject,其parent字段指向这个内嵌的
	kobject指针*/
	struct kobject kobj;
	/*指向用于处理kobject结构的过滤和热插拔操作的回调函数表*/
	struct kset_uevent_ops *uevent_ops;
};

其中ktype指针指向集合(kset)中kobject对象的类型(ktype),list连接该集合(kset)中所有的kobject对象。kobj指向的koject对象代表了该集合的基类,uevent_ops指向一个用于处理集合中kobject对象的热插拔操作的结构体。
5sys文件系统目录实体sysfs_dirent

sysfs文件系统有自己的dirent结构,dirent = directory entry (目录实体)sysfs中,每一个dentry对应了一个dirent结构,dentry->d _fsdata是一个void的指针,它指向sysfs_dirent结构。每个kobject结构对应一个dirent结构。每个文件(包括目录文件)都对应一个sysfs_dirent对象 ,在建立目录和文件后,内存中会形成一sysfs_root为根的目录树结构。属性attribute结构对应sysfs中的文件,也用sysfs_dirent结构表示。这样sysfs_dirent的组织就是sysfs的树形结构的映射。

/*struct sysfs_dirent就是用来做kobject与dentry的互相转换用的*/
struct sysfs_dirent {
	atomic_t		s_count;
	atomic_t		s_active;
	struct sysfs_dirent	*s_parent;
	struct sysfs_dirent	*s_sibling;
	const char		*s_name;
	/*共用体包含四种不同的结构,分别是目录、
	符号链接文件、属性文件、二进制属性文件
	其中目录类型可以对应 kobject,在相应的 s_dir 
	中也有对 kobject 的指针,因此在内核数据结
	构, kobject 与 sysfs_dirent 是互相引用的;
	*/
	union {
		struct sysfs_elem_dir		s_dir;
		struct sysfs_elem_symlink	s_symlink;
		struct sysfs_elem_attr		s_attr;
		struct sysfs_elem_bin_attr	s_bin_attr;
	};

	unsigned int		s_flags;
	ino_t			s_ino;
	umode_t			s_mode;
	struct sysfs_inode_attrs *s_iattr;
};

下面是设备驱动模型核心数据结构关系图

总结:   kobject通常是嵌入到其它结构中的,其单独意义其实并不大。相反,那些更为重要的结构体,比如在struct cdev中才真正需要用到kobject结构。

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

当kobject被嵌入到其它结构中时,该结构便拥有了kobject提供的标准功能。更重要的一点是,嵌入kobject的结构体可以成为对象层次架构中的一部分。比如cdev结构体就可通过其父指针cdev->kobj->parent和链表cdev->kobj->entry来插入到对象层次结构中

三、主要方法

对设备驱动模型中核心的数据结构做了总结,对他们的操作就不难理解了,可以猜得到都是根据上面关系图来实现的方法。对每个数据结构都有很多实现的函数,这里我们只分析一个,即kset的注册。这个函数的实现涉及到了上面的所有数据结构。我也是根据这个函数画出上面的关系图的。

kset注册

/**
 * kset_register - initialize and add a kset.
 * @k: kset.
 */
int kset_register(struct kset *k)
{
	int err;

	if (!k)
		return -EINVAL;
	/*基本的初始化*/
	kset_init(k);
	/*kobj以及其属性建立sd树*/
	err = kobject_add_internal(&k->kobj);
	if (err)
		return err;
	/*事件相关*/
	kobject_uevent(&k->kobj, KOBJ_ADD);
	return 0;
}

kset初始化

/**
 * kset_init - initialize a kset for use
 * @k: kset
 */
void kset_init(struct kset *k)
{
	/*kobject结构初始化*/
	kobject_init_internal(&k->kobj);
	/*初始化kset的kobject链表*/
	INIT_LIST_HEAD(&k->list);
	spin_lock_init(&k->list_lock);
}
static void kobject_init_internal(struct kobject *kobj)
{
	if (!kobj)
		return;
	kref_init(&kobj->kref);/*引用计数加一*/
	INIT_LIST_HEAD(&kobj->entry);/*初始化链表*/
	kobj->state_in_sysfs = 0;
	kobj->state_add_uevent_sent = 0;
	kobj->state_remove_uevent_sent = 0;
	kobj->state_initialized = 1;/*已经初始化*/
}

建立sd树

static int kobject_add_internal(struct kobject *kobj)
{
	int error = 0;
	struct kobject *parent;

	if (!kobj)
		return -ENOENT;

	if (!kobj->name || !kobj->name[0]) {
		WARN(1, "kobject: (%p): attempted to be registered with empty "
			 "name!\n", kobj);
		return -EINVAL;
	}
	/*如果kobj的parent存在,增加其引用计数*/
	parent = kobject_get(kobj->parent);

	/* join kset if set, use it as parent if we do not already have one */
	if (kobj->kset) {
		if (!parent)/*如果其parent不存在,那么用kset中的kobject作为
			其parent,这里可以看出kset->kobj的parent为自身*/
			parent = kobject_get(&kobj->kset->kobj);
		kobj_kset_join(kobj);/*加入kset链表*/
		kobj->parent = parent;/*初始化parent字段*/
	}

	pr_debug("kobject: '%s' (%p): %s: parent: '%s', set: '%s'\n",
		 kobject_name(kobj), kobj, __func__,
		 parent ? kobject_name(parent) : "<NULL>",
		 kobj->kset ? kobject_name(&kobj->kset->kobj) : "<NULL>");

	error = create_dir(kobj);/*为kobj创建dir以及属性树,sd结构表示*/
	if (error) {/*出错释放资源*/
		kobj_kset_leave(kobj);
		kobject_put(parent);
		kobj->parent = NULL;

		/* be noisy on error issues */
		if (error == -EEXIST)
			printk(KERN_ERR "%s failed for %s with "
			       "-EEXIST, don't try to register things with "
			       "the same name in the same directory.\n",
			       __func__, kobject_name(kobj));
		else
			printk(KERN_ERR "%s failed for %s (%d)\n",
			       __func__, kobject_name(kobj), error);
		dump_stack();
	} else
		kobj->state_in_sysfs = 1;/*表示kobj已经在sysfs中*/

	return error;
}
/* add the kobject to its kset's list */
static void kobj_kset_join(struct kobject *kobj)
{
	if (!kobj->kset)
		return;
	/*增加kset->kobj的引用计数,从这里可以看出,kset的引用计数
	为kset中kobj属性的引用计数*/
	kset_get(kobj->kset);
	spin_lock(&kobj->kset->list_lock);/*链表上锁*/
	list_add_tail(&kobj->entry, &kobj->kset->list);/*将kobject加入kset链表*/
	spin_unlock(&kobj->kset->list_lock);
}

创建目录

static int create_dir(struct kobject *kobj)
{
	int error = 0;
	if (kobject_name(kobj)) {
		error = sysfs_create_dir(kobj);/*创建一个dir*/
		if (!error) {
			error = populate_dir(kobj);/*为kobj创建初始化属性结构树*/
			if (error)
				sysfs_remove_dir(kobj);
		}
	}
	return error;
}
/**
 *	sysfs_create_dir - create a directory for an object.
 *	@kobj:		object we're creating directory for. 
 */
int sysfs_create_dir(struct kobject * kobj)
{
	struct sysfs_dirent *parent_sd, *sd;
	int error = 0;

	BUG_ON(!kobj);
	/*找到sd的父节点*/
	if (kobj->parent)
		parent_sd = kobj->parent->sd;
	else
		parent_sd = &sysfs_root;
	/*创建dir,主要是sd结构的链接过程,可以看到创建dir的过程
	其实就是创建一个sd结构,然后和系统的sd以及kobj链接*/
	error = create_dir(kobj, parent_sd, kobject_name(kobj), &sd);
	if (!error)
		kobj->sd = sd;/*这样用object也可以找到sd*/
	return error;
}
static int create_dir(struct kobject *kobj, struct sysfs_dirent *parent_sd,
		      const char *name, struct sysfs_dirent **p_sd)
{
	umode_t mode = S_IFDIR| S_IRWXU | S_IRUGO | S_IXUGO;
	struct sysfs_addrm_cxt acxt;
	struct sysfs_dirent *sd;
	int rc;

	/* allocate */
	/*从slab中申请一个新的sd结构*/
	sd = sysfs_new_dirent(name, mode, SYSFS_DIR);
	if (!sd)
		return -ENOMEM;
	sd->s_dir.kobj = kobj;/*因为创建的是目录,在这里初始化s_dir*/

	/* link in */
	/*在sysfs_sb中通过hash短链,核查父目录parent_sd是否确实已经存在了,
	如果存在那么找到父目录的inode节点,赋值给acxt->parent_inode = inode;
	并初始化acxt的parent属性为parent_sd*/
	*/
	sysfs_addrm_start(&acxt, parent_sd);
	/*将sd借助acxt结构添加到其父节点的子节点链表中*/	
	rc = sysfs_add_one(&acxt, sd);
	/*对acxt做些清除工作*/
	sysfs_addrm_finish(&acxt);

	if (rc == 0)
		*p_sd = sd;
	else
		sysfs_put(sd);

	return rc;
}

新建dirent实体

struct sysfs_dirent *sysfs_new_dirent(const char *name, umode_t mode, int type)
{
	char *dup_name = NULL;
	struct sysfs_dirent *sd;

	if (type & SYSFS_COPY_NAME) {
		/*拷贝name到内核内存--kmalloc分配*/
		name = dup_name = kstrdup(name, GFP_KERNEL);
		if (!name)
			return NULL;
	}
	/*从cachep内的slab对象链中摘掉一个空闲slab对象,返回给sd*/
	sd = kmem_cache_zalloc(sysfs_dir_cachep, GFP_KERNEL);
	if (!sd)
		goto err_out1;

	if (sysfs_alloc_ino(&sd->s_ino))/*通过ida和idr获取一个全新的inode节点号*/
		goto err_out2;

	atomic_set(&sd->s_count, 1);
	atomic_set(&sd->s_active, 0);

	sd->s_name = name;
	sd->s_mode = mode;
	sd->s_flags = type;

	return sd;

 err_out2:
	kmem_cache_free(sysfs_dir_cachep, sd);
 err_out1:
	kfree(dup_name);
	return NULL;
}

sysfs_add_one(&acxt, sd);最终调用下面函数实现

int __sysfs_add_one(struct sysfs_addrm_cxt *acxt, struct sysfs_dirent *sd)
{
	/*从acxt的parent_sd的sibling中查找sd*/
	if (sysfs_find_dirent(acxt->parent_sd, sd->s_name))
		return -EEXIST;
	/*初始化s_parent为acxt的parent_sd*/
	sd->s_parent = sysfs_get(acxt->parent_sd);

	if (sysfs_type(sd) == SYSFS_DIR && acxt->parent_inode)
		inc_nlink(acxt->parent_inode);

	acxt->cnt++;
	/*将sd链入其parent的sibling中*/
	sysfs_link_sibling(sd);

	return 0;
}

dir中属性处理

static int populate_dir(struct kobject *kobj)
{
	struct kobj_type *t = get_ktype(kobj);/*得到kobj的类型结构*/
	struct attribute *attr;
	int error = 0;
	int i;
	/*通过类型找到属性*/
	if (t && t->default_attrs) {
		for (i = 0; (attr = t->default_attrs[i]) != NULL; i++) {
			error = sysfs_create_file(kobj, attr);/*为kobj创建一个属性文件,然后链入到sd结构树中。属性也是由sd结构表示;*/
			if (error)
				break;
		}
	}
	return error;
}

sysfs_create_file(kobj, attr)最终调用如下函数

int sysfs_add_file_mode(struct sysfs_dirent *dir_sd,
			const struct attribute *attr, int type, mode_t amode)
{
	umode_t mode = (amode & S_IALLUGO) | S_IFREG;
	struct sysfs_addrm_cxt acxt;
	struct sysfs_dirent *sd;
	int rc;
	/*创建一个sd*/
	sd = sysfs_new_dirent(attr->name, mode, type);
	if (!sd)
		return -ENOMEM;
	/*由于是属性,所以这里初始化的是s_attr*/
	sd->s_attr.attr = (void *)attr;
	/*下面为固定的调用,将sd链入到sd树种*/
	sysfs_addrm_start(&acxt, dir_sd);
	rc = sysfs_add_one(&acxt, sd);
	sysfs_addrm_finish(&acxt);

	if (rc)
		sysfs_put(sd);

	return rc;
}

 

参考:ULK3、linux内核之旅

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值