文件有两种 ①内存级(被打开的)文件 ②磁盘级文件 该篇学习的文件都是第①种的
文件的理解认识
狭义理解
1.文件存在于磁盘上 ,而磁盘是外设并且是永久性存储的介质,所以文件存储在外设上 并且在磁盘上的存储是永久性的。
2.磁盘上的文件本质上是数据在存储设备上的持久化表示 ,而所有对文件的操作(如创建、读取、写入、删除等)都涉及输入/输出(I/O)操作,即与外设的数据交互。
广义理解
在操作系统中一切兼文件,包括磁盘键盘等外设。
认知
文件=内容+属性
①对文件的各种操作,要么是对文件的内容要么是对文件的属性。
②一个文件大小为0(也就是里面没有内容),在磁盘上也要占空间,因为需要存储文件的属性。
系统角度认识
1.访问文件,需要先打开文件 。 如在c的程序中使用fopen,在c程序没有运行时候不会执行fopen那么文件就不会被打开,只有可执行程序加载到内存运行起来后文件才会被打开,所以其实是进程打开的文件, 所以本质是进程对文件的操作。
2.由之前冯诺依曼体系那里的学习知道操作系统是管理硬件的,而文件在磁盘上,所以文件由操作系统管理,并且只能由操作系统管理 ,我们c或者c++提供的库函数之所以可以对文件操作 ,本质是因为操作系统对其提供了封装的对文件操作的系统调用。
3.一个进程是可以打开多个文件的,多个进程可能打开了多个文件,这些文件可能处于不同的状态(如:已经打开、刚要打开。刚要关闭等),并且不同的文件属于不同进程。而文件是需要操作系统管理的,此时系统当中同时存在了多个不同状态属于不同进程的文件,所以这些文件需要被操作系统维护管理起来,而使用的方式同样是先描述后组织(结构体储存文件信息来管理,然后链表连接,操作系统对文件的管理转换为对数据结构的操作)。
对c文件操作回顾
fopen
第一个参数用于指定要打开文件的路径名。
第二个参数用于指定文件的打开模式 ,例如
"r"(只读)、"w"(只写)、"a"(追加)等。
- 只读模式(
"r") :如果文件不存在,fopen将失败并返回NULL。- 只写模式(
"w") :如果文件已存在,其内容将被截断(即清空) 。如果文件不存在,则创建新文件。然而,如果由于权限问题无法创建或截断文件,fopen将失败。- 追加模式(
"a") :如果文件不存在,则创建新文件。如果文件存在,写入操作将从文件末尾开始。同样,如果由于权限问题无法打开或创建文件,fopen将失败。- 读写模式(
"r+"、"w+"、"a+"):这些模式允许同时进行读写操作,但同样受到文件存在性和权限的限制。如果第一个参数只带文件名称也是可以的,因为进程存在cwd (一个进程会存在cwd,它记录着当前的工作目录 )所以打开的文件会在该路径下找 ,而像w打开模式下如果该文件不存在会新建文件,新建文件默认也会在该路径下, 所以文件新建的位置由进程决定。
函数新建文件失败除了与打开模式有关,还与文件路径、权限、磁盘空间等多种因素有关。
打开成功会返回FILE*类型 打开失败返回NULL。
只写方式创建文件myfile 并写入信息
fwrite
ptr:指向要写入的数据块的指针。size:每个数据项的大小(以字节为单位)。nmemb:要写入的数据项的数量。stream:指向FILE对象的指针,该对象指定了要写入的文件流。返回值为成功写入的数据项的数量,如果与nmemb不同,说明发生了错误。
只读方式从myfile读取信息
fread
ptr:指向用于存储读取数据的数据块的指针。size:每个数据项的大小(以字节为单位)。nmemb:要读取的数据项的数量。stream:指向FILE对象的指针,该对象指定了要读取的文件流。返回值 :成功读取的数据项的数量。如果这个数量与
nmemb不相同,可能意味着已经到达了文件末尾或者出现了错误。feof
用于检查文件流是否已到达末尾,如果已经到达文件的末尾,
feof函数将返回一个非零值(真),否则返回零(假)。
再结合之前知道的命令行参数就可以实现cat命令
cat命令实现

stdin stdout stderr
printf 。fprintf、fwrite都可以把信息打印到显示器上,对于后两个需要指定stdout,而在linux中一切兼文件,stdout其实就是显示器的文件!所以往显示器打印,本质就是往显示器文件写入 !
C默认会打开三个输⼊输出流,分别是stdin,stdout,stderr
这三个流的类型都是FILE*类型
stdin 标准输入 键盘文件
stdout 标准输出 显示器文件
stderr 标准错误 显示器文件
就是因为这三个文件被默认打开了,所以fprintf这样的函数需要传文件指针的时候,我们能够直接传stdout。
为什么默认打开他们?
程序就是做数据处理的,编译器自动帮我们打开了,我们就不需要自己打开了。
重定向理解
上面知道 以w方式打开文件会默认清空里面所有的内容 ,所以重定向的> 会把文件内容清空的原理其实就是以w方式打开了该文件,这种打开的方式让该文件内容清空了。

而以a方式打开文件是追加写,不会清空文件原来的内容,所以重定向的>> 原理就是以a方式打开文件,这种打开方式让文件没有被清空。

系统层面理解
上面的fopen、fclose、fwrite、fread是c库里面的方式,接下来看一下linux系统调用方面的接口。
文件系统调用接口
open

open有两个参数的,有三个参数的。
返回值
打开成功返回一个int类型数据(后文再提),打开失败返回-1。

第一个参数和之前一样是路径
第二个参数是一个int类型的值 ,它是标记位,用于系统和用户直接信息的交互,用到了位图和宏的方式。
如下,宏定义中每一个参数二进制位中只有一个是1其他都为0,而Printf函数会拿参数flags与每一个这样的宏做按位与的操作,这样传的参数flags二进制中哪一位为1就会触发一个选项。
而传的参数是指定的,就是刚刚定义的宏(二进制位中只有一位是1),如果只传一个宏,那么最终会对应着一个选项,也可以同时传多个宏,宏之前通过按位或的方式作为参数,这样就可以同时触发多个选项。这样就只通过一个int类型的参数就能实现最多32个不同的选项控制。
而open的第二个参数常见的就是下面几个
如下,用(只写 | 创建) 打开一个文件,这个文件不存在就会新建,而这个新建的文件权限和我们正常新建的权限并不同?
这就需要open第三个参数的作用,可以指定新建文件的权限,而因为open打开的文件可能是存在的不需要新建所以第三个参数不是必要的,所以open有两个参数的有三个参数的。
如下,新建的文件指定为0666(也就是拥有者、所属组、other都是rw权限),但是新建的文件权限是664,因为存在权限掩码(这些都是在权限学过的内容)。
可以在程序开始时候umask(0)来控制权限掩码为0,手动设置后新建文件的权限就和预期的一样了。
close
close(int fd) 关闭文件 fd就是open打开文件成功后的返回值
write
第一个参数和close的一样,第二个参数是一个void*类型的指针,第三个参数为一次读取的个数,返回值为成功读到的个数。
如下代码,向file.txt文件中写入了三次hello xx\n,显示是正常的。
但是此时再重新向该文件写入(aaa\n)后,我们发现新写的内容(aaa\n)把原来的内容(hell)给替换掉了,而剩下的内容还在。和我们c库里的fopen不同(fopen 以w方式打开会直接清空内容),**系统调用的open不会默认清空原来的内容。**如果需要清空原来的内容,第二个参数还需要带上O_TRUNC,带上后再次运行发现果然就清空了原来的内容。
open第二个参数为(写入+清空)对该文件就是清空并写入,第二个参数是(写入+追加)对文件就是在原来内容基础上追加继续写入。
而我们知道c库里面的函数其实是对系统调用进行了封装,所以对应c库的fopen中的w就是系统调用open第二个参数(写入+清空)的封装,而c库的fopen中的a就是系统调用open第二个参数(写入+追加)的封装。
并且write的第二个参数并不是char*,而是void* ,所以可以传其他类型的指针?
是的,例如可以传int* ,但是发现打印的内容并不是6666666,而是乱码,并且文件大小只为4字节 。
如果我们要打印需要自己手动地把其转换为char*类型再传给write
写入方式分为文本写入和二进制写入,这是编程语言为方便开发者提供的抽象概念。实际上,操作系统只能识别二进制数据,所有写入操作最终都会转换为二进制形式。
像fprintf这样的格式化输出函数,本质上是将内存中的二进制数据转换为字符串格式后再写入。这种转换只是为了方便开发者阅读和处理数据,因为系统本身能直接识别二进制数据。
read

read的参数和write是类似的,对于返回值,返回成功会返回读到字节数,==0说明到了文件结尾,小于0说明读取失败
write close rear都需要用到open的返回值fd,之前c库的函数用到的是FILE*
这个fd是什么呢?和FILE*的关系?
如下,新打开四个文件并打印他们的fd,发现打印了3~6,那么0 1 2呢?为什么从是3开始的?
0 1 2就对应着标准输入 标准输入 标准错误!
fd是文件描述符 ,文件只能由操作系统直接处理,c库的函数是对系统调用的接口,而FIEL其实就是对fd的封装,FILE其实是c一个结构体的重命名,里面存着fd这个信息。

FILE是一个结构体,那么stdin、stdout、stderr就是FILE类型的指针,而里面又存在着pd信息,那我们可以用stdin->_fileno的方式来把fd为0 1 2也给打印出来

所以c库函数的使用及FILE*都是对系统调用的封装,并且不只是c,各种语言的文件操作都进行了封装。
不同的操作系统他们的系统调用是不同的,所以各个语言会针对于不同的操作系统各写一个版本的代码 ,然后根据条件编译, 在使用不同的操作系统时候,就用相应的代码而把其他的屏蔽掉,所以这样就使得上层的语言代码在不同的平台下都能使用 ! 所以就语言就具有了可移植性。
为什么语言要这样设计?方便人,进而更多人去使用它,它不会被淘汰设计者能够从中获利。
fd具体是什么?
fd从0开始,这让我们想到数组下标,而fd其实就是数组下标!
上面学习知道,本质是进程对文件进行操作,而操作系统需要把这些文件管理起来。管理方式是先描述后组织。
如下,每打开一个文件,操作系统就会创建一个struct file来对该文件进行管理,里面包含着文件的各种属性,在该结构体中还存在着文件缓冲区,文件内容会被加载到文件缓冲区中,这样一个文件的内容和属性就被一个结构体对象管理起来了,而各个结构体对象之间再通过链表连接起来,此时系统对文件的管理就转换为对这链表的管理。

而我们知道一个进程可能对应多个不同的文件,上面这样只是让操作系统能够管理起来文件了,那进程该怎么管理文件呢?
每一个进程的task_struct中会有一个file_struct* 的指针,而file_struct中会有一个存着file*的指针数组fd_arry ,而fd就是这个数组的下标,通过数组里面的指针指向的就是刚刚提到的操作系统管理文件的结构体 。
这样每一个进程就可以通过里面的file_struct的指针可以找到对应的文件描述符表,然后通过fd_array数组里面的file类型的指针就能找到对应文件的结构体,进而找到该文件的内容及属性。

所以程序使用read或write需要这个fd这个参数,其实就是进程通过这样的方式找到了对应的文件缓冲区然后进行读或者写的操作( 其实就是进行拷贝),所以write read这样的系统调用函数其实本质是内核到用户空间的拷贝函数。
对文件内容做任何操作,都必须先把文件加载(其实就是拷贝)到内核对应文件缓冲区内。
重定向原理
文件描述符(fd)的分配原则:最小的没有被使用的,作为新的fd给用户。
上面知道了fd初始0 1 2分别指向的分别是标准输入、标准输出、标准错误

所以根据文件描述符(fd)的分配原则,当我们把fd为1或3给关闭了之后,发现新打开的文件分别就是1和3了。

2呢? 因为2是标准输出,2如果被关掉了,打印也不会显示出来fd,但是神奇的发型被打印的信息到了新建的文件log1.txt中! 为什么呢?

这是因为原来fd为1指向的是标准输出,现在把fd为1的文件给关闭了,新建的文件log1.txt的fd为1了,也就是fd为1指向了我们新创建的文件log1.txt。而print函数我们知道它默认打印的是打印到stdout文件中(也就是显示器)的,而这个**指定的方式就是通过这个fd1,**而现在fd1指向的是log1.txt,所以printf现在所默认打印的地方就是我们新建的文件log.1txt了!
而这不就是重定向吗,所有重定向就是改变了文件描述符表的指针指向!
上面是没有在最后加close(fd)的现象,但是加上close后发现不仅该程序没有打印出内容而且新建的文件log1.txt里面的内容也是空的? 这在之后学习的缓冲区会知道,这里先不管,先不加close

上面的现象是通过手动关闭标准输出再新建文件的方式
接下来通过系统调用dup2来看一下

对于返回值,失败会返回-1。
第一个fd里面的file*会把第二个参数里面的file*给替换掉,也就是第二个fd会指向第一个fd里面file*指向的文件

例如,此时要完成之前的现象,也就是需要让fd1指向 新文件的fd指向的内容,那么应该dum2(fd,1)。 如下代码及现象发现果然printf默认就打印到了新建的文件里面。

并且fprintf第一个参数是stdout也会把内容打印到新建的文件log1.txt中,原理是一样的,虽然写了stdout,但是底层都是打印到fd1所指向的文件中了。
同样使用读取的接口的话,只要dup2(新文件的fd,0),那么此时默认读文件就是从新建的文件中去读取内容了。
原来的fd指向了新fd的文件,而这个文件是追加写、覆盖写、读是由新fd指向的文件所决定的
重定向=打开文件方式+dup2,所以结合命令行参数argv,通过参数的控制,就可以自己实现重定向。
在之前的Shell中实现(<读取 >>追加 >覆盖式重写)的重定向
需要在输入命令后 命令行分析之前判断一下输入的指令是不是重定向


执行的时候,它不是内建命令,由子进程根据重定向的符号(通过redir)来控制open的第二个参数,进而决定打开该文件用的什么方式打开,然后用dup2函数来改变相应的标准输入或者标准输出文件描述符表的指针指向。 然后进行程序替换,程序替换之后代码和数据进行替换,文件描述符的指针指向会继承下去不会恢复。

文件描述符的指针指向会继承,那如果我们把最初进程的fd为1的指向其他的文件了,那么stdout不就没有被人指向了吗,那如果我们想再找到它不就找不到了吗?
fd1的文件指针内容被替换之前,操作系统会先把它存到其他位置。
一个文件可能同时被多个进程打开,那么一个进程结束时候把该文件也给关掉了,那么其他进程怎么办?其他进程还要对该文件进行操作的呀?
这就和c++智能指针shared_ptr的引用计数的原理类似了,当一个进程把该文件关闭后,对应的con--,当con==0时,再把这个文件的结构体对象给释放掉。



















