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

【Linux】Linux基础I/O

参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/125714967

一、C语言文件操作

1.1 C语言中相关的文件操作接口介绍

下面的接口用于C语言的文件操作

函数接口函数说明
fopen打开文件
fclose关闭文件
fputc一次写一个字符
fgetc一次读一个字符
fputs写一行数据
fgets读一行数据
fprintf格式化输出函数
fscanf格式化输入函数
fwrite以二进制的形式将数据写入
fread以二进制的形式将数据读出来
fseek根据文件指针的位置和偏移量来定位文件指针
ftell计算文件指针相对于起始位置的偏移量
rewind让文件指针回到起始位置
feof判断是不是遇到文件末尾而结束的
ferror判断是不是遇到错误后读取结束

1.2 C语言中的写操作

  • 下面的例子使用文件操作创建一个文件并写入相应的文本
  • 每次写入一行,使用\n隔开
void test1()
{FILE *fp = fopen("./log.txt", "w");if (fp == NULL){perror("fpen");return;}for(int i =0;i<10;++i){fputs("hello linux!\n",fp);}fclose(fp);
}

在这里插入图片描述

1.3 C语言中的读操作

  • 下面的例子用于从文件中读取数据
  • 每次读取一行,如果读到空则返回NULL
void test2()
{FILE *fp = fopen("./log.txt", "r");if (fp == NULL){perror("fpen");return;}char buffer[1024];int len = 0;while(len = fgets(buffer,sizeof(buffer),fp) != NULL){std::cout << buffer;}fclose(fp);
}

在这里插入图片描述

1.4 C程序默认打开的三个输入输出流

我们都知道C程序会打开三个默认输入输出流:

extern FILE *stdin;   //标准输入 --- 所对应的是键盘
extern FILE *stdout;  //标准输出 --- 所对应的是显示器
extern FILE *stderr;  //标准错误 --- 所对应的是显示器

我们刚刚在使用fputs函数向文件写入数据,而文件的类型和这里默认打卡的三个流的类型是一样的,那么我们也可以直接向显示器写入数据:

void test3(){const char* message = "hello linux!\n";fputs(message,stdout);
}

在这里插入图片描述

fputs能够像一般文件或者是硬件写入数据,我们就可以理解为一切皆文件;

二、系统文件IO

  • 我们无论是写文件还是读文件,文件都是来源于硬件(磁盘…),硬件是由操作系统管理的;
  • 用户不能够直接跳过操作系统将文件写入,必须贯穿整个操作系统。
  • 那么访问操作系统就需要调用系统接口来实现文件向硬件写入的操作;
  • 也就是你在C语言或其他语言上使用的文件相关库函数,其底层都是要调用系统接口的;

如下图所示:

在这里插入图片描述

2.1 open

2.2.1 open的参数

参数:

  1. pathname:你要打开的文件路径,如果没有会自动创建

  2. flags:以什么样的形式打开:

    • O_RDONLY — 以只读的方式打开文件
    • O_WRNOLY — 以只写的方式打开文件
    • O_APPEND — 以追加的方式打开文件
    • O_RDWR — 以读写的方式打开文件
    • O_CREAT — 当目标文件不存在时,创建文件
  3. mode:表示创建文件时的权限设置

下面的例子创建并打开了一个文件,可以发现运行后查找当前目录存在创建好的文件

void test4(){int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);if(fd < 0){perror("open");}close(fd);
}

在这里插入图片描述

  • 在上述代码中的第二个参数需要解释一下,我们刚刚已经了解了第二个参数是以什么样的形式打开文件,它是操作系统在用户层面上给内核层传递的标志位

  • flags的类型是int,他就有32个比特位,一个比特位就可以代表一个标志位,如果两个或起来就可以传递多个标志位。操作系统内部在进行按位与运算,判断那个位被设置了1或0,从而对文件打开方式进行设置

在这里插入图片描述

实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:

在这里插入图片描述

2.1.2 pen的返回值

open的返回值是fd——文件描述符 (文件打开成功,返回对应的fd值,打开失败,返回的是-1)

下面的代码依次创建并打开不同的文件,然后我们输出返回的文件描述符观察

void test5(){int fd1 = open("log_test5_1.txt",O_WRONLY | O_CREAT,0644);int fd2 = open("log_test5_2.txt",O_WRONLY | O_CREAT,0644);int fd3 = open("log_test5_3.txt",O_WRONLY | O_CREAT,0644);int fd4 = open("log_test5_4.txt",O_WRONLY | O_CREAT,0644);std::cout << "fd1 = " << fd1 << std::endl;std::cout << "fd2 = " << fd2 << std::endl;std::cout << "fd3 = " << fd3 << std::endl;std::cout << "fd4 = " << fd4 << std::endl;close(fd1);close(fd2);close(fd3);close(fd4);
}
  • 从下图的运行结果发现,文件描述符从3开始,依次递增,有点像数组的下标;
  • 那么0/1/2是什么呢?这里没有从0开始,其实,是将0/1/2分配给了三个流:0—标准输入、1—标准输出、2—标准错误

在这里插入图片描述

2.2 close

关闭文件描述符:

  • 关闭文件只需要传入你想要关闭的文件描述符即可;
int close(int fd);

2.3 write

向文件描述符写入数据:

  • 参数解读:将buf中的数据写入fd,写入count个
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

下面的代码用于创建一个文件,并且向文件写入内容

void test6(){int fd = open("log_test6.txt", O_WRONLY | O_CREAT, 0644);if (fd < 0){perror("open");}const char* message = "hello linux\n";for(int i =1;i<=5;++i){write(fd, message, strlen(message));}close(fd);
}

查看创建的文件,验证内容是否写入

在这里插入图片描述

2.4 read

从文件描述符读数据

ssize_t read(int fd, void *buf, size_t count);

下面的代码用于从已创建好的文件中读取数据,读取255个字符,最后一个字符存储\0

void test7(){int fd = open("./log_test6.txt",O_RDONLY);if(fd < 0){perror("open");return;}char buffer[256];ssize_t len = read(fd,buffer,sizeof(buffer)-1);if(len > 0){buffer[len] = '\0';}std::cout << buffer;close(fd);}

在这里插入图片描述

三、文件描述符

一个进程可以打开多个文件,当操作系统中存在大量的文件时,操作系统就需要对这些文件进行管理,在内核当中,管理这些已经打开的文件就要设计对应的结构体,把打开的文件描述起来,我们把描述文件的结构体称为(struct file),然后将这些结构体以双链表的形式链接起来,便于管理。

多个进程和多个文件在操作系统中,是如何区分哪一个文件属于哪一个进程的呢?

  • 操作系统为了能够让进程和文件之间产生关系,进程在内核当中包含了一个结构 struct files_struct,这个结构中又包含了一个数组结构struct file* fd_array[ ]task_structPCB当中又包含了一个指针 struct files_struct* fs,用来管理这个struct files_struct。我们把对应的描述文件结构的地址写到特定的下标里。

  • 所以为什么我们在之前打印文件描述符fd时是从3开始的,是因为前面3个地址留给了三个流,当有新的文件打开时,首先是形成struct files结构体,然后将地址写入下标3的位置。然后返回给上层用户,我们就拿到了3这个下标了。

在这里插入图片描述

当我们在使用write和read时,都需要传入fd,本质上就是去进程的PCB中找到fd所对应的文件,就可以对文件进行操作了;

结论:fd本质是内核中进程和文件相关联的数组下标

四、一切皆文件

  • 对于我们的外设(IO设备),在驱动层一定对应了相应的驱动程序,包括他们各自的读写方法;他们的读写方法是不一样的;

  • 在操作系统层面上,对于底层的键盘、显示器、磁盘等外设,需要打开时,操作系统就会给这些外设创建一个struct file的结构体进行维护,这些结构体就包含了相关外设的属性信息,再将它们用双链表管理起来。再与上层的进程结合起来既可以执行对对应的操作了。这里就是所谓的虚拟文件系统(VFS)

  • 我们在C++的学习中,对多态的概念有所了解,就是多个子类继承了相同的父类,每个子类的方法都是不一样的,只要父类的指针或引用调用对应的子类,就去实现对应子类的方法。在C语言中,想要实现多态,我们的方法是通过函数指针;

  • 在这个struct file的结构中,就包含了读写方法的函数指针,对应到了每个外设;在上层看来,所有的文件只要调用对应外设的读写方法即可,根本不关心你到底是什么文件。

本质上,所谓的一切皆文件,就是站在struct file的层面上看待的。

在这里插入图片描述

五、文件描述符的分配规则

我们先来看一下连续打开4个文件,所对应的fd都是什么?

void test5(){int fd1 = open("log_test5_1.txt",O_WRONLY | O_CREAT,0644);int fd2 = open("log_test5_2.txt",O_WRONLY | O_CREAT,0644);int fd3 = open("log_test5_3.txt",O_WRONLY | O_CREAT,0644);int fd4 = open("log_test5_4.txt",O_WRONLY | O_CREAT,0644);std::cout << "fd1 = " << fd1 << std::endl;std::cout << "fd2 = " << fd2 << std::endl;std::cout << "fd3 = " << fd3 << std::endl;std::cout << "fd4 = " << fd4 << std::endl;close(fd1);close(fd2);close(fd3);close(fd4);
}

从运行结果看,它是从3开始向上增长的,因为0/1/2被三个流所占用。

在这里插入图片描述

如果我们将0关闭,会是什么样的呢? 在原来代码基础上加上 close(0);

void test5(){close(0);int fd1 = open("log_test5_1.txt",O_WRONLY | O_CREAT,0644);int fd2 = open("log_test5_2.txt",O_WRONLY | O_CREAT,0644);int fd3 = open("log_test5_3.txt",O_WRONLY | O_CREAT,0644);int fd4 = open("log_test5_4.txt",O_WRONLY | O_CREAT,0644);std::cout << "fd1 = " << fd1 << std::endl;std::cout << "fd2 = " << fd2 << std::endl;std::cout << "fd3 = " << fd3 << std::endl;std::cout << "fd4 = " << fd4 << std::endl;close(fd1);close(fd2);close(fd3);close(fd4);
}

在这里插入图片描述

我们再将2关闭 close(2);

void test5(){close(0);close(2);int fd1 = open("log_test5_1.txt",O_WRONLY | O_CREAT,0644);int fd2 = open("log_test5_2.txt",O_WRONLY | O_CREAT,0644);int fd3 = open("log_test5_3.txt",O_WRONLY | O_CREAT,0644);int fd4 = open("log_test5_4.txt",O_WRONLY | O_CREAT,0644);std::cout << "fd1 = " << fd1 << std::endl;std::cout << "fd2 = " << fd2 << std::endl;std::cout << "fd3 = " << fd3 << std::endl;std::cout << "fd4 = " << fd4 << std::endl;close(fd1);close(fd2);close(fd3);close(fd4);
}

在这里插入图片描述

给新文件分配fd,是从fd_array数组中找一个最小的,没有被使用过的,作为新的fd。

六、重定向

本来应该写到显示器的数据,去写到了文件中,我们把这种叫做重定向;

6.1 输出重定向

我们先看一下面的代码,我们本意是想将hello linux!打印到显示器上:

void test8(){close(1);int fd = open("./log.txt",O_CREAT | O_WRONLY,0644);if(fd < 0){perror("open");return;}std::cout << "fd = " << fd << std::endl;std::cout << "hello linux" << std::endl;std::cout << "hello linux" << std::endl;std::cout << "hello linux" << std::endl;}

我们只是加了一个close(1);这段代码,为什么就打印到文件中了呢?

在这里插入图片描述

  • printf函数本质是向stdout输出数据的,而stdout是一个struct FILE*类型的指针,FILE是C语言层面上的结构体,该结构体当中有一个存储文件描述符fd,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。

  • C语言的数据并不是立马写到操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

在这里插入图片描述

6.2 追加重定向

追加重定向和输出重定向的本质区别在于,前者不会覆盖原来的数据内容

void test9(){close(1);int fd = open("./log.txt",O_CREAT | O_WRONLY | O_APPEND,0644);if(fd < 0){perror("open");return;}std::cout << "fd = " << fd << std::endl;std::cout << "hello linux123" << std::endl;std::cout << "hello linux123" << std::endl;std::cout << "hello linux123" << std::endl;
}

可以发现在原本文件的结尾追加写入了内容

在这里插入图片描述

6.3 输入重定向

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据

void test10(){close(0);int fd = open("./log.txt",O_RDONLY);char buffer[256];while(std::cin >> buffer){std::cout << buffer << std::endl;}close(fd);
}

可以发现,直接从文件输入内容了

在这里插入图片描述

在这里插入图片描述

6.4 stdout和stderr有什么区别

标准输出流和标准错误流对应的都是显示器,它们有什么区别呢?

下面的代码用于将内容输出到stdoutstderr,观察结果

void test11(){fprintf(stdout,"hello stdout!\n");fprintf(stderr,"hello stderr!\n");
}
  • 我们同时向标准输出和标准错误中输出数据,都是能够打印到键盘上的;
  • 当我们将其重定向到文件中去时,却发现只有stdout的内容重定向到了文件中。
  • 实际上我们在使用重定向的时候,是把文件描述符1的标准输出流重定向了,而不会对标准错误流重定向。

在这里插入图片描述

七、系统调用dup2

以上的操作我们都是在关闭标注输入和标准输出后完成重定向的,显得很麻烦,如果标准输入和标准输出都被占用(已经打开了),我们如何去完成重定向呢?

  • 要完成重定向我们就可以将fd_array数组中的元素进行拷贝即可;

  • 例如:我们将fd_array[3]中的内容拷贝到fd_array[1]中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt

    在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。本质上dup2就是将进程中文件描述表中的需要重定向的内容进行相关的拷贝,dup2的函数原型如下:

#include<unistd.h>
int dup2(int oldfd, int newfd);
  • 函数功能:dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]中;
  • 函数返回值:调用成功返回0,失败返回-1;

使用dup2时,我们需要注意以下两点:

  1. 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
  2. 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd
使用dup2——输出重定向
void test12(){int fd = open("log.txt",O_WRONLY);if(fd < 0){perror("open");return;}dup2(fd,1);std::cout << "hello test12!\n";std::cout << "hello test12!\n";}

运行结果,可以发现输出重定向到了文件中

在这里插入图片描述

使用dup2——输入重定向
void test13(){int fd = open("log.txt",O_RDWR);if(fd < 0){perror("open");return;}dup2(fd,0);char buffer[1024];std::string str;std::cin >> str;std::cout << str << std::endl;
}

运行结果,可以发现输入重定向到了文件中

在这里插入图片描述

八、关于FILE

8.1 C库当中的FILE结构体

  • 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。

我们可以使用gedit 打开 usr/include/stdio.h 的文件查看FILE。

sudo su
gedit usr/include/stdio.h

在这里插入图片描述

typedef struct _IO_FILE FILE; //在/usr/include/stdio.h
struct _IO_FILE {int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags//缓冲区相关/* The following pointers correspond to the C++ streambuf protocol. *//* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr;   /* Current read pointer */char* _IO_read_end;   /* End of get area. */char* _IO_read_base;  /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr;  /* Current put pointer. */char* _IO_write_end;  /* End of put area. */char* _IO_buf_base;   /* Start of reserve area. */char* _IO_buf_end;    /* End of reserve area. *//* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base;  /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno; //封装的文件描述符
#if 0int _blksize;
#elseint _flags2;
#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small.  */#define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];/*  char* _save_gptr;  char* _save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

8.2 FILE结构体中的缓冲区

从FILE的源码当中我们发现了FILE结构体里面封装了fd,也就是里面的_fileno。不难发现我们在里面还看到了缓冲区。这里的缓冲区指的是C语言当中的缓冲区。

8.2.1 初步了解缓冲区

我们先来看一下下面的代码,代码的含义是输出重定向,观察是否关闭文件描述符,对结果有何影响?

不关闭文件描述符,进行输出重定向
void test13(){int fd = open("log.txt",O_RDWR);if(fd < 0){perror("open");return;}dup2(fd,0);char buffer[1024];std::string str;std::cin >> str;std::cout << str << std::endl;
}

在这里插入图片描述

关闭文件描述符,进行输出重定向
void test14(){close(1);int fd = open("log.txt",O_CREAT | O_WRONLY,0644);printf("fd:%d\n",fd);    fprintf(stdout,"hello linux!\n");fprintf(stdout,"hello linux!\n");fprintf(stdout,"hello linux!\n");fprintf(stdout,"hello linux!\n");close(fd);
}

在这里插入图片描述

8.2.2 缓冲区的深入理解

  • 通过上面两次的运行结果发现,在关闭文件描述符后,重定向的操作失败了,其本质原因就是数据是暂存在缓冲区(用户级缓冲区)

  • 在操作系统内部也是存在一个内核缓冲区的用户缓冲区到内核缓冲区的刷新策略有如下几种:

    1. 立即刷新:不缓冲
    2. 行刷新(行缓冲 \n),比如,显示器打印
    3. 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件中写入

在这里插入图片描述

  • 当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)

  • 显示器是行缓冲,即遇到\n就会刷新数据到显示器;磁盘是全缓冲,当缓冲区满了以后,才会刷新数据到磁盘上;

  • 当我们在重定向时,其数据的刷新策略也会发生变化,(上面的代码中)本来我们是行缓冲的,但是重定向后就变成了全缓冲;

  • 两者都是要通过系统调用接口(open)来完成数据的写入; 在没有关闭文件描述符fd时,我们能看到重定向后的结果,本质是进程结束了,刷新了缓冲区;

  • 在关闭文件描述符fd后,既没有向显示器打印,也没有向文件中打印,本质就是,它要通过系统调用接口先将数据暂存到内核缓冲区,待进程结束后,才刷新到相应的外设中,但是fd已经关闭,就不会刷新到内核在刷新到硬件,所以就看不到任何数据;

再来看一下下面这段代码:
void test16(){const char* msg1 = "hello stdout\n";const char* msg2 = "hello stderr\n";write(1,msg1,strlen(msg1));write(2,msg2,strlen(msg2));printf("hello printf !\n");fprintf(stdout,"hello fprintf!\n");close(1);}

运行结果:

在这里插入图片描述

  • 通过上面的运行结果,再结合之前所说,我们这里不是关闭了1吗?为什么还是能够打印出来呢?我们可以看到这里四条输出语句都是向显示器上打印的,并且都有’\n’,表明是行刷新,在关闭1之前就已经刷新到显示器上了。
重定向到文件中

在这里插入图片描述

  • 标准错误不会重定向我们能够理解,但是其他三条语句应该是重定向到文件中呀,而这里运行结果只有一条hello stdout。

  • 这是因为,我们的msg1是直接通过系统调用接口,把数据暂存到内核缓冲区,不会把数据暂存到上层的用户级缓冲区,所以关闭1根本就不会影响这个数据刷新到文件;

  • 但是下面两个语句由于重定向的原因,刷新策略发生了变化(行缓冲->全缓冲),数据暂存到用户级缓冲区后,本来是等待进程结束后刷新到文件中去的,但是这个过程中却把1关闭了,才导致这两条数据并没有被刷新到文件中;

对于以上的理解有了新的认识后,我们再来看一下这段代码:
void test17(){const char* msg = "hello stdout\n";write(1,msg,strlen(msg));printf("hello printf!\n");fprintf(stdout,"hello fprintf!\n");fork();
}

运行结果:

在这里插入图片描述

  • 通过上面的运行结果发现, 当我们直接运行程序时,它向显示器上打印了3条语句,但是我们程序中有创建子进程的语句,当我们重定向后发现文件中多打印了两条语句,而且只是针对C语言的接口,而非系统调用接口;

  • 当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。

  • 而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就由行缓冲变成了全缓冲,此时我们使用printf和fprintf函数打印的数据都暂存到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fprintf函数打印的数据就有两份。

8.2.3 如何解决缓冲区不刷新的问题

关闭文件前,调用fflush手动刷新缓冲区即可

void test18(){close(1);int fd = open("log.txt",O_CREAT | O_WRONLY,0644);printf("fd:%d\n",fd);fprintf(stdout,"hello fprintf!\n");fprintf(stdout,"hello fprintf!\n");fprintf(stdout,"hello fprintf!\n");fprintf(stdout,"hello fprintf!\n");fflush(stdout);close(fd);}

运行结果

在这里插入图片描述

http://www.xdnf.cn/news/13648.html

相关文章:

  • 织梦dedecms内容页调用seotitle标题的写法
  • Python训练营---DAY52
  • day01 ——Java基础入门
  • 135. Candy
  • C# 界面检测显示器移除并在可用显示器上显示
  • 关键领域软件测试新范式:如何在安全合规前提下提升效率?
  • 14.FTP传输分析
  • 云安全【阿里云ECS攻防】
  • 解决office各种疑难杂症
  • HarmonyOS运动开发:深度解析文件预览的正确姿势
  • win11系统部署tomcat10教程
  • 详解docker挂载目录常用方式
  • flutter把 pubspec.yaml 中的name改成了新的值
  • window 显示驱动开发-为视频处理创建渲染目标图面
  • 使用 React+Vite+Electron 搭建桌面应用
  • 【机器学习】Teacher-Student框架
  • 佰力博与你探讨表面电阻测试的一些方法和测试应用场景
  • 前端面试七之列表渲染和组件重用
  • 新加坡金融管理局责令未获许可加密货币公司于6月30日前退出,Bitget、Bybit考虑撤离
  • 写实交互数字人:赋能消防知识科普,点亮智能交互讲解新未来
  • java(JDBC)
  • 3分钟入门深度学习(迷你级小项目): XOR 门神经网络训练与测试简明教程
  • 上海市计算机学会竞赛平台2022年7月月赛丙组因数之和
  • 蘑菇街关键字搜索接口技术实现
  • 边缘计算的突破:从云端到边缘的 IT 新前沿
  • 你管这玩意叫网络?网络图解
  • MySQL 索引学习笔记
  • 第16篇:数据库中间件多租户架构与动态数据源隔离机制
  • 黑马点评【缓存】
  • vue的实用且常用的各种api