hello~ 很高兴见到大家! 这次带来的是Linux系统中关于基础 IO这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
- 一、认识文件
-
-
-
- [1. 文件是什么](#1. 文件是什么)
- [2. 如何访问文件](#2. 如何访问文件)
- [3. 打开文件的本质](#3. 打开文件的本质)
- [4. 根据所在位置可以分为两种文件](#4. 根据所在位置可以分为两种文件)
- [5. 管理文件](#5. 管理文件)
- [6. 打开文件的方式](#6. 打开文件的方式)
-
-
- 二、从系统角度重识文件
-
- [1. 认识open接口](#1. 认识open接口)
- [1.2 认识文件描述符fd](#1.2 认识文件描述符fd)
- [1.3 为什么语言要对系统调用进行封装?](#1.3 为什么语言要对系统调用进行封装?)
一、认识文件
1. 文件是什么
- 我们知道:文件 = 文件内容 + 文件属性。如果我们创建一个新的文件,里面什么内容都不写,但它还是占用一定的存储空间的,因为一旦创建了一个文件,它的属性就需要被保存起来,这是需要消耗存储空间的。
- 未来对文件操作,要么修改文件内容,要么文件属性。文件属性比如文件权限。
2. 如何访问文件

- 如果要访问一个文件,一定要知道这个文件它在什么地方,所以c语言里我们在用fopen函数打开文件的时候需要传递路径也就是fopen的第一个参数。那么有没有想过,是由谁打开的文件?--由当前的进程打开,一个进程可以打开多个文件。但是有的时候我们不需要传递路径只传递文件名称也能够打开文件,这是因为cwd(进程当前工作路径)的存在。如果不传递路径,OS会在进程的cwd工作下面找。
- 谁找文件?OS。谁打开文件?当前进程。怎么找?根据路径或进程cwd。
- 为什么是由进程打开的?因为一串fopen代码编译好之后,是没有打开文件的,而只有运行起来之后这个文件才会被打开。

- /proc里面存储的是一个个的进程目录,用 ls /proc/13721(test的pid)列出这个 test 进程的属性,可以看到有一个属性叫cwd,它存储着进程当前的工作路径,test就运行在这个路径下面。
3. 打开文件的本质
- 文件本来是存储在磁盘里面的,一个进程要打开文件,那么就一定要把文件加载进内存里面。文件的本质也就是内容+属性,它会根据进程的需要懒加载进内存里面。进程对文件操作,本质就是通过cpu访问内存中的文件。
4. 根据所在位置可以分为两种文件
- Linux系统里面就也存在着大量的文件,毕竟Linux系统里面一切皆文件,如果当前我们不使用ls这个可执行文件,那么ls这个文件就不会加载内存里面。也就是说,对于Linux,文件从位置上大致可以分为被打开加载进内存的文件和没有被打开依然储存在磁盘里面的文件。现在只研究在内存里也就是被进程打开的文件。
5. 管理文件
- Linux系统里面可以同时存在多个进程,而一个进程又可以打开多个文件,所以Linux系统里面可以存在多个文件。文件这么多,要不要进行管理,那当然是要的。怎么管理?先描述再组织。跟PCB一样,把文件的一些需要管理属性塞进结构体里面,然后构建特殊的链表进行管理。
6. 打开文件的方式

- r:只读模式。它会从文件的开头进行读取。
- r+:可读可写。 它从文件开头直接写入,覆盖原来的内容。
- w:只写模式。它会从文件的开头进行写入。如果文件不存在,就创建这个文件,如果这个文件存在了,就清空这个文件的内容再从文件开头进行写入。
- w+:可读可写。与r+不同的是,如果这个文件不存在,就创建这个文件,存在了就清空这个文件的内容然后再从文件开头进行写入。
- a:追加写入。它会从文件的结尾进行写入,保留文件原来内容。如果文件不存在,就创建这个文件。
- a+:追加可读可写。它从文件的结尾进行写入,保留文件原来内容。如果文件不存在,就创建这个文件。读取需手动调整指针到开头,写入自动回末尾。
- 这6个模式一般用到的就是那三个基本模式r、w和a。读写模式一般不会用到。
二、从系统角度重识文件
1. 认识open接口

- open是系统提供的打开文件的接口,fopen是c语言里面对这个系统接口的封装。使用它们的时候要包含3个头文件如图。
- 第一个参数:pathname,文件的路径,这个就不多说了。
- 第二个参数:flags,这个对应fopen里的模式。对应有O_RDONLY----只读打开、O_WRONLY----只写打开、O_RDWR----可读可写打开、O_CREAT----文件不存在则创建、O_TRUNC----文件存在则清空内容和O_APPEND----追加写入这些宏。可以看到,flags是一个int的类型的整数,奇怪吗?为什么是一个整数,它是如何做到整合这些功能的?接下来看一段代码可以更好的理解。
cpp
6 #define ONE (1<<0) //0000 0001
7 #define TWO (1<<1) //0000 0010
8 #define THREE (1<<2)//0000 0100
9 #define FOUR (1<<3)//0000 1000
10
11 void Print(int flag)
12 {
13 if (flag & ONE)
14 printf("one\n");
15 if (flag & TWO)
16 printf("two\n");
17 if (flag & THREE)
18 printf("three\n");
19 if (flag & FOUR)
20 printf("four\n");
21 printf("\n");
22 }
23
24 int main()
25 {
26 Print(ONE|TWO);
27 Print(ONE|THREE);
28 Print(ONE|TWO|THREE|FOUR);
29 return 0;
30 }

- 看这段代码,我定义了4个宏,这四个宏从ONE到FOUR分别是1即0000 0001左移0位、1位、2位和3位之后得到的数。然后我定义了一个Print函数,这个Print函数会通过&(按位与)来识别flag这个数里面与ONE/TWO/THREE/FOUR对应的二进制数位里面有没有1。有1的话就打印出来。用的时候各个宏之间用|组合起来形成最终要传递的flag整数。
- 而open函数它的这个flag使用的逻辑也是这样的,先用|组合起来,然后用&进行识别。----用单个整数传递多个布尔状态。
cpp
8 int fp = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
9 printf("fp:%d\n", fp);

- 打开已经存在的文件使用两个参数的open函数就足够了,但如果要打开一个不存在的文件即需要创建它时需要对应的权限也就是flag要有O_CREAT,然后还要传递所创建文件的对应三种用户权限也就是第三个参数,mode。

- open函数的返回值:它是一个整数,即文件描述符,现在可以理解为是相对于打开此文件的进程专属文件编号,每一个文件加载到内存里面的文件对于当前进程都有一个编号,打开失败返回值会是-1。可以看到log.txt对应的文件编号是3。至于为什么会是3以及这个文件描述符究竟是什么东西,之后会讲到。

- 如果我们要用系统提供的接口close函数关闭打开的文件,它要求传递的参数就是文件描述符。使用系统提供的文件接口免不了要使用这个文件描述符。
1.2 认识文件描述符fd
- 之前通过open接口讲到,文件描述符就是一个整数。但这个整数为什么为从3开始?这是因为一个进程一旦跑起来就默认会打开三个文件,分别是标准输入流文件----对应键盘,标准输出流和标准错误流文件----对应显示器,它们的文件描述符分别是0,1,2。

-
而且也能够看到,stdin、stdout和stderr它们的类型都是文件指针,文件指针类型其实也就是对fd的一种封装,即FILE是一种结构体,它里面有存储fd也就是每个文件对应的文件描述符。FILE还存储了缓冲区等属性。
-
我们可以通过以下代码进行验证,使用了read和write这两个系统提供的接口:


cpp
10 char buf[1024];
11 read(0, buf, sizeof(buf));
12 write(1, buf, strlen(buf));

- 第一个hello world\n是我从键盘里面输入的一串字符,这是read函数从0文件也就是键盘文件里面读取数据到buf数组里面。第二个hello world\n是write函数将buf里面的数据写入到1文件也就是显示器文件里面,所以我们才能看到这个hello world。

- 在前面讲到Linux系统里面存在多个文件。那么OS一定要对这些文件做管理,先描述再组织,每个文件的属性都会被收集然后储存再struct file结构体里面再形成特殊的链表,方便OS进行管理。
- 而每一个进程都能打开一系列的文件,OS最好进行分类管理,也就是文件要跟打开它的进程所对应上。如图,每一个进程task_struct里面都存有一张用来管理当前进程打开的文件的表file,这张表是struct file*类型的一个指针数组,里面的元素是一个个的struct file类型指针,分别指向对应的加载进内存的文件。既然是数组,那么就一定有对应的下标,而这些下标也就是fd,即文件描述符的本质是数组下标。
- 再来讲一讲这个缓冲区,如果我们想读取一个文件里面的内容,文件会把这部分内容放进缓冲区,然后等时机一到刷新到内存里文件内容的对应位置,同样,要修改磁盘里面文件的内容,也是要先把修改后的内容放进缓冲区,等待被刷新进入磁盘文件。---目前可以这样理解。
1.3 为什么语言要对系统调用进行封装?
- 拿c语言举例,系统提供的fd,c语言封装为FILE;open封装为fopen,close封装为fclose,write封装为fwrite。为什么c语言要这样做?----为了有更好的跨平台性。
- 因为系统不仅有Linux系统,也有windows系统,macOS系统,不同的系统会提供不同的系统调用接口,而C语言为了让同一份代码能在不同系统上运行,就通过标准库封装这些系统专属的调用,对外暴露统一的接口。也就是像fopen这样的函数,它其实封装了多种系统下的打开文件系统调用函数,对于不同的系统它调用的实际是不同的系统调用接口,比如Linux就是open,但对外都是一样的接口fopen。
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!