【Hello Linux】基础IO

作者:@小萌新
专栏:@Linux
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:简单介绍linux中的基础IO 包括语言层面的io知识 系统层面的io知识 文件描述符fd 重定向的原理 缓冲区 软硬连接 以及文件的三个时间和makefile的联系

c语言中的文件操作

关于具体的c语言文件操作相关知识 大家可以参考博主之前写的两篇博客

c语言文件操作上

c语言文件操作下

下面我们会直接使用上两篇博客的知识写出两个文件读取操作的示例

文件写入

  1 #include <stdio.h>
  2 
  3 
  4 int main()
  5 {
    
    
  6   // 打开文件
  7   FILE* fp = fopen("log.txt" , "w");
  8   if (fp == NULL) // 打开失败返回空 报错
  9   {
    
    
 10     perror("fopen");
 11   }
 12   fputs("hello world!\n",fp); // fputs用法                                                                                 
 13   fclose(fp); // 关闭文件
 14   return 0;
 15 }

我们使用 “w” 的格式打开了一个叫做 log.txt 的文件

在这个格式下如果路径中没有该文件则会自动创建

创建完毕之后我们往文件中写入一段文本

扫描二维码关注公众号,回复: 14717404 查看本文章

我们编译之后看看效果
在这里插入图片描述

文件读取

在文件读取中我们用到一个很熟悉的函数 fgets

这个函数我们在之前写shell小程序的时候用来获取stdin中的信息

  1 #include <stdio.h>
  2
  3
  4 int main()
  5 {
    
    
  6   FILE* fp = fopen("log.txt" , "r"); // open the file 
  7   if (NULL == fp) // open fail
  8   {
    
    
  9     perror("fopen!");
 10   }
 11 
 12   char buffer[20];
 13   fgets(buffer , 20 , fp); // ¶ÁÈ¡ÎļþµÄÓ÷¨
 14   printf("%s",buffer);                                                                                                               
 15   return 0;
 16 }

我们使用 “r” 的格式打开了一个叫做log.txt的文件

在这个格式下我们只能对于打开的文件进行读取操作

编译运行之后看看效果

在这里插入图片描述

当前路径再理解

我们发现在上面向文件中写入内容的代码中 我们并没有指定文件的路径

系统默认在当前目录下给我们创建了一个新文件

在这里插入图片描述

那这时候我们猜想

系统默认会在我们当前所处的目录中给我们创建新文件

为了验证这个猜想我们首先退出到上层目录

然后在上层目录中执行lesson12的可执行程序

在这里插入图片描述
我们发现log.txt在我们当前目录下生成了

这也就验证了我们猜想的正确性

为什么会是这样子呢?

我们运行一个进程之后使用 ps 命令查看该进程的pid

之后进入该进程pid之后我们可以发现两个软连接

(软连接的概念我们这篇博客后面会讲解)

在这里插入图片描述

一个软连接是进程运行时的路径 cwd

一个软连接是可执行文件所在路径 exe

我们所说的当前路径就是cwd 即程序称为进程时候我们所在的路径

c语言中默认打开的三个流

一切皆文件

我们之间介绍过了一个概念叫做一切皆文件 所有的东西我们都可以当作文件来看待

比如说显示器可以当作一个文件 我们往显示器上打印数据实际上就是向这个文件中写入数据

比如说键盘可以当作一个文件 我们从键盘上获取数据实际上就是从这个文件中读取数据

c语言中三个流

我们在C语言中会默认打开三个流

他们分别是 stdin stdout stderr

他们对应的设备如下表

设备
stdin 键盘
stdout 显示器
stderr 显示器

我们在cplusplus中查询这三个流
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

当我们的c语言程序运行起来的时候 我们的程序会向操作系统申请这三个流之后我们的printf scanf等程序才可以向显示器打印数据 从键盘读取数据

从上面cplusplus的简介我们可以知道 其实这三个流的数据类型都是FILE*也就是文件类型

既然显示器我们可以当作一个文件 那么我们向这个文件中写入数据不就是向屏幕中打印数据嘛

我们实验下

  1 #include <stdio.h>
  2 
  3 
  4 int main()
  5 {
    
    
  6   fputs("hello world!\n",stdout);
  7   fputs("hello linux!\n",stderr);                                                        
  8   return 0;
  9 }

编译之后运行

在这里插入图片描述
我们发现确实符合我们的预期

系统文件I/O

实际上不光是我们的c语言有这三个流 C++中也有类似的三个流 cin cout cerr 并且其他语言中也有类似的概念 所以说这并非是一个语言特有的而是操作系统所赋予共有的特性

接下来我们开始研究操作系统中的文件操作

首先来看下面这一张图

在这里插入图片描述

实际上我们语言层(c语言 c++)的函数都在用户调用接口这一层

而我们的系统接口函数则在系统调用接口这一层

也就是说其实我们语言层的函数是对于系统接口函数的封装

当我们在linux操作系统下运行c语言代码的时候 c语言的库函数就调用linux平台的系统调用接口进行封装 当我们在windows操作系统下运行c语言代码时 c语言的库函数就调用windows平台的系统调用接口进行封装

这样子我们的语言就具有了跨平台性 也可以二次开发了

open

我们在系统调用接口中一般使用open打开一个文件

它的函数原型如下

   int open(const char *pathname, int flags, mode_t mode);

参数讲解

open的第一个参数

它的第一个参数要求我们输入一个字符串

  • 如果我们输入的字符串是一个路径 当需要我们创建文件时 文件会默认在这个路径下创建
  • 如果我们输入的字符串是一个文件名 当需要我们创建文件时 文件会默认在当前路径创建 (当前路径的概念已经在上面讲解)

open的第二个参数

它的第二个参数要求我们输入一个整数

而实际上我们在使用的时候并不会直接输入一个整数 而是会输入一系列的宏并且将它们进行位操作 这是因为我们并不是使用这个整数去标识打开的状态 而是使用这个整数的位去标识

举个例子(并不准确)

0000 0001 标识可写

0000 0010 表示追加

0000 0100 表示创建文件

如果我们想要这个文件是可写的状态打开 并且还可以追加 如果不存在就创建它 那么我们就需要使用按位或操作(|)

一般我们常用的选项如下表

参数选项 含义
O_RDONLY 以只读的方式打开文件
O_WRNOLY 以只写的方式打开文件
O_APPEND 以追加的方式打开文件
O_RDWR 以读写的方式打开文件
O_CREAT 当目标文件不存在时创建文件

就如我们上面所说 如果想要多种选项只需要按位或就好了

比如说想要以读写的方式打开文件并且当目标文件不存在时创建文件

我们就可以使用下面的格式

  O_WRONLY | O_CREAT

open的第三个参数

它的第三个参数要求我们输入一个权限值

关于这部分的内容大家可以参考我的这篇博客

Linux权限的理解

注意: 如果我们不创建文件 则这个参数不需要填写

返回值讲解

open函数的返回值是一个整数 实际上这个整数就是一个文件描述符

  • 如果我们打开一个不存在的文件 open函数返回-1
  • 当我们打开一个文件之后open函数会返回给我们一个整数

我们下面来测试下上面的话对不对

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 
  6 int main()
  7 {
    
    
  8   int fd = open("log1.txt" , O_RDONLY );
  9   printf ("%d\n" , fd);                                  
 10   return 0;
 11 }

当我们打开一个不存在的文件的时候我们编译运行代码

在这里插入图片描述
我们可以发现如果打开一个不存在的文件 返回值确实是-1

接下来我们试验下连续打开多个文件

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 
  6 
  7 int main()
  8 {
    
    
  9   int fd1 = open("log1.txt" , O_RDWR | O_CREAT , 0666);
 10   int fd2 = open("log2.txt" , O_RDWR | O_CREAT , 0666);
 11   int fd3 = open("log3.txt" , O_RDWR | O_CREAT , 0666);
 12   int fd4 = open("log4.txt" , O_RDWR | O_CREAT , 0666);
 13   int fd5 = open("log5.txt" , O_RDWR | O_CREAT , 0666);
 14   int fd6 = open("log6.txt" , O_RDWR | O_CREAT , 0666);
 15   printf("%d\n",fd1);
 16   printf("%d\n",fd2);
 17   printf("%d\n",fd3);
 18   printf("%d\n",fd4);
 19   printf("%d\n",fd5);
 20   printf("%d\n",fd6);                                                                               
 21   return 0;
 22 }

编译运行之后查看下结果

在这里插入图片描述

运行之后我们发现打印的文件描述符是从3开始并且是连续的整数

看见这一堆数据之后我们脑海中会联想到什么?

是不是和数组的下标很像啊

实际上这里所谓的文件描述符本质上是一个指针数组的下标 指针数组当中的每一个指针都指向一个被打开文件的文件信息 通过对应文件的文件描述符就可以找到对应的文件信息

如果我们文件打开成功时 该数组中的指针个数便会增加 然后将该指针在数组中的下标返回

增加的规则是这样子的

如果前面的指针是从0开始连续的 那么新增的指针就会在原来的指针后面一个位置创建
如果前面的指针式从0开始不连续的 那么新增的指针就会找到缺失的第一个位置创建

如果我们的文件打开失败 则会返回-1 事实上数组下标也不存在-1

为什么文件描述符式从3开始的呢

还记不记得我们前面讲过 c语言程序会默认打开三个流 标准输入 标准输出 标准错误 实际上这三个流的底层就是打开了 0 1 2三个文件描述符

close

我们在系统接口层面使用close关闭文件 它的函数原型如下

   int close(int fd);

它有一个参数 填入我们要关闭的文件描述符

它的返回值式一个整型 如果关闭成功则返回0 如果关闭失败则返回1

write

我们在系统接口中使用write函数像文件中写入文件 它的函数原型如下

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

它的意思是像文件描述符为fd的文件中写入从buf开始count个字节的数据

参数

  • fd 我们要写入文件的文件描述符
  • buf 从buf这个位置开始读取数据写入
  • count 写入count个字节的数据

返回值

  • 如果数据写入成功 则返回实际写入的字节个数
  • 如果数据写入失败 则返回-1

代码示例

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <string.h>
  5 #include <fcntl.h>
  6 #include <unistd.h>
  7 
  8 int main()
  9 {
    
    
 10   // 打开文件
 11   int fd = open("log.txt",O_CREAT | O_RDWR , 0666);
 12 
 13   // 写入数据
 14   const char* tmp = "hello world\n";
 15   write(fd , tmp , strlen(tmp));
 16   close(fd);
 17   return 0;                                                                          
 18 }

这个代码的意思是 以读写的权限打开一个叫做log.txt的文件 如果这个文件不存在就创建它 设置初始权限为0666

之后我们像这个文件中写入tmp里面的数据

结果演示

在这里插入图片描述
我们发现数据确实被写入到了文件当中

read

我们在系统接口中使用read函数像文件中读取文件 它的函数原型如下

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

它的意思是从文件描述符为fd的文件中读取conut个字节的数据存放到buf中

参数

  • fd 我们要读取文件的文件描述符
  • buf 存放数据的地址
  • count 读取的字节数

返回值

  • 如果数据读取成功 实际读取数据的字节个数被返回
  • 如果数据读取失败 返回-1

代码示例

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <string.h>    
#include <fcntl.h>    
#include <unistd.h>    
    
    
int main()    
{
    
        
  // 打开一个文件    
  int fd = open("log.txt" , O_RDONLY);    
  if (fd < 0)    
  {
    
        
    perror("open");    
  }    
    
  char buf[20];    
  read(fd , buf , 20);                                                                                                         
    
  printf("%s",buf);    
  return 0;    
} 

我们这段代码中 使用只读的方式打开了log.txt文件

接着我们使用了一个缓冲区buf来接受文件中的内容

最后我们打印缓冲区buf里面的字符串

结果演示

在这里插入图片描述
我们发现文件中的数据被打印出来了

文件描述符fd

是什么?

文件描述符是进程中用来标识文件的一个无符号整数

为什么要有文件描述符?

文件是由进程运行时打开的 一个进程可以打开大量的文件 与此同时系统中还存在着大量的进程 所以说系统中可能存在着大量被打开的文件

所以说操作系统必须要对系统中存在着的大量的文件进行管理

根据先描述再组织的原则 操作系统为每个文件创建struct file结构体 用来描述这个文件的各项数据 在描述完毕之后使用双链表的数据结构将这些结构体组织起来 之后操作系统对于文件的管理也就变成了对这张双链表的增删查改

但是我们前面也说过 一个进程可能打开大量的文件 当然一个文件也可能被大量的进程打开 那么我们应该如何区别每个进程打开了什么文件呢?

我们都知道进程 = 程序 + PCB + mm_struct + 页表

在这里插入图片描述

而实际上PCB中还有一个指针 它指向名为files_struct的结构体

在files_struct中有一个叫做fd_array的指针数组 该指针数组的下标就是我们所说的文件描述符

在这里插入图片描述

我们我们运行进程 打开log.txt文件的时候 我们首先会将文件从磁盘加载到内存当中 并且形成对应的struct file结构体并且连入双链表中

然后在file_array中找到一个未被使用的空间 让这个空间指向我们刚刚的struct file结构体

最后将这个空间的下标返回给进程

所以说只要我们有一个文件的fd就能够对这个文件进行各种操作

为什么我们打开一个文件之后 默认最小的fd是3呢?

因为当我们c语言程序成为进程的时候 它就会占用0 1 2 三个文件描述符

其中0代表的是默认输入流 它对应的文件是键盘

1代表的是默认输出流 它对应的文件是显示器

2代表的是默认错误流 它对应的文件是显示器

扩展: 简单介绍下磁盘文件和内存文件

在磁盘中的文件就叫做磁盘文件 被加载到内存中的文件就叫做内存文件

磁盘中的文件天然有两部分组成 文件属性 + 文件数据
文件属性代表着文件的创建时间 文件大小 修改时间等信息
文件数据就是我们写入的各种数据
从这里我们可以知道 就算我们只创建了一个空文件 它也是占用空间的

当我们的磁盘文件加载到内存中时一般都是先加载文件属性 当我们需要进行读取数据 写入数据等操作时我们才开始加载文件的数据

文件描述符的分配规则

首先我们连续打开五个文件 看看分配的文件描述符是什么

测试代码如下

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 
  6 
  7 int main()
  8 {
    
    
  9   int fd1 = open("log1.txt" , O_RDWR | O_CREAT , 0666);
 10   int fd2 = open("log2.txt" , O_RDWR | O_CREAT , 0666);
 11   int fd3 = open("log3.txt" , O_RDWR | O_CREAT , 0666);
 12   int fd4 = open("log4.txt" , O_RDWR | O_CREAT , 0666);
 13   int fd5 = open("log5.txt" , O_RDWR | O_CREAT , 0666);
 14   int fd6 = open("log6.txt" , O_RDWR | O_CREAT , 0666);
 15   printf("%d\n",fd1);
 16   printf("%d\n",fd2);
 17   printf("%d\n",fd3);
 18   printf("%d\n",fd4);
 19   printf("%d\n",fd5);
 20   printf("%d\n",fd6);                                                                                                                
 21   return 0;
 22 }

结果如下
在这里插入图片描述
我们发现文件描述符是从3开始连续分配的

前面我们也讲过了 因为 0 1 2已经被系统的三个流占用了

所以说自然从3开始分配

那么如果我们关闭中间文件描述符为2的文件呢?

在这里插入图片描述
我们发现此时文件描述符从2开始连续递增了

那么如果我们只关闭文件描述符为1的文件呢?

在这里插入图片描述
我们发现此时文件描述符从0开始跳过了12 之后连续递增了

注意 我们这里不能关闭fd为1的文件 因为那个是标准输入流 关闭了显示器上就不能打印信息了

那么我们很简单就能总结出文件描述符的分配规则了

它是从file_array中找到未使用空间的最小下标开始分配

重定向

重定向是指将一个程序输出的内容从一个位置(例如终端)传递到另一个位置(例如文件或另一个程序的输入)的过程

重定向的原理

在理解了上面的文件描述符和分配规则之后我们对于重定向原理的理解应该是十分简单的一件事

首先我们给出下面的这样一段代码

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 
  9 int main()
 10 {
    
    
 11   close(1);
 12   int fd = open("log.txt" , O_CREAT | O_RDWR , 0666);
 13 
 14   int count = 5 ;
 15   while(count--)
 16   {
    
    
 17     fputs("hello world!\n" , stdout);
 18   }
 19   fflush(stdout);                                                                                                                               
 20   close(fd);
 21   return 0;
 22 }

我们首先将标准输出流关闭 之后打开一个log.txt的文件

紧接着向标准输出流中写入五句hello world

最后刷新缓冲区

我们编译运行程序

在这里插入图片描述
我们发现屏幕中什么都没有打印 反而是log.txt文件中有了五句hello world

这是为什么呢?

因为我们关闭了标准输入流 (1) 所以自然无法向屏幕中打印数据了

而根据文件描述符的分配规则 文件描述符会优先分配未被占用的下标最小的位置

而这个时候1就是最小的位置 所以说我们新打开文件的描述符fd就会等于1

这时候我们向标准输出流写入的数据都会写入到文件当中

这也就完成了我们的重定向

这也就是我们输出重定向的原理

在这里插入图片描述

输入重定向的原理和输出重定向类似 这里就不再赘述

stderr和stdout的区别

我们首先看下面的这段代码

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
    
    
 10   fputs("hello stderr\n", stderr);
 11   fputs("hello stdout\n", stdout);                                    
 12   return 0;
 13 }

我们这里分别使用stderr和stdout打印了数据

因为两个文件都是对应的显示器 所以当然会往显示器上打印这两句话

在这里插入图片描述
我们发现确实符合我们的预期

接下来看这一段指令

在这里插入图片描述
我们发现只有标准输入流被重定向了 标准错误流没有没重定向 还是打印到了屏幕中

当然这个也很好理解 因为我们重定向的时候是对于文件描述符为1的文件重定向 而sterr的文件描述符为2

这就是它们两者的区别

dup2

我们能不能在不关闭流的情况下进行重定向呢?

首先来看下面这个图
在这里插入图片描述
c语言在底层设计的之后就指定1为标准输出流

stdout就是向文件描述符为1的文件中输出数据

所以说我们只需要改变1指向的位置 让1指向需要重定向的文件就可以实现重定向了

在linux中提供了一个系统函数dup2来完成我们上面说的事情

它的函数原型如下

  int dup2(int oldfd, int newfd);

dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中

如果拷贝成功则会返回newfd 如果拷贝失败则会返回-1

我们下面直接使用代码来演示下dup2的作用

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
    
    
 10   int fd = open("log.txt" , O_RDWR);
 11   if (fd < 0)
 12   {
    
    
 13     perror("open");
 14   }
 15 
 16   dup2(fd,1);
 17   fputs("hello ! im dup2!\n",stdout);                                                                                                
 18   return 0;
 19 }

解释下上面的代码

首先先打开一个叫做log.txt的文件 接受它的文件描述符fd

接着我们将fd内的内容拷贝到stdout中

最后我们向stdout输出语句

我们查看编译运行后的结果

在这里插入图片描述
我们发现即使没有关闭流我们也成功的进行了重定向

FILE

FILE是C语言中处理文件输入输出的关键字,它是一个结构体类型,包含了与文件相关的信息和状态,例如文件指针、文件打开方式、缓冲区等。

我们在cplusplus官网中可以看到这几张图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
我们可以看到 c语言默认打开的三个流 而它们的数据类型全部是FILE*

而我们的底层全部是用文件描述符fd找到 sturuct file去对文件进行操作的

那么我们很轻松的就可以推断出FILE这个结构体中肯定包含了文件描述符fd

推断出了这一点之后我们再来理解下 c语言的文件操作函数是怎么执行的

下面拿fopen来举例

当我们调用fopen函数的时候 在底层fopen函数会调用系统结构open函数打开或创建对应的文件 并且获取到对应的文件描述符fd 将文件描述符fd填写到FILE结构体中的_fileno变量中 在上层返回给用户FILE*的指针

FILE缓冲区

我们首先来看下面的一段代码

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 
  5 
  6 int main()
  7 {
    
    
  8   // c
  9   printf("im printf\n");
 10   fputs("im fputs\n" , stdout);
 11 
 12   // linux
 13   const char* str = "im write\n";
 14   write(1 , str , strlen(str));
 15   fork();
 16   return 0;                                                                                           
 17 }

解释下上面的这段代码 我们分别调用了两个c语言的函数还有一个系统接口函数打印语句

在最后我们使用fork函数创建了一个子进程

接下来我们编译运行这段代码

在这里插入图片描述
正常打印了三条语句 没有问题

可是当我们重定向的时候便会发生一个奇怪的现象

在这里插入图片描述
我们使用c语言打印的两条语句竟然打印了两次 而系统结构打印的语句只打印了一次

解释这个奇怪的现象

我们知道这肯定跟fork函数创建的子进程有关 子进程和父进程会共享数据和代码 所以说会打印两份

可是为什么只有c语言的函数打印了两份

又为什么只有重定向的时候打印两次呢

为什么只有重定向的时候打印两次呢

要解释上面的两个问题我们首先要了解缓冲区以及缓冲区的刷新策略

缓冲区的刷新策略分为三种

  • 无缓冲 (立即刷新)
  • 行缓冲 (往显示器中打印数据)
  • 全缓冲 (对磁盘文件中写入数据)

所以说我们往显示器中打印的时候因为带上了换行符 所以会立即刷新数据

刷新数据之后缓冲区就没有数据了 所以说子进程不会打印任何数据

而我们重定向实际上就是往磁盘文件中写入数据 此时采取的刷新策略就是全缓冲 即缓冲区满了才刷新

所以说在创建子进程之后缓冲区中仍有数据 父子进程都会打印一份 这才造成了打印两次的现象

那么到这里为止我们解决了为什么只有重定向的时候打印两次的问题

为什么只有c语言的函数打印了两份

还记不记得我们上面FILE的定义

FILE是C语言中处理文件输入输出的关键字,它是一个结构体类型,包含了与文件相关的信息和状态,例如文件指针、文件打开方式、缓冲区等。

也就是说FILE这个结构体中包含缓冲区 这也是c语言层面的缓冲区

所以说只有c语言的函数才能享有这个缓冲区

那么操作系统也有缓冲区嘛?

答案是肯定的 操作系统也有自己的缓冲区

当我们刷新用户区的数据的时候实际上并不是将用户缓冲区的数据直接刷新到磁盘文件中而是刷新到操作系统的缓冲区中 之后经由操作系统的缓冲区将数据刷新到磁盘文件中

在这里插入图片描述

文件系统

我们在查看系统中的文件时 使用的是文件名来辨识它们

可是操作系统辨认这些文件也用的是文件名嘛?

答案是否定的 操作系统辨识文件使用的是 inode

inode

我们都知道 磁盘文件由文件属性+文件内容两部分组成

  • 文件属性: 文件的大小 创建时间等 文件属性又被称为元信息
  • 文件内容: 文件中存储的数据

我们在命令行中输入 ll 即可显示当前目录下各文件的属性信息

在这里插入图片描述
在linux操作系统中 文件的元信息和内容是分离存储的 其中保存元信息的结构称之为inode

linux系统下一切皆文件 所以说系统中势必会存在大量的inode 为了标识这些inode 我们给它们编号 称为inode号
在这里插入图片描述
我们ls -i命令即可查看文件的inode号

不论是文件属性还是文件内容都是储存在磁盘当中的

磁盘的概念

磁盘是一种永久性的储存介质 与它相对的是内存 内存是掉电易失存储介质

我们所有的普通文件都是在磁盘中存储的

在这里插入图片描述
在我们的冯诺依曼体系中 磁盘既可以当输入设备也可以当输出设备

磁盘的物理结构大概如下图
在这里插入图片描述
我们在磁盘中寻找一个文件的步骤大概如下

  1. 确定待寻找信息在哪个盘面
  2. 确定待寻找信息在哪个柱面
  3. 确定待寻找信息在哪个扇区

也就是说扇区是磁盘中储存信息的最小单位

磁盘的线性结构

我们要想要深入理解磁盘的结构 首先要把它想象成一个线性的状态

在一张磁盘中分为n个扇区

在这里插入图片描述

磁盘分区

管理一块很大的空间是很难的

就像管理国家一样 不可能一个领导班子就管理好整个国家

所以说我们会把国家划分为各个省 各个市 各个区 各个村 乃至各个街道去细分管理

划分的空间越小则越容易管理

而我们的磁盘分区则更简单 因为我们不用考虑区之间的差异性 只要将一个区分好就能将这套模板套用到各个区中

在linux操作系统中 我们可以使用该命令查看磁盘的分区

ls /dev/vda* -l

在这里插入图片描述

磁盘格式化

当我们的磁盘分区完毕之后便会进行格式化

这种操作通常会导致现有的磁盘或分区中所有的文件被清除

简单来说 磁盘格式化就是对分区后的各个区域写入对应的管理信息

在这里插入图片描述

linux中EXT2文件系统的存储方案

当磁盘分区分好之后我们可以对其内部继续细分

对于每一个分区来说 分区的头部会包括一个启动块(Boot Block) 对于该分区的其余区域 EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)

在这里插入图片描述

磁盘中的启动块是一个特殊的扇区,它包含了一些机器代码,用于在计算机开机时加载操作系统到内存中并执行 通常,磁盘的第一个扇区就是启动块,无论扇区大小和分区方式如何 。启动块对于操作系统的启动非常重要,如果它损坏或丢失,可能会导致无法正常开机 。我们可以使用Bootsect.exe工具来恢复或更新您的启动块 。

所以说我们可以在每个分区中都设置一个启动块来做一个备份的作用

而对于分区中每个启动块的块组 我们还可以继续细分

在这里插入图片描述

每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成

其中超级块和块组描述符表不太重要

  • 超级块 : 存放文件系统本身的结构信息 如inode总量等
  • 块组描述符表: 描述该分区当中块组的属性信息

剩下四个就是特别重要的内容了

  • 块位图 :记录着Data Block中哪个数据块已经被占用 哪个数据块没有被占用
  • inode位图:记录inode Table中inode的使用情况
  • inode表: 存放文件属性 即每个文件的inode
  • 数据块:存放文件内容

从上面的四个概念介绍我们应该能够更深的理解 文件属性和文件内容是分开存放的

我们如何理解创建一个空文件

  1. 首先我们在inode bitmap中找到一个未被使用的inode
  2. 在inode表当中找到对应的inode 并将文件的属性信息填写进inode结构中
  3. 将该文件的文件名和inode指针添加到目录文件的数据块中

如何理解对文件写入信息?

  1. 通过文件的inode编号找到对应的inode结构
  2. 通过inode结构找到存储该文件内容的数据块 并将数据写入数据块

如何理解删除一个文件?

  1. 将该文件对应的inode在inode图中设置为无效
  2. 将该文件inode对应的数据块在块位图中设置为无效

有没有发现 这个步骤中我们根本没有删除任何的文件 只是设置了两个位图

所以说数据被删除之后其实短时间之内是可以被恢复的 只要将两个位图重新设置就可以

但是时间一长 我们下载了其他的文件 该区域就会被覆盖 我们的文件也就再也没办法恢复了

为什么拷贝很慢 删除很快

拷贝要经过很多步骤

  1. 申请inode号 填入属性数据
  2. 申请数据块 跟inode连接
  3. 拷贝被拷贝文件中的内容

但是我们删除文件的时候就只需要将inode对应的位图设置为0就可以

如何理解根目录

linux系统下一切皆文件 当然根目录也不例外

既然是文件 那么它就有属性信息和内容数据

它的属性信息被储存在inode当中 包括大小 拥有者等

它的内容我们可以把他理解为文件名和inode指针一一对应的若干组数据

软硬连接

软连接

我们在linux系统下可以通过下面的指令来简历一个软连接

  ln -s makefile makefile2

我们可以使用ls -i指令来查看它们的inode号
在这里插入图片描述
我们可以发现 它的inode号是不同的

接着我们可以敲出 ll 指令

在这里插入图片描述
我们可以发现 源文件makefile的大小远远大于它的软连接

其实我们就可以把它想象成我们windows系统中的快捷方式

如果我们将源文件删除的话 那么它的软连接也无法使用了

在这里插入图片描述

硬链接

我们可以通过以下命令创建一个文件的硬连接

ln  makefile makefile2

我们可以输入 ll - i 命令来查看它的inode号和大小
在这里插入图片描述
我们发现它们的inode号竟然是一样的

也就是说其实它们指向的是同一个数据块 只要修改其中的一个数据另外一个数据也就被修改了 是不是很熟悉 这不就是我们C++中的引用嘛

在这里插入图片描述
我们修改了一个文件之后另外一个文件的大小也被我们改变了

此外我们ll之后可以在权限后面看到一个文件硬链接数

在这里插入图片描述

软硬连接的区别

  • 软连接一个独立的文件 有独立的inode和数据块 但是如果删除了源文件软连接则会失效
  • 硬链接的本质是引用 当所有的硬链接被删除的时候这个文件才会被删除

文件的三个时间

在Linux系统中我们可以使用stat指令来查看一个文件的对应信息

在这里插入图片描述
我们可以看到这里具有三个时间信息

简单介绍下它们

  • Access: 文件最后被访问的时间
  • Modify: 文件内容最后的修改时间
  • Change: 文件属性最后的修改时间

这里要注意的是如果我们修改文件的内容 文件的属性一般会跟着改变(文件大小属性改变)所以说我们修改完内容之后可以看到文件的内容修改时间和属性修改时间一致 但是我们修改文件属性却不会影响内容

此外我们还需要特别注意的一点是 access是并不是实时更新的 因为我们每时每刻都在访问者不同的文件 文件的访问时间是一个高频事件 如果它实时更新的话我们的系统就会变得卡顿 而另外两项由于不常改变 所以说它们的事件是实时更新的

文件时间和makefile编译的关系

一般来说我们make一次之后系统就不会让我们继续make了

在这里插入图片描述
此时我们只需要改变下源文件的Mondify便可以继续编译

在这里插入图片描述
这也是makefile决定是否进行make的原理

猜你喜欢

转载自blog.csdn.net/meihaoshy/article/details/129459925