文章目录
- 一、文件的读写操作
- 二、文件描述符
- 三、文件重定向
- 四、理解 Linux 一切皆文件
- 五、文件缓冲区
一、文件的读写操作
文件=内容+属性
当文件没有被操作的时候,一般文件还是在磁盘当中
文件操作=文件内容的操作+文件属性的操作,文件操作有可能即改变内容,又改变属性
文件操作其实就是把内容和属性加载到内存当中
文件的系统调用函数一般有以下几个参数,pathname(对应文件路径),flags(文件打开方式),mode(指定文件的权限)。这里的 flags 不能简单当作一个整型来看,而且应该把它当作一个位图,32个 bit 位对应32种状态。打开文件时,可以传入多个参数选项,用下面的一个或者多个宏进行 “或” 运算,构成flags,调用类似下面的 show(ONE | TWO | THREE)
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
flags参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_TRUNC:初始化清空文件
O_APPEND: 追加写
#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define THREE (1<<2) // 4
#define FOUR (1<<3) // 8
void show(int flags)
{
if(flags&ONE) printf("func1\n");
if(flags&TWO) printf("func2\n");
if(flags&THREE) printf("func3\n");
if(flags&FOUR) printf("func4\n");
//………
}
文件操作其实就是一系列的系统调用,调用系统提供的函数。就算是语言层面的操作文件函数,也只是在内部封装了 open、write、read 等系统函数。因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd(文件描述符) 访问的。所以C库当中的 FILE 结构体内部,必定封装了 fd。C/C++ 封装的库函数中对文件打开方式主要有以下几种:
FILE * fopen ( const char * filename, const char * mode )
mode参数:
b:以二进制模式打开文件
r&r+:前者以只读方式打开,后者以读写方式打开文件.文件必须存在,否则打开失败
w&w+:前者以写入方式打开,后者以读写方式打开文件.若文件不存在,则创建新文件。存在,则清空文件内容
a: 以追加写入方式打开文件,若文件不存在,则创建新文件。写入的数据总是添加到文件的末尾
a+:以读写追加方式打开文件,若文件不存在,则创建新文件。可以从文件的任何位置读取数据,写入的数据总是添加到文件的末尾
二、文件描述符
要对文件进行系统的管理,必须先描述再组织。当一个进程打开一个文件时,内核会创建一个 struct file 结构来存储关于这个文件的所有信息,包括:
文件在磁盘上的位置;文件的基本属性,如权限、大小;
文件的读写位置(文件偏移量);
文件的拥有者信息,即哪个进程打开了该文件;
文件的内核缓冲区信息等;
task_struct 结构体里面的成员:struct files_struct,用于管理一个进程打开的所有文件。它包含: 一个文件描述符表 fd_array,这是一个数组,其元素是指向 struct file 的指针,每个元素对应一个打开的文件。文件描述符(FD)是这个数组的索引,每个索引唯一地标识了一个打开的文件。文件描述符是一个非负整数,0,1,2默认被占用为标准输入,标准输出和标准错误三个文件,即我们创建文件的描述符都是从3以后开始。当进程关闭文件时,会减少 struct file 的引用计数。如果引用计数变为0,内核会释放 struct file 实例,并清理相关资源。
文件的简单管理结构如下:
三、文件重定向
文件重定向的本质是修改文件描述符表 fd_array 中的 FD 对应的 struct file* 地址。即文件描述符 FD(0,1,2,3) 等不变,改变 struct file* 的值,使它指向不同的文件
dup2 系统调用实现重定向:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2 函数的作用是将 oldfd 指向的文件地址覆盖 newfd 指向的文件地址,如果 newfd 指向的文件已经打开,则会先关闭 newfd。dup2 函数可以使不同文件描述符可以指向同一个文件,如下图:
四、理解 Linux 一切皆文件
操作系统是一款管理软件,它通过向下管理好各种软硬件资源 (手段),来向上提供良好 (安全、稳定、高效) 的运行环境 (目的)。对键盘、显示器、磁盘、网卡等硬件进行管理需要先描述、再组织:即先将这些设备的各种属性抽象出来组成一个结构体,然后为每一个设备都创建一个结构体对象,再用某种数据结构将这些对象组织起来,这也就是我们上面学习到的文件内核数据结构 file。
由于每种硬件的访问方法都是不一样的,我们需要为每一种硬件都单独提供对应的 Read、Write 等方法,这些方法位于驱动层。参考下图:
struct file 中定义函数指针 read() 和 write() 方法 ,而这些方法在驱动层被实例化,指向不同硬件的访问方法。这是一种多态行为。即使用户使用相同的系统调用接口,实际执行的操作会根据底层文件或设备的类型而变化。file 结构体可以类似于 C++ 中的基类,驱动层的各种方法和结构就相当于子类,可以看作它们继承了基类,同时又扩充了对底层硬件管理的特殊方法。站在操作系统内核数据结构上层来看,所有的软硬件设备和文件统一都是 file 对象,它是操作系统当中虚拟出来的一层文件对象,我们将这一层称为 虚拟文件系统 VFS,通过它,我们就可以摒弃掉底层设备的差别,统一使用文件接口的方式来进行文件操作,即 Linux 下一切皆文件!
五、文件缓冲区
缓冲区分为用户级缓冲区和内核级缓存区,我们所接触到的缓冲区大部分都属于用户级语言缓冲区。文件也有自己的缓冲区,被描述为 FILE 结构体中的一个字段,类似 char buff [size]。而我们常说的刷新缓冲区就是指缓冲区里的数据加载(拷贝)到内核缓冲区,而文件是被进程管理的,进程退出时与文件描述符相关联的内核缓冲区会被刷新到磁盘上。
缓冲区刷新常见策略:
1、无缓冲(立即刷新) :缓冲区中一出现数据就立马刷新,这种很少出现;
2、行缓冲(行刷新 ):遇到换行符(\n)数据就刷新一次
3、全缓冲(缓冲区满刷新):待数据把缓冲区填满后再刷新,这种刷新方式效率最高,一般应用于磁盘文件
显示器采用的刷新策略是行缓冲,与此相对,普通文件采用的刷新策略是全缓冲。显示器是给人看的,按行刷新符合人的阅读习惯,而普通文件更多需要全缓存来提高效率。
系统调用接口 write 是直接往内核缓存区里写,如 C库中的 fwrite 是往文件缓冲区里面写,当文件缓冲区刷新时,再把文件缓冲区里的数据加载(拷贝)到内核缓冲区。下面通过两个例子来加深对缓冲区的理解;
例一:
两张图片中都关闭了1号标准输出文件,但是左图把信息全部打印出来了,而右图只打印了系统调用 write,因为显示器文件是行刷新,左图 fwrite 加了换行符 \n的信息已经从文件缓冲区拷贝到了内核缓冲区,被显示出来。而右图没有加换行符 \n,关闭了文件描述符后,文件缓冲区的信息没有加载到内核缓冲区中,因此右边的 fwrite 信息消失不见。write 系统调用是直接往内核缓冲区写的,因此左右图都会打印 write 信息
例二:
由上面我们知道,文件缓冲区被描述为 FILE 结构体中的一个字段,那么当我们 fork 创建子进程时,子进程会拥和父进程一样的文件描述符表及其缓冲区。当我们向显示器打印时,按行刷新。fork 时,此时父进程的文件缓冲区为空,信息已经刷新到了内核缓冲区中。此时子进程虽然拥有和父进程一样的文件缓冲区,但却是空的,因此显示结果如左边。当我们重定向到普通文件打印时,为全缓冲,fork 时,父进程文件缓冲区有 fwrite 信息,因此 fork 之后子进程的文件缓冲区也有 fwrite 信息,最终导致 fwrite 打印了两次,而 write 打印在前面是因为它直接写入了内核缓冲区,不用像 fwrite 一样刷新了才加载到内核缓冲区