《UNIX环境高级编程》---3.文件I/O

一、打开、创建文件、关闭文件

  1. 文件描述符:一个非负整数,范围是0~OPEN_MAX-1。内核用它来标识进程正在访问的文件。当进程创建时,默认为它打开了3个文件描述符,它们都链接向终端:

    • 0: 标准输入
    • 1: 标准输出
    • 2: 标准错误输出

    通常我们应该使用STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO来替代这三个幻数,从而提高可读性。这三个常量位于<unistd.h>中。

2. openopenat函数:打开文件

```
#include<fcntl.h>
int open(const char* path,int oflag,.../*mode_t mode*/);
int openat(int fd,const char*path,int oflag,.../*mode_t mode */);
```

- 参数:
    - `path`:要打开或者创建文件的名字
    - `oflag`:用于指定函数的操作行为,定义在`<fcntl.h>`中。
    -  `mode`:文件访问权限。文件访问权限常量在 `<sys/stat.h>` 中定义,有下列九个:
        - `S_IRUSR`:用户读
        - `S_IWUSR`:用户写
        - `S_IXUSR`:用户执行
        - `S_IRGRP`:组读
        - `S_IWGRP`:组写          
        - `S_IXGRP`:组执行         
        - `S_IROTH`:其他读
        - `S_IWOTH`:其他写
        - `S_IXOTH`:其他执行 

- 对于`openat`函数,被打开的文件名由`fd`和`path`共同决定:
    **- 如果`path`指定的是绝对路径,此时`fd`被忽略。`openat`等价于`open`**
    **- 如果`path`指定的是相对路径名,则`fd`是一个目录打开的文件描述符。被打开的文件的绝对路径由该`fd`描述符对应的目录加上`path`组合而成**
    **- 如果`path`是一个相对路径名,而`fd`是常量`AT_FDCWD`,则`path`相对于当前工作目录。被打开文件在当前工作目录中查找。**

- 返回值:  
    - 成功:返回文件描述符。
    - 失败:返回 -1

由 `open/openat` 返回的文件描述符一定是最小的未使用的描述符数字。

3. creat函数:创建一个新文件

```
#include<fcntl.h>
int creat(const char*path,mode_t mode);
```
该函数等价于`open(path,O_WRONLY|O_CREAT|O_TRUNC,mode)`。注意:
- 它以只写方式打开,因此若要读取该文件,则必须先关闭,然后重新以读方式打开。
- 若文件已存在则将文件截断为0。
- 

4. close函数:关闭文件

```
#include<unistd.h>
int close(int fd);
```

- 参数:
    - `fd`:待关闭文件的文件描述符
- 返回值:
    - 成功:返回 0
    - 失败:返回 -1

注意:
- 进程关闭一个文件会释放它加在该文件上的所有记录锁。
- 当一个进程终止时,内核会自动关闭它所有的打开的文件。

二、定位、读、写文件

  1. lseek函数:设置打开文件的偏移量

    
    #include<unistd.h>
    
    off_t lseek(int fd, off_t offset,int whence);
    • 参数:
      • fd:打开的文件的文件描述符
      • whence:必须是 SEEK_SETSEEK_CURSEEK_END三个常量之一
      • offset
        • 如果 whenceSEEK_SET,则将该文件的偏移量设置为距离文件开始处offset个字节
        • 如果 whenceSEEK_CUR,则将该文件的偏移量设置为当前值加上offset个字节,offset可正,可负
        • 如果 whenceSEEK_END,则将该文件的偏移量设置为文件长度加上offset个字节,offset可正,可负
    • 返回值:
      • 成功: 返回新的文件偏移量
      • 失败:返回 -1

    每个打开的文件都有一个与其关联的“当前文件偏移量”。它通常是个非负整数,用于度量从文件开始处计算的字节数。通常读、写操作都从当前文件偏移量处开始,并且使偏移量增加所读写的字节数。注意:

    • 打开一个文件时,除非指定O_APPEND选项,否则系统默认将该偏移量设为0
    • 如果文件描述符指定的是一个管道、FIFO、或者网络套接字,则无法设定当前文件偏移量,则lseek将返回 -1 ,并且将 errno 设置为 ESPIPE
    • 对于普通文件,其当前文件偏移量必须是非负值。但是某些设备运行负的偏移量出现。因此比较lseek的结果时,不能根据它小于0 就认为出错。要根据是否等于 -1 来判断是否出错。
    • lseek 并不会引起任何 I/O 操作,lseek仅仅将当前文件的偏移量记录在内核中。
    • 当前文件偏移量可以大于文件的当前长度。此时对该文件的下一次写操作将家常该文件,并且在文件中构成一个空洞。空洞中的内容位于文件中但是没有被写过,其字节被读取时都被读为0
/*
    test lseek function for std file io 
*/
#include "apue.h"
int main(void)
{
    if (lseek(STDIN_FILENO,0,SEEK_CUR) == -1) /*there is must be is equal operation*/
        printf("cannot seek\n");
    else
        printf("seek ok\n");
    exit(0);
}
  1. read函数:读取文件内容

    
    #include<unistd.h>
    
    ssize_t read(int fd,void *buf,size_t nbytes);
    • 参数:
      • fd:打开的文件的文件描述符
      • buf:存放读取内容的缓冲区的地址(由程序员手动分配)
      • nbytes:期望读到的字节数
    • 返回值:
      • 成功:返回读到的字节数,若已到文件尾则返回 0
      • 失败:返回 -1

    读操作从文件的当前偏移量开始,在成功返回之前,文件的当前偏移量会增加实际读到的字节数。有多种情况可能导致实际读到的字节数少于期望读到的字节数:

    • 读普通文件时,在读到期望字节数之前到达了文件尾端
    • 当从终端设备读时,通常一次最多读取一行(终端默认是行缓冲的)
    • 当从网络读时,网络中的缓存机制可能造成返回值小于期望读到的字节数
    • 当从管道或者FIFO读时,若管道包含的字节少于所需的数量,则 read只返回实际可用的字节数
    • 当从某些面向记录的设备(如磁带)中读取时,一次最多返回一条记录
    • 当一个信号造成中断,而已读了部分数据时。
  2. write函数:想文件写数据

    
    #include<unistd.h>
    
    ssize_t write(int fd,const void *buf,size_t nbytes);
    • 参数:
      • fd:打开的文件的文件描述符
      • buf:存放待写的数据内容的缓冲区的地址(由程序员手动分配)
      • nbytes:期望写入文件的字节数
    • 返回值:
      • 成功:返回已写的字节数
      • 失败:返回 -1

    write的返回值通常都是与nbytes相同。否则表示出错。write出错的一个常见原因是磁盘写满,或者超过了一个给定进行的文件长度限制

    对于普通文件,写操作从文件的当前偏移量处开始。如果打开文件时指定了O_APPEND选项,则每次写操作之前,都会将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

三、 原子操作、同步、复制、修改文件描述符

  1. 内核使用三种数据结构描述打开文件。它们之间的关系决定了一个进程与另一个进程在打开的文件之间的相互影响。

    • 内核为每个进程分配一个进程表项(所有进程表项构成进程表),进程表中都有一个打开的文件描述符表。每个文件描述符占用一项,其内容为:
      • 文件描述符标志
      • 指向一个文件表项的指针
    • 内核为每个打开的文件分配一个文件表项(所有的文件表项构成文件表)。每个文件表项的内容包括:
      • 文件状态标志(读、写、添写、同步和阻塞等)
      • 当前文件偏移量
      • 指向该文件 v 结点表项的指针
    • 每个打开的文件或者设备都有一个 v 结点结构。 v 结点结构的内容包括:
      • 文件类型和对此文件进行各种操作函数的指针。
      • 对于大多数文件, v 结点还包含了该文件的 i 结点。

    这些信息都是在打开文件时从磁盘读入内存的。如 i 结点包含了文件的所有者、文件长度、指向文件实际数据在磁盘上所在位置的指针等等。 v 结点结构和 i 结点结构实际上代表了文件的实体。

    file_descriptor

    现在假设进程 A 打开文件 file1,返回文件描述符 3;进程 B 也打开文件 file2,返回文件描述符 4:

    • 内核在文件表上新增两个表项:
      • 这两个文件表项指向同一个 v 结点表项
      • 进程 A 、B 各自的文件描述符表项分别指向这两个文件表项;
    • 对文件的操作结果:
      • 进程 A 每次 write 之后,进程 A 对应的文件表项的当前文件偏移量即增加所写入的字节数。
        • 若这导致当前文件偏移量超过当前文件长度,则修改 i 节点的当前文件长度,设为当前文件偏移量
      • 如果进程 B 用 O_APPEND标志打开一个文件,在相应标志也设置到进程 B 对于的文件表项的文件状态标志中。
        • 每次进程 B 对具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先被置为 i 结点中的文件长度。
      • 若进程 B 用 lseek 定位到文件当前的尾端,则进程 B 对应的文件表项的当前文件偏移量设置为 i 结点中的当前长度
      • lseek 函数只是修改文件表项中的当前文件偏移量,不进行任何 I/O 操作

    可能一个进程中有多个文件描述符指向同一个文件表项。

  2. 原子操作:

    • 追加一个文件时,不能通过lseek到末尾然后write。要用O_APPEND选项打开文件,然后直接write
      • 通过lseek到末尾然后write时,如果多个进程同时执行这两个操作,则会引起竞争条件
      • 通过 O_APPEND选项打开文件,然后直接write时,内核每一次在写操作之前,都会将进程的当前偏移量设置到文件的末尾,于是就不需要执行lseek定位操作
    • pread/pwrite可以执行原子性的定位读/定位写
    • O_CREAT|O_EXCL选项打开文件时,可以原子性的检查文件是否存在和创建文件这两个操作。
  3. pread/pwrite:原子定位读和原子定位写

    
    #include<unistd.h>
    
    ssize_t pread(int fd,void*buf,size_t nbytes,off_t offset);
    ssize_t pwrite(int fd,const void*buf,size_t nbytes,off_t offset);
    • 参数:
      • fd:打开的文件描述符
      • buf:读出数据存放的缓冲区/ 写到文件的数据的缓冲区
      • nbytes:预期读出/写入文件的字节数
      • offset:从文件指定偏移量开始执行read/write
    • 返回:
      • 成功:读到的字节数/已写的字节数
      • 失败: -1

    调用pread相当于先调用lseek再调用read.但是调用pread时,无法中断其定位和读操作,并且不更新当前文件偏移量;调用pwrite相当于先调用lseek再调用write.但是调用pwrite时,无法中断其定位和写操作,并且不更新当前文件偏移量

  4. dup/dup2:复制一个现有的文件描述符:

    
    #include<unistd.h>
    
    int dup(int fd);
    int dup2(int fd,int fd2);
    • 参数:
      • fd:被复制的文件描述符(已被打开)
      • fd2:指定的新的文件描述符(待生成)
        -返回值:
      • 成功: 返回新的文件描述符
      • 失败: 返回 -1

    对于dup函数,返回的新的文件描述符一定是当前可用的文件描述符中最小的数字。对于dup2函数:

    • 如果 fd2已经是被打开的文件描述符且不等于fd,则先将其关闭,然后再打开(注意关闭再打开是一个原子操作
    • 如果 fd2等于fd,则直接返回fd2(也等于fd),而不作任何操作

    任何情况下,这个返回的新的文明描述符与参数fd共享同一个文件表项(因此文件状态标志以及文件偏移量都会共享)。 任何情况下,这个返回的新的文明描述符的close-on-exec标志总是被清除

  5. UNIX操作系统在内核中设有缓冲区,大多数磁盘 I/O 都通过缓冲区进行。当我们想文件写入数据时,内核通常都首先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式称为延迟写。

    • 当内核需要重用缓冲区来存方其他数据时,它会把所有延迟写的数据库写入磁盘
    • 你也可以调用下列函数来显式的将延迟写的数据库写入磁盘
    
    #include<unistd.h>
    
    int fsync(int fd);
    int fdatasync(int fd);
    void sync(void);
    • 参数(前两个函数):

      • fd:指定的打开的文件描述符
    • 返回值(前两个函数):

      • 成功:返回 0
      • 失败: 返回 -1

    区别:

    • sync:将所有修改过的块缓冲区排入写队列,然后返回,它并不等待时机写磁盘结束
    • fsync:只对由fd指定的单个文件起作用,等待写磁盘操作结束才返回
    • fdatasync:只对由fd指定的单个文件起作用,等待写磁盘操作结束才返回,但是它只影响文件的数据部分(fsync会同步更新文件的属性)

    update 守护进程会周期性的调用sync函数。命令sync也会调用sync函数

  6. fcntl函数:改变已经打开的文件的属性

    
    #include<fcntl.h>
    
    int fcntl(int fd,int cmd,.../* int arg */);
    • 参数:

      • fd:已打开文件的描述符
      • cmd:有下列若干种:
        • F_DUPEF常量:复制文件描述符 fd。新文件描述符作为函数值返回。它是尚未打开的个描述符中大于或等于arg中的最小值。新文件描述符与fd共享同一个文件表项,但是新描述符有自己的一套文件描述符标志,其中FD_CLOEXEC文件描述符标志被清除
        • F_DUPFD_CLOEXEC常量:复制文件描述符。新文件描述符作为函数值返回。它是尚未打开的个描述符中大于或等于arg中的最小值。新文件描述符与fd共享同一个文件表项,但是新描述符有自己的一套文件描述符标志,其中FD_CLOEXEC文件描述符标志被设置
        • F_GETFD常量:对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC
        • F_SETFD常量:设置fd的文件描述符标志为arg
        • F_GETFL常量:返回fd的文件状态标志。文件状态标志必须首先用屏蔽字 O_ACCMODE 取得访问方式位,
          然后与O_RDONLYO_WRONLYO_RDWRO_EXECO_SEARCH比较
          (这5个值互斥,且并不是各占1位)。剩下的还有:O_APPENDO_NONBLOCKO_SYNC
          O_DSYNCO_RSYNCF_ASYNCO_ASYNC
        • F_SETFL常量:设置fd的文件状态标志为 arg。可以更改的标志是:
          O_APPENDO_NONBLOCKO_SYNCO_DSYNCO_RSYNCF_ASYNCO_ASYNC
        • F_GETOWN常量:获取当前接收 SIGIOSIGURG信号的进程 ID或者进程组 ID
        • F_SETOWN常量:设置当前接收 SIGIOSIGURG信号的进程 ID或者进程组 IDarg。若 arg是个正值,则设定进程 ID;若 arg是个负值,则设定进程组ID
        • F_GETLKF_SETLKF_SETLKW:获取/设置文件记录锁
      • arg:依赖于具体的命令
    • 返回值:

      • 成功: 依赖于具体的命令
      • 失败: 返回 -1
    
    #include <stdio.h>
    
    
    #include<fcntl.h>
    
    
    #include<unistd.h>
    
    
    #include<string.h>
    
    
    #include<errno.h>
    
    void print_error(int fd,const char* action,int result)
    {
       if(result==-1)
       {
           printf("\t %s on fd(%d) error:beause %s!\n",action,fd,strerror(errno));
       }
    }
    
    void test_get_fd(int fd)
    {
        printf("\tget_fd on fd(%d):",fd);
        int result;
        result=fcntl(fd,F_GETFD);
        print_error(fd,"F_GETFD",result);
        if(result!=-1)
           printf("return:%d !\n",result);
    
    }
    void test_set_fd(int fd, int flag)
    {
       printf("\tset_fd on fd(%d) of flag(%d):",fd,flag);
      int result;
       result=fcntl(fd,F_SETFD,flag);
      print_error(fd,"F_SETFD",result);
      if(result!=-1)
            printf("set_fd ok !\n");
    }
    int test_dup_fd(int fd,int min_fd)
    {
        printf("\tdup_fd on fd(%d),set min_fd(%d),:",fd,min_fd);
       int result;
       result=fcntl(fd,F_DUPFD,min_fd);
       print_error(fd,"F_DUPFD",result);
      if(result!=-1)
            printf("return:%d !\n",result);
      return result;
    }
    int test_dup_exec_fd(int fd,int min_fd)
    {
       printf("\tdup_exec_fd on fd(%d),set min_fd(%d),:",fd,min_fd);
      int result;
      result=fcntl(fd,F_DUPFD_CLOEXEC,min_fd);
      print_error(fd,"F_DUPFD_CLOEXEC",result);
      if(result!=-1)
         printf("return:%d !\n",result);
      return result;
    }
    void test_get_fl(int fd)
    {
       printf("\tget_fl on fd(%d):",fd);
       int result;
      result=fcntl(fd,F_GETFL);
      print_error(fd,"F_GETFL",result);
      if(result!=-1)
      {
         printf("F_GETFL on fd(%d) has ",fd);
            if(result&O_APPEND) printf("\tO_APPEND;");
            if(result&O_NONBLOCK) printf("\tO_NONBLOCK;");
            if(result&O_SYNC) printf("\tO_SYNC;");
            if(result&O_DSYNC) printf("\tO_DSYNC;");
            if(result&O_RSYNC) printf("\tO_RSYNC;");
            if(result&O_FSYNC) printf("\tO_FSYNC;");
            if(result&O_ASYNC) printf("\thas O_ASYNC;");
            if((result&O_ACCMODE)==O_RDONLY)printf("\tO_RDONLY;");
           if((result&O_ACCMODE)==O_WRONLY)printf("\thas O_WRONLY;");
           if((result&O_ACCMODE)==O_RDWR)printf("\tO_RDWR;");
           printf("\n");
      }
    }
    void test_set_fl(int fd,int flag)
    {
        printf("\tset_fl on fd(%d) of flag(%d):",fd,flag);
       int result;
       result=fcntl(fd,F_SETFL,flag);
      print_error(fd,"F_SETFL",result);
       if(result!=-1)
         printf("set_fl ok !\n");
    }
    void test_get_own(int fd)
    {
       printf("\tget_own on fd(%d):",fd);
      int result;
      result=fcntl(fd,F_GETOWN);
      print_error(fd,"F_GETOWN",result);
      if(result!=-1)
               printf("return:%d !\n",result);
    }
    void test_set_own(int fd,int pid)
    {
        printf("\tset_own on fd(%d) of pid(%d):",fd,pid);
       int result;
       result=fcntl(fd,F_SETOWN,pid);
       print_error(fd,"F_SETOWN",result);
      if(result!=-1)
             printf("set_own ok !\n");
    }
    int main(int argc, char *argv[])
    {
        int fd;
      fd=openat(AT_FDCWD,"test.txt",O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR);
      printf("Test dup:\n");
      test_get_fd(test_dup_fd(fd,10));
      test_get_fd(test_dup_fd(fd,0));
      test_get_fd(test_dup_exec_fd(fd,10));
      test_get_fd(test_dup_exec_fd(fd,0));
     printf("Test set_get_fd:\n");
     test_get_fd(fd);
      test_set_fd(fd,~FD_CLOEXEC);
      test_get_fd(fd);
      test_set_fd(fd,FD_CLOEXEC);
      test_get_fd(fd);
      printf("Test set_get_fl:\n");
      test_get_fl(fd);
        test_set_fl(fd,O_RDWR);
     test_get_fl(fd);
        test_set_fl(fd,O_RDONLY|O_NONBLOCK);
     test_get_fl(fd);
     printf("Test set_get own:\n");
     test_get_own(fd);
     test_set_fl(fd,1);
     test_get_own(fd);
        return 0;
    }

    fcntl

    注意:

    • Linux 下,不支持文件状态标志: F_EXEC与F_SEARCH
    • (result&O_ACCMODE)==O_RDONLY 表达式中, &优先级较低
    • F_SETFL命令:当文件读打开时,你无法将文件状态标志修改为O_WRONLYO_WRWR这两种中任何一个。你只能修改:O_APPENDO_NONBLOCKO_SYNCO_DSYNCO_RSYNCF_ASYNCO_ASYNC等标志
  7. /dev/fd目录:该目录下是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符(假定描述符n是打开的)

    • fd=open("/dev/fd/0",mod)fd和文件描述符0共享同一个文件表项。
    • 大多数系统忽略mod参数
    • 在 Linux 操作系统上, /dev/fd/0是个例外,它是个底层物理文件的符号链接。因此在它上面调用creat 会导致底层文件被截断
本书全面介绍了UNIX系统的程序设计界面—系统调用界面和标准C库提供的许多函数。 本书的前15章着重于理论知识的阐述,主要内容包括UNIX文件和目录、进程环境、进程控制、 进程间通信以及各种I/O。在此基础上,分别按章介绍了多个应用实例,包括如何创建数据库函数库, PostScript 打印机驱动程序,调制解调器拨号器及在伪终端上运行其他程序的程序等。 本书内容丰富权威, 概念清晰精辟,一直以来被誉为UNIX编程的“圣经”,对于所有UNIX程序员—无论是初学者还是专家级人士 —都是一本无价的参考书籍。 目 录 译者序 译者简介 前言 第1章 UNIX基础知识 1 1.1 引言 1 1.2 登录 1 1.2.1 登录名 1 1.2.2 shell 1 1.3 文件和目录 2 1.3.1 文件系统 2 1.3.2 文件名 2 1.3.3 路径名 2 1.3.4 工作目录 4 1.3.5 起始目录 4 1.4 输入和输出 5 1.4.1 文件描述符 5 1.4.2 标准输入、标准输出和标准 出错 5 1.4.3 不用缓存的I/O 5 1.4.4 标准I/O 6 1.5 程序和进程 7 1.5.1 程序 7 1.5.2 进程和进程ID 7 1.5.3 进程控制 7 1.6 ANSI C 9 1.6.1 函数原型 9 1.6.2 类属指针 9 1.6.3 原始系统数据类型 10 1.7 出错处理 10 1.8 用户标识 11 1.8.1 用户ID 11 1.8.2 组ID 12 1.8.3 添加组ID 12 1.9 信号 12 1.10 UNIX时间值 14 1.11 系统调用和库函数 14 1.12 小结 16 习题 16 第2章 UNIX标准化及实现 17 2.1 引言 17 2.2 UNIX标准化 17 2.2.1 ANSI C 17 2.2.2 IEEE POSIX 18 2.2.3 X/Open XPG3 19 2.2.4 FIPS 19 2.3 UNIX实现 19 2.3.1 SVR4 20 2.3.2 4.3+BSD 20 2.4 标准和实现的关系 21 2.5 限制 21 2.5.1 ANSI C限制 22 2.5.2 POSIX限制 22 2.5.3 XPG3限制 24 2.5.4 sysconf、pathconf 和fpathconf 函数 24 2.5.5 FIPS 151-1要求 28 2.5.6 限制总结 28 2.5.7 未确定的运行时间限制 29 2.6 功能测试宏 32 2.7 基本系统数据类型 32 2.8 标准之间的冲突 33 2.9 小结 34 习题 34 第3章 文件I/O 35 3.1 引言 35 3.2 文件描述符 35 3.3 open函数 35 3.4 creat函数 37 3.5 close函数 37 3.6 lseek函数 38 3.7 read函数 40 3.8 write函数 41 3.9 I/O的效率 41 3.10 文件共享 42 3.11 原子操作 45 3.11.1 添加至一个文件 45 3.11.2 创建一个文件 45 3.12 dup和dup2函数 46 3.13 fcntl函数 47 3.14 ioctl函数 50 3.15 /dev/fd 51 3.16 小结 52 习题 52 第4章 文件和目录 54 4.1 引言 54 4.2 stat, fstat和lstat函数 54 4.3 文件类型 55 4.4 设置-用户-ID和设置-组-ID 57 4.5 文件存取许可权 58 4.6 新文件和目录的所有权 60 4.7 access函数 60 4.8 umask函数 62 4.9 chmod和fchmod函数 63 4.10 粘住位 65 4.11 chown, fchown和 lchown函数 66 4.12 文件长度 67 4.13 文件截短 68 4.14 文件系统 69 4.15 link, unlink, remove和rename 函数 71 4.16 符号连接 73 4.17 symlink 和readlink函数 76 4.18 文件的时间 76 4.19 utime函数 78 4.20 mkdir和rmdir函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值