当前位置: 首页 > news >正文

云主机 网站吗久久诗词网

云主机 网站吗,久久诗词网,绿色在线网站模板,wordpress 小工具 php目录 1.文件操作 2.文件描述符 3.缓冲区 4.系统的缓冲区 1.文件操作 在C语言学习中#xff0c;我们就已经使用了一些文件操作相关的接口#xff0c;在学习IO之前#xff0c;我们首先要复习一些以前讲过的概念#xff0c; 1. 空文件也要在磁盘中占用空间#xff0c;因为…目录 1.文件操作 2.文件描述符 3.缓冲区 4.系统的缓冲区 1.文件操作 在C语言学习中我们就已经使用了一些文件操作相关的接口在学习IO之前我们首先要复习一些以前讲过的概念 1. 空文件也要在磁盘中占用空间因为文件 内容 属性   2.对文件的操作分为对文件内容的操作、对文件属性的操作以及对这两者的操作 3 表示一个文件要用 文件路径文件名具有唯一性 4 如果我们操作文件没有指定对应的文件路径默认就是在当前路径进行查找 5 当我们在代码中使用了 fopen、fclose、fwrite、fread等接口之后代码编译形成二进制的可执行程序之后但是我们没有运行起来对应的文件操作有没有被执行 当然没有因为我们没有执行对应的代码。所以对文件的操作本质是进程在对文件进行操作 6 一个文件如果没有被打开能够直接进行文件访问和操作吗不能反过来说一个文件要被访问及必须要被打开。而要打开文件需要 进程调用系统接口然后由操作系统执行内核的系统调用的代码。所以文件从应用角度来看能分为两类被打开的文件和没有被打开的问价。 综上文件操作的本质就是进程与被打开的文件的关系。 文件操作 我们知道C语言有对应的文件操作的接口C也有自己的文件操作的接口java、python等语言也都有自己的一套文件操作的接口而不同语言的接口又不一样那么对于我们使用者来说学习这些接口的成本是很高的。但是万变不离其宗所有的接口要进行文件操作底层都是需要调用系统接口来完成的为什么呢这是由我们的冯诺依曼体系结构以及我们的软硬件层状结构决定的因为文件是存储在磁盘上的磁盘是硬件也是外设访问外设的操作只能由OS来完成上层所有的操作想要访问硬件都需要由操作系统来完成都需要调用操作系统提供的一套接口这也间接说明了操作系统是必须给我们提供一套文件级别的系统接口的。 所以不管上层语言的文件接口如何变化他们的接口的底层使用的都是操作系统的接口那么我们只要掌握了文件操作的系统调用不管使用什么语言都不影响我们使用文件操作。 下面我们就直接使用C语言的文件操作接口与系统调用接口来对比学习。 C语言的一套接口 打开文件 fopenconst char*path , const char* mode; 打开文件的选项或者说模式有  r (以只读形式打开文件不存在则打开文件出错)  、 w (以只写方式打开会清空文件的所有数据是覆盖式的写) 、 r (以读写的方式打开文件不存在就出错) “w”(以读写的方式打开覆盖式的读写文件不存在会创建)  a(追加文件不存在创建) a (读写式的打开文件写是追加的写) 这些模式使用的时候要注意的就是读写式的打开文件时要分清楚文件指针当前所处的位置。 关闭文件 fclose( FILE* pf) 写入 : fprintf以标准格式写入使用的方法就是 printf 的参数前面加一个 流  读取fscanf 以标准格式读取 和 fgets 以行为单位从指定的流中读取 ,fgets(char* s , int size ,FILE*stream); fgets读取成功会将读取到的数据放到指定的缓冲区 (数组)中size 是指定的读取的最多字符个数 当读取了 size-1个字符或者读取到换行符换行符会丢弃或者读到文件末尾时fgets 默认会在缓冲区结尾添加  \0 所以我们传size的时候可以直接使用sizeof(s)传参。 当然fgets也有可能读取失败比如你的文件打开失败而你没有检验然后传了一个空指针给fgets读取失败会返回 NULL。 系统调用接口 打开文件的接口 open  open接口使用需要包含三个头文件 这两个接口的区别就是如果文件存在我们使用这两个接口都行如果文件不存在需要再打开文件的时候创建那么我们就只能用 第二个接口 mode 参数就是设置文件创建的初始权限的。当文件是已经存在的文件时mode参数就不会用到而如果当我们需要创建文件时但是我们使用的是第一个接口那么文件的初始权限就是一个随机值这就很麻烦。第一个参数则是文件所在路径和文件名如果不指定路径就默认在当前路径下搜索和创建。 第二个参数 flags 就是我们打开文件的选项常用的就是以下几个 这些选项都是一些宏都是一些标记位 。  对于标记位我们怎么理解呢 我们的C语言中我们可以使用一个整数它的值为0或者为 1来表示是否执行某些操作比如下面的在这个函数 我们的Add函数并不是一定就执行加法运算而是会依据参数传过来的标记位的不同来执行不同的操作。 但是有的函数是不只有一个标记位的一个函数可能要执行多个功能那么就需要多个标记位来判定哪些功能要执行哪些功能不执行而如果按照我们上面的方法使用一个int类型的整数来表示一个标记位那么如果有 很多种不同的功能每一个功能都需要传一个 int 的参数来进行标记这是很不方便的既浪费了空间降低效率同时调用函数时我们传参也会十分繁琐其实最主要的还是浪费了大量的空间因为一个功能只有执行和不执行这两中选择那么在实际中我们只需要一个比特位就能确定是否执行该操作比如这个比特位为1就执行该操作为0就不执行该操作没必要拿一个 32 个比特位的int‘类型的数据来标记浪费了太多空间。 而在操作系统层面就是这样做的他的标记位是以比特位来传递的那么一个 int 类型的整数就能传递32个标记位但是我们要注意标记位不能有二义也就是同一个功能只能用一个比特位来标记同时一个比特位只能标识一个功能。位置不能重复。在函数中就能通过位运算或者直接用一些宏来判断哪些选项或者说功能是要执行哪些功能不执行那么如果想要执行多个功能呢 比如执行 0001 和 0010 对应的功能这也很简单我们可以传参的时候传这两个标记为的 或 运算的结果也就是  0001 | 0010 。 那么我们上面的 open 的选项也是这样的操作系统一般都是通过比特位来传递标记为或者说选项的而我们上面的 open 选项也就是几个常用的宏 通过或操作将选项组合起来传参就能实现多个功能 但是还是有一个前提就是传递的选项不能够冲突比如只读和只写这两个不要一起传。 比如我们写了这样一个代码以只写的方式打开文件外面传的文件名不存在同时我们也给了文件的初始权限我们这样传参能够创建并打开文件吗答案是不能的因为我们只传了 O_WRONLY也就是只写选项而没有传 O_CREAT 文件不存在时创建文件的选项这个选项只是一个建议选项如果文件已经存在这个选项和 mode 参数就不起作用了但是不会影响已经存在的那个文件。 我们发现只写的方式打开并不会我们创建一个文件如果我们对open的返回值进行判断的话就会发现打开文件失败了但是由于我们到这里还没有讲它的返回值所以就不演示了马上就会讲到 。  当我们把O_CREAT加上就能完成创建文件的操作了。 同时如果我们想要打开一个文件进行写入但是不想覆盖掉原来的内容而是进行追加式的写入那么我们可以 | O_CREAT 。 同时我们也发现文件虽然被创建出来了但是他的权限并不是我们指定的权限这很容易理解是由于权限掩码的作用那么如果我们想要在程序中设置权限掩码要怎么设置呢  使用 umask 函数 这样一来我们就能将以指定的权限创建文件并进行写入了。我们使用的 umask 函数只是修改了我们的进程的权限掩码修改的是当前进程的 umask 发生写时拷贝所以并不会影响到我们的bash进程的 umask。  open函数的返回值我们称之为文件描述符你没有听错文件描述符其实就是一个整形这个文件描述符要比C语言的 FILE 结构体要更加底层。如果open打开文件失败会返回 -1 同时也会将错误码设置在errno中。 关闭文件close 关闭文件只需要传文件描述符就可以了。 文件写入write write就是将缓冲区的数据向文件描述符指向的文件中写入。三个参数文件描述符缓冲区以及要写入的字节个数write返回值则是成功写入的字符个数 这里的缓冲区的类型时cosnt void*类型可以接受任何类型的数据同时write的写入是二进制写入而不像C语言还给我们提供了字符写入等接口因为在操作系统看来所有的数据都是二进制不分类型那么为什么我们打开文件时看到的却是我们输入的内容而不是一些我们看不懂的二进制呢文本文件的编码方式将我们的数据转换成的二进制数据重新转换为了源数据。 同时我们在使用C语言的写入文件的接口时当我们需要写入字符串的时候一般都会把\0也算进个数以\0作为字符串的结尾将\0也写入方便我们读取文件。但是在Linux中这个\0却是没有必要的因为操作系统是不会把\0当成字符串的结尾的而是将其当成一个正常的数据不会说要找到\0才会停止写入当然如果你需要这个\0作为读取时的标记当然也可以写进去只是他在文件中的形式可能不是 \0 的形式。  当我面对一个已经存在的文件进行写入时我们会发现一个与C语言的不同。 虽然O_WRONLY的写入默认也是覆盖式的写入但是他是写一个覆盖一个而不是像C语言的“w“一样打开文件的同时就直接将数据清空了。  如果我们要对文件进行清空再写入则要加上O_TRUNC选项 如果是要追加式的写入那么就是用O_APPEND选项。 所以C语言的一个简单的 w 模式底层就是 O_WRONLY | O_CREAT | O_TRUNC这三个选项的传参而 a 则是 O_WRONLY | O_CREAT | O_APPEND 读取文件 read 从文件中读取最多count个字节的数据放到缓冲区中。依旧是读取二进制的数据但是我们可以将二进制的数据强制转换成我们想要的类型如果是字符串在读取的时候我们需要在缓冲区后面预留一个位置用来放 \0 。 read的返回值是读取到的字节个数我们可以判断 返回值是否大于 0 来判断是否读取成功如果读取失败则会返回 -1. 这就跟C语言的时候累死了要预留字符串结尾。 当我们以读写方式打开文件的时候在C语言中需要使用 fseek 来调整文件指针的位置而在linux系统层面则是需要一个系统接口 lseek 来设置读取或者写入的偏移量他的一些选项如下图 这个偏移量目前我们就理解成文件指针就行了因为都是指定读取和写入的位置的偏移量在后面的章节会讲。 语言提供的接口再不同都是对系统调用的封装底层调用的都是系统的这一套接口将系统接口掌握了文件操作就很容易理解了。 2.文件描述符 在讲文件描述符之前我们要先聊一个话题进程可以打开多个文件吗 这当然可以很简单就可以验证出来那么系统中有那么多的进程同时都会打开文件那么系统中就一定会存在大量打开的文件而操作系统需不需要对这些被打开的文件进行管理呢当然需要操作系统需要管理我们所有的软硬件资源当然需要对这些文件进行管理那么操作系统如何管理这些被打开的文件呢 先描述再组织。操作系统为了管理对应的被打开的文件必定要为这些文件创建对应的内核数据结构来表示这些文件这就是 struct file 结构体 在这个 struct file 结构体中包含了文件的大部分属性组织的方式就是用相应的数据结构来管理这些结构体比如链式结构。操作文件/打开文件并不会将整个文件从磁盘加载到内存中试想一下一个超大文件只会讲一些文件的属性以及一些其他的东西加载到内存。 这些被打开的文件又是如何与打开它的进程关联起来的呢 这个问题我们留着后面来讲。既然我要将文件描述符而文件描述符又是一个int类型的数那么我我们把它打印出来看一下。 int main(){//直接循环打开多个文件int i0;char filename[15];//保存文件名int fd[10]; //保存文件描述符while(i10){//生成文件名 sprintf(filename,%s%c,file,i0); fd[i]open(filename,O_WRONLY | O_CREAT,0666);i; } for(i0;i10;i) { printf(%d\n,fd[i]); close(fd[i]); } return 0; } 我们发现open返回的文件描述符是一些连续的小整数从 3 开始的那么晚这里就有两个问题呢为什么是一些小整数呢小整数代表着什么呢 为什么是从3开始而不从0开始呢 我们先解决第二个问题为什么从3开始而不是从0开始。在学习C语言的时候我们讲过一个基本概念就是每一个C语言程序启动时都会默认打开三个流标准输入stdin、标准输出stdout以及标准错误stderr这三个流在C语言中都是FILE*类型的也就是文件结构体的指针这里刚好有三个文件是不是就对应着文件描述符缺失的 0 1 2 呢我们可以来验证一下。 int main() { printf(%d\n,stdin-_fileno); printf(%d\n,stdout-_fileno); printf(%d\n,stderr-_fileno); return 0; } 我们知道语言是上层而操作系统是底层那么语言中的FILE结构体中就一定包含了文件描述符的字段这也就是我们上面的 _fileno 成员。 这就是我们当前进程的fd为 0 、1和2 的文件 这是我们通过上面的程序就能直接验证出来的. 那么为什么文件描述符是一些连续的小整数呢?这就需要我们了解文件与进程的关联的关系了,以一个进程为例 , 一个进程是同时需要关联多个被打开的文件的,这一点光从三个标准流就能看出来,那么晚每一个文件都是一个 struct file 结构体来表示,而进程的所有属性都是在PCB里的,那么晚进程的PCB就一定有字段能够找到进程打开的问价的结构体,其实这中间还有一层.   PCB中有一个 struct file_struct* files ,这是一个指针,指向的是一个属于该进程的用来保存 struct file的数据结构对象. 而在 struct file_struct* files 指向的结构体中,包含了一个数组,这个数组的就是  struct file* fd_array[ ]   不难看出这是一个指针数组,数组的元素就是struct file的指针,也就是指向文件描述的内核结构体的指针,如此一来,进程就能通过PCB中的struct file_struct指针找到一个保存该进程相关文件的结构体,在从其中的 struct file* fd_array[ ]中找到 fd 对应的文件,然后进行操作. 这样一来我们就能理解文件描述符fd是什么了 ,他其实就是文件的结构体的指针在struct file fd_array中的下标,那么 fd 的分配规则是什么样的呢?我们前面创建文件都是直接从 3 开始分配,按照先后顺序,因为 0 1 和 2 都已经被占用了 ,那么如果我们把这三个默认打开的文件流关掉呢(其实是解除关联)? 这时候会如何分配??? 是从 3 开始还是从前往后找到第一个被使用的拿来用呢??这个问题其实很好验证, int main() { close(0); close(3); int fd1open(a.txt,O_RDONLY); printf(%d\n,fd1); int fd2open(my.txt,O_WRONLY | O_CREAT,0666); printf(%d\n,fd2); return 0; } 我们发现,他确实是从前往后遍历数组,找到第一个没有被使用的下标来分配给新打开的文件的.文件描述符的本质就是数组下标. 到这里就又有了一个小疑惑,就是,当 三个默认打开的文件被关闭之后,这时候再打开新的文件, stdout  stdin stderr 这三个文件结构体还能用吗? 当然是能的,其实这三个结构体就是在底层对fd为0 1 2 进行了封装,那么当 0 1 2 这三个文件描述符指向的对象变了,这三个结构体所指向的文件也就变了,因为他们只是指向 0 1 2 文件描述符的文件流,而不是说固定指向显示器和键盘,显示器和键盘在操作系统眼中也就是文件,和其他文件没有很大的不同. 同时我们知道,scanf和printf默认是从stdin中读取数据以及向stdout输出数据,那么当我们的stdin和stdout指向的不是键盘和显示器了,他们也就会向新指向的文件中去读取和输出数据. //stdin指向我们自己打开的文件int main(){close(0);int fd open(a.txt,O_RDONLY);char buf[64];while(scanf(%s,buf)!EOF)printf(%s\n,buf);return 0; } 当我们让stdin指向我们的文件时,scanf读取输入的时候就不是去键盘中读取了,而是直接在我们的文件中读取. 那么修改fd为1的stdout 也是一样的 //修改stdout指向int main(){ close(1);int fdopen(a.txt,O_WRONLY | O_TRUNC);//覆盖写入 char*bufthis is a string for stdout!; printf(%s\n,buf);return 0; } 这时候输出到stdout的内容就顺利的输出到了 fd为1 的文件中了. 这就叫重定向,输入重定向( )和输出重定向( ),以及追加重定向( ) 重定向的本质就是上层的stdin等 内部使用的 fd 不变,在内核中修改数组中 fd 对应的 struct file*  但是我们发现上面的写法很麻烦,首先要关闭一个文件,然后再打开一个文件,使用起来不舒服.而系统中为了支持我们进行重定向,提供了一个系统接口, dup2 int dup2 ( int oldfd , int newfd ) 这个接口的参数命名很坑,虽然他下面解释了他的功能 .简单来说,dup2 就是一个用来进行复制文件描述符的,将下标为  neewfd 的内容复制成 oldfd 一样的内容 ,也就是说, newfd 是要被替换的下标, oldfd 是我们最后要用的下标. //输入重定向int fdopen(a.txt,O_RDONLY);dup2(fd,0); //输出重定向int fdopen(a.txt,O_WRONLY | O_TRUNC);//覆盖写入dup2(fd,1); //追加重定向int fdopen(a.txt,O_WRONLY | O_APPEND);//追加写入dup2(fd,1); 这时候我们就能实现 模拟shell 的重定向的实现了。子进程虽然也会继承拷贝一份父进程的文件描述符数组父子进程是独立的重定向时修改的时都属于自己的文件描述符表数组。 一个进程是可以同时打开多个文件的同时一个文件也是可以被多个文件打开的但是文件的结构体只有一个这些进程都是指向这一个结构体当有一个进程关闭文件时不会真的关闭该文件而是将自己的文件描述符表的该项内容删除也就是断开联系同时文件描述结构体中有一个引用计数这个引用计数会减一当计数为0时说明没有进程在使用该文件了这时候操作系统会自动将该文件关闭。所以close其实并不是关闭文件而是断开此进程于该文件的关系。 那么如何理解键盘和显示器这些外设也是文件呢 操作系统要管理软硬件要对每一个软硬件进行先描述再组织所以每一个硬件都有对应的结构体来描述包含了硬件的所有属性。硬件与文件其实差不多都是需要读属性或者写属性比如读取磁盘就是读属性保存文件到磁盘就是写属性。 但是不同的设备可能读写方法不一样驱动不同但是其他的所有属性都是可以统一描述的。于是Linux做了一个设计struct file结构体里包含了各种文件的属性虽然打开的方式可能不一样但是所有的属性都是可以用数值来抽象描述的而他们的访问方式也不一样对应的就是他们的读写函数不一样在结构体中有两个函数指针来指向该硬件的对应的读写函数这样一来操作系统就能够对所有软硬件进行统一的管理属性的描述都是统一的同时需要读写时就会在结构体中去调用对应的读写函数。 虽然struct file内部是有区别的但是在struct file的上层看来所有的文件都是一样的上层使用的是统一的接口或者结构体就能访问不管何种软硬件在上层摒弃了底层软硬件的区别之间底层的差异是通过各自的方法去弥补的这可以称之为多态。 每一个进程可以打开的文件个数是有限制的我们可以使用ulimit -a命令来查看不过现在的服务器一般都没有严格的限制了。 3.缓冲区 我们前面说我们常见的缓冲区是在用户层的比如我们之前使用缓冲区的特性来制作了一个进度条。我们来观察下面的一段代码 printf(this is printf\n);fprintf(stdout,this is fprintf\n);fputs(this is fputs\n,stdout);write(1,this is write\n,14);fork(); 这段代码就是在屏幕上打印四行信息没有问题。 但是当我们在添加一点东西比如将输出内容全部重定向到一个文件中 int fdopen(a.txt,O_WRONLY|O_TRUNC|O_CREAT,0666); dup2(fd,1); printf(this is printf\n);fprintf(stdout,this is fprintf\n);fputs(this is fputs\n,stdout);write(1,this is write\n,14);fork(); return 0; 这时候如果按照我们的逻辑的话应该也是打印四行内容到文件中因为我们每一个打印都加了 \n那么事实真的是这样吗 可是当我们执行这段代码时却发现他打印了七行内容除了write之外都打印了两次这是为什么呢 我们再把最后一行创建子进程的代码去掉之后结果发现文件中就只有四行了这时候就没问题了这说明了上面的问题就是由于创建子进程而造成的。但是奇怪的是fork是在最后的打印的代码执行完了才执行fork的为什么会出现这个结果呢我们从结果反推中间肯定发生了写时拷贝之类的操作。  联想到我们讲过的一点点关于缓冲区的知识我们讲过printf执行之后数据并不是执行打印到了显示器上而实现放在了缓冲区而由于显示器是行缓冲遇到换行符就刷新到显示器上了当数据从缓冲区刷新出去的那一刻数据就不属于进程了。当上面的程序是输出到显示器上的时候确实是行缓冲的所以结果我们能够猜到而当输出的目标变成一个普通问价之后就出现了刷新两次这是否说明了普通文件和显示器等特殊设备文件的缓冲区刷新策略不同从而导致了这样的情况。 要清楚解释上面的现象我们需要理解两个问题 1  什么叫缓冲区 2 缓冲区是谁申请的属于谁为什么要有缓冲区 第一个问题其实不难理解缓冲区不管怎么样它需要临时存储数据所以他本质上就是一段内存。类比我们的现实生活将数据写入到外设就好比送一个东西给一个人由于外设速度非常慢相当于cpu这就导致了如果你是采用亲自动手将东西送到别人手上这时候就会浪费你的很多时间也就是进程需要花费很多时间来等待外设准备就绪从而导致效率低下那么这时候就需要现实生活中的快递公司当我们两个人相距很远时模拟需要等待时间我们可以将我们的物品交给我们所在地的快递站点进程缓冲区而快递公司会在合适的时间将物品发送出去刷新策略知道送到对方所在地的快递站点...,最终交付到对方手上刷新到外设。 这时候当物品被交给快递站点的时候我们自己就可以做其他的事了不用一直等着。 在上面的例子中发送方就是我们的进程或者说用户接收方就是外设而我们所在地的快递站点就是进程的缓冲区缓冲区也是在内存里面的所以我们的进程将数据发送到缓冲区速度是取决于内存的访问速度的然后从缓冲区刷新到外设中 、而如果我们进程是直接和外设进行IO那么速度就取决于外设的速度了这样效率很低。而将数据送到缓冲区的过程就是将数据拷贝到缓冲区这样一来其实 printf、fprintf、fputs等函数本质上其实是一个拷贝函数将数据从进程拷贝到缓冲区。 那么缓冲区存在的作用就很明显了节省进程进行数据IO的时间提高整体效率 当我们将数据拷贝到缓冲区之后缓冲区什么时候才会将数据拷贝或者刷新到文件或者外设呢这就涉及到了缓冲区的刷新策略。 我们知道缓冲区就是一块内存如果数据的大小刚好就是这块内存的大小那么一次性将数据写入到外设中的效率肯定是要比分多次每次少批量的将数据写到外设中的效率要高得多的。每一次与外设的IO的大部分时间都是在等待外设准备就绪所以一般来说是一次性将所有数据刷新出去是效率最高的但是有的设备其实是有一点特殊的比如我们的显示器显示器不是给其他外设或者机器看的而是给用户看的人的阅读习惯通常是一行一行和从左往右读的所以显示器一般是行刷新能够提高用户体验而类似于磁盘中的文件则没有这方面的要求所以一般是一次性将缓冲区的所有数据刷新。 一般而言有三种策略 1. 立即刷新比如用户手动刷新--无缓冲 2.行刷新显示器 --行缓冲 3.缓冲区满再刷新磁盘文件 -- 全缓冲 还有两种特殊情况 1.用户强制刷新fflush 2 进程退出 -- 一般都要进行缓冲区刷新 我们上面讲的缓冲区都是应用层的缓冲区我们也能够知道这个缓冲区绝对不再操作系统内核中因为write没有打印两次也就是我们C语言的缓冲区那么既然是C语言的缓冲区我们应该是能够在源代码中看到的。 而我们学习C语言的时候对于缓冲区的操作比如fflushfclose传的参数都是 stdout stdin等这些都是FILE类型的结构体所以我们能够推测出缓冲区是在FILE的结构体中。 我们打开/usr/include/libio.h 这个文件找到一个结构体 IO_FILE这之中就是FILE结构体然后我们能够看到一系列相关信息比如缓冲区。简单理解的话我们可以理解为一个char类型的数组每次有数据刷新到缓冲区都是memcpy拷贝到了这个数组中然后根据刷新策略进行输入的刷新。我们能够看到缓冲区是真实存在的 这时候我们就能解释为什么重定向的时候fork之后C语言的接口会打印两次了而没有重定向的时候则只打印一次。因为没有重定向的时候是在向显示器输出数据这时候缓冲区是行刷新策略而我们每一条打印都有\n 则每一次调用完函数之后都会进行刷新缓冲区的数据在fork之前就已经刷新清除了FILE内部的缓冲区就不存在数据了。 而当我们重定向之后由于目标变成了普通的文件这时候采用的刷新策略是全缓冲也就是要缓冲区满了之后才刷新而上面三个函数的数据显然并没有把缓冲区放满所以缓冲区并没有刷新数据还存在stdout的缓冲区中。这时候fork创建子进程的时候子进程会拷贝父进程的PCB而缓冲区里面的数据一开始是被父子进程共享的而当进程退出的时候不管父子进程谁先退出由于要刷新缓冲区也就是将缓冲区的数据写到外设然后清除缓冲区由于存在清除缓冲区的行为也就是写入的行为先退出的进程会发生写时拷贝将缓冲区拷贝一份然后刷新以及清除这份拷贝出来的缓冲区这时候就已经打印一次了。而后退出的进程也会刷新缓冲区也就是原来的空间这时候会再打印一次于是C语言的接口的数据都被打印了两次。  为什么write没有被打印两次呢write是系统调用不使用FILE使用fd不存在C语言的缓冲区。 缓冲区光这么说也能够理解的差不多了我们还可以实现一个简单的缓冲区来加深理解。实现缓冲区也就是实现我们的FILE结构体以及几个简单的C函数比如fopenfclosefwritefflush。 首先将我们需要用到的头文件包含在 .h文件中 实现fopen的时候底层调用的是open同时我们需要判断模式是a w r中的哪一种我们只考虑这三种简单情况 3 _FILE* _fopen(const char* filename , const char* mode) 4 { 5 _FILE* pf(_FILE*)malloc(sizeof(_FILE)); 6 assert(pf); 7 //简单写默认行刷新 8 pf-flagsSYNC_LINE; 9 pf-size0; 10 pf-capacityMAX_SIZE; 11 memset(pf-buf,0,MAX_SIZE); 12 //打开方式 13 if(*moder) 14 { 15 pf-fdopen(filename,O_RDONLY); 16 } 17 else if(*modew) 18 { 19 pf-fdopen(filename,O_WRONLY|O_CREAT|O_TRUNC,0666); 20 } 21 else if(*mode a) 22 { 23 pf-fdopen(filename,O_WRONLY|O_CREAT|O_APPEND,0666); 24 } 25 else 26 { 27 //... 28 } 29 return pf; 30 31 } flose也是很简单调用fflush同时释放掉动态开辟的结构体 37 void _fclose(_FILE* pf) 38 { 39 _fflush(pf); 40 free(pf); 41 } 重点是 fflush 和 fwrite 的实现fwrite要实现返回刷新到缓冲区的数据个数 37 int _fwrite(void*ptr ,size_t size,size_t num,_FILE*pf)38 {39 //返回值能够写入的数据个数40 int retnum;41 42 //判断能否全部写入 W 43 if(pf-size size*num pf-capacity)44 {45 ret(pf-capacity - pf-size)/size;46 }47 //将数据写入到缓冲区48 memcpy(pf-bufpf-size,ptr,ret*size);49 pf-size(ret*size); 50 51 printf(ret:%d size:%d\n,ret,pf-size);52 //判断是否需要刷新53 switch(pf-flags)54 {55 case SYNC_NOW:56 //....57 break;58 case SYNC_FULL:59 //....60 break;61 case SYNC_LINE:62 {63 if(*(pf-bufsize-1)\n)64 _fflush(pf);65 break;66 }67 default:68 break;69 }70 printf(写入成功\n);71 return ret;72 }fflush则需要将数据刷新到外设中简单实现就是直接用write。 76 void _fflush(_FILE*pf) 77 {78 write(pf-fd,pf-buf,pf-size); 79 memset(pf-buf,0,pf-size); 80 pf-size0;81 } 4.系统的缓冲区 我们上面讲的一系列缓冲区都是讲的语言层面的缓冲区而实际上操作系统也为文件创建了缓冲区。当我们把数据从语言的缓冲区刷新的时候其实并不是直接将数据写到了外设中这样做对操作系统的管理是不利的实际上数据从语言层面或者说我们自己实现的缓冲区刷新到啦系统为文件创建的缓冲区中这也就是write的功能所以本质上来说write也是一个拷贝函数将数据从语言的缓冲区拷贝到了内核的缓冲区。 内核缓冲区刷新才是写入到外设中而内核的刷新策略则是极其复杂的由操作系统自己决定从内核缓冲区刷新到外设的过程与用户毫无关系。 所以总结来看数据从我们的程序写到外设中需要经过三次拷贝第一次是将数据拷贝到用户缓冲区中第二次则是将以后缓冲区的数据刷新到内核缓冲区中第三次将内核缓冲区的数据刷新写入到外设中。 怎么证明存在内核缓冲区呢这个我们不好证明得看源码。但是我们也可以通过一些接口来间接证明fsync。  由于操作系统将数据从内核缓冲区写入到外设的过程与用户无关那么用户怎么知道数据是否已经写入到外设或者怎么保证我们的数据写到外设呢因为有的时候我们的数据还在内核缓冲区时如果这时候操作系统崩溃或者宕机的话那么数据在内存中岂不是丢失了我们不希望让操作系统掌握一些重要数据的刷新策略而是我们想要强制主动刷新。而操作系统针对这种问题也提供了解决方案就是给我们提供了接口来主动刷新内核缓冲区。  int fsync ( int  fd ) 如果成功刷新则返回 0 刷新失败则返回 -1 同时错误信息会更新到errno中。 这时候我们就需要再次更新上面的缓冲区的实现也就是我们的 fflush 不只是将数据写到内核缓冲区中而且要将数据写到外设中 本篇文章主要讲的是基础IO也就是被打开的文件与进程之间的数据之间传送的过程这属于文件系统的一部分下一章节还会继续补充文件系统的另一部分即未打开的文件如何被管理。
http://www.zqtcl.cn/news/40611/

相关文章:

  • 松江区网站建设wordpress简单的验证码
  • 网站开发都需要什么wordpress模板导入
  • 网站备案信息核验单怎么如何做网络推广运营
  • 网站做5年有多少流量网站开发工程师ppt
  • 淘宝网站怎么做特价广告推广软件
  • 影视传媒网站设计建设银行浙江网站
  • 新手做网站如何赚钱做视频网站需要什么条件
  • 网站禁止火车头采集做网站哪家好 要钱
  • 网站基本内容天津平台网站建设哪里好
  • 建设银行天津分行网站在线html网页制作工具
  • led行业网站源码电商运营
  • 一个网站的建设需要哪些流程图做wap网站能火吗
  • 定制型网站一般价格wordpress后台怎么改密码
  • 创意智能产品设计嘉兴优化网站价格
  • 怎么快速做网站文章国家企业信用信息查询系统
  • 销售网站是什么如何做网站页面赚钱
  • 2016网站备案科技服务公司网站模版
  • 网站模板没有html文件下载平台seo什么意思
  • 企业网站的建设哪家比较好郑州seo优化顾问热狗
  • 网站建设实训的方法用别人家网站做跳转
  • 网站开发买什么书网站购物车代码怎么做
  • wordpress多站点cdn企业邮箱怎么开通注册免费
  • 网站备案可以强制撤销吗画廊网站模板 frontpage
  • 云虚拟主机 多个网站网站标题关键词长度
  • 给工厂做英文外贸网站做软件的步骤
  • 佛山网站设计专业专业的网页设计流程
  • 在线课程网站开发价格新楼盘网站模板
  • 天津网站建设网站推广收录网站制作
  • 帝国cms电影网站模板济南模板网站
  • 网站建设属不属于无形资产十一冶建设集团有限责任公司网站