目录
C语言下的文件操作
Linux下的文件操作
文件描述符的前因后果
文件描述符的概念
文件描述符的分配规则
理解C语言的FILE结构体
Linux重定向
文件缓冲区
文件系统
文件系统的概念
ext2文件系统
对ext2的补充
虚拟文件系统的概念
软硬链接
C语言下的文件操作
早在学习C语言时我们就学过对文件的操作,诸如fopen、fclose打开关闭文件,fprintf、fscanf按文本读写数据,fwrite、fread二进制形式读写数据等函数,函数声明如下:
// 打开与关闭
FILE* fopen(const char* path, const char* mode);
int fclose( FILE *fp );// 文本形式读写
int fprintf(FILE *fp,const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);// 二进制形式读写
size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file); size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
其中,fopen的mode参数表示文件的打开方式,具体如下:
对于C语言的文件操作在这里不做过多的赘述,如有需要可以参考:C语言文件操作。
Linux下的文件操作
而在Linux下,操作系统也为我们提供了一批对文件进行操作的系统接口,下面就让我们来逐一认识这些系统接口。
open 打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
可以看到open接口有两种实现方式,函数说明如下
- pathname:目标文件的路径,精确到文件名。如果只有文件名没有路径标识,则默认是在工作目录(cwd)下的。
- flags:打开方式,参数是一些特殊定义的宏,本质是利用的整型每一位代表的是不同的含义来控制的,所以我们需要传入多个打开方式时只需要按位或即可。如下是常用的几种参数。
O_RDONLY:只读打开。
O_WRONLY:只写打开。
O_RDWR:读,写打开。
PS:上面这三个参数,必须指定且只能指定一个。
O_CREAT:若文件不存在,则创建它。使用时需要指明新文件的访问权限。
O_APPEND:追加写,从文件的最后开始写。
O_TRUNC:覆盖写,打开文件时会清空原文件的内容。- mode:文件的权限,通常在创建一个新文件时使用,由4位数字组成,即文件权限的数字标识法。创建文件时要记得设置权限,否则将会是随机值。
- 返回值:大于0表示打开文件成功,此时的返回值是一个int型的文件描述符(一般大于2),暂且可以理解为所打开文件对应的标识符。小于0表示打开文件失败。
用法示例
open("log.txt", O_CREAT | O_RDWR | O_TRUNC, 0666);
上面这句代码表示在工作目录下打开log.txt文件,如果不存在则创建(O_CREAT)这个文件,以读写的形式打开这个文件(O_RDWR),打开文件时清空原文件(O_TRUNC)。不考虑权限掩码的情况下,生成的log.txt文件的权限应该为 rwxrwxrwx(0666)。
需要注意的是,open接口是系统级别的接口,而文本文件和二进制文件的概念是语言级别的概念。也就是说open接口的打开方式并不区分是按二进制形式还是文本形式。下面的write、read等也类似,并不区分什么二进制文件还是文本文件。
其中,还有一个umask系统调用,用法等同于umask指令,如果程序代码中调用了umask,那么进程就会就近使用代码中的umask的值,但一般推荐还是用系统的,避免歧义。
close
#include <unistd.h>int close(int fd);
关闭文件描述符为fd所对应的文件。成功返回0,失败返回-1。
write
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
向文件描述符fd对应的文件中写入数据,写入buf所指向内容的count个字节。write写入时,默认并不会清空文件的内容,而是从首位置开始覆盖写入。需要注意的是,C语言或者C++中规定字符串的要以 '\0' 结尾,但系统接口并没有这样的规定。
成功则返回写入的字节数,失败返回-1。
read
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
从文件描述符fd对应的文件中,尝试读取count个字节的数据,并将内容放到buf指向的地方。尝试读取count个字节,也就是说最多能读取count个字节,可以小于但不能比它多。
成功则返回读入的字节数,失败返回-1。
lseek
#include <sys/types.h>
#include <unistd.h>off_t lseek(int fd, off_t offset, int whence);
移动文件中的光标位置。因为write函数在写入的过程中光标是在不断移动的,所以写入之后如果我们想要紧接着读取文件中的数据,就需要用lseek函数将光标移动到我们指定的位置。
打开的每个文件都有一个与其相关联的“当前文件位移量”。它是一个非负的整数,用以度量从文件开始处计算的字节数。通常,读、写操作都是从当前文件位移量处开始的,并使位移量增加所读或写的字节数。当打开一个文件时,除非指定O_APPEND选择项,否则该位移量默认设置为0。
其中fd表示对应文件的文件描述符,whence表示从哪开始移动,offset表示相对whence位置向后移动多少,所以offset为正就表示相对于offset向前移动,为负就表示向后移动。其中whence参数通常是内置的宏,例如:
- SEEK_SET:从文件起始位置开始(包括第一个字节)。
- SEEK_CUR:从当前位置开始。
- SEEK_END:从文件的末尾开始(不包含最后一个字节)。
成功则返回相对文件起始位置的偏移量,失败返回-1。
用法示例
// 打开文件
int fd = open("./log.txt", O_TRUNC | O_RDWR | O_CREAT, 0770);
if (fd < 0) return; // 打开文件失败的情况
// 写入内容
write(fd, "good\n", 5);
char str[10];
// 移动光标
lseek(fd, 0, SEEK_SET);
// 读取内容
ssize_t cnt = read(fd, str, 9);
if (cnt <= 0) puts("ERROR!"); // 读取文件失败的情况
// 打印文件内容
str[strlen(str)] = 0; // C语言的字符串要以0结尾
puts(str);
// 关闭文件
close(fd);
文件描述符的前因后果
文件描述符的概念
当我们打开一个文件时,本质上是通过进程将文件加载到内存中,进而对文件进行读写操作的。因为现代的计算机结构是以存储器为核心的,所以不管我们处理任何数据都要先加载到内存中再进行处理。而将外设(硬盘等)中的数据加载到内存就需要通过进程来完成。
而文件要被加载到内存中就必然要有对文件进行统一管理的相关结构体(struct_file),也就是说一个文件要被打开,首先就要在内核中形成被打开文件的对象。所以对文件的一系列操作就间接的变成了对这个文件在内存中的对象进行操作了。
前面说过将文件加载到内存是需要通过进程来完成的,而一个进程是可以打开多个文件的,所以进程是需要对多个struct_file对象进行管理的。也就是说,操作系统在维护进程时,还需要维护一个struct_file对象的指针数组(struct_file* fd_array[]),数组中的每一个元素都指向其对应的strutc_file对象,否则为空。
而文件描述符其实就是struct_file* fd_array[]中的数组下标,每一个文件描述符对应一个数组中的位置。所以这个数组一般被称为文件描述符表,即文件描述符表中存放的就是指向相关struct_file对象的指针。而操作系统是通过进程控制块(task_struct)对每一个进程进行管理的,所以每进程的task_struct结构体中是有对应的文件描述符表的指针的,其指向进程所对应的文件描述符表。
文件描述符的分配规则
文件描述符的分配规则是,在文件描述符表中从前往后找,将第一个没有被使用的数据的位置,分配给指定的struct_file对象。这个strcut_file对象会绑定对应文件的缓冲区和inode等内容。
如果要打开文件还没有打开,那么就会为其创建缓冲区和inode等内容,并绑定到新创建的struct_file对象中;如果文件已经打开了,那么就不用创建缓冲区和inode等内容,直接将struct_file对象与对应的缓冲区和inode等内容绑定即可。缓冲区和inode在后面的部分会讲到。
所以,其实文件描述符是通过写时拷贝的方式指向struct_file对象的,并不会在struct_file对象在文件描述符表中移除时就直接关闭文件缓冲区和inode等内容。
而在学习C语言时我们知道,为了减少上手成本、提高工作效率,C编译器会默认为我们打开标准输入stdin(键盘)、标准输出stdout(显示器)、标准错误stderr(显示器)这三个文件。但实际上这并不是C编译器为我们做的,实际上是每一个进程在启动时操作系统默认都会为进程打开stdin、stdout和stderr。即进程的文件描述符表的0、1、2位置就是stdin、stdout、stderr,所以当我们打开一个自定义文件时,一般就是从3开始的,这也就是为什么前面说open函数的返回值一般都大于2了。
我们可以通过
cat /proc/sys/fs/file-max
查看系统可打开最大文件描述符。
可以通过
cat /proc/sys/fs/file-nr
查看当前系统使用的打开文件描述符数
内容参考:Linux下最大文件描述符设置
理解C语言的FILE结构体
所以,我们在C/C++下使用的文件相关的操作,一定是封装了相关系统调用接口的。例如C语言中的fopen底层是封装的open接口,fclose是封装的close接口,fwrite、fprintf等是封装的write接口,fread、fscanf等是封装的read接口。
而我们知道,C语言是用FILE结构体来表示文件对象的,所以FILE中一定会封装对应文件在底层的文件描述符信息的,只不过不同的环境具体的实现方式会有所不同。而之所以C语言中不直接使用文件描述符却要自定义一个FILE的原因是,FILE中不单封装了文件描述符的信息,还维护了一个语言级别的缓冲区以及其它C语言特性的一些东西。
Linux重定向
至此我们知道,向文件中读写内容本质上是通过操作文件描述符所对应的struct_file对象做到的,而对文件的操作只以文件描述符为依据,所以如果操作的文件描述符不变,文件描述符与struct_file对象之间的映射关系变了,那么所对应操作的文件也会随之改变。
简单来说就是,上层接口只关心文件描述符,并不关心也不知道这个文件描述符所对应的文件是什么,以及是否被修改了。所以重定向操作就是利用了这种特性,通过改变文件描述符与文件之间的映射做到的。
例如,我们把1号文件描述符关闭,再重新打开一个文件就是输出重定向(普通重定向和追加重定向与打开方式有关,都可以是实现)。那么对应的,如果我们将0号文件描述符先关闭,然后再打开一个文件,那么就是输入重定向。
而另一种简便的重定向的方式就是直接新open一个文件,然后将这个文件对应的文件描述符表中的内容拷贝覆盖给0、1号这样的数组内容中。由于文件的打开方式等信息是被封装在文件描述符指向的struct file结构体中的,所以可以直接拷贝过来。也就是说,重定向的本质就是在文件描述符表级别之间进行拷贝。
而这种通过拷贝覆盖实现重定向的方式在Linux中是有对应的系统调用的,即dup2接口。
#include <unistd.h>int dup2(int oldfd, int newfd);
dup2接口的意思是在文件描述符表中,newfd的文件描述符被oldfd的文件描述符所覆盖
代码示例
#include <iostream>
using namespace std;
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_CREAT | O_RDWR | O_APPEND, 0666);dup2(fd, 1); // 新fd的内容被旧fd的内容覆盖cout << "hello my log!" << endl;return 0;
}
文件缓冲区
之前我们或多或少了解过缓冲区的概念,从操作系统与进程的角度来看,其实有两个缓冲区:用户层面的进程缓冲区和操作系统层面的内核缓冲区。
内核缓冲区
我们在打开一个文件时,操作系统要将其加载到内存中,而加载到内存中,实际上就是加载到内核缓冲区中的。对应的struct_file对象中有着与这块的内核缓冲区所对应的信息。所以通过struct_file对象就可以对打开文件的内核缓冲区内容进行读写。
当一个进程要从磁盘读写数据时,是无法直接读写磁盘内容的,所以实际上是读写的内核缓冲区中的内容,通过刷新内核缓冲区来对磁盘内容进行刷新。但若是内核缓冲区中没有数据,内核会把对数据块的请求,加入到请求队列,然后将进程挂起。
可以认为,read接口是把数据从内核缓冲区复制到进程缓冲区。write接口是把进程缓冲区复制到内核缓冲区。当然,write并不一定会导致内核的写动作,比如操作系统可能会把内核缓冲区的数据积累到一定量后,再一次写入。
其中,内核缓冲区一般是在文件关闭时强制刷新,或者是缓冲区满了时强制刷新,在Linux下也可以通过sync 指令来手动刷新。
进程缓冲区
用户进程在访问内核资源的时候,需要从用户态切换到内核态,而如果用户频繁地对内核缓冲区进行读写操作,就需要频繁地从用户态和内核态之间切换,这会很影响工作效率,所以为了提高效率,进程中也维护了一个缓冲区。
以C语言为例,C语言的FILE结构体中实际上就维护了一个用户层面的缓冲区。在C语言中,有三种缓冲区,分别是全缓冲、行缓冲和无缓冲,参考:C语言文件缓冲区-CSDN博客
而进程缓冲区存在的意义之一就在于能够减少上层调用的成本,借助进程缓冲区可以在内存种的数据积攒一部分之后再切换到内核态对内核缓冲区进行读写。所以每一个进程的运行实际上会用到两个缓冲区,只不过内核缓冲区是由操作系统维护的,所以我们一般感知不到。
所以,本质上C语言printf的格式控制字符,就是在用户级缓冲区向内核缓冲区进行拷贝时进行的字符串分析与替换工作。而我们在编写C/C++程序时所说的缓冲区,一般指的就是用户级的缓冲区。而我们所说的刷新,就是指的用户级的缓冲区,拷贝到系统内核的缓冲区。
文件系统
文件系统的概念
文件是由操作系统来管理的,包括文件的结构、文件的名称、文件的使用、文件的保护、文件的实现等等,而在一个操作系统中,负责处理与文件相关的各种事情的部分,就叫做文件系统。
文件系统就是一种抽象的体系。文件系统对于用户而言,关心的是文件的访问和操作;而对设计者或者相关开发者而言,更关心的是如何实现与之相关的内部结构和功能模块,例如文件系统的布局、空闲块的管理、数据块的大小、文件内容如何分配和查找等等。
以Linux为例,根据存储位置的不同,可以把文件系统分为三类:
- 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
- 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的
/proc
和/sys
文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据数据。- 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录,至于文件系统的挂载不是本文的重点,就不多赘述了。
ext2文件系统
预备知识
磁盘读写的最小单位是扇区,扇区的大小只有
512B
大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为4KB
,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。—— 内容参考:存储系列之 介质 - orange-C - 博客园 (cnblogs.com)
由于文件系统的错综复杂,所以我们这里以了解为主,并不深究,仅以Linux中经典的ext2文件系统为例,来简单地认识一下文件系统。
我们知道,操作系统对进程的管理是通过PCB(Linux下是task_struct)对每一个进行抽象的,同样地,操作系统对文件的管理也是通过一个类似的数据结构FCB管理起来的,其中这个FCB在Linux中的具体体现就叫做inode。接下来我们以下图为切入点,来介绍Linux的ext2文件系统。
(内容参考:存储系列之 Linux ext2 概述 - orange-C - 博客园 (cnblogs.com))
在认识ext2文件系统之前,我们先来了解几个基础概念:
逻辑块(block)
block是在分区进行文件系统的格式化时所指定的“最小存储单位”,这个最小存储单位以扇区的大小(512byte)为基础,大小为扇区的 2ⁿ 倍,一般为4KB(由连续的8个扇区构成)。而磁头一次可以读取一个逻辑块的数据,所以当逻辑块的大小为 4KB时,那么同样读取一个 10M 的文件,磁头要读取的次数则大幅下降为 2560 次,大大提高了文件的读取效率。
需要注意的是,逻辑块也并不是越大越好,因为一个文件至少需要占据一个逻辑块的大小,所以如果逻辑块过大可能会导致空间的浪费。例如,假设逻辑块的大小为 4KB,有一个文件大小为 0.1KB,这个小文件将占用掉一整个块的空间。也就是说,该块虽然可以容纳 4KB 的容量,然而由于文件只占用了 0.1KB,剩下的 3.9KB 空间就被浪费了。inode
可以认为inode是一个结构体,结构体中包含了文件的各种属性信息,每一个文件打开时都会生成一个inode对象。具体细节后面再说。其中每一个inode对象都对应一个inode编号,这个编号是独一无二的。在Linux内核中,识别一个文件,与文件名无关,只与inode编号有关。
block与inode的关系
每个文件都会占用一个 inode,inode 内则有文件数据放置的 block 号码,若文件太大时,会占用多个 block。如下图所示:
接着,让我们来认识一下ext2文件系统,先来看第一层:
该层表示计算机系统的整个硬盘空间被划分出了若干个分区。磁盘的扇区0称为主引导记录,简称为主引导(Master Boot Record,MBR),它主要用来启动计算机。在MBR的末尾有一个分区表,里面记录了每一个分区的起始地址和结束地址。MBR长度固定,即一个扇区的长度512B,而分区表的长度是64B,所以主引导程序长度就是446B。每个分区信息的长度固定16B,所以MBR分区表只能保存4个分区,也被称为主分区,如果需要描述更多的分区,则需要将其中一个分区作为扩展分区,指向更多的逻辑分区组成的链表。所以一般一个多(N)分区系统实际显示的是三个主分区和N-3个逻辑分区。现在很多计算机系统采用新的分区方式GPT,没有主分区个数的限制,而且分区容量也没有2TB的限制。
MBR程序所做的第一件事情是确定活动分区,并读入它的第一个磁盘块,称为引导块(Boot Block),然后装入内存并执行它。引导块是操作系统的引导程序和文件。每个分区都保留了引导块,不管这个分区是否已经安装了操作系统。如果引导程序太长,一个块放不下,也可以指向其他块。在MBR分区的方式下,启动的分区必须是主分区,不能是逻辑分区,逻辑分区只能被管理。
其中每一个分区都维护着一个文件系统,也就是说所有分区都是相互独立的,每一个分区都代表着不同的文件系统,所以每一个分区中文件的管理并不受其它分区的影响。
一个ext2文件系统,除了引导块,其他由多个块组(Block Group)组成,如上图第二层所示。每个块组的内部结构是一样的,如图中第三层所示。接着我们对第三层着重分析:
超级块(Super Block)
存放文件系统本身的结构信息。主要存放整个文件系统的bolck和inode的总量、未使用的block和inode的数量、block和inode的单位大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等,以及其他文件系统的相关信息。倘若Super Block区的内容被破坏,可以说整个文件系统结构就被破坏了。
其中,Super Block只在个别的块组中有,并不是每一个块组都有。换言之,Super Block是多副本保存的。所以操作系统对每个分区的管理就变成了对这些Super Block的管理了。也就是说,要使用一个分区(文件系统)来进行数据访问时,第一个要经过的就是超级块。
引导块(Boot Block)
在文件系统的开头,通常为一个扇区,存放文件系统的引导程序,用于读入并启动操作系统;这个区域维护了计算机的启动相关的信息。
索引节点表(inode Table)
inode table中存放着一个个 inode的对象,inode结构体中的内容主要是记录文件内容实际放置在哪些 block 内以及如下这些信息:
- 文件的读写权限(rwx)
- 文件的拥有者和所属组(owner/group)
- 文件的容量大小
- 文件的 atime(最近一次的读取时间)、ctime(创建时间)、mtime(最近修改的时间)
- 该文件的特殊标识,比如粘滞位等
- 指向文件内容的指针
除此之外,inode还有一些特殊的性质:
- 每个inode大小固定为128字节(ext4为256字节)。
- 每个文件都仅会占用一个 inode,也就是说即使我们对一个文件打开多次,归根结底这些文件所对应的文件描述符都是指向同一个inode的。
- 文件系统能够创建的文件数量与 inode 的数量相关。
- 系统读取文件时需要先找到 inode,并分析 inode 所记录的权限与使用者是否符合,若符合才能够开始读取 block 的内容。
- inode在每一个分区中具有唯一性,在Linux内核中,识别一个文件,与文件名无关,只与inode编号有关。
- 所以删除一个inode只需要在inode位图中将对应的位置由1置0即可,数据恢复大体也是利用了这个原理的。
数据块(Data Block)
data block,数据区。每一个4kb区域都有固定的编号。表现在inode中就是一个数组,数组中存放的信息就是关联的对应数据块的数据。
Data Block 是用来存放文件内容的地方,ext2 文件系统有1K、2K和4K大小的 block。在格式化文件系统时 block 的大小就确定了(一般为4kb),且每个 block 都有编号。需要注意的是,由于 block 大小的差异,会导致文件系统能够支持的最大磁盘容量和最大单个文件的大小并不相同。下表描述了 block 大小与文件系统以及单个文件大小的关系:
此外ext2文件系统的 block 还有下面一些限制:
- block 的大小与数量在格式化后就不能再改变了,除非重新格式化。
- 每个block内最多只能够放置一个文件的数据。
- 如果文件大于 block 的大小,那么一个文件会占用多个 block。
- 若文件小于 block,则该 block 的剩余容量也不能再被使用了,即磁盘空间被浪费。
- 每一个block都有固定的编号。表现在inode中就是一个数组,数组中存放的信息就是关联的对应数据块的数据。
- data black在底层实现上其实是一个诸如blocks[N]的数组,在内存映射时,前面的数组内容是直接映射的block内容,后面的数组则采用多级映射(多级索引)的方式来映射block的。所以,其实单个文件的大小是有上限的。
数据块位图(Block Bitmap)
在创建文件时需要为文件分配 block,文件系统需要选择空闲的 block ,就是通过block bitmap来查看 block 是否已经被使用了的。block bitmap本质上就是一个位图,所以仅用一个比特位的大小就可以保存某个位置是否已经被使用了。
通过 block bitmap 可以知道哪些 block 是空的,因此系统就能够很快地找到空闲空间来分配给文件。同样的,在删除某些文件时,文件原本占用的 block 号码就要释放出来,此时在 block bitmap 当中相对应到该 block 号码的标志就需要修改成"空闲"。这就是 block bitmap 的作用。
索引节点位图(inode Bitmap)
inode bitmap 与 block bitmap 的功能类似,只是 block bitmap 记录的是使用与未使用的 block 号,而 inode bitmap 则记录的是使用与未使用的 inode 号。
组描述符表(Group Description Table)
块组描述符,描述块组属性信息,用来描述每个 group 的开始与结束位置的 block 号码,以及说明每个块分别介于哪几个 block 号之间。
组描述符信息和超级块信息一样,复制到其他组块的开头。但是只有组块1中所包含的超级块和组描述符才由内核使用。实际中,系统启动时,修复工具e2fsck程序会对文件系统进行一致性检查,当发现组块1的超级块和组描述符无效时,系统管理员可以用e2fsck命令从后面的组块中的这两部分信息拷贝过来。
对ext2的补充
上述对ext2的结构组成基本介绍完成,再补充几个细节。
1、一个文件系统有多少个块组呢?
这取决于分区的大小和块的大小。其主要限制在块位图,因为块位图必须存放在一个单独的块中(inode bitmap一样),块位图用来标识一个组中块的占用和空闲状况。所以每组中至多有8*b个块,b是以字节为单位的块大小。因此,块组的总数大约是s/(8*b),这里s是分区所包含的总块数。
举例说明,让我们考虑一个32GB的ext2分区,块的大小为4KB。在这种情况下,每个4KB的块位图描述32K个数据块,即128MB。因此,最多需要256个块组。显然,块的大小越小,块组数越大。
2、文件较大时,多个块是如何管理的?
从上面我们知道,文件的内容存在data block中,但是一个文件只对应一个inode,而一个inode除了包含文件的属性外只能指向15个磁盘块。如果文件的大小超过了这个限制,则把最后一个(地址)指向一个间接块,里面存放了更多的磁盘块地址。如果还不够的话,还可以使用二级间接块和三级间接块。如下图所示(Linux 文件系统(一)---虚拟文件系统VFS)。
3、目录
在Linux系统中,目录(directory)也是一种文件,所以也存在对应的inode。打开目录,实际上就是打开目录文件。目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode编号。也就是说目录文件只是将文件名和它的inode编号形成映射关系的一张表。
对于一个文件来说有唯一的索引节点号与之对应,对于一个索引节点号,却可以有多个文件名与之对应。因此,在磁盘上的同一个文件可以通过不同的路径去访问它。
不同层级的目录构成了目录树,根节点即根目录,即“/”。超级块中包含了inode节点所在的位置,而第一个inode节点指向的是根目录。这样就可以对目录树进行搜索,从而找到所需要的目录或文件。
4、其它文件系统
其实ext2文件系统只是一个早期比较经典的文件系统,后续有出现了ext3和ext4等文件系统,而从CentOS 7开始默认的文件系统变成了xfs文件系统。可见,ext2文件系统其实只是文件系统的冰山一角,文件系统的不断发展和完善使得ext2也逐渐被淘汰,但由于ext2相对简单、易理解,使得我们可以通过学习ext2来简单的理解文件系统的概念。
虚拟文件系统的概念
内容参考:存储系列之 VFS虚拟文件系统简介 - orange-C - 博客园 (cnblogs.com)
VFS,Virtual File System虚拟文件系统,也称为虚拟文件系统开关,就是采用标准的Linux系统调用读写位于不同物理介质上的不同文件系统,即为各类文件系统提供了一个统一的操作界面和应用编程接口,VFS是一个内核软件层。VFS是一个可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作的抽象层,如下图所示。
我们知道在Linux系统中一切皆文件,在Linux系统中基本上把其中的所有内容都看作文件,除了我们普通意义理解的文件之外,目录、字符设备、块设备、 套接字、进程、线程、管道等都被视为是一个“文件”。例如对于块设备,我们通过fdisk -l显示块设备列表,其实块设备可以理解为在文件夹/dev下面的文件。只不过这些文件是特殊的文件。VFS是一个抽象层,其向上提供了统一的文件访问接口,而向下则兼容了各种不不同类型的文件系统。不仅仅是诸如Ext2、Ext4、XFS,windows家族的NTFS和Btrfs等常规意义上的文件系统,还可以是比如上图的proc等伪文件系统和设备,也可以是诸如NFS、CIFS等网络文件系统。
软硬链接
Linux中的分为软链接和硬链接两种链接方式,至于什么是链接,暂且可以按照Windows中的快捷方式来理解。其中,Linux中是通过ln指令创建链接的,详见:ln命令 – Linux命令大全
软连接
软连接的创建方式:
ln -s [参数] [源文件或目录] [目标文件或目录]
其中,文件或目录最好使用绝对地址,如果没有规定地址,那么默认就是在当前目录下。
例如:
ln -s myproc proc
就表示在当前目录下创建一个名为proc,myproc的软连接。
不光是文件,目录也可以进行软连接,用法相同,这里就不再演示了。
要注意,软连接是一份单独的文件,有属于他自己的inode,软链接文件和原文件的关系就好像是指针与变量一样。
硬链接
我们在使用 ls 指令查看文件信息时的这串数字就是这个文件的硬链接数。
创建硬链接的方法与软连接类似,就是去掉ln中的-s参数即可:
ln [参数] [源文件或目录] [目标文件或目录]
创建硬链接时,文件系统会在目录中添加一个新的目录项,并将新目录项指向与原始文件相同的inode。即新建一个inode与文件名之间的映射信息,这样就创建了一个新的文件名指向相同的文件数据。因此,硬链接并不是将文件的内容复制一份,而是共享相同的文件内容和inode。这种共享的机制使得硬链接能够节省磁盘空间,并且当你修改一个硬链接所指向的文件时,其他硬链接也会受到影响,因为它们都指向同一个inode。
特别的,每一个目录下都会有两个特殊的文件,分别是 . 和 .. 。其中 . 表示的是当前目录,.. 表示的是上级目录。而这两个 . 和 .. 就是对应目录的硬链接。所以硬链接数,其实就是相同文件名的目录项的个数。所以新目录创建之后,. 的硬连接数总是2。
特别的,为了安全起见,操作系统并不允许我们对目录进行手动指定硬链接,只能由操作系统自动建立硬链接。这是因为,我们知道Linux操作系统的文件结构是一种树状结构,如果肆意对目录使用硬链接,很可能会出现在某一个区间出现目录树的环状问题。
内容补充
- 不管是软链接还是硬链接,最好都统一使用绝对路径。
- ln指令是创建链接,而取消链接则需要使用unlink指令,之间unlink+链接名即可
- 软链接的本质是一个单独的文件,相当于一个指针,指向了目标文件。硬链接的本质是一个目录项,是原文件的一个别名。
- 软连接可以连接目录,而硬链接不可以链接目录。