目录
一.什么是文件操作
如果学过其他编程语言,那应该都知道文件操作以及IO,那么在linux中,文件操作又是如何进行的呢?以及我们应该如何理解文件操作和IO?
C语言中的文件操作
在讲解Linux的文件操作之前,我们先来看一下在C语言中,文件是如何操作的。同时在后面我也会稍微围绕C语言的文件操作来讲解。
C语言的文件操作,博主之前写过一篇文章,所以这里就不再多说,可以直接看下面这篇博客。
Linux中的文件操作
熟悉完了C语言中的文件操作,那么在Linux中又是怎么样的呢?
首先我们来看一个问题,在众多的编程语言中,每一个语言的文件操作都是不同的,但是归根结底,它们在底层的方式却是相同的,什么意思呢?
文件我们都知道,它是存在磁盘(硬盘)上的,既然是磁盘,那么它就是一个硬件层面上的东西,对于一个硬件层面上的东西,如果我们想要操作它,那就必须由操作系统来进行,所以说,不管是什么语言的文件操作,到最后都是由操作系统来进行的,即文件操作的底层都是一样的。只不过是不同的语言对系统调用进行了不同的封装罢了。
如下图所示:
系统接口比编程语言的函数更加底层。
既然不同的语言的文件操作归根结底都是去调用系统接口完成的,那我们现在来学一下在Linux中的文件操作的系统接口。
close系统调用
close系统调用是用于关闭文件的接口,当一个文件被打开后,就一定要关闭,我们通过这个系统接口来完成关闭文件。close接口如下:
其中,fd是这个文件的描述符,文件描述符会在后边提到。
open系统调用
在Linux的系统调用中,有一个open的系统接口,这个接口就是将一个文件打开,我们先来看一下手册。
我们可以看到,这里有三个接口,其中这里只介绍前两个。
首先,这些接口的返回值都是一个整形,指的是文件描述符,当文件打开失败时,返回-1,打开成功返回>=0的数,至于文件描述符到底是什么,我们后面再解释,同时这两个接口都有相同的两个参数,其中第一个很好理解,就是我们需要打开文件的路径,这个路径可以是相对路径,也可以是绝对路径,第二个参数是一个标记位,那么标记位是什么,我们来解释一下。
标记位
标记位顾名思义就是用来做标记的,但具体怎么做呢?
一个整形由32个比特位组成,那么我可以将一个整形分为32种情况,每一种情况都是只有一位为1,其它位都为0,如下图所示:
同时在使用宏定义来完成我们的标记位。我们来看代码来理解。
#include<stdio.h>
#define ONE (1<<0)//十进制为1
#define TWO (1<<1)//十进制为2
#define THREE (1<<2)//十进制为4
#define FOUR (1<<3)//十进制为8
void show(int status)
{
if(status & ONE)
{
printf("one\n");
}
if(status & TWO)
{
printf("two\n");
}
if(status & THREE)
{
printf("three\n");
}
if(status & FOUR)
{
printf("four\n");
}
}
int main()
{
show(ONE);
printf("----------\n");
show(TWO);
printf("----------\n");
show(THREE);
printf("----------\n");
show(FOUR);
printf("----------\n");
show(ONE | TWO | THREE);
return 0;
}
运行结果:
通过上面的这种方式,就可以实现我们的标记位效果了。
理解了标记位后,我们在来看我们的open接口
这个flags常用的标记位有这些:
O_RDONLY: 只读,不能写
O_WRONLY: 只写,不能读
O_RDWR: 可读可写
O_CREAT: 如果文件不存在,则创建这个文件
O_APPEND: 在文件末尾追加
O_TRUNC: 每次打开文件后,将文件的内容清空
当然远不止还有这些。
我们来演示一下用法。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME,O_WRONLY | O_CREAT);//打开文件,且已读的形式打开,如果文件不存在,则创建这个文件
if(fd == -1)//说明文件打开失败
{
perror("open:");
}
close(fd);//关闭文件
return 0;
}
当我们的程序运行起来的时候,由于原本没有log.txt这个文件,所以就自动创建了一个,因为在status参数中,我们加了O_CREAT,但是创建出来的这个文件却是红的。如下图所示。
![]()
这是为什么呢?
首先,我们在创建文件的时候,文件都会有一个默认的权限,其中普通文件是666,目录是777,但是我们这里创建文件的时候,并没有告诉操作系统,我们所创建的log.txt是什么权限,所以才会导致红的问题,所以这里就要用到open接口的第三个参数,传递一个权限值过去,如下图所示:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);//打开文件,0666指创建的文件默认权限为666
if(fd == -1)//说明文件打开失败
{
perror("open:");
}
close(fd);//关闭文件
return 0;
}
当运行这段代码的时候,我们的文件可以正常的被创建出来。
write系统调用
当我们的文件打开成功后,就可以进行读写操作了,那么具体要怎么进行了,我们接着往下看。
首先我们来看一下write接口的声明:
这个系统调用有三个参数:文件描述符,要写入内容的指针变量,要写入的字节个数。
对于这三个参数,我们一个一个来分析一下:
文件描述符:这文件描述符很好理解,就是open的返回值,所以write中的fd指的就是给这个文件进行写入操作 。
buf指针:指向要写入数据的缓冲区的指针,通俗的讲就是你要写入内容的起始地址。
count:你要写入数据的字节数。
用法示例如下:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);//打开文件,0666指创建的文件默认权限为666
if(fd == -1)//说明文件打开失败
{
perror("open:");
}
char arr[100];
int cnt = 5;
while(cnt)
{
sprintf(arr,"%s:%d\n","hello linux",cnt--);//将我们要写入的内容格式化输出到arr数组中
write(fd,arr,strlen(arr));//将arr数组中的内容写到log.txt文件中
}
close(fd);//关闭文件
return 0;
}
当我们运行起来后,就会在当前目录中创建一个log.txt的文件,以及文件的内容如下:
在这里,在补充一些内容,虽然我们现在已经知道了如何写文件,但是这个过程是建立在这个文件是空文件的基础上的,如果我们在不是空文件的基础上,在进行写入操作,会出现问题,因为文件之前的内容不会被清空,所以对于这种情况,我们可以再打开文件的时候,再加一个标记位,如下图所示。
read系统调用
学习完写入的操作,现在再来看一下读的操作。同样的,我们先来看一下它的手册。
同样的read也有三个参数:要读文件的文件描述符,将文件内容读到buf指针里,要读几个字节。
其中,当读到文件末尾时自动结束。
我们要演示一下用法。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME, O_RDONLY, 0666);//打开文件,0666指创建的文件默认权限为666
if(fd == -1)//说明文件打开失败
{
perror("open:");
}
char arr[100];
read(fd,arr,sizeof(arr)-1);//将读到的内容放入arr数组中
arr[strlen(arr)] = 0;//将arr数组的末尾加个\0,因为C语言规定的字符串是这样的
printf("%s\n",arr);
close(fd);
return 0;
}
当这段程序运行起来的时候,会从log.txt文件中读取内容到arr数组中。
看到这里,我们在Linux中用系统调用来进行文件操作就能进行了,但是文件的操作的系统调用还不止这些,在这里就不过多讲解了。
二.文件操作的本质
知道了在linux中用系统调用进行文件操作,那么文件操作的本质到底是什么呢?
文件操作的本质,可以说是进程和被打开文件之间的关系。
首先,我们再来看一下文件描述符,在上面我们提到,当用open打开文件的时候,会返回一个文件描述符,这个文件描述符到底是什么呢?
我们先来把这个文件描述符打印出来看看。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FILE_NAME(NUM) "log.txt"#NUM
int main()
{
int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT, 0666);//打开文件,0666指创建的文件默认权限为666
int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT, 0666);
int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT, 0666);
int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT, 0666);
int fd5 = open(FILE_NAME(5), O_WRONLY | O_CREAT, 0666);
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
printf("%d\n",fd5);
return 0;
}
我们多打开几个文件来看,这样更好解释
运行结果:
发现运行结果是3,4,5,6,7,且跟我们的文件打开顺序是一样的,说明文件描述符是一个顺序的标识,那么既然是顺序的标识,那么0,1,2去那了呢?
当每个程序运行起来的时候,都会默认打开三个流:stdin(标准输入流) stdout(标准输出流) stderr(标准错误流),这三个流分别对应键盘,显示器,显示器,文件描述符的0,1,2分别给到了这三个流,所以我们在打开一个文件的时候就是从3开始的。
那么我们用open打开一个文件,然后它有一个文件描述符,我们应该可以理解,那么为什么键盘和显示器也有文件描述符呢?
因为在Linux中有一个设计理念:在Linux下,一切皆文件,所以我们可以把键盘和显示器看出一个设备文件,所以它们也有文件描述符。
那从进程的角度去理解文件呢?
我们来看下图:
在我们的PCB进程控制块中,有一个struct files_struct *file的指针,这个指针指向的是一个数组,同时被指向的这个数组,里面存的也是指针,这里面的指针指向的每个被打开文件的struct file结构体,因为每一个被打开的文件,操作系统都会为其创建一个struct file的结构体,以便维护这个文件,所以文件描述符的本质就是struct files_struct这个结构体数组的下标。
三.文件描述符的分配规则
在上面我们知道了,在一个程序中如果我们打开一个文件,则它的文件描述符是从3开始的,因为0,1,2已经被占用了。
那如果我把0,1,2的文件关闭了呢?会发生什么?我们来写个代码来看一下。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
close(0);//在打开文件之前,把stdin文件关闭掉
int fd = open(FILE_NAME,O_RDONLY);//已读的形式打开文件
assert(fd != -1);
printf("fd:%d\n",fd);
close(fd);
return 0;
}
运行结果:
通过我们的运行结果可以看到我们的log.txt文件的文件描述符变成了0,这个很好理解,因为本来stdin、stdout、stderr分别占用了0,1,2,但是现在把0号关闭掉了,所以0号空出来了位置,所以新打开的log.txt就占用了0号的位置,同样的,如果把1或者2关掉,新打开的文件就会占用1或者2,所以这也说明了文件描述符是按照顺序寻找最小的且没有占用的位置。
重定向
什么是重定向呢?
我们先来看一段代码以及现象。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);//以写的形式打开文件,并且每次打开文件时都清空内容
assert(fd != -1);
printf("fd:%d\n",fd);//将这个文件的文件描述符输出
close(fd);
return 0;
}
运行结果:
从这段代码和运行结果来看,很正常,没什么特别的。
但是现在在这段代码的基础上,我进行一个操作,把stdout关掉,看看会发生什么事情。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
close(1);//把stdout关掉
int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);//以写的形式打开文件,并且每次打开文件时都清空内容
assert(fd != -1);
printf("fd:%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
运行结果:
从运行结果可以看到,把stdout关掉后,运行程序后没有东西输出,但是,我们发现,原本要输出内容输出到log.txt里面了。
这是为什么呢?
因为printf函数就是向stdout中输出的,即输出到1号文件描述符中,但先把stdout关掉后,新打开的log.txt就占用了1号文件描述符,所以当再输出时,printf函数就向log.txt中输出了,这种行为也叫输出重定向。
如果在此基础上,打开文件的时候,再加上O_APPEND选项,那么再进行printf输出时,输出的内容就会追加到我们的log.txt文件中,这叫做追加重定向。
同时的,如果我们将0号文件描述符关掉,把log.txt打开去占用0号文件描述符,当我们使用scanf去读的时候,就不会从键盘中读取了,而是从log.txt中读取。
如下面代码所示:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
close(0);
int fd = open(FILE_NAME,O_RDONLY);
assert(fd != -1);
char arr[100];
scanf("%s",&arr);
puts(arr);
return 0;
}
运行结果:
从运行结果可以看到,scanf函数变成了从log,.txt中读取内容, 这叫输入重定向。
同时在我们的shell命令行解释器中,是可以直接支持这种操作的,例如:
输出重定向:ls > log.txt
追加重定向:ls >> log.txt
输出重定向:cat < log.txt
fud2系统调用
在上面,我们通过关闭0,1,2文件描述符的方式,可以达到重定向的功能,但是这种方法未免有点挫,所以在系统中,提供了一种系统调用给我们来实现重定向的功能,dup2系统调用。我们先来看一下它的声明。
可以看到dup有两个接口,但是我们重点谈第二个,因为第二个用的多。
dup2有两个参数,一个是oldfd,一个是newfd,即将oldfd拷贝到newfd中,什么意思呢?我们来看图
通过这个系统调用,我们就可以愉快的实现重定向功能了,如下面代码所示。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
assert(fd != -1);
dup2(fd,1);//将文件log.txt的文件描述符拷贝给1号文件描述符
printf("hello linux\n");
close(fd);//关闭文件
return 0;
}
运行结果:将hello linux输出到log.txt文件中
四.缓冲区的理解
对于IO的部分,大家都应该有听说过缓冲区的概念,那么缓冲区到底是什么,我们又应该如何去理解呢?我们接着往下看。
在讲之前,我们先来看一段代码以及现象。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
printf("printf :hello linux\n");
fprintf(stdout,"fprintf:hello linux\n");
fputs("fputs :hello linux\n",stdout);
const char arr[100] = "write :hello linux\n";
write(1,arr,strlen(arr));
fork();
return 0;
}
当我们默认用./test运行程序时,运行结果如下:
现在看来代码和运行结果还很正常,所以接下来我们做一个将运行结果重定向到log.txt文件的操作。
通过结果对比可以发现,当将程序运行,并默认输出到屏幕上的时候,只有四条内容,但是我们重定向后输出到log.txt中,就变成了7条内容,其中多出来的3条内容是C语言多打印出来的,也就是说C语言函数的输出,输出了两遍。
这是为什么呢?
首先可以明确给出,这个跟代码中的fork有关,并且与缓冲区也有关。
在解释之前,我们来看理解一下什么是缓冲区。
对于缓冲区的理解,可能大家都知道,当我们向显示器,磁盘或者其他外设进行写入的时候,是不会立即马上将数据写入到外设中的,会先将数据存在缓冲区中,当缓冲区满了,或者用户主动刷新等操作时,才会将数据从缓冲区写入到我们的外设中,其中缓冲区的刷新策略有3种情况,2种特殊情况。
1.立即刷新——无缓冲
2.行刷新——行缓冲,例如显示器
3.缓冲区满刷新——全缓冲,例如,磁盘
特殊情况:
1.用户强制刷新,如fflush函数
2.进程退出刷新,进程退出的时候,缓冲区会全部刷新
那么缓冲区到底是什么?
缓冲区是用户级语言层面提供给我们的缓冲区,例如,在C语言的输入输出中,它的缓冲区就是C语言提供给我们的缓冲区。
在C语言的文件操作中,当我们打开一个文件的时候,是要返回一个FILE的结构体指针。
其中在这个结构体里面,就存储着很多char类型的指针,这些指针就是我们所说的缓冲区,所以说白了,在C语言中,缓冲区只不过是FILE结构体中的一个char类型的指针而已。
如下图所示,我将C语言库中的libio.h库头文件打开,找到里面的FILE结构体,可以看到在这个结构体中,有着大量的char类型的指针,这些指针指向的内容就是我们所谓的缓冲区。
这个时候在看待C语言的缓冲区问题,我们就很清楚了。但是这仅仅是C语言的缓冲区问题,那么对于我们的系统来说,又是怎么样的呢,我们知道所有语言的操作的底层都是去调用系统接口进行的,语言层面只不过是对系统接口进行了一个封装罢了,所以当我们的数据保存到了FILE结构体里面的缓冲区的时候,再更进一层的写入就是跟操作系统有关了。我们来看一下图。
所以我们的输入输出的过程,就大概像上图所示。
对于C语言的缓冲区来说,将C语言的缓冲区数据写入到内核缓冲区中,我们有一个fflush函数可以直接将C语言缓冲区中的数据写入到内核缓冲区种,但是对于内核缓冲区来说,操作系统将内核缓冲区中的数据写入到磁盘时,是由操作系统自主决定的。
所以这时就有了一个问题,如果我们已经将数据从C语言的缓冲区写入到了内核缓冲区中,但是这个时候突然停电了,导致电脑关机,使得内核缓冲区中的数据还没写入到磁盘中, 导致数据丢失,显然这种情况时不允许发生的,那么怎么办呢?
操作系统给我们提供了一个系统调用,可以将内核缓冲区中的数据强制写入到磁盘中。
fsync,这个系统调用,可以直接强制刷新内核缓冲区,避免我们所说的情况。
那么现在我们再来解释一下开头那段代码的情况:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()
{
printf("printf :hello linux\n");
fprintf(stdout,"fprintf:hello linux\n");
fputs("fputs :hello linux\n",stdout);
const char arr[100] = "write :hello linux\n";
write(1,arr,strlen(arr));
fork();
return 0;
}
在这段代码中,当我们将运行结果重定向到log.txt时,C语言函数的输出会出现两次:
但是如果不是重定向到log.txt时,是直接运行输出,却没有这种情况,而是很正常的一种情况:
首先我们来分析一下只输出四句的这种情况:
我们知道缓冲区的刷新策略有3种情况,2种特殊情况,在这里符合第二种,行缓冲,因为在我们的各种printf函数中,有\n,这种情况符合行缓冲,即在fork创建子进程之前,字符串就已经输出到我们的显示器上了。
接下来分析将输出重定向到log.txt的情况,因为这个时候不再是向显示器中输出,而是向磁盘中输出,符合缓冲区刷新策略的第三种,全缓冲,当缓冲区满了的时候,才输出,所以这个时候\n不再起作用了,即在我们fork之前,字符串还在FILE的缓冲区中,fork之后,创建了子进程,子进程继承了父进程的所有代码和数据,所以子进程的FILE缓冲区中也有一份字符串,当进程结束的时候,父进程和子进程都将直接缓冲区中的字符串输出出来,所有才有了两份。
过程如下图所示:
这就是为什么上面的代码会出现7条的原因。
模拟C语言中文件操作函数的实现
对于缓冲区的理解,你可能有一些概念了,那么如何更好的理解呢?
我们来模拟实现C语言中的文件操作来试一下。
在这部分中,我们将来模拟实现一下在C语言中,文件操作函数的模拟。
其中我们模拟下面这些内容:
FILE文件结构体、
fopen函数、
fclose函数、
fwrite函数。
且在这一部分中,我将会使用模块化编程。
MyStdio.h
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#define SIZE 1024
//文件结构体
typedef struct FILE_
{
int fileno;//文件描述符
char buffer[SIZE];//缓冲区
int pos;//记录缓冲区末尾的位置,指向\0的位置
}FILE_;
FILE_* fopen_(const char* pathname,const char* mode);//我自己的文件打开函数
void fwrite_(const void* ptr,int sz,FILE_* stream);//我自己的文件写入函数
void fflush_(FILE_* stream);//我直接的刷新缓冲区函数
void fclose_(FILE_* fp);//我自己的文件关闭函数
MyStdio.c
#include"MyStdio.h"
//我自己的文件打开函数,其中参数为:要打开文件的名字,以什么方式打开
FILE_* fopen_(const char* pathname,const char* mode)
{
//判断文件的打开方式
int status = 0;
if(strcmp(mode,"w") == 0)
{
status |= O_WRONLY | O_CREAT | O_TRUNC;
}
else if(strcmp(mode,"r") == 0)
{
status |= O_RDONLY;
}
else if(strcmp(mode,"a") == 0)
{
status |= O_WRONLY | O_CREAT | O_APPEND;
}
//判断打开文件时,是否需要创建文件
int fd = 0;//存储文件描述符
if((status | O_WRONLY) > 0)
{
fd = open(pathname,status,0666);
}
else
{
fd = open(pathname,status);
}
//说明文件打开失败
if(fd < 0)
{
const char* err = strerror(errno);
printf("open:%s",err);
return NULL;
}
FILE_* file = (FILE_*)malloc(sizeof(FILE_));//开辟文件FILE_结构体
file->fileno = fd;
memset(file->buffer,0,sizeof(file->buffer));//将缓冲区数组中的内容全部置0
file->pos = 0;
return file;
}
//我自己的刷新缓冲区函数,其中参数为:那个文件的结构体
void fflush_(FILE_* stream)
{
if(stream->pos > 0)//说明缓冲区中有数据,将缓冲区中全部数据写入
{
write(stream->fileno,stream->buffer,stream->pos);
fsync(stream->fileno);//强制刷新内核缓冲区的数据
}
stream->pos = 0;//将缓冲区末尾位置置0
}
//我自己的写入数据函数,其中参数为:要写入内容的起始指针位置,要写入的字节数,写到那个文件中
void fwrite_(const void* ptr,int sz,FILE_* stream)
{
strcpy((char*)stream->buffer + stream->pos,(char*)ptr);
stream->pos += sz;
//如果缓冲区中有\n,说明符合行缓冲,进程缓冲区的刷新
if(stream->buffer[stream->pos-1] == '\n')
{
fflush_(stream);
}
//如果缓冲区满了,进行缓冲区刷新
if(stream->pos > 100)
{
fflush_(stream);
}
}
//我自己的文件关闭函数,其中参数为:要关闭那个文件
void fclose_(FILE_* fp)
{
fflush_(fp);//文件关闭前,刷新缓冲区
close(fp->fileno);
free(fp);//关闭文件最后,释放内存
}
main.c
#include"MyStdio.h"
#define FILE_NAME "log.txt"
int main()
{
FILE_* fp = fopen_(FILE_NAME,"a");
const char* arr = "hello linux\n";
fwrite_(arr,strlen(arr),fp);
fclose_(fp);
return 0;
}
写到这里,我们的自己模拟实现的C语言文件操作函数就完成了,不过我这里实现的是非常简单的,经不起推敲,实现这个只不过是为了加深文件操作过程的理解,和缓冲区的理解罢了。
五.软硬链接
什么是软硬链接,在讲解之前,我们先来简单的了解以下文件系统。
在一个目录中,从我们人的视角来看,文件名可以唯一标识一个文件,这看起来很简单,但是对于计算机而言,就复杂的多了,但是我也不详细讲,就简单的了解以下。
我们先来看一个现象,首先我们先用 ll -i 命令来将当前路径的内容显示出来:
通过现象我们可以看到,在每个文件的前面都有一串数字,那么这串数字又是什么呢?
这串数字叫做inode,且每个inode都唯一标识一个文件,也就是说,对于我们人来看,我们找一个文件是通过文件名来找的,但是对于操作系统而言,找一个文件是通过inode来找的,inode和目录和文件的关系,我们可以简单的用下图来表示。
在我们的每一个目录中,都会存储着一个映射关系表,这个映射关系表里面有存储着每一个文件的文件名和inode的映射关系,通过这种映射关系,我们的目录就可以找到对应文件的inode,再通过inode就可以找到对应的文件了。
这只不过是非常简单的理解而已,如果要深入理解会非常复杂,还要理解一下硬件层面的东西,但是在这里,就不讲了。
大概简单的了解了一下文件系统,那么软硬链接到底是什么呢?
我们来举一些例子来看一下。
软链接
首先我们来看一下软链接。
在我们的当前目录中,有下面这样文件。
其中,我们的log.txt文件中还有一串“hello linux”的字符串。
这个时候我们来创建一个软链接,创建软链接的方式是:
ln -s <target> <link_name>
其中,<target>为我们要创建软链接的目标文件,<link_name>为要创建软链接的名称。
如上图所示,我们给log.txt文件创建一个软链接,并其名称为tmp,可以看到,在当前目录下,多一条 tmp -> log.txt 的内容,这就是我们软链接所创建的,同时我们查看一下tmp的内容,可以发现,其内容和log.txt是一样的,
这种方式就好比我们在windows中的快捷方式一样。
同时我们在查看一下它们的inode,会发现log.txt和tmp的inode是不一样的。
也就是说,软链接会给该快捷方式创建一个新的inode,同时在当前目录中,添加新的映射关系,如下图所示。
如果这个时候,我们将log.txt删除掉会发生什么事情呢?
我们来看看。
当把log.txt删除后,tmp就会一直闪红,因为tmp只不过是log.txt的快捷方式罢了,删除log.txt,tmp也就自然而然找不到log.txt了。
那么软链接有什么用呢?
答案应该很明显了,软链接就像我们在windows下的快捷方式一样,如果某个程序或者文件的路径很深的时候,不方便我们查找,我们就可以用软链接的方式将它简化。
硬链接
看完了软链接,我们来看一下硬链接。
硬链接的创建方式是:
ln <target> <link_name>
与软链接的用法一致,只不过是没有 -s 选项。
通过对log.txt进行硬链接后,我们可以看到在当前目录中,多了一个alias的文件,这个文件其实就是我们的log.txt文件,只不过是起了一个别名罢了。这就是硬链接的用法。
这个时候,同样的,我们来看一下log.txt和alias的inode来看看。
可以看到,log.txt和alias的inode是相同的,硬链接的原理是在当前目录中,多加一个文件名和inode的映射关系罢了,如下图所示:
所以软链接和硬链接的区别就是,有没有创建一个新的inode,其中,软链接是会创建新的inode的,而硬链接不会。
这个时候,我们也来试一下,把log.txt删掉会怎么样。
将log.txt删掉后,我们可以看到,对应的alias还在,同时这个alias是不受影响的,我把它的内容输出出来是一点问题都没有的。
软链接我们可以看到,把log.txt删掉后,软链接就直接用不了了,而硬链接还能用,这是为什么呢?
在解释这个问题之前,我们先来看另一个现象。
引用计数
什么是引用计数?
在每一个inode中,都会存储着一个引用计数,这个引用计数指的是有多少个文件名引用了这个文件,光说很难理解,我们来详细看看。
首先,我找一个空目录,在这个空目录中,创建两个文件:test.c和log.txt。
再把这个目录的内容列出来。
在文件的详细信息中,我们可以看到,有一栏数字内容,那么这个数字是什么意思呢?
这个数字表示该文件被引用的次数,即,log.txt文件被引用了一次,可能这样还是很难理解,不怕,我们接着往下看。
我们创建一个目录看看。
从上图我们可以看到,当是目录的时候,引用计数为2,这是为什么呢?
因为当前tmp目录被两个文件名引用了,一个是tmp,另一个是目录中的.
这就是所谓的引用计数。
现在再回顾头来看我们的硬链接遗留的问题
把log.txt删掉会怎么样。
将log.txt删掉后,我们可以看到,对应的alias还在,同时这个alias是不受影响的,我把它的内容输出出来是一点问题都没有的。
当我们把log.txt删掉的时候,只不过是将对应的inode的引用计数减减,减减之后,inode的引用计数为1,而不为0,所以源文件log.txt是不会删除的,删除的真正意义是inode的引用计数为0才删除。
如下图所示。
该删除log.txt的过程如下:
六.acm时间
在这里补充一下,什么是acm时间。
首先我们使用 stat <一个文件> 命令将文件的详细信息时间列出来。
可以看到,我们的文件有三个时间:Access、Modify、Change
这三个时间分别对应:访问时间,修改时间,更改时间
我们先来看后两个,修改时间和更改时间有什么区别呢?
更改时间指的是文件属性最近一次被更改的时间,例如下图所示:
接下来是修改时间,修改时间指的是文件内容最近一次被修改的时间,如下图所示:
我们使用vim来修改log.txt的内容。修改后,Modify时间发生了改变,同时Change时间也改变了,因为文件大小变大了,文件的大小也是文件属性的一部分,所以Change时间也改变了。
最后是Access时间,是指访问时间,当我们访问这个文件的时候文件,访问时间就会更新,但是不一定会更新。为什么呢?
因为在我们的日常操作中,访问一个文件是很频繁的,如果每访问一次,就去更新Access时间,就会存在大量IO去修改文件的信息,这样会导致效率低下,所以为了避免效率低下,Linux采用一种方式来更新Access时间,就是当访问了一定的次数或者隔了一定的时间时,才更新Access时间。