
一.理解文件狭义上理解我们电脑中的各种文件它们储存在磁盘中。即狭义上文件指储存在磁盘中的文件磁盘是永久性储存介质因此文件在磁盘上的储存是永久性的磁盘是外设(既是输入设备也是输出设备)对磁盘中文件的操作本质上都是对外设的输入输出简称IO。广义上理解Linux下一切皆文件。文件操作文件 文件属性 文件内容空文件也是会占用磁盘空间的所有文件操作本质上是对文件属性或内容进行操作。系统角度对文件的操作是谁执行的用户吗并不是文件储存在磁盘中而磁盘的的管理者是操作系统也就是说对文件的操作是操作系统完成的进一步讲是对文件的操作本质上是进程对文件的操作我们使用的函数都是系统调用的封装。进程对文件操作前是否要打开文件一个进程是否只能打开一个文件呢如何理解打开文件对文件的任何操作都必须先打开文件且一个进程是可以打开多个文件的那么让系统知道每个文件对应的进程就是必要的(先描述再组织)因此系统会对被打开的文件创建结构体变量管理起来。也就是说文件打开的标志就是OS为其创建结构体变量。二.回顾C语言文件操作接口fopen和fcloseFILE*fopen(constchar*path,constchar*mode);fopen是C语言中用来打开文件的接口path是打开文件的路径(默认当前工作目录)mode是打开文件的方式它有6个选项r只读打开文件对于不存在的文件会打开失败对于存在的文件其内容会被保留r只读写打开文件写入时从文件起始位置写入对于不存在的文件会打开失败对于存在的文件其内容会被保留w只写打开文件写入时从文件起始位置写入对于不存在的文件会创建该文件对于存在的文件其内容会被清空w只读写打开文件写入时从文件起始位置写入对于不存在的文件会创建该文件对于存在的文件其内容会被清空a只写打开文件写入时从文件末尾位置写入对于不存在的文件会创建会保留存在文件的内容a只读写打开文件写入时从文件末尾位置写入对于不存在的文件会创建会保留存在文件的内容它的返回值是FILE类型(库中定义的结构体)的指针可以将它传入fclose以关闭该文件intfclose(FILE*fp);fwrite和freadsize_tfwrite(constvoid*ptr,size_tsize,size_tnmemb,FILE*stream);fwrite是让数据以二进制的方式写入文件而fprintf是让数据以字符串的方式写入。(第三部分会细讲)ptr要写入文件中的数据的指针size一次性向文件写入多少字节nmemb向文件写入多少次stream要写入的文件值得注意的是‘/0’是字符串的终止符是语言层面规定的在系统层面它就是普通的字符所以向文件写入字符串时系统不会遇到’/0’而结束所以size和nmemb要控制好。如果写入成功它会返回写入到文件中的字节数失败返回0size_tfread(void*ptr,size_tsize,size_tnmemb,FILE*stream);fread会以读二进制的方式将读取到的内容写入指针ptrfscanf会以读字符的方式将内容写入字符串。所以fwrite和fread、fprintf和fread最好配套使用。ptr接收文件读取到的内容size一次向文件读取多少字节nmemb读取多少次stream读取的文件。如果读取成功它会返回读取到文件中的字节数失败返回0C语言还有很多其他文件操作相关参数比如控制文件读写位置的rewind等接口这里不细究。三.Linux系统文件I/O我们写程序就是要进行数据处理那我们至少得能向文件输入数据文件至少能向我们输出数据所以我们执行文件时系统会默认打开三个文件stdin标准输入流对应键盘文件stdout标准输出流对应显示器文件stderr标准错误流对应显示器文件位图传参位图传参是向函数传一个参数而达到传多个参数效果的传参方式他利用了一个参数有多个bit位这一特点位图指的就是参数的二进制形式。#includestdio.h#defineONE_FLAG10#defineTWO_FLAG11#defineTHERR_FLAG12#defineFOUR_FLAG13//让它们保存二进制表达上都只有一个1voidTest(intf){if(fONE_FLAG)//ONE_FLAG位图上为1的位置f在此处也为1printf(ONE\n);if(fTWO_FLAG)printf(TWO\n);if(fTHERR_FLAG)printf(THERR\n);if(fFOUR_FLAG)printf(FOUR\n);}intmain(){Test(ONE_FLAG);printf(\n);Test(ONE_FLAG|TWO_FLAG);printf(\n);printf(\n);Test(ONE_FLAG|TWO_FLAG|THERR_FLAG|FOUR_FLAG);return0;}运行结果文件描述符1在进程中文件描述符是用来识别该进程打开的文件的符号用整形来表示每个文件的文件描述符都不同。详细会在接下来慢慢展开。关于文件操作的系统调用打开文件的系统调用open关闭文件的系统调用close将数据写入文件的系统调用write从文件中读取数据的系统调用read。open、close可以看到这里有两个open函数但是这是C语言写的不是函数重载可能是用了某些技术达到了函数重载的效果。pathname要打开文件所在的路径默认是当前工作目录flags如何打开文件(只读、只写…)该参数用很多选项这些选项都是在库中定义的宏而且这个参数的传递方式就是上面说的位图传参mode文件权限使用open可能会创建新文件这个参数就是用来设置新文件的权限的如0666、0656…当然权限的最终结果需要与权限掩码作用(设置权限与权限掩码异或)。flags选项;O_RDONLY只读O_WRONLY只写O_CREAT文件不存在就创建文件O_TRUNC打开文件时清空文件内容O_APPEND在文件末尾追加内容。当然还有其他选项这里只讲这5个。如果打开文件成功open就会返回该文件的文件描述符失败则返回-1。C语言库中fopen就是对该函数的封装其选项r、w等就是以上选项的组合。intfdopen(log.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);//相当于fopen中的wintfdopen(log.txt,O_CREAT|O_WRONLY|O_APPEND,0666);//aintfdopen(log.txt,O_RDONLY);//r当要关闭文件时要用函数close向其传递文件描述符就可以关闭该文件描述符对应的文件。write、readwrite:write是将指针指向的内容写入指定文件的函数。fd目标文件的文件描述符buf要写入文件的内容任何指针类型都可以count要写入文件的字节数。如果写入成功它将会返回写入的字节数(包括0)写入失败返回-1。C语言中的printf系列函数和fwrite函数都是对write的封装但我们上面说printf系列函数是以文本形式写入文件fwrite以二进制形式而它们都是同一个系统调用的封装这是怎么回事实际上在系统层面往文件写入内容只有二进制写入这一种方法(将数据以字节为单位写入)语言层面上的两种方法只是语言定义的本质上都是以二进制方式写入文件。read:将文件中的内容读入buf。fd目标文件的文件描述符buf储存文件内容的指针count从文件中读的字节数。要注意read读到换行符也不会停要控制好count。如果读取成功且读到了文件末尾则返回0如果没到文件末尾就返回读到的字节数读取失败返回-1。C语言中scanf系列函数和fread都是对该函数的封装。实验open、write、closeintmain(){intfdopen(log.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);charput[]hello open\n;intcount5;while(count--){write(fd,put,strlen(put));}close(fd);return0;}open、read、closeintmain(){intfdopen(log.txt,O_RDONLY);charbuffer[64];intn0;intcount0;while(1){nread(fd,buffer,sizeof(buffer));if(n0)break;buffer[n]0;//read不会自动设置/0printf(%s,buffer);count;}printf(%d\n,count);//检查read读了多少次close(fd);return0;}为什么要封装操作系统是有很多种的Linux、windows、macos…每个操作系统内的系统调用都是不一样的如果代码只用系统调用来写会导致代码在A操作系统上可以运行而不能在B操作系统上运行。但是如果把每个操作系统的系统掉用都封装在库中运行代码时再根据当前操作系统把其他操作系统的代码段截掉就可以实现同一代码在不同操作系统下运行。也就是说对系统调用的封装是为了让代码有可移植性。文件描述符2在每一进程中每个文件都有着自己的文件描述符stdin/out/err也有。前面有提到过FILE是C语言库中定义的结构体类型那我们不难猜出在该结构体中有着储存文件描述符的变量(_fileno)。intmain(){intfdopen(log.txt,O_RDONLY);printf(%d\n,stdin-_fileno);printf(%d\n,stdout-_fileno);printf(%d\n,stderr-_fileno);printf(%d\n,fd);close(fd);return0;}可以看到stdin/out/err分别对应文件描述符0、1、2而新打开的文件按顺序紧接其后。操作系统层面的文件管理打开文件时OS会为每个文件生成一个结构体file(先描述)然后再用数据结构(如双链表)将这些结构体组织管理起来(再组织)。而PCB中有一个file_struct指针类型的变量files它指向一个结构体变量在变量中有一个struct file*类型的数组它储存在该进程打开的文件地址而文件描述符实际上就是该数组的下标。因为同一个文件可以被多个进程同时打开所以struct file和PCB一样同时存在于多个数据结构。文件描述符3当我们打开一个文件时系统会为它分配可以分配的最小文件描述符。比如说我关闭了stderr这是文件描述符表中stderr的位置就会空出来当系统遍历该表时找到第一个空的位置就是stderr原本的位置这使系统就会把文件描述符2分配给新打开的文件intmain(){intfd1open(log.txt1,O_CREAT|O_WRONLY|O_TRUNC,0666);printf(%d\n,stdin-_fileno);printf(%d\n,stdout-_fileno);printf(%d\n,stderr-_fileno);printf(%d\n,fd1);printf(\n\n);close(stderr-_fileno);intfd2open(log.txt2,O_CREAT|O_WRONLY|O_TRUNC,0666);printf(%d\n,stdin-_fileno);printf(%d\n,stdout-_fileno);printf(%d\n,fd1);printf(%d\n,fd2);return0;}重定向在Linux中我们可以使用、、这些符号来实现重定向它实际上就是将原本要读取或写入数据的文件替换为了其他文件那它底层是怎么做到的呢重定向的底层比如说我要向stdout打印信息而经过重定向后信息打印在了文件Astdout和A储存在文件描述符表中当发生重定向后文件A的地址会覆盖stdout的地址。而即使发生了重定向文件的文件描述符是不会变的即stdout的fd还是1A的还是3。但当你向函数传stdout或其文件描述符进程在文件描述符中找到的却是取代了stdout位置的A最后使本来要写入stdout内的信息被写入文件A。那么stdout被覆盖后会被关闭吗其实当文件打开时OS创建的结构体内会有一个变量ref用来计数当前文件被几个文件描述符中的指针指着只有ref归0时文件就会自动关闭。如这上面的stdout就会被自动关闭。要注意的是当进程fork子进程后父子进程共享文件描述符表这时也会把子进程内的计数加上。重定向函数重定向函数有两个分别为dup、dup2。dupintdup(intoldfd);dup() uses the lowest-numbered unused descriptor for the new descriptor.dup会将oldfd对应的文件指针拷贝到文件描述表中下标最小且为空的位置但原本文件的文件描述符不会改变只不过是多了一个指针指向它。如果成功它会返回拷贝到的新下标的位置失败返回0。dup2intdup2(intoldfd,intnewfd);dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the followingdup2需要传入两个文件描述符oldfd和newfd它会将文件描述符表中oldfd位置对应的指针拷贝给newfd对应的位置但原本在这两个位置上的文件的文件描述符不会改变。原来在newfd位置上的文件不会关闭如果需要得自行关闭。要注意oldfd和newfd到底该传哪个文件描述符如果成功它会返回newfd失败返回0。intmain(){intfdopen(log.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);charput[]hello dup\n;intndup2(fd,1);printf(%d %d\n,fd,n);printf(%s,put);close(fd);return0;}stdout、stderr标准输出流和错误流都指的是显示器文件#includecstdio#includeiostreamintmain(){//往标准输出流输出printf(hello printf\n);std::couthello std::coutstd::endl;printf(\n);printf(\n);//往标准错误流输出perror(hello perror);std::cerrhello std::cerrstd::endl;return0;}它们都打印在显示器文件。重定向补充当我们将上面代码的输出重定向写入log.txt时会发现文件内只有本应往标准输出流输入的数据这是因为这个指令只默认重定向了标准输出流(1)它的完整写法应该是值得注意的是当显示写文件描述符时它和重定向符号之间不能有空格。那如果我们想把往标准错误流输出的信息也打印进其他文件怎么办像这样显示器文件没有打印任何东西显然是成功将stdout/err重定向了但奇怪的是log.txt文件中只有本来往标准错误流打印的信息。这是因为这条命令会两次打开log.txt相当于这样intfd1open(log.txt,..|TRUNC,...);dup2(fd1,1);close(fd1);intfd2open(log.txt,..|TRUNC,...);dup2(fd2,2);close(fd2);所以这样是没法让两种信息全输入进同一文件的。有两种写法可以做到1.第二次使用追加重定向这里也是open了两次但第二次是追加选项没有清空文件。2.文件描述符复制重定向相当于intfd1open(log.txt,..|TRUNC,...);dup2(fd1,1);close(fd1);dup2(1,2);为何区分经过上面重定向的演示我们可以知道虽然stdout/err指的是同一文件当可以通过重定向把它们分别写入不同文件这样我们就可以更好的拿到错误信息。四.理解一切皆文件Linux中一切皆文件并不是说所有东西本质都是文件而是把一部分设备和内核对象抽象成文件让进程可以用统一的文件描述符和系统调用接口去访问它们但这只是用户空间访问接口的抽象不代表硬件本体是文件也不能把它们都当成普通文件来用。它是如何抽象的呢拿硬件举例子当系统要打开硬件设备时它会为要使用的设备用结构体device描述起来并用数据结构将它们组织管理起来。同时还会为他们创建file结构体也会将它们组织管理起来我们将这组织起来的结构称为VFS虚拟文件系统。在每个file结构体中有一系列函数指针它们用来指向对应硬件的接口(尽管不同硬件的接口实现大不相同也用相同类型的指针指向)这样就PCB-文件描述符表-file结构体-接口这条链来对硬件进行操作。这里的file结构体其实就是C语言版的多态即使其内部成员相同但函数实现不同。五.缓冲区什么是缓冲区缓冲区就是内存的一段空间。拿生活来举例子如果没有快递站会怎么样快递到了你就只能第一时间去拿如果你没空又得麻烦快递员等这样对双方都不利。但有了快递站就解决了这个问题无论你是否有时间快递到了快递员直接将它寄存在快递站你有时间去拿就好这样都有利于对方。在这个例子里快递站就相当于缓冲区你是操作系统快递员是用户。来看这个代码为什么前者没把库函数打印的信息写入文件后者却可以这就是缓冲区的存在导致的我们也不难推出缓冲区和库有关。其实有两种缓冲区用户层缓冲区和内核级缓冲区。当我们用库函数如printf、fprintf、fwrite(观察这些函数的参数不难推测出C语言层缓冲区在结构体FILE中)等将信息写入文件时信息会先被拷贝进语言层缓冲区然后在满足特定情况下语言层缓冲区将被刷新信息被拷贝进文件内核缓冲区最后文件内核缓冲区内的信息再被OS拷贝给文件只有完成了这一系列操作数据才被真正写入文件。我们也可以看出计算机内的数据流动本质上就是拷贝。导致上面那种情况的原因就是数据在缓冲区中要刷新时文件已经关闭了无法再写入。刷新缓冲区的情况对于内核级缓冲区内核中的情况复杂多变OS会根据实际情况刷新缓冲区我们只需要将数据交给内核缓冲区(OS)就相当于交给文件(没关闭)了。对于语言层缓冲区1.强制刷新使用函数强制刷新缓冲区如fflush()。2.进程退出时当进程退出时会自动刷新缓冲区。3.刷新条件满足时3.1 立即刷新(无缓冲写透模式)3.2 缓存满了(全缓冲)该方法效率最高普通文件一般都采用该形式3.3 行刷新(\n行缓存)该条件只对显示器用来看看这个代码intmain(){//使用库函数printf(hello printf\n);fprintf(stdout,hello fprintf\n);constchar*shello fwrite\n;fwrite(s,strlen(s),1,stdout);printf(\n);//使用系统调用constchar*puthello write\n;write(1,put,strlen(put));intidfork();if(id0)exit(0);wait(NULL);return0;}将它重定向写入文件会发现用库函数打印的数据被拷贝了2份。语言层的缓存区是在用户空间的(FILE结构体)所以当父进程fork创建子进程后父子进程将共享一块语言层缓冲区但它们中任一进程要结束时会使用fflush刷新缓冲区这时就会触发写时拷贝操作系统会将还没退出的进程的语言层缓冲区拷贝给要退出的进程后退出的进程又会刷新一遍缓冲区。这就导致向文件写入了两份内容。为什么要有缓冲区系统调用是需要花费时间的如果没有缓冲区那你每一次输入都需要使用系统调用这会花费大量时间。但如果有了缓冲区你可以将多次输入的数据用一次系统调用写入文件这大大减少的时间消耗。也就是说缓冲区可以提高使用者的效率。六.模拟实现极简stdio.h文件mystdio.h:#includestdio.h#defineMAXBUFFER1024#defineNONE_FULSH0#defineLINE_FLUSH1#definePULL_FLUSH2typedefstructIO_FILE{intfileno;//文件描述符intflags;//读写状态charbuffer[MAXBUFFER];intbufferlen;intflush_method;//刷新方法}MYfile;MYfile*MYfopen(constchar*path,constchar*method);voidMYclose(MYfile*file);intMYfwrite(constvoid*ptr,MYfile*file,intcount);voidMYfflush(MYfile*file);文件mystdio.c:#includemystdio.h#includestring.h#includeunistd.h#includesys/stat.h#includesys/types.h#includefcntl.h#includestdlib.hMYfile*BuyFile(intfd,intflag){MYfile*file(MYfile*)malloc(sizeof(MYfile));if(!file){perror(error mlloc);returnNULL;}file-filenofd;file-flagsflag;memset(file-buffer,0,sizeof(file-buffer));file-bufferlen0;file-flush_methodLINE_FLUSH;//默认行刷新returnfile;}MYfile*MYfopen(constchar*path,constchar*method){intflag0;if(strcmp(method,r)0)flagO_RDONLY;elseif(strcmp(method,r)0)flagO_RDONLY|O_WRONLY|O_TRUNC;elseif(strcmp(method,w)0)flagO_CREAT|O_WRONLY|O_TRUNC;elseif(strcmp(method,w)0)flagO_CREAT|O_RDONLY|O_WRONLY|O_TRUNC;elseif(strcmp(method,a)0)flagO_CREAT|O_WRONLY|O_APPEND;elseif(strcmp(method,a)0)flagO_CREAT|O_RDONLY|O_WRONLY|O_APPEND;elsereturnNULL;intfdopen(path,flag,0666);if(fd0)returnNULL;returnBuyFile(fd,flag);}voidMYclose(MYfile*file){if(!file)return;MYfflush(file);close(file-fileno);free(file);}intMYfwrite(constvoid*ptr,MYfile*file,intcount){intold_countcount;while(countfile-bufferlenMAXBUFFER){intsubMAXBUFFER-file-bufferlen;memcpy(file-bufferfile-bufferlen,ptr,sub);ptr(char*)ptrsub;count-sub;MYfflush(file);}memcpy(file-bufferfile-bufferlen,ptr,count);file-bufferlencount;if(file-flush_methodLINE_FLUSHfile-buffer[file-bufferlen-1]\n)MYfflush(file);returnold_count;}voidMYfflush(MYfile*file){if(file-bufferlen0)return;intnwrite(file-fileno,file-buffer,file-bufferlen);(void)n;fsync(file-fileno);//将fd对于的内核级缓存区的内容强制刷新到文件中file-bufferlen0;}