文件系统(内存上的 + 磁盘上的)

基础I/O

0 . 预备

文件是什么?

文件 = 文件内容 + 文件属性。

所以对于文件的学习,就是要学习文件的内容以及属性。

怎么才能访问文件?

文件存放在磁盘上,只有os能够访问,所以如果用户想要自己访问文件,那么os必须提供文件接口。

C语言等语言中的文件操作是系统提供的文件接口吗?

不是,语言接口是对系统提供的文件接口的封装。不同的语言由于语法的不同,文件操作也会不同,所以这并不是系统接口。

学习系统接口的意义

语言接口有多套,系统接口只有一套,学一套会十套。

不同的系统,系统接口相同吗?

显然不同,所以,如果我们自己写代码直接使用系统接口,那么这段代码的可移植性是不好的。C语言具有可移植性是因为C语言源代码将所有平台代码都实现了一遍,你用啥平台,他就放啥代码。

Linux认为一切皆文件。

狭义的文件就只是仅仅指文件,但是Linux认为只要能够写入,能够输出,即具有 I/O属性的设备都叫做文件。

0.1 C语言接口

fopen

fclose

fscanf

fprintf

C语言文件接口分为两部分,第一是打开文件的方式,第二是对文件进行的操作。

打开文件的方式有很多种

打开代码意义若指定的文件不存在
r(只读)打开一个已经存在的文件进行读取出错
w(只写)打开一个文件进行写入新建一个文件
a(追加append)打开一个已存在文件并在末尾进行添加出错

目前只列这么多种,还可以进行组合,也可以添加一个“+”增加一个属性。

0.2 系统接口

上面的所有C语言接口都是封装系统调用接口实现的。

我们要学的系统调用接口有四个

open:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XpB3CUMh-1672026572674)(E:\比特\笔记\文件系统.assets\image-20221208164451327.png)]

open函数返回一个int类型的值,叫做 fd, 也就是我们的文件描述符,若fd为-1表示打开失败,否则成功。

fd 的值为 0, 1, 2分别代表标准输入,输出和错误。

C语言上的stdin, stdout和stderror是对 fd做封装的结构体。

open函数有两种函数原型。这两个函数的区别是是否具有mode这个参数,mode代表文件的权限,mode和之前的chmod一样,通过给八进制数来设置权限,比如说mode设为 0666 ,就是rw-rw-rw-,但是同时也要考虑umask的影响,可以通过umask这个系统接口将umask的初始值设为 0.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nhxX8PaC-1672026572676)(E:\比特\笔记\文件系统.assets\image-20221208203552520.png)]

open函数的第二个参数是 flag,代表操作选项,从文档中可以知道open有

O_RDONLY(只读), O_WRONLY(只写), O_RDWR(读+写), O_TRUNC(如果指定文件存在,就清零,不存在,就创建新的空文件)等等选项,这些选项的组合是通过 ‘或 ‘操作实现的,比如想要实现只读 + 创建新文件或清零就这样写

 umask(0); //设置umask
 int fd = open("hello.txt", O_RDONLY | O_TRUNC | O_CREAT, 0666);

close

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMoRdN2J-1672026572676)(E:\比特\笔记\文件系统.assets\image-20221208204505239.png)]

close的用法很简单,直接将fd传进去就好,

read

从文件中读出数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgmj6pJ0-1672026572677)(E:\比特\笔记\文件系统.assets\image-20221209120459792.png)]

函数参数:

fd : 表示打开文件的文件描述符。

buf:表示将要读入的数据结构

count:表示要读入的个数

返回值:

返回值的类型是 ssize_t,这是一个正整形,表示的是实际读入的字符个数。

write

1 . 文件描述符 (fd)

在上面的fd就是我们要讲的文件描述符,为什么要有文件描述符,文件描述符是什么呢?

一个进程可以打开多个文件,打开的文件多如牛毛,os为了将其管理起来,也要“先描述,再组织”。

实际上os用了指针数组的方式来管理文件存放的指针指向一个个文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awGMEy09-1672026572677)(E:\比特\笔记\文件系统.assets\image-20221209115652539.png)]

files_struct中存储着一个个指向已经打开的文件,所谓的文件描述符就是这个指针数组的下标。

1.1 fd的分配规则

fd的分配规则是分配最小的,且没有被打开的文件描述符。

比如如果0, 1, 2,如果被stdin,stdout和stderr占用了,那么新打开的文件就会被安排上后面的数字中的最小值。

当然,如果将0,1,2中的任意一个close掉,比如close(0),后面的新加入内存的文件就会被分配0(因为此时0是最小的,且没有被打开)。

  //狸猫换太子
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/stat.h>
  4 #include <sys/types.h>
  5 #include <string.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   umask(0);
 11   close(1);
 12   int fd = open("login.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
 13   if (fd < 0)
 14   {
 15     perror("open");
 16     return 1;
 17   }
 18 
 19   printf("%d\n", fd);
 20   fflush(stdout); //如果不刷新缓冲区就不能写入我们的文件
 21   close(fd);                                                                                                          22   return 0;
 23 }

这段代码把stdout关闭了,fd就被分配到stdout的位置,而printf和fprintf都是向stdout内写入信息,自然就写入到了login.txt中。也就是说file_struct这个指针数组中下标为1的位置存放的指针实际上指向的不是显示器,而是我们自己的文件。而这里也就是我们的输出重定向的原理。

要注意的是必须加上fflush或者注释close(fd),不然写入不到我们的文件中,原因后面讲。

重定向

在os内部,更改fd对应的内容的指向,就可以实现。

上面讲的仅仅是重定向的原理,实际上我们没必要使用重定向的时候每次都要close原来的fd。在Linux中有一系例如系统调用接口:dup,其中最常用的是dup2.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UajKlv4v-1672026572678)(E:\比特\笔记\文件系统.assets\image-20221209162544152.png)]

dup2函数原型如下

int dup2(int oldfd, int newfd);

通过dup2函数将file_struct中的oldfd下标所在的指针更换成newfd下表所在的指针,依次来实现重定向的功能。

代码示例

   #include <stdio.h> 
   #include <unistd.h>
   #include <sys/stat.h>
   #include <sys/types.h>
   #include <string.h>
   #include <fcntl.h> 
                      
   int main(int argc, char* argv[])
   {                                    
   	if (argc != 2)	return 2;                   
    umask(0);        
    //重定向接口     
    int fd = open("login.txt", O_WRONLY | O_CREAT | O_A    PPEND, 0666);
                     
    dup2(fd, 1);//让1中内容和fd中内容一致
    printf("fd:%d\n", fd)                              
    printf("%s\n", argv[1]);
    return 0;
   }

//代码输出结果:

//fd:3
//if_the_swap_success?

由上述代码输出结果可以看出,dup2并不会改变oldfd的值,仅仅是改变了newfd的值,所以可以的话,尽量手动关闭oldfd。

2. 一切皆文件

之前一直说Linux下一切皆文件,但是为什么这么说呢?之前一直都只是感性的认识,凭什么这么说呢?

思考一个问题:用C语言如何实现面向对象方法 ?

在C++的类中不仅能包含成员变量,还能够包含成员函数,而在C语言中,可以使用函数指针这样的方式来完成一个面向对象的结构。

struct
{
  	//成员变量
    int (*read)(int n, void *buf);//函数指针,指向方法
    int (*write)(int n, void *buf);
};

而我们说一切皆文件,也就是把键盘,显示器,磁盘,网卡等外设全部一视同仁了,但是这些设备他的物理结构就是不一样的呀,所以他们的访问方式必须是不一样的,那为什么还说一切皆文件呢?

答案就在于上面的struct,尽管你们的读写方式是不同的,但是我同样的可以用一个struct来描述你,只不过函数指针指向你特定的函数,所以在上层看来,所有的外设都一样,都可以用这个struct来描述。同时struct的数量多起来后,也是需要“先描述,再组织”的。

所以才说,Linux下 “一切皆文件”。

3. 缓冲区

缓冲区其实本质上就是一段内存空间,缓冲区存在的意义就是提高效率,他属于是拒绝了一个一个数据刷新的策略,是积攒一些数据后再刷新。

因此缓冲区其实很像快递。而缓冲区的刷新策略和快递的发货策略也很相像

  • 立即刷新

  • 行刷新(刷新\n之前的所有数据)

  • 满刷新数据满了之后再刷新

  • 特殊情况:

    1.用户强制刷新:fflush

    2.进程退出

一般而言 行缓冲的的设备文件 – 显示器

全缓冲的设备文件 — 磁盘文件

但是,所有的设备永远都倾向于全缓冲,因为这样可以有效减少IO操作(IO操作太耗费时间)。

一个例子:

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include <unistd.h>
  7 
  8 
  9 int main()
 10 {
 11   //C语言提供接口
 12   printf("%s", "hello printf\n");
 13   fprintf(stdout, "%s", "hello fprintf\n");
 14 
 15   //os提供接口
 16   const char* s = "hello write\n";
 17   write(1, s, strlen(s));
 18 
 19   fork();                                        
 20   return 0;
 21 }

这段代码在运行之后向显示器上打印,那么结果是这样

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGbJP4Wu-1672026572678)(E:\比特\笔记\文件系统.assets\image-20221209185747629.png)]

如果有fork函数,结果则截然不同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5G8wCvk-1672026572679)(E:\比特\笔记\文件系统.assets\image-20221209185814010.png)]

可以看到,os提供的接口没有重复打印,但是C语言提供的接口却重复打印了

还记得之前说的那个close为什么要加fflush或者注释close的疑问吗?

之前代码再粘贴一下

  //狸猫换太子
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/stat.h>
  4 #include <sys/types.h>
  5 #include <string.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   umask(0);
 11   close(1);
 12   int fd = open("login.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
 13   if (fd < 0)
 14   {
 15     perror("open");
 16     return 1;
 17   }
 18 
 19   printf("%d\n", fd);
 20   fflush(stdout); //如果不刷新缓冲区就不能写入我们的文件
 21   close(fd);                                                                                                          22   return 0;
 23 }

这个时候调用的是printf函数,向stdout的缓冲区中写入了对应的数据,但是此时stdout对应的是磁盘文件,虽然写入了‘\n’,但是仍然不会刷新,当使用close将文件刷新后,stdout的缓冲区数据就无法刷新了。因此文件中没有内容。

从上述现象中可以知道,缓冲区绝对不是os提供的,而是C标准库提供的,因为如果是os提供的,那么系统接口应该也被重复。

第一种结果是向显示器中打印,显示器采用的是行刷新的策略,所以在fork执行的时候,缓冲区已经完全刷新了,所以此时并不会出现重复。

第二种结果中我们进行了输出重定向,向磁盘文件上写入,而磁盘文件是满刷新,fork执行时,明显缓冲区还不会刷新,所以此时如果父子进程退出,就会进行缓冲区刷新,缓冲区刷新要进行写时拷贝,同时由于缓冲区而由于缓冲区是C语言的规定,所以只有C语言的接口受限制,os的接口并不受控制。

缓冲区在哪?

事实上,在之前提到的每个文件都具有的文件结构体file_struct中,每次fputs,fprintf啥的都是先写入到缓冲区中、根据不同的缓冲刷新策略进行刷新。

stderr —— 2 的辨析

1 和 2对应的都是显示器文件,但是它俩是不同的,如同认为一个显示器文件被打开了两次。做一个代码测试

由代码测试可以知道,我们对1做重定向并不会影响2,所以其实1和2是独立的。

perror:perror函数会根据全局的错误码自动输出对应的错误信息。

errno:全局变量,表示错误码。包含在头文件errno.h中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtXMSqLP-1672026572679)(E:\比特\笔记\文件系统.assets\image-20221212213217417.png)]

strerr:错误信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HfeWwGjH-1672026572680)(E:\比特\笔记\文件系统.assets\image-20221212213133150.png)]

4. 没有被打开的文件 —— 磁盘文件

学习目标

  1. 如何对磁盘文件进行分门别类的存储,用来支持更好的存取
  2. 了解磁盘

4.1 磁盘结构

4.1.1 磁盘的物理结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBj4o2Xy-1672026572680)(E:\比特\笔记\文件系统.assets\20200330190841250.png)]

盘面:俯视看磁盘好像只有一个面,但是侧面观看就会发现一个磁盘具有许许多多的磁片。盘面看起来是光滑的,但实际上他是由许许多多个小磁铁组成的(因为磁铁具有二面性,符合计算机的0,1属性),磁头通过改变磁铁的南北极朝向写入数据。

磁头: 上图的 Read / write heads,磁头和盘面之间有一定的距离,属于是悬浮在盘面的上空,通过静电效应改变磁铁朝向。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c2ydn0GL-1672026572680)(E:\比特\笔记\文件系统.assets\v2-7ef4dd23b909affc4c5529bad4b0564b_r.jpg)]

磁道:磁道指上图的一个同心圆,每一个同心圆被称为一组磁道,数据就存放在磁道上。(所以并不是整个盘面都能存放数据)

扇区:磁道上的一段弧段被称为扇区,磁盘的读写以扇区为基本单位。扇区的大小为512字节。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JBPYvcum-1672026572681)(E:\比特\笔记\文件系统.assets\20191109164411288.png)]

柱面:柱面就是以一个磁道为底面,所有磁片的厚度为高度的一个柱形。

如何寻找到一个扇区进行写入?

chs方式:

  1. 找到是哪一个面(磁头)
  2. 找到是哪一个磁道(柱面)
  3. 找到是哪一个扇区

通过这样的方式可以找到磁盘上的任意一个扇区进行写入。

4.1.2 磁盘的逻辑结构:

磁带示例:一卷磁带扯长开来就是一个线性结构,一条长带

–> 将磁盘盘片抽象成线性结构 --> 数组。(LBA)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yyv94txT-1672026572681)(E:\比特\笔记\文件系统.assets\image-20221213115120883.png)]

找到扇区的位置 --> 找到数组中对应的位置

对磁盘的管理 --> 对磁盘数组的管理

将磁盘进行划分 --> 对磁盘数组的划分

对磁盘的管理 --> 对磁盘数组小分区的管理

因此只要管理好一个个小分区,整个磁盘就能够管理好。

4.2 磁盘的管理

4.2.1 磁盘区间划分管理

接下来要讲的就是关于小分区的管理,如下图所示,一个大分区被分成一个个小分区,这些小分区又被分成更小的分区,只要管理好这些小分区,其他的分区都采用类似的方法进行管理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYP8uqE4-1672026572682)(E:\比特\笔记\文件系统.assets\image-20221213101250127.png)]

上述的一个Block_Group就是一个文件系统。

虽然磁盘以扇区(512字节)为基本单位,但是os和磁盘的IO操作的基本单位是4KB, 这是因为512字节太小,会增加IO操作次数降低效率。而且如果os的基本单位和磁盘一样,那就是强耦合,因为如果基本单位一样的话那么磁盘基本单位一变,os也要跟着变,硬件和软件联系紧密,强耦合。

Boot block :存放启动信息

Super blocks:存放的是整个分区,即上图的**第一个矩形(包括所有的Block group和Boot block)**的文件系统属性信息,比如有多少个块组,每个块组的使用情况,但是为甚么整块的信息要放到一个分区内呢?这是因为要防止丢失数据,磁头容易刮花磁片,如果一个分区的Super Blocks损坏可以用另一个区域的Super Blocks进行拷贝恢复。并不是每一个分组都有,前几个会有。

Data blocks:存放对应文件的内容,基本单位是bolck,一个block大小为4KB(8 * 512字节), 也叫做块。

inode table:存放对应文件的属性,基本单位是inode,由一个个的ionode组成table,一个inode的大小是128字节,存放文件的inode编号,文件属性,存放当前文件的块组的编号等内容。

Block Bitmap : 用来管理Data Blocks,假设Data Blocks中有n个块,那么 Bitmap中就有n个比特位,这些比特位是1就表示该block被占用,是0表示可使用。

inode Bitmap:和Block Bitmap类似,用来管理inode Bitmap,如果有n个inode,那么Bitmap中就有n个比特位,是1表示该inode空间已经被占用,0表示可使用

Group Describer Table:存储inode和block的描述信息,例如: 总共有多少个inode,目前的使用情况;总共有多少个block,目前的使用情况。

一个文件对应着一个inode,一个inode编号。

但是,文件大部分不可能只有4kb,因此,一个文件绝大多数情况下不可能只占用一个block块,那么该如何找到该文件对应的内容呢?

在inode中就存储了这个文件占用的块数组的编号

struct inode
{
	int _inode;//inode编号
	//...文件描述信息
	int _blocks[20]; //存储对应文件占用的Block的编号
};

所以只要找到inode编号就能够访问文件属性以及内容了。

但是还有一个问题,如果文件特别大呢?

其实Data block中也不是所有的data block只能存文件数据,也可以存其他块的编号,要知道一个block有4KB,能存储的指向文件的指针的数量就非常可观了。

4.2.2 inode

如何知道inode呢?

在Linux的inode属性中,是没有文件名这一内容的!!

那么文件名去哪了呢?

答案是在上级目录的data block中!!!

目录的内容,也就是目录的data blocks中存放的是文件名 和 inode 的映射关系,在一个目录下文件名是唯一的,而同时在一个文件系统中inode也是唯一的,所以他们互为Key值。

创建一个文件,文件系统做的事

touch test.txt

首先在inode Bitmap中遍历找到第一个为0的inode位,置为1.对Block Bitmap做同样的事情,如有必要对blocks进行数据写入。根据目录的inode找到目录的Data Blocks,将这个文件inode编号以及文件名写入目录的Data Blocks。

删除一个文件,文件系统所做的事

rm -f test.txt

根据文件名,在上级目录中找到自己的inode,根据inode找到对应的inode bitmap和block bitmap,将该文件对应的位图置为0。然后在目录的data blocks中文件名和inode的映射关系去掉。

查看一个文件,文件系统所做的事

cat test.txt

inode, data blocks所占用的空间是固定的

所以就会导致还有空间但是无法创建文件。

5. 软硬链接

软硬链接通过 ln 命令执行

#软链接
ln -s testLink soft.link
#硬链接
ln testLink1 hard.link

软硬链接的本质区别:

是否具有独立的inode,软链接具有独立的inode,硬链接没有。

所以软链接是一个独立的文件,而硬链接不是。

软链接:

有一个独立的文件,上述指令生成一个链接文件testLink --> soft.Link,这个testLink实际上指向的是soft.Link的路径,执行testLink相当于执行soft.Link,所以软链接相当于一个快捷方式。

硬链接:

没有独立的文件,硬链接就是在指定的目录的Data Blocks中,写入硬链接文件名和指定的文件的inode的映射关系。硬链接文件和原文件共用一个inode。硬链接相当于起别名。

在属性中有这么一个数字,他在删除文件前是2,删除文件后是1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2R2wS2cl-1672026572683)(E:\比特\笔记\文件系统.assets\image-20221215165556277.png)]

​ 删除前

rm -f test

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4R7FEpO-1672026572683)(E:\比特\笔记\文件系统.assets\image-20221215165813830.png)]

​ 删除后

这个数字叫做硬链接数。

前面讲过的是一个文件只有一个inode,inode是唯一的,这里要完善一下说法:inode对于文件是唯一的,但是一个inode可以对应若干个文件。

在inode的结构中,有一个计数器,用来计算使用这个inode编号的文件数,这个数就是硬链接数。所以其实我们删除一个文件其实是对这个计数器减减,当硬连接数为0时,这个inode空间就被标为可用了。

unlink 系统调用

unlink soft.link
unlink hard.link

unlink 清零对应文件的硬链接数。

默认创建目录,硬链接数默认是2,这是因为在目录中存在两个隐藏文件: . 和 … 。

[外链图片转存中…(img-2R2wS2cl-1672026572683)]

​ 删除前

rm -f test

[外链图片转存中…(img-S4R7FEpO-1672026572683)]

​ 删除后

这个数字叫做硬链接数。

前面讲过的是一个文件只有一个inode,inode是唯一的,这里要完善一下说法:inode对于文件是唯一的,但是一个inode可以对应若干个文件。

在inode的结构中,有一个计数器,用来计算使用这个inode编号的文件数,这个数就是硬链接数。所以其实我们删除一个文件其实是对这个计数器减减,当硬连接数为0时,这个inode空间就被标为可用了。

unlink 系统调用

unlink soft.link
unlink hard.link

unlink 清零对应文件的硬链接数。

默认创建目录,硬链接数默认是2,这是因为在目录中存在两个隐藏文件: . 和 … 。

. 表示当前目录,当然也就是当前目录的别名,所以 . 是当前目录的一个硬链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值