一、文件的理解
每种语言都有进行文件操作的函数接口,例如C语言的fopen、fwrite、fprintf等等,但是进行文件操作的前提是代码已经跑起来,因为文件的打开与关闭要通过CPU来运行程序代码,所以打开文件的本质是进程打开文件,文件操作也是进程进行文件操作。
既然文件的打开与关闭是依靠进程的,而操作系统中避免不了存在许多进程,那么一定有大量打开的文件,所以OS就需要将这些文件管理起来(先描述在组织),所以在OS内部,每一个被打开的文件应该有一个专门描述文件的结构,类似于PCB
文件没有被打开的时候是在哪里呢?
------解释:
文件没有被打开的时候是存在磁盘中的,而磁盘属于硬件,用户要进行文件操作是不可以直接访问硬件的,因为操作系统不相信任何人,用户只能通过操作系统的函数调用来进行文件操作,而我们在平常一般使用的是语言层面的文件函数接口,不同语言对于文件的函数调用却是有差异的,但它们都是对系统调用接口的封装。
二、文件系统调用接口介绍
1. 打开文件:open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//一般用于打开已存在的文件
int open(const char *pathname, int flags);
//一般用来打开并创建新的文件
int open(const char *pathname, int flags, mode_t mode);
- pathname: 要打开或创建的目标文件
- flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 “或” 运算,构成flags。
- mode:给新创建的文件设置权限
- flag参数:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读,写打开
- 这三个常量,必须指定一个且只能指定一个
- O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
- O_TRUNC :如果文件存在再次以写方式打开清空文件
2. 关闭文件:close
#include <unistd.h>
int close(int fd);
3. write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
4. read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
代码演示:
以写的方式打开文件,若文件不存在则创建文件,再次向文件写入向后追加
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
const char* buf="hello Linux file!";
write(fd,buf,strlen(buf));
close(fd);
return 0;
}
5. 如何理解open中的标记选项
在日常写程序时,我们想创建一个标记一般会直接定义 int flag ,如果需要两个则定义
int flag1 、int flag2 ,若需要多个标记的话这样写不仅难以维护而且有点浪费空间。
做标记无非就是要表达有或没有、是或不是的信息,一个整形变量有32个比特位,
每个比特位都单独可以做为标记位,这是OS设计系统调用的常用方式。接下来我们自己模拟实现一个位图的函数理解一下:
#include<stdio.h>
#define ONE (1) //1
#define TWO (1<<1) //2
#define THREE (1<<2) //4
#define FOUR (1<<3) //8
void print(int flag)
{
if(flag&ONE)
printf("one\n"); //替换成其他功能
if(flag&TWO)
printf("two\n");
if(flag&THREE)
printf("three\n");
if(flag&FOUR)
printf("four\n");
}
int main()
{
print(ONE);
printf("\n");
print(TWO);
printf("\n");
print(ONE|TWO);
printf("\n");
print(ONE|TWO|THREE);
printf("\n");
return 0;
}
三.理解open返回值(文件描述符)
为什么fd是从3开始的呢?0、1、2去哪里了呢?
--- 0:标准输入流(键盘)
--- 1:标准输出流(显示器)
--- 2:标准错误流(显示器)
这三个流在程序启动时就自动开启了 ,Linux下万物皆文件,操作系统将这三个标准也视为文件,所以文件创建时是从3开始的
既然0、1就可代表键盘与显示器,那向显示器打印信息除了printf还可以这样做:
为什么一个整数就可以代表一个文件呢?文件描述符的本质是什么?
---- 上面说过,操作系统需要将打开的文件进行管理,区分哪些文件被打开了,哪些文件马上就要关闭等等信息,在Linux下,每一个打开的文件都会被struct file的结构体所描述,为了便于管理,会将这些结构体用双链表进行连接管理,这个结构体中还存在一个指针,表示文件内核级的缓冲区,想要读取磁盘的文件中内容,只需将内容放入该缓冲区,程序就可以使用这些内容了,同理若想要向磁盘中的文件写入内容,需要将内容先写入缓冲区,再讲缓冲区的内容导入文件,这样对文件管理只需要对文件的struct file进行管理了
对文件管理的实质是进程对文件进行管理,那进程是如何管理自己文件的呢?
进程在创建时会创建自己的PCB,一个进程可以打开多个文件,为了区分这些文件具体是哪些进程管理的,每个进程的PCB中存在着一个struct files_struct* 的指针,这个指针用来管理进程打开的文件,为了进程与文件的对应关系,struct files_struct结构体中保存着struct file* [ ]的指针数组,既然是数组一定会有数组下标,则操作系统会将0号下标指针指向保存标准输入的struct file的地址,将1号下标指针指向保存标准输出的struct file的地址,将2号下标指针指向保存标准错误的struct file的地址,同理将3号下标指针指向自己打开的文件struct file的地址
综上所述:
# 文件操作符的本质就是文件映射关系数组的下标,所以一个整数就可以访问一个文件
# open过程的操作:
1.创建file
2.开辟文件缓冲区,传输文件数据(延后)
3.查询文件描述符表,构建映射关系
4.返回下标
四.理解Linux下万物皆文件
在上文中,我们谈到操作系统将键盘与显示器等也看做文件,但是他们本身为硬件,该怎么理解万物皆文件呢?在Linux下是怎么做到的?
对于设备其实我们关心的数据就两个,属性与操作方法,对于属性一个结构体就可以描述,而方法的话,不同设备的方法一定是不同的,但是可以把他们的参数与返回值设置的类型相同。
操作系统在打开一个设备时,就会创建其对应的struct file,该结构体除了一些关于文件信息的内容,还会存在一些函数指针来实指向对应设备的方法,struct file结构体中一定还有一个指针指向设备的属性,可以让我们访问到,所以想要访问一个设备,只需要找到其对应的struct file通过内部函数指针或者指向描述属性结构体的指针就可以操作这个设备。在Linux下一套叫做vfs(虚拟文件系统)
所以在上层看来,不论是磁盘级的文件还是设备级的文件不做区分,只将他们看做struct file,实现了万物皆文件
五、重新理解不同语言对文件操作的封装
上文解释到在Linux操作系统下,进程是依靠文件描述符来操控文件的,但是C语言中对于文件操作的函数却使用的是FILE*的指针,连stdout、stdin、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()
{
FILE* fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
printf("fp:%d\n",fp->_fileno);
return 0;
}
语言的文件操作函数,本质底层都是对系统调用的封装!
那C语言为什么要这样做呢?
因为C语言(其他语言也是这样)想实现跨平台性,不同操作系统的系统调用是不同的,仅使用系统调用的代码不具有跨平台性,所以语言会对系统调用进行封装,在哪个操作系统下就使用该操作系统的系统调用,这样就可以实现语言的跨平台性。