目录
回顾C语言文件接口
c文件接口
c语言文件接口汇总如下图:
下面让我们来复习一下C语言常用文件接口的使用过程。
文件写入操作
#include <stdio.h>
int main()
{
//用写的方式打开文件log.txt
FILE* fp = fopen("log.txt", "w");
if (fp == NULL){
perror("fopen");
return 1;
}
int count = 5;
while (count){
//向被打开的文件中写入相应的内容
fputs("hello world\n", fp);
count--;
}
fclose(fp);
return 0;
}
文件读取操作
#include <stdio.h>
int main()
{
//用读的方式打开文件
FILE* fp = fopen("log.txt", "r");
if (fp == NULL){
perror("fopen");
return 1;
}
char buffer[64];
int i;
//读取文件当中的内容 并且打印到标准输出
for (i = 0; i < 5; i++){
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
pwd当前路径
从上面的操作中,我们不难发现,当fopen以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?
例如,我们在当前目录下运行可执行程序a.out,那么该可执行程序创建的log.txt文件会出现在当前目录下。
但是当前目录并不是指文件所在的目录,继续看下面的实验。
由此我们得出结论:
实际上,我们这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。
Linux系统层面的文件操作
操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。
我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。
下面介绍linux系统文件操作接口:
open
系统接口中使用open函数打开文件,open函数的函数原型如上图:
int open(const char *pathname, int flags, mode_t mode);
参数1:pathname
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
参数2:flags
open函数的第二个参数是flags,表示打开文件的方式。常用的有下面几个选项。
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
O_WRONLY | O_CREAT | O_APPEND
【提示】
系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。
实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
参数3:mode
open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限就是:-rw-rw-rw-,但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
注意:只用被访问的文件不存在需要创建的情况下,才会使用第三个参数。
返回值:
open函数的返回值是新打开文件的文件描述符。
我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的。
后面会解释为什么文件描述符从3开始,并且是连续的无符号整数。
close
系统接口中使用close函数关闭文件,close函数的函数原型如下:
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
write
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);
我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-1被返回。
read
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。
- 如果数据读取成功,实际读取数据的字节个数被返回。
- 如果数据读取失败,-1被返回
系统文件接口使用演示
write使用方式:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
const char* msg = "hello syscall\n";
for (int i = 0; i < 5; i++){
write(fd, msg, strlen(msg));
}
close(fd);
return 0;
}
read使用方式:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0){
perror("open");
return 1;
}
char ch;
while (1){
ssize_t s = read(fd, &ch, 1);
if (s <= 0){
break;
}
write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
}
close(fd);
return 0;
}
运行结果:
文件描述符fd
文件是由进程运行时打开的,一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
因此,操作系统务必要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。
当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置。
还记得上面遗留的问题,为什么文件描述符是从3开始的连续无符号整数?此时这个问题可以解答一半,因为文件描述符就是数组下标。那么又为什么会从3开始?因此操作系统总会默认打开三个流。
默认打开的三个流
都说linux下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么显示器和键盘当然也可以看作是文件。我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据。
为什么我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作?
需要注意的是,打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。
不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。
因此上面的问题就迎刃而解了,当我们打开其他文件时,数组下标012的位置已经被标准输入、标准输出、标准错误占据了,新打开文件的文件描述符自然也就会从3开始了。
文件描述符fd的分配规则
上文中我们已经强调过文件描述符都是从3开始连续递增的,这很好理解,因为文件描述符本质就是数组的下标,而当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。
若我们在打开这五个文件前,先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?
可以看到,第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的。
结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的。同时注意不要关闭1,因为1是标准输出,关闭后我们就无法从屏幕当中得到输出的信息了。
重定向
重定向的原理
在明确了文件描述符的概念及其分配规则后,现在我们已经具备理解重定向原理的能力了。从上面的例子我们不难联想到,如果我们先打开一个文件,将其关闭后再打开另一个文件。此时新文件便使用了原来文件的文件描述符,如果我们还是对旧文件的文件描述符就行操作,那么最终操作的内容不就重定向到了新文件当中了吗。这便是重定向的基本原理。
下面通过这三个具体的例子,让我们认识一下重定向的本质就是修改文件描述符下标对应的struct file*的内容。
例如,如果我们想让本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//将printf函数重定向到了log.txt
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
fflush(stdout);
close(fd);
return 0;
}
运行结果后,我们发现显示器上并没有输出数据,对应数据输出到了log.txt文件当中。
dup2
上面我们介绍了重定向的原理就是改变文件描述符,但实际上我们使用重定向并不需要这么麻烦,系统给我们提供了专门的接口。
在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。dup2的函数原型如下:
int dup2(int oldfd, int newfd);
函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
模拟shell实现重定向功能
下面模拟实现了一个支持重定向功能的shell,感兴趣的朋友们可以自行查看。
FILE
FILE当中的文件描述符
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
首先,我们在/usr/include/stdio.h
头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE
结构体的一个别名。
而我们在/usr/include/libio.h
头文件中可以找到struct _IO_FILE
结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno
的成员,这个成员实际上就是封装的文件描述符。
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
};
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
FILE的缓冲区
首先我们应该知道的是,C语言当中FILE缓冲区的方式有以下三种:
- 无缓冲。
- 行缓冲。(常见的对显示器进行刷新数据)
- 全缓冲。(常见的对磁盘文件写入数据)
实际上这个缓冲区是C语言自带的,而linux操作系统并没有提供文件缓冲区 。我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*
的指针,在FILE结构体当中还有一大部分成员是用于记录缓冲区相关的信息的。
//缓冲区相关
/* 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. */
也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。
操作系统实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上。(操作系统有自己的刷新机制,我们不必关系操作系统缓冲区的刷新规则)。
文件系统
上面我们主要介绍的是内存文件,但操作系统处理的更多的是磁盘文件,下面将介绍磁盘文件。
磁盘
在学习磁盘文件前我们需要首先了解一下磁盘的工作原理。
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
磁盘的基本组成
打开一个机械硬盘(当然最好不要打开,因为磁盘必须在绝对无尘的环境中运行打开后进入磁盘当中的灰尘会影响磁盘的正常工作,打开后这个硬盘也就相当于报废了)我们会发现它是由一摞盘片以及一组可动的磁头组成的。
磁盘的工作运行主要依靠的就是磁头和磁片。而我们的磁盘文件主要就是存储在这一摞盘片当中。盘片具有磁性,根据磁性可以将磁片的每一个小单元区分成0和1的指令,这样磁盘就可以存储相应的二进制指令了。而磁头可以通过改变磁片磁性的方式实现对存储在磁盘中的文件的增删查改工作。
因为磁盘中一整摞磁片的工作方式都是相同的,下面介绍单个磁片是如何工作的。
对磁盘进行读写操作时,一般有以下几个步骤:
- 确定读写信息在磁盘的哪个盘面。
- 确定读写信息在磁盘的哪个柱面。
- 确定读写信息在磁盘的哪个扇区。
通过以上三个步骤,最终确定信息在磁盘的读写位置。
磁盘分区与格式化
理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。
磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区。
磁盘格式化
磁盘只是一个可以存储数据的机械结构,上面介绍的是磁盘如何管理文件,而想要达到上面的效果,我们需要向磁盘中先写入管理文件的方法。所以当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是操作系统向分区写入文件系统的管理属性信息,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
EXT2文件系统的存储方案
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
通过上面的指令我们可以看到关于文件的7种信息,按顺序分别是:文件权限、硬链接数、文件所有者、文件所属组、文件大小、文件最后的修改时间、文件名。但是涉及到文件系统,这些信息还不能完全体现出文件在文件系统中是如何被管理的。
我们还可以通过stat命令查看更详细的文件信息。
注意上图中的blocks和Inode便是接下来我们将要介绍的重点内容。
上文中我们提到为了更方便地管理,系统会将磁盘分区。但实际上分区之后,每个区仍然会有至少上百G的大小,而对于计算机来说,管理几百G的内容和管理几十G的内容本质上都是相同的,如果可以管理好几十G的内容,那么再管理其他的内容时只需要复制原来的管理方法就可以了,因此在分区之后,我们还要进行分组。这种分治的思想对于文件系统来说是至关重要的。
对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
注意: 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
因此,要想真正搞懂文件系统,就需要理解每一个块组的工作原理。每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性如:文件大小,所有者,最近修改时间等。
- 数据区:存放文件内容。
理解文件系统
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
ls -i指令可以查看文件的inode,1179791即新创建的abs文件的inode。
为了说明问题,我们将上图简化:
创建一个文件的具体过程
- 通过遍历inode位图的方式,找到一个空闲的inode。
- 在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中。
- 将该文件的文件名和inode指针添加到目录文件的数据块中。
对文件写入信息
- 通过文件的inode编号找到对应的inode结构。
- 通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
- 若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。
【注意】inode存储文件的属性信息,以及文件使用的block数据块的位置,block数据块中存储文件的具体内容,inode只需要能够找到数据块即可。
补充说明:
1.多级索引
inode当中与block建立映射关系的时候,是通过一个数组进行维护的,该数组一般可以存储15个元素,但它并不是单纯地使用了一个数组,而是使用了多级索引的方式。如果只是一个数组那么相当于一个文件最多使用15个block,很显然这样是不是满足大文件的存储的。实际上则是前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。
2.确定好分组之后,inode编号是在一个分区内唯一有效,不能跨分区使用,也可以理解为在不同的分区中是存在相同的inode编号的,因此只能在每一个分区内使用inode区分不同的文件。
文件的删除过程
- 将该文件对应的inode在inode位图当中置为无效。
- 将该文件申请过的数据块在块位图当中置为无效。
因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。
同时与写入文件相比,删除一个文件的速度是非常快的。拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的
理解目录文件
- 都说在Linux下一切皆文件,目录当然也可以被看作为文件。
- 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
- 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针
注意: 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
软硬链接
软链接
我们可以通过以下命令创建一个文件的软连接。
通过ls -i -l
命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。软链接就类似于Windows操作系统当中的快捷方式。
硬链接
我们可以通过以下命令创建一个文件的硬连接。
通过ls -i -l
命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几。总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。
我们创建一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。那为什么我们创建一个目录后,该目录的硬链接数是2?
因为每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。
软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
文章评论