文件与fd
- 一、前置预备
- 二、复习c语言文件
- 三、系统文件认识
- 3.1 系统层面有关文件的接口(open):
- 3.2 简单使用open参数
- 3.3 语言vs系统
- 3.4 进一步理解文件描述符:
- 四、内核中的文件
- 4.1 初步了解
- 4.2 理解一切皆文件:
- 4.3 IO基本过程
- 4.4 重定向
一、前置预备
1、首先我们先回忆一下,在linux的前几章,文件=内容+属性(时间等属性)
2、在c语言中使用文件的形式是固定的—先打开文件,然后操作文件,最后关闭文件,那为什么在访问文件之前我们必须先打开它了?
(1)首先我们需要知道文件没有被打开时存放在哪里?----一般来说文件都存放于磁盘
(2)那是谁在访问文件?----是不是当前我们写的代码所编译形成的程序啊,这个程序我们也可以称为一个进程
(3)既然是进程来访问我们的文件,那根据我们前面所学的知识,进程是不是需要被加载到内存中然后被cpu来执行,当进程运行到打开文件这一行代码时,cpu能够直接去读取磁盘上的文件吗?—很显然是不能的(冯洛伊曼),所以打开文件本质上就是文件加载到内存当中,那我们可以类比一下,进程被加载到内存时,进程=内核数据结构+代码、数据,文件是不是可以认为,文件=内核数据结构+文件内容。
3、结论:我们研究打开的文件,是在研究:进程和文件的关系
4、研究文件系统可以分为两部分来学习:
(1)没有被打开的文件(磁盘)
(2)被打开的文件(内存)
二、复习c语言文件
1、在c语言或者c++中,一个进程会默认打开3个文件,就是标准输入输出流和错误流,对应的就是键盘、显示器。但是我们知道键盘和显示器都是硬件设备,我们能够直接通过程序访问硬件设备吗?—很显然不能,原因后面讲解。(stdin / stdout / stderr)
2、是谁默认打开这三个标准输入输出流?----进程
下面就是c语言中对文件的两种操作方式:
1、w(只写),Truncate file to zero lengh or create text file for writing.(>)
(1)没有该文件,创建该文件,并且写入
(2)有该文件,但是没有对文件写入,则清空
(3)有该文件,并且有写入,从0位置覆盖式写入
2、a(追加),顾名思义不会清空该文件内容,而是从结尾继续添加写入的内容(>>)
3、我们有几种打印到屏幕的方式?:(纯C)
4、我们知道用户是不能直接访问硬件的(磁盘,显示器,键盘),要想访问必须经过OS,所以我们所使用的C文件接口,底层一定封装了对应的文件类系统调用
三、系统文件认识
在c语言或者c++中,文件的操作其实是给我们封装好的,我们调用语言内的函数,然后它又调用系统接口,那我们现在就来看一下系统中文件的接口
3.1 系统层面有关文件的接口(open):
open中的参数:
(1)pathname:文件的文件名
(2)flags:常见的打开方式
O_RDONLY(只读)、 O_WRONLY(只写)、O_RDWR(读写),O_TRUNC(存在该文件就清空)、
O_CREAT(创建)、O_APPEND(若文件操作,追加),这些打开方式本质上是宏
第二个位置的操作方式可以传多个,但是一般来说系统的接口是c语言的,不支持可变参数,这里是通过位图的方式实现的,下面我们通过简单的代码来看下位图是如何控制标记位的
(3)mode:权限位,在系统中文件管理与权限管理是两个不同的模块,如果文件已经存在,第三个参数没有影响,但是如果我们是打开一个新的文件时,会创建一个新的文件,如果不带第三个参数,那么该文件的权限是乱码。
(4)系统中的open调用是有返回值的,它的返回值类型为int,当文件操作失败返回-1,正常操作返回 !0,我们一般称这个返回值叫做文件描述符。
由上图我们可以看到,本质上系统中文件调用的接口只有两个,两个接口差别很微小,只有第三个参数的差别。(如果文件不存在,我们建议使用三参数open,如果文件已经存在则建议使用两参数open)
3.2 简单使用open参数
1、打开文件:
这里的umask不会改变系统的umask
2、touch的简单实现:
3、关闭文件(close),读(read),写(write),
综合使用文件接口:
3.3 语言vs系统
总结:语言上的fopen就是封装的系统调用接口
3.4 进一步理解文件描述符:
1、文件描述符我们可以简单的理解为该文件在当前程序操作的所有文件中的序号,一般来说我们打开的文件默认从3开始,因为系统默认打开了三个文件,stdin(0),stdout(1),stderr(2)。
2、字符串以\0结尾是c语言的规定,跟文件无关,所以写文件时只写字符长度,如果写的时候让长度+1,也就是想将\0也写入,这个时候系统会默认\0为乱码
四、内核中的文件
4.1 初步了解
上面我们简单的介绍了系统中的文件调用,现在我们来理解一下内核中的文件:
1、在文件没有启动时它是以内容+属性的形式存储于磁盘空间内部
2、文件被打开是进程(task_struct)来打开的,也就是文件加载到内存中(创建struct file)
3、进程被CPU调度时会调用open系统调用
4、在内存中存在很多个struct file的结构体对象,它管理着文件的打开属性,系统默认会打开3个文件(键盘,显示器,显示器),在OS层面上,有一个file list将所有结构体链接起来,对文件的管理,变成了对链表的增删查改
5、进程也是由一个链表链接起来管理的(task_struct)
6、进程链表管理进程,file链表管理文件,OS层面是解耦合的,(一个进程-----多个文件)为了让两者相关联,PCB内部存在一个指针(struct files_struct* files),OS会为每个进程里的该指针创建一个struct files_struct的结构体,其中会存在一个叫做struct file* fd_array[N]的数组(指针数组)
7、上面这个数组中下标对应的位置直接连接文件的结构体
8、文件描述符本质上是数组下标,进程和文件用指针来相关联
9、OS层面,fd是唯一访问文件的方式,FILE是C提供的访问文件的结构体,它会有很多的属性 --(其中有一个属性必定对fd做封装)
4.2 理解一切皆文件:
1、之前我们说过每一个硬件都有一个共同的名字叫做外设,我们如果整体来看的话可以发现所有设备可以拥有相同的属性类别,但是属性具体的值可以不一样,怎么理解呢?例如:设备号,状态值等,所以我们是可以将所有的外设用一个结构体去描述的,现在我们要访问键盘、网卡、磁盘以及显示器,对上述设备的访问的方式一定是不同的,例如键盘只能读,显示器只能写,那这个结构体怎么才能将所有设备统一呢?
2、在外设的角度上,所有的设备都其实实现了read和write方法,但上面我们刚说有些外设只需要其中一种,这个不必担心,需要的方法我们不去实现它即可
3、站在linux操作系统上,对一个文件进行操作之前,我们都会创建一个struct file,这个结构体里会有一系列的调用外设的方法,我们来介绍其中最典型的两个,就是
int (*read) ();
int (*write) ();
操作系统通过函数指针的形式来访问外设的操作函数,如此现在我们就不需要关心键盘、网卡、显示器、、、的具体函数实现,我们只需要调用接口即可,这一套机制我们称为vfs(虚拟文件系统)
4、在struct file中又会提供文件描述符,所以我们对文件的操作转化到了对文件描述符上的操作,系统调用也是通过对这些函数指针操作的
5、上面介绍的这种上层的struct file与外设的struct device这种技术实际上就是多态的实现原理
文本写入 vs 二进制写入
1、我们需要明白显示器显示12345,这里到底是一个整数还是5个数字字符?–其实这里显示器显示的是5个字符,所以实际上我们编写的代码中输出所有整数类型,系统都会先转化为字符,然后再在显示器打印,系统就觉得太麻烦了,每次都需要我们自己转换,并且我们写的可能还会出现许多问题,故此系统封装了一系列函数
2、c语言中的scanf与printf它叫做格式化输入与输出,它的作用就是将文件内的内容以固定格式来输入输出,其次计算机所谓的文本与二进制其实没有本质差别,不管我们输入的是字符,数字,符号,本质上计算机都是以二进制处理的,例如可执行文件,我们要知道可执行文件会被编译成二进制,我们向二进制文件输入文件,它会认识吗?
3、那为什么c、c++还会封装一系列函数呢?这是因为虽然系统给我们做了一层封装,但是要是换系统了呢?上面我们都是基于linux系统来谈的,封装的好处就是语言的可移植性
4、为什么我们使用c语言的库函数在linux或者win下都可以随便使用?这是因为这些库函数的代码其实全部都封装在库内(glic)当我们需要调用库内的函数时,系统会将该函数编译成当前系统的版本,例如win版,linux版。所以在使用一门语言时,需要先安装环境,安装环境就是安装库
4.3 IO基本过程
1、input:当我们调用write函数时,我们通过文件描述符找到了文件操作表,又在文件操作表中通过函数指针调用写的操作,但我们写的内容并不是直接就写入硬盘的,而是先写入文件内核缓冲区,再刷新至硬盘文件空间,刷新是由os自主决定,意识是指缓冲区有多少内容或者多少时间执行一次刷新,由操作系统来决定
2、output:与上面的操作相反,但也是现将硬盘上的文件内容拷贝至缓冲区,然后os自主决定刷新
缓冲区(系统)的存在是因为内存的操作速度太快了,外设的操作速度非常的慢(例如我们要进行IO操作,如果没有缓冲区,我们每进行一个字符的操作就都要进行一次外设的访问,效率太低了,但现在我们有了缓冲区,也就是说我们现在可以向缓冲区写入100或者1000的数据然后进行一次写入写出,效率大大的提升了),同时系统也会进行预加载,进一步提升IO地效率
所以write与read本质上是拷贝函数。
4.4 重定向
1、fd的分配规则:进程打开文件,需要给文件分配新的fd(最小的,没有被使用的),如果系统默认的0,1,2被关闭,系统也会将0,1,2分配出去
1、在上面的代码中如果没有关闭1,这个文件的话,会默认打印3,4,5,6在显示器上,这也符合我们的预期,但当我们关闭1后,本来应该向显示器打印的内容,却写入到了log1.txt文件中,这是为什么呢?
这是因为printf函数默认会向stdout这个文件打印,我们知道printf也是通过文件描述符去操作文件的,但是printf只认1这个文件描述符,但此时1这个文件描述符已经被log1.txt给占用了,所以现在的操作变为了向log1.txt写入了
所谓的重定向就是系统只认0,1,2,但是现在我们让特定文件描述符内的内容做出修改,而系统毫不知情
系统的中重定向接口叫做dup2
系统实现重定向的方式非常简单,0,1,2这三个位置本质上是指针数组,我们现在让数组内3号位置的指针,拷贝到1号位置,这就是输出重定向,这一些列操作都必须使用系统调用也就是dup2(oldfd,newfd)
例如输出重定向就是dup2 (fd,1);