揭开字符设备驱动程序的面纱

Linux驱动开发庖丁解牛之三

——揭开字符设备驱动程序的面纱


http://www.kuqin.com/article/04linux/1047068.html

By:dreamice 2008-11-23

[email]dreamice.jiang@gmail.com[/email]



1.写在前面的话
我们知道,在Linux设备驱动开发中,包括三大设备类:字符设备,块设备和网络设备。而字符设备,作为最简单的设备类,为此,我们将从最简单的字符设备开始,走进Linux驱动程序设计的神秘殿堂。
——我们已经踏上了真正的设备驱动开发的道路了!
有志者,事竟成。付出越多,而上苍定会以同等的收获回馈于你,当然,最重要的一点是:我们必须走上正确的道路,做正确的付出。开始吧……
参考书目:
《Linux Device Driver》第三版
《Understanding the linux kernel》第三版
《Linux设备驱动开发详解》

2.必备之“砖”
盖大楼,得预先准备好砖头。同样的道理,要写好驱动程序,我们也必须准备好自己的“砖头”,拿好这些砖头,便会真正如庖丁解牛般,游刃于Linux驱动程序设计的神奇艺术之中。
在Linux的设计之初,曾提出:一切皆文件,如果一个东西不是文件,那就是进程。由此可见,文件的概念在Linux系统中可谓是根深蒂固,以至于它深入到对驱动程序的控制,这也是情理之中的事。
下图描述了Linux系统中虚拟文件系统和进程之间的关系:


fs





 dreamice 回复于:2008-11-23 22:37:23

图表 1进程和文件系统的关系
在上图中,我们看到了Process,File object,dentry object,inode object以及Sperblock object等概念。Process就是指一个特定的进程,而File obeject对应于进程打开的一个文件;dentry object描述了一个目录项;inode object则对应于磁盘上一个特定的文件;Sperblock object描述了文件系统的相关信息。从这个图中,可以看到进程到磁盘上一个文件实体的路径及对应关系。下面,我们一次看看这些实体结构在内核中的定义。
2.1 File object
File结构代表一个打开的文件,系统中每个打开的文件,在内核空间都对应一个file结构。它由内核在调用open时创建,并传递给在该文件上操作的所有函数,直到最后的close函数。在文件的所有实例都被关闭以后,内核才会释放这个结构。
在内核中,通常以filp来代表指向file结构的指针。File结构的详细定义如下:
//linux/fs.h
779 struct file {
 780         /*
 781          * fu_list becomes invalid after file_free is called and queued via
 782          * fu_rcuhead for RCU freeing
 783          */
 784         union {
 785                 struct list_head        fu_list;
 786                 struct rcu_head         fu_rcuhead;
 787         } f_u;
 788         struct path             f_path;
 789 #define f_dentry        f_path.dentry
 790 #define f_vfsmnt        f_path.mnt
 791         const struct file_operations    *f_op; //与文件操作相关的函数指针结构
 792         atomic_t                f_count;
 793         unsigned int            f_flags;
 794         mode_t                  f_mode;
 795         loff_t                  f_pos;
 796         struct fown_struct      f_owner;
 797         unsigned int            f_uid, f_gid;
 798         struct file_ra_state    f_ra;
 799
 800         u64                     f_version;
 801 #ifdef CONFIG_SECURITY
 802         void                    *f_security;
803 #endif
 804         /* needed for tty driver, and maybe others */
 805         void                    *private_data;
 806
 807 #ifdef CONFIG_EPOLL
 808         /* Used by fs/eventpoll.c to link all the hooks to this file */
 809         struct list_head        f_ep_links;
 810         spinlock_t              f_ep_lock;
 811 #endif /* #ifdef CONFIG_EPOLL */
 812         struct address_space    *f_mapping;
 813 };

1166 /*
1167  * NOTE:
1168  * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
1169  * can be called without the big kernel lock held in all filesystems.
1170  */
1171 struct file_operations {
1172         struct module *owner;
1173         loff_t (*llseek) (struct file *, loff_t, int);
1174         ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1175         ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1176         ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1177         ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1178         int (*readdir) (struct file *, void *, filldir_t);
1179         unsigned int (*poll) (struct file *, struct poll_table_struct *);
1180         int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
1181         long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1182         long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1183         int (*mmap) (struct file *, struct vm_area_struct *);
1184         int (*open) (struct inode *, struct file *);
1185         int (*flush) (struct file *, fl_owner_t id);
1186         int (*release) (struct inode *, struct file *);
1187         int (*fsync) (struct file *, struct dentry *, int datasync);
1188         int (*aio_fsync) (struct kiocb *, int datasync);
1189         int (*fasync) (int, struct file *, int);
1190         int (*lock) (struct file *, int, struct file_lock *);
1191         ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
1192         unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
1193         int (*check_flags)(int);
1194         int (*dir_notify)(struct file *filp, unsigned long arg);
1195         int (*flock) (struct file *, int, struct file_lock *);
1196         ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
1197         ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
1198         int (*setlease)(struct file *, long, struct file_lock **);
1199 };
其中,蓝色字体标出部分,为与驱动程序最为密切的部分。由于很多书中都对这些结构体做了详细的阐述,这里就不再赘述了。

2.2 inode object
内核用inode结构在内部表示文件,它和file结构的不同之处在于:file表示打开的文件描述符,对单个文件,可能有多个表示打开的文件描述符的file结构,但他们都指向同一个inode结构。
Inode结构的详细定义如下:
593 struct inode {
 594         struct hlist_node       i_hash;
 595         struct list_head        i_list;
 596         struct list_head        i_sb_list;
 597         struct list_head        i_dentry;
 598         unsigned long           i_ino;
 599         atomic_t                i_count;
 600         unsigned int            i_nlink;
 601         uid_t                   i_uid;
 602         gid_t                   i_gid;
 603         dev_t                   i_rdev;
 604         u64                     i_version;
 605         loff_t                  i_size;
 606 #ifdef __NEED_I_SIZE_ORDERED
 607         seqcount_t              i_size_seqcount;
 608 #endif
 609         struct timespec         i_atime;
 610         struct timespec         i_mtime;
 611         struct timespec         i_ctime;
 612         unsigned int            i_blkbits;
 613         blkcnt_t                i_blocks;
 614         unsigned short          i_bytes;
 615         umode_t                 i_mode;
 616         spinlock_t              i_lock; /* i_blocks, i_bytes, maybe i_size */
 617         struct mutex            i_mutex;
 618         struct rw_semaphore     i_alloc_sem;
 619         const struct inode_operations   *i_op;//inode操作函数集合
 620         const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
 621         struct super_block      *i_sb;
 622         struct file_lock        *i_flock;
 623         struct address_space    *i_mapping;
 624         struct address_space    i_data;
 625 #ifdef CONFIG_QUOTA
 626         struct dquot            *i_dquot[MAXQUOTAS];
 627 #endif
 628         struct list_head        i_devices;
 629         union {
 630                 struct pipe_inode_info  *i_pipe;
 631                 struct block_device     *i_bdev;
 632                 struct cdev             *i_cdev;
 633         };
 634         int                     i_cindex;
 635
 636         __u32                   i_generation;
 637
638 #ifdef CONFIG_DNOTIFY
 639         unsigned long           i_dnotify_mask; /* Directory notify events */
 640         struct dnotify_struct   *i_dnotify; /* for directory notifications */
 641 #endif
 642
 643 #ifdef CONFIG_INOTIFY
 644         struct list_head        inotify_watches; /* watches on this inode */
 645         struct mutex            inotify_mutex;  /* protects the watches list */
 646 #endif
 647
 648         unsigned long           i_state;
 649         unsigned long           dirtied_when;   /* jiffies of first dirtying */
 650
 651         unsigned int            i_flags;
 652
 653         atomic_t                i_writecount;
 654 #ifdef CONFIG_SECURITY
 655         void                    *i_security;
 656 #endif
 657         void                    *i_private; /* fs or device private pointer */
 658 };
2.3 Super block object
Super block object对应于一个特定的文件系统,通常对应于存放在磁盘扇区中的文件系统超级块或文件系统控制块,而对于非基于文件系统的文件,他们会在使用现场创建超级块,并将其保存到内存中。
一下是结构体的详细描述:
981 struct super_block {
 982         struct list_head        s_list;         /* Keep this first */
 983         dev_t                   s_dev;          /* search index; _not_ kdev_t */
 984         unsigned long           s_blocksize;
 985         unsigned char           s_blocksize_bits;
 986         unsigned char           s_dirt;
 987         unsigned long long      s_maxbytes;     /* Max file size */
 988         struct file_system_type *s_type;
 989         const struct super_operations   *s_op;
 990         struct dquot_operations *dq_op;
 991         struct quotactl_ops     *s_qcop;
 992         const struct export_operations *s_export_op;
 993         unsigned long           s_flags;
 994         unsigned long           s_magic;
 995         struct dentry           *s_root;
 996         struct rw_semaphore     s_umount;
 997         struct mutex            s_lock;
 998         int                     s_count;
 999         int                     s_syncing;
1000         int                     s_need_sync_fs;
1001         atomic_t                s_active;
1002 #ifdef CONFIG_SECURITY
1003         void                    *s_security;
1004 #endif
1005         struct xattr_handler    **s_xattr;
1006
1007         struct list_head        s_inodes;       /* all inodes */
1008         struct list_head        s_dirty;        /* dirty inodes */
1009         struct list_head        s_io;           /* parked for writeback */
1010         struct list_head        s_more_io;      /* parked for more writeback */
1011         struct hlist_head       s_anon;         /* anonymous dentries for (nfs) exporting */
1012         struct list_head        s_files;
1013
1014         struct block_device     *s_bdev;
1015         struct mtd_info         *s_mtd;
1016         struct list_head        s_instances;
1017         struct quota_info       s_dquot;        /* Diskquota specific options */
1018
1019         int                     s_frozen;
1020         wait_queue_head_t       s_wait_unfrozen;
1021
1022         char s_id[32];                          /* Informational name */
1023
1024         void                    *s_fs_info;     /* Filesystem private info */
1025
1026         /*
1027          * The next field is for VFS *only*. No filesystems have any business
1028          * even looking at it. You had been warned.
1029          */
1030         struct mutex s_vfs_rename_mutex;        /* Kludge */
1031
1032         /* Granularity of c/m/atime in ns.
1033            Cannot be worse than a second */
1034         u32                s_time_gran;
1035
1036         /*
1037          * Filesystem subtype.  If non-empty the filesystem type field
1038          * in /proc/mounts will be "type.subtype"
1039          */
1040         char *s_subtype;
1041
1042         /*
1043          * Saved mount options for lazy filesystems using
1044          * generic_show_options()
1045          */
1046         char *s_options;
1047 };

2.4 Identry object
Linux中把目录也当作文件,为了方便查找操作,虚拟文件系统(VFS)引入了目录项的概念。每个dentry代表路径中一个特定部分。由于驱动程序很少涉及到dentry,所以在这里就不做描述了。

3. 实例剖析Linux字符设备驱动程序
Linux的变化真的是太快了。当我们还在研读最新版的LDD3(基于内核2.6.11)时,而实际上,它的驱动程序框架结构已经发生了很大的变化。当我还在投入的学习scull示例驱动程序的时候,我发现,对于编写一个字符设备驱动程序,已经有了新的变化。原本打算剖析scull程序,来达到融会贯通的目的,然而,面对变化,我不得不去学习一种全新的字符设备驱动程序的编写。
我想这是一种挑战,学习,是一个终身的过程。
3.1 描述字符设备体结构(cdev)
在新版的linux内核中,使用cdev结构体描述一个字符设备,其定义如下:
//include/linux/cdev.h
13 struct cdev {
 14         struct kobject kobj;//kobject对象
 15         struct module *owner;
 16         const struct file_operations *ops;//文件操作结构体
 17         struct list_head list;
 18         dev_t dev;//定设备的主次设备号
 19         unsigned int count;
 20 };
(关于kobject还正在研究中……)
结构中的ops定义了操作File 的函数指针集,dev存储了主次设备号。注意,在很多著作中谈到Linux2.6内核中,dev_t是一个32位的类型,其中高12位存储主设备号,低20位存储次设备号。但我们最好不要这样去引用,内核版本的变化可谓是瞬息万变,很可能有一天这个约定就变化了,因此就将导致旧的驱动程序将不再兼容新的内核。最好的方式是使用下面两个宏定义获取主次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
使用下面的宏则可以将主次设备号生成一个dev_t:
MKDEV(int major, int minor)
一下是操作cdev结构体的相关内核函数:
22 void cdev_init(struct cdev *, const struct file_operations *); //初始化cdev结构,并建立//cdev结构和file_operations之间的连接
 23
 24 struct cdev *cdev_alloc(void);//动态申请cdev内存
 25
 26 void cdev_put(struct cdev *p);//解除读一个cdev的引用,相应的cdev_get函数增加
//对cdev的引用计数
 27
 28 int cdev_add(struct cdev *, dev_t, unsigned);//向系统添加一个cdev
 29
 30 void cdev_del(struct cdev *);//删除系统中一个cdev
 31
 32 void cd_forget(struct inode *);//
3.2 分配和释放设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
该函数用于已知起始设备的设备号的情况。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
对设备号未知的情况,使用该函数向内核动态申请未被占用的设备号。
对于一个驱动程序员来说,如果不确定设备号,最好不要妄自定义一个设备号进行注册,这样的后果是:如果该设备号已用,将注册不成功;如果该设备号在后来将被引用,那么将导致其他设备无法注册。所以,在这种情况下,最好使用动态注册的方式。
void unregister_chrdev_region(dev_t from, unsigned count)
该函数释放先前注册的设备。

3.3 一个简单的字符设备驱动程序
global_mem.c
  1 /*=============================================================
  2     A simple example of char device drivers
  3
  4     
  5     <[email]dreamice.jiang@gmail.com[/email]>
  6  ============================================================*/
  7 #include <linux/module.h>
  8 #include <linux/types.h>
  9 #include <linux/fs.h>
 10 #include <linux/errno.h>
 11 #include <linux/mm.h>
 12 #include <linux/sched.h>
 13 #include <linux/init.h>
 14 #include <linux/cdev.h>
 15 #include <asm/io.h>
 16 #include <asm/system.h>
 17 #include <asm/uaccess.h>
 18
 19 #define GLOBALMEM_SIZE  0x1000  /* 虚拟字符设备内存缓冲区大小 */
 20 #define MEM_CLEAR 0x1  /*  ioctl操作指令 (这里只是简单起见,不建议这么定义)*/
 21 //#define GLOBALMEM_MAJOR 254    /*  静态定义主设备号 */
 22 #define GLOBALMEM_MAJOR 0 /*定义动态申请主设备号*/
 23
 24 static int globalmem_major = GLOBALMEM_MAJOR;
 25 /*globalmem 虚拟字符设备结构体 */
 26 struct globalmem_dev
 27 {
 28   struct cdev cdev; /*cdev结构体 */
 29   unsigned char mem[GLOBALMEM_SIZE]; /*虚拟设备内存区大小*/
 30 };
 31
 32 struct globalmem_dev *globalmem_devp; 
 34 int globalmem_open(struct inode *inode, struct file *filp)
 35 {
 36   /*将设备结构体指针赋给文件私有数据 */
 37   filp->private_data = globalmem_devp;
 38   return 0;
 39 }
 40 /*释放函数*/
 41 int globalmem_release(struct inode *inode, struct file *filp)
 42 {
 43   return 0;
 44 }
 45
 46 /* ioct操作函数*/
 47 static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned
 48   int cmd, unsigned long arg)
 49 {
 50   struct globalmem_dev *dev = filp->private_data;/*&raquo;&ntilde;&micro;&Atilde;&Eacute;è±&cedil;&frac12;á&sup1;&sup1;&Igrave;&aring;&Ouml;&cedil;&Otilde;&euml;*/
 51
 52   switch (cmd)
 53   {
 54     case MEM_CLEAR:
 55       memset(dev->mem, 0, GLOBALMEM_SIZE);
 56       printk(KERN_INFO "globalmem is set to zero\n");
 57       break;
 58
 59     default:
 60       return  - EINVAL;
 61   }
 62   return 0;
 63 }
 64
 65 /*read函数*/
 66 static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size,
 67   loff_t *ppos)
 68 {
 69   unsigned long p =  *ppos;
 70   unsigned int count = size;
 71   int ret = 0;
 72   struct globalmem_dev *dev = filp->private_data; 
 73
 74   
 75   if (p >= GLOBALMEM_SIZE)
 76     return count ?  - ENXIO: 0;
 77   if (count > GLOBALMEM_SIZE - p)
 78     count = GLOBALMEM_SIZE - p;
 79
 80   
 81   if (copy_to_user(buf, (void*)(dev->mem + p), count))
 82   {
 83     ret =  - EFAULT;
 84   }
 85   else
 86   {
 87     *ppos += count;
 88     ret = count;
 89
 90     printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
 91   }
 92
 93   return ret;
 94 }
 95
 96 /*&ETH;&acute;&ordm;&macr;&Ecirc;&yacute;*/
 97 static ssize_t globalmem_write(struct file *filp, const char __user *buf,
 98   size_t size, loff_t *ppos)
 99 {
100   unsigned long p =  *ppos;
101   unsigned int count = size;
102   int ret = 0;
103   struct globalmem_dev *dev = filp->private_data;
104
105   
106   if (p >= GLOBALMEM_SIZE)
107     return count ?  - ENXIO: 0;
108   if (count > GLOBALMEM_SIZE - p)
109     count = GLOBALMEM_SIZE - p;
110
111   //
112   if (copy_from_user(dev->mem + p, buf, count))
113     ret =  - EFAULT;
114   else
115   {
116     *ppos += count;
117     ret = count;
118
119     printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
120   }
121
122   return ret;
123 }
124
125 
126 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
127 {
128   loff_t ret = 0;
129   switch (orig)
130   {
131     case 0:  //起始位置
132       if (offset < 0)
133       {
134         ret =  - EINVAL;
135         break;
136       }
137       if ((unsigned int)offset > GLOBALMEM_SIZE)
138       {
139         ret =  - EINVAL;
140         break;
141       }
142       filp->f_pos = (unsigned int)offset;
143       ret = filp->f_pos;
144       break;
145     case 1:   /*当前位置*/
146       if ((filp->f_pos + offset) > GLOBALMEM_SIZE)
147       {
148         ret =  - EINVAL;
149         break;
150       }
151       if ((filp->f_pos + offset) < 0)
152       {
153         ret =  - EINVAL;
154         break;
155       }
156       filp->f_pos += offset;
157       ret = filp->f_pos;
158       break;
159     default:
160       ret =  - EINVAL;
161       break;
162   }
163   return ret;
164 }
165
166 /*操作函数结构体*/
167 static const struct file_operations globalmem_fops =
168 {
169   .owner = THIS_MODULE,
170   .llseek = globalmem_llseek,
171   .read = globalmem_read,
172   .write = globalmem_write,
173   .ioctl = globalmem_ioctl,
174   .open = globalmem_open,
175   .release = globalmem_release,
176 };
177
178 /*初始化并注册cdev */
179 static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
180 {
181   int err, devno = MKDEV(globalmem_major, index);
182
183   cdev_init(&dev->cdev, &globalmem_fops);
184   dev->cdev.owner = THIS_MODULE;
185   dev->cdev.ops = &globalmem_fops;
186   err = cdev_add(&dev->cdev, devno, 1);
187   if (err)
188     printk(KERN_NOTICE "Error %d adding LED%d", err, index);
189 }
190
191 /*设备驱动模块加载*/
192 int globalmem_init(void)
193 {
194   int result;
195   dev_t devno = MKDEV(globalmem_major, 0);
196
197   /*静态指定设备编号*/
198   if (globalmem_major)
199     result = register_chrdev_region(devno, 1, "globalmem");
200   else  /*动态申请*/
201   {
202     result = alloc_chrdev_region(&devno, 0, 1, "globalmem");
203     globalmem_major = MAJOR(devno);
204   }
205   if (result < 0)
206     return result;
207
208   /* 动态申请设备结构体内存*/
209   globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
210   if (!globalmem_devp)    /*&Eacute;ê&Ccedil;&euml;&Ecirc;§°&Uuml;*/
211   {
212     result =  - ENOMEM;
213     goto fail_malloc;
214   }
215   memset(globalmem_devp, 0, sizeof(struct globalmem_dev));
216
217   globalmem_setup_cdev(globalmem_devp, 0);
218   printk(KERN_INFO"Init global_mem success!\n");
219   return 0;
220
221   fail_malloc: unregister_chrdev_region(devno, 1);
222   return result;
223 }
224
225 /*模块卸载函数*/
226 void globalmem_exit(void)
227 {
228   cdev_del(&globalmem_devp->cdev);   /*注销cdev*/
229   kfree(globalmem_devp);     /*释放结构体内存*/
230   unregister_chrdev_region(MKDEV(globalmem_major, 0), 1); /*释放设备号*/
231   printk(KERN_INFO"Bye-bye global_mem!\n");
232 }
233
234 MODULE_AUTHOR("Dreamice");
235 MODULE_LICENSE("Dual BSD/GPL");
236
237 module_param(globalmem_major, int, S_IRUGO);
238
239 module_init(globalmem_init);
240 module_exit(globalmem_exit);


Makefile:
  1 TARGET = global_mem
  2 KDIR = /lib/modules/$(shell uname -r)/build
  3 PWD = $(shell pwd)
  4 obj-m := $(TARGET).o
  5 default:
  6         make -C $(KDIR) M=$(PWD) modules
  7 clean:
  8         $(RM) *.o *.ko *.mod.c Module.symvers modules.order

测试:
# cd /sys/module/globalmem/parameters
# cat globalmem_major
251
# mknod /dev/globalmem c 251 0
# echo ‘hello dreamice’ > /dev/gloablmem
# cat /dev/gloablmem
hello dreamice

当然,我们也可以写c程序来测试该程序。
4.总结:
    在编写字符设备驱动程序中,最重要的是file_operations这个结构,我们通常只需要实现read,write,ioctl等函数的操作。
对于一个特定的字符设备驱动的编写,必须把该设备同一个cdev结构关联起来。我们必须完成设备号的指定或者动态申请,这是设备在系统中的唯一标识。然后,完成设备驱动程序的模块初始化,以及模块注销的相关实现。
注意,file_operations与cdev结构的关联,这与老版本的驱动程序编写有很大差异。
5.后续工作
(1)研究总结kobject以及sysfs;
(2)深入学习,通过改进该实例驱动程序,使之包含更多模块及设备驱动程序编写涉及的知识点,如多个子设备,内核同步相关知识点等,来达到更深入领会字符设备驱动程序的目的。
(3)深入剖析内核文件系统。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值