Linux —— 基础IO


前言: 学习本章Linux—— 基础IO,我们必须搞懂俩个概念,文件描述符 fd以及文件的inode。为了搞懂这俩个内容,我们展开不少的铺垫以及拓展。


1. C语言文件的IO操作

我们之前学习的C语言文件操作,用的是用户级别的接口函数,这次我们用系统接口来进行文件操作,更接近底层。

1.1 系统文件操作接口

用到的系统接口:

  • open()
  • close()
  • write()
  • read()

(1) 先介绍open()和close():
在这里插入图片描述

  • open()的返回值:int整型,返回的是文件描述符 fd
  • open()的参数:第一个参数pathname是文件名(含路径,缺省为当前路径),第二个参数 flags是打开文件的方式,第三个参数mode用于设置文件权限
    在这里插入图片描述
  • close()的返回值:关闭文件成功返回0,失败返回 -1,失败原因会记录再errno中。
  • close()的参数:文件的描述符

(2) 再介绍 write()和read()
在这里插入图片描述

  • write()的参数:第一个参数是文件描述符,第二个参数是一个指针:往文件写的 内容的指针,第三个参数是往文件中要写入的内容的大小。
  • write()的返回值:写入文件成功,返回写入的字节数;写入失败,返回 -1,错误信息记录到errno。

在这里插入图片描述

  • read()的参数:第一个参数是文件描述符,第二个参数是一个指针:从文件读取 的地址,第三个参数是从文件中要读取的内容的大小。
  • read()的返回值:读取文件成功,返回读取的字节数;读取失败,返回 -1,错误信息记录到errno。

1.2 使用以上接口

(1)写入操作

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
  int fd=open("test.txt",O_WRONLY|O_CREAT,0644);
  
  // 写入操作
  
 const char *s="hollow fd";

  write(fd,s,strlen(s));


  close(fd);
  return 0;
}

open()的第三个参数在创建新文件时很重要,创建新文件必须设置权限,否则创建出来的文件的权限是乱的,比如上面的程序,我不加第三个参数,大家看看效果:
在这里插入图片描述
加上参数0644,我们再看效果:
在这里插入图片描述
我们可以看看test.txt,有没有我们写入的内容:
在这里插入图片描述

(2) 读取操作

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
  int fd=open("test.txt",O_RDONLY);
  
 // 读取操作 
 
 const char* s="hollow fd";
 char ss[10];

 read(fd,ss,strlen(s));

 printf("%s\n",ss);

  close(fd);
  return 0;
}

可以看下结果:
在这里插入图片描述


2. 文件描述符 fd

2.1 文件描述符的概念

上面其实我们已经简单的用了文件描述符fd,它是open()函数的返回值,那么这个fd到底是什么?

进程和文件的关系是1:n的,操作系统管理进程会用一个进程控制块,进程控制块中就有一个* file ,这个*file指向一个结构体 -> files_struct{},在这个结构体中有一个非常重要的文件指针数组,数组中的文件指针指向了文件,那么文件指针数组的下标就是 -> 文件描述符 fd。

具体关系可以看下图:

在这里插入图片描述

2.2 文件描述符fd的规则

一切皆是文件,我们知道进程会默认打开三个流,stdin,stdout,stderro。那么我们就来看看,它们的fd是多少?来验证一下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
  printf("stdin -> %d\n", stdin->_fileno);
  printf("stdout -> %d\n", stdout->_fileno);
  printf("stderr -> %d\n", stderr->_fileno);
 return 0;
}

_fileno,存的就是文件的fd,我们一会 会讲到的。
在这里插入图片描述
可以看到,确实是默认打开此三个文件,并且它们的fd分别是0,1,2。

那么我们新创建一个文件,我们来看看它的fd是多少?

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
  int fd=open("test1.txt",O_WRONLY|O_CREAT,0644);
  
  printf("%d\n",fd);
  close(fd);
  return 0;
}

在这里插入图片描述
fd是3,那么很明显,创建的此文件排到了三个默认打开文0,1,2下面。我们如果先关闭某一个默认打开文件呢?比如我关闭标准输入流:0,那么新建立的文件的fd是多少呢?

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
  close(0);
  int fd=open("test1.txt",O_WRONLY|O_CREAT,0644);
  
  printf("%d\n",fd);
  close(fd);
  return 0;
}

新建的文件的fd是0。
在这里插入图片描述
得出结论: 默认打开三个文件它们的文件描述符是0,1,2。文件描述符的分配规则是在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。


2.3 重定向

默认情况下,fd=1的是标准输出流,如果我们将fd=1的标准输出流给关闭了,那么我们新创建的文件的fd就是1,系统认为fd=1的就是标准输出,那么原来向屏幕输出的内容,会输出到文件中嘛?

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
  close(1);
  int fd=open("test1.txt",O_WRONLY|O_CREAT,0644);
  
  printf("%d\n",fd);
  printf("hi ly\n");
  printf("hi ly\n");
  printf("hi ly\n");
  printf("hi ly\n");
  printf("hi ly\n");
  printf("hi ly\n");

  fflush(stdout);
  close(fd);
  return 0;
}

运行上面的程序确实不会输出到屏幕,但是我们查看创建的文件test1.txt:

发现原来向屏幕输出的内容,输出到了创建的文件中,并且此文件的fd=1

在这里插入图片描述
就是因为默认的标准输出流fd=1,关闭了标准输出流后,新创建的文件的fd=1,所以完成了重定向。这就是重定向的原理。

这里要为下文做个铺垫,上面的重定向程序中我用了fflush(),如果不用fflush()可以完成重定向嘛?我记得刷新缓存区有\n就足够了,所以没必要加上fflush()。我们来试一下:

关谷神奇发现:并没有发生重定向,为什么呢?
在这里插入图片描述
因为在屏幕上打印刷新缓存区可以是换行,但是重定向到文件中,刷新缓存区不是换行,所以内容还在缓冲区中并没有刷新到文件中。进程退出,不是也会刷新缓冲区嘛?对的,但是在进程退出前,你已经关闭了文件了。


3. File的重定向再次理解

上面的重定向,说明一件事:访问文件都是通过fd来执行的。文件加载到内存中也是一个结构体,此结构体中必然包括了fd。我们可以看下面的这段代码再次理解重定向,以及缓冲区的相关知识。

#include <stdio.h>
#include <string.h>
#include<unistd.h>
int main()
{
 const char *msg0="hello printf\n";
 const char *msg1="hello fwrite\n";
 const char *msg2="hello write\n";

 printf("%s", msg0);

 fwrite(msg1, strlen(msg0), 1, stdout);

 write(1, msg2, strlen(msg2));

 fork();
 
 }

现在输入到屏幕是这样的:

在这里插入图片描述
如果我是重定向呢?

执行脚本:./ly > test.txt
然后查看test.txt:
在这里插入图片描述
关谷神奇发现:write()只输出了一遍,但是其余的两个都输出了两遍。

  1. 缓存区的刷新机制
  • 立即刷新:用fflush(),进程退出
  • 行刷新:比如显示器打印
  • 全缓冲:等缓存区满了才会刷新,往磁盘写入
  1. 用户端的缓冲区以及操作系统的缓存区
  • 用户端的缓冲区一般是C语言接口函数用的,比如:printf,fprintf等。
  • 操作系统的内核缓冲区是系统调用接口所以用的,比如:write等。

有了以上基础,我们来讲讲为什么会发生上面的情况:

没有发生重定向时,刷新缓冲区的机制是行刷新:所以fork()不会发生写时拷贝,因为fork()之前的内容都直接刷新到屏幕了。发生重定向后,刷新缓冲区的机制变成全缓存,fork()会发生写时拷贝,拷贝缓冲区的内容,所以导致重定向输入到文件中的内容有两份。但是为啥write()不受影响呢?因为它是系统调用接口,它用的缓存区是操作系统的,所以上面的刷新缓存区啥的,和它没关系。


4. 理解文件系统

上面不是说过嘛,打开后的文件会被一个结构体管理,感兴趣可以去看一下这个结构体:
typedef struct _IO_FILE FILE; 在/usr/include/路径下里的stdio.h:它的里面必然包括文件描述符fd,去里面找找看,名称是 int _fileno,提升一下很难找。

但是没打开的文件在磁盘上是如何管理的呢?

4.1 文件的磁盘管理

讲老年代的磁盘,现在都是固态磁盘,但是还是老的好理解些:

在这里插入图片描述
这就是一个磁盘是圆形的,由一个一个的扇区组成,磁针在磁盘上运作,来对文件进行读和写操作。磁盘可以看做一个线性结构,这有点难理解,但是磁带见过吧,可以把磁盘抽象成磁带:
在这里插入图片描述

那么磁盘就可以看作一个非常大的储存空间:

在这里插入图片描述
如果管理好了扇区,其实就管理好了磁盘:

压力来到了扇区的管理,对扇区的管理还可以下分,分成了一个一个的小块,从而进行管理,这个小块就是block group ,block group 的大小不固定。

在这里插入图片描述

这个boot block是用于启动的,后面的block group 是一个一个块,所以我们是不是只要管理好了block group 就管理好了扇区。

这就是block group的结构:

在这里插入图片描述


4.2 文件的inode

系统是不认识文件名的,比如:不同的目录下,可以有多个文件名。系统识别文件靠的是文件编号(inode编号),多个文件名可以对应一个inode编号,这可以理解成文件的重命名。

上面讲到的block group是存放文件的,每个block group都可以有多个文件,要创建文件那么磁盘上会开辟空间,在磁盘上进行文件管理,就是block group储存文件,然后得到的 inode。

现在该讲讲block group是如何储存的:

在这里插入图片描述

4.3 文件在block group中的储存

文件 = 文件属性 + 文件数据

文件属性在inode table中,文件数据在Date blocks中:

我们可以将这两个块看作一个较大储存块:

在这里插入图片描述
在这里插入图片描述


(1) 文件属性

那个inode table中存的是一个一个的结构体,也就是inode,inode中存的是文件的属性,包括:与Date blocks的映射关系,以及文件的inode编号。

(2) 文件数据

文件的数据就存在Date blocks中,不过需要与inode table建立映射关系。

打个比方:

indoe table中存的是文件的属性,用的是结构体inode,inode中用于存放与Date blocks映射关系的是一个int block[64]
这个数组中就存的是Date blocks 中存数据的数据块的位置。

在这里插入图片描述
这幅图 画的还有点问题,不过不影响我们理解。

4.4 位图的使用

文件是这样存储的,但是文件的属性和内容具体应该怎么存放呢?系统是如何判断那个数据是空的?

用的是位图:

在这里插入图片描述
就是这俩个货,第一个是 block bitmap用于查看Date blocks中哪个数据块是空的,第二个是inode bitmap 用于查看inode table中哪个是空的。

位图的话就是用比特位来看:

0000 0101

  • 比特位的位置:表示的就是 inode的编号\ blocks的编号
  • 比特位的内容:0,1 。0表示此位置的没占用,1表示已经占用

所以只需要遍历位图,就可以找到空出来的inode,然后存放数据也能找到空的blocks,最后建立映射关系就可以了。至于inode是如何建立映射关系的,本章不着重讲。

大家可能对上面的inode编号还有疑问,或者是上面blocks编号:
这幅图上面的编号都是0,说明都是空的,假如我使用了一个inode去存文件属性:

在这里插入图片描述

对应的编号位置内容设置成1,说明就占用了此inode。
在这里插入图片描述


5. 文件的创建,删除,恢复的原理

5.1 文件的创建

先提个问题:目录是不是文件?答案:是的。

目录是文件,那么绝对也是有inode的,那么它的数据内容是什么呢?那就是将文件名和文件的inode编号映射起来,目录下的文件,用户看到的是文件名,其实都和其inode编号做好了映射。

文件的创建必须在目录下创建。

那么我们来创建一个文件:touch hh.txt

  • 首先会去遍历位图,然后将文件属性放到inode中,并有了inode编号,再将文件的数据放到空的blocks中,并于inode建立映射关系,然后将此文件的文件名和inde编号的映射关系写入到目录下。

查看文件内容:cat hh.txt

  • 先去查看当下目录的Date blocks找到此文件的inode编号,通过此编号找到文件的inode table中对应的inode,然后通过inode与Date blocks的映射关系,从而打印出此文件在Date blocks中存的数据。
5.2 文件的删除

创建一个文件是复杂的,删除是非常容易的,只需要将此文件在block group中占用的inode,blocks的编号内容从1设成0。这样就相当于完成了对文件的删除。

window的回收站其实并没有删除文件,只不过是将文件换到了一个叫做回收站的目录下,如果想真正意义的删除需要在回收站中再一次删除。

5.3 文件的恢复

文件删除不过是将其占用的inode,blocks的编号设为0,恢复怎么办呢?当然就是将编号从0在设为1。但是文件是一定可以被恢复嘛?不一定,很可能被删除了的文件的inode以及blocks都被其他的文件覆盖了。这种情况下,文件不能被恢复,所以删除了重要文件,要及时恢复,如果继续使用电脑,那么文件可能就真的无法恢复了。


6. 软链接和硬链接

6.1 软链接的创建以及使用

(1) 创建软链接

使用指令ln - s 文件名 软链接名,就可以创建软链接。比如我想要创建一个软连接关联上文件test.txt。
在这里插入图片描述
这个my_test.txt 就和test.txt构成了软链接,这个my_test.txt中有test.txt的所有内容:

在这里插入图片描述
(2) 使用软链接

嗯,创建软链接我知道了,但是这有什么用呢?其实是有用的,比如我在 /home/ly/test_9_21/ly/ny/yy 这个路径下有个可执行程序hello,想要运行的话比较不方便,还得找路径,但是我在当下目录创建一个软链接与可执行程序hello相关联,是不是就简单多了。

ln -s /home/ly/test_9_21/ly/ny/yy/hello my_hello
现在,my_hello与hello构成软链接:
在这里插入图片描述
(3) 对比一下windows

软链接就是Linux的快捷方式,我们看看windows的快捷方式是如何实现的:

在这里插入图片描述
windows下,创建快捷方式和Linux类似,它也是对目标文件进行了软链接。


6.2 硬链接的创建以及使用

(1) 创建硬链接

ln 文件名 硬链接文件名

在这里插入图片描述

这样就完成了硬链接,当然硬链接的My_test也可以看到test.txt的所有内容:

在这里插入图片描述
(2) 硬链接和软链接的区别

ls -ali 可查看当下路径的文件的属性,我们主要有-i 选项,它是用于查看文件的inode编号:

在这里插入图片描述

开头部分就是inode编号,关谷神奇发现:软链接my_test.txt的inode编号和test.txt的不一样,硬链接My_test和test.txt的inode编号是一样的。

所以可以得出结论:硬链接本质就是在目录下创建的一个文件名和inode编号的映射,它并没有真正意义上的去开辟空间存文件内容;软链接有自己的独立inode,它是创建了一个新的文件,里面的内容是拷贝原文件。

那么我有个疑问:删除掉硬链接,是否会干掉原文件,毕竟它俩的inode编号都是一样的。来实验一下:

在这里插入图片描述

发现对原文件并没有影响,这是为什么呢?因为inode中用的是引用删除,解释引用删除前,我们先理解一下东东。

在这里插入图片描述
这一列数字表示的含义就是:硬链接的数目。

我可以创建一个目录,帮助大家理解这个:

我创建了一个新的目录:mkdir wk

在这里插入图片描述
我们平常总是使用 cd … ,现在就能懂了:

创建一个新的目录,系统会默认创建硬链接 . ,它和当下目录相关联。其次就是..,它的子目录会默认有.. ,也是对此目录的硬链接。

那么我们就知道了,在inode中绝对会一个变量:int ref,去记录此文件的硬链接数。每创建一个硬链接,ref就加一,删除硬链接那么ref就减一,知道硬链接数为0,再删除文件,才是真正意义上的删除文件。

6.3 解除链接

解除链接有两种方式:

  1. 直接删除链接文件:rm -f 链接文件
  2. 使用指令unlink: unlink 链接文件名

7. 学习指令 stat

在这里插入图片描述

stat用于查看文件的详细信息:

  • File:文件名
  • Blocks :占用数据块个数
  • IO Block :IO数据块大小
  • regular file :文件类型
  • Device : 设备编号
  • Inode :inode编号
  • Links :硬链接数
  • Acess:文件的权限
  • uid和gid: 文件的所有者id

以上是简单的介绍,主要学习一下后面的三个时间参数:

  • Access :文件被最近被访问的时间

接下来我多次访问文件,但是不修改文件内容。

再次用stat查看文件,看看Access时间变了没?

在这里插入图片描述
很明显时间是改变了。

  • Modify:最近一次修改文件内容的时间
    现在我去修改文件的内容。
    echo ”everyone“ >> test.txt
    在这里插入图片描述

  • Change: 最近一次修改文件属性的时间

其实发现,上面修改文件的内容,这个时间也变化了,原因是修改文件的内容,其实也修改了文件的属性。文件的大小也是文件属性之一。


8. 动态库和静态库

8.1 动态库和静态库的基础知识

基本上,我们写的程序都是需要用到库的,具体使用到了某个库函数,链接时,会去库中找。

库一般有两种:

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静
    态库
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

查看一个程序链接了哪个库?静态链接或动态链接?

ldd 文件名

比如:我写了一个可执行程序,用ldd查看它的链接关系:

在这里插入图片描述
可以看到这个程序,依赖的关系是动态库,后缀是 .o 说明是动态库。

当然也可以用file指令查看是动态库还是静态库,但是不如ldd详细:

在这里插入图片描述
uses shared libs,就是使用动态库。

默认情况下,Linux是使用动态库的,如果编译时加上-static ,那么就是使用静态库:

在这里插入图片描述

file查看一下:

在这里插入图片描述
显示是静态链接。

8.2 制作静态库和动态库

我们想要给别人使用代码有两种方式:

  • 将源文件和头文件给出去
  • 将库文件和头文件给出去

显然,第二种是封装后的,比较的隐秘。头文件用于查看有哪个函数可以调用,但是函数如何实现(定义)?是查看不到的。库文件中有函数的实现,但全是二进制,根本看不懂。

8.2.1 静态库的制作

一个程序编译的过程是:
预处理,编译,汇编,链接。

前三个步骤我们不管,最后一个步骤是链接:这是去需要静态库中找的。链接,链接的是什么?链接的是 .o 文件,也就是目标文件。所以我们只需要将程序编译成 .o 文件,然后再对 .o 文件进行一个打包,其实形成的就是静态库。

以上是原理,现在我们一步一步的实现:

(1) 写一个程序,包括头文件和源文件,假如就是打印十行“hello ly”。

在这里插入图片描述

在这里插入图片描述

(2) 形成 .o 文件,并且打包成静态库

形成了 .o 文件:
在这里插入图片描述
打包成静态库:

在这里插入图片描述
用的是指令ar :归档工具,选项 -rc:replace and create。

现在,我们有了头文件和静态库,那么就可以传给别人使用了。

比如:

我要在一个程序中,调用上面的 my_print()函数,需要将库文件和头文件传到这个程序的路径下:

简单点,就在当下的路径:

在这里插入图片描述

我们直接编译:
在这里插入图片描述

发现报错了,原因是找不到,my_print()这个函数,我只包了头文件,虽然当下路径有静态库,但是编译时也需要我们指定路径,那么为什么,平时我们调用库函数不需要这样做?因为,我们所用的库,它默认路径,系统是知道的。

在这里插入图片描述
这样尽然就成功了,赶快说一说:

gcc test.c -I. -L. -lmyprint

看这些选项:

-I 后跟的是头文件的路径。-L后跟的是静态库的路径。-l后跟的是静态库的名称。

静态库的名称:我们定义成lib+库名称+后缀(静态库为.a,动态库是.so)。

运行一下:

在这里插入图片描述

我指定的路径都是当下路径,当然,在别的路径下也是可以使用的,只需要编译的时候指明路径就可以了。
比如:我创建了一个目录friend,并写了一个程序,调用了上面的my_print()函数,静态库和头文件在project目录下。

在这里插入图片描述
在这里插入图片描述
现在我来编译一下:

在这里插入图片描述
照样编译成功了,所以综上,我们只需要有头文件和静态库,就可以隐秘的让别人使用我们定义的函数。

我们可以看看 .o 文件,全是二进制:

在这里插入图片描述


8.2.2 动态库的制作

动态库:首先对比一下静态库,静态库是将代码直接拿到程序中,动态库是使用的时候才会去找相应的代码。所以在运行时,是不是还需要我们提示一下路径呢?方便它去找相应代码呢?

(1) 写一个程序:打印10行"hello ybw" ,有头文件和源文件
在这里插入图片描述
在这里插入图片描述

(2) 形成动态链接的共享库

首先,必须得形成 .o 文件这是好理解的,但需要注意的是 编译时需要加上选项 -fPIC,形成位置无关码。
其次,就是形成共享库,也就是将 .o 文件再次编译,正常情况下编译后会形成可执行文件,但是加上选项
-shared,就是形成共享动态库格式。

在这里插入图片描述
在这里插入图片描述

现在就已经形成了共享库,那么我在当下目录写一个test.c:
在这里插入图片描述
再来编译一下:
在这里插入图片描述
这样就编译成功了,我们再来执行:

在这里插入图片描述

关谷神奇发现:无法正常运行。

(3) 运行动态库

默认情况下,LD_LIBRARY_PATH 环境变量是没有被配置的,比如用虚拟机学习的同学。这就会导致一种情况,上面的程序编译成功,但是运行时不成功。这是因为动态库的原理,它并没有将代码拷贝到程序中,而是需要动态链接的,可以通过环境变量配置一下,当然也能 配置/etc/ld.so.conf.d/。

  1. 配置环境变量

我查看一下这个环境变量:

echo $LD_LIBRARY_PATH

在这里插入图片描述
很明显是空的。

我们配置的话,就是将所在路径导入到这个环境变量中:

export LD_LIBRARY_PATH=/home/ly/test_9_21/project2

在这里插入图片描述

  1. 配置 /etc/ld.so.conf.d/

动态库链接也会在这个路径下,查看 .conf文件,所以需要在这个路径下创建一个 .conf文件,再将路径拷贝进这个 .conf文件中,就可以完成配置。

先将上面的环境变量LD_LIBRARY_PATH变成空的,有俩种方式:

  • 手动置空:export LD_LIBRARY_PATH=
  • 重启虚拟机或xshell

可以看到这个路径下的 .conf文件有很多:
在这里插入图片描述
(1) cd /etc/ld.so.conf.d
(2) touch ly.conf 这一步需要root来操作,名字随便起,没必要都叫ly.conf,但是后缀必须是 .conf
(3) 向ly.conf中写入路径,可以用vim,nano都行,写入路径就好了
在这里插入图片描述

这就已经配置好了,我们回到程序那,运行一下,看看怎么说:

在这里插入图片描述
没得问题!


9. 总结基础IO

首先,我们复习了C语言文件的IO操作,从而引出了文件描述符fd,通过fd再次理解重定向。之后我们又学习了文件的储存机制,理解了inode,从而明白了文件的创建,删除,恢复。再理解了inode之后,学习软硬链接是简单的。最后我们学习了动态库和静态库的基础知识,并创建了属于我们自己的动,静态库。


结尾语: 以上就是本章的知识,内容有点肝,不过肝完后,绝对是有收获的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

动名词

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值