
系统级别的文件操作1、学习系统调用1.1、open1.2、其它系统调用1.3、利用所学系统调用简单模拟fopen的打开模式进而读写文件2、文件描述符(fd)2.1、为什么从3开始0、1、2哪去了2.2、为什么是连续的2.3、为什么可以表示文件2.4、fd的本质2.5、fd的分配规则3、将系统调用与C语言进行关联所谓系统级别的文件操作其实就是Linux中与文件操作有关的系统调用。1、学习系统调用Linux有一些文件操作相关的系统调用这些系统调用往往被封装在了上方库函数中形成了我们在C语言见到的文件操作库函数(fopen fwrite fprintf ...)1.1、openintopen(constchar*pathname,intflags);intopen(constchar*pathname,intflags,mode_tmode);系统调用open顾名思义就是用来打开文件的。const char *pathname这个参数与fopen一样都是填文件所处的路径只输入文件名open就去“当前路径”下找同名文件输入指定路径open就去指定的路径下找同名文件。返回值open的返回值打开文件成功返回文件描述符(fd)打开文件失败返回-1同时设置errnoint flags这个参数其实是一种“标志位”表示的是文件的打开模式。我们可以观察到Linux内部提供了一些宏以输入到flags参数位置上O_RDONLY O_WRONLY O_APPEND O_CREAT...要理解这些宏的含义以及宏如何使用以能够设定文件的打开方式我们可以先来看一个小demo#includestdio.h#defineONE1#defineTWO(11)#defineTHREE(12)#defineFOUR(13)#defineFIVE(14)voidPrint(intflags){if(flagsONE)printf(ONE );if(flagsTWO)printf(TWO );if(flagsTHREE)printf(THREE );if(flagsFOUR)printf(FOUR );if(flagsFIVE)printf(FIVE );printf(\n);}intmain(){Print(ONE);Print(ONE|TWO);Print(ONE|TWO|THREE);Print(ONE|TWO|THREE|FOUR);Print(ONE|TWO|THREE|FOUR|FIVE);return0;}我们通过定义宏和位操作将多个选项“融合”为一个变量flags传入PrintPrint中再对flags做解析执行不同的选项。这样我们就做到了用一个变量接收多个选项(标志位)的目标。实际上这样的方法借助了位图的思想O_RDONLY O_WRONLY O_APPEND O_CREAT O_TRUNC等宏就是只有一个比特位是1的整型值就像上面 demo 中定义的ONE TWO THREE FOUR FIVE。我们想以哪些方式打开文件就将哪些宏处理成一个值(按位或)传入flagsopen内部就会进行解析从而按照我们想要的方式打开文件。比如我们想要按照“创建新文件、只写、追加”的方式打开文件我们就可以这样传参open(log.txt,O_CREAT|O_WRONLY|O_APPEND,0666);标志位一般表示的是是 / 否的问题。比如我们想以“创建新文件、只写”的方式打开文件那么“创建新文件、只写”的标志位就是“是”其它标志位就是“否”。如果选项有多少就定义多少标志位那么就会出现变量太多、空间浪费的情况。所以我们可以借助位图的思想采取“用一个变量接收多个选项”来节省空间、提高效率。mode_t mode这个参数表示的是文件的权限。只有在文件不存在需要新建文件的时候才需要这个参数。因为已经存在的文件权限是已经确定好了的。对于不存在的文件我们就需要新建同名文件。假设我们设置权限为“0666”(对拥有者、所属组、other都是rw)#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includestdlib.hintmain(){intfdopen(log.txt,O_CREAT|O_WRONLY,0666);// 没有 O_CREAT 且文件不存在的时候 open 会执行失败// 一定要写 0666 不能写 666 。因为权限码是八进制数if(fd0){perror(open failed!\n);exit(1);}return0;}但是我们再看一眼log.txt的权限为不是 666 而是 664 !想到了什么权限掩码文件的最终权限是最终权限 起始权限 (~权限掩码)我们在原来代码基础上再加一行语句#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includestdlib.hintmain(){umask(0);intfdopen(log.txt,O_CREAT|O_WRONLY,0666);if(fd0){perror(open failed!\n);exit(1);}return0;}代码中写umask(0)其实是在程序内部将权限掩码设置为了 0000 。我们看到结果是log.txt的权限是 0666 明显使用的是权限掩码 0000 。所以Linux中设定好的umask和程序中重新设定的umask优先采用程序中设定的umask。(就近原则)1.2、其它系统调用read ···ssize_tread(intfd,void*buf,size_tcount);参数fd文件描述符buf指向接收读取内容的位置(缓冲区)count接收读取内容位置的大小返回值如果成功返回读取到的字节数如果失败返回-1。write ···ssize_twrite(intfd,constvoid*buf,size_tcount);参数fd文件描述符buf指向要写入文件的内容count写入文件内容的大小返回值成功返回写入的字节数失败返回-1。更多的系统调用我们可能会在接下来的学习中遇到。1.3、利用所学系统调用简单模拟fopen的打开模式进而读写文件w模式文件不存在新建文件清空文件历史内容从文件的开始处写入(数组下标index 0处开始)。用open模拟是这样的open(log.txt,O_CREAT|O_TRUNC|O_WRONLY,0666);用系统调用完成仿照fopen中的w模式打开文件做写入。#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.h#includestring.h#includestdlib.hintmain(){intfdopen(log.txt,O_CREAT|O_TRUNC|O_WRONLY,0666);if(fd0){perror(open);exit(1);}constchar*strHello world.\n;intcnt9;while(cnt--)write(fd,str,strlen(str));close(fd);return0;}a模式文件不存在新建文件不会清空文件历史内容从文件末尾开始写入。用open模拟是这样的open(log.txt,O_CREAT|O_APPEND|O_WRONLY,0666);用系统调用完成仿照fopen中的a模式打开文件做追加写入。#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.h#includestring.h#includestdlib.hintmain(){intfdopen(log.txt,O_CREAT|O_APPEND|O_WRONLY,0666);if(fd0){perror(open);exit(1);}constchar*strHello world.\n;intcnt10;while(cnt--)write(fd,str,strlen(str));close(fd);return0;}r模式就是读取文件的模式可以将读取到的内容输入到指定位置(缓冲区)。用open模拟是这样的open(log.txt,O_RDONLY);用系统调用完成仿照fopen中的r模式打开文件做内容读取。#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.h#includestring.h#includestdlib.hintmain(){// if (argc ! 2)// {// printf(Usage: %s filename\n, argv[0]);// }intfdopen(log.txt,O_RDONLY);if(fd0){perror(open);exit(1);}charinbuffer[100001];// while (1)// {// int n read(fd, inbuffer, sizeof(inbuffer) - 1);// if (n 0)// {// inbuffer[n] 0; // 最后一个位置放 \0// printf(%s, inbuffer);// }// else if (0 n)// {// printf(end of file!\n);// break;// }// else// {// perror(read);// exit(2);// }// }intnread(fd,inbuffer,sizeof(inbuffer)-1);inbuffer[n]0;// 最后一个位置放 \0printf(%s,inbuffer);close(fd);return0;}2、文件描述符(fd)上面学习open等系统调用时我们都见到了文件描述符fd。事实上用户无法直接访问操作系统里的内核数据结构必须通过系统调用。而与文件操作相关的系统调用几乎都需要文件描述符fdopen打开文件成功后返回该文件的文件描述符write需要传入文件描述符参数read需要传入文件描述符参数……那么我们就可以粗浅地下一个定论文件操作的过程中操作系统只认文件描述符fd。还记得吗一些上方的C语言库函数底层其实是封装了系统调用。文件操作相关的C语言库函数封装了文件操作相关的系统调用——函数的封装C函数能够使用的、描述文件的数据结构肯定也要系统能够使用的、描述文件的数据结构——数据结构的封装我们知道系统能够使用的、描述文件的数据结构是文件描述符fd。那么C函数能够使用的、描述文件的数据结构是什么呢FILE#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.hintmain(){FILE*pffopen(log.txt,w);printf(%d\n,pf-_fileno);// FILE封装了fdfclose(pf);intfdopen(log.txt,O_RDONLY);printf(%d\n,fd);close(fd);return0;}实际上FILE是一个结构体其中包含了文件描述符fd这个成员变量。即FILE封装了文件描述符fd。我们将回答下面四个问题来进一步理解文件描述符fd。2.1、为什么从3开始0、1、2哪去了小贴士1对文件进行操作需经过“读、改、写”三大步骤读将文件加载到内存中打开文件读取文件内容改进行对文件内容的修改写按照实际需求刷新文件内容将磁盘上的文件做更新然后关闭文件。一个系统可以同时创建多个进程那么一个进程能否同时打开多个文件答案是可以的。#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.hintmain(){intfdaopen(loga.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);intfdbopen(logb.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);intfdcopen(logc.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);printf(%d\n,fda);printf(%d\n,fdb);printf(%d\n,fdc);close(fda);close(fdb);close(fdc);return0;}打开多文件的同时我们很好奇比如我们同时打开了三个文件loga.txt logb.txt logc.txt那么打开这三个文件后获得的文件描述符分别是多少呢分别是3、4、5那么问题来了文件描述符fd从 3 开始0、1、2 哪去了#includestdio.hintmain(){printf(%d\n,stdin-_fileno);printf(%d\n,stdout-_fileno);printf(%d\n,stderr-_fileno);return0;}还记得吗程序运行(进程创建)的时候默认打开了三个流stdin标准输入流stdout标准输出流stderr标准错误流查阅这三个流//SYNOPSIS#includestdio.hexternFILE*stdin;externFILE*stdout;externFILE*stderr;这三个流本质都是FILE*量那么底层一定也封装有文件描述符fd。2.2、为什么是连续的在 2.1 中我们发现文件描述符 0、1、2 分别对应stdin stdout stderr。又因为进程创建的时候默认打开了这三个流。所以我们可以合理猜测接下来进程打开文件(流)的时候文件描述符就是从 3 开始。我们可以进一步猜测文件描述符也许是数组下标2.3、为什么可以表示文件就像操作系统需要对多个进程进行管理一样操作系统也需要对多个文件进行管理——先描述再组织。所以Linux内部一定有一个数据结构描述文件文件 属性 内容那么文件结构体file里一定会包括属性和内容我们很容易能发现结构体file中也有内嵌的链接字段struct list_head。所以创建出来的多个文件就能以内嵌指针的方式连接在一起文件与文件之间倒是联系起来了。但是进程如何找到文件呢实际上进程PCB中保存了一个数据结构files_struct深入探讨files_struct发现其中还包含一个数组structfiles_struct{// ...structfile*fd_array[NR_OPEN_DEFAULT];// 数组};数组fd_array的类型就是文件结构体file的指针。那么一切就明朗了打开文件后文件结构体file的指针就会存入数组fd_array进程想要打开指定的文件只需拿着下标在数组fd_array中找到对应的file*指针就能找到指定的文件这就是系统调用open进程想要通过read读取文件只需要拿着下标找到对应的文件进而找到文件的内容即可进程想要通过write向文件写入内容只需要拿着下标找到对应的文件进而找到文件的内容对文件内容进行修改即可。进程想要关闭文件只需刷新文件内容到磁盘上释放文件结构体file的空间将fd_array对应下标存储的file*指针置空。到这里我们就可以回答“文件描述符fd为什么可以表示文件”进程通过文件描述符fd访问file*数组对应下标位置上存储的file*指针进而访问到文件结构体file最终访问到文件的内容和属性。2.4、fd的本质有了 2.3 的认识我们就可以回答fd的本质是什么文件描述符fd本质上就是一个数组下标(index)。fd是进程PCB中类型为files_struct的结构体(指针形式存在)存储的fd_array数组的下标。其中我们称struct file *fd_arrar[];为打开文件表。2.5、fd的分配规则对于“fd是数组fd_array的下标”这一结论有人就会有疑问如果关闭了一个文件再新打开一个文件那么这个新打开的文件文件file*指针存放在fd_array的哪个位置找到当前最大的已经存入指针的下标然后直接放到它的下一位找到未存入指针的最小的下标然后放进去我们可以做一个实验#includestdio.h#includeunistd.h#includesys/types.h#includesys/stat.h#includefcntl.hintmain(){close(0);// 关闭1号stdin流intfdopen(log.txt,O_CREAT,0666);printf(log.txt: %d\n,fd);close(fd);return0;}答案是后者。所以文件描述符fd的分配规则是新打开文件统一分配fd_array中未存file*指针的下标最小的fd给该文件。3、将系统调用与C语言进行关联C语言函数对系统调用进行了封装C语言定义的FILE对文件描述符fd进行了封装。那么为什么要“封装”呢直接用系统调用实现方法不就行了吗原因是封装使得语言具有了跨平台性。不同平台的系统调用是不一样的比如Linux的系统调用就和windows的系统调用不一样。C语言为了能够让更多的人使用这些人包含Linux开发者windows开发者其它系统开发者就必须考虑到“不同平台的系统调用不同”这一点。所以C语言对于一种上方的库函数会实现不同系统不同系统调用封装而成的版本比如printf函数会实现Linux系统调用封装成的版本和windows系统调用封装成的版本以及其它系统系统调用封装成的版本。C语言这么做完善了C语言本身的生态使得C语言至今仍是世界上使用最广泛的语言。而一些语言比如汇编语言由于没有做好跨平台性已逐渐不再被更多人使用直至淘汰。