Linux操作系统之基础IO

news2024/9/21 0:33:51

目录

系统IO调用接口

open

write

read

理解文件描述符fd

理解Linux操作系统的设计哲学,多态的思想是如何应用在Linux文件系统的

输出,追加,输入重定向的本质

子进程共享父进程的文件

IO的两个缓冲区

 Linux特有的EXT文件系统

磁盘系统的基本知识

概述

inode概念理解

inode编号

inode 编号

目录的inode

软链接和硬链接

 引用计数


我们在使用一个语言的时候,一定会使用过这个语言的IO函数,例如C语言的fopen,fclose,Java的System.println,Go语言的fmt.Println,C++的cin,cout等等。这些语言的输入输出函数以及对应的一些文件读写操作我们都可以称作为语言层面的IO函数。我们这里之后全部使用C语言的输入输出函数来举例子,因为C语言更加贴近于系统一点。

但是,虽然C语言很接近操作系统,但是它也同样停留在了我们的语言层面。

可以看到的是,我们的语言操作是用户层的,而我们要和OS进行沟通,因此必须要经过system call,系统调用层。因此我们语言层面上的IO函数全部是系统调用接口的封装。

那么,我们现在来看一看有哪些系统调用接口:

系统IO调用接口

open

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

 我们来介绍一下这个系统调用接口:第一个参数是pathname,它的意思是你要打开或者要创建的文件是什么,我们知道打开一个文件要知道它在哪里,还要知道文件名,所以这个传的就是文件的路径和文件名。第二个参数是flags,这个是一个标记位,用来记录文件打开的方式,我们一会儿详细来讲。第三个参数是mode,这个参数代表的是权限。

参数 pathname 指向欲打开的文件路径字符串. 下列是参数flags 所能使用的旗标:

flag是是一个标记位,我们在系统里面把一些常用的数位进行了define,例如

#define O_WRONLY 0x1   (0000 0001)
#define O_RDONLY 0x2   (0000 0010)
#define O_CREAT 0x4    (0000 0100)

我们第二个参数传入的一些东西,都是在系统里面都是一个只有一个比特位的数据,而且不重复,如上面的代码。我们在传入参数的时候,只需要利用位操作的特点,例如

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

我们使用 | 把两个数字合并在一起,而因为比特位1的位置是不重复的,所以我们可以根据flag的数值反推出我们哪些比特位上面有数字,从而弄清楚用户想要进行什么样的操作。

后面的权限操作请看Linux权限相关的文章,你就能明白0644是什么东西,这里不是重点。

write

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

函数说明:

write()会把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内. 当然, 文件读写位置也会随之移动.

返回值:

  1. 如果顺利write()会返回实际写入的字节数.
  2. 当有错误发生时则返回-1, 错误代码存入errno 中.

read

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

这里函数的说明和write几乎一样,只不过一个是读,一个写而已。

理解文件描述符fd

我相信大家看完这三个IO系统调用接口之后是没有理解的。哪里不好理解呢?read和write函数的fd参数是什么东西?

于是接下来我们引出一个话题,fd。

fd是read和write函数的第一个参数,同时也是open函数的返回值。

我们先通过程序来感受一下open的这个返回值。

  1 #include <stdio.h>                                                                                                                
  2 #include <unistd.h>
  3 #include <fcntl.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 
  7 int main()
  8 {
  9   int fd = open("./log.txt", O_CREAT | O_WRONLY, 0644);
 10   if (fd < 0)
 11   {
 12     perror("open");
 13     return 1;
 14   }
 15   else
 16   {
 17     printf("fd: %d", fd);
 18   }
 19   return 0;
 20 }

这里输出的答案是3,这个时候我们产生了一个疑问,为什么fd返回值是3,而不是从0开始呢?

这个时候我们就要开始从底层的逻辑开始梳理了:

我们所有的文件操作本质上都是进程执行了对应了函数,那么要对文件操作,执行对应的函数,我们就必须要打开文件,得到文件的属性信息,而我们的操作一定都是在内存中进行的,所以我们必须把文件加载到内存中去,因此一个进程在多次进行文件操作的时候,会打开多个文件,进程:文件数 = 1:n。那么多文件,操作系统就一定会把他们管理起来,我们可以知道,一个文件会对应抽象成一个file结构体。

如图,我们使用一个双向链表把文件管理起来。

struct file {
    // 文件的相关属性
    // 链接属性
    // ...
}

我们知道,文件系统是一个单独的系统,我们不同进程打开的所有文件都会被一个双向链表串起来进行管理。那么我们应该如何知道我们的文件对应的是哪个进程,或者说这个进程里面打开了哪些文件呢?

接下来请看这张图:

 我们可以看到我们的进程里面有一个指针叫struct file_struct *fs这个指针指向了一个叫struct file_struct的结构体,这个结构体里面有一个部分是一个指针数组,这个数组的名字叫struct file* fd_array[]。这个数组指向对应的struct file,当我的进行打开或者操作一个文件的时候,我们把struct file用双向链表管理起来的同时,这个file的地址就会被存放在我的fd_array数组里面。我们的进程有唯一的pid,因此可以唯一的找到进程,找到进程就可以找到我们的fd_array,而fd_array里面指向的就是文件系统里面的file的地址。

我们可以看到,这个fd_array是一个指针数组,它有它自己的下标,而这个下标其实就是我们的fd!!fd叫做文件描述符(file describtor)。我们通过fd可以找到的文件原理就是我们通过数组下标,通过file指针快速找到了文件。

于是问题迎刃而解了,为什么open的返回值是fd,我们打开了一个文件,当然要返回它存在在fd_array的哪个地方,以便于我们找到以及打开了的文件,而read和write的第一个参数也是fd,这样是为了找到这个文件,从而进行文件的读写。

那么问题又来了,我们刚才有一个问题没有解决:为什么fd是从3开始的。不是0,1,2。

当我们的程序运行起来变成进程之后,OS会默认的帮我们打开3个标准输入输出。分别是标准输入(用于键盘写入),标准输出(用于显示器显示),表示错误(也是用于显示器显示)。因为我们基本的输入,比如输入命令行指令,还有看到我们的命令行,以及我们出现的一些错误我们是必须看到的,所以任何一个进程在创建的时候都必须开启这3个标准输入输出。而Linux下一切皆文件!!!这3个标准输入输出也对应的有struct file。我们进程一创建就已经指向了这个struct file了。因此0,1,2已经被占用,数组从3开始继续进行存放。

理解Linux操作系统的设计哲学,多态的思想是如何应用在Linux文件系统的

Linux有一个很重要的设计哲学,就是一切都是文件!

我们外设硬件的目的是与操作系统进行互动,俗称就是IO,那么既然涉及到IO就涉及到read和write操作。这个时候出现了一个问题: 我们每一个外设的读写方式是不一样的,例如键盘:我们只能从键盘里面读,不能往键盘里面写,再例如显示器:我们只能写到显示器上面一些数据,但是不能从显示器上面读取数据。如果操作系统对每一种外设的读写方式进行记忆的话,那么代价是很大的,那么Linux操作系统是如何解决的呢?它利用了多态的思想。

这里先回顾一下多态的思想:多态,就是我的一个父类函数可以调用多个子类函数,父类函数指向的子类函数不同,最终得到的结果也会不同。说白了就是:一个引用调用同一个方法,产生的行为不一样。加入我们定义了一个父类,叫做animal,然后定义多个子类,比如cat,dog,rabbit,每一种动物都有不同的行为,我调用animal的方法,然后让animal指向某一个子类。这样就实现了我只需要调用animal,不同的动物种类会造成不同的行为方式,但是我不需要关心是哪种动物,我只需要调用动物的方法就可以得到对应的结果,一切都是动物。那么反观Linux文件系统的设计也是如此,它用函数指针实现了多态的思想。我的驱动层,把每一种外设的不同调用方法写好,然后虚拟文件系统的struct file里面有read和write的文件指针,我只需要对应的指向驱动层的方法,就不需要格外关注每一个外设的不同行为。驱动层进行了解耦和统一,它把不同外设的行为抽象成了read和write的函数,而且所有的外设都遵循同一个规则,因此文件系统在使用某一个外设的时候,就可以使用统一的方法来操作外设。就像是父类调用子类一样。一切都是文件,我的OS只需要关注文件,不需要关注不同外设的不同的行为方式。多么强大的设计思想!

我们来看一下相关的源码:

   struct file {
      mode_t f_mode;
      loff_t f_pos;
      unsigned short f_flags;
      unsigned short f_count;
      off_t f_reada;
      struct file *f_next, *f_prev;
      int f_owner;    /* pid or -pgrp where SIGIO should be sent */
      struct inode * f_inode;
      struct file_operations * f_op; //我们可以看到这里就是我的函数指针存放的位置
      unsigned long f_version;
      void *private_data;  /* needed for tty driver, and maybe others */
    };
struct file_operations { 
  struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES  
   loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置  
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据   
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据  
   ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作   
   ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作   
  int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL   
   unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入   
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令   
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl  
  long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替   
  int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间  
  int (*open) (struct inode *, struct file *); //打开   
  int (*flush) (struct file *, fl_owner_t id);   
  int (*release) (struct inode *, struct file *); //关闭   
  int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据   
  int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据   
  int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化   
  int (*lock) (struct file *, int, struct file_lock *);   
  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);   
  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);   
  int (*check_flags)(int);   
  int (*flock) (struct file *, int, struct file_lock *);  
  ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);  
  ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);   
  int (*setlease)(struct file *, long, struct file_lock **);   
};  

输出,追加,输入重定向的本质

在这之前,我们需要弄清楚OS制定的一个fd分配的规则。

文件描述符分配规则,给新文件分配的fd,是从fd_array里面找一个最小的,没有被使用的,作为新的fd。

 我们先来看一组代码:

 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <fcntl.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 
  7 int main()
  8 {
  9   close(1); //把标准输入流关掉,close的参数也是fd哦
 10   int fd = open("./log.txt", O_CREAT | O_WRONLY, 0644);
 11   printf("fd: %d", fd);                                   
 12   return 0;
 13 }

我们执行这个程序,理论上应该打印fd:3.但是我们却发现我们的显示器上面没有进行任何打印!

此时打开log.txt文件可以发现:

这句话被写入到了log.txt这个文件里面。这是为什么呢?

其实,printf等各语言的输入输出函数的本质是向stdout标准输出写入数据。而写入数据一定使用到我们的系统调用接口,因为操作系统不相信任何人,作为使用者我们必须使用操作系统给我们的系统调用接口,这也就是一种解耦合,我操作系统只认我的系统调用接口,其他的你再怎么封装我都是不管的。因此作为语言层的函数,本质上也是在使用系统调用接口,我把fd为1的标准输出的文件关闭了,而我们前面提到过Linux操作系统fd分配的规则。因此我们在打开文件的时候,这个文件对应的文件描述符是1。而我们说过printf本质上是在使用系统调用接口,它最本来的功能是往stdout,也就是fd为1的文件写入数据,而现在fd为1的文件是我们刚才打开的文件,因此printf顺利成章的在这个文件里面写入数据了。printf只认数字,它不会去判断这个文件是不是stdout。Linux操作系统处处体现了分工,体现了解耦!

因此我们就可以解释输出重定向了,输出重定向的本质就是我把fd为1的stdout标准文件输出关闭。然后把我的某一个文件打开,这样本该输出到屏幕上的数据就顺利成章的写入到文件内部了。

那么追加重定向的本质就是在open函数的flags参数后面多添加了一个标记位。

open("./log.txt", O_CREAT | O_WRONLT | O_APPEND);

同理,输入重定向的本质就是:

open("./log.txt", O_RDONLY);

子进程共享父进程的文件

现在我们有一个问题:如果我们的一个进程打开了很多文件,此时我们fork一次,那么此时的子进程会继承父进程的文件吗?

先说答案,答案是会继承,但是文件不会备份一份。

 我的所有东西都会继承自我的父进程,包括指针的指向。但是Linux的文件系统是单独的一个系统,是所有进程都可见的,所以即使进程fork了,文件不会再出现一份,只是指向某一些文件的指针增加了几份。

那么此时我们可以很清楚的解释为什么为什么所有的进程都自动打开了stdin,stdout,stderr。因为所有进程的父进程是bash,bash为了用户的正常使用一定会打开这三个东西,而其他继承自bash的子进程也自然而然的打开了。

IO的两个缓冲区

我们先需要看一组奇怪的现象:

  1 #include <stdio.h>  
  2 #include <unistd.h>  
  3 #include <fcntl.h>  
  4 #include <sys/types.h>  
  5 #include <sys/stat.h>  
  6   
  7 int main()  
  8 {  
  9   close(1);  
 10   int fd = open("./log.txt", O_CREAT | O_WRONLY, 0644);  
 11   printf("fd:%d\n", fd);  
 12   fprintf(stdout, "hello world\n");
 13   fprintf(stdout, "hello world\n");
 14   fprintf(stdout, "hello world\n");
 15   fprintf(stdout, "hello world\n");
 16   fprintf(stdout, "hello world\n");  
      close(fd);                     
 17   return 0;
 18 }
     

我们预测的结果是,我们会直接把这些信息写入到log.txt文件里面去。但实际情况是,我不仅屏幕上没有打印,而且log.txt文件里面也没有写入,数据完全丢失了!

这个close(fd)到底发生了什么从而导致了这样的结果。

在解决这个问题之前我们需要了解一下缓冲区的刷新策略:

1.立即刷新(不缓冲)

2.行刷新(行缓冲 \n),比如打印器打印

3.缓冲区满了,才进行刷新(全缓冲),你如往磁盘文件中写入

 我们的stdout这个概念是语言层的,而且是C语言,这个stdout里面封装了一个文件结构体FILE,里面存放有我们之前讲过的fd,用于指名我们的stdout对应的文件描述符号是多少,于此同时,这个结构体里面也维护了几个指针,这几个指针就是我们所谓的C语言层缓冲区。

 我们通过这张图可以看到,实际上不仅是OS给我们提供了内核缓冲区,我们的语言层实际上也给我们提供了缓冲区,为什么语言层也给我们提供了缓冲区呢?这件事情其实非常容易理解,因为从用户态到内核态的切换是很花费时间和系统资源的。我们为了避免频繁的用户态和内核态的切换,我们需要在语言层也提供一个缓冲区,这样一次性的可以传输多份数据到内核缓冲区。

那么我们这个时候再解释一下上面的缓冲区策略。如果我们直接使用系统调用接口,例如write写入数据,那么我们就不会经过语言层面的缓冲区,不缓冲直接写入OS内核缓冲区,这个叫不缓冲。我们使用语言层面的函数,例如printf等,遇到换行符的时候,语言层的缓冲区才会刷新到OS缓冲区,这个叫做行缓冲。那么最后,全缓冲就是我的语言层的缓冲区饱满了才会进行刷新,我们向文件中写入数据的时候采用的策略是全缓冲。

现在我们就可以解释为什么close(fd)之后数据完全丢失的问题了:

首先我们一开始close(1),并且open了文件,也就是向文件里面写入数据,这个时候刷新策略从行缓冲变成了全缓冲。我们写入的数据还停留在C语言缓冲区里面,而这么一点数据缓冲区是肯定无法被撑爆的,所以一直等到我们close(fd)并且return 0了之后,进程结束了,进程结束了就意味着缓冲区里面的内容会强制进行刷新,确实,我们现在缓冲区里面的数据已经刷新到内核缓冲区了。但是我们的内核缓冲区隔一段时间会按照fd,来向磁盘里面相应的文件里面写入数据,而此时我已经close(fd)了,而且进程也已经关闭了,我们OS缓冲区里面的数据没有去向,最终只能被丢弃。

我们的解决方法就是使用fflush进行手动刷新。强制的在close(fd)之前把数据刷新到OS内核,OS内核会在close(fd)之前把数据写入磁盘。

C语言类似fclose这样的操作其实就是进行了fflush在关闭之前强制刷新缓冲区。

其次就是,如果我们直接使用系统调用接口的话,其实是不存在这样的问题的,因为系统调用接口在层次结构在用户层下面,直接刷新到了内核缓冲区。

这样做的结果是hello 标准输出打印一份,其他的打印两份原因是:

如果我们这样做的话,使用一个fork,那么子进程也同样会继承父进程的缓冲区里面的内容!!从而打印出两份内容。

这个原因的本质是发生了写时拷贝,刷新策略发生了变换

我们之前已经讲解过了,这些东西会保存在c语言提供的buffer里面,因为重定向使刷新策略发生改变,所以变成全缓冲了之后没有刷新,等我们fork的时候,子进程是会完全按照父进程的来进行的,所以这个时候子进程和父进程的缓冲区里面都有同样的buffer,然后return 0之后一起被刷新出来,是2份。

 Linux特有的EXT文件系统

我们知道文件 = 文件属性 + 文件内容。就算这个文件的内容是空的,这个文件也同样的会占用内存。文件打开的时候当然是放在内存里面,不然无法进行文件操作。而文件不打开的时候,是放在磁盘里面的。因此为了更好的理解EXT文件系统,我们先要对磁盘有一个认识。

磁盘系统的基本知识

概述

1. 盘片( platter
2. 磁头( head
3. 磁道( track
4. 扇区( sector
5. 柱面( cylinder
盘片 片面 和 磁头
硬盘中一般会有多个盘片组成,每个盘片包含两个面,每个盘面都对应地有一个读 / 写磁头。受到硬盘整体体积和生产成本的限制,盘
片数量都受到限制,一般都在 5 片以内。盘片的编号自下向上从 0 开始,如最下边的盘片有 0 面和 1 面,再上一个盘片就编号为 2 面和 3 面。
扇区 和 磁道
下图显示的是一个盘面,盘面中一圈圈灰色同心圆为一条条磁道,从圆心向外画直线,可以将磁
划分为若干个弧段,每个磁道上一个弧段被称之为一个扇区(图践绿色部分)。扇区是磁盘的最小组成单元,通常是512 字节。(由于不断提高磁盘的大小,部分厂商设定 每个扇区的大小是4096 字节)

磁头 和 柱面
硬盘通常由重叠的一组盘片构成,每个盘面都被划分为数目相等的磁道,并从外缘的 “0” 开始编号,具有相同编号的磁道形成一个圆柱,称之为磁盘的柱面。磁盘的柱面数与一个盘面上的磁道数是相等的。由于每个盘面都有自己的磁头,因此,盘面数等于总的磁头数。 

磁盘容量计算
存储容量 = 磁头数 × 磁道 ( 柱面 ) × 每道扇区数 × 每扇区字节数
3 中磁盘是一个 3 个圆盘 6 个磁头, 7 个柱面(每个盘片 7 个磁道) 的磁盘,图 3 中每条磁道有 12 个扇区,所以此磁盘的容量为:
存储容量 6 * 7 * 12 * 512 = 258048
每个磁道的扇区数一样是说的老的硬盘,外圈的密度小,内圈的密度大,每圈可存储的数据量是一样的。新的硬盘数据的密度都一致,
这样磁道的周长越长,扇区就越多,存储的数据量就越大。
磁盘读取响应时间
  1. 寻道时间:磁头从开始移动到数据所在磁道所需要的时间,寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般在315ms,一般都在10ms左右。
  2. 旋转延迟:盘片旋转将请求数据所在扇区移至读写磁头下方所需要的时间,旋转延迟取决于磁盘转速。普通硬盘一般都是7200rpm,慢的5400rpm
  3. 数据传输时间:完成传输所请求的数据所需要的时间。
小结一下:从上面的指标来看、其实最重要的、或者说、我们最关心的应该只有两个:寻道时间;旋转延迟。
读写一次磁盘信息所需的时间可分解为:寻道时间、延迟时间、传输时间。为提高磁盘传输效率,软件应着重考虑减少寻道时间和延迟
时间。
/
磁盘块 / 簇(虚拟出来的)。 块是操作系统中最小的逻辑存储单位。操作系统与磁盘打交道的最小单位是磁盘块。
通俗的来讲,在 Windows 下如 NTFS 等文件系统中叫做簇;在 Linux 下如 Ext4 等文件系统中叫做块( block )。每个簇或者块可以包括 2、 4 8 16 32 64…2 n 次方个扇区。
为什么存在磁盘块?
读取方便:由于扇区的数量比较小,数目众多在寻址时比较困难,所以操作系统就将相邻的扇区组合在一起,形成一个块,再对块进行整体的操作。
分离对底层的依赖:操作系统忽略对底层物理存储结构的设计。通过虚拟出来磁盘块的概念,在系统中认为块是最小的单位。解耦解耦解耦!!!
page
操作系统经常与内存和硬盘这两种存储设备进行通信,类似于 的概念,都需要一种虚拟的基本单位。所以,与内存操作,是虚拟一
个页的概念来作为最小单位。与硬盘打交道,就是以块为最小单位。
扇区、块 / 簇、 page 的关系
1. 扇区: 硬盘的最小读写单元
2. / 簇: 是操作系统针对硬盘读写的最小单元
3. page : 是内存与操作系统之间操作的最小单元。
扇区 <= / <= page

了解过了磁盘之后,我们可以顺带想象一下磁带这个东西,其实早期我们使用的磁带和磁盘的功能是差不多的。我们可以把磁盘想象成磁道,把磁盘的一个一个磁道拉长成一个巨大的数组。

 每一个扇区我们都可以认为是数组的一个元素。这个巨大的数组的下标我们成为LBA(Logic Block Adress),我们的操作系统往磁盘里面写数据的时候只认LBA这个下标,因为这样会减少操作系统的工作量,而将LBA转换成具体的磁盘的地址有一套算法。这一套算法可以把LBA下标转换成第几盘片,第几磁道,第几扇区。然后就可以写入和读取数据了。

而我们现在知道数据是怎么从磁盘里面读取的了,但是有一个问题就是:这个数据太过于巨大了,我们很难以管理。就像一个国家一样,国家太大了很难管理,因此我们把国家分成很多省份,如果我们可以把每一个省份都管理好的话,那么国家自然就管理好了,因此我们这个时候对磁盘进行分区。

这个分区就有点儿像windows操作系统的C,D,E,F等磁盘。

每一个区域的特点都不一样,就跟国家的省份的地方风情是不一样的一个道理。

接下来我们的任务就转换成了管理巨大数据里面的一个小分区,我们接下来来看看这个小分区:

 可以看到每一个大分区里面有一个Boot Block和若干个Block Group,而Block Group仍然太大了,因此我们必须再次进行细分。

接下来我们逐一的介绍部分分区名称的功能:

Boot Block:它存放的是与启动相关的东西。例如我的操作系统其实也是文件,我系统怎么知道我操作系统的文件是放在哪里的,这就是Boot Block里面有个指针指向了操作系统启动的位置。

Super Block:描述的是整个分区的一些信息,例如这个分区里面有多少个Block Group,使用率是多少等等。我们每一个Block Group都有一个Super Block,因为我们需要备份,如果一个Block Group里面的Super Block损坏了,我们会损失很多信息,导致文件系统报废,而如果每一个Group里面都有Super Block的话,我们就可以进行备份了,不怕被损坏。

Block Descriptor Table:这个存放的是本Block Group的一些相关信息,例如还有多少inode和block可以使用。

可以看到,我们刚才提出了一个概念,叫做inode。接下来我们需要了解一下inode的概念:

inode概念理解

我们注意到两个字段:inode Table和DataBlocks。inode Table里面存放的是文件的属性,DataBlocks里面存放的是文件的内容,这两块空间里面有很多不同的零碎小块儿。inode Table里面存放文件属性的小块儿叫做inode(512字节),DataBlocks里面存放文件内容的小块儿叫做block(4kb)。每一个inode和block小块儿都有自己的编号。而且要么把一阵个小块儿给你,要么不给你,取整的。

我们创建一个文件的时候,会把文件属性放在一个空的inode里面,把文件内容放在一个空的block里面。

但是由此,我们会产生几个问题:

问题一:是我们创建了一个文件之后,系统是如何标识一个文件的,是通过文件名吗?

答案是否定的,文件名只是给用户使用的,系统一定不会使用文件名来标识一个文件,系统通过唯一的inode编号来标识一个文件,什么叫inode编号,我们inode Table这一块空间里面的零碎空间从0-n依次有自己的编号,而且每一个inode都对应着一个编号,系统通过这个inode编号来标识一个文件。

问题二:我的inode存放的是文件的属性,那么文件的inode是如何与我的内容相挂钩的呢?

实际上inode内部有一个结构体:

struct inode {
    int I_ino; //这个代表的是我的inode编号
    int block[32]; //这个数组里面存放的是我的block编号
    // ...
}

这个是inode的大概的结构体。

这个是完整的结构体:
 

struct inode {
    umode_t         i_mode;//文件的访问权限(eg:rwxrwxrwx)
    unsigned short      i_opflags;
    kuid_t          i_uid;//inode拥有者id
    kgid_t          i_gid;//inode拥有者组id
    unsigned int        i_flags;//inode标志,可以是S_SYNC,S_NOATIME,S_DIRSYNC等

#ifdef CONFIG_FS_POSIX_ACL
    struct posix_acl    *i_acl;
    struct posix_acl    *i_default_acl;
#endif

    const struct inode_operations   *i_op;//inode操作
    struct super_block  *i_sb;//所属的超级快
    /*
        address_space并不代表某个地址空间,而是用于描述页高速缓存中的页面的一个文件对应一个address_space,一个address_space与一个偏移量能够确定一个一个也高速缓存中的页面。i_mapping通常指向i_data,不过两者是有区别的,i_mapping表示应该向谁请求页面,i_data表示被改inode读写的页面。
    */
    struct address_space    *i_mapping;

#ifdef CONFIG_SECURITY
    void            *i_security;
#endif

    /* Stat data, not accessed from path walking */
    unsigned long       i_ino;//inode号
    /*
     * Filesystems may only read i_nlink directly.  They shall use the
     * following functions for modification:
     *
     *    (set|clear|inc|drop)_nlink
     *    inode_(inc|dec)_link_count
     */
    union {
        const unsigned int i_nlink;//硬链接个数
        unsigned int __i_nlink;
    };
    dev_t           i_rdev;//如果inode代表设备,i_rdev表示该设备的设备号
    loff_t          i_size;//文件大小
    struct timespec     i_atime;//最近一次访问文件的时间
    struct timespec     i_mtime;//最近一次修改文件的时间
    struct timespec     i_ctime;//最近一次修改inode的时间
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes;//文件中位于最后一个块的字节数
    unsigned int        i_blkbits;//以bit为单位的块的大小
    blkcnt_t        i_blocks;//文件使用块的数目

#ifdef __NEED_I_SIZE_ORDERED
    seqcount_t      i_size_seqcount;//对i_size进行串行计数
#endif

    /* Misc */
    unsigned long       i_state;//inode状态,可以是I_NEW,I_LOCK,I_FREEING等
    struct mutex        i_mutex;//保护inode的互斥锁

    //inode第一次为脏的时间 以jiffies为单位
    unsigned long       dirtied_when;   /* jiffies of first dirtying */

    struct hlist_node   i_hash;//散列表
    struct list_head    i_wb_list;  /* backing dev IO list */
    struct list_head    i_lru;      /* inode LRU list */
    struct list_head    i_sb_list;//超级块链表
    union {
        struct hlist_head   i_dentry;//所有引用该inode的目录项形成的链表
        struct rcu_head     i_rcu;
    };
    u64         i_version;//版本号 inode每次修改后递增
    atomic_t        i_count;//引用计数
    atomic_t        i_dio_count;
    atomic_t        i_writecount;//记录有多少个进程以可写的方式打开此文件
    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    struct file_lock    *i_flock;//文件锁链表
    struct address_space    i_data;
#ifdef CONFIG_QUOTA
    struct dquot        *i_dquot[MAXQUOTAS];//inode磁盘限额
#endif
    /*
        公用同一个驱动的设备形成链表,比如字符设备,在open时,会根据i_rdev字段查找相应的驱动程序,并使i_cdev字段指向找到的cdev,然后inode添加到struct cdev中的list字段形成的链表中
    */
    struct list_head    i_devices;,
    union {
        struct pipe_inode_info  *i_pipe;//如果文件是一个管道则使用i_pipe
        struct block_device *i_bdev;//如果文件是一个块设备则使用i_bdev
        struct cdev     *i_cdev;//如果文件是一个字符设备这使用i_cdev
    };

    __u32           i_generation;

#ifdef CONFIG_FSNOTIFY
   //目录通知事件掩码
    __u32           i_fsnotify_mask; /* all events this inode cares about */
    struct hlist_head   i_fsnotify_marks;
#endif

#ifdef CONFIG_IMA
    atomic_t        i_readcount; /* struct files open RO */
#endif
    //存储文件系统或者设备的私有信息
    void            *i_private; /* fs or device private pointer */
};

注意:inode结构体里面没有存放文件名这个信息,这代表着文件名对于inode来说根本不重要,只是给用户来看的。

我们最好还是看上面那个简略的结构体比较好。

inode是以一个类似数组的方式(用数组好理解一点,实际上可能是链表,因为空间很难是连续的)存储的空间里面的,数组的下标就是inode编号,我们的结构体里面也有相应的变量进行记录,然后我们还有一个int block[32],我们知道,inode有编号,那么相应的block也会有编号,我们把文件内容放在block里面,然后把对应的数组下标放在这个数组里面。这样就可以通过inode找到所有的文件内容了。可能有认会问32个位置的下标够吗?这个问题很好,确实可能不够,但是如果真的不够的话,系统会用链表进行一些操作,这个大家不用担心。

回顾一下inode编号的概念

inode编号

inode 编号

每一个 inode 都有一个编号,系统根据 inode 编号可以快速的计算出 inode 信息在磁盘 inodes 存储区的偏移,然后从中获取 inode 信息,再根据 inode信息中记录的 Block 块位置,从Block存储区读出文件内容

inode 编号在一个文件系统中是唯一的,多个文件系统之间可能会出现相同的编号,前面的磁盘存储结构示意图中 /dev/vda1/dev/vda2 在各自的文件系统中 inode 编号是唯一的

创建一个新文件的时候,文件名和对应的 inode 编号会存储在目录文件的Block块中(关于目录文件后面会讲到)

文件的 inode 信息中记录了文件 Block 块的位置,Block块中存储着文件的内容,可以使用 ls -i 命令查看文件的 inode 编号

那么接下来我们再次出现了下一个问题:

我们怎么知道哪些inode被用了以及哪些block被用了。这就是字段inode BitMap和block BitMap的作用了。

这两个字段是位图结构,通过比特位来记录某一个位置是否被占用。比特位是1就代表被占用,比特位为0就代表没有被占用。而第一位就对应的是第一个inode编号,依次类推。

这样,我们只需要遍历一遍比特位图,找到第一个没有被使用的inode或者block,然后就可以在里面存放数据了。

因此,总结一下:我们查找一个文件的步骤的是

  1. 找到inode编号
  2. 找到对应的inode
  3. 通过inode找到对应文件内容

好了问题解决了,不对,等一下,我们应该怎么找到inode编号呢!!

目录的inode

我们知道,我们的目录也是一个文件,因为Linux下一切都是文件,而是文件就有inode,所以我们的目录也是有自己的inode的,而有inode里面就存放了文件内容,而目录的文件内容是什么东西呢?

目录数据块里面存放的是文件名和inode编号的映射关系

输入命令ls -ali就可以看到inode编号和文件名的对应关系!

而当我们工作的时候,一定是在某一个目录下的,我们找一个文件,运行某一个程序也必须在相应 目录下解决。因此我们找一个文件的时候,例如cat test.c,其实是cat ./test.c,只是系统默认你在当前目录所以把./给省略了,也就是当我们cat的时候,实际上是要从文件系统里面找到test.c这个文件,./test.c代表的是我的目录是当前目录,系统从当前的目录中寻找到test.c的文件名对应的inode编号,然后通过inode编号找到inode结构体,通过inode结构体找到对应的文件内容,然后cat到显示器上面。

我们要查看一个文件,运行一个文件就必须要找到一个路径,这是必须的

我们找到一个文件,就必须首先找到目录里面存放的inode和文件名的映射关系,但是我们要找到目录里面的内容,就必须要先找到目录的inode,这个怎么解决呢?

其实,我们的目录里面是有两个隐藏文件的,一个是.  一个是.. 。我们的.代表的是当前的目录,有对应的inode编号,因为我们允许cat ../test.c这样查看上一个目录的文件的操作,因此我们也需要有上一级目录的inode的信息,因此我们也有..这个文件。这就是目录里面有.和..的原因了!!

然后就是如果我们是拷贝文件的话,那么要把文件写进去,但是如果是删除文件的话我们只需要把位图里面的1写成0代表没有用过就可以了,所以拷贝很慢,删除很快。

在windows里面我们把东西删除到回收站里面,实际上根本就没有删除,把回收站想象成一个目录,我们只是把一个文件放到了回收站这个目录里面,本质上根本就没有删除掉。

软链接和硬链接

在有了上面的知识基础之后,我们再理解软硬链接就变得十分容易了。

我们制作软硬链接用到的命令是:

ln -s log.txt log_s //软链接
ln log.txt hard //硬链接

 软链接是用来干嘛的,软链接其实就是windows里面的快捷方式,我们为了避免调用某一个程序需要寻找路径很长时间,所以就创建了一个快捷方式。

我们使用log_s就相当于启动了log.txt,但是我们可以很清楚的看到log_s的inode编号和log.txt的inode编号是明显不一样的。这说明软链接有自己的inode。保存的是指向文件的所在路径 + 文件名 。

而硬链接是没有自己独立的inode的。他们的inode编号是完全一样的。

硬链接本质上根本就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode!

所以创建硬链接本质上做了什么呢??? 本质是在特定的目录下,填写一对文件名和inode的映射关系。仅仅是对目录的inode的文件内容做操作而已。

如果是软链接的话,我把源头删除掉就炸了,如果是硬链接的话,我把源头删了实际上还有一份。

这个时候出现了一个问题:

我创建一个文件,默认的硬链接数是1,但是创建一个目录,默认是硬链接数是2,这是怎么回事儿呢?

原因很简单,我们之前说过目录隐藏文件的问题。

 我这里有一个dir和inode的对应关系,进入dir之后还有一个.和inode的对应关系,所以是2个。因为我们有隐藏文件..,所以如果我们在dir里面再次创建一个目录temp的话,那么就会成3个硬连接数,因为我的temp里面有一个..指向的是你这个dir。

 引用计数

这一排其实就是我们的硬链接数。

struct inode {
    int ref; //这个变量用来存储我当前文件有多少个硬链接,增加一个硬链接这个inode结构体里面的ref就++,减少一个就--,当减少到0的时候,说明这个文件已经没有硬链接,就代表你可以消失掉了。
}

 硬链接代表的是有多少文件名和你有关联,如果你的硬链接数为0的话,就代表没有文件名和你有关联了,于是就可以删除了,反之,只要有一个硬链接,你就不能删除文件,因为要有用处。

好了,这就是基础IO的全部内容。大家看懂了嘛?

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

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

相关文章

代码训练营第二十天|530.二叉搜索树的最小绝对差 ● 501.二叉搜索树中的众数 ● 236. 二叉树的最近公共祖先

530 .二叉搜索树的最小绝对差 看完题后的思路 因为是二叉搜索树&#xff0c;所以直接按照二叉搜索树中序遍历&#xff0c;得到递增序列。遍历过程中一个指针指向遍历过的前一个元素 prenull&#xff1b; void f&#xff08;root&#xff09;if rootnull return递归 f&#x…

git语义化定制版本规范

目录说明说明 语义化版本控制规范,语义化的版本控制规范要求版本号由三部分构成&#xff1a;x.y.z MAJOR&#xff08;X&#xff09;&#xff1a;这个是主版本号&#xff0c;一般是涉及到不兼容的 API 更改时&#xff0c;这个会变化。MINOR&#xff08;Y&#xff09;&#xff…

剑指Offer pow() 函数实现(快速幂)!!!

剑指 Offer 16. 数值的整数次方 实现 pow(x, n) &#xff0c;即计算 x 的 n 次幂函数&#xff08;即&#xff0c;xn&#xff09;。不得使用库函数&#xff0c;同时不需要考虑大数问题。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示…

早已有所耳闻的堆排序,你知道如何用C语言实现吗? 【堆排序|C语言版】

目录 0.写在前面 1.什么是堆&#xff1f; 2. 堆排序 2.1 建堆 2.1.1 AdjustUp&#xff08;向上调整算法&#xff09; 2.1.2 AdjustDown&#xff08;向下调整算法&#xff09; 2.2 两种建堆算法的时间复杂度 2.2.1 AdjustUp建堆的时间复杂度 2.2.2 AdjustDown建堆的时间…

神经网络(模型)量化介绍 - PTQ 和 QAT

神经网络&#xff08;模型&#xff09;量化介绍 - PTQ 和 QAT1. 需求目的2. 量化简介3. 三种量化模式3.1 Dynamic Quantization - 动态量化3.2 Post-Training Static Quantization - 训练后静态量化3.3 Quantization Aware Training - 量化感知训练4. PTQ 和 QAT 简介5. 设备和…

Flutter 小技巧之 3.7 性能优化background isolate

Flutter 3.7 的 background isolate 绝对是一大惊喜&#xff0c;尽管它在 release note 里被一笔带过 &#xff0c;但是某种程度上它可以说是 3.7 里最实用的存在&#xff1a;因为使用简单&#xff0c;提升又直观。 Background isolate YYDS 前言 我们知道 Dart 里可以通过新建…

CODESYS开发教程9-文件读写(CAA File库)

今天继续我们的小白教程&#xff0c;老鸟就不要在这浪费时间了&#x1f60a;。 前面一期我们介绍了CODESYS的定时器及触发相关的功能块。这一期主要介绍CODESYS的CAA.File库中的目录和文件读写功能块&#xff0c;主要包括文件路径、名称、大小的获取以及文件的创建、打开、读、…

软测(概念) · 软件测试的基本概念 · 什么是需求 · 测试用例的概念 · 软件错误(bug)的概念

一、什么是软件测试软件测试和开发的区别测试和调试的区别一个优秀的软件测试人员具备的素质二、什么是需求从测试人员角度看待需求三、测试用例的概念四、软件错误&#xff08;bug&#xff09;的概念一、什么是软件测试 最常见的解释是&#xff1a;软件测试就是找 BUG&#x…

个人博客美化

总体参考&#xff1a; Butterfly 文档&#xff1a;https://butterfly.js.organzhiyu &#xff1a;https://anzhiy.cn张洪 Heo &#xff1a;https://blog.zhheo.comLeonus &#xff1a;https://blog.leonus.cn 注&#xff1a;博客所有美化大部分&#xff08;全部&#xff09;都参…

React项目实战之租房app项目(九)登录模块基础布局和功能实现

前言 目录前言一、房屋详情模块二、登录模块2.1 登录模块效果图2.2 基础布局2.3 调用接口实现登录2.4 实现表单验证功能2.4.1 formik介绍2.4.2 formik基本使用2.4.3 添加表单验证2.5 代码优化总结一、房屋详情模块 房屋详情模块主要是展示之前获取到的房源信息&#xff0c;由于…

为防护加码,飞凌嵌入式i.MX93系列开发板让通信安全又稳定

来源&#xff1a;飞凌嵌入式官网www.forlinx.com随着新基建的加快推进&#xff0c;智能制造迎来了更好的发展时机&#xff0c;嵌入式板卡等智能设备也在更多的应用场景中大放异彩。但随着现场的设备数量的剧增&#xff0c;环境中的各种干扰信号也随之增加&#xff0c;这就对设备…

windows下GitHub的SSH key配置

SSH Key 是一种方法来确定受信任的计算机&#xff0c;从而实现免密码登录。 Git是分布式的代码管理工具&#xff0c;远程的代码管理是基于SSH的&#xff0c;所以要使用远程的Git则需要SSH的配置。 下面的步骤将完成 生成SSH密钥 并 添加公共密钥到GitHub上的帐户 先设置GitHub…

Apifox接口测试工具详细解析

最近发现一款接口测试工具--apifox&#xff0c;我我们很难将它描述为一款接口管理工具 或 接口自测试工具。 官方给了一个简单的公式&#xff0c;更能说明apifox可以做什么。 Apifox Postman Swagger Mock JMeter Apifox的特点&#xff1a; 接口文档定义&#xff1a; Apif…

接口测试学习第二天

1、全局变量 概念&#xff1a;在postman全局生效的变量&#xff0c;全局唯一。设置&#xff1a; 代码设置&#xff1a;pm.globals.set("glb_age",100)//示例&#xff1a; pm.globals.set("glb_age",100) 获取&#xff1a; 代码获取&#xff1a;var 接收值…

Java的内部类详解(成员内部类、静态内部类、局部内部类、匿名内部类)

Java知识点总结&#xff1a;想看的可以从这里进入 目录2.2.4、 内部类1、成员内部类2、静态内部类3、局部内部类4、匿名内部类2.2.4、 内部类 一个类定义在另一个类内&#xff0c;那么这个类就是一个内部类&#xff0c;比如&#xff1a;在类A中定义一个类B&#xff0c;B就是内…

英特尔锐炫秒杀RTX 3060,XeSS现已支持超过35款游戏!

一款显卡的性能可以达到什么程度&#xff1f;除了架构、规格等硬件因素&#xff0c;驱动的优化程度同样至关重要。Intel携带Arc锐炫回归独立显卡市场&#xff0c;作为“后起之秀”&#xff0c;驱动的优劣更是关键中的关键。Intel也正是这么做的。2022年6月&#xff0c;Intel正式…

2023 NFT防骗指南:六大骗局,3招带你远离…

网上流传着一句&#xff1a;币圈一天&#xff0c;人间一年。在刚刚过去的农历新年&#xff0c;一直低迷的加密领域迎来了“短暂性复苏”&#xff0c;加密市场总市值重回万亿美元。 同时复苏的还有NFT市场&#xff0c;据欧科云链OKLink链上数据显示&#xff0c;2023年1月份的NFT…

计算机网络-http协议版本对比

概述 HTTP 是基于 TCP/IP 协议的一个应用层协议&#xff0c;是现代互联网的一个基础协议。规定了客户端与服务端之间的通信格式以及所占用的服务端口80(HTTPS是443)。 超文本传输协议&#xff08;Hyper Text Transfer Protocol&#xff0c;HTTP&#xff09;是一个简单的请求-响…

【Flutter】Flutter Developer 101 入门小册 专栏指引

你好&#xff0c;我是小雨青年&#xff0c;一名程序员。 在2023年&#xff0c;我决定做这个Flutter专栏&#xff0c;从基础到部署&#xff0c;一站式解决大家对于Fulltter的学习需求。 目前本专栏的大概目录为本文最后所示&#xff0c;后续随着内容的不断更新&#xff0c;会逐…

2023年“华数杯”国际大学生数学建模B题赛题发布

ICM 问题B&#xff1a;社会稳定早期预警研究 背景 人类和所有的动物一样&#xff0c;都有寻求利益和避免伤害的本能。人类成为创造之主 的关键在于&#xff0c;他们比其他动物更善于避免伤害。危机总是潜伏着未来。人类发展的 历史是一部不断尝试超越危机的历史 (严耀军&#x…