常识:
1 文件包括属性和内容
2 文件有打开和未打开文件,
3 本文先讨论谁打开的文件,以及如何管理已经打开的文件
一 回忆c接口
1 fopen
我们在test.c里面用一下fopen函数,不存在打开的文件会默认创建,那为什么默认新建在当前目录下,是因为cwd,而不是PWD,我们知道PWD是环境变量,如果我们去到其它工作目录,PWD会变,但是CWD不变,此时再运行一下test.c,此时文件还是会创建在CWD存的路径下,而不随着环境变量改变而变化。
2 fwrite
fwrite函数可以往文件写数据,值得一提的是fwrite的size参数,这个是要写入的字节数,我们大部分时候都是往文件写个字符串,例如"hello linux",有时候size传strlen("hello linux"),有时候传sizeof(hello linux),我们会发现sizeof多写入的\0被文件识别为乱码,这说明一个问题,字符串结尾有\0这个字符是c语言的规定,文件是不认这个\0是字符的。
二 文件操作和系统调用
fwrite库函数一定封装了系统调用,因为文件是在磁盘上的,fwirte要往硬件写数据,那不就相当于访问硬件的资源,由于操作系统不相信用户,所以fwrite一定不是直接把数据弄给硬件,而是通过操作系统的接口将数据传给硬件。接下里就来认识认识几个系统调用。
1 认识open
从fopen的名字上看,我们也知道fopen封装的是open,接下来就看看open的参数和返回值。man 2 open就可从手册调出open的信息。
显然,函数1是函数2的的子集合,我们只要说清楚了函数2,函数1的使用也就明白了。
参数1 文件名,不带路径应该是默认在当前路径下找。
参数2 flags是什么呢?
我们要给flags传的是上面图片中的宏,这些宏都表示一个一个的整数,接下来就介绍介绍这些宏的意义,以及如何使用。O_RDONLY表示open以只读方式打开,O_WRONLY表示open以写方式打开, 而O_RDWR则表示以读写方式打开,这三个宏最好只出现一个,至于其它的宏O_APPEND,这个是表示向文件写时以追加的方式去写。
使用:
| :按位或?这个使用又是啥意思呢?
举个例子,O_RDONLY可能是用0001来表示,O_WRONLY则是0010,O_RDWR则是0100来表示,同理得,O_APPEND就要用1000来表示,只用了一个比特位就能唯一表示一个宏,这样的设计非常巧妙,首先我们可能会传多个宏,设计者没有用可模板参数来接收,而是只用一个整型,因为我们可以对传的参数进行按位或,这样只要对按位或的结果一分析,就知道你打开文件是要读还是写了,所以flag的类型就只是个朴素的int,可其实里面门道也不少。
参数3 权限初始化,因为我们open发现打开文件不存在,要创建文件,此时文件的权限是要指定的,不然会给一个初始值,但这个初始值如下图。
2 认识write和read
write和read就简单多了。
write:往fd这个文件描述符对应的文件写入buf数组中的元素,字节数为count。
read:从fd这个文件描述符对应的文件读取count个字节的数据,写入buf数组中。
现在我们就能解释:现在我们就可以解释为什么fwrite就传一个"w"可以实现清空写,以及创建文件,"w+"为什么能实现追加写,就是因为在底层封装了这些宏,然后操作系统识别到了,在调用系统调用的时候传了给flag传了不同的宏,至于返回值会在下面访问文件的本质中提及。
三 访问文件的本质
open的返回值-文件描述符,这个文件描述符怎么是int类型呢,我fopen用的可是FILE*,这两者有什么关系吗?
先来看看操作系统如何管理文件,首先操作系统打开的文件有很多,这些必然会被操作系统管理,操作系统管理文件,就像管理进程一样,只要用一个file结构体描述文件即可,根本就不需要管文件内容,这样在系统内核处,就又增加了一个数据结构,将所有的file结构体管理起来。诶,不对啊,文件不是进程打开的吗,那不是应该进程管理吗,如果仅仅是被进程管理,那如果进程出异常了,被kill了,这些文件不就丢失了?所以系统必须也要管理。如下图:
好吧,既然上面是系统管理文件的方式,那进程呢怎么管理呢?
所以会有一个files_struct(这个和FILE*可不相同)来管理,这个结构体内部会有一个数组,数组每元素就是一个文件指针,而文件描述符就是数组下标,所以说一个文件描述符一定对应一个文件,当然多个文件描述符可以对应同一个文件(file结构体内部肯定是会有引用计数记录的)。也就是说底层进程是通过下标来找文件指针,从而找到文件的,所以FILE*内部一定封装了文件描述符,不然系统调用找不到文件。
我们发现所有语言写的代码运行起来都要默认打开三个文件,stdin,stdout,stderror,因为系统就要这样做,系统设计这就认为开机后天然需要键盘,显示器文件,所以就要打开,而所有语言写的代码不管写了啥,形成进程后,就会把已经打开的文件填到files_struct内,所以程序一运行,该数组内就有了三个元素。
既然stderror和stdout都是指向显示器文件,它们的区别是什么,我想也就是其内部的封装的文件描述符不同,当我们close(1),printf就用不了了,但是perror还可以向显示器打印。
四 重定向
周边小知识:write写的时候如果不close,一直写,那就会一直往后写,而不是覆盖,open以追加方式写,指的是第一次write写的时候从哪开始写。
1 文件描述符的分配规则
自数组开头遍历,在数组中最先遇到的空格位置的下标,就是被分配的文件描述符。先前已经说了系统会打开两个文件(说打开三个是方便理解),这两个文件是键盘,显示器,而显示器被打开了两次,在files_struct内的数组就会有三个文件描述符,其中0存的是键盘文件指针,1,2存的都是显示器文件指针,我前面说多个文件描述符可以对应同一个文件,这就是实实在在的例子。
2 手动实现重定向
输出重定向就是:printf本来是要写给显示器的,但是由于close(1),然后又打开了myfile.txt后占用了一号位(这就验证了文件描述符的分配规则)。
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 #include<string.h>5 #include <sys/types.h>6 #include <sys/stat.h>7 #include <fcntl.h>8 int main()9 {10 printf("我的id:%d\n",getpid());11 close(1);
W> 12 int fd = open("myfile.txt", O_CREAT|O_WRONLY,0666);13 printf("我的id:%d\n",getpid()); 14 return 0;15 }~
所以两句printf就只有一句输出,因为第二句printf就变成往myfile.txt输出了。
这也侧面说明在操作系统看来,往显示器写和普通的文件没有区别,而printf内部用的stdout一定是封装了1号文件描述符,这是编码定死的,printf也不管这个标识符对应的文件变了没,拿到就写,就有了输出重定向这种乌龙。
3 系统调用dup2
虽然可以先close,再打开文件实现重定向,但这样的代码还是不如直接调用系统调用那么优雅,直接上代码,看看使用。结果一致。
问题1 oldfd和newfd谁覆盖谁,显然从先前的例子来看,是oldfd上的内容覆盖到newfd的内容,本质是数组对应下标上元素,也就是文件指针的拷贝。
读者可以尝试进行输入重定向
同理也就是对0号位置下标做手脚