Linux基础IO

Linux基础IO

  • 回顾C语言的文件操作方案
    • stdin、stdout、stderr
  • 系统IO方案
    • 系统方案的文件操作与C语言文件操作的关系?
  • open的返回值---文件描述符
  • 重定向的原理

回顾C语言的文件操作方案

C语言的文件操作有fopen/fclose、fread/fwrite等文件操作函数!
具体用法:
比如,现在我想想一个文件里面写入一点东西:
我们首先要做的就是fopen以“w”的形式打开文件,fwrite进行数据写入,最后fclose关闭文件:
测试代码:
Linux基础IO_第1张图片
如果现在我们想读取bite.txt文件里面的内容呢?
第一步还是fopen打开文件,第二步fread读取文件数据,第三步fclose关闭文件!
具体操作如下:
Linux基础IO_第2张图片

stdin、stdout、stderr

  1. 对于C语言来说,会默认打开3个流:stdin(标准输入流)=>(键盘)、stdout(标准输出流)=>(显示器)、stderr(标准错误流)=>(显示器);
  2. 通过查阅资料,我们可以发现stdin、stdout、stderr都是FILE*文件类型:
    Linux基础IO_第3张图片

系统IO方案

上面的fopen/fclose、fwrite/fread都是C语言对于文件进行操作的办法;
那么站在OS的角度,OS是如何进行文件操作的呢?
系统给我们提供了4个常见接口:open/close、write/read;
在具体使用这些接口之前,我们先来认识一下这些接口:

int **open**(const char *pathname, int flags);
int **open**(const char *pathname, int flags, mode_t mode)
;//
open函数有两种,我们通常在利用open以写的方式打开文件的时候,会用第二个open函数,在使用只读打开文件的时候通常会用第一张open函数;
参数介绍:
pathname:要打开的文件的名字;
flag:以什么样的方式打开文件;
该参数不能当作一个普通的int类型来看待,而是要将其当作一个32位的位图来看待,这32个位图中的每一位都表示一种方法,这么说比较抽像,我们画个图来解释:
Linux基础IO_第4张图片
我们在来举个实际的例子就行了:
Linux基础IO_第5张图片
运行结果:
Linux基础IO_第6张图片
比较常用的flag:
O_CREAT:文件不存在就创建;
O_WRONLY:只写文件;
O_RDONLY:只读文件;
O_TRUNC:清空文件;
O_APPEND:追加;
这些字符都是#define定义的宏,在比特位上都是不重复的,如果想要同时使用多个flag的话,我们可以将这些宏以“|”(按位或)连接起来,比如:O_CREAT|O_WRONLY|O_TRUNC//表示以只读的方式打开文件,如果文件不存在的话就创建,成功打开文件过后,清空文件内容;
mode:创建的文件的权限(这个权限是有我们给的,受umask码限制)
返回值介绍:
如果成功打开文件,则返回文件描述符(具体什么是文件描述符,后面会细谈);
如果失败打开文件,则返回-1;
ssize_t write(int fd, const void *buf, size_t count)
参数介绍:
fd:文件描述符(相当于告诉write要操作那个文件);
buf:待写入文件的数组;
count:需要写入文件总元素字节数;
返回值介绍: 返回实际写入文件的字节个数,因为有可能文件空间不足了,无法写入count个字节元素,可能实际写入的元素少于count;
ssize_t read(int fd, void *buf, size_t count)
参数介绍:
fd:文件描述符,告诉read需要操作的文件;
buf:讲读取到的数据存入buf数组;
count:预期读取的总字节数;
返回值:
返回实际读取到的字节数,可能小于预期,比如文件里面并没有count个字节;
具体应用:写入文件:
int close(int fd);
参数:
fd:需要关闭的文件的文件描述符;
返回值:
关闭成功返回0;
关闭失败返回-1;
Linux基础IO_第7张图片
读取文件:
Linux基础IO_第8张图片

系统方案的文件操作与C语言文件操作的关系?

也就是说fopen/fclose、fwrite/fread与open/close、write/read的关系?
C语言的文件操作是封装了系统调用的!与系统的文件操作属于上下级关系;
C文件操作在上,系统的文件操作在下:
Linux基础IO_第9张图片
站在系统调用的角度,我们可以看到所有语言的文夹操作都是封装的系统调用,底层实现都是差不多的!只不过对系统调用的封装形式不同,上层呈现给用户的就是不同形式的文件操作方案,但是实际站在系统调用的角度,我们就能用一个同一的视角,来看待各种语言的文件操作!

open的返回值—文件描述符

想要理解文件描述符呢,就得先理解open系统调用的工作机制,open是代表着OS的,也就是说我们需要理解OS是如何做到!
因此在真正理解文件描述符之前,我们需要了解一些预备知识:

1、文件在未被打开之前,存储在哪里?
答:磁盘!
2、当我们对文件需要对文件进行操作时,文件需要在哪里?
答:内存!为什么?因为冯诺依曼体系决定的!也就是说我们打开文件的本质就是将文件加载进内存!
3、当我们需要对文件进行操作时,文件会被加载进内存,同时文件=内容+属性,那么加载进内存的是文件属性还是文件内容?
答:至少得有文件属性!
4、当我们对文件进行操作的时候,文件需要被提前加载进内存,是只有我一个进程需要打开文件吗?
答:当然不是,一个进程可以打开多个文件,没个进程也都可以打开多个文件;
5、既然每个进程都可以打开多个文件,那么在内存中就会存在大佬量的被打开的文件,那么作为计算机内部的管理者–OS要不要把这些被打开的文件管理起来呢?
答:要的!OS如何管理起这些被打开的文件?先描述,在组织!:首先OS会拿一个struct file的结构体来存储这些被打开的文件的信息(与OS拿struct task_struct结构体来描述进程一样),然后呢,再利用某种数据结构将这大量是struct file结构体组织起来,比如使用链表!那么最后OS对于被打开的文件的管理是不是就转变成对于链表的增删查改!
那么struct file里面都存储的一些什么信息呢?
至少得有对应文件的属性吧!
其次呢,还有一些各个struct file结构体之间的链接关系(指针),以及该文件对应的缓冲区和对于该struct file对应文件的读写方法(C语言结构体中是不允许存在成员函数的,但是允许存储函数指针啊!因此每个struct file结构体里面都存储得有该文件对于的读写函数的函数指针!)
Linux基础IO_第10张图片

有了前面的预备知识,我们现在就可以来谈谈文件描述符了;
解释文件描述符,也就是解释open的工作机制!
首先,我们的进程调用open系统调用接口打开文件(为了描述方便,我们就给文件一个名字:log.txt),按照前面的预备知识,log.txt文件就会被open系统调用加载进内内存,加载进内存过后,OS要管理它吧,所以OS用了一个struct file的结构体来描述log.txt文件,OS在做完这一切过后,是不是就完成了对于log.txt文件的管理?是的,可是还没有结束!虽说是OS(也可以认为是open系统调用)将我们的文件加载进内存的,可是是"谁"让OS这么干的?是进程!!那么OS在将这些文件管理起来过后要不要与进程建立练习呢?答案是要的!!进程想要对一个文件操作,首先第一件事情就是找到这个文件,如果这个文件进程都找不到,那还操作个屁!那么OS是如何将我们的文件与进程建立联系的?首先,在内存中,只要我们找到了struct file结构体,就能找到该结构体对应的文件,这是我们需要知道的!
在进程PCB结构体中有一个指针,该指针指向一个结构体,这个结构体中有一张存储struct file*的表!OS会将进程要求自己打开的文件的struct file结构体的指针存入这张表中,以此来建立进程与文件之间的关系!这么说,可能比较抽象,我们举个具体的例子:现在我们的进程不是要打开log.txt文件嘛
,OS就会将log.txt文件加载进内存,然后用struct file描述它,然后将log.txt的struct file指针存入打开log.txt文件的进程所维护的struct file*表中!
又由于,每个进程被创建出来过后,都会默认先打开3个文件:标准输入文件(键盘)、标准输出文件(显示器)、标准错误文件(显示器);因此这3个文件的struct file结构体指针会被依次存储在进程所维护的struct file*表中!进程后续打开的文件的struct file*就只能从下标位置为3的位置开始存储!OS(open系统调用)在做完这一切过后,会向上层(我们用户)返回此次被打开的文件的struct file*在进程维护的struct file*表中的存储位置,也就是数组下标!这也就是open系统调用的返回值,我们通常把这个返回值叫做文件描述符,本质上文件描述符就是数组下标!
Linux基础IO_第11张图片
那么后续的write/read系统调用是如何根据文件描述符找到对应文件的呢?
首先write与read是系统调用,既然是系统调用,那么必定得有进程来调用吧!write/read就会在调用他们的进程的PCB中自动搜索当前进程所维护的struct file*表,然后根据我们用户传入的文件描述符去定位write/read需要操作的文件!(文件描述符本质就是一个数组下标嘛!所以write/read在struct file*表中查找起来效率就比较高!)

重定向的原理

在Linux下如果我们不想让我们的程序将结果输出到文件的话,我们可以利用>(输出重定向)到一个文本文件中!同时我们也可以利用>>将我们程序的输出结构以追加的方式写入文本文件中,同时我们也可以利用<从文本文件按中读取数据!
Linux基础IO_第12张图片
可是,为什么用<、>、>>这些符号就能让原本输出到屏幕上的信息,全都输出到文本文件中呢?这是什么原理呢?这就是我们接下来要来探讨的问题;

前面我们说了一个进程自被创建出来就会默认打开三个文件:标准输入文件(键盘文件)、标准输出文件(显示器文件)、标准错误文件(显示器文件);由于是进程最先打开的3个文件,所以这3个文件会被最先存储在进程维护的struct file*表中!因此这3个文件对应的文件描述符依次是:0、1、2,后续进程再打开文件时,对应文件的struct file*指针就从3号文件描述符的位置开始存储(或者数组下标为3的位置开始存储),那么既然同样作为文件,我们可以利用close来关闭普通文件,那么我们可不可以用close来关闭这3个标准文件呢?
当然可以!我们知道这3个标准文件的文件描述符,加上close系统调用,可以轻松搞定!
既然这样的话,我们就先来验证一下:比如我们利用close关闭标准输出文件,那么我再使用printf就应该在屏幕上看不到printf的输出内容!
Linux基础IO_第13张图片
程序的运行结果是符合我们的预期的,因此我们的推理是正确的!
下面我们在来探讨一下文件描述符的分配规则
首先,我们关闭文件描述符所对应的文件过后,文件描述符依然存在,因为文件描述符的本质就是数组下标!我们文件描述符对应的文件,其实本质上就是清除该数组下标对应的元素中的内容!
下面我们通过一段程序来探讨文件描述符的分配规则:
Linux基础IO_第14张图片
我们通过运行结果可以发现,文件描述符的分配似乎是连续的且递增的!为此我们可以大胆猜测,文件描述符的分配规则是:找到struct file*表中下标最小的且没有被使用的空间的下标!
这只是我们的猜测,我们还可以在来验证一下,进程不是默认打开了3给标准文件嘛,如果我们关闭掉标准输入文件标准错误文件,那么我们的文件描述符是不是就从0号位置开始分配了呢?
Linux基础IO_第15张图片
为此我们可以推导出文件描述符的分配规则:进程在调用系统调用open函数打开文件之后,OS会遍历当前进程所维护的struct file*表,然后找到该表中未被分配的空间,且下标在未被分配的空间中最小!找到过后,将对应文件的struct file*指针存入当前下标位置;

因此,如果我们关闭掉1号文件描述符所匹配的文件也就是标准输出文件,随后再马上打开一个新文文件(比如log.txt),那么根据文件描述符的分配规则,1号文件描述符所匹配的文件是不是就是log.txt文件,那么我们使用printf函数输出内容的时候是不是就是在向log.txt文件输出!
为什么这么说呢?
printf是向标准输出文件输出对吧!然后呢标准输出文件在C语言中的表示方式就是stdout,通过查阅资料我们得知stdout是个FILE*的文件指针!:
Linux基础IO_第16张图片
然后呢?我们知道在C语言中是用FILE*来寻找文件的,但是在底层OS是不认识FILE*的,要想找到文件,就必须拿到文件描述符,因此在C语言中FILE结构体中必定封装的有文件描述符!这一点我们可以利用代码来证实:
Linux基础IO_第17张图片
我们可以发现FILE结构体里面确实是封装了文件描述符!同时我们发现stdin(标准输入)所指向的结构体封装的文件描述符为0,stdout(标准输出)所指向的结构体封装的文件描述符为1,stderr(标准错误)所指向的结构体封装的文件描述符为2;
再结合printf是像标准输出,也就是向stdout输出,本质上就是向1号文件描述所匹配的文件进行输出!
默认情况1号文件描述符所匹配的文件是显示器,但是如果我们改变了1号文件描述符所匹配的文件,printf就会向1号文件描述符所匹配的新文件中输出!
Linux基础IO_第18张图片
像这种,printf本应该输出到屏幕上的,但是最后输出到文件中去的操作,就叫做重定向!上面修改文件描述符所匹配的文件的动作,就是重定向的原理!因此重定向的本质就是修改数组下标所对应的空间中的地址指向!
当然对于stdin、stderr也是一样的!;
当然我们一般不这样来完输出重定向!
我们可以通过一个函数来实现:
int dup2(int oldfd, int newfd);
这个函数的意思就是用oldfd文件描述符的内容去覆盖newfd文件描述符的内容!
成功返回newfd描述符,失败的话返回-1;
具体操作:
Linux基础IO_第19张图片
Linux基础IO_第20张图片
同样能完成任务!
拿如果我想用追加的方式写,该怎么办?
我们只需要以追加的方式打开文件就行:
Linux基础IO_第21张图片
Linux基础IO_第22张图片
这也就是追加重定向的原理!
Linux基础IO_第23张图片
我们还可以通过命令行的方式,来改变文件描述符所匹配的文件:
1、命令 >log.txt//等价于命令 1>log.txt//意思就是将该命令的1号文件描述符所匹配的文件换成log.txt
2、命令 >log.txt 2>&1//先将1号文件描述符所匹配的文件换成log.txt,然后再将2号文件描述符所匹配的文件换成1号文件描述符所匹配的文件!
Linux基础IO_第24张图片

你可能感兴趣的:(Linux,linux,运维,服务器)