编写 ls -l

 作者:高玉涵
 时间:2022.3.27 16:23
 博客:blog.csdn.net/cg_i
 环境:Linux ubuntu 4.15.0-163-generic #171-Ubuntu SMP Fri Nov 5 11:53:11 UTC 2021 i686 i686 i686 GNU/Linux

 ls 要做两件事情 ,一是列出目录的内容,二是显示 文件的详细信息,这实际上是两件不同的工作,目录包含文件名,文件信息则需要从另外的途径获得,接下来从 3 个问题入手,来解决文件信息显示的问题。

1.1 问题 1:ls -l 能做些什么

 先来看 ls -l 的输出 :

gao@ubuntu:~/c$ ls -al
总用量 80
drwxrwxr-x 3 gao gao 4096 3月  27 16:34 .
drwxr-xr-x 8 gao gao 4096 3月  27 16:01 ..
-rw-r--r-- 1 gao gao 7548 3月  27 14:53 copy.of.cp1
-rwxrwxr-x 1 gao gao 7548 3月  27 14:53 cp1
-rw-rw-r-- 1 gao gao 1037 3月  27 14:52 cp1.c
drwxrwxr-x 2 gao gao 4096 3月  27 16:34 dir
-rwxrwxr-x 1 gao gao 7392 3月  27 16:11 ls1
-rw-rw-r-- 1 gao gao  754 3月  27 16:11 ls1.c
-rwxrwxr-x 1 gao gao 7488 3月  17 22:12 who1
-rw-r--r-- 1 gao gao 1285 3月  27 15:05 who1.c
-rwxrwxr-x 1 gao gao 7544 3月  27 15:16 who2
-rw-rw-r-- 1 gao gao 1890 3月  27 15:16 who2.c
-rwxrwxr-x 1 gao gao 7416 3月  17 22:03 who_macos
-rw-r--r-- 1 gao gao  390 3月  17 22:03 who_macos.c
gao@ubuntu:~/c$ 

 每行都包含 7 个字段。

模式(mode)每行的第一个字符表示 文件类型。“-”代表普通文件,“d“代表目录,等等。
接下来的 9 个字符表示 文件访问权限,分为读权限、写权限和执行权限,又分别针对 3 种对象:用户、同组用户和其它用户,所以一共需要 9 位来表示。从前面 ls -l 的输出中可以看出,所有文件和目录对所有用户都是可读的,只有文件的所有者才能对文件进行修改,所有用户都有 tail 的执行权限。
链接数(links)链接数指的是该文件被引用的次数。
文件所有者(owner)指出文件所有者的用户名。
组(group)指文件所有者所在的组。
大小(size)第 5 列显示文件的大小。在前面的 ls -l 的输出中,所有的目录大小相等,都是 4096 字节(因系统而异),因为目录所占空间的分配是以块(block)为单位,每个块 512 字节,所以目录的大小径常是相等的。如果是一般文件,size 列显示了文件中数据 的实际 字节数。
最后修改时间(last-modified)对于较老的文件,只能列出月、日和年。
文件名(name)文件名。
1.2 问题 2:ls -l 是如何 工作的

 如何得到文件的信息呢?在键盘上输入:

$man -k file|grep -i information

 在有些系统上可以得到有用的参考信息,有些却不可以。因为这些系统使用的术语是文件状态(file status)而不是文件信息(file information)或者文件属性(file properties)来代表文件的各种信息。提取文件状态的系统调用是 stat。

1.3 用 stat 得到文件信息

 stat 的工作方式。

stat(name, ptr)

 将 name 所指定的文件信息读入一个结构中。

 磁盘上的文件有很多属性,如文件大小、文件所有者的 ID 等。如果需要得到文件属性,进程可以定义一个结构 struct stat,然后调用 stat,告诉内核把文件属性放到这个结构中。

stat
目标得到文件的属性。
头文件#include <sys/stat.h>
函数原型int result = stat(char *fname, struct stat *bufp)
参数fname 文件名
bufp 指向 buffer 的指针
返回值-1 遇到错误
0 成功返回

 stat 把文件 fname 的信息复制到指针 bufp 所指的结构中。下面的代码展示了如何用 stat 来得到文件的大小:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main()
{
	struct stat infobuf;

	if( stat( "/etc/passwd", &infobuf ) == -1 )
		perror( "/etc/passwd" );
	else
		printf( "The size of /etc/passwd is %d\n", infobuf.st_size );
}

 stat 把文件的信息复制到结构 infobuf 中,程序从成员变量 st_size 中读到文件大小。

1.4 stat 提供的其他信息

 stat 的联机帮助和头文件 /usr/include/sys/stat.h 描述了 struct stat 的成员变量:

st_mode文件类型和许可权限
st_uid用户所有者的 ID
st_gid所属组的 ID
st_size所占的字节数
st_nlink文件链接数
st_mtime文件最后修改时间
st_atime文件最后访问时间
st_ctime文件属性最后改变的时间

 stat 结构中其他未被 ls -l 用到的成员变量未在这里列出。下面的例子 fileinfo.c 得到以上这些属性并显示出来。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
 
void show_stat_info( char *fname, struct stat *buf );

int main(int ac, char *av[])
{
	struct stat info;

	if( ac>1 )
		if( stat( av[1], &info) != -1 )
		{
			show_stat_info( av[1], &info );
			return EXIT_SUCCESS;
		}
		else
			perror( av[1] );

	return EXIT_FAILURE;
}

void show_stat_info( char *fname, struct stat *buf )
{
	printf( " mode:%o\n", buf->st_mode);
	printf( " links:%d\n", buf->st_nlink);
	printf( " user:%d\n", buf->st_uid);
	printf( " group:%d\n", buf->st_gid);
	printf( " size:%ld\n", buf->st_size);
	printf( " modtime:%ld\n", buf->st_mtime);
	printf( " name:%s\n", fname);
}

 编译并运行 fileinfo,并把它跟 ls -l 作对比:

gao@ubuntu:~/c$ gcc fileinfo.c -o fileinfo
gao@ubuntu:~/c$ ./fileinfo fileinfo.c
 mode:100664
 links:1
 user:1000
 group:1000
 size:680
 modtime:1648371939
 name:fileinfo.c
 gao@ubuntu:~/c$ ls -l fileinfo.c
-rw-rw-r-- 1 gao gao 680 3月  27 17:05 fileinfo.c
1.5 如何实现

 链接数(links)、文件大小(size)的显示都没有问题,最后修改时间(modtime)是 time_t 类型的,可以用 ctime 将其转化成字符串,也没有问题。

 fileinfo 将模式(mode)字段以数字形式输出,然而需要的是如下的形式:

-rw-rw-r--

 结构中的用户所有者(user)和组(group)字段都是数值,而显示出来的应该是用户名和组名,为了完善 ls -l,必须进一步处理模式、用户名和组的显示。

1.6 将模式字段转换成字符

 文件类型和许可权限是如何存储在 st_mode 中呢?又如何将它们转成 10 个字符的串?八进制 100664 又与 “-rw-rw-r–"有什么关系呢?对这 3 个问题的回答就构成了本节的内容。

 st_mode 是一个 16 进制的二进制数,文件类型和权限被编码在这个数中,如图 1.6.1 所示。
在这里插入图片描述

图 1.6.1 文件类型与许可权限

 其中前 4 位用作文件类型,最多可以标识 16 种类型,目前已经使用了其中的 7 个。

 接下来的 3 位是文件的特殊属性,1 代表具有某个属性,0 代表没有,这 3 位分别是 set-user-ID 位、set-group-ID 位和 sticky 位,它们的含义以后介绍。

 最后的 9 位是许可权限,分为 3 组,对应 3 种用户,它们是文件所有者、同组用户和其他用户。其他用户指与用户不在同一个组的人。每组 3 位,分别是读、写和执行的权限。相应的地方如果是 1,就说明该用户拥有对应的权限,0 代表没有。

  1. 字段的编码

 把多种信息编码到一个整数不同字段中是一种常用的技术,如:

编码的例子
617-495-4204电话号码(区号-局号-线号)
027-93-111社会保障号
128.103.33.100IP 地址
  1. 如何读取被编码的值

 怎么来读取被编码的值呢?比如怎么知道 212-222-4444 所对应的区号是 212?很简单,一种方法是将号码的值前 3 位同 212 比较,另一种方法是将暂时不需要的地方置 0,这里把电话号码的后 7 位置 0,然后同 212-000-0000 比较。

 为了比较,把不需要的地方置 0,这种技术称为掩码(masking),就如同带上面具把其他部位都遮起来,就只留下眼睛在外面。这里用一系统掩码来把 st_mode 的值转化成 ls -l 要显示 的字符串。

 子域编码(subfield coding)是系统编程中一种重要且常用的技术,以下四方面详细介绍子域编码与掩码。

 (1) 掩码的概念

 掩码会将不需要的字段置 0,需要的字段和值不发生改变。

 (2) 整数是 bit 组成的序列

 整数在计算机中是以 bit 序列的形式存在的,图 1.6.2 显示了如何以二进制的 0 和 1 的串来表示十进制的215。想一下 00011010 表示十进制的几?
在这里插入图片描述

图 1.6.2 在整数和二进制之间转换

 (3) 掩码技术

 与 0 作位与(&)操作可以将相应的 bit 置为 0,图 1.6.3 是八进制的 1000664 通过位与操作把一些 bit 置为 0。注意,数字中的某些 1 是如何被置为 0 的。
在这里插入图片描述

图 1.6.3 位与操作

 (4) 使用八进制数

 直接处理二进制数是很枯燥乏味的。如同处理一长串十进制数时人们常将它们三位一组分开(如 23,234,456,022)一样,一种简化的方法是将二进制数每三位分为一组来操作,这就是八进制数(0 至 7)。

 如可以把二进制的 1000000110110100 分为 1,000,000,110,110,100,从而得到八进制的 100664,这样更容易理解。

  1. 使用掩码来解码得到文件类型

 文件类型在模式字段的第一个字节的前四位,可以通过掩码来将其他的部分置 0,从而得到类型的值。

 在<sys/stat.h>中有以下定义:

#define S_IFMT  00170000
#define S_IFSOCK 0140000
#define S_IFLNK  0120000
#define S_IFREG  0100000
#define S_IFBLK  0060000
#define S_IFDIR  0040000
#define S_IFCHR  0020000
#define S_IFIFO  0010000
#define S_ISUID  0004000
#define S_ISGID  0002000
#define S_ISVTX  0001000

 S_IFMT 是一个掩码,它的值是 0170000,可以用来过滤出前四位表示 的文件类型。S_IFREG 代表普通文件,值是 0100000,S_IFDIR 代表目录文件,值是 0040000 。下面的代码:

if( (info.st_mode & 0170000) == 0040000 )
	printf( "this is a directory" );

 通过掩码把其他无关的部分置 0,再与表示目录代码比较,从而判断这是否是一个目录。

 更简单的方法是用<sys/stat.h>中的宏来代替上述代码:

#define S_ISLNK(m)      (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m)      (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m)      (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m)     (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m)     (((m) & S_IFMT) == S_IFSOCK)

 使用宏的话就可以这样写代码:

if( S_ISDIR( info.st_mode ))
	printf( "this is a directory" );
  1. 解码得到许可权限

 模式字段的最低 9 位是许可权限,它标识了文件所有者、组用户、其他用户的读、写、执行权限。ls 将这些位转换为短横和字母的串。在<sys/stat.h>中每一位都有相应的掩码,下面的代码给出了如何使用的例子:

void mode_to_letters(int mode, char str[] )
{
	strcpy( str, "----------" );		/* default = no perms */
	if( S_ISDIR(mode) ) str[0] = 'd';	/* directory? */
	if( S_ISCHR(mode) ) str[0] = 'c';	/* char devices */
	if( S_ISBLK(mode) ) str[0] = 'b';	/* block device */
	
	if( mode & S_IRUSR ) str[1] = 'r';	/* 3 bits for user */
	if( mode & S_IWUSR ) str[2] = 'w';
	if( mode & S_IXUSR ) str[3] = 'x';
	
	if( mode & S_IRGRP ) str[4] = 'r';	/* 3 bits for group */
	if( mode & S_IWGRP ) str[5] = 'w';
	if( mode & S_IXGRP ) str[6] = 'x';
	
	if( mode & S_IROTH ) str[7] = 'r';	/* 3 bits for other */
	if( mode & S_IWGRP ) str[5] = 'w';
	if( mode & S_IXGRP ) str[6] = 'x';
}
  1. 解码并编写 ls

 到此为止,已经可以正确处理文件大小、链接数、文件名、模式、最后修改时间。最后还有一个要解决的问题是文件所有者(user)和组(group)的表示。

1.7 将用户/组 ID 转换成字符串

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

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

 (1) /etc/passwd 包含用户列表

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

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

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin

 这是个纯文本文件,每一行代表一个用户,用冒号“:”分成不同的字段,第一个字段是用户名,第二个字段是密码,第三个字段是用户 ID,第四个字段是所属的组,接下来的是用户全名、主目录、用户使用 shell 程序的路径。所有的用户对这个文件都有读权限,关于这个文件的详细信息,参见联机帮助。

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

 (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.  */
struct passwd
{
  char *pw_name;                /* Username.  */
  char *pw_passwd;              /* 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.  */
};

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

char *uid_to_name( uid_t uid )
{
	return getpwuid( uid )->pw_name;
}

 这段代码很简单,但还不够健壮,如果 uid 不是一个合法的用户 ID,那 getpwuid 返回空指针 NULL,这时 getpwuid(uid)->pw_name 就没有意义,这种情况会发生吗?常用的 ls 命令有一种处理这种情况的办法。

 (4) UID 没有对应的用户名

 假设在一台 Unix 主机上有一个账号,用户名是 pat,用户 ID 是 2000,创建了一个文件,这个文件的 st_uid 的值就是 2000 。

 标准的 ls 如果遇到这种情况,会打印出 UID 。

 当新加入一个用户时,新用户有可能与一个已被删除的用户相同的 UID,这时,老用户所留下来的文件会被用户所拥有,新用户对这些文件有所有的权限。

 最后一个问题是组 ID 如何处理?什么是组?什么是组 ID?

 (5) /etc/group 是组的列表

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

root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:syslog,gao
tty:x:5:
disk:x:6:
lp:x:7:
cdrom:x:24:gao
sudo:x:27:gao
dip:x:30:gao
plugdev:x:46:gao
gao:x:1000:

 第一字段是组名,第二个是组密码,这个字段极少用到,第三个是组 ID(GID),第四个是组中的成员列表。

 (6) 用户可以同时属于多个组

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

 (7) 通过 getgrgid 来访问组列表

 在网络计算系统中,组信息也被保存在 NIS 中。Unix 系统提供 getgrgid 函数屏蔽掉实现的差异。用这个函数,用户可以得到组名而不用操心实现的细节。getgrgid 的用户手册对这个函数及相关函数做了详细解释。在 ls -l 中,可以这样得到组名:

char *gid_to_name( gid_t gid)
{
	return getgrgid(gid)->gr_name;
}
1.8 编写 ls1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>

void do_ls( char[] );
void dostat( char * );
void show_file_info( char *, struct stat * );
void mode_to_letters( int, char[] );
char *uid_to_name( uid_t );
char *gid_to_name( gid_t ); 
 
int main(int ac, char *av[])
{
	if( ac == 1 )
		do_ls( "." );
	else
		while( --ac )
		{
			printf( "%s:\n", *++av);
			do_ls( *av );
		}

}

/*
 * list files in directory called dirname
 */
void do_ls( char dirname[] )
{
	DIR *dir_ptr;	/* the directory */
	struct dirent *direntp;	/* each entry */

	if( (dir_ptr = opendir( dirname )) == NULL )
		fprintf( stderr, "ls: cannot open %s\n", dirname );
	else
	{
		while( (direntp = readdir( dir_ptr )) != NULL )
			dostat( direntp->d_name );
		closedir( dir_ptr );
	}
}

void dostat( char *filename )
{
	struct stat info;

	if( stat( filename, &info) == -1 )
		perror( filename );
	else
		show_file_info( filename, &info );
}

void show_file_info( char *filename, struct stat *info_p )
{
	/* char *uid_to_name(), *ctime(), *gid_to_name(), *filemode();
	void mode_to_letters(); */
	char modestr[11];

	mode_to_letters( info_p->st_mode, modestr );

	printf( "%s", modestr );
	printf( "%4d ", (int)info_p->st_nlink );
	printf( "%-8s ", uid_to_name(info_p->st_uid) );
	printf( "%-8s ", gid_to_name(info_p->st_gid) );
	printf( "%8ld ", (long)info_p->st_size );
	printf( "%.12s ", 4 + ctime( &info_p->st_mtime) );
	printf( "%s\n", filename);
}

void mode_to_letters(int mode, char str[] )
{
	strcpy( str, "----------" );		/* default = no perms */

	if( S_ISDIR(mode) ) str[0] = 'd';	/* directory? */
	if( S_ISCHR(mode) ) str[0] = 'c';	/* char devices */
	if( S_ISBLK(mode) ) str[0] = 'b';	/* block device */
	
	if( mode & S_IRUSR ) str[1] = 'r';	/* 3 bits for user */
	if( mode & S_IWUSR ) str[2] = 'w';
	if( mode & S_IXUSR ) str[3] = 'x';
	
	if( mode & S_IRGRP ) str[4] = 'r';	/* 3 bits for group */
	if( mode & S_IWGRP ) str[5] = 'w';
	if( mode & S_IXGRP ) str[6] = 'x';
	
	if( mode & S_IROTH ) str[7] = 'r';	/* 3 bits for other */
	if( mode & S_IWGRP ) str[5] = 'w';
	if( mode & S_IXGRP ) str[6] = 'x';
}

char *uid_to_name( uid_t uid )
{
	/* struct passwd *getpwuid(), *pw_ptr;*/
	struct passwd *pw_ptr;
	static char numstr[10];

	if( (pw_ptr = getpwuid( uid )) == NULL )
	{
		sprintf( numstr, "%d", uid );
		return numstr;
	}
	else
		return pw_ptr->pw_name;
}

char *gid_to_name( gid_t gid )
{
	struct group *getgrgid(), *grp_ptr;
	static char numstr[10];

	if( (grp_ptr = getgrgid(gid)) == NULL )
	{
		sprintf( numstr, "%d", gid );
		return numstr;
	}	
	else
		return grp_ptr->gr_name;

}

 将 ls1 的输出与标准的 ls 对比:

gao@ubuntu:~/c$ ./ls1
-rw-r--r--   1 gao      gao           390 Mar 17 22:03 who_macos.c
drwxrwxr--   3 gao      gao          4096 Mar 27 18:53 .
-rw-r--r--   1 gao      gao          7548 Mar 27 14:53 copy.of.cp1
-rw-r--r--   1 gao      gao          1285 Mar 27 15:05 who1.c
-rw-rw-r--   1 gao      gao           244 Mar 27 16:51 filesize.c
-rwxrwxr--   1 gao      gao          7544 Mar 27 15:16 who2
-rwxrwxr--   1 gao      gao         12052 Mar 27 18:53 ls2
drwxrwxr--   2 gao      gao          4096 Mar 27 16:34 dir
-rw-rw-r--   1 gao      gao          1037 Mar 27 14:52 cp1.c
-rw-rw-r--   1 gao      gao          2698 Mar 27 18:53 ls2.c
-rwxrwxr--   1 gao      gao          7408 Mar 27 17:05 fileinfo
-rw-rw-r--   1 gao      gao          1890 Mar 27 15:16 who2.c
-rwxrwxr--   1 gao      gao          7376 Mar 27 16:51 filesize
-rwxrwxr--   1 gao      gao          7392 Mar 27 16:11 ls1
-rw-rw-r--   1 gao      gao           754 Mar 27 16:11 ls1.c
-rwxrwxr--   1 gao      gao          7488 Mar 17 22:12 who1
-rwxrwxr--   1 gao      gao          7548 Mar 27 14:53 cp1
drwxr-xr--   8 gao      gao          4096 Mar 27 16:01 ..
-rw-rw-r--   1 gao      gao           680 Mar 27 17:05 fileinfo.c
-rwxrwxr--   1 gao      gao          7416 Mar 17 22:03 who_macos
留给大家的问题

 ls1 的输出看起来已经很不错了,模式字段、用户名和组名的处理已经完成。但还有些工作要做。标准的 ls 会显示记录总数,ls1 不会,而且 ls1 还没将结果按文件名排序,也不支持选项 -a 。它还假设参数是目录。

 ls1 还有一个致命问题,不能显示指定目录的信息,你可以试一试,在命令行输入: ls1 /tmp 。这些留给大家来修正吧。(_)/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值