【Linux进阶之路】一文吃透文件

前言

  • 先来谈一下文件的共识
  1. 文件 = 内容 + 属性。
    解释:文件在创建时就有基本属性,比如权限,文件名,文件的创建时间等基本信息。
  2. 文件分为打开的文件与未被打开的文件。
    解释:打开的文件由操作系统进行管理。未打开的文件要解决如何找的问题(下面细讲)。
  3. 文件是通过先组织再描述进而间接被管理的。
    解释:其中文件通过其共有的属性被描述为 struct file结构体对象,这些结构体之间通过链表的形式被操作系统组织,进而统一的进行管理。
  4. 一个进程可能会打开多个文件,多个进程可能会打开同一份文件。
    解释:以IO的角度来看,显示器文件,每个进程都要打开这个文件(多对一)。一个进程创建多个文件进行打开(一对多)。因此每个进程都有属于自己打开的一个或多个文件,因此进程这里就抽象出了 struct files_struct进行管理属于自己打开的文件。

一.文件操作

1.文件描述符

  • 文件描述符的本质是什么?
  • 本质是数组的下标。


引入问题:为什么要这样做呢?
解释:

  1. 进程打开与关闭文件,并不一定只打开或者关闭一个文件,因此需要将打开的文件都用数组的方式管理起来。
  2. 如此以来,进程就多了一种访问的方式,即数组的下标。而数组的下标相当于将文件指针又封装了一次,返回给上层,也就是文件描述符,看起来更加的安全。
  3. 并且只需要将文件描述符,下层也可以通过数组的形式看出文件指针进行操作,相当于一箭双雕。

光看内容是理解不了的,我们画图更近一步理解一下。

在这里插入图片描述

再来解释一下为什么要这样做?而不是直接进程指向file。

解释:

  1. 文件是操作系统打开,并且由操作系统进行管理的,跟进程关系到不大,进程只是通过系统调用让操作系统对文件访问修改等。
  2. 每个进程都有自己要打开与关闭的文件,彼此之间是独立的,因此需要每个进程都要有属于自己的files_struct。

2.C文件接口

2.1相对路径

这里我们解决一个小小的疑问:

问题: fopen在用相对路径打开文件时是如何找到路径的?

  • 这里我们大胆猜测一下跟进程有关系,下面我们验证。
#include<stdio.h>
int main()
{FILE* fptr = fopen("test.txt","w");while(1);//死循环是为了方便查看进程的相关信息。fclose(fptr);return 0;
}

先看实验的结果:
在这里插入图片描述

  • 此时进程正在进行死循环。

再查看进程的信息:
在这里插入图片描述
最后用pid查看进程的基本信息:

在这里插入图片描述

  • 到这我们大胆猜测,进程的文件创建的路径与此有关,因此我们把cwd改一下,再查看创建的文件是否还在原路径。

说明:使用chdir接口进行改cwd。

#include<unistd.h>
int chidr(char* path);
参数:所要更改的目录,可以是相对目录,也可以是绝对目录。
返回值:成功返回0,失败返回-1,并且设置合适的错误码。
//补充一点:进程的工作目录必须是合法且当前用户能访问的目录。

更改代码:

#include<stdio.h>
#include<unistd.h>
int main()
{chdir("/home/shun_hua"); FILE* fptr = fopen("test.txt","w");while(1);fclose(fptr);return 0;
}

运行查看结果:
在这里插入图片描述
ps命令查看进程状态:
在这里插入图片描述
用进程pid查看进程的基本信息:
在这里插入图片描述

  • 综上所述:这里的相对路径与进程的cwd(current working directory,即当前工作目录)有关。

2.2写权限

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{FILE* fptr = fopen("test.txt","w");const char* message = "hello fwrite";fwrite(message,strlen(message),1,fptr);//这里的参数我简单提一下,第一个是内容的指针,第二个是文件的内容的大小,//第三个是这样文件的内容大小的个数, 第4个是文件的指针。fclose(fptr);return 0;
}

提出问题:这里的strlen求出的结果之后还需要 加1吗?

不妨都试一下:

  • 不加一的结果:
    934fcdb.png =400x)

  • 加一的结果:
    在这里插入图片描述

可见:对于\0来说,文本编译器并不认可,因此我们可以得出——

  1. 字符串以\0结尾只是语言层次为了标记字符串为结尾,从而设置的一个标记位。
  2. 对于操作系统或者对于一些文本编辑器来看,并不需要这个标记位。
  3. 多说两句,其实加也是无所谓的,只是用文本编辑器打开之后人看不懂。

3.系统接口

3.1open

  • 使用说明
头文件:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
函数声明:
int open(const char* pathname,int flags,mode_t mode);函数参数:
1. pathname,文件创建的路径。
2. flags,文件打开的方式,具体常见的方式有以下五种:2.1 O_CREAT,文件没有就创建。2.2 O_TRUNC,文件打开即清空。2.3 O_APPEND,文件以追加形式打开。2.4 O_WRONLY,文件以只写方式打开。2.5 O_RDONLY,文件以只读方式打开。2.6 O_RDWR ,文件以读和写的方式打开。
除此之外,如果想要多种功能可以以 | 进行相连。
3.mode,文件的默认打开权限,一般文件设置为666,目录设置为777。补充一点:这里不要忘了,权限掩码(umask)这个东西,最后会影响所看到的结果。  
如何设置umask,头文件与此相同,且函数名也为umask。
其函数声明为:mode_t umask(mode_t mask);	    返回值:1. 文件打开成功,返回fd,即返回打开的文件描述符。2. 文件打开失败,返回-1,且会设置合适的错误码。这里顺便把close的接口讲了:
头文件:
#include<unistd.h>
函数声明:
int close(int fd);
参数:文件描述符。
返回值:
1.关闭成功,返回0.
2.关闭失败,返回-1,并且会设置合适的错误码。
  • 简单使用
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{umask(0);//设置权限掩码为0。int fd = open("test.txt",O_CREAT|O_TRUNC|O_WRONLY,0666);//这里是在当前路径下,以如果没有就创建,并且清空只写的方式进行打开。//文件设置的权限为0666(八进制数),切记不能设置为666(十进制数),if(fd < 0){perror("open");return -1;}close(fd);return 0;
}

说明:perror会在字符串的后面跟上 : + 错误原因 。

  • 这里我们再来谈一谈文件打开方式原理,即为什么用 | 就能将功能连接一块。

先看看常用的打开方式其定义:

所在文件:/usr/include/bits/fcntl-linux.h
#define  O_RDONLY	     00
#define  O_WRONLY	     01
# define O_TRUNC	  01000
# define O_CREAT	   0100
# define O_APPEND	  02000
  • 可见,其设置有一个特点,就是指定的比特位进行设置,通过或的结果进行判断是否为此权限,比如假如设置打开方式为0101(八进制),与O_RDONLY的结果与O_RDONLY的结果相等,则存在此权限,同理或上O_CREAT与O_CREAT也相等,因此也有此权限,等等类似。

3.2write

  • 使用说明
头文件:
#include<unistd.h>
函数声明:
ssize_t write(int fd,const void* buf,size_t count);参数:
1. fd,文件描述符。
2. 要写入文件内容的指针。
3. 写入文件内容的大小。返回值:
1.如果写入文件成功,返回写入文件的字节数,0表示啥也没写进去。
2.如果写入文件失败,返回-1,并且设置合适的错误码。
  • 简单使用
#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("test.txt",O_CREAT|O_TRUNC|O_WRONLY,0666);if(fd < 0){perror("open");return -1;}const char* message = "hello write\n";write(fd,message,strlen(message));close(fd);return 0;
}
  • 实验结果:
    在这里插入图片描述

3.3read

  • 基本说明
头文件:
#include<unistd.h>
函数声明:
ssize_t read(int fd,void* buf,size_t count);
参数:
1. fd,文件描述符
2. buf,存放读入内容的指针。
3. count,读的字节数.
返回值:
1. 读成功,返回读入的字节个数,0意味着读到了文件的末尾。
2. 读失败,返回-1,并设置合适的错误码。 
  • 简单使用
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#define MAX_SIZE (1024)
int main()
{int fd = open("test.txt",O_CREAT|O_TRUNC|O_RDWR,0666);if(fd < 0){perror("open");return -1;}//向文件写入hello write\nconst char* message = "hello write\n";write(fd,message,strlen(message));//等会我们会在这下面加上一句代码://向文件中读入hello write\nchar buf[MAX_SIZE] = {0};read(fd,buf,strlen(message));printf("read:%s\n",buf);close(fd);return 0;
}
  • 实验结果:
    在这里插入图片描述

竟然啥都没读到,这是怎么回事呢?

  • 其实很简单,对文件操作,是有一个文件指针的,我们之前向文件里写时,文件指针已经指向hello write\n的最后位置了,肯定读不到,因此我们应该将文件指针调整到开头再开始读。
  • 调整文件的接口——lseek
头文件
#include<unistd.h>
#include<sys/type.h>
函数声明
off_t lseek(int fd,off_t offset,int whence);
参数
1. fd,文件描述符
2. offset,移动到相对于whence偏移量offset的位置。
3. whence, 移动的起点位置,常见的有SEEK_SET(文件开头)SEEK_CUR(文件的当前
位置)SEEK_END(文件末尾位置)。
常用的几种:
lseek(fd,0,SEEK_SET);//移动文件指针到开始
lseek(fd,0,SEEK_CUR);//移动文件指针到当前位置
lseek(fd,0,SEEK_END);//移动文件指针到结束位置返回值:
1. 打开成功,返回距离文件开头的字节数。
2. 打开失败,返回-1,并设置合适的错误码。 

此时我们只需在上面的代码加上:

lseek(fd,0,SEEK_SET);//移动文件指针到开始

再次运行程序,即可看到:
在这里插入图片描述

说明:这里多了一个\n的原因是我们写文件时有一个\n,我们在printf处也加了一个\n,因此有两个\n。


谈完这几个接口,我们接下来就该谈一谈,C接口与系统接口的之间的联系。

先来引入一下:

  1. 任何用户再访问底层时,必然绕不开操作系统。
  2. 操作系统给上层提供系统调用接口来间接的访问底层设备。
  • 因此:我们肯定C语言的文件接口必然封装了系统调用接口。

如何证明呢?

  1. C语言的文件操作是通过文件指针来进行实现的。

就比如我们常见的C语言默认打开的三个流,其实就是文件指针。

typedef struct _IO_FILE FILE;
extern struct _IO_FILE *stdin;
extern struct _IO_FILE *stdout;
extern struct _IO_FILE *stderr;

那我们可以判断stdin 与 stdout 与stderror里面必然封装了文件描述符。

实验验证:

#include<stdio.h>
int main()
{printf("stdin_fd:%d\n",stdin->_fileno); printf("stdout_fd:%d\n",stdout->_fileno); printf("stderror_fd:%d\n",stderr->_fileno); return 0;
}

运行结果:
在这里插入图片描述
如何验证呢?

我们上面的write与read,其第一个参数就是文件描述符,我们可以使用这两个接口进行验证。

  • 从文件描述符0中读入一段数据,再向文件描述符1和2中写入读入的一段数据。
  • 实验符合预期也就是,从键盘读入一段数据,再向显示器中打印两端读入的数据。
#include<unistd.h>
#include<string.h>
#define MAX_SIZE (1024)
int main()
{char buf[MAX_SIZE] = {0};//read max MAX_SIZE - 1 from 0 fd to buf;read(0,buf,MAX_SIZE - 1);write(1,buf,strlen(buf));write(2,buf,strlen(buf));return 0;
}
  • 实验结果:
    在这里插入图片描述
  • 首先陷入阻塞状态,说明需要读入信息。
  • 然后我们输入 hello world,然后按下回车。
  • 最终显示器打印两次 hello world。
  • 结论:实验与预期相符,因此可以断定,stdin,stdout,stderr对应的就是 0 1 2 号文件描述符。

这时我们可以接着进行推断,C语言默认打开这三个流是C语言的特性吗?

  • 肯定不是,既然是操作系统管理,那么所有语言必然直接或间接的封装了这三个流,也就意味着这是进程的特性。
  • 补充:既然C接口是由系统掉用接口进行封装了,那么像一些输入输出,比如scanf必然也封装了read之类的系统调用,printf必然封装了write这类的系统调用。

二.文件重定向

1.基本原理

  • 先来看这一样一段代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{//先关闭文件描述符1int fd1 = open("test1.txt",O_CREAT|O_TRUNC|O_RDWR,0666);int fd2 = open("test2.txt",O_CREAT|O_TRUNC|O_RDWR,0666);int fd3 = open("test3.txt",O_CREAT|O_TRUNC|O_RDWR,0666);printf("test1.txt:%d\n",fd1);printf("test2.txt:%d\n",fd2);printf("test3.txt:%d\n",fd3);close(fd1);close(fd2);close(fd3);return 0;
}
  • 运行结果:

在这里插入图片描述

因为前面的文件描述符(0 1 2)是进程默认打开的,这里新打开的文件自然在之后。

  • 再来看这样一段代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{//先关闭文件描述符1close(1);int fd = open("test.txt",O_CREAT|O_TRUNC|O_RDWR,0666);printf("test.txt:%d\n",fd);printf("hello world\n");return 0;
}
  • 实验现象:

    在这里插入图片描述

  1. 首先经过前面的讨论,这里我们关闭的文件描述符1,即stdout。
  2. 而且printf 默认向stdout里面输出数据。
  3. 其次这里我们看到的现象为并没有向显示器里面输出,而是向test.txt里面输出。
  4. 而且我们看到test.txt里面的文件描述符变成了1。
  • 总结:这里的1号文件描述符关闭之后,后面打开的文件占据了1号描述符的文职,进而变成了stdout,而语言层面识别看不出来变化,因此向test.txt里面打印了数据。

这是什么原理呢?画个图就比较清晰了。

在这里插入图片描述

  1. 可见,stdout只是语言层面的概念,就是1号文件描述符下标存放的文件指针。底层变化,stdout,即下标并没有发生变化,但文件指针发生了变化。
  2. 可以判断,文件打开的原理本质上就是在指针数组中遍历寻找一个为空的位置,并将打开的文件指针放入其中,返回数组的下标,即所谓的文件描述符。
  • 这样的现象我们称之为重定向,但是这里代码的重定向是否过于牵强了呢?
  • 毕竟我们是先关闭文件描述符,再将文件打开,之后我们观察到现象,再把这种现象解释为重定向。
  • 那有没有直接进行重定向的接口呢?

2.dup2

  • 基本说明
头文件
#include<unistd.h>
函数声明
int dup2(int oldfd,int newfd);
参数
1.oldfd,是旧的文件描述符。
2.newfd,是新的文件描述符。
这里的意思是将oldfd下标指向的文件指针覆盖newfd下标的文件指针。
返回值
1. 成功,返回newfd.
2. 失败,返回-1,设置合适的错误码。
  • 简单使用
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{int fd = open("test.txt",O_TRUNC | O_CREAT | O_RDWR,0666);//直接进行重定向dup2(fd,1);//将fd指向的文件指针覆盖到1里面。printf("hello world\n");close(fd);return 0;
}

实验结果:
在这里插入图片描述


  • 此处我们或许会疑惑这里,多个文件描述符存放的文件指针竟然是一样的,这可以吗?
  • 理解原理就不难理解,进程中每存放一个文件指针,文件指针指向的file结构体对象的引用计数就会加1,这样在关闭时引用计数减减即可,这样不会出现一个文件释放多次的情况。
  • 除此之外,如果你觉得另一个文件指针多余,也可在重定向之后将其关闭。

除此之外,我们还可以解决一个疑惑,即stdout与stderr有什么区别?

先看这样一段代码:

#include<unistd.h>
#include<string.h>
int main()
{const char* message = "hello world\n";write(1,message,strlen(message));write(2,message,strlen(message));return 0;
}

我们先正常执行这一段代码:
在这里插入图片描述

到这里应该都理解。

我们再将./test输出的内容重定向到test.txt中。

在这里插入图片描述

疑惑的是,这里的内容竟然只输出了一半,另一半还打印到了屏幕上,这是怎么回事呢?

  • 联想之前的知识,这里其实很容易解释,进程启动时只重定向了,stdout,即1号文件描述符的文件指针为test.txt,而stderr,即2号文件描述符的文件指针并没有进行重定向。

那该如何做呢?

有两种做法:

  1. ./test 1 > test1.txt 2> test2.txt ; 细节: 第二个 >必须与前面挨着。
  2. ./test 1 > test1.txt 2>& 1; 细节: >& 必须与前面挨着。
  • 第一种
    在这里插入图片描述
  • 第二种
    在这里插入图片描述

这里多解释一下第二种方法,是在前面的 ./test 1 > test1.txt执行后,1的文件指针已经变为指向test1.txt的, 然后把 1里面的文件指针,再拷贝到2里面。因此 1 2 里面都是test1.txt的文件指针。

  • 总结: 除此之外,stdout与stderr没有本质区别。

拓展:

  1. 程序替换时,会将进程的文件也进行替换吗?
  • 答案是不会的,程序替换是替换页表中虚拟地址与物理地址的联系并且在内存中加载相应进程的内容,是与 mm_struct对象有关的。而文件的管理是与 files_struct相关的,两者之间没有必然联系,各干各的,互不影响。
  1. 如何理解"一切皆文件"?
  • 图解
    在这里插入图片描述

解释:

  1. 首先在进程看来,只需要看到struct file 即可,而具体的操作方法并不关心,再底层封装了一个 file_operations,即具体的操作方法,里面全部都是函数指针,且当打开一个具体的文件时,会初始化有对应的操作方法。
  2. 这样做体现是封装 + 多态的思想。既然C语言这么做了,那么可以看出面向对象是历史发展的必然结果,且不是语言特有的思想,是大量工程实验出来的结果。
  3. 这样封装出来的一套struct file 被称之为 虚拟文件系统(VFS)。
  • 课外小知识: bash是媒婆广义上的命令行解释器,而shell是王婆是具体的某个系统的命令行解释器。

三.文件缓存区

 缓存区是一个比较抽象的东西,我们在最初的了解printf时,就听过缓存区,可是它到底是个什么,我们是不知道的,或者说是不清楚的,下面我们就来了解一下缓冲区。

1.C缓存区

  • 先观察现象

例1:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{printf("hello printf");//向stdout 输出hello worldconst char* message = "hello write";write(1,message,strlen(message));close(1);//在这里我们将文件关闭。return 0;
}

现象:
在这里插入图片描述

 感觉是不是有点奇怪,write为啥能进行输出呢?而printf不输出呢?
解释:

  1. 首先我们之前文章中有一个小结论,那就是C语言的缓存区不在内核当中。
  2. 其次printf与write中我们都没有加 \n,也就意味着我们并没有主动的刷新缓存区。
  3. 而且C接口,也就是这里的printf必然封装了write之类的写的系统调用接口。
  4. 由于C语言的缓存区不在内核中,在程序关闭时进行文件刷新时,很显然的前面已经关闭了文件指针/文件描述符,C语言的缓存区刷不到文件里面,所以看不到这里的"hello printf",而write是在close之前掉的,且是直接写到内核缓存区了,因此这里能看到write的信息。

图解:

在这里插入图片描述

 到这里现象想必已经一目了然,那既然这么麻烦,我们为什么还要为C语言设立这样一个缓冲区呢?

解释:

  1. 我们在用系统调用进行刷新时,是将内存刷新到文件中的,而C语言比系统调用多做了一步——将数据格式化,即变为字符串,变成字符串之后再写入文件,那人就能够看懂了!
  2. 这样做可以提升磁盘的读写效率,就好比开快递公司,租一辆飞机进行空运快递,肯定是一批快递进行空运,而不是一个快递进行空运,肯定是一批效率高,而不是一个效率高。类比一下C缓存区存放就是快递,而刷新函数就是那辆飞机,目的地是磁盘。

其次存放快递的我们一般之为快递站,那这个快递站——C缓存区在哪?

  • 在FILE文件指针指向的FILE对象中,由FILE结构体对象进行管理。

除此之外,快递有的时候,也分轻重缓急,比如有的快递需要紧急运输,有的快递正常运输,有的快递延迟运输。这就对应了C缓存区的刷新方式

2.刷新方式

  • 无缓冲刷新——直接刷

此处我们需要回顾一下刷新缓存区的接口:

头文件
#include <stdio.h>
函数声明
int fflush(FILE *stream)
返回值:成功返回0;失败返回-1并设置错误码。
  • 简单使用
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{printf("hello printf");//向stdout 输出hello worldfflush(stdout);//立即刷新。close(1);return 0;
}

现象:

在这里插入图片描述

  • 行刷新 —— 遇到 \n 再进行刷新(显示器)
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{printf("hello printf\n")close(1);return 0;
}

现象:

在这里插入图片描述

  • 全缓冲 —— 缓冲区满了再进行刷新。

示例:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{FILE* f_ptr = fopen("test.txt","w");int fd = f_ptr->_fileno;const char* message = "hello";int n = 1;while(n <= 1000){fprintf(f_ptr,"%d %s ",n,message);n++;}close(fd);return 0;
}

我们可以看到:
在这里插入图片描述

这里在写入第831次时,缓冲区满了,再进行刷新的,除此之外我们可以看出缓存区还是不小的。

其次我们来验证一下文件刷新的方式,上述只是把缓存区撑满了,再刷新的,这里我们试着一边向文件写入一些行刷新的内容,一边看文件中是否有内容进行验证。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{FILE* f_ptr = fopen("test.txt","w");const char* message = "hello\n";fprintf(f_ptr,"%s",message);sleep(1);fprintf(f_ptr,"%s",message);sleep(1);fprintf(f_ptr,"%s",message);sleep(1);return 0;
}

我们在运行程序的同时,再开辟一个监视窗口用以下脚本打印信息:

while :; do cat test.txt; sleep 1; echo "---------------"; done

现象:

在这里插入图片描述

  • 由前几秒都没有内容输出,可见文件是全缓冲进行刷新的。

拓展:

思考下面代码的打印结果:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{printf("hello world");//向stdout 输出hello worldfork();return 0;
}

结果:
在这里插入图片描述

解释:

  1. 此处创建了一个进程,父子进程数据在创建后共享,但会发生写时拷贝。
  2. 缓存区也是数据,在程序关闭时会自动进行刷新,因此父子进程发生写时拷贝,各刷各的互不影响。

四.文件系统

1.磁盘结构

当前我们就来解决一个问题:磁盘如何查找文件?

我们从硬件出发,慢慢过渡到软件。

  • 我们当代的文件一般都存在硬盘当中,而硬盘一般我们用的计算机现在都使用的是固态硬盘,但是企业在存储一些大型数据时为了降低成本,一般使用的是机械硬盘。
  • 固态硬盘
    在这里插入图片描述
  • 机械硬盘
    在这里插入图片描述
  • 机械硬盘的结构比较便于理解,我们就从它入手,先看这一张图片:

在这里插入图片描述

分析:

  1. 磁盘(盘面):用于存储文件信息,二进制信号,一般有多个磁盘。
  2. 磁头:用于寻找文件,其与磁盘的距离很小,一般有多个磁头,与磁盘对应。
  3. 主轴:方便磁头进行定位。

补充:机械硬盘

  1. 造价低,寿命长。
  2. 制作环境要求高。
  3. 马达与磁盘转动声音可能较大。
  4. 磁盘与磁头一一对应,且不接触,通过空气与电进行写入信息。

抽象出来的逻辑结构是这样的:

  • 正面图
    在这里插入图片描述
  1. 所有磁头共进退。
  2. 柱面:盘面的相对位置相同的磁道形成的立体结构。
  • 俯视面

在这里插入图片描述

  1. 磁道:就是磁盘的同心圆的一周。
  2. 扇区:就是对磁道进行划分的一段,一般为512byte / 4 KB且扇区是磁盘访问的基本单位。
  • 到这里我们应该能大致看出磁盘是如何找到文件的。

解释:

  1. 第一步:先定位磁盘盘面/磁头(所有磁头共进退)。
  2. 第二步:再定位磁盘的磁道。
  3. 第三步:定位磁盘的扇区位置。

硬件大概清楚了,那么接下来我们就来谈谈软件层是如何定位的吧。


首先,得先对磁盘进行行管理,如何管理呢?

  1. 先来联想一下,我们小时候英语听力的磁带。
    在这里插入图片描述

  2. 当我们用复读机进行读取数据时,数据存放在磁带当中进行读取。

  3. 再回过头来看机械硬盘,如果把磁带的宽度变成0不也是一样吗。

因此我们可以抽象出这样一个线性的磁盘:

在这里插入图片描述

 但是一般来说,磁盘的容量太大,比如有1000GB,我们还需要,把这1000GB再进行分成若干个单位进行管理,假如分为256GB为一个单位。既然这样我们如果还觉得256GB还大呢?我们可以再分,直到操作系统觉得不大为止。

  • 这种分法体现的是分治的思想
  • 这里的C盘和D盘就是通过把磁盘进行分块进行管理的。在这里插入图片描述

在这里插入图片描述

  • 与此同时对空间进行切分的同时,也有对应的数组结构对磁盘,磁道,扇区进行切分,并由在此数据区的编号找到硬件对应的扇区。

假如说,分到10GB能进行管理了,该如何进行管理呢?

对这10GB进行分组,描述组织和管理就是下图的文件系统:

在这里插入图片描述

  • Block group在开始使用之前,需进行格式化,以便于之后的管理。

 这里的innode Table就是存放索引结点(struct innode)的地方,innode存放的是文件的属性,比如说文件的编号,文件的权限,文件的大小,文件的时间信息,还有文件的数据区(Data blocks)

  1. innode有128字节,且innode有唯一的编号
  2. 文件是通过innode编号进行查找文件的,而不是文件名。
  • 如何存储数据呢?

Data block里面存放的是数据块,即用来存放的数据。且每个数据块大小为4KB。

图解:
在这里插入图片描述

  • 如何删除数据呢?
  • 通常我们都知道下载很慢,但删除很快,这是为什么呢?删除文件就意味着这个文件不在了。因此判断在不在,用位图的数据结构查找起来很快且很省内存。

因此:

  • 这里的innode Bitmap表示的是innode对应文件是否有效,可以快速删除文件。
  • 这里的Block Bitmap表示的是所对应编号的数据是否被使用,可以标记数据块的使用情况,以便于删数据。

拓展:

  1. Boot Block(引导块)是指存储在存储设备起始扇区的特定区域。通常包含引导加载程序(boot loader),用于启动操作系统。
  2. Super Block(超级块),通常记录文件系统类型、大小、状态、块大小、inode(索引节点)数量等关键参数。
    3.Group Descriptor Table(组描述符),记录的是数据块与innode的使用情况,位图的存储位置,目录以及其它文件信息。
  3. 一级索引的原理为,指向的4KB,即4096byte存放的又是数据块的编号,假设编号类型为int 4字节,可以指向1024个数据块,也就意味着4KB就可管理 4MB的内存。

2.再识目录

之前我们对目录从权限的角度进行了如下解释:

  1. 没有x权限,目录无法进入。
  2. 没有r权限,无法查看对应的文件。
  3. 没有w权限,无法创建新文件。
  • 结合我们刚学的:文件是通过innode编号进行索引的,而不是通过文件名进行索引的,且结合常识文件是在目录下进行创建的。

由此我们可以得出如下结论:

  1. 目录必然存放着innode编号,且与文件名进行映射。
  2. 文件名是key,innode是val,且在目录下进行索引时不能存在同名文件,因为存在二义性。
  1. 所谓的r权限,其实就是查看文件名对应的映射。
  2. 所谓的w权限,其实就是在目录中添加对应的文件名与innode的映射关系。
  • 那目录也是文件,其文件也是文件名形式存在,也要进行查找其对应innode,才能找到其目录文件下的innode,如何进行查找呢?
  1. 递归向上进行查找,找到根目录就进行停止,因为根目录的innode是确定的。
  2. 为了提升查找的效率,可以借助环境变量,直接索引,也可进行缓存将可能要访问到的目录的路径进行提前的加载。

3.软硬链接

  • 软链接(符号链接)

简单使用

ln -s 【指定文件】 【目标文件】
  • 对普通文件软链接
    在这里插入图片描述
    对指向文件进行删除:
    在这里插入图片描述
    此时变成了悬空链接,即指向的文件失效了。
  • 对目录软链接
    在这里插入图片描述
  • 总结:软链接产生的只是一种快捷方式,和指向的文件并没有必然的联系,且权限也不相同,而且删除指向文件会使指向文件的快捷方式失效。
  • 补充:软连接只是对路径文件名产生的链接,假如再删除再在原目录下创建相同的文件,软连接也能执行,只不过不是相同的文件。
  • 硬链接

简单使用

ln  【指定文件】 【目标文件】
  • 普通文件
    在这里插入图片描述
  • 可见这里的1变为了2,这里的数字就指向文件的引用个数,简称硬链接数。
  • 目录
    在这里插入图片描述

问题: 为什么不能对目录硬链接呢?

  • 其实很简单,假如你要找find某个文件,此时你在某个路径下对上面的一个目录进行硬链接,此时你进入目录时,就避免不了查找硬链接的目录,一旦你查找对应的目录,此时就陷入了死循环。
    在这里插入图片描述
  • 补充:在目录下的 . … 这些文件系统是不会查找的,因此一般查找时不会陷入递归。
  • 总结:硬链接是一种引用,不同文件名指向同一个文件,即拥有相同的inode,且在删除时如果硬链接数大于1,则只需将硬链接数减减即可。

删除链接(也可删除文件):

unlink 【指定文件】

4.物理内存

  • 文件如何组织与管理解决了,那文件是如何加载的呢?
  1. 首先文件要被访问,不能直接在磁盘中访问,这样速度太慢了。
  2. 因此文件要首先加载到内存中,才能被高效的管理。

因此问题就转换为了:文件是如何被加载到物理内存的?
谈到这里我们就要普及两个概念:

  1. 页帧(Page Frame):页帧是物理内存(RAM)中的一个固定大小的区域,用于存储从辅存(如硬盘)中调入的页面内容。操作系统使用页帧来管理物理内存,将页面映射到这些页帧上。
  2. 页框(Page Frame):页框通常指的是在内存管理单元(MMU)中用于存储页面表的一组连续条目。页框包含了用于地址转换的页表项,以便将虚拟地址映射到物理地址上的对应页帧
  • 总之,页帧就是管理物理内存的,用于加载硬盘内容,页框是管虚拟地址与物理地址映射的。

那物理内存又是如何被管理的呢?

  1. 先描述,抽象出具体的属性,再组织,用数据结构的形式进行管理。
  2. 比如物理内存都有地址,空间大小,是否被使用等基本信息,因此可以通过结构体进行描述,再用结构体数组进行组织,进而管理整个物理内存。
  3. 因此在Linux系统中,通过struct page arrary[SIZE],这样进行管理内存,每个page管理4Kb的内存,且这个数组所占的内存必然不会太大,否则会影响其它进程,因此这个数组内部采用的是联合体的形式。
  • 因此,对物理内存的使用变为了对page数组的管理。

总而言之:

  1. 打开文件分为找文件和加载文件到内存两步。
  2. 找文件。通过文件系统对磁盘进行管理,其中的inode可以帮我们找到磁盘中文件的定位。
  3. 加载文件到内存,找到文件之后,通过page数组进行申请空间,通过页帧加载磁盘文件内容到内存,且页框进行物理与虚拟的映射,从而将文件加载到内存。
  • 注意:这里文件系统的Data blocks,是磁盘中的数据,这里的加载的文件是到内存中去了。

  • 图解:
    在这里插入图片描述

  • 拓展

问题1 : 为什么文件要按照4KB的形式,从磁盘进行加载?

  • 局部性原理,下次可能会使用其周围的数据,这样做可以减少外设的访问次数。
  • 外设与内存简直就不在同一个量级,可以理解内存像火箭,外设就像汽车,虽然汽车对我们来讲也算快的,但是有更快的火箭,火箭可不想等汽车,因此需要4KB进而一步到位。

问题2: 4KB是如何得到的?

  • 大量的科学家进行实验,具体可看学术论文。

问题3 : 大量的IO请求,系统如何进行处理?

  • 系统会进行排序(按照盘面,磁道,扇区的优先级),然后一次性的进行处理。因此断电很可能会导致数据的丢失,不管是在系统层面还是在语言层面。

  • 总结重点
  1. 文件描述符的原理,系统接口与C接口的参数,以及相关细节。
  2. 文件重定向的原理,接口使用,stdin与stderr的一点区别。
  3. 深入理解文件缓存区,以及刷新方式。
  4. 文件系统,从磁盘到内存到软件,理解文件的查找与加载,软硬链接。

尾序

 如果觉得文章有所收获,顺手点个赞鼓励一下吧!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JZ22:链表中倒数第k个结点

JZ22&#xff1a;链表中倒数第k个结点 题目描述&#xff1a; 输入一个链表&#xff0c;输出该链表中倒数第k个结点。 示例1 输入&#xff1a; 1,{1,2,3,4,5} 返回值&#xff1a; {5} 分析&#xff1a; 快慢指针思想&#xff1a; 需要两个指针&#xff0c;快指针fast&…

云课五分钟-03第一个开源游戏复现-贪吃蛇

前篇 云课五分钟-02第一个代码复现-终端甜甜圈C 视频 云课五分钟-03第一个开源游戏复现-贪吃蛇 一个终端的动态字符显然很难调动编程的积极性&#xff0c;那么更有趣的开源的游戏也许是一种更好的启发。 文本 蓝桥ROS机器人之绚丽贪吃蛇 如何在Linux下使用 DungeonRush-mast…

【java学习—十四】反射机制调用指定方法、指定属性(5)

文章目录 1. 调用指定方法2. 调用指定属性 1. 调用指定方法 通过反射&#xff0c;调用类中的方法&#xff0c;通过 Method 类完成。步骤&#xff1a;     ①通过 Class 类的 getMethod(String name,Class...parameterTypes) 方法取得一个 Method 对象&#xff0c;并设置此…

C#使用时序数据库 InfluxDB

一、安装 https://docs.influxdata.com/influxdb/v2/install/?tWindows 解压后使用cmd运行 访问 localhost:8086 配置 第一次登入会初始化 配置登入账号 保存TOKEN 这个TOKEN用于后期代码链接访问数据库&#xff0c;忘记了只能删除重新生成 点击QUCK START进入管理页面 …

【vue实战项目】通用管理系统:api封装、404页

前言 本文为博主的vue实战小项目系列中的第三篇&#xff0c;很适合后端或者才入门的小伙伴看&#xff0c;一个前端项目从0到1的保姆级教学。前面的内容&#xff1a; 【vue实战项目】通用管理系统&#xff1a;登录页-CSDN博客 【vue实战项目】通用管理系统&#xff1a;封装to…

Android抓包工具—Fiddler详解

前言 平时和其他大佬交流时&#xff0c;总会出现这么些话&#xff0c;“抓个包看看就知道哪出问题了”&#xff0c;“抓流量啊&#xff0c;payload都在里面”&#xff0c;“这数据流怎么这么奇怪”。 &#x1f449;这里出现的名词&#xff0c;其实都是差不多的意思啊&#xf…

ElementUI表格el-table自适应高度(表头表尾固定不动)

ElementUI表格el-table自适应高度&#xff08;表头表尾固定不动&#xff09;&#xff0c;内容只在中间滚动&#xff0c;效果如图&#xff1a; 实现代码 <div class"mt-10" :style"{height:tableHeight}"><div class"operation-bar">…

PyCharm 安装库时显示连接超时

在setting->python Interpreter 中用“” 安装库时&#xff0c;出现一个弹窗&#xff0c;提示信息如下&#xff1a; Error updating package list: Connect timed out 通过查阅资料&#xff0c;发现是镜像源的问题&#xff0c;具体的解决方案如下&#xff1a; 1. 更新一下…

电源电压范 围宽、功耗小、抗干扰能力强的国产芯片GS069适用于电动工具等产品中,采用SOP8的封装形式封装

GS069电动工具直流调速电路是CMOS专用集成电路&#xff0c;具有电源电压范 围宽、功耗小、抗干扰能力强等特点。通过外接电阻网络&#xff0c;改变与之相接 的VMOS 管的输出&#xff0c;达到控制电动工具转速的作用。该电路输出幅值宽&#xff0c; 频率变化小&#xff0c;占空比…

云原生下GIS服务规划与设计

作者&#xff1a;lisong 目录 背景云原生环境下GIS服务的相关概念GIS服务在云原生环境下的规划调度策略GIS服务在云原生环境下的调度手段GIS服务在云原生环境下的服务规划调度实践 背景 作为云原生GIS系统管理人员&#xff0c;在面对新建的云GIS系统时&#xff0c;通常需要应对…

2023-2024-2 高级语言程序设计-二维数组

7-1 矩阵运算 给定一个nn的方阵&#xff0c;本题要求计算该矩阵除副对角线、最后一列和最后一行以外的所有元素之和。副对角线为从矩阵的右上角至左下角的连线。 输入格式: 输入第一行给出正整数n&#xff08;1<n≤10&#xff09;&#xff1b;随后n行&#xff0c;每行给出…

uniapp——项目day05

购物车页面 结算区域 把结算区域封装为组件 1. 在 components 目录中&#xff0c;新建 my-settle 结算组件&#xff1a; 2. 初始化 my-settle 组件的基本结构和样式&#xff1a; <template><view class"my-settle-container">结算组件</view> …

【中间件篇-Redis缓存数据库05】Redis集群高可用高并发

Redis集群 Redis Cluster是Redis的分布式解决方案&#xff0c;在3.0版本正式推出&#xff0c;有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时&#xff0c;可以采用Cluster架构方案达到负载均衡的目的。之前,Redis分布式方案一般有两种: 1、客户端分…

使用Filebeat+Kafka+Logstash+Elasticsearch构建日志分析系统

随着时间的积累&#xff0c;日志数据会越来越多&#xff0c;当您需要查看并分析庞杂的日志数据时&#xff0c;可通过FilebeatKafkaLogstashElasticsearch采集日志数据到Elasticsearch中&#xff0c;并通过Kibana进行可视化展示与分析。本文介绍具体的实现方法。 一、背景信息 …

Python代码运行速度提升技巧!Python远比你想象中的快~

文章目录 前言一、使用内置函数二、字符串连接 VS join()三、创建列表和字典的方式四、使用 f-Strings五、使用Comprehensions六、附录- Python中的内置函数总结关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项…

iPhone或在2024开放第三方应用商店。

iPhone或开放第三方应用商店&#xff0c;可以说这是一个老生常谈的话题。对于像是iOS这样封闭的系统来说&#xff0c;此前传出苹果可能开放侧载消息的时候&#xff0c;又有谁能信&#xff0c;谁会信&#xff1f; 如果是按照苹果自身的意愿&#xff0c;这种事情自然是不可能发生…

文件上传 [ACTF2020 新生赛]Upload1

打开题目&#xff0c;发现是一道文件上传题目 随便上传个一句话木马上去 发现网站前端有白名单限制&#xff0c;只能上传含有jpg&#xff0c;png&#xff0c;gif的后缀文件 那我们便传个2.jpg的一句话木马上去&#xff0c;bp抓包 我们改成php文件后缀试试&#xff0c;发现重发…

Oracle(17)Managing Roles

目录 一、基础知识 1、基础介绍 2、Predefined Roles 预定义的角色 3、各种角色的介绍 二、基础操作 1、创建角色 2、修改用户默认role 3、回收role 4、删除role 5、为角色授权 6、授予角色给用户 7、查看用户包含的角色&#xff1a; 8、查看角色所包含的权限 9、…

systemverilog:interface中的modport用法

使用modport可以将interface中的信号分组并指定方向&#xff0c;方向是从modport连接的模块看过来的。简单示例如下&#xff1a; interface cnt_if (input bit clk);logic rstn;logic load_en;logic [3:0] load;logic [7:0] count;modport TEST (input clk, count,output rst…

一键帮您解决win11最新版画图工具难用问题!

&#x1f984;个人主页:修修修也 ⚙️操作环境:Windows 11 正文 自从win11更新后,新版的画图工具变得非常难用,如: 使用橡皮擦后露出背版马赛克 框住某部分拖动移动时背景露出马赛克剪贴板上图片信息无法直接插入到画图板 目前没有一个好一些的能够在软件内部解决这些问题的方…