【基础IO上】复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux系统下,一切皆文件 | 重定向原理

news2025/4/26 3:21:10

1.关于文件的预备知识

1.1 文件的宏观理解

广义上理解,键盘、显示器等都是文件,因为我们说过“Linux下,一切皆文件”,当然我们现在对于这句话的理解是片面的;狭义上理解,文件在磁盘上,磁盘是一种永久存储介质,不会受断电的影响,磁盘也是外设之一,所以对文件的所有操作,都是对外设的输入输出,简称IO(input、output)。

1.2 文件操作的本质是什么?

针对文件的操作都是对文件内容和属性的操作,语言层面的文件操作就是直接使用库函数。而实际上,文件操作是系统层面的问题,就像进程管理一样,系统也会通过 先描述、再组织 的方式对文件进行管理、操作。

并不是只有C/C++这种偏底层的语言才有文件操作,其他语言也支持文件操作,如Java。在进行文件操作时,不同语言使用的方法可能有所不同,但本质上都是在调用系统级接口进行操作。

1.3 文件的组成

当我们在window下新建一个文本文件,它是否占用磁盘空间?----虽然它是一个空的文本文件,并且这里显示的是0KB,但是它依旧会占用磁盘空间,因为一个文件新建出来,它有很多数据信息都需要维护,包括文件名、修改日期、类型、大小、权限等。

而当我们在空文本中写入字符时,这里可以直观看到文本的大小由0KB到1KB。

所以说文件=属性(元数据)+内容,也就是说我们要学习的所有文件操作,无外乎就是对文件的属性和内容操作。比如说之前所学的fread、fwrite、fgets、fputs、fgetc、fputc是对文件的内容操作;fseek、ftell、rewind是对文件的属性操作。

1.4 文件操作

本文讨论的是已被加载至内存的文件的相关操作,当我们没有打开文件时,文件位于磁盘中,当文件被打开时,文件就要被操作系统加载到内存中。这也是冯·诺伊曼体系结构对文件操作的限制。
我们以前写的fread、fwrite等对文件操作的C程序,经过编译形成可执行程序,双击 或者 ./运行程序,把程序加载到内存中。所以对文件操作的本质就是进程对文件的操作。所以,文件操作的本质就是进程 和 被打开文件 的关系
我们在操作文件时所使用到接口,如fread、fwrite,这是C语言提供的接口,而要操作的文件是在磁盘这个硬件上。同时我们很明确磁盘的管理者是操作系统,用户不可能直接去访问硬件,在计算机体系结构中我们知道用户是通过C语言所提供的这些接口来贯穿式的访问硬件(用户->库函数->系统调用接口->操作系统->驱动程序->硬件)。所以本质上不是C语言帮我们把数据写到磁盘文件中,C语言只提供方便用户使用的接口,真正干活的是操作系统所提供的文件相关的系统调用接口。所以,基础IO系列的文章主要学的是进程和系统调用接口这两个角度看待文件的方式。
调用库函数进行文件操作时的流程:

1.5 系统是如何管理文件的?

实际进程在运行的过程中肯定会打开多个文件,对于多个文件操作系统要将它们组织起来以便于管理和维护,会为被打开的文件创建相应的内核数据结构。
像使用task_struct管理进程一样,通过struct file存储文件属性进程管理。
struct file结构体中包含了文件的各种属性和链接关系。

2.C语言文件操作

2.1 写文件



FILE *fp = fopen("log.txt","w");
fopen函数返回值类型为FILE。参数列表中,path为文件路径,mode为打开方式。若文件打开失败,会返回NULL,可以在打开后判断是否成功。
注意:
1、若参数1直接使用文件名,该文件默认会在当前路径中被创建;
2、仅仅以w方式打开文件,C语言会自动情况内部的数据;
3、普通文件被创建时的默认权限是664。

(注:普通文件的起始权限666,去掉umask(umask为0002)中出现的other的写权限,就变为了110 110 100,故为664  rw-rw-r--。)

虽然没有./指定路径,但是还是在当前路径下新建文件了,因为每个进程都有一个内置的属性cwd(可以在/proc目录下查找对应进程的属性信息),cwd可以让进程知道自己当前所处的路径。这也解释了在VS中不指明路径,它也能新建对应的文件在对应的路径。换言之,进程在哪个路径运行,文件就新建在哪个路径。


fprintf()函数根据指定的format(格式)发送信息(参数)到由stream(流)指定的文件,因此fprintf()可以使得信息输出到指定的文件。比如:

printf是打印输出到屏幕,fprintf是打印输出到文件。fprintf()的返回值是输出的字符数,发生错误时返回一个负值。在有些地方,有这样的定义:printf(...)=fprintf(stdout,...)。

2.2 读文件


由输出结果可知,输出结果一共有两个换行符,这是因为fgets()函数会读取一个换行符放入到数组buffer中;puts函数在打印时,也会在字符串的的结尾添加一个换行符。

2.2.1 fgets()函数


fgets函数可以理解为(char *fgets("容器地址", "容器的大小","从哪里读取"))

一般用法:

char a[100]={0};
fgets(a,100,stdin);

fgets()函数的作用:从第三个参数指定的流中读取最多第二个参数大小的字符到第一个参数指定的容器地址中。在这个过程中,在还没读取够第二个参数指定大小的字符前,读取到换行符'\n'或者需要读取的流中已经没有数据了。则提前结束,并把已经读取到的字符存储进第一个参数指定的容器地址中。
在正常情况下,fgets()函数的返回值和它第一个参数相同。即读取到数据后存储的容器地址,但是如果读取出错或读取文件时文件为空,则返回一个空指针。

fgets()函数的运行流程:当系统调用这个函数时,系统便会阻塞等待用户的输入(第三个参数为stdin标准输入时),直到用户输入回车符'\n'才返回程序。然后用户输入的内容会被系统放进输入缓存区中。fgets()函数便会进来读取其“第二个参数减1”个字节存进它第一个参数指向的内存地址中,如果还没读取够需要的字节大小前就读取到换行符'\n'则提前返回。

fgets()函数的注意事项1:fgets()函数的最大读取大小是其“第二个参数减1”,这是由于字符串是以'\0'为结束符。fgest()为了保证输入内容的字符串格式,当输入的数据大小超过了第二个参数指定的大小时,fgets()会仅仅读取前面的“第二个参数减1”个字符,而预留1个字符的空间来存储字符串结束符'\0'。
fgets()函数的注意事项2:换行符也是fgets()函数要读取的一个普通字符,在读取键盘输入时,会把最后输入的回车符也存进数组中,即会把'\n'也存进数组中。而又由于字符串本身是以'\0'结尾的,所以在输入字符个数没有超过第二个参数指定大小之前,你输入n个字符按下回车输入,fgets()存储进第一个参数指定内存地址的是n+2个字节。最后会多出一个'\n'和一个'\0',而且'\n'是在'\0'的前面(\n\0)。

2.2.2 puts()函数

1、puts函数的原型:int puts(const char* str)它接受一个以null结尾的字符串作为参数,并将其写入标准输出,然后自动添加一个换行符。
2、输出内容:puts函数用于输出字符串。
3、返回值:如果成功则返回非负整数,表示输出字符数(不包括结尾的换行符),如果发生错误则返回EOF。

2.3 追加文件

3.系统文件I/O

3.1 为什么要学习文件系统接口

根据之前所说,在C语言中要访问硬件,必须贯穿计算机体系结构,而fopen、fclose等系列的库函数,其底层都要调用系统接口,这里他们对应的系统接口也很好记忆--去掉"f"即为系统接口。不同语言所提供的接口本质是系统接口的封装,学习封装的本质学的就是语言级别的接口。换言之,要学习不同的语言,就得学习不同语言操作文件的方法,但是实际上对特定的操作系统,最终都会调用系统提供的接口。

所以接下来我们当然是要学习系统接口,我们要学习系统接口的原因主要有两点:其一,只要把系统调用接口学懂了,以后学习其他语言的文件操作,只是学习它的语言,底层就不用学习了;其二,这些系统接口更加贴近于系统,所以我们就能通过接口,来学习文件的若干特性。

3.2 open

我们打开man手册在2号目录里面可以查看到open的函数原型以及介绍:

参数1:pathname待操作文件名,与fopen一样,文件路径可以是相对路径,也可以是绝对路径;
参数2:flags为打开选项,open以位图的形式传递选项信号,用一个int类型至多可以表示32个选项;
参数3:mode权限设置,文件起始权限为0666;
返回值:如果打开成功会返回一个大于或等于0的整数,这个整数叫做文件描述符,用来标定一个文件,如果打开失败会返回-1表示打开失败,下面会详细解释。

参数2解释:使用了位图的方式进行多参数传递。

可以利用这个特性,写一个关于位图的小demo:

函数open中的参数2正是位图,其参数有很多个,这里列举部分:
O_RDONLY 只读打开                                                (英文read only的缩写)
O_WRONLY只写打开                                                (英文write only的缩写)
O_RDWR 读写
O_CREAT 文件不存在就创建该文件                           (英文creat)
O_TRUNC 文件每次打开都会进行清空                       (英文truncate)
O_APPEND 文件写入时以追加的方式进行写入          (英文append)

由上文可知,使用open需要包含三个头文件,它有两个版本。版本1:以flags方式打开pathname;版本2:以flags方式打开pathname,并设置mode权限。

flags可以是O_RONLY、O_WRONLY,且必须包含以上访问模式之一。此外,访问模式还可以带上 | 标志位,下面会介绍一两个标志位,实际还要看场景使用。

3.2.1 open("log.txt",O_WRONLY);

以写的方式打开一个存在的文件,与fopen一样,如果没有写操作,原文件的内容不会被覆盖;如果有写操作,原文件的内容会被覆盖成写的内容;不同的是,若文件不存在,则不会新建文件。

3.2.2 open("log.txt",O_WRONLY,0655);

若以写的方式打开不存在的文件,权限是655,程序当然不会新建文件,若文件存在,也不会修改文件的默认权限,下面会讲。

我们发现open的返回值不是FILE*,而是int。open函数执行成功后,会返回一个新的文件描述符(file descriptor),如果打开失败,返回-1。在C语言中,我们把FILE*称为文件指针,FILE*和file descriptor必然有联系,下面再谈。


这里不存在log.txt文件,我们发现返回值是-1。


若文件存在,则返回文件描述符3,且log.txt文件的权限没有被修改。


O_CREAT后发现文件不存在,将会新建文件,且必须指定mode权限(如果没有指定,那么新建的文件会变成可执行程序),通常如果没有写O_CREATE,说明文件是存在的,则可忽略mode权限(因为就算写了权限,也不会更改原来的权限)。

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


很明显,open和fopen不一样,fopen如果以写的方式打开,文件不存在,则会新建文件。而open想做到类似的效果,需要带上O_CREAT标志位,且必须指定mode权限。这里我们运行程序,发现文件描述符是3,说明文件打开成功,而且权限设置成了655。

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

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
 
#define FILE_NAME "log.txt"
 
int main()
{
  // 三种参数组合,就构成了 fopen 中的 w
  int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC);
  
  // 成功与否打印信息
  if(fd == -1)       
  {
    printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));
  }
  else
  {
    printf("fd: %d, error: %d, errstring: %s\n", fd, errno, strerror(errno));
  }
  close(fd);
 
  return 0;
}

O_CREAT,如果文件不存在,则自动创建该文件。O_WRONLY为以只读模式打开文件。需要注意的是,他们不会对原始文件内容做清空,下一次写入时,虽然还是从开头开始写,但是原本的内容没被覆盖的部分仍然会残留。

打开文件所返回的文件描述符是3,文件描述符下面会做详细的解释。

参数3解释:先看上述代码的执行结果。

为什么每次创建文件的权限都是随机的呢?
因为我们使用的第一个open函数是没有指明创建后的文件权限是什么,所以创建出的文件权限是一个随机值,这时我们就要考虑第二个open函数了。

3.2.5 open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);

第三个参数:mode_t是一种无符号整型,我们的第三个参数可以以8进制的方式传入我们创建的文件的权限。

将第3个参数加上,因为创建的是普通文件,所以就给它的默认权限是0666。


可以看到,此时创建的文件就正常了,但是权限并不是我们设定的0666,而是0664,这是因为有默认权限掩码(umask)的影响。


我们也可以使用系统调用函数umask,在程序中修改umask的值。



将默认权限掩码修改成0之后,再创建的文件的权限就是我们设置的0666了。

3.3 close函数


将打开文件时返回的int类型的fd值传递给close系统调用后,这个被打开的文件就被关闭了。关闭成功返回0,如果关闭失败就返回-1。

3.4 write函数


参数解释:
int fd:打开文件时返回的int类型整数(文件描述符);
const void* buf:要写入的数组地址。对于系统调用来说,它并不在意写入的数据是什么类型的,它接收到的数据都是二进制的数字,然后按照字节为单位写入。
size_t count:该参数是一个变量,表示最多写入多少个字节的数据。
返回值:实际写入的字节个数。

清空的方式写入:



strlen(outBuffer)+1时,文件中有乱码。


此时,文件中没有乱码。可以看到成功将字符串写入到了log.txt文件中。

追加的方式写入:


注意:通过系统级函数write写入字符串时,不要刻意加上'\0'。因为对于系统来说,调用写入时要的只是数据,但不包括'\0',这也只是一个普通的字符(因为'\0'作为字符串结尾只是C语言的规定,但不是系统的规定。)

3.5 read函数


参数解释:
int fd:打开文件时,返回的文件描述符;
void* buf:从文件中读取的数据放在这个数组中,同样系统不管文件中的数据类型是什么,都是按字节放入这个数组中;
size_t count:要读取的字节个数;
返回值:实际读到的数据。


使用系统接口来进行IO时,一定要注意'\0'的问题。
read函数会读取换行符\n,并将其作为普通字符处理,只有在读取到指定的字节数或者遇到文件的结束才会停止读取。


C语言的文件操作函数,封装了对应的系统调用接口函数。所以说,无论什么语言,文件操作相关的函数都是对系统调用的封装。

文件操作的本质:进程 和 被打开文件 的关系。

4.文件描述符

在使用系统调用open函数时,返回的整数就是文件描述符。

执行结果:

现在,我们看到了文件描述符,就是几个数字。

①前文已经讲过,文件操作的本质就是进程和被打开文件的关系。
②系统中会存在大量被打开的文件,而操作系统同样会管理这些被打开的文件。
③操作系统管理文件的方式与管理进程类似,也是采用先描述、再组织的方式。

当一个文件被打开后,操作系统会创建一个对应的结构体对象,类型是struct file。每打开一个文件,操作系统就会创建这样的一个结构体对象来对该文件进行描述;将多个这样的结构体对象采用一定的方式组织起来,比如链表的方式,以方便操作系统管理这些被打开的文件。

struct file
{
    //文件大小
	//文件类型
	......
	//文件的各种属性
}

在描述进程的结构体task_struct中,有一个指针,struct files_struct *files,这个指针指向一个结构体对象,该对象类型如下:

struct files_struct
{
	//......
	struct file* array[];
}

①struct files_struct结构体中存在一个指针数组array,该数组中的指针指向的是一个个的struct file类型的结构体对象。
②换言之,该数组中存放的是被打开文件结构体对象的地址。
③每一个被指向的struct file结构体对象都描述着一个被打开的文件。

从上述代码可以看到,打印出来的fd值是连续的整数,这些整数就是struct files_struct结构体中指针数组struct file *array[]的下标。文件描述符的本质就是数组的下标,如下图所示:

①当一个程序被加载到内存中时,操作系统会创建一个结构体struct task_struct对象,在该结构体中有一个指针struct files_struct *files,指向一个struct files_struct结构体对象。
②struct files_struct也被叫做进程的文件描述符表,该结构体中有一个数组struct file *array[],数组中存放的是被打开文件的结构体对象的地址。如上图,下标为3,也就是fd=3时,访问到的是struct file *array[3]。
③通过数组访问到的地址,可以找到对应打开文件的结构体对象,如上图中的log.txt。

只有被打开的文件才会在内存中创建struct file结构体对象,没有被打开的文件就静静的躺在磁盘中。不是该进程打开的文件,该进程执行的文件描述符表中则没有这个文件的地址。

4.1 文件描述符fd=0/1/2

从上面的代码,可以看出文件的fd值是从3开始的,那么fd=0/1/2是什么呢?
C语言默认会打开三个输入输出流,分别是stdin、stdout、stderr。可以看到,这三个流是FILE*类型的指针,暂时不用管FILE是什么,只需直到它是一个结构体。



此时文件描述符0 1 2都出现了:
fd=0:标准输入流(stdin);
fd=1:标准输出流(stdout);
fd=2:标准错误(stderr)。

此时,我们便清楚了为什么我们打开的文件的文件描述符是从3开始的,因为0 1 2被默认打开的三个流占用了。0下标指向键盘、1下标指向显示器、2下标指向对应的显示器,这叫做进程默认创建时为我们打开的标准输入输出流,分别叫做stdin、stdout、stderr。当再打开一个文件时,我们从上到下寻找,没有被占用的文件描述符就是从3开始的。将文件从磁盘中加载到内存中,构建struct file结构体,并将struct file对象的地址填写到对应的3号文件描述符中。此时,3号文件描述符就指向新打开的文件了,然后再将3号文件描述符通过系统调用返回给用户,用户就得到了数字3。当一个进程在访问该新文件时,需要传入3,通过系统调用函数,操作系统找到进程的文件描述符表,根据文件描述符表中的地址找到对应的文件,就可以对文件进行操作了。
文件描述符为什么是0、1、2这样的数字呢?因为文件描述符的本质就是数组下标。

每个进程的文件描述符表如上图所示。

4.2 文件描述符fd的分配规则

由上文可知,我们打开的文件fd是从3开始的,为什么不是从5或者6开始呢?


此时,新创建的文件log.txt的文件描述符为3。


我们将fd=0的标准输入流关闭,再打开文件log.txt,发现文件描述符为0。


我们将fd=2的标准错误流关闭,再打开文件log.txt,发现文件描述符为2。

根据这个现象,可以得出结论:文件描述符fd的分配规则是:从小到大,按顺序查找,将没有被占用的数组下标作为被打开文件的文件描述符fd值。如:由于默认0、1、2被占用了,所以默认新创建的文件打开时,文件描述符是3。把0关掉,此时最小且没有被占用的文件描述符就是0;把2关掉,此时最小且没有被占用的文件描述符就是2。这就是文件描述符的分配规则。

5.重定向

5.1 重定向简单介绍

上小节中,我们只关闭过0和2,没有关闭过1,下面的代码中关闭一下1来看看。

按照文件描述符fd的分配规则可知,上述代码中的fd的值应该为1。但是根据执行结果可知,fd的值并没有打印出来。为什么?如下图所示。

代码中关闭了文件描述符1,即当前进程对应的1号文件描述符(显示器)被关掉了。执行open函数,首先是打开文件,此时从小到大,按照顺序寻找最小的且没有被占用的文件描述符fd,将myfile的地址填到1号下标里,1号下标指向新打开的文件myfile。

但是fd的值1,在屏幕中为什么打印不出来呢?
原因:我们想标准输出显示器上打印时,默认是向stdout中打印,而stdout里对应的文件描述符fd是1。在关闭1之前,1号下标指向的是标准输出(显示器);关闭1之后,1号下标不再指向标准输出,而是指向新打开的文件myfile,继续向1号下标打印时,数据实际上是被写到myfile文件中去了。


此时,新打开的文件log.txt中没有文件描述符1,这与缓冲区有关,没有被显示出来。将数据强制刷新一下,fflush(stdout)。如下图所示,此时,log.txt文件中的内容,是原本应该打印在屏幕上的内容。

同样的,将1关闭之后,以追加的方式打开一个文件,并且写入多行内容。同样没有打印到屏幕上,而是打印到了新打开的文件中。


重定向的特征,上层调用不变的情况下,改变底层的输出方向。比如,上层调用fwrite(stdout,...);此时一定使用的是stdin、stdout、stderr。如上图,假如将数组fd_array的3号下标中的地址写到了1号下标中,此时1号下表就从指向标准输出变为指向了新打开的文件myfile。同理,将3号下表中的地址写入到0号下标中,那么0号下标就指向了新打开的文件myfile。
重定向的本质:上层使用的fd不变,在内核中更改对应的struct file*的地址。

5.2 重定向系统调用

上一章节介绍的重定向的实现总感觉有点怪,需要先进行关闭操作,然后再打开新文件,很不方便。操作系统提供了一个系统调用,可以直接实现重定向。

我们常用的函数是dup2,可以在内核中进行文件描述符拷贝。dup2的作用是在两个文件描述符之间进行拷贝,不是将0拷贝成3,或者将3拷贝成0,而是将数组下标0中的内容拷贝到数组下标3中。即拷贝的是调用当前dup2函数的进程对应的0、1、2这样的文件描述符下标里面的内容。

参数解释


dup2() makes newfd be the copy of oldfd. 即newfd是oldfd的一份拷贝,所以调用完dup2,则newfd和oldfd一样,即最后都是oldfd。如下图所示。

oldfd是我们新打开文件的fd,newfd是标准输出到fd,也就是1。
我们使用dup2函数实现使用close方法实现的效果,即将显示器显示的内容写入到log.txt文件中。
显示器显示的内容:1
写入log.txt:fd

此时,将1号文件描述符不再指向标准输出,而是指向新打开的文件myfile。就是将3号文件描述符的指针内容拷贝到1号文件描述符下标的位置中。此时1号、3号文件描述符指向同一个内容,且以3号文件描述符的内容为准。
由上文可知,newfd是oldfd的一份拷贝,所以调用完dup2,则newfd和oldfd一样,即最后都是oldfd。dup2(fd,1),所以这里的oldfd传递的是3号文件描述符fd,newfd传递的是1号文件描述符。将本来应该显示到显示器的内容即1号文件描述符指向的内容,直接写入到fd指向的文件中,这就叫做重定向。


原本应该输出到显示器的内容,输出到了文件log.txt中。我们上文中一直演示的都是本应该输出到显示器的内容重定向输出到了文件中,这种从显示器到文件的重定向叫做输出重定向

在shell中有命令可以直接实现输出重定向:

ll命令原本是将文件,包括文件属性显示到屏幕上,使用大于号>输出重定向到了log.txt文件中,如上图中的蓝色框所示。


上述代码,是从标准输入stdin键盘中读取数据并存放到数组line中。

此时,我不想从标准输入stdin中读取数据了,我想从某个文件中读取数据。我们首先打开要读取数据的文件log.txt,并进行输入重定向,输入重定向的前提条件是这个文件必须存在。如以下代码所示。

文件log.txt中的内容如上图中的红色框中所示;将原本struct file* array[]数组中下标0中的内容修改为下标为fd的内容,也就是dup2(fd,0)的作用;屏幕上打印读取到的内容。
运行时,数组line中的数据不是从标准输入stdin键盘中输入的,而是fgets函数从文件log.txt中读取并输入的。
这种从文件中输入、而不是从键盘中输入内容的重定向叫做输入重定向

shell中同样有输入重定向的命令:是小于号<。

使用追加的方式打开文件,并且采用输出重定向的方式输出内容。

在原文件内容的基础上追加内容,这种以追加的方式打开文件,并且采用输出重定向的方式称为追加重定向。追加重定向和dup2函数没有关系,只与打开文件的方式有关。

shell中同样有追加重定向:

使用双大于号>>,实现了追加重定向,在原本log.txt内容的基础上追加内容。

5.3 进程独立性

6.Linux下一切皆文件

学习果Linux的同学都听过一句话:Linux下一切皆文件。这一点对于文件操作非常重要,下面我们就来谈谈对于这句话的理解。

我们知道像:键盘、显示器、网卡 ... 这些都是硬件,根本不是文件,那么我们为什么把它们当作文件去看待呢?

首先,我们是通过驱动程序来对硬件进行操作,而对于不同的硬件它们的驱动程序肯定是不同的。例如,键盘只能被读取而不能被写入;显示器只能被写入,而不能被读取;网卡既可以被读取又可以被写入...

尽管它们的驱动是不同的,但是对于每一个硬件设备,我们可以设计一个类,对该硬件的属性和方法进行封装。封装完之后,上面的调用者就看不到底层的内容了,上面的调用者只需要对这个类进行操作,就可以达到自己想要的效果。

操作系统使用struct file结构体封装硬件的属性和驱动的函数指针,上层调用时,只需要调用函数指针,并通过这些函数指针调用具体设备中的驱动程序中相应的函数,就可以直接使用相应的硬件设备了。比如,当打开键盘时,给键盘创建一个struct file对象,并让readp指针指向键盘对应的都方法,writep指针指向键盘对应的写方法。对于不同的硬件若不需要读或写的操作方法,则只需要将对应的函数指针置空就行。这样进程在使用键盘、显示器时就像是在使用文件一样,则无论是是普通文件还是硬件,对于进程来说都是文件。

用户是通过进程的方式来使用操作系统的,所以用户的视角和进程的视角是一样的,从用户的视角来看想要让磁盘帮我们保存一些数据,我们只需要保存一下文件就行,而没有必要将磁盘拿出来,然后通过一些物理手段向磁盘中刻入数据,进程也是和我们用户一样,它也认为只要保存一下文件就能完成任务了,所以对于用户和进程来说一切都是文件,我们也只能接触到文件,所以Linux下一切皆文件。

struct file是在操作系统层虚拟出来的文件对象,一般在Linux中将这层叫做虚拟文件系统vfs,通过这层虚拟的文件系统就可以摒弃掉底层设备之间的差别,而统一使用文件接口的方式来进行所有的文件操作。

每一个硬件,操作系统都会维护一个struct file类型的结构体,硬件的各种信息都在这个结构体中,并且还有对应的读写函数指针(对硬件的操作主要就是读写)。

每个硬件的具体读写函数的实现都在驱动层中,使用到相应的硬件时,操作系统会通过维护的结构体中的函数指针调用相应的读写函数。

站在操作系统的角度来看下层,无论驱动层和硬件层中有什么,在它看来都是struct file结构体,都是通过维护这个结构体来控制各种硬件。
站在操作系统的角度来看上层,无论用户层以及系统调用有什么,在它看来都是一个个的进程,都是一个个的task_struct结构体,都是通过维护这个结构体来调度各个进程的。

真正的文件在操作系统中的体现也是结构体,操作系统维护的同样是被打开文件的结构体而不是文件本身。

一且皆文件是指:在操作系统中一切都是结构体

7.缓冲区

7.1 缓冲区存在的意义

当一个进程要向文件中写入数据时,有两种方案:
1、进程直接将数据写入到文件中,如上图中蓝色箭头所示;
2、进程将数据写入到缓冲区中,然后再由缓冲区将数据写入到文件中,如上图黑色箭头所示。

这里两种方式哪种好呢?看起来第一种方案好,因为比较简单,数据直接从进程流向文件就行,但事实上不是这样。

第一种方案中,无论是在向磁盘上的文件写入数据,还是向显示器等其他硬件写入数据,都需要很长的时间,因为硬件的访问相对于CPU的速度来说是非常慢的,此时CPU就需要进行等待。
第二种方案中,将数据写入到缓冲区中,缓冲区的访问速度肯定要比访问硬件快的多,数据写入到缓冲区以后,CPU就可以去干其他事情了,而缓冲区中的数据会由操作系统在合适的时间写入到文件中。

从上面的分析可以得出结论,缓冲区的存在是为了发送方节省时间

既然缓冲区的存在是为了给CPU节省时间,那么它的访问速度肯定是比文件要快的多的,所以它只能是内存。所以说,缓冲区本质上就是一段内存

7.2 缓冲区的位置

既然缓冲区是一段内存,那么这段内存是谁申请的?它是属于谁的?
来看一个现象:

如上图所示的代码,使用C语言提供的打印函数和系统调用接口,发现各个接口只调用了一次。

在程序执行完毕,但是进程没有结束时,使用fork函数创建子进程,再将运行结果输出重定向到log.txt文件中,发现C语言提供的接口调用了两次,而系统调用接口只调用了一次。

这是什么原因造成的?从这个现象中能得到什么结论呢?
1、这个现象一定和缓冲区有关;
2、缓冲区一定不在操作系统内核中,如果在操作系统内核中,write应该也打印两次。

既然缓冲区不在操作系统内核中,也就是说缓冲区不是由操作系统来维护的,那么它只能由进程去维护,也就是编程语言本身来维护。
拿C语言来说,和文件相关的操作,FILE*类型的指针是至关重要的。我们已经知道,FILE是一个结构体,它里面有文件描述符fd,在结构体中定义的变量名是_fileno。所以我们大胆猜测,所谓缓冲区就在FILE这个结构体中。
实际上,我们之前谈论的所有缓冲区,都指的是用户级语言层面给我们提供的、在FILE结构体里面包含的缓冲区。数据的格式化、打印等操作最终都是写到了FILE结构体对应的缓冲区中,所有我们自己要强制刷新,刷新使用的是fflush(文件指针)函数,关闭文件时调用的是fclose(文件指针)函数。我们在fflush、fclose函数形参中传入的文件指针里面包含的是内部的缓冲区。
FILE*fp=fopen(FILE_NAME,"w"); fprintf(fp,"%s:%d\n","hello"); 以上操作会将数据写入到fp所指向的FILE结构体内部的缓冲区里,FILE结构体里封装了fd,所以C语言会自动的在合适的时候,将我们的数据刷新到外设里。

来大概看看Linux的源码:

在源码中,与文件有关的结构体中有很多的指针变量,如上图中红色框所示,这些指针就是在维护缓冲区。
此时我们就可以知道,缓冲区是由要打开文件的进程申请的,也是这个进程来维护的,缓冲区存在于FILE结构体中

7.3 缓冲区的刷新策略

只知道缓冲区存在于FILE结构体中还不足与回答上面那个现象提出的问题,接着再介绍一下缓冲区的刷新策略。


同样的代码,但是没有进行输出重定向,而是直接打印,虽然有fork,但是各个接口只调用了一次。


将上面代码中的字符串的换行符\n去掉,而且不进行重定向,直接打印,发现C接口调用函数也被调用了两次。

这是什么原因?从这个现象中又可以看出什么呢?
1、缓冲区如果及时刷新,那么各个接口只调用一次;
2、缓冲区的刷新和换行符\n有关。

这种缓冲区的刷新和换行符\n有关的策略叫做行缓冲


再看,同样的代码,都是有换行符的,进行输出重定向以后,C接口就调用了两次,没有进行重定向的C接口就只调用了一次。
这又是为什么?从这个现象中可以看出什么?
1、输出重定向后,输出终端变成了文件,没有重定向时,输出终端是显示器;
2、行缓冲的策略在文件和显示器上的作用效果不同。

文件采用的是全缓冲的方式,只有当缓冲区满了以后,操作系统才会刷新缓冲区。


在C语言的打印函数接口调用完之后,使用了fflush将缓冲区立刻刷新,然后进行输出重定向到log.txt文件中,此时C语言接口也是只打印了一次。
进行了重定向,又仅打印了一次,和上面进行重定向后只打印一次的结果完全不同。
这是为什么?从这个现象中又可以看出什么?
1、fflush进行缓冲区的刷新;
2、没有遵守行缓冲或者全缓冲的策略。

使用fflush对缓冲区进行刷新,是由用户控制的,直接将缓冲区中的全部内容刷新到对应的终端上去。

还有两种情况,缓冲区也会刷新:
1、其一就是当一个进程结束之后,操作系统会自动将属于该进程的缓冲区进行刷新,并且将对应的内存空间释放。
2、其二就是当一个文件被关闭时,操作系统也会自动将属于该进程的缓冲区进行刷新。

总结一下缓冲区的刷新策略:

不同的缓冲区刷新策略是根据一定的情况定死的,我们一般情况下是不会进行重新定义的。
显示器:直接给用户看的,一方面要照顾到效率,另一方面要考虑到用户是一行一行看文本的,所以用行缓冲策略。
磁盘文件:用户不需要立刻看见文件中的内容,为了效率,采用全缓冲的方式。

缓冲区刷新一次是很耗费时间的,比如1000个字节的数据,刷新一次是1000个,刷新10次也是1000个,但是刷新10次要使用的时间比刷新1次要使用的时间长的多。
在进行缓冲区刷新时,数据量的大小不是主要矛盾,和外设预备IO的过程才是最耗费时间的。

解答疑惑:

由上述代码的执行结果可知,只有C语言接口的被调用次数发生了变化,系统调用接口一直都是只调用1次,说明系统调用不存在缓冲区。

1、如果没有进行重定向,log.txt文件中有4条打印信息,C语言接口和系统调用接口各打印了1次。stdout默认使用的是行刷新策略,在执行fork函数之前,3条C语言函数已经将数据打印输出到了显示器(外设)上了。在FILE内部就不存在对应的数据了。
2、如果进行了重定向,写入的文件不再是显示器,而是普通的文件,采用的刷新策略是全缓冲,3条C语言显示函数虽然带了\n,但是不足以将stdout缓冲区写满,数据并没有被刷新到对应的文件中。
3、执行fork时,stdout文件对应的缓冲区属于父进程。创建子进程时,紧接着就是进程退出。谁先退出一定要进行缓冲区刷新,所谓的刷新就将数据从缓冲区中放到外设里,刷新的本质就是修改。修改时就会发生写时拷贝,即在缓冲区中会显示两份数据。
4、write为什么没有显示两份呢?因为上面的过程和write无关,write没有FILE,而使用的是fd,没有C语言提供的缓冲区。

7.4 简单模拟用户缓冲区

为了能够对缓冲区有更深的理解,下面模拟实现简单的用户缓冲区。首先要建立FILE结构体,根据我们学到的内容,有文件描述符fd、缓冲区。

同样需要一个刷新策略标志,使用32位中的3个比特位来表示无缓冲、行缓冲、全缓冲。这里仅仅是模拟一个缓冲区,实际的缓冲区肯定不是一个数组。

打开文件函数:

对于不同的打开方式,给打开标志flags不同的比特位赋值,如上图中的代码。


只读方式打开,调用只有两个参数的系统调用open;其他以写的方式打开时,调用有三个参数的open。从这里也可以看出,无论上层语言是什么,打开文件时最终都会调用系统调用open函数。


将文件成功打开以后,对我们自定义的FILE_结构体初始化。
1、结构体中的刷新方式默认采用行缓冲的方式;
2、将使用系统调用open返回的文件描述符fd赋值给结构体中的fd;
3、将缓冲区(数组)进行初始化。

最后返回动态开辟的FILE_指针。

写入函数:

无论写入的内容是什么,都要放在FILE_结构体中的缓冲区中,这里使用了memcpy函数,可以看出:
1、使用write系统调用后,与其认为将数据写入到了文件中,不如认为是将数据复制到了文件中;
2、与其认为write是一个写入函数,不如认为它是一个复制函数。


根据设定的不同刷新策略,将FILE_结构体中缓冲区里的数据通过系统调用write写到Linux内核中,也就是写到文件中。

缓冲区刷新函数:

如果缓冲区中有数据,调用该函数时,立刻将缓冲区中的数据写到Linux内核中,再将内核中的数据写入到文件中。


这里调用了一个fsync函数,该函数的作用就是将内核缓冲区中的数据刷新到文件描述符fd所执行的文件中。
我们使用系统调用write时,其实是将数据写入到了内核缓冲区中,而不是直接写入到了文件中。操作系统会将内核缓冲区中的数据再写入到文件中。

这里使用该函数来强制刷新内核缓冲区中的数据,而没有让操作系统自主去刷新数据,是为了防止内核缓冲区中的数据还没有刷新出去的时候系统就宕机了,此时会导致数据的丢失。
至于操作系统是如何将内核缓冲区中的数据刷新到文件中的,这是操作系统的事情了,我们不需要再了解,我们要掌握的是用户层语言所维护的缓冲区。

文件关闭函数:

在关闭文件时,将缓冲区中的数据刷新到内核中,然后再通过系统调用关闭文件描述符所指向的文件。最后再释放FILE_结构体,以防造成内存泄漏。

验证:

执行结果:

由执行结果可知,在cnt==5时,刷新一次缓冲区,此时log.txt文件中有6条hello world数据。当程序运行结束,关闭文件时会刷新缓冲区,此时log.txt文件中有10条hello world数据。这个测试使用的是行缓冲策略。

7.5 缓冲区和操作系统的关系


当调用write函数时,数据是直接写入到磁盘中的文件上的吗?不是,而是写到了操作系统内的文件所对应的缓冲区里了。struct file结构体里包括了一组操作方法,而且还有对应的文件内核缓冲区。数据通过struct file写到了内核缓冲区中,接着内核缓冲区中的数据刷新到磁盘。此时,数据才真正由内存刷新到磁盘,这个由操作系统自主决定。此时,不一定就是简单的行缓冲或者全缓冲了。
行缓冲、全缓冲指的是在应用层C语言上的缓冲策略。操作系统自主决定将数据刷新到磁盘比我们所说的缓冲策略复杂的多,操作系统什么时候会刷新呢?可能在内存空间不足时,或者间隔一段时间来刷新,缓冲区满了也会刷新。总之,操作系统会权衡自己的内存使用情况来对数据做刷新,而不是简单的行缓冲、全缓冲以及无缓冲这样简单的方法。

行缓冲、全缓冲是C语言上的缓冲区,这个刷新策略比较简单。操作系统还要进行内存管理,当操作系统的内存不够时,会把数据多保存会。如果缓冲区内存不足时,会强制要求操作系统将数据刷新到外设里。操作系统将内核缓冲区的数据刷新到外设里,完完全全由操作系统自己决定。
用户通过write函数将数据交给了操作系统,数据暂时缓存在内核缓冲区里,如果操作系统宕机了,此时可能会出现数据丢失。
无论是C语言提供的fwrite()函数,还是操作系统提供的write()函数,这两个接口本质上都不是写入,而是拷贝函数。

总之,你自己写的代码里的字符串"hello"并不是直接写入到磁盘中,而是使用C语言的接口,将数据拷贝到C语言的缓冲区里,此时采用的是行缓冲或者全缓冲等刷新策略。
然后该数据经过write接口将数据拷贝到内核缓冲区中,然后再由操作系统定期刷新到外设里。从用户自己的代码拷贝到C语言的缓冲区里(1次拷贝),再由C语言缓冲区拷贝到内核缓冲区里(2次拷贝),再将数据刷新到外设里(3次拷贝)。

8.简单模拟用户缓冲区的代码

myStudio.h

#pragma once

#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#define SIZE 1024   //缓冲区大小
#define SYNC_NOW  1 //立马刷新
#define SYNC_LINE 2 //按行刷新
#define SYNC_FULL 3 //全刷新

typedef  struct _FILE
{
    int flags;//刷新方式
    int fileno;//文件描述符                                                                                                                                            
    int cap;//缓冲区buffer的总容量
    int size;//缓冲区buffer当前的使用量
    char buffer[SIZE];
}FILE_;

    FILE_ *fopen_(const char*path_name,const char *mode);
    void fwrite_(const void *ptr,int num, FILE_ *fp);
    void fclose_(FILE_ *fp);
    void fflush_(FILE_ *fp);

myStudio.c

#include "myStdio.h"

FILE_ *fopen_(const char*path_name,const char *mode)
{
    int flags=0;
    int defaultMode=0666;
    if(strcmp(mode,"r")==0)
    {
        //只写方式打开
        flags |= O_RDONLY;
    }
    else if(strcmp(mode,"w")==0)
    {
        //只读方式打开
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if(strcmp(mode,"a")==0)
    {
        //追加方式打开
        flags |= (O_WRONLY | O_CREAT | O_APPEND);
    }
    else
    {
        //TODO
    }
    int fd=0;
                                                                                                                                                                      
    if(flags & O_RDONLY)
    {
        //只读方式打开,调用open不需要传权限
        fd=open(path_name,flags);
    }
    else
    {
        //其他方式打开,调用open需要传权限
        fd=open(path_name,flags,defaultMode);                                                                                                                          
    }

    if(fd<0)
    {
        const char*err=strerror(errno);
        write(2,err,strlen(err));
        return NULL;//文件打开失败会返回NULL
    }

    //开辟缓冲区,创建FILE*_指针
    FILE_ *fp=(FILE_*)malloc(sizeof(FILE_));
    assert(fp);

    //初始化fp
    fp->flags=SYNC_LINE;//默认设置成为行刷新
    fp->fileno=fd;
    fp->cap=SIZE;
    fp->size=0;
    memset(fp->buffer,0,SIZE);

    //返回文件指针
    return fp;//这就是为什么打开一个文件,就会返回一个FILE*的指针
}
 
void fwrite_(const void *ptr,int num, FILE_ *fp)
{
    //写是将数据写到缓冲区中.
    //1、把数据写入到缓冲区中,本质就是拷贝到缓冲区
    memcpy(fp->buffer+fp->size,ptr,num);
    fp->size+=num;//不考虑溢出的情况
  
    //2、判断是否要刷新
    //使用系统调用进行写入
    if(fp->flags & SYNC_NOW)
    {
        //无缓冲,立即刷新
        write(fp->fileno,fp->buffer,fp->size);
        fp->size=0;//清空缓冲区
    }                                                                                                                                                                  
    else if(fp->flags & SYNC_FULL)
    {
        //全缓冲
        if(fp->size==fp->cap)
        {
            write(fp->fileno,fp->buffer,fp->size);
            fp->size=0;//清空缓冲区
        }
    }
    else if(fp->flags & SYNC_LINE)
    {
        //行缓冲,不考虑字符串中间有换行符的情况
        if(fp->buffer[fp->size-1]=='\n')
        {
            write(fp->fileno,fp->buffer,fp->size);                                                                                                                     
            fp->size=0;//清空缓冲区
        }
    }
    else{

     }
}

void fflush_(FILE_ *fp)
{
    if(fp->size >0 )
    {
        //将缓冲区数据刷新
        write(fp->fileno,fp->buffer,fp->size);

        //将内核缓冲区数据刷新,即强制将数据刷新到外设。
        fsync(fp->fileno);
 
        fp->size=0;
    }

}
 
void fclose_(FILE_ *fp)
{
    //在关闭文件之前要进行文件刷新
    fflush_(fp);
    close(fp->fileno);

    free(fp);
}

main.c

#include "myStdio.h"
#include<stdio.h>

int main()
{
    FILE_ *fp=fopen_("./log.txt","w");
    if(fp==NULL)
    {
        return 1;
    }

    int cnt=10;
    const char*msg="hello world ";
    while(1)
    {
        fwrite_(msg,strlen(msg),fp);
        sleep(1);
        printf("count:%d\n",cnt);
        if(cnt==5) fflush_(fp);
        cnt--;
        if(cnt==0) break;
    }
    fclose_(fp);//关闭文件时会刷新缓冲区

    return 0;
}   

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2342919.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Freertos--统计所有任务栈信息以及CPU占比和钩子函数

一、概念 在 FreeRTOS 中统计任务栈信息和 CPU 占比是为了分析栈使用情况防止溢出、优化性能识别高负载任务、合理分配资源避免内存浪费、调试系统排查阻塞或优先级问题&#xff0c;有助于提升效率、确保稳定性、快速定位问题并防止崩溃&#xff0c;比如在你的蜂鸣器任务中可以…

京东商品详情API接口调用技术指南‌

本文基于京东宙斯开放平台&#xff08;JD Open API&#xff09;的 jingdong.ware.product.detail.search.get 接口&#xff0c;提供商品详情数据获取的完整技术方案&#xff0c;包含参数说明、代码实现及实战避坑指南。 一、接口功能与权限‌ 核心能力‌ 获取商品SKU的完整信…

基于Java(JSP)+MySQL实现深度学习的音乐推荐系统

基于深度学习的音乐推荐系统简述 本文简要介绍我做的基于深度学习的音乐推荐系统。主要从需求分析与设计实现的角度来进行介绍。 需求分析 基于深度学习的音乐推荐系统旨在以个性化音乐推荐模型为基础&#xff0c;使用B/S架构的形式实现。个性化推荐模型使用了 随机梯度下降…

Linux:进程间通信---匿名管道

文章目录 1. 进程间通信1.1 什么是进程间通信&#xff1f;1.2 为什么进程要进行进程间通信&#xff1f;1.3 怎么实现进程间通信&#xff1f; 2. 匿名管道2.1 匿名管道的原理2.2 匿名管道的系统接口2.3 匿名管道的使用2.4 匿名管道的运用场景 序&#xff1a;在上一篇文章中我们知…

深度学习小记(包括pytorch 还有一些神经网络架构)

这个是用来增加深度学习的知识面或者就是记录一些常用的命令,会不断的更新 import torchvision.transforms as transforms toPIL transforms.ToPILImage()#可以把tensor转换为Image类型的 imgtoPIL(img) #利用save就可以保存下来 img.save("/opt/data/private/stable_si…

【数据可视化-32】全球住房市场分析(2015-2024 年)数据集可视化分析

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

DAX Studio将PowerBI与EXCEL连接

DAX Studio将PowerBI与EXCEL连接 具体步骤如下&#xff1a; 第一步&#xff1a;先打开一个PowerBI的文件&#xff0c;在外部工具栏里打开DAXStudio&#xff0c;如图&#xff1a; 第二步&#xff1a;DAXStudio界面&#xff0c;点击Advanced选项卡-->Analyze in Excel&#…

使用spring boot vue 上传mp4转码为dash并播放

1.前端实现 <template><div class"video-upload"><el-uploadclass"upload-demo"action"/api/upload":before-upload"beforeUpload":on-success"handleSuccess":on-error"handleError":show-file-…

深入理解指针 (1)

1.内存和地址 1.1内存 1.1.1内存的使用和管理 &#xff08;1&#xff09;内存划分为一个个的内存单元&#xff0c;每个内存单元的大小是1个字节&#xff0c;一个内存单元可以存放8个bit。 &#xff08;2&#xff09;每个内存单元有一个编号&#xff0c;内存单元的编号在计…

Leetcode98、230:二叉搜索树——递归学习

什么是二叉搜索树&#xff1a;右子树节点 > 根节点 > 左子树节点&#xff0c; 二叉搜索树中的搜索&#xff0c;返回给定值val所在的树节点 终止条件为传进来的节点为空、或者节点的值 val值&#xff0c;返回这个节点&#xff1b; 单程递归逻辑&#xff1a;定义一个resu…

15. LangChain多模态应用开发:融合文本、图像与语音

引言&#xff1a;当AI学会"看听说想" 2025年某智慧医院的多模态问诊系统&#xff0c;通过同时分析患者CT影像、语音描述和电子病历&#xff0c;将误诊率降低42%。本文将基于LangChain多模态框架与Deepseek-R1&#xff0c;手把手构建能理解复合信息的智能系统。 一、…

2022李宏毅老师机器学习课程笔记

机器学习笔记目录 1.绪论&#xff08;内容概述&#xff09;2.机器学习和深度学习的基本概念transformer 1.绪论&#xff08;内容概述&#xff09; 机器学习&#xff1a;让机器找一个函数&#xff0c;通过函数输出想要的结果。应用举例&#xff1a;语音识别&#xff0c;图像识别…

笔试强训:Day2

一、字符串中找出连续最长的数字串(双指针) 字符串中找出连续最长的数字串_牛客题霸_牛客网 #include <iostream> #include <string> #include <cctype> using namespace std;int main() {//双指针string str;cin>>str;int nstr.size();int begin-1,l…

linux合并命令(一行执行多个命令)的几种方式总结

背景&#xff1a; 最近安装配置机器&#xff0c;需要手打很多命令。又不能使用docker&#xff0c;所以就使用iTerm2连接多台服务器&#xff0c;然后move session到一个窗口中&#xff0c;shift command i使用XSHELL类似的撰写功能&#xff0c;就可以一次在多台服务器命令窗口…

基于归纳共形预测的大型视觉-语言模型中预测集的**数据驱动校准**

摘要 本研究通过分离共形预测&#xff08;SCP&#xff09;框架&#xff0c;解决了大型视觉语言模型&#xff08;LVLMs&#xff09;在视觉问答&#xff08;VQA&#xff09;任务中幻觉缓解的关键挑战。虽然LVLMs在多模态推理方面表现出色&#xff0c;但它们的输出常常表现出具有…

docker学习笔记5-docker中启动Mysql的最佳实践

一、查找目录文件位置 1、mysql的配置文件路径 /etc/mysql/conf.d 2、mysql的数据目录 /var/lib/mysql 3、环境变量 4、端口 mysql的默认端口3306。 二、启动命令 1、启动命令说明 docker run -d -p 3306:3306 -v /app/myconf:/etc/mysql/conf.d # 挂载配置目录 -v…

从零开始搭建Django博客③--前端界面实现

本文主要在Ubuntu环境上搭建&#xff0c;为便于研究理解&#xff0c;采用SSH连接在虚拟机里的ubuntu-24.04.2-desktop系统搭建&#xff0c;当涉及一些文件操作部分便于通过桌面化进行理解&#xff0c;通过Nginx代理绑定域名&#xff0c;对外发布。 此为从零开始搭建Django博客…

系统与网络安全------弹性交换网络(3)

资料整理于网络资料、书本资料、AI&#xff0c;仅供个人学习参考。 STP协议 环路的危害 单点故障 PC之间的互通链路仅仅存在1个 任何一条链路出现问题&#xff0c;PC之间都会无法通信 解决办法 提高网络可靠性 增加冗余/备份链路 增加备份链路后交换网络上产生二层环路 …

Cursor 配置 MCP Tool

文章目录 1、MCP Tool 的集合2、一个 demo :Sequential Thinking2.1、搜索一个 MCP Tool 获取 command 命令2.2、在 Cursor 配置2.3、配置状态检查与修正(解决网络问题)检查解决办法 2.4、使用 1、MCP Tool 的集合 https://smithery.ai/ 2、一个 demo :Sequential Thinking …

【金仓数据库征文】-《深入探索金仓数据库:从基础到实战》

目录 前言 什么是金仓数据库&#xff1f; 金仓数据库的特点 金仓数据库的核心特点 金仓数据库与其他数据库的对比 金仓数据库的安装 常见的语句 总结 前言 为助力开发者、运维人员及技术爱好者快速掌握这一工具&#xff0c;本文将系统性地介绍金仓数据库的核心知识。内…