【看表情包学Linux】系统下的文件操作 | 文件系统接口 | 系统调用与封装 | open,write,close 接口 | 系统传递标记位 O_RDWR,O_RDONLY,O_WRONLY...
🤣 爆笑教程 👉 《看表情包学Linux》👈 猛戳订阅 🔥
💭 写在前面:本章我们将正式接触系统接口,为斯坦福大学官方 OS 项目 Pintos 做铺垫,系统接口是非常重要的前置知识。本篇主要讲解底层文件系统接口,详细介绍 open 接口和它的 flags 参数 (即系统传递标记位),重点讲解 O_RDWR, O_RDONLY, O_WRONLY, O_CREAT 和 O_APPEND 这些操作模式。flags 标记位这一块的知识点,再一次出现了对 "位图" 的使用,这一块的知识点尤为重要,后期可能会大量涉及这样的设计手法。然后再顺带讲解 close 接口和 write 接口,在讲解这些系统底层文件接口前,我们还需要复习一下 C 语言中的文件操作知识,需要重新理解文件操作,不能仅停留在语言层面,而是要将眼光放宽到操作系统的层面去看待!重新理解当前路径、复习文件读取的接口 (fopen, fgets) 和文件操作模式 (w, r, a, a+) 后,再讲解文件系统接口时就能做到 "心中自有丘壑",能够有参照地通过比对的方式去学习了。通过学习底层文件接口,你就能体会到使用原生系统接口成本有多高了(可自行对比 fopen (C接口) 和 open (底层接口) 之间的使用成本)。系统调用封装把操作系统内核提供的系统调用封装成更容易使用的接口,使得应用程序可以更加轻松地使用操作系统提供的服务,而不需要了解内核的具体实现细节,并且提高了程序的可移植性,使其具有跨平台的能力。所以,对系统接口的封装无疑是一项伟大的工程……由于内容较多需控制篇幅,本篇就不深入地理解这些操作模式和接口了,我们将放到下一章继续讲解。值得注意的是,本章涉及底层文件操作,难免牵扯到进程, 权限, umask 权限掩码, 位图, C基础文件操作等知识点,难度相较于其他章节偏大,如果你还没有储备或掌握这些必要的前置知识,建议阅读或复习下面的文章:
🔗 前置知识:
- 【维生素C语言】第十六章 - 文件操作(上)
- 【维生素C语言】第十六章 - 文件操作(下)
- 【看表情包学Linux】Linux 权限 - Ⅲ0x03, Ⅴ0x02
- 【看表情包学Linux】进程的概念
📜 本章目录:
Ⅰ. 系统下的文件操作(System File Operation)
0x02 文件操作模式(File Operation Mode)
0x00 引入:系统调用与封装(Syscall and Wrapper)
本篇博客全站热榜排名:11
Ⅰ. 系统下的文件操作(System File Operation)
0x00 引入:关于文件操作的思考
(* 我们将引入一些思考的点,用红色数字标明)
我们曾经讲过:文件 = 文件内容 + 文件属性 ①
文件属性也是数据!这意味着,即便你创建一个空文件,也要占据磁盘空间!所以:
② 文件操作 = 文件内容的操作 + 文件属性的操作
因此,在操作文件的过程中,既改变内容又改变属性的情况很正常,不要把它们割裂开来!
💭 举个例子:当你往文件写入内容时,你的最新的修改时间、以及文件的 Size 大小时间以及属性数据也可能同时变化!所以,一般而言:属性是随着内容的变化而 (可能) 变化的。
我们一开始学习语言时,要读写一个文件,我们首先要做的事就是打开文件!
❓ 那么,所谓的 "打开" 文件,究竟在做什么? ③
"打开文件不是目的,访问文件才是目的!"
访问文件时,都是要通过 fread,fwrite,fgets... 这样的代码来完成对文件的操作的,
如果通过这些方式,那么 "打开" 文件就需要 将文件的属性或内容加载到内存 (memory) 中!
因为这是由冯诺依曼体系结构决定的,将来 要执行 fread,fwrite 来对文件进行读写的。
既然如此…… 是不是所有的文件都会处于被打开的状态呢?并不是! ④
那没有被打开的文件在哪里?在磁盘上静静地躺着呢! (存储在磁盘中)
对于文件的理解,在宏观上我们可以区分成 内存文件 (打开的文件) 和 磁盘文件。 ⑤
"如果一个文件从来都没打开,那么它就应该是个纯纯的磁盘文件。"
⑥ 通常我们打开文件、访问文件和关闭文件,是谁在进行相关操作?
运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。
实际上是 进程在对文件进行操作! 在系统角度理解是我们曾经写的代码变成了进程。
进程执行调度对应的代码到了 fopen,write 这样的接口,然后才完成了对文件的操作。
当我执行 fopen 时,对应地就把文件打开了,所以文件操作和进程之间是撇不开关系。
🔺 结论:学习文件操作,实际上就是学习 "进程" 与 "打开文件" 的关系。 ⑦
为了后续的讲解,我们先来简单回顾一下 C 语言的文件写入操作。
💬 代码演示: 在一个文件写入 10 行数据 (牛魔王火锅店开业大酬宾,简称牛魔酬宾)
#include int main(void){ FILE* pf = fopen("log.txt", "w"); // 写入 if (pf != NULL) { perror("fopen"); return 1; } const char* msg = "牛魔酬宾!\n"; int count = 1; while (count < 20) { fprintf(pf, "%s: %d\n", msg, count++); } fclose(pf);}
🚩 运行结果如下:
默认这个文件会在哪里形成?我们看到了,log.txt 默认在 当前路径 形成了。
对于什么是 "当前路径",本章就要把当前路径给讲清楚!
如果对当前路径的理解,仅仅停留于 "当前路径就是源代码所在的路径" 是远远不够的!
0x01 当前路径(Current Path)
我们前面说了,文件的本质实际上是进程与打开文件之间的关系。
因此文件操作和进程有关系,我们修改一下我们的代码,获取进程 ,查一下进程信息:
#include #include int main(void){ FILE* pf = fopen("log.txt", "w"); // 写入 if (pf == NULL) { perror("fopen"); return 1; } printf("Mypid: %d\n", getpid()); while (1) { sleep(1); } const char* msg = "牛魔酬宾!"; int count = 1; while (count <= 10) { fprintf(pf, "%s: %d\n", msg, count++); } fclose(pf);}
🚩 运行结果如下:
getpid 拿到进程 后,得益于 "昏睡指令" while(1){sleep(1);)
我们的进程就一直在欢快的跑着,再打开一个窗口,通过 $ls proc 指令检视该进程信息:
"这个小夫就是我们的进程,哈哈哈哈哈哈,"
指令后面接上 ,然后我们再加上个 -l 选项,就能把进程信息尽收眼底:
我们重点关注 和 , 后面链接指向的是可执行程序 mytest,即 路径 + 程序名。
而 (current working directory),即 当前工作目录,记录着当前进程所处的路径!
每个进程都有一个工作路径,所以我们上一节实现的简单 程序可以用 chdir 更改路径。
创建文件时,如果文件在当前目录下不存在,fopen 会默认在当前路径下自动创建文件。
默认创建在当前路径,和源代码、可执行程序在同一个路径下,因为这取决于 :
cwd -> /home/foxny/lesson18
我们可以来验证一下,使用 chdir 接口更改一下:
#include #include int main(void){ chdir("home/foxny/code"); FILE* pf = fopen("log.txt", "w"); // 写入 if (pf == NULL) { perror("fopen"); return 1; } printf("Mypid: %d\n", getpid()); while (1) { sleep(1); } const char* msg = "牛魔酬宾!"; int count = 1; while (count <= 10) { fprintf(pf, "%s: %d\n", msg, count++); } fclose(pf);}
此时 log.txt 应该不在默认路径 /home/foxny/lesson18 下了,而是在 /home/foxny/code下。
我们再次通过 $ls proc 指令查看信息明细时, 会变成 chdir 对应的路径:
所以,当前路径更准确的说法应该是:在当前进程所处的工作路径。
" 当前路径指的是在当前进程所处的工作路径 "
只不过默认情况下 (默认路径) ,一个进程的工作路径在它当前所处的路径而已,这是可以改的。
所以我们在写文件操作代码时,不带路径默认是源代码所在的路径,注意是默认!而已!
0x02 文件操作模式(File Operation Mode)
由文件操作符 (mode) 参数来指定,常用的模式包括:
r:只读模式,打开一个已存在的文本文件,允许读取文件。r+:读写模式,打开一个已存在的文本文件,允许读写文件。w:只写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。w+:读写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。a:追加模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。a+:读写模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。
* 有些我们已经在 C 语言专栏中详细介绍过了,如果对该知识点不熟可以复习一下。
🔗 复习链接:https://foxny.blog.csdn.net/article/details/119715195
这里我们重点讲一下 a 和 a+
a 对应的是 appending 的首字母,意为 "追加" 。属于写入操作,不会覆盖源文件内容。
💬 代码演示:测试追加效果
#include int main() { FILE* pf = pf = fopen("test.txt", "a"); // 写入数据到文件 fprintf(pf, "Hello, World!\n"); fclose(pf); return 0;}
🚩 运行结果如下:
每次运行都会在 test.txt 里追加,我们多试几次看看:
a(append) 追加写入,可以不断地将文件中新增内容。(这让我联想到了追加重定向)
不同于 w,当我们以 w 方式打开文件准备写入的时候,其实文件已经先被清空了。
0x03 文件的读取(File Read)
文件的读取在 C 专栏中我们也详细讲过了,这里我们只做一个简单的回顾。
我们复习一下文本行输入函数 fgets,它可以从特定的文件流中,以行为单位读取特定的数据:
char* fgets(char* s, int size, FILE* stream);_
💬 代码演示:fgets()
#include int main(void){ FILE* pf = fopen("log.txt", "r"); // 读 if (pf == NULL) { perror("fopen"); return 1; } char buffer[64]; // 用来存储 while (fgets(buffer, sizeof(buffer), pf) != NULL) { // 打印读取到的内容 printf("echo: %s", buffer); } fclose(pf);}
🚩 运行结果如下:
我们下面再来实现一个类似 $cat 的功能,输入文件名打印对应文件内容。
💬 代码演示:实现一个自己的 cat
#include int main(int argc, char* argv[]) { if (argc != 2) { printf("Usage: %s filename\n", argv[0]); return 1; } FILE* pf = fopen(argv[1], "r"); // 读取 if (pf == NULL) { perror("fopen"); return 1; } char buffer[64]; while (fgets(buffer, sizeof(buffer), pf) != NULL) { printf("%s", buffer); } fclose(pf);}
如果再把可执行程序 mytest 改名成 cat,$mv mytest cat
,
我们就实现了一个自己的 cat 代码。
Ⅱ. 文件系统接口(Basic File IO)
0x00 引入:系统调用与封装(Syscall and Wrapper)
当我们向文件写入时,最终是不是向磁盘写入?是!磁盘是硬件吗?就是硬件。
当我们像文件写入时,最后是向磁盘写入。磁盘是硬件,那么谁有资格向磁盘写入呢?
只能是操作系统!
既然是操作系统在写入,那我们自然不能绕开操作系统对磁盘硬件进行访问。
因为操作系统是对软硬件资源进行管理的大手子,你的任何操作都不能越过操作系统!
" OS:给你随便访问,那岂不是显得我很随便? "
所有的上层访问文件的操作,都必须贯穿操作系统。
想要被上层使用,必须使用操作系统的相关的 系统调用 (syscall) !
💭 思考:我们来回顾一下我们学习 C 语言的第一个函数接口:
printf("Hello, World!\n");
如何理解 printf?我们怎么从来没有见过这些系统调用接口呢?
显示器是硬件,我们 printf 的消息打印到了硬件上,是你自己调用的 printf 打印到硬件上的,
但并不是你的程序显示到了显示器上,因为显示器是硬件,它的管理者只能是操作系统,
你不能绕过操作系统,而必须使用对应的接口来访问显示器,我们看到的 printf 一打,
内容就出现在屏幕上,实际上在函数的内部,一定是 调用了系统调用接口 的。
🔺 结论:printf 函数内部一定封装了系统调用接口。
任何语言都是这样的,用到的接口都是语言提供给你的!正所谓……
" 纵横不出方圆,万变不离其宗!"
所有的语言提供的接口,之所以你没有见到系统调用,因为所有的语言都被系统接口做了 封装。
所以你看不到对应的底层的系统接口的差别。为什么要封装?原生系统接口,使用成本比较高。
系统接口是 OS 提供的,就会带来一个问题:如果使用原生接口,你的代码只能在一个平台上跑。
直接使用原生系统接口,必然导致语言不具备 跨平台性 (Cross-platform) !
如果语言直接使用操作系统接口,那么它就不具备跨平台性,可是为什么采用封装就能解决?
封装是如何解决跨平台问题的呢?很简单:
" 穷举所有的底层接口 + 条件编译 "
🔺 结论:我们学习的接口,C 库提供的文件访问接口,系统调用。它们两具有上下层关系,C 底层一定会调用这些系统调用接口。
- 解释:不同的语言,有不同的文件访问接口。
- 系统调用:这就是我什么我们必须学习文件级别的系统接口!
行文至此,我们已经正式引入了系统调用接口的概念!
0x01 文件打开:open()
打开文件,在 C 语言上是 fopen,在系统层面上是 open。
open 接口是我们要学习的系统接口中最重要的一个,没有之一!所以我们放到前面来讲。
#include #include #include int open(const char* pathname, int flags);int open(const char* pathname, int flags, mode_t mode);
我们可以看到,相较于 C 的 fopen 来说,这个接口一上来就显得很不友好。
"这特喵的有三个头文件啊,用个 open 要引三个头文件!"
然而,更恐怖的还在后面,有一大坨繁冗而复杂的东西……
我们看到,这个 open 接口一个是两参数的,一个是三参数的,这个我们放到后面解释。
① open 接口的 pathname 参数表示要打开的文件名,和 C 语言的 fopen 一样,是要带路径的。
② flags 参数是打开文件要传递的选项,即 系统传递标记位,我们下面会重点讲解。
③ mode 参数,就是 "文件操作模式" 了。但在这里:
" 就变得又臭又长,不经让人感叹 fopen 居然是如此的人性化!"
#if (defined _CRT_DECLARE_NONSTDC_NAMES && _CRT_DECLARE_NONSTDC_NAMES) || (!defined _CRT_DECLARE_NONSTDC_NAMES && !__STDC__) #define O_RDONLY _O_RDONLY #define O_WRONLY _O_WRONLY #define O_RDWR _O_RDWR #define O_APPEND _O_APPEND #define O_CREAT _O_CREAT #define O_TRUNC _O_TRUNC #define O_EXCL _O_EXCL #define O_TEXT _O_TEXT #define O_BINARY _O_BINARY #define O_RAW _O_BINARY #define O_TEMPORARY _O_TEMPORARY #define O_NOINHERIT _O_NOINHERIT #define O_SEQUENTIAL _O_SEQUENTIAL #define O_RANDOM _O_RANDOM#endif
真是让人心脏骤停…… 我们慢慢来 ~
❓ 思考:在 Linux 下,C 语言中文件不存在,就直接创建它,创建是不是需要权限?
当然是需要的,我们需要给文件设置初始权限,这个 mode 参数就是干这个活的。
我们再来看看这个接口的返回值,居然是个 int,而不是我们 fopen 的 FILE*
④ open 的返回值是个 int,返回 -1 表示 error,并设置 errno。
0x01 flags 系统传递标记位
open 接口中的 flags 参数在 OS 底层接口函数中还是很常见的,所以我们要重点讲解。
我们可以输入 man 2 open 看看如何设置 flags 参数,实际上就是设置文件操作模式的。
我们重点关注下面这几个文件操作模式,它们被定义在
如果你是第一次见,可能会感到强烈的 "陌生感",这个 O 是什么鬼,好像还都是宏!
O 实际上就是 Open 的意思,它们的用途通过名字不难猜:
- (open read only) :只读方式打开
- (open write only) :只写方式打开
- (open read write) :读写方式打开
- (open append) :追加方式打开
- (open create) :创建 (若文件不存在就创建)
int open(const char* pathname, int flags);
我们称 flags 为 标记位,并且它是个整数类型(C99 标准之前没有 bool 类型)
标记位实际上我们造就用过了,比如定义 flag 变量,设 flag=0,设 flag=1,传的都是单个的。
❓ 思考:但如果我想一次传递多个标志位呢?定义多个标记位?flag1, flag2, flag3...
那我要传 20 个呢,定义 20 个标记位不成?遇到不确定传几个标志位的情况下,该怎么办?
我们看看写底层的大佬是如何解决的:
👑 方案:系统传递标记位是通过 位图 来进行传递的。
想必大家已经对位图不陌生了,在前几章我们讲解 waitpid 的 status 参数时就介绍过了:
status 参数也是整型,也是被当作一个 "位图结构" 看待的,这里的 flags 也是如此!
当成位图,就是一串整数 ,如此一来标记的可能性就直接超级加倍了。
0000 0000
我们可以让不同的位表示,是否只读,是否只写,是否读写…… 等等等等:
每个 宏标记一般只需要满足有一个比特位是 1,并且和其它宏对应的值不重叠 即可。
让每一个宏对应不同的比特位,在内部就可以做不同的事情,为了大家能够更好的理解,
下面我们自己设计一个接口,仿照系统传递标记位的做法,通过这种思路去实现一下标记位。
💬 代码演示:我们创建一个 test_flag.c
#include #define PRINT_A 0x1 // 0000 0001#define PRINT_B 0x2 // 0000 0010#define PRINT_C 0x4 // 0000 0100#define PRINT_D 0x8 // 0000 1000#define PRINT_DFL 0x0// openvoid Show ( int flags ){ if (flags & PRINT_A) printf("Hello A\n"); if (flags & PRINT_B) printf("Hello B\n"); if (flags & PRINT_C) printf("Hello C\n"); if (flags & PRINT_D) printf("Hello D\n"); if (flags == PRINT_DFL) printf("Hello Default\n");}int main(void){ printf("# PRINT_DFL: \n"); Show(PRINT_DFL); printf("# PRINT_A: \n"); Show(PRINT_A); printf("# PRINT_B: \n"); Show(PRINT_B); printf("# PRINT_A AND PRINT_B: \n"); Show(PRINT_A | PRINT_B); printf("# PRINT_C AND PRINT_D: \n"); Show(PRINT_C | PRINT_D); printf("# PRINT_A AND PRINT_B AND PRINT_C AND PRINT_D: \n"); Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D); return 0;}
🚩 运行结果:
💡 说明:通过标记位,可以在内部做对应的事情。打印 A 就打印 hello A,打印 A 和 B 就打印 hello A 和 hello B,现在我们再理解别人给我们传递宏标志位的做法。我们每一个宏所对应的值,在二进制位上互相都是不重叠的,一人用一个比特位。我们调用时要同时打印多个就按位或,内部再做条件判断,检测条件是否成立,这即是系统传参的做法。一个系统调用接口可以穿十几乃至三十几个的标志位,基本是够用的。
0x02 open 接口用法演示
讲完了 flags 标记位,现在我们可以演示 open 接口的用法了。
int open(const char* pathname, int flags);
💬 代码演示:是用 open() 打开 log.txt 文件没有就创建。
#include #include #include #include int main(void){ int fd = open("log.txt", O_WRONLY | O_CREAT); if (fd < 0) { // 打开失败 perror("open"); return 1; } printf("fd: %d\n", fd); // 把 fd 打出来看看 return 0;}
💡 代码说明:
① 这里我们选择取名为 fd,而不是我们 fopen 习惯用的 pf/fp,因为 fd 描述文件描述符,这也是我们后面章节要重点讲解的,所以这里取 fd 来接收 open 接口的返回值,也算是预热一下
② 只写是 O_WRONLY,如果没有对应文件就创建,创建时 O_CREAT,这里我们用 | 把二者相关联可以了。
③ open 的返回值是 int,如果返回 -1 则表示 error,所以如果 fd < 0 就说明打开失败了,我们礼貌性的 perror 一下(保持我们 fopen 的习惯,这是好习惯)。
④ 最后,文件打开成功,我们把 fd 的值顺便打印出来看看。
🚩 运行结果如下:
如果你要创建这个文件,该文件是要受到 权限的约束的!
创建一个文件,你需要告诉操作系统默认权限是什么。
当我们要打开一个曾经不存在的文件,不能使用两个参数的 open,而要使用三个参数的 open!
也就是带 mode_t mode 的 open,这里的 mode 代表创建文件的权限:
int open(const char* pathname, int flags, mode_t mode);
修改一下我们的代码,使用带 mode 参数的 open:
int main(){ int fd = open("log.txt", O_APPEND | O_CREAT, 0666); // 八进制表示 if (fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); return 0;}
🚩 运行结果如下:
* 如果你对这里出现的 "权限八进制表示法" 感到疑惑,可以看权限章节,那一章有讲。
🔗 链接:【看表情包学Linux】权限 - Ⅲ 0x03
因为你要创建的文件,你再怎么🐂🍺,你也要听操作系统的!我们来看看 umask:
你要 666,操作系统不能给你,因为 umask 是 0002,所以最多只能给你 664。
那好!我今天非要创建 666!神挡杀神,操作系统挡杀操作系统!
" 没关系,我会出手!umask 限制我?我特喵直接改 umask! "
我们在权限章节介绍过 umask,实际上它也是一个系统级接口。
我们现在就是要 666,我们只需要调用 umask(),然后传 0:umask(0)
就可以让权限掩码暂时不听按操作系统的默认权限掩码,而用你设置的!
int main(){ umask(0); // umask现在就为0,听我的,别听操作系统的umask了 // 额... 好吧,okok int fd = open("log.txt", O_APPEND | O_CREAT, 0666); if (fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); return 0;}
🚩 运行结果如下:
实际上,umask 命令就是调用这个接口的。
umask 设为 0,可以让我们以确定的权限打开文件,比如服务器要打开一个日志文件,权限就必须要按照它对应的权限设置好,不要采用系统的默认权限,可能会出问题。
0x03 文件关闭:close()
在 C 语言中,关闭文件可以调用 fclose,在系统接口中我们可以调用 close 来关闭:
#include int close(int fd);
我们输入 man 2 close 查看一下手册:
该接口相对 open 相对来说比较简单,只有一个 fd 参数,我们直接看代码:
#include #include #include #include #include // 需引入头文件int main(void){ umask(0); int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); perror("open"); return 1; } printf("fd: %d\n", fd); close(fd); // 关闭文件 return 0;}
🚩 运行结果如下:
0x04 文件写入:write()
文件打开和文件关闭都有了,我们总要干点事吧?现在我们来做文件写入!
在 C 语言中我们用的是 fprintf, fputs, fwrite 等接口,而在系统中,我们可以调用 write 接口:
#include ssize_t write(int fd, const void* buf, size_t count);
📚 write 接口有三个参数:
- fd:文件描述符
- buf:要写入的缓冲区的起始地址(如果是字符串,那么就是字符串的起始地址)
- count:要写入的缓冲区的大小
💬 代码演示:向文件写入 5 行信息
#include #include #include #include #include // 需引入头文件int main(void){ umask(0); int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); if (fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); // 向文件写入 5 行信息 int cnt = 0; const char* str = "牛魔酬宾!\n"; while (cnt < 5) { write(fd, str, strlen(str)); cnt++; } close(fd); return 0;}
🚩 运行结果如下:
⚡ 顺便教一个清空文件的小技巧: > 文件名 ,前面什么都不写,直接重定向 + 文件名:
$ > log.txt
📌 [ 笔者 ] 王亦优📃 [ 更新 ] 2023.3.17❌ [ 勘误 ] 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免, 本人也很想知道这些错误,恳望读者批评指正!
📜 参考资料 C++reference[EB/OL]. []. http://www.cplusplus.com/reference/. Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/. 比特科技. Linux[EB/OL]. 2021[2021.8.31 xi |
来源地址:https://blog.csdn.net/weixin_50502862/article/details/129531872
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341