【Linux】基础IO与文件描述符
【内容引入】
文件大小0,在磁盘上要不要占空间?——要
文件 = 内容 + 属性
1.理解文件
1-1 狭义理解
- 文件在磁盘里
- 磁盘是永久性的存储介质,因此文件在磁盘上的存储是永久的
- 磁盘是外设(既是输出设备也是输入设备)
- 磁盘上的文件,本质是对文件的所有操作,都是对外设的输入和输出(简称IO)
1-2 广义理解
- Linux下一切皆文件(键盘、显示器、网卡、磁盘......这些都是抽象化的过程)
1-3 文件操作的归类认知
- 对于0kb的空文件是占用磁盘空间的
- 文件是文件属性(元属性)和文件内容的集合(文件 = 属性(元数据)+ 内容)
- 所有的文件操作本质是文件内容操作和文件属性操作
1-4 系统角度
- 对文件的操作本质是进程对文件的操作
- 磁盘的管理者是操作系统
- 文件的读写本质不是通过C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的
访问文件,需要先打开文件。那么是谁打开文件?是进程打开了文件,对文件的操作,本质是进程对文件的操作。操作系统内存在很多打开的文件,操作系统也需要把被打开的文件管理起来,先描述,再组织。
文件分为这么几种:
1.“内存级(被打开)”文件
2.磁盘级文件
2.回顾C接口(部分)
2-1 stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是stdin,stdout,stderr
- 仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,文件指针
- stdin,标准输入,键盘文件
- stdout,标准输出,显示器文件
- stderr,标准错误,显示器文件
为什么C要默认打开它们三个呢?因为程序是做数据处理的,得要有一种默认的方式获得数据,也要有一种默认方式返回结果。
2-2 打开文件的方式
1.以写(w)方式打开
在打开文件时,文件默认会先被清空。这就解释了为什么每次输出重定向时文件每次都会被清空再重新写入新的内容。因为要往里面写入,就要先打开,一打开就会被清空。
2.以追加(a)方式打开
追加重定向的方式,在往文件中写入时不清空原本的内容。
3.系统文件IO
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:
3-1 open函数打开方式
open函数具体使用哪个和具体运用场景相关,如果目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则使用两个参数的open。
这个flag是标记位,其实就是宏,他是一个32位标记位。
使用open:这里想使用open打开log.txt,如果不存在就创建,并且以读写的方式去打开
运行结果我们可以看到文件权限这里是乱的,因为如果你要新建文件的话,必须把权限带上
我们加上权限值:
重新运行一下就正确了。但是我的权限值给的是666,为什么这里是664?因为受到了权限掩码umask初始值002的影响。但是这个也是可以更改的。
我们在代码中调用umask函数把值设为了0
再重新运行一下,此时就并不受系统权限掩码影响了。
要关闭文件,就可以使用close函数,close(fd)。
如果要对文件进行写操作,可以调用write函数。它的第一个参数fd是文件描述符,第二个是buffer,第三个参数表示写多少个字符。
要注意,在写入时,不用写入\0,\0是C语言的东西,往系统里写会乱码。
如果在这基础上把代码修改成const char *msg = “abcd\n”,并只让它往文件中写入一次,我们会发现,文件内容变成了这样:
文件清空并不是必然的,你在创建时要告诉系统让系统清空,否则系统是不会默认清空的
如果我们要打开文件并且把文件内容清空,要是使用系统级函数,我们需要传递如上的几个标志位。
以追加方式打开:(也即是上面语言层的a打开方式)
清空并打开:(也即是上面语言层w打开方式)
write接口:
第二个参数并不关心你写入的方式和类型。而所谓的文本写入还是二进制写入只是语言层的概念。
read接口:类比C接口。
理解完接口,再来理解一下这个fd:
为什么会是3、4、5、6?那么0、1、2去哪里了?答案是:0、1、2代表其标准输入,标准输出和标准错误,这叫做默认的文件流,默认已经把这三个打开了。那么之前C语言的FILE*是什么呢?他是一个C语言提供的结构体,后面再讨论。在操作系统层面,只认fd,即文件描述符,但是我们可以大胆猜测一下,FILE中一定封装了文件fd。
【问题1】为什么各个语言都要做这种语言级别的封装呢?
把平台级别的差异封装在库里,使语言具有可移植性!语言为什么要增加自己的可移植性?让更多人使用,增加市场占有率。
【问题2】那么C、C++、Java、PHP、Go中的文件操作一致吗?类似吗?
任何语言,在顶层的语言的封装不一致,但在底层都是一致的,只认fd操作符。
上面的部分都是往上层说,往下层说,fd是什么呢?fd的值是从0开始的整数,见到这样的整数,我们就要敏感起来了,这不是数组下标吗?操作系统内有那么多文件,打开了那么多文件,操作系统也会把他们管理起来,先描述,再组织!
操作系统在打开一个文件时,会创建一个struct file结构体,把struct file用链表连接起来,对被打开的文件的管理就是对链表的增删查改。
这么多打开的文件,哪个文件是某个进程所打开的呢?文件描述符表会包含一个数组,struct file * fd_array[],在进程PCB里面会存在一个struct files_struct *files就可以指向刚刚所说的数组了。再将前面所说的结构体填充到数组中,这样就将被打开的文件和进程建立了映射关系了。
以read来举例:
read(fd,buffer,sizeof(buffer));
传入fd让操作系统去指针数组里查找对应下标的文件的结构体,找到之后将文件加载到这个结构体的缓冲区中,最后再将缓冲区里的内容拷贝到用户缓冲区里面。
所以read函数本质上是内核到用户空间的拷贝函数。
增删都很简单,那么如果我要修改文件呢?要先把磁盘中的文件加载到缓冲区,然后在内存中进行修改,修改后再写入磁盘!
通过上面两个举例,可以得出一个结论,对文件内容进行任何操作,都必须先把文件加载(磁盘到内存的拷贝)到内核对应的文件缓冲区内!
3-2 重定向原理
【补充】文件描述符的分配原则:最小的,没有被使用的,作为新的fd给用户。
为什么close(1)之后没有打印出fd值,但是在cat log.txt时发现本应该打印到显示器文件的值被写入了log文件中?因为1是显示器文件,把显示器文件关掉了所以没有显示出来,但是中途又查看打开了log文件,所以写到了log里面。这个现象就叫做重定向。
原理:更改文件描述符表的指针指向。数组下标不变。要完成重定向的操作时,先把显示器文件关掉,再根据文件描述符的分配原则,那么新建的log.txt文件就会被分配到1号,而printf默认就是将其打印到1号文件,这样就完成了一个文件的重定向操作。
但是这样写也太奇怪了,先关闭了又打开,有没有哪个系统调用接口可以实现这样的类似的功能的?有的兄弟,有的。请看下面的dup2:
#include <unistd.h> int dup2(int oldfd, int newfd);
功能说明:
dup2 会创建一个新的文件描述符 newfd,使其与 oldfd 指向同一个文件、管道或网络连接等资源。
具体行为如下:
1. 如果 newfd 已经指向某个打开的文件,会先关闭它
2. 然后让 newfd 成为 oldfd 的副本,即两者共享同一个文件表项
3. 成功时返回 newfd,失败时返回 -1 并设置 errno
所以:
重定向:打开文件的方式 + dup2