2 文件

1、文件系统

1.1 文件系统的逻辑结构

在这里插入图片描述

·一个磁盘驱动器被划分成一到多个分区,其中每个分区上都建有独立的文件系统,每个文件系统包括:

  • 引导块:计算机加电启动时,ROM B1OS从这里读取可执行代码和数据,以完成操作系统自举
  • 超级块:记录文件系统的整体信息,如文件系统的格式和大小,节点和数据块的总量、使用量和剩余量等等
  • 若干柱面组,其中每个柱面组包括
    ·级块副本:同上
    ·柱面组信息:柱面组的整体描述
    ·i节点映射表:i节点号与i节点磁盘位置的对应表
    ·块位图:位图中的每个二进制位对应一个数据块,用1和0表示该块处于占用或是空闲状态
    ·节点表:包含若干i节点,记录文件的元数据和数据块索引表
    ·数据块集:包含若干数据块,存储文件的内容数据
    总结:一个文件对应一个i节点,i节点存储的是文件的元数据信息和数据块的编号,一个文件可能对应多个数据块,数据块存储的是文件的内容

1.2 文件的访问流程

针对给定的文件名,从其所在目录中可以得到与之对应的i节点号,再通过i节点映射表可以查到该i节点在磁盘上的具体位置,读取i节点信息并从中找到数据块索引,进而找到相应的数据块,最终获得文件的完整内容
在这里插入图片描述

2、文件类型

  • 普通文件
    • 在linux中以’-'来表示,包括源代码文件、目标文件、可执行文件、脚本文件、音频、视频文件等
    • 一个文件包含两部分的数据,一部分是元数据,一部分是内容数据
  • 目录文件
    • 在linux中以’d’来表示
    • 一个目录的数据块中存储的是该目录下所有子文件的文件名和i节点号的对应关系
  • 符号链接文件
    • 在linux中以’l’来表示
    • 通过命令ln -s xxx.c yyy.c可以创建一个符号链接
  • 特殊文件
    • 本地套接字文件:以’s’表示
    • 字符设备: 以’c’表示
    • 块设备:以’b’表示
    • 有名管道:以’p’表示
      通过ls -l命令可以查看文件的类型

3、文件的打开与关闭

  • 相关函数
    1:open
// 头文件 fcntl.h
int open(char const* pathname,int flags,mode_t mode);
- 功能:打开已有的文件或创建新文件
- 参数:
	- pathname 文件路径
	- flags 状态标志,可以以下取值
		O_RDONLY - 只读
		O_WRONLY - 只写
		O_RDWR - 读写
		O_APPEND - 追加
		O_CREAT - 不存在即创建,已存在即打开
		O_EXCL - 不存在即创建,已存在即报错
		O_TRUNC - 不存在即创建,已存在即清空
- 参数:mode 权限模式
		仅在创建新文件时有效,可用形如0XXX的三位八进制数表示,由高位到低位依次表示拥有者用户、同组用户和其它用户的读、写和执行权限,读权限用4表示,写权限用2表示,执行权限用1表示,最后通过加法将不同的权限组合在一起
- 返回值:所返回的一定是当前未被使用的,最小文件描述符
/*调用进程的权限掩码会屏蔽掉创建文件时指定的权限位,
如创建文件时指定权限0666,进程权限掩码0022,所创建文件的实际权限为:0666&~0022=0644(rw-r-r-)
查看文件掩码的命令是umask*/

2:close

// 头文件 unistd.h
int close(int fd);
- 功能:关闭处于打开状态的文件描述符
- 参数:fd处于打开状态的文件描述符
  • 案例
// 文件的打开与关闭
#include <stdio.h>
#include <fcntl.h> // open
#include <unistd.h> // close
int main(){
	// 打开文件
	int fd = open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0777);
	if(fd==-1){
		perror("open");
		return -1;
	}
	printf("fd = %d\n",fd);
	// 关闭文件
	close(fd);
	return 0;
}

4、文件的内核结构

  • 一个处于打开状态的文件,系统会为其在内核中维护一套专门的数据结构,保护该文件的信息,直到它被关闭

    • V节点与V节点表
      • 文件的元数据信息保存在i节点中,而i节点保存在分区柱面组的i节点表中,在打开文件时,i节点信息读入内存,保存在进程的内核空间中的一个专门的数据结构中,而包含文件i节点信息的数据结构称之为V节点,多个V节点结构以链表的形式构成V节点表
    • 文件表项与文件表
      • 由文件转态标志、文件读写位置、和V节点指针等信息组成的内核数据结构称之为文件表项。多个文件表项以链表的形式构成文件表
  • 多次打开同一个文件,无论是在同一个进程还是在不同的进程中,都只是在系统内核中产生一个V节点

  • 每次打开文件都会产生一个新的文件表项,各自维护各自的文件状态标志和当前文件偏移,如果打开的都是同一个文件,则都共享同一份V节点
    在这里插入图片描述

  • 文件描述符

    • 作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是个大于等于0的整数
    • 每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项
    • 每关闭一个文件描述符,无论被其索引的文件表项和节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用
    • 系统内核缺省为每个进程打开三个文件描述符,它们在unistd.h头文件中被定义为三个宏
      #define STDIN_FILENO 0 //标准输入
      #define STDOUT_FILENO 1 //标准输出
      #define STDERR_FILENO 2 //标准错误
#include <stdio.h>
#include <fcntl.h> // open
#include <unistd.h> // close
int main(){
	setbuf(stdout,NULL);// 关闭输出缓存区
	close(/*1*/ STDOUT_FILENO);// 关闭标准输出流
	// 打开文件
	int fd = open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0777);
	if(fd==-1){
		perror("open");
		return -1;
	}
	printf("fd = %d\n",fd); // 此时就是向文件中输出内容
	// 关闭文件
	close(fd);
	return 0;
}
// 解释:因为printf是标准输出,在linux底层是调用1代表标准输出,但是我们将1关闭,并且打开了一个文件,此时系统开始在文件描述符表中寻找最小的未使用项,所以这里1就指向了文件,导致内容输出到了文件中

5、文件的读写

  • 相关函数
    1:write
// 头文件 unistd.h
ssize_t write(int fd,void const* buf,size_t count);
- 功能:向指定的文件写入数据
- 参数:
	- fd 文件描述符
	- buf 内存缓冲区,即要写入效据
	- count 期望写入的字节数
- 返回值:成功返回实际写入的字节数,失败返回-1

2:read

// 头文件 unistd.h
ssize_t read(int fd,void* buf,size_t count);
- 功能:从指定的文件中读取数据
- 参数:
	- fd文件描述符
	- buf 内存缓冲区,存读取到的数据
- count期望读取的字节数
- 返回值:成功返回实际读取的字节数,失败返回-1
  • 案例
#include <stdio.h>
#include <fcntl.h> // open
#include <unistd.h> // close write
#include <string.h>
int main(){
	// 打开文件
	int fd= open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0,0);
	if(fd==-1){
		perror("open");
		return -1;
	}
	// 写入数据
	char* buf = "hello world!\n";
	ssize_t size = write(fd,buf,strlen(buf));
	if(size==-1){
		perror("write");
		return -1;
	}
	printf("实际向文件中写入%ld个字节\n",size);
	// 关闭文件
	close(fd);
	// 打开文件
	int rd = open("./open.txt",O_RDONLY);
	if(rd ==-1){
		perror("open");
		return -1;
	}
	// 读取文件
	char buff[32]={};
	size = read(rd,buff,sizeof(buff)-1);
	if(size == -1){
		perror("read");
		return -1;
	}
	printf("读取到的文件内容是:%s",buff);
	// 关闭文件
	close(rd);
	return 0;
}

4.1 顺序与随机读写

文件的读写位置:本质是一个整数,是一个相对于文件首的偏移量,决定了文件从哪读往哪写,随着读写同步增加

  • 相关函数
    1:lseek
// 头文件 unistd.h
off_t lseek(int fd,off t offset,int whence);
- 功能:人为调整文件读写位置
- 参数:
	- fd:文件描述符
	- offset:文件读写位置偏移字节数
	- whence:offset参数的偏移起点,以如下取值:
		SEEK_SET - 从文件头(首字节)开始
		SEEK_CUR - 从当前位置(最后被读写字节的下一个字节)开始
		SEEK_END - 从文件尾(最后一个字节的下一个字节)开始
- 返回值:成功返回调整后的文件读写位置,失败返回-1
  • 案例
#include <stdio.h>
#include <fcntl.h> // open
#include <unistd.h> // close write
#include <string.h>
int main(){
	// 打开文件
	int fd= open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0,0);
	if(fd==-1){
		perror("open");
		return -1;
	}
	// 写入数据
	char* buf = "hello world!\n";
	ssize_t size = write(fd,buf,strlen(buf));
	if(size==-1){
		perror("write");
		return -1;
	}
	printf("实际向文件中写入%ld个字节\n",size);
	// 关闭文件
	close(fd);
	// 打开文件
	int rd = open("./open.txt",O_RDONLY);
	if(rd ==-1){
		perror("open");
		return -1;
	}
	// 读取文件
	char buff[32]={};
	size = read(rd,buff,sizeof(buff)-1);
	if(size == -1){
		perror("read");
		return -1;
	}
	printf("读取到的文件内容是:%s",buff);
	// 关闭文件
	close(rd);
	return 0;
}

4.2 文件描述符的复制

  • 相关函数
    1:dup
// 头文件:unistd.h
int dup(int oldfd);
- 功能:复制文件描述符表的特定条目到最小可用项:
- 参数:oldfd:源文件描述符
- 返回值:成功返回目标文件描述符,失败返回-1
/* dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项对应的文件描述符。dup函数返回的文件描述符一定是调用进程当前未使用的最小文件描述符。*/

dup函数只复制文件描述符表项,不复制文件表项和V节点,因此该函数所返回的文件描述符可以看做是参数文件描述符oldfd的副本,它们标识同一个文件表项

// 文件描述符的复制
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(){
	// 打开文件 oldfd
	int oldfd = open("./open.txt",O_WRONLY|O_CREAT|O_TRUNC,0664);
	if(oldfd == -1){
		perror("oldfd open");
		return -1;
	}
	printf("oldfd = %d\n",oldfd);
	// 复制文件描述符oldfd得到newfd
	int newfd = dup(oldfd);
	if(newfd==-1){
		perror("newfd dup");
		return -1;
	}
	printf("newfd = %d\n",newfd);
	// 通过oldfd向文件写入数据hello world
	char* buf = "hello world!";
	if(write(oldfd,buf,strlen(buf))==-1){
		perror("oldfd write");
		return -1;
	}
	// 通过newfd修改文件读写位置
	if(lseek(newfd,-6,SEEK_END)==-1){
		perror("newfd lseek");
		return -1;
	}
	buf = "linux!";
	// 通过oldfd再次写入数据 linux
	if(write(oldfd,buf,strlen(buf))==-1){
		perror("oldfd write");
		return -1;
	}
	// 关闭文件
	close(oldfd);
	close(newfd);
	return 0;
}

2:dup2

// 头文件:unistd.h
int dup2(int oldfd,int newfd);
- 功能:复制文件描述符表的特定条目到指定项
- 参数:
	- oldfd:源文件描述符
	- newfd:目标文件描述符
- 返回值:成功返回目标文件描述符(newfd),失败返回-1

dup2函数在复制由oldfd参数所标识的源文件描述符表项时,会先检查由newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将目标文件描述符newfd关闭,使之成为空闲项,再行复制。

// 文件描述符的复制
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(){
	// 打开文件 oldfd
	int oldfd = open("./open.txt",O_WRONLY|O_CREAT|O_TRUNC,0664);
	if(oldfd == -1){
			perror("oldfd open");
			return -1;
	}
	printf("oldfd = %d\n",oldfd);
	// 复制文件描述符oldfd得到newfd,指定目标文件描述符表项,如果在使用,则先目标文件描述符关闭,在进行复制
	int newfd = dup2(oldfd,STDOUT_FILENO);
	if(newfd==-1){
			perror("newfd dup");
			return -1;
	}
	printf("newfd = %d\n",newfd);
	// 通过oldfd向文件写入数据hello world
	char* buf = "hello world!";
	if(write(oldfd,buf,strlen(buf))==-1){
			perror("oldfd write");
			return -1;
	}
	// 通过newfd修改文件读写位置
	if(lseek(newfd,-6,SEEK_END)==-1){
			perror("newfd lseek");
			return -1;
	}
	buf = "linux!";
	// 通过oldfd再次写入数据 linux
	if(write(oldfd,buf,strlen(buf))==-1){
			perror("oldfd write");
			return -1;
	}
	// 关闭文件
	close(oldfd);
	close(newfd);
	return 0;
}

4.3 访问测试

1: access

// 头文件 unistd.h
int access(char const* pathname,int mode);
- 功能:判断当前进程是否可以对某个给定的文件执行某种访问。
- 参数:
	- pathname 文件路径
	- mode 被测试权限,可以以下改值
		R_OK - 可读否
		W_OK - 可写否
		X_OK - 可执行否
		F_OK - 存在否
- 返回值:成功返回0,失败返回-1

案例

#include <stdio.h>
#include <unistd.h>

int main(int argc,char* argv[]){
        for(int i=0;i<argc;i++){
                if(access(argv[i],F_OK)==-1){
                        printf("当前文件:%s不存在\n",argv[i]);
                }else{
                        printf("当前文件:%s存在,%s可读,%s可写,%s执行\n",argv[i],(access(argv[i],R_OK)==-1?" 不":" "),(access(argv[i],W_OK)==-1?" 不":" "),(access(argv[i],X_OK)==-1?" 不可":" "));
                }
        }
        return 0;
}

4.4 修改文件大小

1:truncate和ftruncate

// 头文件 unistd.h
int truncate(char const* path,off_t length);
int ftruncate(int fd,off_t length);
- 功能:修改指定文件的大小
- 参数:
	path 文件路径
	length 文件大小
	fd 文件描述符
- 返回值:成功返回0,失败返回-1/*该函数既可以把文件截短,也可以把文件加长,所有的改变均发生在文件的尾部,新增加的部分用数字0填充。*/

案例

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(){
        int fd = open("./open.txt",O_WRONLY|O_CREAT|O_TRUNC,0664);
           if(fd==-1){
                perror("open");
                return -1;
        }
        char* buf = "abcde";
        if(write(fd,buf,strlen(buf))==-1){
                perror("write");
                return -1;
        }
        // 该函数既可以把文件截短,也可以把文件加长,所有的改变均发生在文件的尾部,新增加的部分用数字0填充。
        if(truncate("./open.txt",3)==-1){
                perror("truncate");
                return -1;
        }
        if(ftruncate(fd,5)==-1){
                perror("ftruncate");
                return -1;
        }
        close(fd);
        return 0;
}

5、文件锁

目的:解决文件的读写冲突

5.1 读写冲突

  • 如果两个或两个以上的进程同时向一个文件的某个特定区域写入数据,那么最后写入文件的数据极有可能因为写操作的交错而产生混乱
  • 如果一个进程写而其它进程同时在读一个文件的某个特定区域,那么读出的数据极有可能因为读写操作的交错而不完整
  • 多个进程同时读一个文件的某个特定区域,不会有任何问题,它们只是各自把文件中的数据拷贝到各自的缓冲区中,并不会改变文件的内容,相互之间也就不会冲突

5.2 文件锁

  • 为了避免多个进程在读写同一个文件的同一个区域时发生冲突,Linux系统引入了文件锁机制,并把文件锁分为读锁和写锁两种,它们的区别在于,对一个文件的特定区域可以加多把读锁,对一个文件的特定区域只能加一把写锁

  • 基于锁的操作模型是:读/写文件中的特定区域之前,先加上读/写锁,锁成功了再读/写,读/写完成以后再解锁
    在这里插入图片描述

  • 相关函数
    1:fcntl

// 头文件 fcntl.h
int fcntl(int fd,F_SETLK/F_SETLKW,struct flock* lock);
- 功能:加解锁
- 参数:
	- F_SETLK非阻塞模式加锁,F_SETLKW阳塞模式加锁
	- 1ock对文件要加的锁
- 返回值:成功返回0,失败返回-1
// flock 结构体
struct flock{
	short I_type; ∥锁类型:F_RDLCK/F_WRLCK/F_UNLCK
	short I_whence;∥锁区偏移起点:SEEK_SET/SEEK_CUR/SEEK_END
	off_t I_start;//锁区偏移字节数
	off_t I_len;//锁区字节数 为0表示一直锁到文件尾
	pid_t l_pid;// 加锁进程的PID,-1表示自动设置
	}
/*通过对该结构体类型变量的赋值,再配合fcntl函数,以完成对文件指定区域的
加解锁操作*/
  • 案例
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc,char* argv[]){
	int fd= open("./open.txt",O_RDWR|O_CREAT|O_APPEND,0,0);
	if(fd==-1){
		perror("open");
		return -1;
	}
	// 以阻塞模式加锁
	struct flock l;
	l.l_type = F_WRLCK;// 写锁
	l.l_whence = SEEK_SET;
	l.l_start = 0;
	l.l_len = 0;
	l.l_pid=-1;
	if(fcntl(fd,F_SETLKW,&l) == -1){
		perror("加锁失败");
		return -1;
	}
	for(int i=0;i<strlen(argv[1]);i++){
		if(write(fd,&argv[1][i],1)==-1){
			perror("写入失败");
			return -1;
		}
		sleep(1);
	}
	// 解锁
	l.l_type = F_UNLCK;
	fcntl(fd,F_SETLKW,&l);
	close(fd);
	return 0;
}
  • 说明
    • 当通过close函数关闭文件描述符时,调用进程在该文件描述符上所加的一切锁将被自动解除
    • 当进程终止时,该进程在所有文件描述符上所加的一切锁将被自动解除
    • 文件锁仅在不同进程之间起作用,同-个进程的不同线程不能通过文件锁解决读写冲突问题
    • 通过fork/vfork函数创建的子进程,不继承父进程所加的任何文件锁
    • 通过exec函数创建的新进程,会继承原进程所加的全部文件锁,除非某文件描述符带有FD_CLOEXEC标志。

5.3 文件锁的内核结构

  • 每次对给定文件的特定区域加锁,都会通过fcntl函数向系统内核传递flock结构体,该结构体中包含了有关锁的一切细节,诸如锁的类型(读锁/写锁),锁区的起始位置和大小,甚至加锁进程的PID(填-1由系统自动设置)
  • 系统内核会收集所有进程对该文件所加的各种锁,并把这些flock结构体中的信息,以链表的形式组织成一张锁表,而锁表的起始地址就保存在该文件的V节点中
  • 任何一个进程通过fcntl函数对该文件加锁,系统内核都要遍历这张锁表,一旦发现有与欲加之锁构成冲突的锁即阻塞或报错,否则即将欲加之锁插入锁表,而解锁的过程实际上就是调整或删除锁表中的相应节点

6、文件的元数据

  • 相关函数
// 头文件 sys/stat.h
int stat(char const* path,struct stat* buf);
int fstat(int fd,struct stat* buf);
int Istat(char const* path,struct stat* buf);
- 功能:从i节点中提取文件的元数据,即文件的属性信息
- 参数:
	- path 文件路径
	- buf 文件元数据结构
	- fd 文件描述符
- 返回值:成功返回0,失败返回-1
/* stat中的结构可以查man手册查到相关的结构体定义 man 2 stat*/
  • 案例
#include<stdio.h>
#include<string.h>
#include<time.h>
#include<unistd.h>
#include<sys/stat.h>
//类型和权限的转换
// hello.txt --> stat()  --> struct stat s; 
// --> s.st_mode --> mtos() --> -rw-rw-r--
char* mtos(mode_t m){
    static char s[11] = {};
    if(S_ISDIR(m)){
        strcpy(s,"d");
    }else
    if(S_ISLNK(m)){
        strcpy(s,"l");
    }else
    if(S_ISSOCK(m)){
        strcpy(s,"s");
    }else
    if(S_ISCHR(m)){
        strcpy(s,"c");
    }else
    if(S_ISBLK(m)){
        strcpy(s,"b");
    }else
    if(S_ISFIFO(m)){
        strcpy(s,"p");
    }else{
        strcpy(s,"-");
    }
    strcat(s,m & S_IRUSR ? "r" : "-");
    strcat(s,m & S_IWUSR ? "w" : "-");
    strcat(s,m & S_IXUSR ? "x" : "-");
    strcat(s,m & S_IRGRP ? "r" : "-"); 
    strcat(s,m & S_IWGRP ? "w" : "-"); 
    strcat(s,m & S_IXGRP ? "x" : "-"); 
    strcat(s,m & S_IROTH ? "r" : "-"); 
    strcat(s,m & S_IWOTH ? "w" : "-"); 
    strcat(s,m & S_IXOTH ? "x" : "-");
    return s;
}
//时间转换
char* ttos(time_t t){
    // 2024-4-18 15:34:10
    static char time[20] = {};
    struct tm* l = localtime(&t);
    sprintf(time,"%04d-%02d-%02d %02d:%02d:%02d",
            l->tm_year + 1900,l->tm_mon + 1,l->tm_mday,
            l->tm_hour,l->tm_min,l->tm_sec);
    return time;
}
int main(int argc,char* argv[]){
    // ./a.out ./hello.txt
    struct stat s;//用来输出文件的元数据
    if(stat(argv[1],&s) == -1){
        perror("stat");
        return -1;
    }
    printf("        设备ID:%lu\n",s.st_dev);
    printf("       i节点号:%ld\n",s.st_ino);
    printf("    类型和权限:%s\n",mtos(s.st_mode));
    printf("      硬连接数:%lu\n",s.st_nlink);
    printf("        用户ID:%u\n",s.st_uid);
    printf("          组ID:%u\n",s.st_gid);
    printf("    特殊设备ID:%lu\n",s.st_rdev);
    printf("      总字节数:%ld\n",s.st_size);
    printf("    IO块字节数:%ld\n",s.st_blksize);
    printf("      存储块数:%ld\n",s.st_blocks);
    printf("  最后访问时间:%s\n",ttos(s.st_atime));
    printf("  最后修改时间:%s\n",ttos(s.st_mtime));
    printf("  最后改变时间:%s\n",ttos(s.st_ctime));
    return 0;
}

7、内存映射文件

函数同虚拟地址中的mmap和munmap一样,案例

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h> 
#include <sys/mman.h>
int main(){
	// 打开文件
	int fd= open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0,0);
	if(fd==-1){
		perror("open");
		return -1;
	}
	// 建立映射
	char* start = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(start == MAP_FAILED){
		perror("mmap");
		return -1;
	}
	// 操作文件
	if(ftruncate(fd,4096)==-1){ // 修改文件大小
		perror("ftruncate");
		return -1;
	}
	strcpy(start,"UC真好玩");// 相当于write,但是这里要求文件必须要有空间
	printf("%s\n",start);
	// 关闭映射
	if(munmap(start,4096)==-1){
		perror("munmap");
		return -1;
	}
	// 关闭文件
	close(fd);
	return 0;
}
  • 总结
    当用mmap和munmap映射物理内存时,是malloc的底层,而当使用mmap和munmap映射磁盘文件时,要比直接操作文件的write要快,尤其是读写大文件时,效果更显著,还能实现进程中的通信。
  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

启航zpyl

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

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

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

打赏作者

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

抵扣说明:

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

余额充值