LINUX输入输出与文件

1 文件描述符

内核为每个进程维护一个已打开文件的记录表(实现为结构体数组),文件描述符是一个较小的正整数(0—1023)(结构体数组下标),它代表记录表的一项,通过文件描述符和一组基于文件描述符的文件操作函数,就可以实现对文件的读、写、创建、删除等操作。

常用基于文件描述符的函数有open(打开)、creat(创建)、close(关闭)、read(读取)、write(写入)、ftruncate(改变文件大小)、lseek(定位)、fsync(同步)、fstat(获取文件状态)、fchmod(权限)、flock(加锁)、fcntl(控制文件属性)、dup(复制)、dup2、select和ioctl。基于文件描述符的文件操作并非ANSI C的函数。

2 打开、创建和关闭文件

open和creat都能打开和创建函数,原型为:

#include <sys/types.h>   //头文件
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);        //文件名  打开方式
int open(const char *pathname, int flags, mode_t mode);//文件名  打开方式  权限
int creat(const char *pathname, mode_t mode);    //文件名  权限   //现在已经不常用了
creat函数等价于open(pathname,O_CREAT|O_TRUNC|O_WRONLY,mode);

open()函数出错时返回-1,相关参数如下:

flags和mode都是一组掩码的合成值,flags表示打开或创建的方式,而mode表示文件的访问权限。

flags的可选项如下:

掩码

含义

O_RDONLY

以只读的方式打开

O_WRONLY

以只写的方式打开

O_RDWR

以读写的方式打开

O_CREAT

以写方式打开文件,如果文件不存在,则创建文件(注意要指定权限)

O_EXCL

仅与O_CREAT连用,如果文件已存在,则强制open失败

O_TRUNC

如果文件存在,将文件的长度截至0,即将所有内容清空。(不加O_TRUNC的话,从文件开头从前往后覆盖,原文件没被覆盖的内容仍然存在)

O_APPEND

已追加的方式打开文件,每次调用write时,文件指针自动先移到文件尾,用于多进程写同一个文件的情况。

O_NONBLOCK

非阻塞方式打开,无论有无数据读取或等待,都会立即返回进程之中。

O_NODELAY

非阻塞方式打开

O_SYNC

同步打开文件,只有在数据被真正写入物理设备设备后才返回

mode的可选项有:

S_IRWXU 00700 权限,代表该文件所有者具有可读、可写及可执行的权限。
S_IRUSR 或S_IREAD,00400权限,代表该文件所有者具有可读取的权限。
S_IWUSR 或S_IWRITE,00200权限,代表该文件所有者具有可写入的权限。
S_IXUSR 或S_IEXEC,00100权限,代表该文件所有者具有可执行的权限。

S_IRWXG 00070权限,代表该文件用户组具有可读、可写及可执行的权限。
S_IRGRP  00040权限,代表该文件用户组具有可读的权限。
S_IWGRP 00020权限,代表该文件用户组具有可写入的权限。
S_IXGRP  00010权限,代表该文件用户组具有可执行的权限。

S_IRWXO 00007权限,代表其他用户具有可读、可写及可执行的权限。
S_IROTH  00004权限,代表其他用户具有可读的权限
S_IWOTH 00002权限,代表其他用户具有可写入的权限。
S_IXOTH  00001 权限,代表其他用户具有可执行的权限。

但是通常采用直接赋数值的形式,如:

int fd = open(“1.txt”,O_WRONLY | O_CREAT,0755);  //表示给755的权限
if(-1 == fd)
{
    perror("open failed!\n");
    exit(-1);
}

注意:LINUX中基于文件描述符的open函数,对于一个不存在的文件,不能通过O_WRONLY的方式打开,必须加上O_CREAT选项。

close用于文件的关闭:

int close(int fd);//fd表示文件描述词,是先前由open或creat创建文件时的返回值。

文件使用完毕后,应该调用close关闭它,一旦调用close,则该进程对文件所加的锁全都被释放,并且使文件的打开引用计数减1,只有文件的打开引用计数变为0以后,文件才会被真正的关闭。

Kris_示例代码_1

/*************************************************************************
    > File Name: my_open.c
    > Author: KrisChou
    > Mail:zhoujx0219@163.com 
    > Created Time: Mon 18 Aug 2014 10:27:49 AM CST
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
    /* 打开文件
    int fd; //描述符
    fd = open(argv[1],O_RDONLY);
    if(fd == -1)
    {
        perror("open failed");
        exit(1);
    }
    return 0;
    */
    
    int fd;
    fd = open(argv[1],O_WRONLY | O_CREAT,0666);
    if(fd == -1)
    {
        perror("open failed");
        exit(1);
    }
    return 0;

}

3 读写文件

读写文件的函数原型为:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);//文件描述词  缓冲区  长度
ssize_t write(int fd, const void *buf, size_t count);

对于read和write函数,出错返回-1,读取完了之后,返回0,其他情况返回读写的个数。

Kris_示例代码_2

/*************************************************************************
    > File Name: my_read.c
    > Author: KrisChou
    > Mail:zhoujx0219@163.com 
    > Created Time: Mon 18 Aug 2014 11:11:48 AM CST
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
    
    int fd;//描述符
    char buf[128];
    int readn;
    fd = open(argv[1],O_RDONLY);
    if(fd == -1)
    {
        perror("open failed");
        exit(1);
    }
    while(memset(buf,0,128))
    {
        readn = read(fd,buf,127);
        printf("readn : %d : %s \n",readn,buf);
        if(readn == 0)
        {
            printf("read end of file! \n");
            break;
        }
    }
    close(fd);
    return 0;
}

Kris_示例代码_3

/*************************************************************************
    > File Name: my_write.c
    > Author: KrisChou
    > Mail:zhoujx0219@163.com 
    > Created Time: Mon 18 Aug 2014 11:35:36 AM CST
 ************************************************************************/

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;
    int fd_2;
    char buf[128];
    int read_number;
    int write_number;
    fd = open(argv[1],O_RDONLY);
    fd_2 = open(argv[2],O_WRONLY | O_CREAT, 0666);
    if(fd == -1 || fd_2 == -1)
    {
        perror("open failed");
        exit(1);
    }
    while(memset(buf,0,128))
    {
        read_number = read(fd,buf,127);
        printf("read_number: %d \n",read_number);
        if(read_number == 0)
        {
            printf("read end of file! \n");
            break;

        }
        write_number = write(fd_2,buf,read_number);
        printf("write: %d \n",write_number);
    }
    close(fd);
    close(fd_2);    
}

4 获取文件信息

可以通过fstat和stat函数获取文件信息,调用完毕后,文件信息被填充到结构体struct stat变量中,函数原型为:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *file_name, struct stat *buf);    //文件名      stat结构体指针
int fstat(int fd, struct stat *buf);                  //文件描述词  stat结构体指针

结构体stat的定义为:

struct stat {
           dev_t         st_dev;      /*如果是设备,返回设备表述符,否则为0*/
           ino_t         st_ino;      /* i节点号 */
           mode_t        st_mode;     /* 文件类型 */
           nlink_t       st_nlink;    /* 链接数 */
           uid_t         st_uid;      /* 属主ID */
           gid_t         st_gid;      /* 组ID */
           dev_t         st_rdev;     /* 设备类型*/
           off_t         st_size;     /* 文件大小,字节表示 */
           blksize_t     st_blksize;  /* 块大小*/
           blkcnt_t      st_blocks;   /* 块数 */
           time_t        st_atime;    /* 最后访问时间*/
           time_t        st_mtime;    /* 最后修改时间*/
           time_t        st_ctime;    /* 创建时间 */
};

对于结构体的成员st_mode,有一组宏可以进行文件类型的判断

描述

S_ISLNK(mode)

判断是否是符号链接

S_ISREG(mode)

判断是否是普通文件

S_ISDIR(mode)

判断是否是目录

S_ISCHR(mode)

判断是否是字符型设备

S_ISBLK(mode)

判断是否是块设备

S_ISFIFO(mode)

判断是否是命名管道

S_ISSOCK(mode)

判断是否是套接字

同样有一组宏可以进行文件权限的判断

S_IRUSR     00400     owner has read permission
S_IWUSR    00200     owner has write permission
S_IXUSR     00100     owner has execute permission

S_IRGRP     00040     group has read permission
S_IWGRP    00020     group has write permission
S_IXGRP     00010     group has execute permission

S_IROTH    00004     others have read permission
S_IWOTH   00002     others have write permission
S_IXOTH    00001     others have execute permission

实例

如何编写 ls –l

思路

在struct stat中,文件所有者和组是以ID的形式存在的,然而ls要求输出用户名和组名,如何根据ID找到用户名和组名呢?

可以试着在联机帮助中查找关键字username、uid、group,看看又什么结果。不同的系统中可能有不同的结果。下面是一些说明:

1 /etc/passwd包含用户列表

回想一下登录过程,输入用户名和密码,经过验证后登录成功,出现提示符。那么系统是怎么知道用户名和密码是正确的呢?

这就涉及到/etc/passwd这个文件,它包含了系统中所有的用户信息,下面是一个例子:

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
purple:x:500:500:purple:/home/purple:/bin/bash
newUsr1:x:501:501::/home/newUsr1:/bin/bash
test_user_3:x:502:502::/home/test_user_3:/bin/bash

这是个纯文本文件,每一行代表一个用户,用冒号“:”分成不同的字段,第一个字段是用户名,第二个字段是密码,第三个字段是用户ID,第四个字段是所属的组,接下来的是用户的全名、主目录以及登录shell。所有的用户对这个文件都有读权限。

似乎使用这个文件就可以解决用户ID和用户名的关联问题,只需搜索用户ID,然后就可以得到相应地用户名。然而在实际应用中并不是这样做的,搜索文件是一件很繁琐的工作,而且对于很多网络计算系统,这种方法是不起作用的。

2 /etc/passwd并没有包含所有的用户

每个Unix系统都有/etc/passwd这个文件,但它并没有包括所有的用户,在有些网咯计算系统中,用户需要能够登录到系统中的任何一台主机上,如果通过/etc/passwd来实现,就必须在所有主机上维护用户信息,要修改密码的话也必须修改所有主机上的密码,如果有一台主机刚好宕机了,那么在宕机期间的用户变化情况就无法同步到这台主机上。

较好的解决方法是在一台大家都能够访问到的主机上保存所有用户信息,它被称作NIS,所有的主机通过NIS来进行用户身份验证。所有需要用户信息的程序也从NIS上获取。而本地只保存所有用户的一个子集以备离线操作。

3 通过getpwuid来得到完整的用户列表

可以通过库函数getpwuid来访问用户信息,如果用户信息保存在/etc/passwd中,那么getpwuid会查找/etc/passwd的内容,如果用户信息在NIS中,getpwuid会从NIS中获取信息,所以用getpwuid使程序有很好的可移植性。

getpwuid需要UID(user ID)作为参数,返回一个指向struct passwd结构体的指针,这个结构定义在/usr/include/pwd.h中:

The passwd structure is defined in <pwd.h> as follows:

struct passwd {
    char   *pw_name;       /* username */
    char   *pw_passwd;     /* user password */
    uid_t   pw_uid;        /* user ID */
    gid_t   pw_gid;        /* group ID */
    char   *pw_gecos;      /* real name */
    char   *pw_dir;        /* home directory */
    char   *pw_shell;      /* shell program */
};

tips: getpwuid() functions return a pointer to a passwd structure, or NULL if the matching entry  is  not  found  or  an  error occurs.

而这正是ls –l 的输出中需要的:

/**
 * return a username associated with the specified uid
 * NOTE: doesn't work if there is no username
 */
char *uid_to_name(uid_t uid)
{
    return getpwuid(uid)->pw_name;     
}

接下来我们来讨论组ID如何处理。

4 /etc/group是组的列表

对一台公司的主机而言,可能要将用户分为不同的组,如销售人员一组、行政人员一组等。要是在学校里,可能有教师组和学生组。Unix提供了进行组管理的手段,文件/etc/group是一个保存所有的组信息的文本文件:

root:x:0:root
bin:x:1:root,bin,daemon
daemon:x:2:root,bin,daemon
sys:x:3:root,bin,adm
adm:x:4:root,adm,daemon
tty:x:5:
disk:x:6:root
lp:x:7:daemon,lp
purple:x:500:
newUsr1:x:501:
zjxgroup:x:502:
其中第一个字段是组名,第二个字段是组密码,这个字段极少用到,第三个字段是组ID(GID),第四个是组中的成员列表。

5 用户可以同时属于多个组

passwd文件中有每个用户所属的组,实际上那里列出的是用户的主组(primary group)。用户还可以是其他组的成员,要将用户添加到组中,只要把它的用户名添加到/etc/group中这个组所在行的最后一个字段即可。这个列表在处理访问权限时会被用到。例如一个文件属于sys组,且组成员有这个文件的写权限,则用户adm就可以修改这个文件。

6 通过getgrgid来访问组列表

在网络计算系统中,组信息也被保存在NIS中。与getpwuid类似,Unix系统提供getgrgid函数屏蔽掉实现的差异。用这个函数,用户可以得到组名而不用操心实现的细节。

The group structure is defined in <grp.h> as follows:

struct group {
    char   *gr_name;       /* group name */
    char   *gr_passwd;     /* group password */
    gid_t   gr_gid;        /* group ID */
    char  **gr_mem;        /* group members */
};

tips: getgrgid() functions return a pointer to a group structure, or NULL if the matching entry  is  not  found  or  an  error occurs.

在ls –l 中,可以这样得到组名:

/**
 * returns a groupname associated with the specified gid
 * NOTE: doesn't work if there is no groupname
 */
 char* gid_to_name(gid_t gid)
 {
         return getgrgid(gid)->gr_name;
 }

kris_示例代码_4

/*************************************************************************
  > File Name: my_stat.c
  > Author: KrisChou
  > Mail:zhoujx0219@163.com 
  > Created Time: Mon 18 Aug 2014 09:45:09 PM CST
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>

void mode_to_str(mode_t mode, char *dest);
char *time_format(char *src);

int main(int argc, char *argv[])
{
    struct stat my_stat;
    char buf[11] = "";
    char *pstr;
    memset(&my_stat,0,sizeof(my_stat));
    if(stat(argv[1], &my_stat) == -1)
    {
        perror("stat:");
        exit(1);
    }
    printf("ino: %u\n", my_stat.st_ino);

    mode_to_str(my_stat.st_mode, buf);
    printf("mode: %s\n",buf);

    printf("nlink: %u\n", my_stat.st_nlink);
    printf("uid: %s, gid: %s\n",getpwuid(my_stat.st_gid)->pw_name,getgrgid(my_stat.st_gid)->gr_name);  
    printf("size: %u\n",my_stat.st_size);
    pstr = ctime(&(my_stat.st_atime)); //Mon Aug 18 09:45:09 2014
    pstr = time_format(pstr);
    printf("atime: %s\n",pstr);

    printf("------------------------------------------------------------\n");
    printf("%10s.%2d%7s%7s%5d%13s %s\n",buf, my_stat.st_nlink, getpwuid(my_stat.st_uid)->pw_name, getgrgid(my_stat.st_gid) ->gr_name, my_stat.st_size, pstr,argv[1]);
    return 0;
}

void mode_to_str(mode_t mode, char* dest)
{
    memset(dest,'-',10);
    printf("dest: %s\n", dest);//----------
    if(S_ISDIR(mode))
    {
        dest[0]='d';
    }
    if(S_ISREG(mode))
    {
        dest[0]='-';
    }

    if(mode & S_IRUSR)
    {
        dest[1] = 'r' ;
    }
    if(mode & S_IWUSR)
    {
        dest[2] = 'w' ;
    }
    if(mode & S_IXUSR)
    {
        dest[3] = 'x' ;
    }
    if(mode & S_IRGRP)
    {
        dest[4] = 'r' ;
    }
    if(mode & S_IWGRP)
    {
        dest[5] = 'w' ;
    }
    if(mode & S_IXGRP)
    {
        dest[6] = 'x' ;
    }
    if(mode & S_IROTH)
    {
        dest[7] = 'r' ;
    }
    if(mode & S_IWOTH)
    {
        dest[8] = 'w' ;
    }
    if(mode & S_IXOTH)
    {
        dest[9] = 'x' ;
    }
}

char* time_format(char* src)
{
    int index = strlen(src) - 1 ;
    for(; src[index]!=':'; index -- )
    {

    }
    src[index] = '\0' ;
    return src + 4 ;
}

运行结果如下:

[purple@localhost 0818]$ gcc my_stat.c
[purple@localhost 0818]$ ./a.out my_stat.c
ino: 271579
dest: ----------
mode: -rw-rw-r--
nlink: 1
uid: purple, gid: purple
size: 2114
atime: Aug 18 22:33
------------------------------------------------------------
-rw-rw-r--. 1 purple purple 2114 Aug 18 22:33 my_stat.c

5 文件描述符的复制

系统调用函数dup和dup2可以实现文件描述符的复制,经常用来重定向进程的stdin(0),stdout(1),stderr(2)。

dup返回新的文件描述符(没有使用的文件描述符的最小编号)。这个新的描述符是旧文件描述符的拷贝。这意味着两个描述符共享同一个数据结构。

dup2允许调用者用一个有效描述符(oldfd)和目标描述符(newfd),函数成功返回时,目标描述符将变成旧描述符的复制品,此时两个文件描述符现在都指向同一个文件,并且是函数第一个参数(也就是oldfd)指向的文件。

原型为:

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

文件描述符的复制是指用另外一个文件描述符指向同一个打开的文件,它完全不同于直接给文件描述符变量赋值。看以下例子:

char szBuf[32];
int fd=open(“./a.txt”,O_RDONLY);
int fd2=fd;   //类似于C语言的指针赋值,当释放掉一个得时候,另一个已经不能操作了
close(fd);    //导致文件立即关闭
printf(“read:%d\n”,read(fd2,szBuf,sizeof(szBuf)-1);    //读取失败
close(fd2);   //无意义

在此情况下,两个文件描述符变量的值相同,指向同一个打开的文件,但是内核的文件打开引用计数还是为1,所以close(fd)或者close(fd2)都会导致文件立即关闭掉。

描述符的复制:

char szBuf[32];
int fd=open(“./a.txt”,O_RDONLY);
int fd2=dup(fd);            //内核的文件打开引用计算+1,变成2了
close(fd);                //当前还不会导致文件被关闭,此时通过fd2照样可以访问文件
printf(“read:%d\n”,read(fd2,szBuf,sizeof(szBuf)-1);
close(fd2);                //内核的引用计数变为0,文件正式关闭

示例

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char *argv[])
{
    char szBuf[32] = {0};
    int fda = open("./a.txt",O_RDWR); //假设a.txt的内容为:hello world
    int fdaa = dup(fda);
    read(fda, szBuf, 4);
    puts(szBuf);                    //关闭之前先输入原来的内容
    close(fda);
        
//    lseek(fdaa, 0, SEEK_SET);
    read(fdaa,szBuf,sizeof(szBuf));
    puts(szBuf);                    //输出现在的内容
     close(fdaa);
}

解析:假设a.txt中的内容为:hello world。上面的例子会发现第一次输出的结果是hell。关闭close(fda)的时候,文件实际上还没有真正的关闭,此时文件指针已经向后移动了。执行第二次read命令将o world读出来,最后关闭fdaa。

dup有时会用在一些特定的场合,如下:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
        int fd = open("a.txt", O_WRONLY | O_CREAT);
        if(fd == -1)
        {
                perror("open error");
                exit(-1);
        }
        printf("\n");    /* 必不可少 */
        close(1);
        int fd2 = dup(fd);
        close(fd);
        printf("hello world\n");
        close(fd2);
        return 0;
}

该程序首先打开了一个文件,返回一个文件描述符,因为默认的就打开了0,1,2表示标准输入,标准输出,标准错误输出。而用close(1);则表示关闭标准输出,此时这个文件描述符就空着了。后面又用dup,此时dup(fd);则会复制一个文件描述符到当前未打开的最小描述符,此时这个描述符为1.后面关闭fd自身(不关也不影响程序结果),然后在用标准输出的时候,发现标准输出重定向到你指定的文件了。那么printf所输出的内容也就直接输出到文件了。

dup2(int fdold,int fdnew)也是进行描述符的复制,只不过采用此种复制,新的描述符由用户用参数fdnew显示指定,而不是象dup一样由内核帮你选定(内核选定的是随机的)。

对于dup2,如果fdnew已经指向一个已经打开的文件,内核会首先关闭掉fdnew所指向的原来的文件。此时再针对于fdnew文件描述符操作的文件,则采用的是fdold的文件描述符。如果成功dup2的返回值于fdnew相同,否则为-1。

看以下程序:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char *argv[])
{
    char szBuf[32]={0};
    int fda=open("./a.txt",O_RDONLY);
    int fdb=open("./b.txt",O_RDONLY);
    int fdbb=dup(fdb);
    int fda2=dup2(fda,fdb);  //可以设定为:int fda2 = dup2(fda,5);即自己设为5
    
    printf("fda:%d fdb:%d fdbb:%d fda2:%d",fda,fdb,fdbb,fda2);
    read(fdb,szBuf,sizeof(szBuf)-1);        //此时fdb已经不再定位b.txt而是a.txt
    printf("result:%s\n",szBuf);
    
    close(fda);
    close(fdb);
    close(fdbb);
    close(fda2);
}

输出:

fda:3 fdb:4 fdbb:5 fda2:4 result:how are you

Note:

1

系统定义:

#define STDIN_FILENO    0       -->对应文件指针为 stdin

#define STDOUT_FILENO 1       -->对应文件指针为 stdout

#define STDOUT_FILENO 2       -->对应文件指针为 stderr

2

Linux下文件指针与文件描述符是可以通过函数进行转换的

fd <—> fp

fdopen

fileno

3

char buf[128];

readn = read(0,buf,128); //从键盘读

/* 如果用printf打印,必须有这一步 */

buf[readn] = ‘\0’;      

printf(“%s ”,buf);   

/* 当输入hello world\n 时,write不打印\n,此处用\n刷新缓冲区 */   

write(0,buf,readn)

4

以下代码,在linux下输出不了hello world,因为缓冲区并没有满

printf(“hello world !”)

while(1){}

转载于:https://www.cnblogs.com/jianxinzhou/p/3919300.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值