我们学习基础IO其主要目的是强化对文件的理解,这需要文明进入到系统层面去认识!
1.C/C++文件操做 && 结论
首先,我可以直接向大家输出几个结论:
a. 文件 = 内容 + 属性;所以对文件的操做,实际上是对文件的内容和属性进行操做
b. 访问文件之前,需要通过路劲找到文件再打开文件(路径+文件名)
而文件均由进程打开,只有当代码运行之后成为进程时才被打开
访问文件时,必须要有路径,不过有时路径由进程的cwd提供
c. 打开文件,实际上是把文件加载到内存中;对文件进行操做,本质上是进程通过CPU访问内存中的文件
d. Linux中存在大量的文件,而文件实际上分为:打开文件 和未被打开文件
而从位置上说:也就是1. 在内存中的文件(这篇博客的重点);2. 在磁盘中的文件
e. Linux系统中,可以同时存在很多被打开的文件;OS对文件的管理和当时学习进程管理时的思路基本一致:"先描述,再组织" 通过就结构体链表的增删查改完成对文件的管理
接下来,由我带领大家对C/C++的文件操做进行学习:
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
const char *filename = "log.txt"; //filename就是文件"log.txt"
FILE *fp = fopen(filename,"w"); //打开文件为fp
if(fp == NULL)
{
perror("fopen"); //为空就打印错误信息
return 1;
}
int cnt = 1;
while(cnt++ <= 10)
{
const char *s = "hello world\n";
fputs(s,fp); //向文件中写入10次hello world
}
fclose(fp); //打开文件后一定要记住关闭文件!!!
return 0;
}
简单写一个向文件中写入的代码带领大家进入文件的大门,至于执行结果,希望大家可以自行尝试~~~
接下来,我们就正式进入到文件的世界中了!
fopen ( ):

'w' 方式:
对于 **'w'**写操做,我们有更多的细节告诉大家。

这似乎和echo命令很像!

因为,echo重定向时,其本质也需要将文件打开,而打开的方式也是'w'打开,自然会将原来存在的内容清空!
'w' 不存在就创建,存在就清空!
'a' 方式:
'a' 方式写入时会向文件的结尾处写入

其实,也就想到要 echo "xxx" >> log.txt (不删除原内容,在原内容的基础上进行写入)
接下来的内容,我想先向大家输出一部分前置知识:
2.读写位置:
要知道,任何文件的读写都有其对应的读写位置

那么,什么才是读写位置呢?

所以,文件对于我们来说,实际上也就是一个一维数组!!读写位置也就是数组下标!!
3.open( ):

想要理解上图有些困难,什么是打开模式?为什么打开模式是一个一个的宏?
接下来,我使用一段简单的代码,带大家来理解,为什么在一个open函数中却可以实现,多个宏函数的操做!

由上图可知,我们传递给Print函数不同的数字,就可以实现不同的函数,open( )的实现原理就与其类似,我只需传入不同的打开模式,就可以实现不同的函数功能!而再加上 ' | ' ' & ' 的操做,我们就可以在一条语句中同时实现两个函数功能!也就是同时实现 读写 操做!
返回值:
在上面的学习中,我们知道open的返回值是整数,整数这个返回值对于open这样的函数很奇怪,所以,接下来,我们将去探索,这个整数到底是什么!
我们通过open( )函数的使用来逐渐了解其返回值的概念:
1.打开不存在的文件

2.权限问题

3.umask问题

最终,我们回到最初的返回值问题!
4.返回值问题:

为什么返回值为 3 呢?这个问题我们放在后面再谈,我们现在先了解这个返回值(文件描述符)究竟有什么作用!
4.write( ):


学到这里,我希望大家此时拥有一个鉴定的思维:对文件进行操做和C语言不一样!
我们也成功向log.txt中写入了内容

在C语言中,这种类似的操做是会将文件内容清空的,但在文件操做时,它却在原内容开始的位置进行覆盖式的写入

所以,当我们再给open添加选项:O_APPEND就会在文件原内容的基础上往后添加新的内容了
小结:
我们在学习C语言阶段时,就学习了fopen(),fclose(),等库函数,用于打开文件和关闭文件,而这次,我们学习的是系统调用,open( ),write( )等,而C语言中的库函数和系统调用有什么区别呢?
C语言库函数和系统调用实际上是上下级的关系:
我们所使用的库函数,实际上是在库函数中封装了系统调用,我们使用库函数的本质实际上也就是在使用系统调用!库函数将复杂的选项帮我们封装了,所以我们在使用库函数时也就是不用考虑权限

原因是012文件描述符已经被默认使用了!
0:标准输入
1:标准输出
2:标准错误
C语言中也有类似的:
既然0 1 2是被默认打开的,那么我在使用write时,是不是可以直接向文件标识符为 1 中直接写入?它也就可以直接在我们的显示器上输出

所以,我们不妨大胆推测,再C语言中的FILE结构体,实现输入输出操做时,在结构体中也封装0、1、2这样的文件描述符!

所以,我们也不难看出,结果确实和我们所推测的一致!
也就是说,实际上C语言中很多的库函数,都是封装了系统调用,这一上下级的关系也就更加明确了!
5.文件描述符的本质
我通过一张图让大家可以详细地了解到什么是文件描述符:

文件标识的问题解决完了,接下来我们需要解决的就是:为什么0、1、2是默认打开的?

那么,既然我们可以直接使用系统调用完成对文件的操做,为什么C语言还要对其进行封装?
答案是:对于一个语言来说,需要具备良好的跨平台性,我们再Linux下直接使用系统调用就只能在Linux系统下完成对文件的操做,如果进入MacOS或者Windows系统下,就会因为系统调用不兼容而无法对文件进行操做,也就无法做到很好的跨平台性,所以,C语言就会在不同的系统下封装不同的系统调用 ,但用的都是同一个库函数,这样就可以使用同一个库函数而实现不同平台的系统调用,也就实现了良好的跨平台性!
6.重定向的本质
前面的学习我们知道了当我们打开一个新的文件时,会给该文件分配一个文件描述符,也就是该文件的地址,并且这个文件描述符是从 3 开始的,可为什么会是 3 ?文件描述符分配的规则是什么呢?
文件描述符的分配规则: 
所以,从这我们就不难看出,OS对打开的新文件分配文件描述符的规则就是:分配文件描述表数组中,会优先分配最小的&&没有被使用的fd!
我们知道,fd = 1对应的是显示器输出,如果我们将fd = 1文件关闭,会发生什么呢?

而这种现象,就是我们这一小节需要讨论的关键话题,输出重定向!

也就是说,我们只需要该表0、1、2文件描述符指向的文件,就可以实现重定向了
使用同样的原理,我们就可以完成追加重定向:

既然有了追加重定向和输出重定向,那么输入重定向该如何实现?

综上,我们可以通过关闭标准文件标识符实现重定向的功能,那么有没有办法可以让我们不用关闭标准文件标识符来实现重定向的功能呢?
我们知道重定向的本质,就是在代码层,功能的实现只看对应的fd也就是文件标识符的值,而不在意该文件标识符指向的内容,所以我们是否可以通过改变标准文件标识符所对应的文件内容来实现重定向功能呢?也就是说,将我们自己的文件 log.txt 的地址拷贝复制到标准文件标识符中,此时就可以实现重定向功能了
如果可以通过这样的方式实现重定向,我们就不仅仅只能实现对标准文件标识符的重定向功能了,可以实现任意文件的重定向,只需将文件内容进行更改即可!
dup2( ):

使用dup实现对应功能:

其余的功能实现类似,且该函数使用时只需注意目标文件和原文件的顺序即可,相信大家可以独立完成!
7.一切皆文件
我们前面提到了在Linux下一切皆文件,可一直没有对一切皆文件做出解释,为什么键盘等输入外设还是文件呢?又为什么显示器这样的输出外设也能是文件呢?这次我们将这个困惑解开!

也就是说,Linux通过struct file将不同的硬件使用同样的接口联系在了一起,也就可以通过struct file文件访问到硬件!在进程层面,只看到了同样的struct file文件,通过文件可以访问硬件,所以,对进程来说,Linux下一切皆文件
8.myshell进一步完善
在上一篇文章中,我们实现我们的myshell,不过我们并没有真正实现shell的所有功能,比如:重定向!
a.分析:


b.函数实现:
ParseRedir

ParseRedir函数只能用于对重定向命令进行解析,并没有实现重定向命令!
前面分析部分我们已经知道,经过ParseRedir函数的处理,我们就将重定向部分删除了,留下的就只有单纯的命令和文件名!
执行部分也需要进行更改:

总结:
通过这篇文章,我们对Linux下文件的操做以及理解有了更深刻的理解,和更熟练的使用,并且对Linux下一切皆文件也有了一个新的认知,对于功能不全的myshell也有了一个进一步的完善myshell添加重定向功能 · e02a760 · wgl520666/Linux学习仓库 - Gitee.com
