一.理解⽂件1-1 狭义理解⽂件在磁盘⾥磁盘是永久性存储介质因此⽂件在磁盘上的存储是永久性的磁盘是外设即是输出设备也是输⼊设备磁盘上的⽂件 本质是对⽂件的所有操作都是对外设的输⼊和输出 简称 IO这是我们需要先了解的五条原则访问一个文件都必须先把这个文件打开没有被打开的文件在磁盘上被打开的文件才在内存中最后一条就是我们一个进程可以打开多个文件我们操作系统是存在多个进程的所以我们的操作系统内一定存在大量被打开的文件。所以我们研究文件本质就是研究进程和文件之间的关系。1-2 代码理解和部分接口我们直接来看一个代码这个代码就是通过打开一个文件然后关闭它我们先看这个fopen函数。第一个还是传入文件路径第二个传入属性。大概这几个属性首先我们先来看看我们图中传入的第一个参数是一个文件名字这个文件在我们的当前目录中并不存在是这样子的一个过程首先就是现在当前路径下查找一下这个文件如果没有找到的话那么就创建这个文件给一个写入属性就行。我们下面来运行一下这个代码。为什么会在当前路经下运行呢因为我们每个进程都有自己的cwd我们的这个代码就是一个进程他有自己的cwd。跑一下。我们通过如图的指令就能找到我们的cwd了。我们这样也就修改了当前路径。我们通过这个代码也能清晰的了解到打开文件本质就是我们的进程打开文件。cwd就是我们打开文件的默认路径。我们之前写的这个第一个我们“”表示的就是我们从当前路径中去找表示从系统指定的路径下去找谁帮我们找啊就是编译器编译器跑起来也是一个进程他也有cwd等路径。这个fwrite接口的作用就是往我们的文件中写入内容的我们创建了log.txt文件然后在文件中写入内容第一个参数表示我们写入内容的起始地址第二个参数表示我们需要写入的单个参数的大小第三个表示我们要写入元素的个数第四个参数是我们要写入的文件的指针。以w方式创建的文件在写入之前都是需要先清空文件内容再写入的。那么如果我们要想写一个专门清空文件中内容的软件该怎么设计呢就是类似一个这样子的代码了此时我们log.txt中的内容就被清空了我们写的比较简陋。如果是以a方式打开的文件此时就不会清空原来的数据了。这是这几个打开方式的手册。文件位置我们怎么理解呢你可以把文件看成一个一维数组存放的都是string类型的字符串我们的代码换行就相当于我们给当前位置的string数组中的下一个位置一个\n的所以我们之前用的c语言的一些接口都是把文件的起始位置设为0的意思就是从第一行开始读取。你像之前我们往文件中写入内容的时候这个我们通过这个重定向的方式打开文件打开文件的方式就是以w方式打开的先清空再写入的。这种就是以a方式打开的文件。我们经常使用cat这个命令我们下面自己通过学习fread接口来自己实现一个简单的cat指令吧。这是我们实现这个操作所使用的代码首先就是先判断了你使用这个命令是否正确应该是cat文件名的用法如果你的argc2说明你错误使用了我们的cat指令然后就是通过读方式打开一个文件然后判断是否成功打开了buff[0]0就能快速的初始化我们的buff数组然后通过调用我们的fread函数第一个就是传入一个指针我们传了数组首元素的地址然后再你传入的单个数据元素的字节数然后是想要读取的元素个数最后是你指向打开文件的指针传进去就行了这里的返回值最主要要看第三个参数如果失败 / 遇到 EOF 时会小于nmemb甚至为 0如果正确读完了但是你的文件中的元素的个数小于你的第三个参数那么就返回你的文件中元素的个数否则才返回第三个参数那个fwrite也是一样的。这个feof(fp)就是判断你的文件是否读完的我们来使用一下这个。此时符合我们的需求。我们来理解一下这些东西首先我们之前也就说过我们不管是输入还是向显示屏打印一些数字本质都是一些字符串一个一个的字符你是如何通过%d什么的再把这些字符变成我们的整型的数字呢就是通过调用一些接口来实现的通过putchar这些接口再把一个一个的字符变成整形的数字你的scanf函数也是一样的键盘输入的全是字符内部调用了接口才把这些字符都转换为了整形存放到我们的变量里面的这叫做格式化输入我们的prinf叫做格式化输出我们也说过键盘和显示器的本质都是文件也可以叫做文本文件那么文本文件和我们的二进制文件是通过什么确定的呢是你调用了不同的接口才导致的这些性质还是文件本身自己的属性决定的呢它是这样子的一种关系首先就是你是什么类型的文件是你本身自己的性质决定的然后通过你的这些性质再选择调用不同的接口这是一个因果关系。所以现在问大家一个问题。就是我们输出信息到显示器你都有哪些方法呢通过printf标准化输出我们就不说了下面再来说几种其他的方法。fputsfprintffwrite这几个方法。就是通过下面的几种方法来实现就是直接向文件中写入一些东西比如你直接向显示器上写入一些内容此时就会在显示屏上显示出来了。我们使用一下它们。这个fprintf就是向指定的文件中打印我们把第一个参数传入显示器文件也是一样的。这个fputs就是第一个参数传入我们的字符串第二个参数还是传入我们的需要打印到的文件。这个fwrite我们之前讲过。下面我们用代码来看一下这几个函数的使用。就是这样子的这四个函数都是向显示器输出的。1.2.1 open这个方法是系统调用打开文件所使用的函数。第一个open函数是你要打开的文件存在第二个参数就是设置打开的方式。第二个open就是打开的文件不存在最后一个参数是给这个新创建的文件一个权限。这个就是open有一个返回值正确了就返回文件描述符错误就返回了-1。这是我们一些常见的打开方式。我们的int flags是拥有32个比特位的一个比特位一个标志为但是我们的这些宏都是只有一个比特位的。下面我们通过一个代码来理解一下。这里我们就是定义个几个宏分别将我们的1向左边移动多少个比特位我们都知道int是32个比特位所以我们的第一个宏就是....00001表示的就是1第二个宏就是.....00010表示的就是2以此类推我们接下来来揭秘一下它为什么可以传入多个宏呢因为我们用的是按位或|这个符号比如我们传入的是VERSION1 | VERSION2此时二进制是0001和0010按位或就是只要对应位有一个是 1结果就是 1否则为0按位与之后就是0011就是3了我们传入3之后再通过按位与)操作来找到我们传入的宏这个只有对应位都为 1结果才是 1否则是 0。我们传入了3也就是0011我们给VERSION1 也就是0001按位与之后就是0001结果为1我们与VERSION2也就是0010按位与是0010也就是2也符合if中的条件我们按位与VERSION3也就是0100按位与之后就是0000不符合if语句条件所以进不去。这就是我们的使用 我们要注意几个地方如果文件不存在如图所示我们就必须给它O_CREAT这个打开方式否则会找不到文件也要把umask设置为0否则默认的umask会干扰我们给文件设置的权限因为系统存在一个默认的umask因为我们文件的最终权限 预设权限 (~umask)预设权限 按位与 取反后的 umask。这个close函数就是通过这个fd这个返回值来关闭文件的。我们想向log.txt中写入内容。这个就是写入我们系统调用使用的是write函数写入的c语言是fwrite都是封装了操作系统中的方法实现的我们第三个打开方式O_TRUNC的作用就是我们再写入的时候会先清空这个文件中的内容然后再写入要不然就是覆盖了举个例子第一次写入abcd此时log.txt中就是abcd如果没有第三个打开方式的话此时你再次写入123的话此时就会变成123d了。还有你c语言的fopen也是封装我们上面的open方法来实现的。下面我们来看一下这个这个返回值也就是文件操作符。首先我们这里是调用了四次这个open方法有了四个返回值然后打印了四个返回值我们来看拿一下结果。这里出现了3456整齐排列的整数为什么会是这样呢它到底是什么呢我们打开的文件都是存在一个struct file的结构体来管理这些进程的也就是我们打开的文件它是通过双链表的形式管理的。我们的struct task_struct的结构体中存在一个struct files_struct结构体这个结构体中存在一个fd_array数组专门存放这些进程的地址的它就会把这些打开的文件放入到我们的这个数组当中返回值就是我们的数组的下标。这是我们通过创建进程打开文件和对文件如何管理的一个过程首先创建了进程这个进程的PCB就是我们的task_struct这个结构体中存在一个files_struct结构体这个files_struct结构体中又存在一个fd_array数组这个数组就是来管理我们通过进程打开的文件的。那为什么不是从012开始啊为什么直接就从3开始了啊。下面我们来解释一下我们在之前也说过打开文件是通过进程打开的那么进程在运行的时候必然会先打开三个流一个stdinstdout和stderr这三个文件都是进程的标准流这三个进程会占据前三个位置也就是012但是这三个文件都是FILE* 类型的啊我们也说了我们的操作系统只认我们的文件操作符就是我们上面写的代码的fd所以说我们的FILE这个类型必然是一个结构体我们的结构体中必然存在一个变量存放我们的文件操作符否则操作系统无法操作这几个文件就像我们上面写的代码一样它是通过我们fd来关闭和打开文件的我们写个代码验证一下我们的猜想。我们的猜想得到了验证发现是正确的。1.2.2 操作系统是如何往文件中写入内容的呢通过这张图我们来理解一下首先就是左边打开进程创建内核数据结构如何打开了三个流文件然后进行管理这就不说了我们要是通过open打开文件的时候首先文件是存在在磁盘当中的文件包含内容加属性然后就是我们的属性会通过我们的file结构体是管理我们打开的文件的它其中的变量会关联你可以理解为可以找到我们的文件属性你也可以理解为储存了文件属性而我们的内容则是存放在 内存中的一块叫做文件内核缓冲区的东西我们的用户往文件写入的时候你调用的接口都封装了write文件操作符内容这样的用法直接把内容拷贝到我们的文件内核缓冲区的起始write的本质就是拷贝所以此时拷贝完之后你的内容存在在文件内核缓冲区当中而不是磁盘当中的。如果我们读内容也只能从内存缓冲区当中读取而不是从磁盘中读取此时如果没有内容就会阻塞到这里存在内容就拷贝到用户空间中让我们看到。我们进行增删查改操作的时候都需要先让文件的内容从磁盘中拷贝到内存缓冲区当中此时从内存缓冲区当中进行操作然后剩下的就交给操作系统来刷新我们的磁盘了。1.2.3 文件描述符分配的规则这个就是规则我们来验证一下。这样子的一个代码大家可以猜一下结果。答案变成0了因为我们把stdin关闭了0的位置就腾出来了我们打开的文件就可以放到此处了。这里不要关闭fd为1的因为它是stdout。我们原来1指向的是我们的标准输出printf就是往1中打印的此时你把1关闭了打开了一个文件此时这个1指向的就是我们新打开的文件了但是我们的printf还是依然会往1指向的内容中打印结果此时结果就打印到了新打开的文件当中了这个现象叫做输出重定向。这是输入重定向首先就是先关闭我们的stdin然后通过读方式打开文件此时你要输入的内容就不是从键盘获取了而是从我们的log.txt中获取了你可以给log.txt内容设为10 20 30此时a就是10后面的依次类推了和我们从键盘获取我们输入达到的效果一样这叫输入重定向。我们理论上应该是可以在log.txt中看到打印的结果的下面我们来看一下。我们发现上面都没有啊下面我先说两种解决办法后面再讲原理。此时就可以看到了。1.2.4 dup2就是输入两个fd我们简单来说一下用法输入两个fd然后旧的fd会把新的fd的内容给覆盖掉比如我们输入13此时3的内容就会被覆盖为1的内容此时13一摸一样此时都是原来1的内容就是这样子用的。就是这样子使用的原来的都一样此时我们不用关闭1了而是打开新文件之后直接用新文件的fd区覆盖我们的1位置的内容此时效果和我们上面通过close写的效果一样。我们来思考一个问题它俩指向同一个文件此时如果你关闭其中一个文件此时是否还可以往里面写入内容呢下面看例子大家可以思考一下哪个111不会被追加到我们的log.txt中呢我就直接来讲了答案是第二个不会因为打印操作系统默认找到的是1位置打印的如果你把1关闭了此时1位置不指向任何文件此时肯定不会打印啊但是我们关闭了3由于13指向同一个文件你通过一个指针把这个文件关闭了为什么1指向的还可以往里面写入内容呢这是因为文件中还存在一个计数器的变量两个指针指向同一个文件计数器就是2关闭一个此时计数器变为1只要不为0就还可以往里面写入内容。思考两个问题子进程会直接继承父进程的所有东西此时都会指向同一块内容只要你不做出修改它俩的数组指向的内容是一样的。此时父子进程会向同一个文件中打印内容。所以这里也可以解释了为什么默认打开了012呢这是因为bash是开始所有进程的父进程你创建的进程都是继承bash的所以都会默认打开这三个因为bash打开了。第二个问题也是不会影响的因为你程序替换仅替换进程的用户空间代码 / 数据不会修改内核中的文件描述符表因此历史打开的文件描述符会被完整保留。只有给文件描述符设置FD_CLOEXEC标志时exec才会关闭该描述符默认不关闭。这种设计的核心是 “资源继承”让程序替换后能复用已打开的文件、管道、网络套接字等资源。二.理解“⼀切皆⽂件⾸先在windows中是⽂件的东西它们在linux中也是⽂件其次⼀些在windows中不是⽂件的东西⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件你可以使⽤访问⽂件的⽅法访 问它们获得信息甚⾄管道也是⽂件将来我们要学习⽹络编程中的socket套接字这样的东西, 使⽤的接⼝跟⽂件接⼝也是⼀致的。 这样做最明显的好处是开发者仅需要使⽤⼀套 API 和开发⼯具即可调取 Linux 系统中绝⼤部分的 资源。举个简单的例⼦Linux 中⼏乎所有读读⽂件读系统状态读PIPE的操作都可以⽤ read 函数来进⾏⼏乎所有更改更改⽂件更改系统参数写 PIPE的操作都可以⽤ write函 数来进⾏。你像我们底层的这些硬件都是谁来调用使用的呢答案是我们的进程我们的进程通过调用这些方法来实现这些硬件的使用每个硬件都有很多相似的方法如果没有这个功能就不在这个函数体中写内容但是还是存在的类似于多态比如我们的基类是动物动物可以是猪和鱼他们都会吃饭睡觉但是鱼会游泳猪不会但是也要给猪游泳的这个方法。我们看一下上层是怎么样的。上层就是创建进程进程的内核结构通过打开不同的文件因为Linux下一切皆文件吗所以键盘什么的全是文件打开之后通过数组管理然后这些文件都有自己的结构体通过结构体中的函数指针来指向这些硬件的方法这就是c语言实现多态的方式基类的函数指针都是一样的以下的硬件全是子类都有上层函数指针的全部方法如果自己并没有这个功能就不写那个函数即可这就是C语言实现多态的方式。三.缓冲区我们先来一个例子来简单了解一下缓冲区。缓冲区的本质就是一段内存空间举个例子张三要送一个礼物给李四如果自己去送的话可能需要一个月的时间但是如果给了菜鸟驿站虽然菜鸟驿站可能也需要一个月的时间但是张三的时间节省了这个菜鸟驿站就相当于缓冲区缓存的意义就是提高使用缓存的进程的效率的。但是菜鸟驿站的老板不可能说来一个快递就发走一个快递这是不可能的而是需要积压几天然后一块发送好多快递我们的缓冲区也是积压多个数据然后一块刷新到磁盘上这样提高了双方的效率。这是几个刷新策略。3.1 FILE缓冲区下面我们来看一个我们之前一直疑惑的问题现在我们来解决一下。我们加了\n和不加这个为什么会有不同的结果呢我们之前只是粗糙的说了一下现在我们展开说一下首先我们printf执行完成之后数据会在缓冲区中哪个缓冲区呢FILE缓冲区那么它和内核缓冲区有什么区别呢现在我先来说说这个FILE缓冲区是什么FILE缓冲区的本质就是这个结构体中的一些数组。这是操作系统源码中的实现的就是通过这些变量来构成一个缓冲区。进行了如图的操作才有了我们的FILE。我们的缓冲过程我来讲一下就是我们的FILE打开了一个文件然后往文件中写入内容此时就是通过键盘键盘这个文件调用fgets写入到我们的缓冲区当中这个缓冲区就是我们的buff数组我们的文件操作符给了fd此时这个buff数组中存放的数据刷新策略和内核的刷新策略一样此时如果满足刷新策略就会调用fputs中它封装了write方法来写入到了我们的内核文件缓冲区当中了然后再刷新我们有\n的时候此时就是满足了行刷新此时直接进入到内核缓冲区中然后再刷新到磁盘就能看见了我们就能在显示器中看见了但是没有的话只能等进程结束再刷新了。​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​我们看一下第三个问题因为我们如果没有语言级别的缓冲区的话你每次写入都是需要调用write这个系统调用的方法的这样才能将内容写入到内核文件缓冲区中的此时就会频繁的调用系统调用会导致效率低下浪费时间而我们语言级别的缓冲区只有在我们满足刷新策略的时候才会刷新到内核中只调用一次系统调用提高了效率。​​​​​​​ ​​​​​​​为什么会提高效率呢以我们的printf为例子。如果没有这个缓冲区此时我们的这每一个printf都需要调用系统调用使用write函数写入到内核缓冲区当中此时效率就会变得很低。我们此时这个格式化输出的时候也是格式化到我们的语言级别的缓冲区当中了。我们看一下上面没有解决的一个问题这个我们疑惑为什么没有显示到这个文件当中呢注释去掉之后这是因为我们打印的东西此时都是存在在语言级别的缓冲区当中的你通过fd把1这个文件关闭了那么我怎么通过fd调用write方法加载到内核当中呢所以肯定不会在这个文件当中了我们加了个fflush这个方法的本质就是在关闭之前调用了一下write的。下面我们再来思考几个问题。可能大家还是存在疑问我不是\n了吗为什么没有直接从用户缓冲区加载到内核当中呢可以参考一下这个解释它就是说如果你往显示器上打他确实是行刷新但是如果你是往普通文件什么的打的话默认是全缓冲的不是行刷新但其实操作系统刷新策略是非常复杂的原来我们的stdout指向的是显示屏文件但是现在指向的是我们普通文件。那么我们既然说了用户缓冲区的刷新策略那么内核缓冲区呢这是我们内核的刷新策略。我们看一下这个代码再看一下两个不同的结果分析一下。上面的是我们打印到我们的log.txt中下面是打印到我们的显示器上为什么会出现这种情况的我们来分析一下。首先打印到显示屏上就是行刷新的每一行都刷新到内核然后刷新到磁盘可以让我们看见。但是打印到普通文件的时候就不一样了需要全缓冲所以当我们前三个进入到用户缓冲区的时候会在里面停留然后我们最后一个调用的是系统调用直接进入到内核中所以最后一个会先进入到内核当中等待然后我们其他的还在用户缓冲区创建子进程此时父子进程公用一个缓冲区此时子进程结束需要将用户缓冲区的内容刷新到内核缓冲区中然后删除用户缓冲区此时对缓冲区进行了修改需要发生写时拷贝此时父子进程都有独立的用户缓冲区当我们的父子进程都结束的时候就会刷新进入内核了。此时前三个就会存在两份了这是由于子进程造成的。内核缓冲区只有一个它是属于操作系统的不属于父子进程个人不会继承。再看一下这个代码。为什么会造成这个现象呢往log.txt中写入的时候它会先把标准错误给输出出来然后我们的log.txt中只有标准输入啊。看一下图中的代码为什么我们写入到普通文件的时候标准错误并没有写入进去呢这是因为我们./a.out log.txt是./a.out 1 log.txt的简写所以我们只是把标准输入stdout打印到普通文件了我们的标准错误对应的是2stderr还是打印到显示屏中的所以才会出现这个现象。这个就是分别打印到不同文件中。这个是都打印到一个文件中的操作。那么为什么我们的perror没有换行符也能比下面有换行符的先打印出来呢这是因为我们的perror标准错误是直接进入到内核的然后尽快刷新到磁盘的你可以理解为自动加上换行符了。相信上面的图大家还是存在疑问为什么perror先打印了这个cerr后打印了呢首先执行第一句此时它会进入到我们的用户缓冲区等待全缓冲但是我们的perror不需要等待所以直接进入内核先打印然后我们的cout再进入又因为这个stdendl能强制刷新缓冲区所以我们用户缓冲区的两个语句就会进入到我们的内核缓冲区了再次触法强制刷新刷新到磁盘然后最后才是cerr的刷新。