第13天 定时器(2)

第13天 定时器(2)

2020.4.15

1. 简化字符串显示(harib10a)

  • harib09g的bootpack.c有210行,代码中多次出现了:

    boxfill8(buf_win, 160, COL8_C6C6C6, 40, 28, 119, 43);
    putfonts8_asc(buf_win, 160, 40, 28, COL8_000000, s);
    sheet_refresh(sht_win, 40, 28, 120, 44);
    

    这三个函数的组合。这三个函数完成的工作是:先在特定位置涂上背景色,然后写上字符,最后完成刷新。

  • 将这部分归纳到一个函数putfonts8_asc_sht(bootpack.c中):

    void putfonts8_asc_sht(struct SHEET *sht, int x, int y, int c, int b, char *s, int l)
    {
        boxfill8(sht->buf, sht->bxsize, b, x, y, x + l * 8 - 1, y + 15);
        putfonts8_asc(sht->buf, sht->bxsize, x, y, c, s);
        sheet_refresh(sht, x, y, x + l * 8, y + 16);
        return;
    }
    
    • sht是要刷新的图层。
    • x,y是显示位置的坐标。
    • c是字符颜色(color)
    • b背景颜色(back color)
    • s是字符串(string)
    • l是字符串长度(length)
  • 改写bootpack.c。(此处改写不再赘述)改写完成后,成功缩短了2行。

2. 重新调整FIFO缓冲区(1)(harib10b)

  • 查看HariMain中的代码:

    if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) + fifo8_status(&timerfifo2) + fifo8_status(&timerfifo3) == 0) {
    	io_sti();
    }
    

    上述代码是3个超时计时器。要是100个那代码可太长了。

  • 可以只定义一个FIFO缓冲区,向其中写入不同的数据来区分到底是哪个超时计时器传来的数据

  • 改写HariMain中代码:

    fifo8_init(&timerfifo, 8, timerbuf);
    timer = timer_alloc();
    timer_init(timer, &timerfifo, 10); /*timer data写入10*/
    timer_settime(timer, 1000); 
    
    fifo8_init(&timerfifo2, 8, timerbuf2);
    timer2 = timer_alloc();
    timer_init(timer2, &timerfifo2, 3); /*timer2 data写入3*/
    timer_settime(timer2, 300);
    
    fifo8_init(&timerfifo3, 8, timerbuf3);
    timer3 = timer_alloc();
    timer_init(timer3, &timerfifo3, 1); /*timer3 data=1*/
    timer_settime(timer3, 50); 
    
  • 继续修改HariMain:

    for (;;) {
    	sprintf(s, "%010d", timerctl.count);
    	putfonts8_asc_sht(sht_win, 40, 28, COL8_000000, COL8_C6C6C6, s, 10);
    
    	io_cli();
    	if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) == 0) {
    		io_sti();
    	} else {
    		if (fifo8_status(&keyfifo) != 0) {
    			……
    		} else if (fifo8_status(&mousefifo) != 0) {
    			……
    		} else if (fifo8_status(&timerfifo) != 0) {
    			i = fifo8_get(&timerfifo); /* 从缓冲区读取数据 */
    			io_sti();
    			if (i == 10) { /*timer*/
    				putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
    			} else if (i == 3) { /*timer1*/
    				putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
    			} else { /*timer3*/
    				if (i != 0) {
    					timer_init(timer3, &timerfifo, 0); /*data = 0*/
    					boxfill8(buf_back, binfo->scrnx, COL8_FFFFFF, 8, 96, 15, 111);
    				} else {
    					timer_init(timer3, &timerfifo, 1); /*data = 1*/
    					boxfill8(buf_back, binfo->scrnx, COL8_008484, 8, 96, 15, 111);
    				}
    				timer_settime(timer3, 50);
    				sheet_refresh(sht_back, 8, 96, 16, 112);
    			}
    		}
    	}
    }
    
  • make run正常。同时bootpack.c精简了4行。

3. 测试性能(harib10c~harib10f)

  • 我们一直在改善定时器,现在需要知道到底改成啥样子了。之所以如此专注于改良定时器,是因为在今后的开发中,需要经常用到定时器。

  • 测试性能的方法:

    【完全不显示计数,是为了让CPU全力计数,因为输出数据会导致CPU不能全力计数。】

  • HariMain函数节选:

    for (;;) {
    	count++;
    
    	io_cli();
    	if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) == 0) {
    		io_sti();
    	} else {
    		if (fifo8_status(&keyfifo) != 0) {
    			……
    		} else if (fifo8_status(&mousefifo) != 0) {
    			……
    		} else if (fifo8_status(&timerfifo) != 0) {
    			i = fifo8_get(&timerfifo); 
    			io_sti();
    			if (i == 10) {
    				putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
    				sprintf(s, "%010d", count); /*输出*/
    				putfonts8_asc_sht(sht_win, 40, 28, COL8_000000, COL8_C6C6C6, s, 10);
    			} else if (i == 3) {
    				putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
    				count = 0; /* 3秒后count复位,正式开始计数 */
    			} else {
    				……
    			}
    		}
    	}
    }
    
  • make run后不要移动鼠标和键盘,让CPU全力计数:

    count=103256250.

  • 多次测试,count值:

    103256250
    103828731
    104334477
    103598788
    103678979
    

    最大和最小差值为1078227。差值有一百多万。这说明QEMU受到Windows的影响比较大。

  • 在VMware上试试:

    1047258405
    1048709262
    1048497937
    1048932095
    1051931689
    

    最大值和最小值差值为4673284。差值四百多万。这说明VMware受到Windows的影响更大。

  • 由于只有一台电脑,无法使用真机运行。但真机误差的确会很小。

  • 将harib09d改写成harib10d测试count值。将harib09e改写成harib10e测试count值。将harib09f改写成harib10f测试count值。(这里采用书上的结果)

    (harib10c对应harib09g)可以看出count值随着我们的优化而增大,速度就提高。

  • 启动3秒后将count置为0的原因:

4. 重新调整FIFO缓冲区(2)(harib10g)

  • 测试完性能,将代码改回harib10c。

  • 我们将鼠标和键盘也归纳起来,让它们和定时器共用一个FIFO缓冲区。
    向FIFO缓冲区写不同的数据,可以区分是哪个定时器、鼠标和键盘。

  • 向FIFO写入的数据与中断类型设定:

    • 这里的256~511共256(0xff)个。因为每次键盘中断都会产生一字节数据,这一字节数据刚好是0x??。
    • 同理,512~767这256个字节也是一样。鼠标变化会产生三次中断,每次中断传送1字节数据。
  • 结构体FIFO8只能存放1字节数据,最大255。存放不了256及以上的数字。因此,我们需要将FIFO8改成FIFO32,以存放4字节数据。(只是修改了buf的数据类型,以及将FIFO8改成FIFO32)

  • bootpack.h中FIFO32定义:

    struct FIFO32 {
        int *buf;
        int p, q, size, free, flags;
    };
    
  • fifo.c中修改:

    void fifo32_init(struct FIFO32 *fifo, int size, int *buf)
    /* FIFO缓冲区的初始化 */
    {
        fifo->size = size;
        fifo->buf = buf;
        fifo->free = size; /* 空 */
        fifo->flags = 0;
        fifo->p = 0; /* 写入位置 */
        fifo->q = 0; /* 读取位置 */
        return;
    }
    
    int fifo32_put(struct FIFO32 *fifo, int data)
    /* 给FIFO发送数据并将数据存储在FIFO中 */
    {
        if (fifo->free == 0) {
            /* 没有空余空间,溢出 */
            fifo->flags |= FLAGS_OVERRUN;
            return -1;
        }
        fifo->buf[fifo->p] = data;
        fifo->p++;
        if (fifo->p == fifo->size) {
            fifo->p = 0;
        }
        fifo->free--;
        return 0;
    }
    
    int fifo32_get(struct FIFO32 *fifo)
    /* 从FIFO取得一个数据 */
    {
        int data;
        if (fifo->free == fifo->size) {
            /* 当缓冲区数据为空的情况下返回-1 */
            return -1;
        }
        data = fifo->buf[fifo->q];
        fifo->q++;
        if (fifo->q == fifo->size) {
            fifo->q = 0;
        }
        fifo->free++;
        return data;
    }
    
    int fifo32_status(struct FIFO32 *fifo)
    /* 返回已经存储了多少数据 */
    {
        return fifo->size - fifo->free;
    }
    
    • 注意,fifo32_put中data的类型也因此变成了int。
  • 修改keyboard.c

    struct FIFO32 *keyfifo;
    int keydata0;
    
    void inthandler21(int *esp)
    {
        int data;
        io_out8(PIC0_OCW2, 0x61);	
        data = io_in8(PORT_KEYDAT);
        fifo32_put(keyfifo, data + keydata0);
        return;
    }
    
    void init_keyboard(struct FIFO32 *fifo, int data0)
    {
        /* 将FIFO缓冲区的信息保存到全局变量中 */
        keyfifo = fifo;
        keydata0 = data0;
        /* 键盘控制器初始化 */
        wait_KBC_sendready();
        io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
        wait_KBC_sendready();
        io_out8(PORT_KEYDAT, KBC_MODE);
        return;
    }
    
    • init_keyboard函数添加了两个参数,参数fifo用于将现在唯一的一个开辟了空间的FIFO缓冲区传进来,data0是我们规定的256。
  • 修改mouse.c

    struct FIFO32 *mousefifo;
    int mousedata0;
    
    void inthandler2c(int *esp)
    /* 基于PS/2鼠标的中断 */
    {
        int data;
        io_out8(PIC1_OCW2, 0x64);	
        io_out8(PIC0_OCW2, 0x62);	
        data = io_in8(PORT_KEYDAT);
        fifo32_put(mousefifo, data + mousedata0);
        return;
    }
    void enable_mouse(struct FIFO32 *fifo, int data0, struct MOUSE_DEC *mdec)
    {
        /* 将FIFO缓冲区的信息保存到全局变量中 */
        mousefifo = fifo;
        mousedata0 = data0;
        /* 使鼠标有效 */
        wait_KBC_sendready();
        io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
        wait_KBC_sendready();
        io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
        /* 顺利的话,ACK(0xfa)会被发送 */
        mdec->phase = 0; /* 等待鼠标的0xfa */
        return;
    }
    
    • enable_mouse添加了两个参数fifo和data0。fifo是唯一一个分配了内存的缓冲区,data0是我们规定的512。
  • 修改定时器结构体:

    struct TIMER {
        unsigned int timeout, flags;
        struct FIFO32 *fifo;
        int data;
    };
    
  • 修改time.c:

    void timer_init(struct TIMER *timer, struct FIFO32 *fifo, int data)
    {
        timer->fifo = fifo;
        timer->data = data;
        return;
    }
    
    void inthandler20(int *esp)
    {
        int i, j;
        io_out8(PIC0_OCW2, 0x60);	
        timerctl.count++;
        if (timerctl.next > timerctl.count) {
            return;
        }
        for (i = 0; i < timerctl.using; i++) {
            if (timerctl.timers[i]->timeout > timerctl.count) {
                break;
            }
            timerctl.timers[i]->flags = TIMER_FLAGS_ALLOC;
            fifo32_put(timerctl.timers[i]->fifo, timerctl.timers[i]->data);
        }
        timerctl.using -= i;
        for (j = 0; j < timerctl.using; j++) {
            timerctl.timers[j] = timerctl.timers[i + j];
        }
        if (timerctl.using > 0) {
            timerctl.next = timerctl.timers[0]->timeout;
        } else {
            timerctl.next = 0xffffffff;
        }
        return;
    }
    
  • 修改bootpack.c:

    void HariMain(void)
    {
        struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
        struct FIFO32 fifo;
        char s[40];
        int fifobuf[128];
        ……
        fifo32_init(&fifo, 128, fifobuf);
        init_pit();
        init_keyboard(&fifo, 256); // 键盘传入256
        enable_mouse(&fifo, 512, &mdec); // 鼠标传入512
        io_out8(PIC0_IMR, 0xf8); 
        io_out8(PIC1_IMR, 0xef);
    
        timer = timer_alloc();
        timer_init(timer, &fifo, 10);
        timer_settime(timer, 1000);
        timer2 = timer_alloc();
        timer_init(timer2, &fifo, 3);
        timer_settime(timer2, 300);
        timer3 = timer_alloc();
        timer_init(timer3, &fifo, 1);
        timer_settime(timer3, 50);
        ……
        for (;;) {
            count++;
    
            io_cli();
            if (fifo32_status(&fifo) == 0) {
                io_sti();
            } else {
                i = fifo32_get(&fifo);
                io_sti();
                if (256 <= i && i <= 511) { /* 键盘数据 */
                    sprintf(s, "%02X", i - 256);
                    putfonts8_asc_sht(sht_back, 0, 16, COL8_FFFFFF, COL8_008484, s, 2);
                } else if (512 <= i && i <= 767) { /* 鼠标数据 */
                    if (mouse_decode(&mdec, i - 512) != 0) {
                        sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
                        if ((mdec.btn & 0x01) != 0) {
                            s[1] = 'L';
                        }
                        if ((mdec.btn & 0x02) != 0) {
                            s[3] = 'R';
                        }
                        if ((mdec.btn & 0x04) != 0) {
                            s[2] = 'C';
                        }
                        putfonts8_asc_sht(sht_back, 32, 16, COL8_FFFFFF, COL8_008484, s, 15);
                        mx += mdec.x;
                        my += mdec.y;
                        if (mx < 0) {
                            mx = 0;
                        }
                        if (my < 0) {
                            my = 0;
                        }
                        if (mx > binfo->scrnx - 1) {
                            mx = binfo->scrnx - 1;
                        }
                        if (my > binfo->scrny - 1) {
                            my = binfo->scrny - 1;
                        }
                        sprintf(s, "(%3d, %3d)", mx, my);
                        putfonts8_asc_sht(sht_back, 0, 0, COL8_FFFFFF, COL8_008484, s, 10);
                        sheet_slide(sht_mouse, mx, my);
                    }
                } else if (i == 10) { /* 10s定时器 */
                    putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
                    sprintf(s, "%010d", count);
                    putfonts8_asc_sht(sht_win, 40, 28, COL8_000000, COL8_C6C6C6, s, 10);
                } else if (i == 3) { /* 3s定时器 */
                    putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
                    count = 0; /* 复位 */
                } else if (i == 1) { /* 光标定时器 */
                    timer_init(timer3, &fifo, 0); 
                    boxfill8(buf_back, binfo->scrnx, COL8_FFFFFF, 8, 96, 15, 111);
                    timer_settime(timer3, 50);
                    sheet_refresh(sht_back, 8, 96, 16, 112);
                } else if (i == 0) { /* 光标定时器 */
                    timer_init(timer3, &fifo, 1); 
                    boxfill8(buf_back, binfo->scrnx, COL8_008484, 8, 96, 15, 111);
                    timer_settime(timer3, 50);
                    sheet_refresh(sht_back, 8, 96, 16, 112);
                }
            }
        }
    }
    
    • 经过修正,bootpack.c简化了8行。
  • make run:

    • 比harib10c运行结果103256250速度提升了181%!!
    • 在真机上运行速度提升130%。
    • 原因如下:

5. 加快中断处理(4)(harib10h)

  • 现在的问题是,inthandler20和timer_settime中都有移位处理。timer_settime是在中断禁止期间进行的,因此也要尽可能不用移位处理。

  • 倘若有多个定时器,使用移位操作是非常浪费时间的。

  • 使用链表优化。

  • 修改TIMER结构体:

    struct TIMER {
        struct TIMER *next;
        unsigned int timeout, flags;
        struct FIFO32 *fifo;
        int data;
    };
    
    • 添加新的指针变量next,用于指向下一个即将超时的定时器的地址。
    • 注意,结构体TIMERCTL中的next的含义是即将超时的(最紧迫)时刻
  • 定时器们维护一个链表,这个链表是有序的,往这个链表中添加数据时遍历O(n),从链表中取出数据(只取头结点数据,最紧迫)O(1)。

    • 添加next指针以后,timers指针数组就不再需要了,因为我们只需要使用timers[0]。timers[0]我们是需要的,因为每次都是从timers[0]中取得下一时刻的定时器。因此,将TIMERCTL中timers[]改成t0即可,每次从t0中取得下一时刻的定时器即可。
    • 修改TIMERCTL:
      struct TIMERCTL {
          unsigned int count, next, using;
          struct TIMER *t0;
          struct TIMER timers0[MAX_TIMER];
      };
      
  • 修改inthandler20:

    void inthandler20(int *esp)
    {
        int i;
        struct TIMER *timer;
        io_out8(PIC0_OCW2, 0x60);	/* 通知 */
        timerctl.count++;
        if (timerctl.next > timerctl.count) {
            return;
        }
        timer = timerctl.t0; 
        for (i = 0; i < timerctl.using; i++) { 
            /*防止多个定时器在同一时刻结束*/
            /* timers中定时器都是运行的,因此不必确认flags */
            if (timer->timeout > timerctl.count) {
                break;
            }
            /* 超时 */
            timer->flags = TIMER_FLAGS_ALLOC;
            fifo32_put(timer->fifo, timer->data);
            timer = timer->next; 
        }
        timerctl.using -= i;
    
        /* 获取下一时刻的定时器,新移位 */
        timerctl.t0 = timer;
    
        if (timerctl.using > 0) {
            timerctl.next = timerctl.t0->timeout;
        } else {
            timerctl.next = 0xffffffff;
        }
        return;
    }
    
  • 修改timer_settime:

    void timer_settime(struct TIMER *timer, unsigned int timeout)
    {
        int e;
        struct TIMER *t, *s;
        timer->timeout = timeout + timerctl.count;
        timer->flags = TIMER_FLAGS_USING;
        e = io_load_eflags();
        io_cli();
        timerctl.using++;
        if (timerctl.using == 1) {
            /* 处于运行状态的定时器只有1个 */
            timerctl.t0 = timer;
            timer->next = 0; /* 没有下一个 */
            timerctl.next = timer->timeout;
            io_store_eflags(e);
            return;
        }
        t = timerctl.t0;
        if (timer->timeout <= t->timeout) {
            /* 插入到最前面的情况 */
            timerctl.t0 = timer;
            timer->next = t; /* 下面是t */
            timerctl.next = timer->timeout;
            io_store_eflags(e);
            return;
        }
        /* 搜寻插入位置 */
        for (;;) {
            s = t;
            t = t->next;
            if (t == 0) {
                break; /* 最后面 */
            }
            if (timer->timeout <= t->timeout) {
                /* 插入到s和t之间 */
                s->next = timer; 
                timer->next = t; 
                io_store_eflags(e);
                return;
            }
        }
        /* 插入到最后面的情况 */
        s->next = timer;
        timer->next = 0;
        io_store_eflags(e);
        return;
    }
    
    • 自己模拟一下就OK。
  • make run:

    貌似count值变小了?

6. 使用“哨兵”简化程序(harib10i)

  • timer_settime函数有些冗长,分析一下,当向链表中添加一个定时器时,无非就只有以下四种情况:

    • 链表为空,也就是说当前插入的定时器是唯一一个在运行的定时器。
    • 插入到链表头部
    • 插入到两个定时器之间
    • 插入到链表尾部
  • 规定:设置一个定时器(哨兵),它的超时时刻是0xffffffff。
    这个定时器,不需要设定它的fifo(但是其他的属性还是要设定的),因为在到达这个时刻之前,我们已经修改了count值(一年修改一次)。

  • 在init_pic函数中加入初始化哨兵的代码(已经删除了using,下面有解释):

    void init_pit(void)
    {
        int i;
        struct TIMER *t;
        io_out8(PIT_CTRL, 0x34);
        io_out8(PIT_CNT0, 0x9c);
        io_out8(PIT_CNT0, 0x2e);
        timerctl.count = 0;
        for (i = 0; i < MAX_TIMER; i++) {
            timerctl.timers0[i].flags = 0; /* 未使用 */
        }
        t = timer_alloc(); /* 为哨兵分配定时器 */
        t->timeout = 0xffffffff; /*哨兵的超时时刻*/
        t->flags = TIMER_FLAGS_USING;
        t->next = 0; /* 只有哨兵一个定时器,末尾 */
        timerctl.t0 = t; /* 正在运行的只有哨兵,哨兵是最紧迫的 */
        timerctl.next = 0xffffffff; /* 下一时刻是哨兵的超时时刻 */
        return;
    }
    
  • 修改一年调整一次的程序:

  • 加入了哨兵,四种情况中只有两种可能发生:

    • 链表为空,也就是说当前插入的定时器是唯一一个在运行的定时器。【不可能】
    • 插入到链表头部 【可能】
    • 插入到两个定时器之间 【可能】
    • 插入到链表尾部 【不可能】
  • 修改timer_settime(已经删除了using,下面有解释):

    void timer_settime(struct TIMER *timer, unsigned int timeout)
    {
        int e;
        struct TIMER *t, *s;
        timer->timeout = timeout + timerctl.count;
        timer->flags = TIMER_FLAGS_USING;
        e = io_load_eflags();
        io_cli();
        t = timerctl.t0;
        if (timer->timeout <= t->timeout) {
            /* 插入到最前面的情况 */
            timerctl.t0 = timer;
            timer->next = t; 
            timerctl.next = timer->timeout;
            io_store_eflags(e);
            return;
        }
        /* 搜寻插入位置 */
        for (;;) {
            s = t;
            t = t->next;
            if (timer->timeout <= t->timeout) {
                /* 插入s和t之间 */
                s->next = timer; 
                timer->next = t; 
                io_store_eflags(e);
                return;
            }
        }
    }
    
  • 简化inthandler20:

    • using对正在运行的定时器计数。不使用timers数组以后,根据using是否为0来决定怎样设定next。现在有了哨兵,所以,using恒大于0。这样一来,using就没啥用了。删掉using吧
    void inthandler20(int *esp)
    {
        struct TIMER *timer;
        io_out8(PIC0_OCW2, 0x60);	/* 通知 */
        timerctl.count++;
        if (timerctl.next > timerctl.count) {
            return;
        }
        timer = timerctl.t0; /* 先把最前面的定时器赋值给timer */
        for (;;) {
            /*防止多个定时器在同一时刻*/
            /* 不必确认flags */
            if (timer->timeout > timerctl.count) {
                break;
            }
            /* 超时 */
            timer->flags = TIMER_FLAGS_ALLOC;
            fifo32_put(timer->fifo, timer->data);
            timer = timer->next; /* 遍历 */
        }
        /*更新*/
        timerctl.t0 = timer;
        timerctl.next = timer->timeout;
        return;
    }
    
  • make run:

    在QEMU上的确变快了。

7. 5月27号

  • 今天早上得知,本科生大四年级毕业设计(论文)的定稿(DDL)日期是2020.5.27。
  • 这让我有点小失落和小确幸。
  • 小失落是,还有40天,便真正开始结束自己的大学生涯了。
  • 小确幸是,自己从3.28开始,到今天是19天,其中还有好几天因为腰痛休息了几天。已经结束了13天了,总进度是261/710=36.8。这还是值得表扬的。
  • 同时,还应该看到,还有大部分没有完成。因此,需要加快速度了。
  • 革命尚未成功,同志仍需努力啊!
  • 2020.4.16 10:48
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值