【Linux】--- 基础IO
【Linux】--- 基础IO
- 一、C语言文件IO
- 1、C语言文件接口汇总
- 2、C的IO总结
- 二、系统的文件 IO
- 1、open
- (1)open的第一个参数
- (2)open的第二个参数
- (3)open的第三个参数
- (4)open的返回值
- 2、close
- 3、write
- 4、read
- 三、文件描述符fd
- 四、文件描述符的分配规则
- 五、重定向
- 1、重定向的原理
- 2、dup2
- 六、FILE
- 1、FILE中的文件描述符
- 2、FILE中的缓冲区
- 七、理解文件系统
- 1、初识inode
- 2、磁盘的概念
- 3、磁盘分区与格式化介绍
- 4、EXT2文件系统的储存方案
- 八、软硬链接
- 1、软链接
- 2、硬链接
- 3、软硬链接的区别
- 九、文件的三个时间
一、C语言文件IO
1、C语言文件接口汇总
语言中的文件操作函数如下:
对文件进行写入操作示例:
#include <stdio.h>
int main()
{
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];
for (int i = 0; i < 5; i++){
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
2、C的IO总结
该函数会返回一个FILE*的指针,C语言中,通过操作这个 FILE * 来控制文件的IO。
当我们打开文件时,每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息。
这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE。我们可以通过操纵这个FILE类型的结构体,来操控文件。
大部分情况下,我们可以得到一个指向该结构体的指针FILE*,即文件指针,后续通过文件指针来操控文件。
上图中,三个指针pf1,pf2,pf3,它们都指向了一个FILE类型的文件信息区。而每个文件信息区都存储着一个文件的信息,后续就通过这三个指针来操控这三个文件。
C语⾔程序在启动的时候,默认打开了3个流:
stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊。
stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯。
stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这三个流也是通过文件指针来操作的。
以上是从C语言的视角来看文件,接下来从操作系统的角度来看文件。
二、系统的文件 IO
操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。
相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。
我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。
1、open
系统接口中使用open函数打开文件,open函数的函数原型如下:
int open(const char *pathname, int flags, mode_t mode);
(1)open的第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
(2)open的第二个参数
open函数的第二个参数是flags,表示打开文件的方式
其中常用选项有如下几个:
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
O_WRONLY | O_CREAT
(3)open的第三个参数
open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0
umask(0); //将文件默认掩码设置为0
注意: 当不需要创建文件时,open的第三个参数可以不必设置
(4)open的返回值
open函数的返回值是新打开文件的文件描述符。
我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。
为什么我们所看到的文件描述符是从3开始?
文件描述符:fd
前三个已经被占用!
0:标准输入
1:标准输出
2:标准错误
2、close
系统接口中使用close函数关闭文件,close函数的函数原型如下:
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
3、write
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);
解释一下 函数的作用 以及 参数的含义:
我们可以使用write函数,将从buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-1被返回。
对文件进行写入操作示例:
#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;
}
4、read
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
函数的使用:我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。
- 如果数据读取成功,实际读取数据的字节个数被返回。
- 如果数据读取失败,-1被返回
对文件进行读取操作示例:
#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
文件描述符的本质:进程 与 文件 由于映射关系所形成的数组的下标!
(在底层就是:如果要对某一个文件进行操作,流程就是:进程找到文件描述符fd进而找到对应的进程进行操作!)
task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
示例:
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。
什么叫做进程创建的时候会默认打开0、1、2?
0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。
而键盘和显示器都属于硬件,属于硬件就意味着操作系统能够识别到,当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。
四、文件描述符的分配规则
尝试连续打开五个文件,看看这五个打开后获取到的文件描述符。
#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开始连续递增的,这很好理解,因为文件描述符本质就是数组的下标,而当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。
若我们在打开这五个文件前,先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?
close(0);
可以看到,第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的。
我们再试试在打开这五个文件前,将文件描述符为0和2的文件都关闭(不要将文件描述符为1的文件关闭,因为这意味着关闭了显示器文件,此时运行程序将不会有任何输出)。
close(0);
close(2);
可以看到前两个打开的文件获取到的文件描述符是0和2,之后打开文件获取到的文件描述符才是从3开始依次递增的。
结论: 文件描述符是从最小并且没有被使用的fd_array数组下标开始进行分配的。
五、重定向
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()
{
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文件当中。
说明一下:
- printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
- C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
追加重定向原理
追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。
例如,如果我们想让本应该输出到“显示器文件”的数据追加式输出到log.txt文件当中,那么我们应该先将文件描述符为1的文件关闭,然后再以追加式写入的方式打开log.txt文件,这样一来,我们就将数据追加重定向到了文件log.txt当中。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
if(fd < 0){
perror("open");
return 1;
}
printf("hello Linux\n");
printf("hello Linux\n");
printf("hello Linux\n");
printf("hello Linux\n");
printf("hello Linux\n");
fflush(stdout);
close(fd);
return 0;
}
运行结果后,我们发现对应数据便追加式输出到了log.txt文件当中。
输入重定向原理
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据
例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
char str[40];
while (scanf("%s", str) != EOF){
printf("%s\n", str);
}
close(fd);
return 0;
}
运行结果后,我们发现scanf函数将log.txt文件当中的数据都读取出来了。
说明一下:
scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。
标准输出流和标准错误流对应的都是显示器,它们有什么区别?
我们来看看下面这段代码,代码中分别向标准输出流和标准错误流输出了两行字符串。
#include <stdio.h>
int main()
{
printf("hello printf\n"); //stdout
perror("perror"); //stderr
fprintf(stdout, "stdout:hello fprintf\n"); //stdout
fprintf(stderr, "stderr:hello fprintf\n"); //stderr
return 0;
}
实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。
2、dup2
要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。
在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。dup2的函数原型如下:
int dup2(int oldfd, int newfd);
函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
1. 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭
2. 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
例如,我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
代码运行后,我们即可发现数据被输出到了log.txt文件当中。
六、FILE
1、FILE中的文件描述符
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。
typedef struct _IO_FILE 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
};
现在我们再来理解一下C语言当中的fopen函数究竟在做什么?
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
2、FILE中的缓冲区
、我们来看看下面这段代码,代码当中分别用了两个C库函数和一个系统接口向显示器输出内容,在代码最后还调用了fork函数。
#include <stdio.h>
#include <unistd.h>
int main()
{
//c
printf("hello printf\n");
fputs("hello fputs\n", stdout);
//system
write(1, "hello write\n", 12);
fork();
return 0;
}
运行该程序,我们可以看到printf、fputs和write函数都成功将对应内容输出到了显示器上。
但是,当我们将程序的结果重定向到log.txt文件当中后,我们发现文件当中的内容与我们直接打印输出到显示器的内容是不一样的。
操作系统有缓冲区吗?
操作系统实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上。(操作系统有自己的刷新机制,我们不必关系操作系统缓冲区的刷新规则)
因为操作系统是进行软硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。
七、理解文件系统
我们知道文件可以分为磁盘文件和内存文件,内存文件(被打开的文件/马上要操作的文件)前面我们已经谈过了,下面我们来谈谈磁盘文件(暂时没有被打开的文件)。
1、初识inode
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小、文件类型以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
在命令行当中输入ls -l,即可显示当前目录下各文件的属性信息。
其中,各列信息所对应的文件属性如下:
在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。
也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。
在命令行当中输入ls -i,即可显示当前目录下各文件的inode编号。
注意: 无论是文件内容还是文件属性,它们都是存储在磁盘当中的。
2、磁盘的概念
什么是磁盘?
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
磁盘在冯诺依曼体系结构当中既可以充当输入设备,又可以充当输出设备。
磁盘的基本概念
磁盘的寻找方案
3、磁盘分区与格式化介绍
线性存储介质
理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。
磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:
ls /dev/vda* -l
磁盘格式化
当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。
其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
4、EXT2文件系统的储存方案
计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
注意: 启动块的大小是确定的,而分组的大小是由格式化的时候确定的,并且不可以更改。
其次,每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
- Super Block: 存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- Group Descriptor Table: 块组描述符表,描述该分区当中块组的属性信息。
- Block Bitmap: 块位图当中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode Bitmap: inode位图当中记录着每个inode是否空闲可用。
- inode Table: 存放文件属性,即每个文件的inode。
- Data Blocks: 存放文件内容。
注意:
- 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
- 磁盘分区并格式化后,每个分区的inode个数就确定了。
如何理解创建一个空文件?
- 通过遍历inode位图的方式,找到一个空闲的inode。
- 在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中。
- 将该文件的文件名和inode指针添加到目录文件的数据块中。
如何理解删除一个文件?
- 将该文件对应的inode在inode位图当中的1置为0。
- 将该文件申请过的数据块在块位图当中的1置为0。
因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。
为什么拷贝文件的时候很慢,而删除文件的时候很快?
因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。
这就像建楼一样,我们需要很长时间才能建好一栋楼,而我们若是想拆除一栋楼,只需在这栋楼上写上一个“拆”字即可。
如何理解目录?
- 都说在Linux下一切皆文件,目录当然也可以被看作为文件。
- 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
- 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。
注意:
每个文件的 文件名 并没有存储在自己的inode结构当中, 而是存储在该文件所处目录文件的文件内容当中。
因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
八、软硬链接
1、软链接
我们可以通过以下命令创建一个文件的软连接。
[cl@VM-0-15-centos SHLink]$ ln -s myproc myproc-s
通过ls -i -l命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。(软链接的内容:目标文件的路径,相当于Windows的快捷方式!)
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。
2、硬链接
我们可以通过以下命令创建一个文件的硬连接。
[cl@VM-0-15-centos SHLink]$ ln myproc myproc-h
通过ls -i -l命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为924344的文件有myproc和myproc-h两个文件名,因此该文件的硬链接数为2。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。
总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。
为什么刚刚创建的目录的硬链接数是2?
因为每个目录创建后,该目录下默认会有两个隐含文件 (.)和(. .) 它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的 . 所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的 . 的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。
小技巧: 一个目录下相邻的子目录数等于该目录的硬链接数减2。
3、软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
九、文件的三个时间
在Linux当中,我们可以使用命令stat 文件名来查看对应文件的信息。
- Access: 文件最后被访问的时间。
- Modify: 文件内容最后的修改时间。
- Change: 文件属性最后的修改时间。
当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。
注意: 当某一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息。