初识文件
之前我们认识到当我们进行创建出一个空文件在磁盘上也是占用一部分空间的,因为文件的组成是由文件内容和文件属性共同构成。
文件=内容+属性,那我们对文件进行操作无外乎就是对内容和属性两个方面进行操作。
文件在磁盘上进行存储,那我们进行访问文件是如何进行访问的呢?
其实我们用户层进行访问文件的一般步骤代码→exe可执行程序→运行→访问文件,在这个过程中其实真正进行与访问文件进行直接关联的是进程。我们用户层要想进行文件的访问必须通过操作系统进行,操作系统为我们提供访问文件的函数接口,我们通过函数结构进行对文件的访问。
语言层面的访问文件的操作和操作系统中访问文件的操作的联系?
语言层面不管是C/C++/python/java进行访问文件的函数底层都是通过封装操作系统的函数接口进行实现的。
语言为什么要对操作系统中的函数进行封装?
操作系统中与文件有关系的函数都比较复杂,语言层面通过对操作系统中与文件有关系的函数进行封装,减少了我们进行访问文件的成本,并可以实现代码的跨平台性,库平台性的实现主要就是通过条件编译进行实现的,通过写三套(一般通过继承进行实现)代码,在每个操作系统下选择适配的代码方式进行运行。
既然语言层面已经将操作系统的文件接口进行封装好了,我们为什么还需要在进行学习操作系统的文件接口?
贴近底层的接口可以直接和文件进行交互,性能比较高
为什么说Linux下一切皆文件?
感性的认识:
我们在感性层次方面通常认为可读可写的称为文件,这里的可读可写是站在文件本身的角度,文件可以读取我们通过键盘输入的数据,文件也能将从键盘中读取的数据进行写到显示器上。那么对于硬件键盘而言,键盘只有读取的功能写入功能为空,同理对于显示器只有写入的能力读取的能力为空,所以说键盘和显示器这种硬件也是文件。
对文件进行下定义
站在系统层面的角度,能够进行input读取和output进行写入的设备叫做文件。也就是说绝大多数的硬件设备也是文件,例如键盘、显示器、网卡、声卡、显卡、磁盘在广义层面上都是称为文件。
C语言封装系统的接口
fopen-----打开文件函数
函数返回值
返回一个 FILE *
类型的指针,用于指向打开的文件。如果文件成功打开,返回一个指向文件的指针;如果打开文件失败,则返回 NULL。
函数参数
path:fopen函数进行打开文件的路径,可以是绝对路径也可以是相对路径
mode:fopen函数进行打开文件的方式,具体有以下几种
- r:以只读的方式进行打开文件,流信息位于文件的开头,进行只读方式文件必须存在。
- r+:以读写的方式进行打开文件,其他于r方式相同。
- w:以写入方式进行打开文件,如果文件存在删除文件内容,文件不存在进行创建文件。
- w+:以读写方式进行打开文件,其他和w方式相同。
- a:以追加的方式进行打开文件,如果文件不存在则创建文件,如果文件存在则在文件末尾进行追加新内容。
- a+:以读写方式进行打开文件,其他和a方式相同。
fwrite、fread
r方式进行打开文件
w方式进行打开文件
a方式进行打开文件
语言和系统层面的文件接口的来联系
语言层面kan的文件接口其实就是对于系统接口的封装,语言层面的简单实现实际上都是对系统接口的封装,这个封装是通常是比较复杂的。
系统的文件接口
查看系统的文件接口
以打开文件的接口为例
man 2 open
打开文件(open)
参数解析:
pathname:指定打开文件的路径
flag:定义打开文件的方式和选项
常用选项
O_RDONLY
:只读方式打开文件。O_WRONLY
:只写方式打开文件。O_RDWR
:读写方式打开文件。O_CREAT
:如果文件不存在,则创建该文件。O_TRUNC
:如果文件已经存在,并且打开方式是写入(O_WRONLY
),则将文件长度截断为零。O_APPEND
:以追加方式打开文件
定义文件打开方式的选项是如何通过int 类型进行传参的呢?
常用选项都是大写字母进行定义,我们见过的都是宏定义,这些选项其实也是宏定义,通过位图的方式向整形进行映射。
fopen和open的联系
fopen通过 r 的方式进行打开文件对应的系统的选项是 O_RDONLY;
fopen通过 w 的方式进行打开文件对应系统的选项为 O_WRONLY,O_CREAT,O_TRUNC;
fopen通过a的方式进行打开文件对应系统中的选项为O_WRONLY,O_CREAT,O_APPEND。
语言层面看似非常简单的操作实际上都是对底层的持续封装。
返回值解析
这里的返回值类型和语言层面的返回值类型不同,这里是通过整形进行返回,其实这只是记录文件的方式,C语言中选用了结构体指针的方式,而系统相当于选用了通过数字的方式进行记录打开文件的方式,这个数字称为文件描述符。FILE* 指向的结构体中肯定包含文件描述符。
关闭文件(close)
写入文件(write)
参数解析
fd:需要进行写入的已经打开的文件描述符
buf:指向要写入数据的缓冲区的指针。buf
应包含要写入的内容。
count:需要写入的数据的字节数。
通过umask进行修改文件的权限
文件描述符
文件描述符的初识
我们进行打开文件时,文件描述符为什么是从3开始进行连续,0、1、2这三个文件描述符为什么会消失??
其实我们在进行打开使用系统时,0,1,2这三个文件描述符就已经打开了,分别是标准输入(0),标准输出(1),和标准错误(2)。
文件描述符的理解
当时我们进行学习进程的时候,进行进程的管理时就是通过进程控制块(task_struck进行管理),task_struct中还存在一个指向files_struct的结构体指针,files_struct结构体中还存在文件指针数组,文件指针数组中又存放了每个文件的结构体信息。
进程管理:
进程通过fopen进行进行调用系统接口open,open通过文件描述符(fd)进行找到FILE结构体,然后通过FILE结构体中的FILE然后进行文件管理
文件管理:
fwrite进行调用系统接口write进行读取fd进行执行内部操作找到task_struct结构体然后通过task_struct中的指针找到files_struct结构体,然后从files_struct结构体中找到fd_array,进而根据fd_array[ fd ]找到文件的file结构体,内存文件就被找到了,从而对文件进行操作。
进程要想进行访问文件,首先需要先进行打开文件,一个进程可以打开多个文件,所以说进程:文件=1:n,所以操作系统要进行管理文件,维管理文件的方式---先描述再组织。
输出重定向
现象
通过关闭流输出的文件操作符,通过系统接口open打开log.txt文件,然后对log.txt进行写入可以发现本来应该打印到屏幕上的文件操作符fd的值并没有进行打印,字符串的内容被写到了log.txt文件中。
本质
操作系统默认打开标准输入、标准输出、标准错误通过close进行关闭标准输出,然后fd_array[1]=nullptr,通过open打开新文件,fd_array[1]存的就是新文件的struct file的地址。
注:fd的分配规则:最小的没有被占用的文件操作符
重定向的本质就是操作系统更改fd的指向。
通过手动去关闭fd_array数组中文件文件操作符的指向是非常挫的,我们可以通过dup2函数进行
对于参数的理解
通过dup函数进行重定向本质就是更改fd_array数组中文件操作符的索引及对应的内核对象的关联信息指向,将老的文件操作符的指向换成新的,将数组中fd_array中原来文件操作符索引及对应的内核对象的关联信息指向新文件。oldfd改成新的文件指向。
关于向显示器进行重定向
虽然标准输出和标准错误都是在显示器上进行打印,但是这两个显示器文件并非是一个文件,而是两个不同的文件,一般我们将可能含有错误的信息通过stderr或者cerr进行打印,一般的文本内容通过stdout和cout进行打印.将文件内容进行重定向到文件中,其实是将1号文件描述符所在的数组位置的指号针进行重新指向到新文件,2>文件名是将2号文件描述符所在的数组位置的指号针进行重新指向特定文件.可以通过这种方式进行错误日志的填写
一切皆文件的理念
在软件层面我们可以将所有的所有的软件都看成文件,当磁盘文件被加载到内存中就变成了进程,但是一些硬件在Linux下也是文件,这就比较有说法了,其实就是C++中的面向对象的思想,但是Linux操作系统是用C语言写的,面向对象都是通过类实现的,C语言中没有类只能通过结构体进行实现,结构体中可以包含成员属性,但是不能直接进行包含成员方法,成员方法的实现是通过函数指针进行实现的,通过结构体中的成员属性和成员方法进行实现硬件的功能,底层不同的硬件一定对应不同的操作方法,对于外设,每个外设的核心就是访问函数,都可以是write read IO有关,所有的设备都可以有自己的read和write,但是write和read的代码实现一定是不一样的
缓冲区
概念
缓冲区故名思意就是一段空间(这个空间是谁提供的呢??是用户??操作系统??C语言??)
作用及意义
用户进行写入的数据先在缓冲区中进行存放,当满足缓冲区的刷新策略时,数据才被打印到指定的文件,减少IO的次数,提高整机的效率,提高用户的响应速度。
缓冲区的刷新策略
- 行刷新
- 满刷新
- 立即刷新
- 特殊情况
- 用户强制刷新
- 进程退出
在语言层面(C语言)和系统层面执行打印带屏幕上时,能够将内容进行打印,当将内容进行重定向到文件中时,语言层面的内容并没有在文件中进行显现,通过fflush进行刷新之后即可在文件中进行显现
缓冲区的认识
对于所有的外设来说,更加的都倾向于全缓冲,缓冲区满了才会进行刷新,目的是减少IO的次数,在进行IO的过程中最耗时间的不是数据量的大小而是进行IO的次数。
对于显示器来说就是行缓冲设备,之所以是行缓存设备是需要进行照顾用户的体验,极端条件下是可以进行自定义规则的。但是磁盘文件就是全缓冲--主要是处于效率考量。
缓冲的存在位置
现象
通过fork()函数进行创建子进程,像显示器中进行打印和向文件中进行打印,系统层面未出现任何不同,而语言层面出现了不同,通过上面的现象我们可以知道缓冲区其实是C语言层面进行维护的
解释现象
当向显示器进行打印时,显示器的刷新策略是行刷新,当代码进行到fork时,打印的函数都已经执行完了,内容也进行打印到了显示器上,fork函数不会产生任何影响;但是当向文件中进行打印时,此时刷新策略变成了满刷新,此时当代码运行到fork时,代码全部都在C语言的缓冲区中,缓冲区中的代码也是属于父进程,此后共享的代码将进行写实拷贝,就导致了语言层面的打印呈现双倍的状态
FILE结构体中不仅只包含了文件描述符,还包含了缓冲区结构