一、关于文件
文件=内容+属性
- 那么所有对文件的操作,就是对内容/属性操作。
- 内容是数据,属性也是数据,那么存储文件,就必须既存储内容数据,又存储属性数据。默认就是在磁盘中的文件。
- 当进程访问一个文件时,都需要先把文件打开。对于普通的磁盘文件,“打开”就是将文件加载到内存。
- 一个进程可以通过操作系统(操作系统提供系统调用接口)打开多个文件,多个进程可以通过操作系统打开多个文件。加载到内存中被打开的文件可能会存在多个。(进程:打开的文件 = 1 : n)
- 加载磁盘上的文件,一定会涉及访问磁盘设备,是由操作系统来做的。
操作系统在运行时,可能会打开很多文件,那么,操作系统该如何对文件进行管理呢?
一个文件要被打开,一定要在内核中先形成被打开的文件对象,例如:
struct file
{
//文件的属性
struct file* next;
...
}
文件的属性与文件内容一样,在磁盘中同样有保存。
我们使用链表对这些对象进行管理。那么,对于文件的管理,就转换成了对链表的增删查改。
被打开的文件在内存中,未被打开的文件在磁盘中。 所有的文件操作,都是在内存中操作的。
因此,研究文件操作的本质就是研究进程和被打开文件的关系。
二、常见文件接口(C语言)
1、fopen
我们可以在Linux中使用man来查看fopen的手册(man fopen):
打开的模式:
以“w”方式打开为例:
当文件 以“w”方式打开时,若该文件不存在,则自动创建文件。
打开文件时,会先清空文件内容。
#include<stdio.h>
//以"w"打开,若文件不存在则自动创建该文件
int main()
{
FILE* fp = fopen("log.txt","w");
if(NULL == fp)
{
perror("fopen");
return 1;
}
const char* s = "hello world\n";
int i = 0;
for(i = 0; i < 10; i++)
{
fputs(s,fp);
}
fclose(fp);
return 0;
}
在执行该程序之前,当前目录下只有两个文件。
make会产生一个myfile.c的可执行文件myfile。
执行该程序后,当前目录下出现了一个新文件:log.txt。
我们可以看到此时该log.txt文件中,就写入了我们程序中设定的内容。
我们对该C程序进行修改:
#include<stdio.h>
//以"w"打开,若文件不存在则自动创建该文件
int main()
{
FILE* fp = fopen("log.txt","w");
if(NULL == fp)
{
perror("fopen");
return 1;
}
fputs("aaa\n",fp);
fclose(fp);
return 0;
}
重复执行上述内容(注意:我们的log.txt文件并未删除,仍旧存在),我们再次查看log.txt中的内容:
可以看出,之前写入的内容已经被清空。
注:我们使用“> log.txt”也可以实现对该文件进行清空。“>”是重定向,向文件写入。要向文件写入,就一定要打开文件。此时文件内容就会被清空。
以"a"方式打开为例:
append:追加。
“a”方式也是写入,只不过是从文件结尾进行写入。
我们将myfile.c文件进行修改:
#include<stdio.h>
int main()
{
FILE* fp = fopen("log.txt","a");
if(NULL == fp)
{
perror("fopen");
return 1;
}
fputs("bbbbbbbbb\n",fp);
fclose(fp);
return 0;
}
运行该程序,我们可以看出,原先log.txt文件中的内容并未变化,我们是在此基础上追加了内容。
我们可以不断执行,不断追加。
注:我们使用“echo “cc” >> log.txt”也可以实现追加。
2、
三、系统调用接口
由于打开文件的操作只能由操作系统来完成,所以我们C语言中打开文件的接口,底层一定是封装了系统调用接口的。
1、open
第一个参数是文件名称(不带路径默认当前路径下)。
第二个参数是flags:
第三个参数表示我们要以什么权限来创建新文件。
返回值:
file descriptor文件描述符:fd
模拟fopen("filename","w"):
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
//umask(0);//权限掩码改为0
//O_TRUNC:将打开的文件的长度清零(清空)
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char* s = "abc\n";
write(fd, s, strlen(s));
close(fd);
return 0;
}
模拟fopen("filename","a") :
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
//O_APPEND:在文件末尾进行写入(追加)
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char* s = "abc\n";
write(fd, s, strlen(s));
close(fd);
return 0;
}
四、fd:文件描述符
返回值类型为int,那么fd究竟是什么?
我们可以输出fd观察:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd1:%d\nfd2:%d\nfd3:%d\nfd4:%d\n",fd1,fd2,fd3,fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
可以看到,fd是连续的整数。
当进程打开文件时,操作系统会给每一个文件创建文件对象(被打开文件的描述结构体对象),并对它们使用链表进行维护。
操作系统需要对进程(task_struct)和打开的文件(struct file)之间的对应关系进行维护,就产生了struct files_struct。而task_struct中包含:struct files_struct* files,该指针就指向struct files_struct。
struct files_struct这个结构体中,包含了一个数组:struct file* fd_array[]。该数组中,存放struct file的地址。
因此,当进程打开文件时,操作系统会创建struct file对象,并在该数组:struct file* fd_array[]中查询可使用的下标,将struct file对象的地址填入到该下标的位置。然后将该数组的下标返回,我们将该数字称为文件描述符。我们将struct file* fd_array[]称为:进程文件描述符表。
因此,文件描述符fd本质就是数组的下标。
那么,为什么我们显示打印的fd是从3开始的呢?
这是因为进程在运行的时候,默认打开:
标准输入:键盘 stdin 0
标准输出:显示器 stdout 1
标准错误:显示器 stderr 2
stdin/stdout/stderr都是FILE*类型。FILE是一个结构体,其中封装了文件描述符。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
printf("stdin->fd:%d\n",stdin->_fileno);
printf("stdout->fd:%d\n",stdout->_fileno);
printf("stderr->fd:%d\n",stderr->_fileno);
return 0;
}
那么,我们为什么要把stdin/stdout/stderr打开呢?
这是为了能够让我们默认进行输入输出代码编写。