当前位置: 首页 > news >正文

【寻找Linux的奥秘】第十章:基础文件IO(上)

QQ20250615-205935

前言

本专题将基于Linux操作系统来带领大家学习操作系统方面的知识以及学习使用Linux操作系统。前面我们认识了Linux的各种指令以及工具,并且学习了进程的相关知识,那么接下来让我们进入新的章节,学习新的内容。本章我们要学习的是——基础文件IO。

本节重点:

  • 复习C语言⽂件IO相关操作
  • 认识⽂件相关系统调⽤接⼝
  • 认识⽂件描述符,理解重定向
  • 对⽐fdFILE,理解系统调⽤和库函数的关系
  • 理解重定向操作

1. 理解“文件”

1.1 一般角度

狭义上来说,文件是存储在磁盘上的数据,而磁盘内保存的数据与内存中不同,磁盘是永久性存储介质,当我们拔掉电源后磁盘上的数据不会消失,因此文件在磁盘上的存储是永久性的。

我们知道,磁盘也是外设,它既是输出设备,也是输入设备。因此我们对于文件的操作本质上是对外设的输入和输出,简称IO。

对于文件的理解,还有一个角度,叫做Linux下一切皆文件,在Linux系统中,像键盘、显示器、网卡、磁盘等等都是文件,Linux把各种硬件也都当作了文件。(至于是如何实现的后面再讲)

我们知道,文件中不单单只有文件内容,文件是文件属性(元数据)和文件内容的集合文件 = 属性(元数据)+ 内容),因此对于大小为0KB的空文件也是有大小的,会占用一定的磁盘空间。因此我们对文件的操作本质上是操作文件内容和操作文件属性两方面。

1.2 系统角度

我们知道,要想访问一个文件,首先我们需要先“打开”文件,那么是谁去打开文件呢?在操作系统中,实际上是由进程去打开文件,因此对文件的操作本质上是进程对文件的操作

文件都保存在磁盘上,而磁盘的管理者是操作系统,也就是说文件的管理者是操作系统,那么操作系统对于被打开的文件会进行管理,管理的方法与进程的管理是一致的,都是先描述,再组织。(具体的管理方法下面会讲)

我们在之前学习C/C++的时候,包括其他的语言,都可以通过相关接口去对文件进行操作。例如在C语言/C++中我们可以使用对应的库函数去创建文件、修改文件等等。不过文件的读写本质其实不是通过这些库函数来操作的,而是通过在库函数中调用文件相关的系统调用接口来实现的。像C语言中的fopen、fwrire等库函数都封装了底层OS的文件系统调用。

2. 回顾C语言文件接口

既然是讲文件操作,那让我们回顾一下之气在C语言中学习的文件操作,因为Linux的底层就是用C语言来实现的。

2.1 C中的读写操作

在C语言中,我们如果想对文件进行操作,首先要使用fopen函数打开对应的文件,并且通过传递不同的参数来确定以什么样的权限来打开文件。

这里我们使用的函数都是库函数。

QQ20250609-134806

这些都是我们之前学习过的,我们简单介绍一下即可。

首先是两个参数:

path

  • path可以是相对路径,就是以当前进程所在的路径,之前我们在讲解进程时讲过进程的PCB中包含着一个cwd,也就是该进程当前的工作路径。所以我们可以直接输入文件名,这样查找文件和创建文件都是在cwd所对应的路径下进行的。

    打开⽂件,本质是进程去打开文件。由于进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS就能知道要创建的⽂件放在哪⾥。

  • 此外,path还可以直接写绝对路径。

mode

  • 'r':只读模式,打开文件进行读取。如果文件不存在,返回 NULL
  • 'w':只写模式,打开文件进行写入。如果文件已存在,会将文件内容清空;如果文件不存在,则会创建新文件。
  • 'a':追加模式,打开文件进行写入。如果文件存在,数据会被追加到文件末尾;如果文件不存在,则会创建新文件。
  • 'r+':读写模式,打开文件进行读取和写入。如果文件不存在,返回 NULL
  • 'w+':读写模式,打开文件进行读取和写入。如果文件存在,文件内容会被清空;如果文件不存在,则会创建新文件。
  • 'a+':读写模式,打开文件进行读取和写入。如果文件存在,数据会被追加到文件末尾;如果文件不存在,则会创建新文件。
  • 'b':二进制模式。在文件操作时以二进制形式打开文件。例如,"rb" 表示以二进制方式读取文件,"wb" 表示以二进制方式写入文件。

而它的返回值类型FILE*是结构体指针,它代表一个已经打开了的文件,并持有有关这个文件的所有信息。至于该类型的具体含义我们下面再说。

我们以相应的权限打开文件后,就可以进行相应的读写操作了,这里又需要用到两个库函数,分别是freadfwrite。它们用于读文件和写文件,我们简单的回顾一下:

fwrite

QQ20250614-125106

功能: 向文件中写入二进制数据。

参数说明:

  • ptr:指向要写入数据的内存缓冲区的指针
  • size:每个数据项的字节大小
  • nmemb:要写入的数据项个数
  • stream:文件指针

返回值: 实际成功写入的数据项个数

一个简单的代码示例:

#include <stdio.h>
#include <stdlib.h>int main() {FILE *file;int numbers[] = {1, 2, 3, 4, 5};// 写入数据file = fopen("data.bin", "wb");if (file != NULL) {size_t written = fwrite(numbers, sizeof(int), 5, file);printf("写入了 %zu 个整数\n", written);fclose(file);}return 0;
}

fread

QQ20250614-125055

功能: 从文件中读取二进制数据。

参数说明:

  • ptr:指向存储读取数据的内存缓冲区的指针
  • size:每个数据项的字节大小
  • nmemb:要读取的数据项个数
  • stream:文件指针

返回值: 实际成功读取的数据项个数。

一个简单的代码示例:我们从上面写入的文件中读出数据

#include <stdio.h>
#include <stdlib.h>int main() {FILE *file;int read_numbers[5];// 读取数据file = fopen("data.bin", "rb");if (file != NULL) {size_t read = fread(read_numbers, sizeof(int), 5, file);printf("读取了 %zu 个整数\n", read);for (int i = 0; i < read; i++) {printf("%d ", read_numbers[i]);}printf("\n");fclose(file);}return 0;
}

这里需要注意的是,当我们对一个文件连续调用fread()读取文件时并不会从头开始读,这是因为文件指针的位置是自动向前移的,也就是说每次调用 fread() 后,文件指针 FILE* 会自动移动到读取数据的末尾处,下一次再调用 fread() 时,就会从上次读取完的位置继续往后读。

这是因为当我们用 fopen() 打开一个文件时,系统为你创建了一个文件指针(FILE* 类型,它其实是一个结构体),它内部维护了一个“当前位置”(文件偏移量)的变量:

  • 第一次读取时,从文件开头读取
  • 每次读取完数据后,偏移量自动向前移动
  • 所以不会重复读相同位置

我们可以通过使用 rewind()fseek() 函数去改变文件偏移量:

  • rewind:用于将文件指针重置到开头。

    rewind(FILE *stream); //stream为需要重置的文件指针
    
  • fseek:用于手动控制文件指针的位置。

    fseek(FILE *stream, long offset, int whence);
    //stream为需要设置的文件指针
    //offset为偏移量,单位为字节
    //whence为基准值,也就是从哪个位置偏移
    

    whence 有三个取值(定义在 <stdio.h> 中):

    常量含义
    SEEK_SET文件开头开始偏移
    SEEK_CUR当前位置偏移
    SEEK_END文件末尾开始偏移

注意事项:

  1. freadfwrite按块(block)读写,适合处理结构体、数组等二进制数据。
  2. 返回值是成功读/写的块数(不是字节数),要用它判断操作是否成功。
  3. 文件必须以 "rb" / "wb" 模式打开,否则可能会出错或产生不可预期行为。
  4. 对文本文件请使用 fprintf / fscanf,不要用 fwrite / fread

2.2 标准输入输出流

我们先来认识一下什么是流,大家可能一直听过各种流,但流究竟是什么呢?

在 C 语言中,“流”stream)指的是数据的有序传输通道,用于在程序和输入/输出设备(如文件、终端、网络)之间进行数据传输

简单来说:流是你和外部世界之间的桥梁,数据像水一样通过这条“流”流进来或流出去。

在 C 标准库中:

  • 不直接操作“文件”或“终端”,而是操作一种抽象对象:FILE*(流指针)
  • 你使用 fopen() 打开一个文件,实际上系统为你创建了一个流对象(指针)

在 C 语言中,“流”是你与文件、终端等设备交换数据的通用通道,用来隐藏底层设备差异,统一进行读写操作。

我们平常往显示器上输出信息,实际上就是往标准输出流中进行写入,标准输出流一般就是显示器文件。

当我们在执行C程序时,C会默认打开三个输入输出流:
QQ20250614-182549

分别是:stdin、stdout、stderr,观察可以发现它们的类型都是FILE,而fopen的返回值类型也是FILE*,也就是说它们其实都是一个个被打开的文件。

名称类型默认连接的设备用途
stdin输入流键盘接收输入
stdout输出流屏幕(终端)打印正常输出内容
stderr输出流屏幕(终端)打印错误或调试信息

为什么这三个文件流会默认打开呢?

因为这三个标准流是所有程序与“外部世界”交互的最基本通道,C语言运行时会自动打开它们,这样你的程序就能立即读入数据、打印输出、报告错误无需手动处理底层设备逻辑。如果这三个流不自动打开,程序连最基本的输入输出都做不了——你必须自己用 open()fopen() 打开终端设备,很麻烦。

那么我们如果想将信息输出到显示器上就可以通过多种不同的方法了:

#include <stdio.h>
#include <string.h>
int main()
{const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}

这些是我们之前学习C语言时学习过的有关文件方面的知识,我们进行了简单的回顾,下面让我们进入新的学习环节。

3. 系统文件IO

打开⽂件的⽅式不仅仅是fopen,ifstream等语⾔层的⽅案,其实它们的底层都是通过系统调用去打开⽂件,我们这里主要讲解一下Linux系统下的系统调用。不过,在学习系统⽂件IO之前,先要了解下如何给函数传递标志位,该⽅法在系统⽂件IO接⼝中会使⽤到。

3.1 标志位

给函数传递标志位也就是通过给函数传入特定的参数,使其执行特定的功能。而这个功能的实现,我们一般采用位图加宏的方式,例如下面的代码:

#include <stdio.h>#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100void func(int flags) 
{if (flags & ONE) printf("flags has ONE! ");if (flags & TWO) printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE! ");printf("\n");
} int main() 
{func(ONE);func(THREE);func(ONE | TWO);func(ONE | THREE | TWO);return 0;
}

如上面的代码所示,我们通过位操作来实现通过传递不同的参数使函数执行不同的功能。简单解释一下上面的代码,我们将不同的标志位定义为ONE、TWO、THREE,当我们传入ONE时,在函数内部只有flag & ONE的结果为真,因此只会执行该代码块内的代码;而当我们传入ONE | WTO时,在函数内部有flag & ONEflag & TWO的结果为真,所以会执行这两个代码块中的代码,其他类似。

在C语言中,像fopen、fclose、fread、fwrite这些库函数在底层实际上是封装了系统调用,在Linux系统中,这些系统调用分别是open、close、read、write,下面让我们来认识一下这些系统调用接口。

3.2 文件系统调用

有了上面标志位的介绍,下面让我们来看一看Linux中关于文件操作的系统调用:

3.2.1 open

image-20250615201439133

在 Linux 系统中,open() 是一个用于打开或创建文件或设备的系统调用,它返回一个文件描述符(file descriptor)(后面详将),供后续的 read()write()close() 等函数使用。

参数说明:

  • pathname: 要打开的文件路径(如 "file.txt""/dev/sda"

  • flags:标志位,指定打开方式,常见的值如下表所示

    宏名含义
    O_RDONLY只读
    O_WRONLY只写
    O_RDWR读写
    O_CREAT文件不存在则创建
    O_TRUNC文件存在则清空内容
    O_APPEND每次写入都追加到文件末尾
    O_EXCLO_CREAT 一起用,确保文件不存在
    O_NONBLOCK非阻塞打开(如设备或管道)
  • mode(权限位):仅当使用 O_CREAT 创建文件时使用,指定新文件的权限。

例如:

open("log.txt", O_WRONLY | O_CREAT, 0644);

表示:

  • 以只写方式打开 log.txt
  • 如果文件不存在就创建它
  • 新文件的权限为 rw-r--r--

也就是说,当我们在C语言中使用fopen打开文件时,如果我们打开文件的权限设为'w',那么在fopen的底层实现中,实际上是调用了open这个系统调用并且给它传入的flagsO_WRONLY | O_CREAT | O_TRUNC,如果打开文件的权限为’a’,那么传入的flagsO_WRONLY | O_CREAT | O_APPEND。当我们传入的标志位中如果有O_CREAT ,那么我们就需要在传入一个参数mode,也就是权限位,指定创建新文件的权限。

函数具体使⽤哪个,和具体应⽤场景相关,如⽬标⽂件不存在,需要open创建,则第三个参数表⽰创建⽂件的默认权限,否则,使⽤两个参数的open。

open成功执行,返回值是新打开文件的文件描述符(后面详将),如果失败则返回-1。

C 语言不支持函数重载,但 open() 有两个同名版本,它是怎么做到的?

这是不是 C 语言函数重载,而是函数的可变参数机制(变参) + 宏

Linux 的 open() 实际在源码中定义如下:

int open(const char *pathname, int flags, ...);

它用的是 C 语言中的变参(...)语法。这个机制允许传入 可选的第三个参数(即 mode_t mode),用于在创建文件时指定权限。

这是 C 语言中通过 ...(变参)实现“伪重载”的一种技巧。

随着Linux的发展,open() 曾经是系统调用,但在现代 Linux 中已演进为库函数,它通过调用 openat() 系统调用来实现功能。这是 Linux 系统 API 演进的典型例子——保持接口兼容性的同时,底层实现更强大、更安全。从严格的技术角度,现在不应该说 open() 是系统调用,它是库函数。但由于历史习惯和使用体验相同,很多文档和程序员仍然这样称呼,在这里我们还是先称其为系统调用,因为其的确是我们fopen的底层调用。准确的说法是:“open() 是对 openat() 系统调用的包装”。

3.2.2 colse

QQ20250615-204138

close这个系统调用就相对简单,它的作用就是用于关闭一个打开的文件描述符fd,它的返回值成功返回0,失败返回-1并设置errno

closefclose的底层调用,由于我们现在还并不了解什么是文件描述符,所以先了解一下即可。

3.2.3 read

read() —— 从文件描述符中读取数据

QQ20250615-204752

参数说明:

  • fd:文件描述符(由 open()socket() 等返回)
  • buf:数据缓冲区指针,读入的数据存放在这里
  • count:最多读取的字节数

返回值:

  • 成功:返回实际读取的字节数(<= count
  • 遇到文件结尾(EOF):返回 0
  • 失败:返回 -1,并设置 errno

readfread的底层调用,所以它们的参数是比较相似的,不同的在于我们使用fread读取文件的时候我们需要的从哪个文件流中读取,而read是从哪个文件描述符中读取。下面是代码示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;} const char *msg = "hello bit!\n";char buf[1024];while(1){ssize_t s = read(fd, buf, strlen(msg));if(s > 0){printf("%s", buf);}else{break;}} close(fd);return 0;
}
3.2.4 write

write() —— 向文件描述符写入数据

QQ20250615-205347

参数说明:

  • fd:文件描述符
  • buf:要写的数据缓冲区指针
  • count:写入的字节数

返回值:

  • 成功:返回实际写入的字节数(可能 < count
  • 失败:返回 -1,并设置 errno

writefwrite的底层调用。readwrite这两个系统调用的使用方法极为类似,我们可以类比来看。下面让我们看一下代码示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{umask(0);int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;} int count = 5;const char *msg = "hello bit!\n";int len = strlen(msg);while(count--){write(fd, msg, len);//fd: 后⾯讲, msg:缓冲区⾸地址, len: 本次读取,期望写⼊多少个字节的数据。 返回值:实际写了多少字节数据} close(fd);return 0;
}

read()write() 是 Linux 中最基本、最通用的系统调用,它们直接操作文件描述符,支持各种 I/O 对象,包括文件、设备、socket 等,是一切高级 I/O 的基础。

3.3 文件描述符

前面我们认识文件操作的系统调用时我们发现,这些系统调用都和一个称作文件描述符的整数有关,就跟我们文件操作的库函数中的文件流FILE*指针一样。那么文件描述符到底是什么呢?在回答这个问题之前我们先回顾一下刚开始所说的东西。

我们知道操作系统不仅要管理我们的进程,还需要管理被打开的文件。我们知道对文件操作的本质实际上进程对文件进行操作,那么在一个进程中我们可以打开很多个文件,那么操作系统就需要对这些打开的文件进行管理,因此在我们进程的PCB中就存在一个*files指针,它的类型是files_struct的结构体,在这个结构体中包含着当前进程所打开的文件的一些信息,其中包含一个指针数组,它的类型是file*,对应着一个个文件对象,每打开一个对象操作系统就会创建一个对应的file结构体对象,里面存放了文件相关的inode元信息。

而我们的文件描述符,其实就是上面我们所说的指针数组的下标!下面我们通过图示来理解一下:

CT-20250615132613

所以,我们所说的文件描述符,其实就是fd_array[]数组的下标,当我们程序运行的时候,系统会默认打开三个文件,分别是stdin、stdout、stderr三个文件,它们也刚好对应了fd_array[]数组的前三个元素,因此它们所对应的文件描述符就是0、1、2。那么我们也就知道了在C语言中的FILE结构体中一定封装了文件描述符fd

⽽现在知道,⽂件描述符就是从0开始的⼩整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,⽂件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

文件描述符的分配规则:

我们通过代码来看:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

我们可以看到结果是fd:3。那么关闭文件描述符0呢?

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{close(0);int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

发现是结果是: fd: 0 。可⻅,⽂件描述符的分配规则:files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。

说了那么多,接下来让我们验证一下我们上面所说的是否正确,我们在Linux的内核源码中去寻找一下答案:

QQ20250615-215614

在操作系统接口层面,它们只认fd,也就是文件描述符。那么为什么会存在文件描述符呢?这是我们Linux系统层面的概念,C语言的FILE结构体中只是封装了fd,其实不论是C语言也好,C++、java也罢,它们都有自己的文件操作接口,这些接口的底层其实都调用的是系统接口,这是为了方便我们使用这些语言的可移植性!试想我们在Linux中使用C语言写了一个程序,里面调用了Linux的系统调用,那么当我们把这个程序在windows下去执行就会发生错误,毕竟windows有自己的系统调用,它并不认识Linux的系统调用。我们使用的这些语言它们在每个系统上都有属于该系统对应的库文件,它们确保了我们在使用库函数的时候可以根据系统的不同去调节库函数底层实现的具体细节,这样一来,我们写的程序便可以在不同的系统上运行,便具有了可移植性。

4. 重定向

当我们认识了文件描述符后,我们就可以对重定向操作进行解释了。我们先来看一段代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{close(1);int fd = open("myfile", O_WRONLY|O_CREAT, 00644);if(fd < 0){perror("open");return 1;} printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}

上述代码中我们先关闭了文件描述符1,也就是标准输出stdout,这样一来,我们新打开的文件myfile的文件描述符是1,运行该程序我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中fd==1。这种现象叫做输出重定向。

那么重定向的本质是什么呢?

QQ20250615-221533

因此,重定向的本质实际上就是把对某个文件的操作通过改变文件描述符指向的内容从而改变操作的文件。这个过程是通过dup2系统调用来实现的:

QQ20250615-222206

它的作用就是把 oldfd 的文件指针复制给 newfd,替换 newfd 的原内容。

下面我们来看一下具体示例:

输出重定向:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>int main() {int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perror("open");return 1;}dup2(fd, 1);  		      // 标准输出 → out.txtclose(fd);                // fd 已不再需要printf("Hello, world!\n");  // 实际写入到 out.txtreturn 0;
}

输入重定向:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>int main() {int fd = open("input.txt", O_RDONLY);if (fd < 0) {perror("open");return 1;}dup2(fd, 0);  // 标准输入 ← input.txtclose(fd);char buf[128];fgets(buf, sizeof(buf), stdin);  // 从 input.txt 读取printf("Read: %s", buf);return 0;
}

追加重定向:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>int main() {int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);if (fd < 0) {perror("open");return 1;}dup2(fd, 2);  // 标准输出 → 追加到 log.txtclose(fd);printf("Appended line\n");  // 会追加而不是覆盖return 0;
}

像我们的printf函数,我们知道它是在终端上打印相应的内容,实际上它的底层是向文件描述符为1的文件中写入,而在C语言中声明的stdin、stdout、stderr实际上文件描述符为0、1、2的文件流,也就是说它们对应的文件描述符,而不是对应的文件,我们可以通过更改文件描述符使其代表的变为其他文件。

因此我们在终端中的重定向操作实际上是通过dup2系统调用和文件的打开权限一同完成的。

尾声

本章讲解就到此结束了,若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

相关文章:

  • 华为流程体系拆解系列:L1-L6分层拆解逻辑
  • CentOS 7 环境下 Visual Studio Code 安装与部署
  • 分布式系统全链路监控之一:分布式全链路监控基础概念和OpenTelemetry
  • 5.安装IK分词器
  • 鸿蒙组件通用事件开发全攻略:从基础交互到工程实践
  • 华大TAU1114-1216A00四系统GNSS定位模块,车载/穿戴/IoT全适配!-165dBm高灵敏度,定位快人一步!“
  • 基于nacos和gateway搭建微服务管理平台详细教程
  • 安宝特案例丨突破传统手术室,Vuzix AR 眼镜圆满助力全膝关节置换术
  • 【力扣 中等 C】912. 排序数组
  • 高级网络中间人攻击与加密防护机制
  • 安宝特方案丨AR破解带电配网作业困局!全方位解决方案赋能电力运维新变革
  • 日志混乱与数据不一致问题实战排查:工具协同调试记录(含克魔使用点)
  • java 数组排序算法
  • 【Linux指南】文件内容查看与文本处理
  • OpenCV CUDA模块设备层------简介
  • C++初阶-queue和deque(队列和双端队列)
  • cockplit 出现 Cannot refresh cache whilst offline 处理
  • 时间序列分析
  • 【Java】抽象类与接口全解析
  • android 启动速度优化
  • 国内哪家网站做的系统纯净/百度官方网站下载
  • 笔记本怎么建设网站/seo诊断分析工具
  • 凡科网的网站建设好用吗/如何快速网络推广
  • 静态网站 apache/2022好用值得推荐的搜索引擎
  • 佛山市网站建设公司/百度搜索引擎官网
  • iis 7.5 网站/网络营销能干什么工作