每一滴眼泪,每一次心碎,什么爱能无疚无悔..........................................................................
文章目录
前言
一、【认识文件I/O】
1.1、【回顾C语言文件I/O】
1.2、【操作系统文件I/O】
1.2.1、【open函数】
1、【open函数的三个参数】
2、【open函数的返回值】
3、【认识文件描述符fd】
4、【文件描述符分配规则】
1.2.2、【close函数】
1.2.3、【write函数】
1.2.4、【read函数】
1.3【理解语言与系统文件操作的关系】
二、【重定向】
2.1、【什么是重定向】
2.2、【重定向的原理】
【输出重定向原理】
【追加重定向原理】
【输入重定向原理】
2.3、【标准输出流和标准错误流的区别】
2.4、【dup2函数】
三、【认识FILE结构体】
3.1、【FILE当中的文件描述符】
3.2、【FILE当中的缓冲区】
1、【缓冲区的理解以及刷新方式】
2、【一个重要的缓冲区样例】
3、【解决两个重要的问题】
4、【模拟封装C语言库函数】
四、【理解文件系统】
4.1、【初识inode】
4.2、【磁盘的概念】
4.3、【磁盘分区与格式化介绍】
1、【磁盘分区】
2、【磁盘格式化】
3、【EXT2文件系统的存储方案】
4.4、【几个重要的问题】
五、【软硬链接】
1、软链接
2、硬链接
3、软硬链接的区别
4、再谈“当前目录”
总结
前言
本篇博客重点讲述了什么是文件I/O,以及介绍了重定向和缓冲区的概念,还有对FILE结构体的讲解,以及磁盘的工作原理,还有最后的软硬连接问题,相信看完本篇博客,不会再对文件操作感到陌生。
一、【认识文件I/O】
还记得我们之前说过一句话,叫做“Linux下,一切皆文件”,可见无论是我们写的可执行程序,常见的文本文件,甚至我们的键盘,显示器,网卡等硬件设备对于操作系统来说,都是一个个文件罢了,既然是文件就少不了对文件的写入和读取,那么什么是IO呢?,I就是in,O就是out,分别对应入和出,一般是指输入和输出,其实我们之前就接触过一些IO接口,比如C语言中的scanf,printf,fopen,fclose等一些操作文件的函数,还有C++中的cin,cout,这些都是我们之前接触过的IO函数解救,下面我们来看看C语言中的文件操作函数。
1.1、【回顾C语言文件I/O】
我们先来回顾一下之前学过的一些I/O接口,下面是C语言文件操作的相关函数:具体介绍——【【C语言中的文件操作】-CSDN博客】
下面我们看一个对文件进行写入操作示例:
#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);//向fp中换行写入 count--; } fclose(fp);//关闭文件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; }
运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上,结果:
1.2、【操作系统文件I/O】
实际上,操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。
相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。我们在Linux平台下运行C代码时,C库函数中的文件操作函数就是对Linux系统调用接口进行的封装,而在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。
1.2.1、【open函数】
1、【open函数的三个参数】
系统接口中使用open函数打开文件,open函数的函数原型如下:
int open(const char *pathname, int flags, mode_t mode);
pathname:open函数的第一个参数是pathname,表示要打开或创建的目标文件。
- 表示要打开或创建的目标文件,若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
flags:open函数的第二个参数是flags,表示打开文件的方式。
其中常用选项有如下几个:
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用 “ 或(|)” 运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:O_WRONLY | O_CREAT
那么这些选项本质上是什么呢?
实际上,系统接口open的第二个参数flags是一个整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。
实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
例如,
O_RDONLY
、O_WRONLY
、O_RDWR
和O_CREAT
在系统当中的宏定义如下:#define O_RDONLY 00 #define O_WRONLY 01 #define O_RDWR 02 #define O_CREAT 0100
这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用 “ 与(&)” 运算来判断是否设置了某一选项。
int open(arg1, arg2, arg3) { if (arg2&O_RDONLY) { //设置了O_RDONLY选项 } if (arg2&O_WRONLY) { //设置了O_WRONLY选项 } if (arg2&O_RDWR) { //设置了O_RDWR选项 } if (arg2&O_CREAT) { //设置了O_CREAT选项 } //... }
mode:open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用
umask
函数将文件默认掩码设置为0。umask(0); //将文件默认掩码设置为0
注意: 当不需要创建文件时,open的第三个参数可以不必设置,因为只有创建文件的同时我们才应为其指定权限,这样才能方便我们对其进行操作。
2、【open函数的返回值】
首先我们要知道open函数的返回值是新打开文件的文件描述符,关于什么是文件标识符后面会有讲解,我们现在只需要知道文件标识符fd类似于前面学过的进程标识符PID,是一个整数,用来标识文件,从而方便对文件进行管理。
来看一个例子,我们尝试一次打开多个文件,然后分别打印它们的文件标识。
#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开始连续且递增的。
我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("test.txt", O_RDONLY); printf("%d\n", fd); return 0; }
运行程序后可以看到,打开文件失败时获取到的文件描述符是-1。
到这里我们可能会有疑问,这个文件标识符到底是什么呢?还有为什么文件标识符是从3开始的呢?
3、【认识文件描述符fd】
文件在没有被操作时实际上是存放在磁盘中的,当用户想打开文件时,必然要从磁盘中访问想打开的文件,而只有操作系统能访问底层硬件,而操作系统只能通过创建进程来打开文件,也就是说文件是由进程运行时打开的,一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
因此,操作系统势必要对这些已经打开的文件进行管理,那怎么管理呢?还是那句话先描述再组织,实际上操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。进程和文件之间的对应关系是如何建立的?
我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就对应我们所谓的文件描述符。
比如我们要打开一个名叫myproc的文件,实际上是通过操作系统创建进程来完成一些列操作的,首先操作系统需要先将该文件从磁盘当中加载(拷贝)到内存,然后通过进程形成对应的struct file,并将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。
现在我们知道了文件标识符实际上就是管理 “已创建完成的文件” 的数组下标,那么为什么我们新创建的文件对应fd是3呢?下面就让我们来看看文件描述符的分配规则。
4、【文件描述符分配规则】
我们尝试连续打开五个文件,看看这五个打开后获取到的文件描述符。
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> int main() { umask(0);//修改权限掩码为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开始的呀,那为什么不从0,1,2,开始呢?
这是因为操作系统一旦开始运行就默认会打开三个标准流分别是:stdin stdout stderr,反映到文件标识符上就是对应0,1,2这三个文件标识符,其中0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。
若我们在打开这五个文件前,使用close(0)先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> int main() { umask(0);//修改权限掩码为0 close(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; }
可以看到,第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的。
我们再试试在打开这五个文件前,将文件描述符为0和2的文件都关闭(不要将文件描述符为1的文件关闭,因为这意味着关闭了显示器文件,此时运行程序将不会有任何输出)。
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> int main() { umask(0);//修改权限掩码为0 close(0);//关闭标准输入 close(2); 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; }
可以看到前两个打开的文件获取到的文件描述符是0和2,之后打开文件获取到的文件描述符才是从3开始依次递增的。
结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的。
1.2.2、【close函数】
系统接口中使用close函数关闭文件,close函数的函数原型如下:
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
1.2.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; }
运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容。
1.2.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; }
运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上。
1.3【理解语言与系统文件操作的关系】
经过前面的学习,我们知道C语言中的文件操作函数,底层其实是封装了系统调用接口的,但是C语言fopen返回的是FILE* 指针,而系统open返回的是int类型整数fd,后续也是通过fd来对指定文件做处理的。
并且我们也知道操作系统默认将stdin stdout stderr 打开,就是为了让程序员方便进行输入输出代码,因为可以直接scanf 和 printf进行输入和显示。而我们C语言第一个头文件基本都是stdio.h,其实stdio就是指标准输入输出,C++第一个头文件 iostream,也是指输入输出流。
操作系统要管理硬件,也是先描述在组织,会将各种硬件使用结构体组织起来,不同的硬件,读写的方法肯定是不一样的,但是struct file里的接口read和write函数指针会指向对应硬件的读写。这样从统一的视角去看待不同硬件。我们也就理解了为何操作系统先会打开0 1 2这三个标准流。
我们知道了0 1 2号文件标识符分别为stdin,stdout,stderr,也知道了linux一切皆文件,那么我们尝试下使用 read与write + 文件标识符 进行读写来代替(scanf和printf),代码如下,从0(stdin)中读取输入到buffer,从buffer写入数据到1(stdout)中。
二、【重定向】
2.1、【什么是重定向】
顾名思义:更改重新更改命令读取/输入以及输出的方式、方向。Linux重定向有三种类型,分别是标准输入重定向、标准输出重定向、标准错误输出重定向,而体现到命令中也有三种形式分别是输出重定向——“ > ”,输入重定向——“ < ”,追加重定向——“ >> ”。
下面我们先来看几个例子:
输出重定向:
输入重定向:
追加重定向:
2.2、【重定向的原理】
在明确了文件描述符的概念及其分配规则后,现在我们已经具备理解重定向原理的能力了。看完下面三个例子后,你会发现重定向的本质就是修改文件描述符下标对应的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的文件读取数据。2.3、【标准输出流和标准错误流的区别】
我们首先要知道知道文件标识符1——stdout对应显示器文件,同样的文件标识符2——stderror也对应显示器文件:
这里我们就会有一个问题,文件标识符1,2都对应着显示器这个文件,那么当我们使用close()函数关闭文件标识符1时,会对文件标识符2产生影响吗?
我们可能会以为,毕竟它们对应同一个文件,如果关闭了这个文件,理论上会对另一个对应该文件的文件标识符造成影响吧,实则不然,当我们使用某个fd关闭其对应的文件时,并不会影响该文件对应的另一个fd的使用,因为这里有引用计数的存在,也就是说在struct_file中会存在一个count变量,一个文件有几个指针指向,其对应的count就为几,某个指针取消指向,或者使用close进行释放,就会让count-1,直到count为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; }
直接运行程序,结果很显然就是在显示器上输出四行字符串。
这样看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。但我们若是将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。
实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向,所以不管2号文件描述符做什么事情,stderr都是老样子,维持不动。
如果我们非要将标准输出和标准错误的内容都放到log.txt里,则需要在后面添加 2>&1 。这句代码的意思是将&1里面的内容放到2号文件描述符里,由于1号文件描述符已经被 log.txt 覆盖了,因此2好描述符相当于也被 log.txt 覆盖,也就都打印到了log.txt里面。
为什么这这样写可以呢?因为 ./myfile > log.txt 其实上是 ./myfile 1 > log.txt 。代表将log.txt里的内容放到1号文件描述符中。2>&1 也是同理,1号地址的内容放到2号。
这样设计的目的,是让一个程序在运行时,将标准输出和标注错误放到不同的文件中,方便我们排查错误,如下就打印到了分别的两个文件中。
2.4、【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时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果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结构体】
回想一下,我们第一次见到FILE结构体应该是在C语言的文件操作中,我们定义了一个FILE类型的指针,像下面这样:
FILE* fp=fopen("text.txt","w");
实际上FILE结构体的作用远不止这些,下面让我们来看看FILE结构体中都有什么!
3.1、【FILE当中的文件描述符】
实际上我们上面介绍了这么久的文件描述符(fd)就存在于FILE结构体中,这是因为库函数(这里指C语言中那些用于文件操作的函数fopen等)是对系统调用接口(open等函数)的封装,本质上访问文件都是通过文件描述符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结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
3.2、【FILE当中的缓冲区】
1、【缓冲区的理解以及刷新方式】
其实我们之前已经接触过缓冲区,在编写进度条小程序的时候,我们介绍了行缓冲区的概念,如下图:
这里举个例子:
比如你在安徽,你有一个朋友在辽宁,这时候你想要给他寄礼物的, 你肯定不会选择开车慢悠悠的送过去,然后亲自交到他手上之后再返回,因为这样不仅浪费时间而且效率非常低,所以正常人会选择将礼物通过快递邮寄过去,比如菜鸟驿站,可以让菜鸟驿站帮你送过去,虽然时间没有变快,但是效率肯定变高了,这里的效率提高在哪了呢,实际上这里不仅节省了你的时间,因为对于你而言你只需要将礼物交给快递站你的任务就结束了,而对于很多和你一样需要寄东西到辽宁的人来说,也节省了他们的时间,因为菜鸟驿站肯定是攒够到辽宁的快递才会进行配送,所以这里实际上提高的是菜鸟驿站用户的效率。
这里,菜鸟驿站就是缓冲区,所以缓冲区的作用就是提高使用者的效率,将菜鸟驿站的快递送到目的地就是刷新,而把送到同一个目的地的快递攒到一定数量再进行配送,就是一种缓冲区刷新策略,就是说存放到缓冲区的数据要在满足某个条件以后才能进行刷新,而这里的刷新实际上就是指对缓冲区中的数据进行清空和写入。
首先我们应该知道的是,缓冲的方式对应如下:
一般策略:
1.无缓冲(立即刷新)
2.行缓冲(行刷新)
3.全缓冲(缓冲区满了,再刷新)
特殊情况:
1.用户输入fflush(),强制刷新。
2.进程退出的时候,也要进行刷新缓冲区。
这里,缓冲区的本质就是内存的一部分,C标准库提供了他的缓冲区,操作系统也有自己的缓冲区,而我们绝大部分使用的缓冲区是C/C++的缓冲区。
2、【一个重要的缓冲区样例】
我们来看看下面这段代码,代码当中分别用了两个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文件当中后,我们发现文件当中的内容与我们直接打印输出到显示器的内容是不一样的。
我们可以看到不仅write输出顺序不同,printf和fputs的输出次数也不相同,那为什么C库函数打印的内容重定向到文件后就变成了两份,而系统接口打印的内容还是原来的一份呢?
实际上,当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,代码中进程创建完成以后程序就退出了,而程序一旦退出就会强制刷新缓冲区,无论父子进程哪一个先退出,都要对缓冲区进行刷新,而刷新实际上是写入和清空的过程,所以本质就是先退出的那个进程要对对父子进程共享的数据进行修改,而我们知道由于进程地址空间的存在,此时就会发生写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和puts函数打印的数据就有两份。但由于write函数是系统调用接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。下面我们看一个图片:
这里从C语言缓冲区写入到操作系统内部称为刷新,而对于write函数,根本没有经过C语言缓冲区,直接在操作系统内部,因此重定向时 write 会先打印!!
C语言搞缓冲区出来就是为了提高效率,C语言追求的就是效率,姿势可以不帅,但速度一定要快,写入到C语言缓冲区速度要比写入到操作系统快多了,写入完成直接返回运行后续代码了,直到写满了缓冲区或者进程退出时,一起将缓冲区的内容再写到操作系统中。
而后续操作系统中给每一个文件也设置了文件缓冲区,操作系统再根据他自己的刷新策略进行刷新,写入到磁盘中,和用户已经没关系了。
根据上述内容,我们总结一下:
1.显示器文件的刷新策略是行刷新,我们在显示器上打印都是带了‘\n’,fork之前,数据已经全部被刷新,包括系统调用接口。
重定向到log.txt,本质是向磁盘进行写入,刷新方式由行刷新变成了全缓冲。
3.全缓冲意味着缓冲区变大,写入的数据不足以将缓冲区填满,fork执行后,数据依然在缓冲区中,没有刷新出去。
4.这里谈到的缓冲区,是C语言提供的缓冲区,跟操作系统没关系(因为系统调用打印并没有加倍,C加倍了,一定跟C脱不了关系)
5.C/C++提供的缓冲区,里面一定保存了用户的数据,这属于当前进程自己的数据,如果缓冲区刷新,将数据交给了操作系统,就由操作系统来帮我们处理了,不属于当前进程了。
6.当进程退出时,一般都要强制刷新缓冲区。
fork之后,数据仍在C语言的缓冲区中,属于当前进程自己的数据,那么fork之后,父子进程代码和数据共享,同时父子进程陆续退出,退出时会刷新缓冲区(将数据给操作系统——也算写入),就会发生写时拷贝,父给操作系统一份,子也会给操作系统一份,因此会打印两份内容!!!
而write系统调用,没有使用C语言的缓冲区,直接写入到操作系统中,并不属于该进程,因此不会写时拷贝。
3、【解决两个重要的问题】
【1】那么C语言提供的缓冲区在哪里呢?
答案是在FILE结构体里,我们输入输出都要有一个FILE,像printf函数,虽然传参不需要FILE,但他内部一定封装了,比如我们常说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结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。
【2】那么操作系统有缓冲区吗?
实际上是有的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上。(操作系统有自己的刷新机制,我们不必关系操作系统缓冲区的刷新规则)
因为操作系统是进行软硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。
4、【模拟封装C语言<stdio>库函数】
我们写简单一点,目的是帮助我们更好理解缓冲区和重定向,只需要四个函数,fopen,fclose,fprintf,fflush。
mystdio.h头文件如下,MyFile结构体中,有fileno,flag,buffer,end。
#pragma once #define SIZE 4096 #define FLUSH_NONE 1 #define FLUSH_LINE (1<<1) #define FLUSH_ALL (1<<2) typedef struct _myFILE { int fileno; int flag; char buffer[SIZE]; int end; }myFILE; extern myFILE *my_fopen(const char *path, const char *mode); extern int my_fwrite(const char *s, int num, myFILE *stream); extern int my_fflush(myFILE *stream); extern int my_fclose(myFILE*stream);
mystdio.c
my_fopen函数,首先先判断方式为“r”、“w”、“a”,还是其他,如果flag中有O_CREAT,则open打开文件需要添加权限掩码。malloc开辟MyFile空间,同时初始化一下。其次注意 my_fflush函数刷新,本质上使用write函数将数据写到操作系统中,以及my_fwrite函数,先使用memcpy从s里面把数据拷贝到buffer(不用strcpy是不拷贝"\0"),判断刷新方式与end大小,看看是否需要刷新,循环遍历‘\n’,防止出现str = "abcd\nefg"的情况。发现 '\n' 就先处理end大小,再退出循环开始刷新,后面再将str中\n后面的数据拷贝回buffer的起始位置,再处理end,最后my_fclose函数简单,退出前先刷新一下就可以了,再调用close。
#include "mystdio.h" #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #define DFL_MODE 0666 myFILE *my_fopen(const char *path, const char *mode) { int fd = 0; int flag = 0; if(strcmp(mode, "r") == 0) { flag |= O_RDONLY; } else if(strcmp(mode, "w") == 0) { flag |= (O_CREAT | O_TRUNC | O_WRONLY); } else if(strcmp(mode, "a") == 0) { flag |= (O_CREAT | O_WRONLY | O_APPEND); } else{ // Do Nothing } if(flag & O_CREAT) { fd = open(path, flag, DFL_MODE); } else { fd = open(path, flag); } if(fd < 0) { errno = 2; return NULL; } myFILE *fp = (myFILE*)malloc(sizeof(myFILE)); if(!fp) { errno = 3; return NULL; } fp->flag = FLUSH_LINE; fp->end = 0; fp->fileno = fd; return fp; } int my_fwrite(const char *s, int num, myFILE *stream) { // 写入 memcpy(stream->buffer+stream->end, s, num); stream->end += num; // 判断是否需要刷新, "abcd\nefgh" if((stream->flag & FLUSH_LINE) && stream->end > 0 && stream->buffer[stream->end-1] == '\n') { my_fflush(stream); } return num; } int my_fflush(myFILE *stream) { if(stream->end > 0) { write(stream->fileno, stream->buffer, stream->end); //fsync(stream->fileno); stream->end = 0; } return 0; } int my_fclose(myFILE*stream) { my_fflush(stream); return close(stream->fileno); }
main.c
#include "mystdio.h" #include <stdio.h> #include <string.h> #include <unistd.h> int main() { myFILE *fp = my_fopen("./log.txt", "w"); if(fp == NULL) { perror("my_fopen"); return 1; } int cnt = 20; const char *msg = "haha, this is my stdio lib"; while(cnt--){ my_fwrite(msg, strlen(msg), fp); sleep(1); } my_fclose(fp); return 0; }
四、【理解文件系统】
我们知道文件可以分为磁盘文件和内存文件,内存文件前面我们已经谈过了,下面我们来谈谈磁盘文件。
4.1、【初识inode】
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
在命令行当中输入
ls -l
,即可显示当前目录下各文件的属性信息。其中,各列信息所对应的文件属性如下:
在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集,起一个唯一的编号,即inode号。
也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。在命令行当中输入
ls -i
,即可显示当前目录下各文件的inode编号。注意: 无论是文件内容还是文件属性,它们都是存储在磁盘当中的。
用户只用文件名,内核只用inode编号,文件名和inode可以互相映射。自己目录内部直接保存的文件(包括文件夹)都有一份映射关系,因此同一目录下的所有文件不能重命名。所以我想在一个目录下新建、修改、删除文件需要 w 权限(可以改变文件映射关系)。
4.2、【磁盘的概念】
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
磁盘在冯诺依曼体系结构当中既可以充当输入设备,又可以充当输出设备。
比起内存,我们肯定是将文件放到磁盘中的,这是因为磁盘的特性,他速度虽然不快,但可以长期存储文件,并且容量也比较大。现在我们的电脑存储设备大多是固态硬盘,SATA接口或者M.2接口,比起磁盘来体积小速度快,但成本也高,同时数据难以恢复。所以企业更多的使用的是磁盘,也就是机械硬盘。
磁盘的设计图如图所示:
从图片中我们可以看出,磁盘一圈一圈的有很多磁道,每一个磁道又划分了很多扇区,扇区是磁盘的最小存储单元---512字节,这是因为磁盘本身比较慢,按比特位来读写效率非常差,就算只需访问1比特位,也得将整个扇区放到内存中。
如果我想向一个扇区写入,首先得选择哪一面——选择磁头,其次选择哪一个磁道,最后选择磁道中的哪一个扇区。这就是CHS定位法(柱面cylinder,磁头head,扇区sector)。
磁盘的寻找存储数据的方案
对磁盘进行读写操作时,一般有以下几个步骤:
- 确定读写信息在磁盘的哪个盘面。
- 确定读写信息在磁盘的哪个柱面。
- 确定读写信息在磁盘的哪个扇区。
通过以上三个步骤,最终确定信息在磁盘的读写位置。
4.3、【磁盘分区与格式化介绍】
理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,将线卷起来就类似于磁盘,但当我们把磁带拉直后,其就是线性的。
1、【磁盘分区】
既然这样,我们可以将磁盘空间抽象为线性空间,有好几面,每一面又有N多个磁道,每一个磁道又分为N个扇区,这不就类似于数组了呀。
比如1-100000为第一面,1-1000为第一个磁道,依次类推,那么我们对磁盘的管理,也就变成了对数组的管理。
操作系统可以按照扇区(512Byte)为单位进行存取, 但更好的是按照文件块(8个扇区 4KB)为单位进行数据存取。这样能保证8个扇区是连续的,能更好利用局部性原理,让随机寻址变连续寻址,提升效率。
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区。
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:
ls /dev/vda* -l
2、【磁盘格式化】
当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
3、【EXT2文件系统的存储方案】
假设我们现在有500G的磁盘,我对其进行分区,三个区100G,一个区200G,分区之后,需要进行格式化,也就是将管理的数据写入到块组中。每一个块组的大小是格式化的时候确定的,不可修改,具体分布如下:
实际上计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(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 Bitmap的原理如下:
4.4、【几个重要的问题】
【1】如何理解创建一个空文件?
- 通过遍历inode位图的方式,找到一个空闲的inode。
- 在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中。
- 将该文件的文件名和inode指针添加到目录文件的数据块中。
【2】如何理解对文件写入信息?
- 通过文件的inode编号找到对应的inode结构。
- 通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
- 若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。
说明一下:
一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。【3】如何理解删除一个文件?
- 将该文件对应的inode在inode位图当中置为无效。
- 将该文件申请过的数据块在块位图当中置为无效。
因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。【4】为什么拷贝文件的时候很慢,而删除文件的时候很快?
因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。
这就像建楼一样,我们需要很长时间才能建好一栋楼,而我们若是想拆除一栋楼,只需在这栋楼上写上一个“拆”字即可。
【5】如何理解目录?
- 都说在Linux下一切皆文件,目录当然也可以被看作为文件。
- 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
- 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。
注意: 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
五、【软硬链接】
1、软链接
我们可以通过以下命令创建一个文件的软连接。
ln -s 源文件名 新文件名
通过
ls -i -l
命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。
2、硬链接
我们可以通过以下命令创建一个文件的硬连接。
ln 源文件名 新文件名
我们知道,inode与文件名互相映射,硬链接不是一个独立文件,文件名不同,但inode相同,由此可推断硬链接的本质就是在指定目录内部添加的一组映射关系。同时今天我们也能明白文件权限后面那个数字是什么意思了,我们可以看到,当我们删除test的时候,inode没变,有一个值从2变成了1,该值就是硬链接数。
通过
ls -i -l
命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为924344的文件有myproc和myproc-h两个文件名,因此该文件的硬链接数为2。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。
总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。
那么为什么刚刚创建的目录的硬链接数是2?
我们创建一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。那为什么我们创建一个目录后,该目录的硬链接数是2?
因为每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。
小技巧: 一个目录下相邻的子目录数等于该目录的硬链接数减2。
3、软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
- 软链接可以链接目录,而硬链接无法链接目录,这是因为操作系统设立限制,比如当使用find查找某个文件时,使用硬链接就会发生环形链接的问题,find如果没有第一时间找到,则会陷入循环。因为硬链接的inode没有改变,他本质就是一个目录文件。
而软链接inode改变了,他本质是一个普通文件,find查找时发现名字对不上,便会跳过去寻找其他文件。
4、再谈“当前目录”
我们知道,当fopen以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?
例如,我们在BasicIO目录下运行可执行程序myproc,那么该可执行程序创建的log.txt文件会出现在BasicIO目录下。
那是否意味着这里所说的“当前路径”是指“当前可执行程序所处的路径”呢?
这时我们可以将刚才可执行程序生成的log.txt文件先删除,然后再做一个测试:回退到上级目录,在上级目录下运行该可执行程序。这时我们可以发现,该可执行程序运行后并没有在BasicIO目录下创建log.txt文件,而是在我们当前所处的路径下创建了log.txt文件。
当该可执行程序运行起来变成进程后,我们可以获取该进程的PID,然后根据该PID在根目录下的proc目录下查看该进程的信息。
在这里我们可以看到两个软链接文件cwd和exe,cwd就是进程运行时我们所处的路径,而exe就是该可执行程序的所处路径。
总结: 实际上,我们这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。
总结
本篇博客到这就结束了,感谢观看!
................................................................................越过表面,我看见你美丽的心,你最美丽
————《太美丽》