C语言中有着许多对文件操作的函数,包括其他语言也有,但是从语言来了解文件有点浅显计算机的一切都离不开操作系统,那么文件跟操作系统也有着密切的关系,所以我们从系统层面来了解文件(在进程中打开的文件)
文章目录
- 1.系统中被打开文件的认识
- 2.C语言中的文件接口
- a. fopen/fclose
- b.fwrite/fread
- c. 部分演示
- 3. 系统调用接口open/close
- a. open的参数和返回值
- path和返回值
- 标志位
- mode
- b. open的返回值fd再认识
- c. 标准输入输出
- d. 一切皆文件
- 4.对文件结构体认识
- 5. fd的再再认识
- a. 追加重定向
- b. 输入重定向
- c. 问题的引入
1.系统中被打开文件的认识
我们首先要知道文件 = 文件内容 + 文件属性
其次为什么要从系统来了解文件?我们使用编程语言写好的程序也能打开文件对文件的数据进行修改。但是我们要知道,文件刚开始是在磁盘里的,语言写好的程序跑起来之后是一个进程,一个进程要打开文件,他能直接打开吗?很显然是不能的,因为文件没被打开前一般是在磁盘里的,而cpu只跟内存打交道,所以要打开一个文件必须要将它加载到内存中,那么谁来管理磁盘跟内存的交互呢?那肯定是操作系统。所以从语言来了解文件是不够本质的。并且从这里也就把文件分成两类:打开的文件和未打开的文件。而我们研究被打开的文件,也就是内存中的文件。
再想一个问题,一个进程能带开多个文件吗?多个进程能打开多个文件吗?答案是可以的,Linux中万物皆文件,在一个进程跑起来的时候会默认打开三个流:标准输入、标准输出、标准错误。而这三个其中标准输入是键盘,标准输出和标准错误是显示器,而计算机中肯定会有着许多的进程。关于这些详细内容先不细谈,目前只要知道这三个流其实也是文件就ok了,所以一个进程可以打开多个文件,多个进程也可以打开多个文件。进程与打开的文件是1:n的关系。
这么说来,内存中会存在多个被打开的文件,而哪些文件被哪些进程打开了这些都需要被管理,所以文件需要被管理,操作系统管理的方法就是先描述,再组织,所以当一个文件要被打开时,除了要将文件加载到内存中,还要用文件结构体将文件描述好,这个结构体里面有着文件的信息可能有磁盘中的存储位置、文件名等等。然后再用数据结构比如链表将它们管理起来。也说明进程打开文件也是凭借操作系统来打开的,那么操作系统一定要封装系统调用才能让进程打开文件。
2.C语言中的文件接口
在这里我们只认识部分函数
a. fopen/fclose
这两个是成对出现的,而fclose就是关闭一个文件,我们主要看fopen:
它是一个库函数,它的参数第一个是文件的路径(这个路径可以是相对路径也可以是绝对路径),第二个是文件被打开的模式。它的返回值是一个库里封装的FILE的指针,这个指针会指向被打开的文件,如果打开失败会返回空值针。
我们再看看文件可以有哪几种打开方式:
r模式:以只读方式打开文件,并且文件被指向的位置是文件内容的开头
r+模式:以读写方式打开问价,并且文件被指向的位置也是文件内容的开头
w模式:以读写的方式打开文件,在打开的时候会先将文件内容清空,然后指向文件内容的开始
w+模式:以读写的方式打开文件,在打开的时候会先将文件内容清空,然后指向文件内容的开始,当文件不存在的时候会创建文件
a模式:以追加(其实也是写)的方式打开文件,但是不会将文件内容清空,而且指向文件内容的末尾,当文件不存在时创建文件
a+模式:以读和追加的方式打开文件,不会清空文件内容,当读文件的时候从文件的开始读,写的时候从文件的末尾开始写。文件不存在时创建文件
b.fwrite/fread
接下来我将演示打开文件,并对文件进行读写。在此之前我们还要认识一个函数:fread、fwrite
这里它们的第一个参数是指向被写入/读的一段空间,第二个参数是每个元素字节大小,第三个是有多少个元素,第四个是要操作的文件。
c. 部分演示
部分演示:
这里我们使用了fopen并且以w+的方式打开文件,刚开始我们这个目录底下是没有文件log.txt的所以要先创建好文件在向文件中写入上面代码中的要写入的内容。下面也是差不多,不过多解释
我们发现我们上面的三个文件打开方式和之后的读写和Linux的重定向好像差不多。
3. 系统调用接口open/close
a. open的参数和返回值
我们上面知道,进程中打开文件,对文件的操作一定离不开操作系统,那么操作系统一定有着封装好的系统调用,来让进程能够对文件进行操作,而上面的进程是C语言编译好的程序,其中C语言中的文件打开对文件的读写肯定也是封装了系统调用。那么我们来认识两个系统调用open和close:
close不多介绍了就是关闭文件描述符所对应的文件。
首先是open,可以看到他有两个,这种好像是C++中的重载,怎么会在C语言中呢?其实这只是一种宏替换实现的并不是函数重载。
我们来介绍它的参数和返回值:
path和返回值
返回值:该函数打开文件成功后返回对应的文件描述符fd,失败返回-1
pathname:被打开文件的路径
标志位
后面两个参数我们要着重介绍一下,既然C语言中有着文件打开的方式,俺么系统调用里一定也有,这个就是文件打开的方式,但是它跟C语言中的传的方式不一样,这里使用了位操作的方式传,我们用代码来展示一下位操作传的标志问参数如何使用:
这就是标志位的使用,使用一个比特位就能描述一种状态。
这样我们就能介绍这个open中的flag参数怎么使用了,而手册里也告诉我们几种不同的打开方式,这里我们只介绍部分:
O_CREAT:文件不存在的话创建文件。
O_RDONLY:以读方式打开文件。
O_WRONLY:以写的方式打开文件。
O_TRUNC:打卡文件的时候会清空文件内容,并指向文件的开头。
O_APPEND:打开文件会指向文件的末尾。
我们看到这些参数也是全部以横杠+大写的方式命名,说明他们也只是一群宏常数罢了。
mode
接下来是第三个参数,mode:
我们打开一个文件log.txt。不在的时候创建它,在这里我们呢什么都没干,就把他打开后关闭了。
当我们看到如上结果的时候,发现文件的权限里怎么会出现S,并且文件是红的,这是因为文件的权限控制其实是乱码,而open中的mode就是控制这一权限的(被进程中创建的文件)。
这样就可以了,但是我们又发现,被进程创建好的文件设定的权限和实际的权限不一样,这是因为系统中有着掩码:
我们也可以在代码中设置进程中进程的掩码:
但是不推荐这么做
b. open的返回值fd再认识
上面说到,open打开一个文件之后,会返回一个与之对应的文件标识符fd,文件标识符是什么?在C语言中使用fopen打开一个文件之后会返回一个指向被打开文件的FILE的一个指针,它与fd又有什么关系?接下来我们来一一解惑,既然涉及到了文件和进程那就又得从系统开始了解:
我们目前知道被打开的文件会被描述成文件结构体然后用数据结构组织起来(假设链表),那么在一个进程中,可能会需要打开多个文件,那进程和被打开文件又是如何联系起来的呢?其实它们是有着如下图的关系:
这其中文件如何加载到内存中先不说。
进程pcb中会有一个指针,这个指针指向一个指针数组,这个数组里面存储着被打开文件结构体的指针,这个数组也叫进程文件描述符表,那么这一张表让进程和文件而实现了解耦,就跟进程和内存的页表一样。我们现在来打开一下文件:
我们发现,文件标识符是一串连续的整数,这里直接提出结论:文件标识符就是进程文件标识符表的下标,操作系统访问文件就是根据文件标识符来访问
那么C语言中的FILE其实也肯定封装了fd,稍后我们可以进一步验证。
现在我们发现上面的数字只有3、4、5、6。0、1、2呢?我们知道进程在运行的时候会默认打开三个流:标准输入、标准输出、标准错误,那么结果显而易见,012就是这三个。代码说话:
那么这张图也说明了012对应的那三个标准输入输出流,FILE结构体里也封装了fd。
那么问题就来了:
为什么要默认打开这三个流?
文件描述符对应着着硬件,一切皆文件到底怎么理解?
c. 标准输入输出
首先来说明第一个问题:
默认打开这三个流,是为了让程序员能够默认进行输入输出的代码的编写(printf,scanf)。
d. 一切皆文件
第二个问题:
我们在使用open,fopen打开文件对文件进行各种操作,然后操作系统定时刷新文件缓存区,将操作得到的文件覆盖到磁盘中的文件中,说白了这些操作不还是和磁盘之间的交互吗?磁盘具有读写的功能,磁盘也是硬件,操作系统会和键盘交互,和网卡、麦克风、屏幕交互。那对于键盘来说该硬件只有读的功能,而屏幕只有写的功能。网卡可以读写,麦克风也只能读。这些功能肯定是在各自驱动中,有着对应的函数。而我们操作系统中的文件结构体中也肯定有着读写的函数指针,创建好一个文件对象的时候,我们让这个文件结构体的读写方法指向硬件的读写方法上:
每个硬件的读写的实现的方式肯定是不一样的,但是我们都是通过文件结构体对象来统一调用read和write方法来实现对硬件的读写,这种情况下我们把文件结构体看成父类,下面的键盘、屏幕等等看作子类,这不就是多态吗?而这一中调用的方式也叫做虚拟文件操作系统。所以对Linux来说一切皆文件。
4.对文件结构体认识
我们现在要知道当我们在进程中打开一个文件的时候,是怎么打开的:
我们打开一个文件,肯定是要对它进行各种操作,而文件无非就是文件内容+文件属性。所以文件结构体中应该会有文件的属性,以及对文件操作的各种方法集,操作文件不会在磁盘中操作,cpu不跟外设打交道,所以需要将文件加载到内存中,但是文件的加载又不是全部加载进来,而是,在创建好文件结构体的时候内存中还会构建一片文件缓冲区,文件的数据会加载到这里面。而这一系列操作都是由操作系统完成的。
那么我们使用系统调用打开文件后,返回系统标识符,用户根据这个文件标识符,对文件进行操作,而操作系统会到进程pcb中找到标识符指针数组的指针来找到那个数组,然后通过标识符下标来找到那个文件,再执行用户的操作。
那操作系统是如何对文件实现读写操作的呢?
首先是读:读肯定是读文件缓冲区的内容,当缓冲区中没有内容的的时候就会触发缺页中断,再让磁盘向缓冲区中加载(拷贝)。
写:我们在增加文件数据的时候,无非是开始增加,或者追加增加,这些都不需要加载文件到缓冲区,但是删除文件数据呢?这时候就需要将文件加载到缓冲区中了。而对文件内容的增加或删除在文件缓冲区中执行,然后由操作系统定时刷新文件缓冲区内容到磁盘中(也是一个拷贝的过程)。
那至此,我们得到,不论是文件的读还是写,都需要将文件加载到文件缓冲区中,并且用户对文件的读写对操作系统来说,也只是文件缓冲区的来回拷贝而已。
5. fd的再再认识
我们上面知道,进程创建好之后,会默认打开三个流,那假如我们关闭其中一个,再打开一个文件会怎么样呢?
关闭2呢?
不关闭
那我们就得出了fd的分配规则:寻找最小的,没有被使用的数据的位置,分配给指定的打开的文件
再认识两个个函数write/read:
返回值先不做介绍,使用起来很简单。
我们先对write和read和标准输入输出流进行简单的使用
需要注意的是这里我们没有将斜杠零算上去,那是因为“字符串结束的标志是斜杠零”这个是只在C语言中,对文件不适用。之后再验证。
我们再对上面进行一些小修改呢?
我们发现两个现象,一个是我们关闭1号文件描述之后对应的文件之后,再次打开一个文件,向1号文件写入时,写到了log.txt里。
第二个是当我们向1号文件里写入字符串时,字符串的斜杠零也被计算入内,但是log.txt中出现了乱码。
要是不计算呢?
就不会。
我们再看一个东西:
我们刚才的行为不就跟重定向一样吗?
a. 追加重定向
b. 输入重定向
重定向的本质其实就是修改文件描述符表的下标内容。
我们上面是直接关闭了01两个流,有没有方法是不通过文件描述符分配规则关闭进行修改呢?
这里就有了dup2函数:
简而言之就是新的newfd就是最后保留下来的文件下标。
这个函数能实现文件描述符表级别的拷贝。
c. 问题的引入
经过上面的了解我们再来一段这样的代码:
关于这个问题,我们之后再说明,并且再次详细阐述重定向的问题以及文件机结构体部分具体成员的认识。