【Linux】基础IO(万字详解) —— 系统文件IO 文件描述符fd 重定向原理_delayed_fput

1. 回顾C中的文件操作

🥑C读写文件

文件操作:

  • 首先要打开文件:打开成功返回文件指针;打开失败,返回NULL
  • 最后要关闭文件
FILE \*fopen(const char \*path, const char \*mode);//路径 + 打开方式
int fclose(FILE \*fp);

💦C写文件
们可以fputs/fgets以字符串形式读写;也可以fprintf/fscanf格式化读写

int fputs(const char \*s, FILE \*stream);  向特定文件流写入字符串

int fprintf(FILE \*stream, const char \*format, ...);

什么叫做当前路径

  • 当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径

在这里插入图片描述

如果以w模式打开文件,默认把原始内容清掉,再写入(类似输入重定向)

在这里插入图片描述

如果要以追加方式写,则要以"a" append模式打开文件
在这里插入图片描述

💢细节提问: 此处的strlen要不要+1?

   //进行文件操作
   const char\* s1 = "hello fwrite\n";
   fwrite(s1, strlen(s1), 1, fp);

不要!\0结尾是C语言的规定,文件用遵守吗?文件保存的是有效数据!
否则就会出现乱码

在这里插入图片描述

tips:快速清空文件

>log.txt  //输入前已经清空文件,输入的又为空白

💦C读文件
fgets从特定文件流中按行读取,内容放在缓冲区。读取成功返回字符串起始地址,读失败返回NULL

char \*fgets(char \*s, int size, FILE \*stream); //size:为缓冲区大小

在这里插入图片描述

🥑关于stdin stdout stderr

C语言默认会打开三个输入输出流:stdin、stdout、stderr, 它们的类型都是FILE*,这三个东西是什么呢?

  • C语言把它们当做文件看待;站在系统角度,stdin对应的硬件设备是键盘stdout对应显示器stderr对应显示器,本质上我们最终都是访问硬件。C++中也有cin、cout、cerr,几乎所有语言都提供标准输入、标准输出、标准错误

既然fputs是向文件写入,stdout也是FILE*类型,我们是不是可以向显示器标准输出打印了?这说明显示器被看做文件(有那味了) 喊出那句话:Linux下,一切皆文件

2.系统文件 I / O

通过之前的学习,这些文件操作最终都是访问硬件(显示器、键盘、磁盘)。众所周知,OS是硬件的管理者。所有语言上对“文件”的操作,都必须贯穿操作系统。然而OS不相信任何人,访问操作系统,就必须要通过系统接口!!

open/fclose,fread/fwrite,fputs/fgets,fgets/fputs 等库函数一定需要使用OS提供的系统调用接口,接下来我们就来学习文件的系统调用接口,才能做到万变不离其宗!!

在这里插入图片描述

🌈open & close

💢通过手册查找 man 2 open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char \*pathname, int flags);//路径 + 选项
int open(const char \*pathname, int flags, mode\_t mode);

参数说明

pathname: 要打开或创建的目标文件文件名
flags:    打开方式。传递多个标志位,下面的一个或者多个常量进行“或”运算,构成flags.
             O_RDONLY: 只读打开
             O_WRONLY: 只写打开
             O_RDWR  : 读写打开
          以上这三个常量,必须指定一个且只能指定一个
             O_CREAT : 若文件不存在,则创建它。同时需要使用mode选项,来指明新文件的访问权限
             O_APPEND: 追加写
mode: 	  设置默认权限信息 

返回值说明:

return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).
     成功: 新打开的文件描述符 
     失败: -1

💢man 2 close

#include <unistd.h>

int close(int fd);

话不多说,用起来。open如果以写入方式打开且文件不存在,需要或|上O_CREAT,这与C中以"w"模式打开完全一样 (为什么是|呢,继续看下去吧)。如果我们先不带第三个参数去实现

在这里插入图片描述
发现open并没有帮我创建新文件,因为我刚刚用的是C语言的接口,C语言的接口创建难道在系统也一样?

在这里插入图片描述
可以看见文件的权限为什么是这样子的?有这个文件,要创建它,系统层面就必须指定权限是多少!,也是要用到我们的第三个参数了,我们采用权限设置的八进制方案——

  • 我们在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能做了非常多的动作! 我根本就不关心什么只写、创建、权限这些与系统相关的概念。语言为我们做了封装,我用就好了。
  • 也就是:哪有什么岁月静好,有人负重前行罢了
fopen("./log.txt", "w");
int fd = open("./log.txt", O_WRONLY | O_CREAT, 0666);//umask过滤权限,可以把umask设为0

💢那第二个参数flags(int)为什么要把模式|在一起呢?

  • 这是一种用户层给内核传递标志位的常用做法。
  • int有32个比特位,不重复的一个bit,就可以表示不同状态,就可以传递多个标志位且位运算效率较高。这些O_RDONLYO_WRONLYO_RDWR都是只有一个比特位是1的宏,并且相互不重复,这样|在一起,就能传递多个标志位
//用int中的不重复的一个bit,就可以表示不同状态
#define ONE 0x1 //0000 0001
#define TWO 0X2 //0000 0010
#define THREE 0X4 //0000 0100 

//系统内部用 & 来验证标志位是否为1
void show(int flags)//0000 0011
{
  if(flags & ONE)  printf("hello one\n");//0000 0011 & 0000 0001 为真
  if(flags & TWO)  printf("hello two\n");
  if(flags & THREE) printf("hello three\n");
}

int main()
{
  show(ONE);
  show(ONE | TWO); //0000 0001 | 0000 0010
  show(ONE | TWO | THREE);
}

进入fcntl-linux.h此文件,可以看到

在这里插入图片描述

🌈read & write

找辣个男人问问 哈哈哈
💦man 2 write

#include <unistd.h>

ssize\_t write(int fd, const void \*buf, size\_t count);
参数:
    buf: 用户缓冲区
    count: 期望写的字节数
返回值:实际写入的字节数

在这里插入图片描述

又一次写入时,我们发现:

在这里插入图片描述

O_TRUNC: 打开文件的时候直接清空文件

O_TRUNC:
    如果文件已经存在并且是一个常规文件,并且开放模式允许写入(即是0_RDNRor O_MwRONLY),那么它将被截断为长度为0(也就是清空文件)

在这里插入图片描述

O_APPEND: 追加文件

在这里插入图片描述

注意注意:写入文件的过程中,不需要写入\0!因为\0是C语言层面上规定字符串的结束标志,而写入文件关心的是字符串的内容,文件和语言不要搞混了

💦 man 2 read

#include <unistd.h>

ssize\_t read(int fd, void \*buf, size\_t count);
参数:
    buf: 读到的内容放在用户层缓冲区中,也就是自己定义缓冲区
    count: 期望读多少个字节
返回值:实际读多少个字节

读文件的前提:文件已经存在,不涉及创建及权限的问题,那么用两个参数的open打开文件即可

在这里插入图片描述

3.文件描述符(fd)

通过上面的练习,我发现每次成功open的fd都是3
在这里插入图片描述

接下来,我们连续的打开文件,观察fd。我们知道打开文件失败返回-1,那么012去哪了呢?012消失的原因,要么是不让用,要么是被别人占用

在这里插入图片描述

事实上,当我们的程序运行起来变成进程,默认情况下,OS会帮助我们打开三个标准输入输出,012其实分别对应的就是标准输入、标准输出、标准错误

实践出真知:

在这里插入图片描述
c语言上的stdin标准输入、stdout标准输出、stderr标准错误,对应硬件设备也是键盘、显示器、显示器,这有什么关联呢?

话说回来,我们还是一直没搞懂FILE*是什么东西,FILE*open

  • FILE其实是一个struct结构体!是C语言库提供的,一般内部有多种成员
  • C文件 库函数内部 一定要调用 系统调用
  • 在系统角度,只认识fd
  • 所以FILE结构体里面,必定封装了fd

怎么样证明FILE结构体里面,必定封装了fd?

在这里插入图片描述

🎨file descriptor(fd文件描述符)

所有的文件操作都是进程执行对应的函数,即本质上是进程对文件的操作

🔸 如果一个文件没有被打开,这个文件是在磁盘上。如果我创建一个空文件,该文件也是要占用磁盘空间的,因为文件的属性早就存在了(包括名称、时间、类型、大小、权限、用户名所属组等等),属性也是数据,所谓“空文件”是指文件内容为空

🥑文件 = 内容 + 属性。对文件的操作也是分成两类的:对文件内容的操作 + 对文件属性的操作

🔸 要操作文件,必须打开文件(C语言fopen、系统上open),本质上,就是文件相关的属性信息从磁盘加载到内存的过程

文件:被进程打开的文件(内存文件),没有被打开的文件(磁盘文件)

操作系统中存在大量进程,一个进程可以打开多个文件:进程:文件 = 1:1。系统中会存在大量的被打开的文件!所以OS要不要把如此之多的文件在内存中也管理起来呢? 必须管理! 先描述再组织

我们操作系统是C语言写的,OS内部要为了管理每一个被打开的文件,构建struct file

//创建struct file 对象,充当一个被打开的文件
struct file
{
    struct file \* next;
    struct file \* prev;
    //包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性,权限,缓冲区)
}

  • 🌍文件描述符:01234567,从0开始,连续的小整数,会让我们联想到数组下标!

打开的这么多文件,怎么知道哪些是我们进程的呢?操作系统为了让进程和文件之间产生关联,进程在内核创建struct files_struct 的结构,这个结构包含了一个数组 struct file* fd_array[] ,也就是一个指针数组,把表述文件的结构体地址填入到特定下标中。

在这里插入图片描述

🥑那么现在就能解释了为什么打开文件返回的是3:

  • 新打开一个文件本质是内核会为我们描述struct file结构,再把struct file地址填入到fd_array[]数组下标去,因为012已经被占用了,于是填到3号下标,对应的数组下标3返回给用户,这样就能通过fd从而找到了文件对象

这也解释了为什么write和read这样的系统调用接口为什么一定要传入文件描述符fd:执行系统调用接口是进程执行的,通过进程PCB,找到自己打开的文件列表,通过fd索引数组找到对应的文件,从而对文件进行操作

✅ 结论:文件描述符fd,本质是内核中进程和打开文件关联数组下标

🍈接下来我们看看源代码:

在这里插入图片描述

刚好对应我们的猜测:
在这里插入图片描述
我们理一下逻辑:

  • 🔥调用fopen -> 底层是调用open -> 得到fd -> 封装成FILE -> 返回FILE*给给open,传到用户手里
  • 🔥:用户调用fwrite() -> FILE* -> 包含了fd -> 内部封装write -> 最后是write(fd, …)-> 自己执行操作系统内部的write -> 找到进程的task_struct -> 再找到*fs 指针 -> 找到文件描述符表 files_ struct -> fd_ array[ fd ] -> struct _file -> 找到了内存文件
🎨理性认识一切皆文件

一切皆文件是linux设计哲学,体现在操作系统的软件设计层面

Linux是C语言写的!那如何用C语言实现面向对象,甚至多态?

  • 我们知道:类是由成员函数 + 成员方法组成,c语言里的struct就能实现
struct file
{
	int size:
	mode_t mode;
	int user;
	int group;
	......
	//函数指针
	int (\*readp)(int fd, void \* buffer, int len);
	int (\*writep)(int fd, void \* buffer, int len);
	.....
}

对于键盘显示器等等这些外设,一定都有比如像read、write读写方法,因为由冯诺依曼体系结构知,外设是要和内存打交道的。这可能有些奇怪,比如键盘能读我知道,但能写吗注意,我们有统一的读写方法,但不代表非要每一个都实现,比如键盘就可以没有写方法,即方法为空
在这里插入图片描述

  • 底层不同的硬件,一定对应的是不同的操作方法!!
  • 上面所述的外设,每一个设备的核心访问函数,都可以是read,write (IO)

所有的设备都可以有自己的readwrite,但是代码的实现方法一定是不一样的

那又是如何做到一切皆文件的呢?Linux中做了软件的虚拟层vfs(虚拟文件系统),会统一维护每一个打开文件的结构体struct file. 上层的struct file操作系统OS实行维护

我们在每个struct file当中包含上一大堆的函数指针,这样,在struct file上层看来所有的文件都是调用统一的接口;在底层我们通过函数指针指向不同硬件的方法。
在这里插入图片描述

说白了就是,我上层不管你具体是什么鸡鸭鹅,都统一被我看成了动物类,类里面有具体的辨别方法,鹅的话就调用鹅的辨别方法,鸡就调用鸡的方法,这样就是一切皆动物的思维了,可以理解为C++的多态是漫长的软件开发摸索中实现“一切皆…”的高级版本/语言版本

在源代码中,struct file就有这样一个结构体指针,指向底层各种实现方法

在这里插入图片描述

这就是面面向对象的手法

🎨文件描述符的分配规则

观察如下代码,可以看到,我把0关掉后,再打开文件是分配的文件描述符就是0 ~

在这里插入图片描述在这里插入图片描述
⚡我们得出文件描述符的分配规则:每次给新文件分配的fd,是从fd_array[]中找一个最小的未被使用的作为新的fd.

其实很好理解,就是从0开始遍历数组中找一个未被使用的下标,并填入文件地址

4. 重定向原理

🌍输出重定向

有没有细心的同学,上面我们唯独没有关闭1,我们现在上手试一下。按照文件描述符的规则,再打开就是打印我们刚刚关闭的1

最全的Linux教程,Linux从入门到精通

======================

  1. linux从入门到精通(第2版)

  2. Linux系统移植

  3. Linux驱动开发入门与实战

  4. LINUX 系统移植 第2版

  5. Linux开源网络全栈详解 从DPDK到OpenFlow

华为18级工程师呕心沥血撰写3000页Linux学习笔记教程

第一份《Linux从入门到精通》466页

====================

内容简介

====

本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。

华为18级工程师呕心沥血撰写3000页Linux学习笔记教程

本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。

需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论

加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值