目录
一:C语言接口类比系统调用接口
C语言的文件接口,已经详细讲过了,如下:
八种顺序读写函数的介绍(fput/getc;fput/gets;fscanf,fprintf;fwrite,fread)-CSDN博客
接口不止这些,但重要的就这些,在本篇博客只用得着这些
1:fopen函数的w 和 >重定向
①:fopen函数的w
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt", "w");
if (fp == NULL){
perror("fopen");
return 1;
}
fputs("hello world\n", fp);
fclose(fp);
return 0;
}
结果:
解释:对于存在的文件会打开,不存在的文件会创建,而log.txt不存在,所以在我们的当前路径下创建了文件,再去完成了写入
此时更改代码,让其写的字符改变一下:
fputs("hello world2\n", fp);
再gcc编译,然后执行可执行程序,发现文件的内容如下:
总结:fopen的w权限,无文件则创建,有文件则打开,写入是清空在写入!
②:>重定向
执行命令:
echo hello world > log2.txt
效果:
再次执行命令:
echo hello world > log2.txt
效果:
总结:重定向>的效果和fopen的w权限一模一样!
注:fopen的w,不写内容,也会情况文件,和>直接对文件使用效果仍然一样!
2:fopen函数的a 和 >>追加重定向
①:fopen函数的a
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "a");
if (fp == NULL){
perror("fopen");
return 1;
}
fputs("hello world\n", fp);
fclose(fp);
return 0;
}
效果:
再次写入效果:
总结:fopen的a,和w的区别在于,不再清空写入
②:>>追加重定向
执行指令:
echo hell world >> test2.txt
再次执行效果:
总结:fopen的a 和 >>追加重定向效果一样!
3:当前路径
在fopen的参数中,有一个字符串参数是传路径的,我们可以传我们想打开的文件所处的路径,但是我们上面不传,其照样在某个路径下创建或者打开了文件,这个默认的路径叫作当前路径
Q:什么叫当前路径?
A:当前路径是指该可执行程序运行成为进程时我们所处的路径!这也是为什么我们生成的文件和当前可执行程序在同一目录下的原因!
Q:fopen如何获取到的当前路
A:这一点在前面的博客中谈到过,任何进程运行起来,比如这个a.out进程运行起来,我们可以获取该进程的PID,然后根据该PID在根目录下的proc目录下查看该进程的信息,进程以进程PID为名的目录,里面存储的大量的信息,其中的cwd则保留了"该可执行程序运行成为进程时我们所处的路径"
类似下图(仅做参考)
所以在cwd这个文件中就存储了该可执行程序运行成为进程时我们所处的路径,然后再拼接上我我们的文件名字,就可以完成创建打开等一系列操作了 ,这就是为什么,我们默认不写的时候,能够在可执行程序运行起来的路径中创建文件或者打开文件!
注:可以在fopen所处的main中用chdir函数修改路径,则我们的文件的创建和打开将会在修改后的路径中进行,不再演示
4:三个流
我们想向一个文件中读写的时候,必定要使用fopen先打开这个文件,且我们知道,在Linux下,一切皆文件,显示器这种硬件,本质在OS的底层也是一个文件,所以我们用printf或者cin向显示器这个文件进行写操作,所以我们显示器能够显示出效果!
Q:那为什么我们的printf和cin函数对显示器文件写入的时候,我们并没有调用fopen,而是直接使用prinf或者cin函数来进行写入?
A:因为任何一个进程有运行起来,就会默认打开显示器文件,所以不用再用fopen打开它!
打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。
所以三个流对应的是两个文件!
而我们知道我们fopen函数的返回值都是FILE*类型的,所以我们才用FILE*l来接收其的返回值,然后再向读写函数中传递这个FILE*类型的值,所以stdin、stdout以及stderr的类型必定也是FILE*!
查看man手册我们就可以发现,stdin、stdout以及stderr这三个家伙实际上都是FILE*
类型的。
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
例子:用IO函数直接向stdout流中写入:
#include <stdio.h>
int main()
{
fputs("hello world\n", stdout);
return 0;
}
解释:所以我们可以将读写函数中的参数用stdout填写,实现直接向显示器打印的效果!
总结:三个流,本质就是三个文件被打开后的返回值,类型为FILE*
二:系统调用接口
我们知道C语言的这个IO接口想在Linux环境下跑,本质就是封装了Linux的系统调用接口,好比我们平时在电脑上下载一个VS直接使用C语言的IO接口,本质就是我们电脑是windows系统,所以我们的C语言IO接口封装了windows系统的系统调用接口罢了,这也是为什么C语言既能在Linux下运行,也能在windows上运行,因为其都封装了对应系统的接口,这也是为什么说C语言具有可移植性!所以下面我们来了解一下Linux下的关于文件IO的系统调用接口!
注:系统调用接口和C语言的接口的名字仅仅少了个f
1:open
int open(const char *pathname, int flags, mode_t mode);
①:open的第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
· 若 pathname 以路径的方式给出,则当需要创建该文件时,就在 pathname 路径下进行创建。
· 若 pathname 以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建
(当前路径的含义---->可执行程序运行起来的路径 )
②:open的第二个参数
open函数的第二个参数是flags,表示打开文件的方式。
其中常用选项有如下几个:
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
③:open的第三个参数
open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
注意: 当不需要创建文件时,open的第三个参数可以不必设置(因为权限指的是新建文件的权限)
④:open的返回值
open函数的返回值是新打开文件的文件描述符。
看完这4点后,疑问应该如下:
Q1:open的第二个参数为什么填写的是一堆大写字母?
Q2:open的第二个参数如果想要达到fopen的w效果,岂不是要用两个参数? (O_CREAT和O_WRNOLY),两个参数的写法是什么?
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
A1 :因为每个选项都是宏,所以是一堆大写字母,官方的宏都是大写字母!
A2:写法如下
O_WRONLY | O_CREAT
Q3:为什么用按位或运算符 | 就能起到双份效果?
A3:系统接口open的第二个参数flags是int整型,所以像O_WRONLY | O_CREAT这样,本质就是32比特位和32比特位之间的按位或运算,得到的结果就是会保留两个整形之中的1,而我们系统检测到某一位为1的时候,就知道它具有一个功能,多个1则多个功能!
下图为Linux 系统中open函数第二个参数的宏定义:
总结:想要一个参数起到不同的作用,我们可以使用让一个整形的不同比特位具有不同的功能,然后按位或运算,就可以了!不一定非要传递多个参数,才能起到多功能!
所以我们知道了fopen函数的本质:
int fd=Open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);////不存在则新建 清空式写入
//和fopen的w效果一样
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//不存在则新建 追加式写入
//和fopen的a效果一样
这就是为什么说fopen封装了open(C接口封装了系统调用),因为open本来就有对应的功能,而fopen只是将其疯转,让其简化,且具有移植性!!
Q4:文件描述符是什么?
A4:首先应该明白的是,open函数的返回值一定是用来找到该文件的,类比fopen函数,其返回值是FILE*类型的值,我们在进行读写函数(fwrite futs..)的时候,需要将这个值填写到读写函数的参数中,读写函数才能够找得到该文件,所以文件描述符一定是用来找到文件的!!文件描述符是整形!代表的是一个数组的下标!现在暂且了解到这即可
Q5:先不问文件描述符是怎么找到文件的,我想问对于C中默认打开的三个流对应的两个文件,在open的角度来看变成了什么?
A5:很简单,在C接口中,你给这三个流返回的是FILE* ,那在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开始的,因为在open的返回值中,0,1,2已经被三个流占用了!所以从3开始!
Q6:如何证明就是被三个流占用了?
A6:我们在用open函数前,我们先用close(系统调用)来关闭一下stdin(标准输入流 对应键盘)
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(0);//关闭了标准输入流 对应的是键盘
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;
}
运行结果:
解释:我们关闭了标准输入流 其对应的文件描述符为0,所以我们的1空余了出来,我们创建文件的时候,就用到了1,这里设计文件描述符的分配规则,在后面会解释!你也可以close(1);关闭默认的输出流 对应显示器,这样你程序运行的结构就看不到了,因为无法显示到显示器上!
2:write
write函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);
我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
所以buf一般就是一个字符串的名字,然后count就是strlen(字符串的名字)!
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-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;
}
运行结果:
注:用strlen就是因为其不计入'\0',只要文件的内容!若是strlen(str)+1 则文件中显示出来是乱码
3: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;
}
运行结果:
4:close
系统接口中使用close函数关闭文件,close函数的函数原型如下:
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
至此,文件IO的系统调用的常用接口介绍完毕~
4:结构图
文件描述符是如何找到文件的?
①:struct file
首先我们知道:文件是由进程运行时打开的,一个进程可以打开多个文件,而OS当中又存在大量进程,所以,在OS中任何时刻都可能存在大量已经打开的文件。因此,OS肯定要对已经打开的文件进行管理,根据"先描述 再组织"的思想,所以OS会为每个已经打开的文件创建各自的结构体,名为:struct file,struct filez之间以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的管理!
其次我们知道:当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
这是我们以前就学会了,现在我们要在这个图的基础上,扩展了!!
②:files_struct和fd_array
· files_structs是一个结构体
· fd_arrahi是一个指针数组 也叫作文件描述符表
所以:task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可
至此,我们知道为什么文件描述符能够找到一个文件了!
Q1:如何通过此图理解默认的流?
A1:由图可知,文件描述符表的0 1 2 对应的位置已经被三个流占用,元素内容就是三个流对应的文件对应的struct file的地址!所以默认打开三个流的本质就是:OS就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流
Q2:磁盘文件 和 内存文件的区别?
A2:当文件存储在磁盘当中时,我们将其称之为磁盘文件,而当磁盘文件被加载到内存当中后,我们将加载到内存当中的文件称之为内存文件。磁盘文件和内存文件之间的关系就像程序和进程的关系一样!
三:文件描述符的分配规则
再上面我们演示用close关闭标准输入流的时候,我们其实已经能看出文件描述符的分配规则!
1:创建五个文件,观察fd
#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;
}
2:close(0)后 观察fd
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(0);
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:close(0)和close(2)后 观察fd
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(0);
close(2);
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;
}
结论: 文件描述符是从最小且没有被使用的fd_array数组下标开始进行分配的(从小到大 见缝插针)
Q:为什么讲文件描述符分配的规则,这些系统自己就做了,用你操心吗?
A:为了理解 输出重定向> 和 追加重定向>> 和 输入重定向< 的本质!
4:输出重定向的底层
输出重定向功能:将我们本应该输出到一个文件的数据重定向输出到另一个文件中。
例子:
我们本来是朝显示器输入字符"hello":
但现在我们想向一个file.txt中输入hello,就需要用到 输出重定向了>
解释:本质不就是关闭了默认输出流(显示器),把file.txt的文件描述符设为0了吗,让file.txt文件变成了默认输出流!所以才会把内容写进这个文件!如下图:
例子:用open函数模拟实现输出重定向>(文件名为file2.txt 用于区分file.txt):
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);//关闭默认输出流 stdout 显示器
int fd = open("file.txt2", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
printf("hello");
fflush(stdout);
close(fd);
return 0;
}
运行结果:
Q1:默认输出流被关闭了,printf还能够直接使用吗,不给其传递file2.txt对应的FILE*吗?
A2:大可不必!printf函数虽然是默认向stdout输出数据的,但printf实际上就是无脑的向文件描述符为1的文件输出数据。所以直接使用即可!
Q2:fflush是什么,为什么要用fflush?
A2:暂不解释,第四大点会讲!
5:追加重定向的底层
追加重定向和输出重定向的唯一区别就是:输出重定向是清空式输出数据,而追加重定向是追加式输出数据。
直接模拟实现吧!例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
int fd = open("file2.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
if(fd < 0){
perror("open");
return 1;
}
printf("hello\n");
fflush(stdout);
close(fd);
return 0;
}
解释:向刚才的file2.txt再次打印数据,其并未情况之前的字符串
总结:> 和 >> 的本质就是关闭了标准输出流,然后结合open函数的第二个参数实现出清空和追加的功能!
6:输入重定向的底层
例子:
我们想向之间写入了两个"hello"的file2.txt中读取内容:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("file2.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函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是无脑的向文件描述符为0的文件读取数据。
7:dump2
当我们想要在函数中实现一个输出重定向或者追加重定向,不需要向上面那样手动的关闭某个流!我们只需进行fd_array数组(文件描述符表)当中元素(struct file 的地址)的拷贝即可。
例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出的内容,重定向到了文件log.txt。
而OS综合上述功能,给我们提供了dump2函数!
int dup2(int oldfd, int newfd);
// 源 目标
函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中
函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
· 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
· 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
例子:
#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时获取到的文件描述符fd和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,此时文件描述符为1的文件变成了log.txt!因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。
8:标准输出流/标准错误流的区别
我们知道标准输出流stdout以及标准错误流stderr对应的设备都是显示器。那我们进程输出重定向的时候,我们关闭的是下标为1,因为其对应显示器,所以我们就把数据重定向到了一个文件中!那我们关闭标准错误流stderrd的文件描述符2可以达到同样的效果吗,毕竟其对应的也是显示器
例子:
代码中分别向标准输出流和标准错误流输出了两行字符串:
#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当中:
./a.out > log.txt
运行结果:
解释:我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。
结论:
实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。
9:FILE* 和 fd 的关系
我们C接口的fopen函数返回的是FILE*类型的值fp,而系统调用接口返回的是int类型的fd,关于fd我们已经知道怎么通过fd找到一个文件了,那C接口的读写函数如何FILE*来找到文件呢?
我们需要明白的是,C接口本来就是封装的系统调用接口,所以毫无疑问,C接口用的方法一定是系统调用的方法,就好比:
想象你在一家餐厅吃饭:
系统调用:像是直接对厨房里的厨师下指令(最底层操作,比如“煎牛排,五分熟”)。
C语言接口:像是通过服务员点餐(封装后的便捷方式,比如“我要一份牛排”)。
为什么需要服务员(C接口)?
你不需要知道厨师具体怎么煎牛排(避免直接操作内核)。
服务员会帮你处理细节(比如转达火候、配菜等),并返回结果(上菜)。
那毫无疑问,fopen肯定也是通过fd(文件描述符)来找到文件的,所以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
};
Q:所以C语言当中的fopen函数究竟在做什么?
A:fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
说白了,仅仅拐了个弯,系统调用能直接获取fd,而C接口要通过FILE*去获取fd罢了!!!
四:C的缓冲区
注:谈论的只是C语言的缓冲区 不是OS的缓冲区
1:缓冲区的意义
例子:我经常送礼物给我的女朋友,我在贵州,她在北京,我可以选择每准备好一个礼物,就坐车去北京送给她,也可以选择等多准备几个礼物,再邮寄给她!
很明显,当我们的礼物很多的时候,我们有一件就送一件和堆满了,再去邮寄,毫无疑问,后者的时间效率高得多,而取件点就是一个缓冲区,其是一个内存,存放的就是我们的数据!
注:在C语言的角度来看,每个文件都有自己的缓冲区,被FILE结构体所维护!而OS也有自己的缓冲区(该缓冲区不属于某个文件),就好比我楼下的取件点和她楼下的取件点!所以刷新我们目前说的刷新缓冲区,本质就是指的将C的缓冲区中的内容刷新到了OS中的缓冲区中!
2:刷新策略
寄快递你可以选择顺丰,京东,所以刷新缓冲区也有不同的策略!
C语言缓冲区的三种刷新策略:
①:无刷新 没有缓冲区
②:行刷新->遇见\n刷新(显示器)
③:全刷新->缓冲区满了才刷新(普通文件)
两种特殊情况:
①:强制刷新(fflush()函数)
②:进程退出的时候(进度条不加\n显示不出来 程序退出显示出来的原因)
刷新要结合普通的刷新策略和特殊情况来看,好比你打算寄顺丰,但是女朋友生气了,你真好选择更快的京东速运(直接上飞机的那种)!
例子1:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello1\n");
printf("hello2");
sleep(5);
return 0;
}
运行结果:
解释:我们是向显示器打印
①:hello1直接被打印出来,是因为向显示器打印,是行刷新->遇见\n刷新!
②:hello2五秒后才被打印出来,是因为我们向显示器打印,但是hello2后面没有\n,所以其会被存储到缓冲区,然后进程退出的时候(特殊情况),被刷新打印到了显示器上
例子2:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello1\n");
printf("hello2");
fflush(stdout);
sleep(5);
return 0;
}
运行结果:
解释:为什么hello2被直接打印了,因为我们用fflush函数进行了强制刷新!所以第三大点的第4小点中对于fflush的疑问我们可以解答了,fflush是一个用来强制刷新缓冲区的函数,而在第三大点的第4小点什么要用fflush,就是因为我们关闭了默认输出流stdout,将内容写进了文件中,所以导致现在的刷新策略变成了全刷新->缓冲区满了才刷新(普通文件),所以我们用fflush将C语言的缓冲区的内容 强制立即写入文件,避免未写满缓冲区导致内容迟迟不写进文件中!但其实这只是表面的理解......
Q:可是程序退出不也是特殊情况吗,那main函数结束,就应该会把文件的内容刷新到文件中啊,为什么还要fflush呢?
A:
printf("hello"); // 数据在缓冲区
close(fd); // 文件描述符已关闭,缓冲区可能无法写入
return 0; // 程序退出时,缓冲区已无关联文件
所以你等待程序退出去刷新数据到文件中,是不可取的,文件都已经被关闭了,相当于你的文件已经不是打开状态了,还如何进行写入?所以fflush(stdout)
的关键作用体现在 close(fd)
前写入文件!!
3:证明缓冲区的存在
例子:分别用了两个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;
}
运行结果:
解释:符合预期,毕竟我们向显示器写入,采取的行刷新,遇见\n就会把C语言缓冲区的内容刷新到OS的缓冲区!
但当执行下面命令时:
./a.out > log.txt
log.txt内容如下:
Q:为什么文件当中的内容与我们直接打印输出到显示器的内容是不一样的?!
A:首先需要知道的是printf和fputhiC接口,所以其具有C语言的缓冲区,而write是系统调用函数,其当然没有C语言缓冲区!
第一次打印的解释:
向显示器打印,刷新策略是行刷新(遇到\n),所以在fork之前,就已经被刷新到OS的缓冲区了,所以不论是系统调用还是c接口总共打印三句;
重定向打印到文件的解释:
向文件中写入,刷新的策略是全刷新->满了才刷新,此时我们的三句话,第一句是已经写入了OS的缓冲区,第二句第三句还在C语言层面的缓冲区没有刷新(因为缓冲区没满)。此时我们fork之后再退出,此时不论是父子进程的哪一个退出,都会触发写时拷贝,也就是父子各自的缓冲区中都有第二句和第三句,然后纷纷退出,全部写进了文件里面!所以有五句,这个例子也证明了C语言自带缓冲区的存在!
4:模拟缓冲区
由于讲解和实现的内容过多,所以单独写在了后续的博客中~
五:磁盘
Q:为什么要讲磁盘?
A:之前讲的都是被打开的文件,其存储到内存中,那如果没有被打开,其就存储在磁盘中,而OS用open函数打开一个文件,本质就是要现在磁盘上快速的查找到一个文件去打开,所以连磁盘都不知道,都没见过,讲起来实属抽象!
而OS想要管理磁盘上的文件和已经加载到内存中的文件,这才有了文件系统!
例子:快递站(文件系统)管理快递(文件) 让我们更快找到
类比:快递站->磁盘 快递老板->文件系统 我们->OS 包裹->未打开文件
1:磁盘的机械构成
磁盘就是左图中的这一个圆形的盘,其双面都可以进行重复的增删查改数据, 而右图光盘只可以单面存储数据,并且只可以进行不可逆的写数据和重复的读,二者差距可见一斑!而左图中有一个磁头,就是用来读取数据的,当磁盘在主机中工作起来的时候,磁盘会高速旋转,磁头会来回摆动!磁头无限接近于磁盘,但不会接触(由常识都知道,高速旋转还接触必定高温起火)
上图中的仅仅是我们家用电脑用的磁盘,在企业中,对磁盘的存储能力要求极高,所以如下:
由图可知,磁盘多了,磁头也就多了,一层磁盘对应两个磁头
注:
现在家用电脑,特别是笔电,一般都不用磁盘,而是SSD,而台式机和企业才会用磁盘!
所以为什么以前的用磁盘的笔电,一摔,就会蓝屏或者数据丢失,本质就是摔的时候磁头接触盘面,导致刮花,影响数据,类似光盘刮花!
2:磁盘的物理存储
物理存储要了解的就是:盘面 磁道 扇面 扇区 (由大到小)
解释:
①:盘面(Platter)
俗称的正反面,叫作磁盘的盘面,正面是一个盘面,反面是另一个盘面
②:磁道(cylinder)
磁道是指单个盘面的一个圆形路径,所以一个盘面有多个磁道;其也叫作柱面,因为多个磁盘的一条竖线上的词磁道,所围成的形状就是一个柱面,如下图:
③:扇面
盘面被分成许多扇区的区域
④:扇区 (sector)
磁盘进行IO的基本单位 即使你只想修改某个扇区中的一个比特位 你也得把这个比特位所处的扇区加载到内存 ;扇区一般512字节;但靠近圆心的扇区和远离圆心的扇区即使面积不同 但经过密度的调整,也会大小一致
所以为什么磁盘运行的时候,磁盘高速旋转,磁头来回摆动,就是因为找到某个磁道上的扇区:
对磁盘进行读写操作时,一般有以下几个步骤:
- 确定读写信息在磁盘的哪个盘面。
- 确定读写信息在磁盘的哪个磁道。
- 确定读写信息在磁盘的哪个扇区。
通过以上三个步骤,最终确定信息在磁盘的读写位置,这叫CHS定位法~
3:磁盘的逻辑存储
要想理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想上图磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的,类似一条直线。
所以磁盘就可以看作如下:
所以磁盘在OS看来就是一个数组罢了( sector array[n] ),不同位置对应不同的扇区!
而OS觉得管理的单位为一个扇区,才512字节,太小了,所以OS用一个数据,其中元素叫作数据块,一个数据块对应 sector array[n] 数组的八个元素,所以大小为4KB,如下提:
注:OS的这个数组存储的地址叫LBA地址
磁盘分区:
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
解释:
①:当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。
②:其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
而大名鼎鼎的文件系统,就位于每一个独立的区中,一区一份,我们学习的文件系统就是EXT2!
六:文件系统
我们现在知道了每个分区都有一个文件系统,那么EXT2文件系统是如何管理一个区的?
如上图所示,文件系统不是直接管理一个区,而是把一个区分成了多个块组,管理好了每一个块组,就自然而然的管理好了整个分区
注:
Boot Block仅存在于分区1中,协助开机的时候,让电脑能够找到操作系统软件,了解即可
所以学习EXT2文件系统就是学习每一个块组的内部构成:
文件分为属性和内容,其是分开存储的,而且能够想通其肯定是分开存储的,属性必定是存在于管理单个文件的结构体中,而内容肯定不在该结构体中(先描述 再组织思想);且文件的名字不属于文件属性,因为其大小不固定,所以肯定不会放进一个结构体中!
所以在EXT2中,文件内容存储在数据区中,文件属性存储在struct inode结构体中
1:inode
inode是每一个文件的编号,独一无二,好比进程的PID一样
ll -i //可以查看文件的indoe
下图红框部分就是每个文件对应的inode
我们知道了文件的属性存储在struct inode结构体中,而inode这个变量也是存储在其中的,因为其是属性,注意名字类似,但是一个是整形变量,一个是结构体!
2:数据区(Data Block):
数据区就是块组中的一块区域,专门用来存放文件内容 ,里面单位是前面说过的4kb的数据块(OS用来管理磁盘数组的那个数组的基本单位),文件的内容就是存储在数据区的,所以文件内容变化对应的就是在数据区中数据块使用的多少罢了!
3:inodetable
inodetable(inode表),指的是一段连续的,用来存放文件属性的数据块;里面存放的就是一个 一个的inode结构体(128字节) ,所以一个数据块就能保存4kb/128 = 32个文件的struct inode
注:
该数据块不是位于数据区中,因为数据区中的数据块是用来存储文件内容的,而文件属性对应的struct inode肯定不可能也在数据区中,只是二者的存储单位都是一个个的数据块罢了
4:inode位图
inode位图(inode Bitmap):用比特位来反映每个struct inode 的状态!
比特位的位置:第几个struct inode (1w个inode 只需约1kb来表示即可!)
比特位的内容:表示该struct inode是否被使用!
5:块位图
块位图(Block Bitmap):用比特位来表示数据区中数据块的状态!
比特位的位置:第几个数据块
比特位的内容:对应的数据块是否被使用
Q1:文件的内容和属性分开存储,那请问如何联系起来?假设现在我们得到了sruct inode,又如何找到存储文件内容的数据块呢?
A1:sruct inode本身除了有文件属性外,还存储了一个数组 in block[15 ](也叫作i_block[15]
),其存储了文件内容在数据区中数据块的下标!所以能够找到
Q2:in block[15 ],最多只能存储15个数据块的下标,也就是15×4 = 60kb,那文件过大怎么办?
A2:
数组索引 (i_block[N] ) |
作用 | 指向的块类型 |
---|---|---|
0~11 (前12个元素) | 直接块(Direct Blocks) | 直接存储文件数据 |
12 (第13个元素) | 一级间接块(Single Indirect Block) | 存储指向数据块的指针 |
13 (第14个元素) | 二级间接块(Double Indirect Block) | 存储指向一级间接块的指针 |
14 (第15个元素) | 三级间接块(Triple Indirect Block) | 存储指向二级间接块的指针 |
如下图:
(
i_block[15]
的存储机制,仅作了解即可)
6:两个理解
Q1:如何理解创建一个空文件?
先通过遍历inode位图的方式,找到最近的一个为0的比特位将其改为1。
再去索引到该比特位对应的 inodetable中的空余的inode结构体填写属性
然后同理再通过blockbitmap 去找在data blocks未使用的的数据快,将其置为1,并使用,然后inode结构体中的int block[15]记录数据区中使用的数据块的下标
Q2:如何理解对文件写入信息?
通过文件的inode编号找到对应的inode结构。
通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块下标和inode结构题中i_block[15]
的对应关系。
所以为什么一个几十个G的游戏下载需要几小时不等,但是卸载只需几秒,因为不用去删除数据块上的内容,而是对应的比特位置为0(两个位图),等到下次下载新的游戏,将这些数据块覆盖即可
7:inode编号的初始化机制
我们ls -i就能看到文件的inode编号,又知道inode编号特别的重要,可以用inode编号快速的找到一个文件对应的inode结构体,从而进行文件的增删查改!
Q1:那inode编号是按照什么机制赋给一个文件的?
A1:OS在每一个块组中,会先确定该个块组会有多少个struct inode ,假设现在是在第一个分区的第一个块组中,且OS确定了给该块组分配1w个struct inode!所以此时每个struct inode中inode的编号就是1~10000;(第二个块组就是10001~20000,以此类推);现在新建了一个文件,OS会先去inodebitmap中找闲置的struct inode对应的位0的比特位,假设在第20个位中找到了位为0的,将其置为1后代表该位对应的struct inode已经被使用,然后用这下标20,在inodetable中找到对应的inode结构体,除开新建文件属性的填写,最重要的是=填写结构体中的inode编号,编号填写的规则就是下标(相对偏移量)+该分组的起始编号,得到21(20+1)!所以如果实在第一分区的第二块组,该文件的inode就应该是10001+20 = 10021!这就是inode编号的初始化机制!
注:inodetable里面全是结构体,所以应该说偏移量!下标只是方便理解
Q2:那怎么通过inode找到对应的struct inode?
A2:两个块组例子解释
假设inode为21,OS会判断其处于分组1,然后再减去inode起始编号(0),得到inode在该块组的中的inodebitmapd的下标了为20,所以就能快速查找了到struct inode了!
假设inode为10021,OS会判断其处于分组2,然后再减去inode起始编号(10001),得到inode在该块组的中的inodebitmapd的下标了为20,所以就能快速查找了到struct inode了!
所以,inode编号对应的文件在每个分区中是唯一的!因为不同分区的inode编号都是从1开始的,所以两个分区同一个inode编号对应的不是同一个文件!这和上文的inode编号的初始化机制相照应
8:超级块
超级块(Super Block)存储了整个分区的文件系统的 全局信息,相当于该分区的文件系统的“大脑”。如果超级块损坏,文件系统可能无法挂载或访问!
其用来存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。所以说Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,整个分区无法使用!
Q:一个分区只有一个超级块(Super Block),容错岂不是很低?
A:所以为了增加容错!Super Block会通过某个比例,存在于该分区内的某些块组中,因为其损坏的后果太严重!所以不同块组的Super Block肯定是一致的!所以当我们的电脑误删一些文件后,蓝屏,你重启后,询问你是否需要修复,本质就是将副本超级块内容拷贝到损坏的超级块!
注:一般分区的第一个块组必有一个超级块,其他的按照比例随机存储在其他块组,这也是为什么我们的图中包含了超级块,因为改图本来就是分区的第一块组!
9:GDT
GDT(Group Descriptor Table)又叫块组描述符,描述该分区当中块组的属性信息!
既然是描述块组,所以每个块组有一个GDT,GDT是一个结构体,OS管理块组就是管理的GDT
其存放的是信息为:
①:块位图(Block Bitmap)的位置
记录该块组中哪些数据块已被使用,哪些是空闲的。②:inode 位图(Inode Bitmap)的位置
记录该块组中哪些 inode 已被使用,哪些是空闲的。③:inode 表(Inode Table)的位置
存储该块组中所有 inode 的起始块号。④:块组中空闲块的数量
块组中空闲 inode 的数量
块组中目录的数量(Ext4 特有)
用于优化目录查找。
10:文件名和inode的关系
①:文件的内容是什么
Q:所以为什么我们平时都是用文件名来查找文件,不应该用inode编号吗?
A:
目录是文件吗?是!只不过为了区分目录和文件,我们各有叫法,但是本质目录也是一个文件,所以目录就会有自己的inode编号!
那目录的内容放什么?放文件?我们在目录中的文件,并不是目录存储的的内容,否则我们进入一个目录进行ll的时候,应该显示文件的内容啊,结果我们显式的是文件的一大堆属性,所以目录的内容并不是其中的文件,而是文件的属性吗?所以我们进入目录执行ll -i指令如下:
但是我想说的是,文件的属性是由文件自己的struct inode存储,所以目录不存储其中文件的属性,而是建立映射关系:inode编号对应一个文件名!那些属性你能看见,不过是OS去每个文件对应的struct inode中获取再显示给你看的罢了,对于目录来说,其内容就是inode编号和文件名的映射关系,所以为什么我们能使用文件名,因为其和inode都是独一无二的,为什么要建立一个映射关系,让用户使用文件名呢?那我问你,你想叫一个人的名字还是一个人的身份证号?很显然,文件名更直观简单!
②:重新理解目录的wr权限
目录的w权限
w没有权限我们只知道不能在目录中创建或者删除文件,但是为什么呢?
因为w权限应该影响的是该目录的内容写操作,而目录的内容就是文件名和inode的映射关系,所以OS会让你无法建立文件名和inode的映射关系!同理删除文件会删除目录内容中的某一条inode和文件名的映射关系,所以也会被OS驳回操作,这就是为什么目录w权限影响其中的文件增删!
目录的r权限
r没有权限我们只知道不能在该目录中通过ls相关的指令来查看信息,为什么?
因为文件的信息指的不就是文件的内容,无r权限所以不让你看
Q1:但是我想看文件的属性也不行吗?文件的属性(如权限、大小、类型等)存储在 文件的 inode 中,上文不是说过其部署属于目录内容吗?那我ls相关指令,为什么会让我连文件的属性也卡不见?
A1:获取文件属性需要先读取目录内容(即 文件名 → inode
的映射表),得到了inode才能去拿到文件的属性(而这一步受目录的 r
权限控制),而inode都不让你拿,谈何观看文件属性?
Q2:那我们获取一个文件的inode与文件名的映射关系,要去所属的目录A中找,那我们要找到目录A,不是也要去目录A所属的目录B中得到目录A的inode和目录名的映射关系吗?以此类推,岂不是无限递归了?
A2:OS查找一个文件的inode和映射关系,永远是默认从根目录开始查找 所以不存在套娃
Q3:那/的inode如何获取?
A3:OS一开始就能直接获取,不用过多了解
七:软硬链接
1:软链接
建立软链接指令如下:
ln -s a.out myaout.soft
// 被链接 谁去链接
效果如下:
通过ls -i -l
命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
Q:软链接的作用是什么?
A:软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。所以我们执行myaout.soft等同于执行a.out:
我的例子是在统计目录下软链接,只是为了方便写例子,一般软链接都是在浅目录链接深目录的一个可执行程序,好比windows中的快捷方式!
所以当删除了源文件后,链接文件不能独立存在,虽然仍保留软链接文件名,但却不能执行或是查看软链接的内容了。
2:硬链接
通过以下命令创建一个文件的硬连接:
ln a.out mya.out.hard
// 被链接 谁链接
通过ls -i -l
命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
注:文件权限后面的数字就是该文件的硬链接数,硬链接数属于文件的属性之一!
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为405533的文件有a.out和myaout.hard两个文件名,因此该文件的硬链接数为2。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。
总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。
Q:iinode不是文件唯一的吗 为什么硬链接后有两个文件的inode一致?这不是矛盾吗?
A:硬链接不是独立的文件,所有硬链接共享相同的 inode 和数据块,只是文件名不同罢了,就好比一个身份证对应的人叫作张三,张三还有小名三三,绰号小三等等,这些名字都是同一个人,所以不存在一个身份证被多个人使用的情况;所以该inode仍然是唯一的,没有被多个文件共享!
软硬链接的区别
①:软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
②:软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
3:目录的硬链接数
我们会发现,任意创建一个新的目录,其的硬链接数一定为2!
因为每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir1(dir2/dir3)另一个就是该目录下的. ,所以刚创建的目录硬链接数是2。当我们进入dir1,观察dir1目录下的.文件的时候,会发现二者node号是一样的,也就可以说明它们代表的实际上是同一个文件。
定律:一个目录下有多少个子目录 通过硬链接数-2计算得到!
A:因为如果你有子目录,那你这个目录一定会被子目录的..进行硬链接!