目录
前言:
在我们学习C语言的时候,我们都会学到有关C语言的文件操作。包括fopen,fclose等函数的使用。实际上,在操作系统中,文件的操作与管理并没有表面看上去的那么简单。
本篇开始的文件专栏将会与大家更深层次的讨论一下在linux系统下的文件操作。
一、文件
在Windows/Mac中:
文本、图片、视频才是"文件"
键盘、打印机是"设备"
但是在linux中,却发生了一场革命:
这就是著名的 "一切皆文件" 哲学,也就是为什么我们经常说:“linux下一切皆文件”的由来:
键盘 →
/dev/input
(输入文件)打印机 →
/dev/lp0
(输出文件)硬盘分区 →
/dev/sda1
(存储文件)甚至CPU信息 →
/proc/cpuinfo
(虚拟文件)
当你在终端输入:
$ cat hello.txt Hello Linux World!
你看到的是文件内容——存储在磁盘上的二进制数据,可能是:
文本(ASCII/UTF-8编码)
图片(像素数据)
可执行程序(机器指令)
当你再输入:
$ ls -l hello.txt -rw-r--r-- 1 tom users 26 Jun 5 10:30 hello.txt
最下面这段神秘代码就是文件的属性:
所以我们可以得出结论:
文件 就是 内容+属性
在访问一个文件前,我们肯定是要先把这个文件打开的,这是天经地义的事情。可是,在没被打开前,文件存储在哪里呢?
答案是磁盘中。
而我们在访问一个文件时,又是依靠谁来访问呢?
答案是进程。
打开一个文件的本质就是把他加载到内存中:一个文件就是由一个进程去访问,由于进程在内存中,所以最后由CPU来实际执行。
一个进程能同时访问多个文件吗?
这是可以的。
那多个进程呢?
照样可以!出现了这么多的文件,我们的操作系统需不需要去对文件进行管理呢?就像操作系统对进程的管理一样?
二、操作系统对文件的管理
操作系统必须要对加载到内存中的一系列文件进行管理,如何管理呢?
答案依旧是:先描述,再组织
所以在内核中,文件如同进程一般,等于内核数据结构加内容。
已经知道进程在内核中是由task_struct(PCB)进行管理,进程访问文件,所以归根结底就是task_struct与文件结果的关系。
文件可以划分为两类,一种是被打开的文件,它在内存里,另一种是没有被打开的文件,它在磁盘里。
我们今天主要讨论第一种文件。
我们都知道,每个进程默认会打开三个输入输出流:
stdin 标准输入 键盘
stdout 标准输出 显示器
stderr 标准输出 显示器
不只是C语言与C++,其他语言也会有这三个输入输出流。这说明这不是一种语言的特性,而是系统层次上的特性。
那么这三个有什么意义呢?带着这个疑问,我们继续往下看:
在讲文件管理之前,我想先问问大家,平时在C语言中,有几种方法可以打印内容到显示器上。
#include<stdio.h>
int main()
{
printf("hello\n");
fprintf(stdout,"hello\n");
fputs("hello\n",stdout);
fwrite("hello\n",1,5,stdout);
return 0;
}
一般我们有这四种方法打印出来,对不对?
他们之间有什么共同点呢?
没错,他们底层其实都是调用的系统接口write,我们所用的C文件接口,底层一定要封装对应的系统文件接口。
包括fopen与fclose也是一样,底层实际上是对我们open与close的封装。
我们就来简单的说一下open函数。
三、open
open的第一个参数与fopen一样,也是一个路径下的文件,但是从第二个参数开始,就已经有了变化了。flags参数用于控制文件的打开方式,它由多个常量通过按位或(|
)组合而成。通过这种位图形式的传参,是我们可以达到自己想要的结果。
比如O_RDWR宏常量,他就代表以读写的方式打开:
#include<stdio.h>
#include <fcntl.h>
int main()
{
int fd = open("./test.txt",O_RDWR);
if(fd < 0)
{
perror("open error");
}
return 0;
}
如果是fopen以读写方式打开,哪怕你不存在这个文件都不会失败,因为会给你自动创建一个同名文件,但放在open里你会运行错误,因为我们没有设置专门的创建的宏常量:O_CREAT
所以我们加上该宏:
运行:
怎么回事,大家可以看到,这次运行确实成功了,但txt文件的权限却是乱码的。
这就是open第三个参数负责的事情,创建出的文件的默认权限
int fd = open("./test.txt",O_RDWR | O_CREAT,0666);
把代码改成这样在运行,就正常了:
但有人说这个文件的权限不应该是0666吗,怎么显示的是0664(r权限为4,w为2,x为1),这是因为umask的存在,会导致真正的权限为:mode & ~umask
我们可以在代码中或者指令umask来修改这个值。
open的返回值又是什么呢?为什么是一个int?
我们是这把这个返回值打印出来试试:
int main()
{
int fd = open("./test.txt",O_RDWR | O_CREAT,0666);
if(fd < 0)
{
perror("open error");
}
else
{
printf("fd = %d\n",fd);
}
return 0;
}
为什么会是3呢?3又是个什么东西?
我们可以先提前告诉你,这个3是一个文件标识符。open函数成功后,会返回一个文件标识符!
四、文件标识符
聊到这里,我相信大家已积累了一肚子的疑问了。
别着急,我们一一把这些疑问为大家串联起来解答:
这个文件标识符,是我们访问文件的唯一方式。
我们之前说过,C语言访问文件的接口,都是封装的系统调用,而系统调用,归根结底就是通过文件标识符来找到对应的文件的。如图数组的下标一样(实际上本身就是下标),有了下标,你就能找到在内存中被管理的文件。
在内存中,进程是由task_struct进行管理的,我们之前说过文件也会被操作系统用一个内核数据结构管理起来,类似的,我们把这个数据结构叫做:struct file。
struct file { struct path f_path; // 文件的路径(包含 dentry 和 vfsmount) struct inode *f_inode; // 文件的 inode const struct file_operations *f_op; // 文件操作函数(read/write 等) atomic_long_t f_count; // 引用计数 loff_t f_pos; // 当前读写位置 // ... };
在struct file结构体中包含的struct file的指针,使被加载到内存的多个文件通过指针的方式链接形成链表。我们对文件的管理,就转变为了对链表的增删查改!
而我们说的那三个输入输出流,在linux下也是一种文件,所以无独有偶,也应该被这样的方式进行管理。
我们说文件都是被进程所访问的,进程如何访问呢?就是去寻找task_struct与struct file的联系。
而很不巧,task_struct中就存在这样一个结构体指针,struct files_struct*files指针。
struct task_struct { // ... struct files_struct *files; // 指向进程的文件管理结构 // ... };
这个指针所指向的结构体中又包含了一个指针数组:struct file* fd_array。
struct files_struct { // ... struct file __rcu * fd_array[NR_OPEN_DEFAULT]; // 文件描述符数组(默认大小是 64) // ... };
没错,我们上面所说的3,就是这个指针数组的下标元素,那为什么是3呢?
因为我们说过,进程会默认打开三个输入输出流!!
而他们正好占据了数组的前三个位置。
所以,0,1,2的位置早早就被占领了。当我们的代码执行到open的时候,他们先从这个数组里搜索空位,搜到3是空的了,于是把文件加载到内存中形成内核数据结构并连入链表(实际上可能不是链表,会是更加优秀的数据结构比如红黑树之类的)。随后把文件的地址放到3号位置处!
五、C语言中的FILE*
我们看一下C语言中的输入输出流吧:
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
为什么这里是个FILE*类型呢?
答案还是封装,C语言不仅对接口进行封装,还把他们的类型做了封装。
我们可以通过打印来验证一下,首先,FILE*是个结构体指针,他的参数含有一个fileno表示文件标识符,我们打印一下试试:
由此可以验证我们上述内容。
结语
本篇文章就到此结束,由于是文件系统的第一篇,所以篇幅有限,且大部分都是概念,需要同学们好生理解与掌握,尤其是文件被打开的过程。
如果有所疑问或者指正,欢迎评论区留言!!