概述
文件 == 内容 + 属性
文件的内容就是在文件中写入的数据
要怎么理解 “ 属性 ” 一词呢?如果有一个苹果,怎么才能认识到它是一个苹果呢,我们很难说清一个苹果的本质到底是什么,但可以通过它的形状,色泽口感来判断这是一个苹果。这就是一个苹果的属性,一个事物的属性可以帮助我们认识 “ 它 ” 是什么
文件的属性能帮我们认识一个文件
文件的基本属性可以用 ll 指令查看
一个文件的基本属性:文件类型,权限,硬链接数,拥有者,所属组,文件内容大小,最近一次被修改的时间
当我们只创建了文件,并未在文件中写入数据,该文件的大小也不为 0 。该文件的相关属性也会被保存到磁盘中。
文件会被谁访问呢?
答案是进程,进程创建文件,进程打开文件,进程对文件做写入。
认清文件的各种操作本质是认清进程和文件的关系。进程和文件之间是怎么联系起来的呢?
文件保存在哪里呢?
答案是磁盘,磁盘的物理结构是怎样的?怎么做到从从硬件层到软件层的抽象化呢?
文件是怎么被管理的呢?
答案是文件系统,什么是文件系统呢?它是怎么把文件管理起来的呢?
上述问题是该文章的主框架。此外,还会谈一谈周边问题:重定向原理,缓冲区,内存管理(基本认知),目录
回答了上述问题就可以谈一谈应用层的数据如何通过操作系统写入磁盘的
注:本文旨在让读者对文件有进一步的认知,打消对文件的零认知。并未涉及对文件系统的专业讲解以及对linux文件系统源码级理解。
文件描述符
进程要打开某个文件时,操作系统会在内存中创建file结构体来维护文件的属性。上文已经提到属性的概念。
一个进程能打开多个文件,所以进程和文件的对应关系是一对多
所以一个 task_struct(进程控制块,用来描述进程属性的结构体)可以对应多个file结构体。
在task_struct中有一个指针指向一个files_struct的数组,而数组中存的是的file*类型的指针,这样就实现了一个进程对应多个文件。
files_struct数组的下标就是文件描述符
所有的操作系统提供的接口(系统调用)中只认文件描述符
有关文件系统调用接口(用法可用man手册查看):open close read write lseek
语言层对文件操作的函数和系统提供的文件接口关系如下
语言层提供的函数必须封装文件描述符和系统调用
0 1 2 文件描述符
操作系统会默认打开三个流:标准输入 标准输出 标准错误
标准输入对应键盘文件
标准输出和标准错误对应显示器文件
0号文件描述符代表代表键盘文件,1号和2号文件描述符代表显示器文件
在files_struct数组中,0下标存的file*的指针指向键盘文件的file结构体,而1下标和2下标的的file*的指针指向显示器文件的file结构体
1号和2号文件描述符都指向显示器文件。在file结构体中会维护一个引用计数,每多一个文件描述符指向自己引用计数加加,反之减减。引用计数为 0 就释放该结构体资源。
文件描述符分配规则
在files_struct数组中找最小并且未被使用的文件描述符,分配给新的file*指针。
重定向原理概述
如果关掉一号文件描述符,根据文件描述符分配规则,1号文件描述符会被分配给新创建的文件。但上层接口不知道1号描述符已经不指向显示器文件,但该接口依然会向新的文件输出。对显示器文件输出数据变成对其他文件输出数据,这种现象成为输出重定向。
如果0号描述符被改变指向,本来该往显示器文件读取数据转为从其他文件读取数据,这种现象成为输入重定向。
如下是输出重定向代码示例
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> int main() { close(1); int fd = open("myfile", O_WRONLY|O_CREAT, 00644); if(fd < 0){ perror("open"); return 1; } printf("fd: %d\n", fd); fflush(stdout); close(fd); exit(0); }
dup2系统调用
头文件:#include <unistd.h>
函数原型:int dup2(int oldfd, int newfd);
功能:把oldfd文件描述符的file*指针拷贝给newfd文件描述符的file*指针,可以不用关闭文件描述符也能完成输入,输出重定向
代码示例
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("./log", O_CREAT | O_RDWR); if (fd < 0) { perror("open"); return 1; } close(1); dup2(fd, 1); for (;;) { char buf[1024] = {0}; ssize_t read_size = read(0, buf, sizeof(buf) - 1); if (read_size < 0) { perror("read"); break; } printf("%s", buf); fflush(stdout); } return 0; }
缓冲区
缓冲区刷新策略:
1.直接刷新 2.按行刷新 3.缓冲区满了在刷新 4.进程退出时强制刷新
用户级缓冲区
在C语言中,对文件操作时都绕不开一个东西——FILE
FILE是一个结构体,里面封装了文件描述符和用户缓冲区的相关的属性。
可以在/usr/include/stdio.h查看,typedef struct _IO_FILE FILE
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags //缓冲区相关 /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; //封装的文件描述符 #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
内核缓冲区
用户缓冲区是由进程维护的一块空间,该缓冲区不能直接与外设交互,需要刷新到内核缓冲区,内核缓冲区与外设交互的行为叫做IO。
如果通过函数向外设输出数据,该数据需要经过用户缓冲区和内核缓冲区。如果通过系统调用向外设输出信息,该数据会被直接刷到内核缓冲区。
缓冲区的好处
用户缓冲区:提高函数的效率,只需把数据交给用户缓冲区即可返回。
内核缓冲区:提高了效率,因为内核可以直接与硬件交互,而无需频繁地在用户和内核空间之间切换
认识磁盘
磁盘的重要的两个结构是盘片和磁头,盘片上有磁性物质,可以记录而进程数据。磁头用来对二进制数据做存取。
盘面上可以划分为多个磁道,每个磁道又可以划分为多个扇区,,扇区是硬盘的最小的存储单元,一般是存储512字节(byte)
磁盘不仅是外设,而且还是机器设备。相同数据量下磁头移动的次数越少磁盘的效率越高。
磁盘的寻址
对磁盘上的数据做存取,首先要定位哪个盘面,然后定位哪个磁道,然后在定位哪个扇区
通过确定盘面,磁道,扇区来定位磁盘上的数据,这种寻址方式称为CHS寻址。
磁盘的抽象化理解
如果把所有磁道展开成一条直线,每个扇面在这条直线上均匀分布。像数组一般,每个扇面都有唯一的编号。这种编号的方式称为 LBA寻址
LBA地址=(柱面编号×硬盘磁头的总数+磁头编号)×扇区数+扇区编号-1
扇区数为每磁道的扇区数
文件系统
已经可以通过LBA地址访问磁盘中的数据,那么怎么管理这些数据呢
磁盘大小
台式机和笔记本电脑中使用的传统机械硬盘的容量范围很广,从几百GB到数十TB不等。常见的容量包括1 TB、2 TB、4 TB、8 TB乃至更大
磁盘的分区
指的是将一个物理磁盘的存储空间逻辑地划分为多个独立的部分。每个这样的部分被称为一个分区,它在操作系统中表现为一个单独的驱动器或卷。分区允许用户组织数据,例如,可以将操作系统、应用程序和用户数据分别存储在不同的分区上,这样即使某个分区出现问题,其他分区的数据仍然可以保持安全
举个简单的例子,在Windows系统下,所谓的C盘,D盘,E盘等就属于磁盘的分区
block
Linux ext2文件系统,磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。 只需管理好每一个block就可以管理好整个磁盘的数据了。
怎么管理好一个block呢
介绍
Super Block:超级块,存放文件系统本身的结构信息。
记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
Group Descriptor Table:块组描述符,描述块组属性信息
Block Bitmap:Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode Bitmap:每个bit表示一个inode是否空闲可用
inode Table:节点表,存放文件属性 如 文件大小,所有者,最近修改时间等
Data blocks :数据区,存放文件内容
详解
Data blocks中存放的是文件的内容,以 八个扇区为一个数据块(大小是4KB),每个数据块都有自己的编号。
inode Table是一张节点表,每个节点对应一个文件的属性。每一个文件属性只对应一个inode,系统会为每个inode分配一个唯一的编号。
文件内容和文件属性是分开管理的。那么怎么通过inode找到在Data blocks 中的数据呢?
在inode结构体中不仅会维护文件的属性,还会维护一个数组。数组的大小为15。数组中存的是Data blocks中的数据块编号,0到11下标的编号为一级索引,12下标和13下标的编号为二级索引,14号下标的编号为三级索引。
一级索引:对应的数据块的内容就是文件的内容
二级索引:对应的数据块的内容是一级索引的数据块的编号
三级索引:对应的数据块的内容是二级索引的数据块的编号
如下是简单的示意图,细节部分并不准确,仅供参考
在Block Bitmap中用比特位映射的方式(位图)来代表哪一个数据块是否被使用,1代表使用,0代表未使用
在inode Bitmap中用比特位映射的方式(位图)来代表哪一个inode是否被使用,1代表使用,0代表未使用
Super Block用来记录整个分区文件系统的情况,它不会在每一个block中保存但会有备份
删除文件
在计算机中,删除 == 可以被覆盖
把一个文件删除时,并不会对文件对应的数据块做写入,也就是不会对文件的内容做清空。操作系统只会把Block Bitmap和inode Bitmap中位图的由 1 置 0 即可,所以一个被删除的文件是可以被恢复出来的,因为文件的内容并未被清空。
创建文件
核心工作如下:
在inode表中找到找空闲的inode,在这个inode中填入文件的属性
在数据块中写入 (如果有写入需求)
把位图中相关的信息由0 置 1
格式化
格式化的工作是把文件系统中 Super Block ,Group Descriptor Table,Block Bitmap ,inode Bitmap , inode Table,的相关字段恢复为初始状态 。但中Data blocks的数据块不会被写入,所以,即使整个磁盘被格式化了,其数据也可以恢复出来。(理论上哦~ 代价比较大)
重新认知目录
目录文件的数据块中存的是文件名和inode编号的映射。
linux中不会根据一个文件名查找一个文件,只会通过inode编号查找这个文件。
但我们只给了系统路径和文件名,它是怎么找到该文件的呢?
想要找到当前目录下的文件,就需要知道当前目录和inode编号的映射关系,因此就必须读取当前目录的数据块。那就需要找到当前目录的inode,因此就必须读取上级目录的数据块,因此就必须知道上级目录的inode,以此类推到根目录,根目录的文件名和inode的关系是确定的,就能找到根目录,并读取根目录的数据块,然后就能找到下级目录的inode,进而读取下级目录的数据块,一直向下推,直到找到目标文件的inode编号。
这样的递归式的寻找会有效率上的问题,所以系统会把常用的目录缓存起来,这个缓存叫dentry缓冲
操作系统会把相关文件系统的字段加载到内存,用用结构体维护起来,然后用链表维护起来,这样做是为了加速查找,不然查找文件的时候就需要先从磁盘读取,速度太慢。
上层数据如何流动到磁盘中
调用printf向显示器文件输出数据。数据会被刷到用户缓冲区。
在file结构体中会有一个指针指向address_space结构体,该结构体中会维护一颗字典树,用来建立数据和内存的映射关系,按4KB大小建立映射。然后被刷入内存缓冲区
被刷入缓冲区的数据需要建立IO请求,缓冲区中有很多IO请求,每一个请求会用结构维护起来,再用配套的算法使这些数据以最小的代价刷入磁盘——因为磁盘使机器设备