文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-09-05 18:10

关注

  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    在这里插入图片描述

请添加图片描述

基础IO

请添加图片描述

0.感性认识一切皆文件

linux认为,一切皆文件。
对文件而言:

曾经理解的文件:read 、 write显示器:printf/cout ——》 一种write键盘:scanf/cin ——》一种read

在这里插入图片描述 🧐什么叫做文件呢?

侠义上的文件:普通的磁盘文件
广义上的文件:显示器,键盘,网卡,磁盘,显卡,声卡,几乎所有的外设,都可以称为文件

1. 回顾C中的文件操作

🥑C读写文件

文件操作:

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*,这三个东西是什么呢?

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

2.系统文件 I / O

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

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

在这里插入图片描述

🌈open & close

💢通过手册查找 man 2 open

#include #include #include 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 int close(int fd);

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

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

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

fopen("./log.txt", "w");int fd = open("./log.txt", O_WRONLY | O_CREAT, 0666);//umask过滤权限,可以把umask设为0

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

//用int中的不重复的一个bit,就可以表示不同状态#define ONE 0x1     //0000 0001#define TWO 0X2     //0000 0010#define THREE 0X4   //0000 0100                        //系统内部用 & 来验证标志位是否为1void 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 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 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结构体里面,必定封装了fd?

在这里插入图片描述

🎨file descriptor(fd文件描述符)

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

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

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

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

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

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

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

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

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

在这里插入图片描述

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

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

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

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

在这里插入图片描述

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

🎨理性认识一切皆文件

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

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

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读写方法,因为由冯诺依曼体系结构知,外设是要和内存打交道的。这可能有些奇怪,比如键盘能读我知道,但能写吗注意,我们有统一的读写方法,但不代表非要每一个都实现,比如键盘就可以没有写方法,即方法为空
在这里插入图片描述

所有的设备都可以有自己的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

在这里插入图片描述

惊奇的发现,居然没有打印出来,而是全部打印到文件中呢?

在这里插入图片描述

本来应该显示到显示器中,却被打印到文件内部,这种行为我们早就知道叫做输出重定向。咱们无意之间居然完成了一次重定向操作,为什么是这样呢?

💢这是因为:我们以上来就close(1), 断开了和显示器文件的关系,相当于置NULL,对于新打开的log.txt,根据文件分配规则,1是指向log.txt的

在这里插入图片描述
思考printf底层是在做什么?事实上,它本质是向标准输出(stdout)打印 ——

int fprintf(FILE *stream, const char *format, ...);stdout -> FIEL{fileno = 1} -> log.txt// stdout只认识1,只对1输入输出

这就是重定向的本质:在OS内部,更改fd对应的内容的指向!!

🌍追加重定向

追加重定向与输出重定向唯一的差别就是在打开方式上不要O_TRUNC 清空,增加O_APPEND选项。

在这里插入图片描述

🌍输入重定向

输入重定向就是把本来应该从键盘获取内容变成从文件中获取

char *fgets(char *s, int size, FILE *stream); 

在这里插入图片描述

🌍dup2

以上情况都是先关闭了文件然后再打开文件这样重定向,但是情况不会总是这样子

🍑看文档得知:dup2是对指针做拷贝

#include int dup2(int oldfd, int newfd); //oldfd->newfddup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:*  If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.*  If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.

这里有点绕的,copy后最后要和谁一样?,嘿嘿我刚学也绕晕了

💢假设:输出重定向 显示器(1)-> log.txt, dup2()的参数该怎么样填呢?

在这里插入图片描述

➰输出重定向

dup2(fd, 1);  本来应该显示到显示器的内容,写入到文件

在这里插入图片描述
注意,系统层面,open打开文件时带了选项O_TRUNC,以清空原来内容。而在C语言中"w"也会先把原始文件清空,说明上层封装了这个选项

➰追加重定向

只需在输出只写的基础上添加O_APPEND选项

在这里插入图片描述

➰输入重定向

dup2(fd, 0);  原本从键盘读,现在从文件中读。

在这里插入图片描述

5.课后练习

Linux下两个进程可以同时打开同一个文件,这时如下描述错误的是:

A.两个进程中分别产生生成两个独立的fd
B.两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性
C.进程可以通过系统调用对文件加锁,从而实现对文件内容的保护
D.任何一个进程删除该文件时,另外一个进程会立即出现读写失败
E.两个进程可以分别读取文件的不同部分而不会相互影响
F.一个进程对文件长度和内容的修改另外一个进程可以立即感知

以下描述正确的是 [多选]

A.程序中打开文件所返回的文件描述符, 本质上在PCB中是文件描述符表的下标
B.多个文件描述符可以通过dup2函数进行重定向后操作同一个文件
C.在进程中多次打开同一个文件返回的文件描述符是一致的
D.文件流指针就是struct _IO_FILE结构体, 该结构体当中的int _fileno 保存的文件描述符, 是一对一的关系

解析:
A和D不用多说,都是正确的,重点我们来看B我当时没选):不同信息表数组下标的位置可以存放相同的文件描述信息结构指针,dup2重定向的本质原理,就是改变对应位置的文件信息而改变操作对象文件的

答案是ABD

bash中,需要将脚本demo.sh的标准输出和标准错误输出重定向至文件demo.log,以下哪些用法是正确的 [多选]

A.bash demo.sh &>demo.log
B.bash demo.sh >&demo.log
C.bash demo.sh >demo.log 2>&1
D.bash demo.sh 2>demo.log 1>demo.log

题目解析:
比较典型的方式是:bash demo.sh 1>demo.log 2>&1

A command &> file 表示将标准输出stdout和标准错误输出stderr重定向至指定的文件file中。

B 与A选项功能雷同

C 比较典型的写法,将标准输出和标准错误都重定向到文件, >demo.log是一种把前边的标准输出1忽略的写法

D 比较直观的一种写法,不秀技,直观的将标准输入和标准错误分别重定向到文件

答案是ABCD

以下代码的功结果是

   void func() {     int fd = open("./tmp.txt", O_RDWR|O_CREAT, 0664);     if (fd < 0) {     return -1;     }     dup2(fd, 1);     printf("hello bit");     return 0;   }

A.将hello bit打印到终端显示
B.将hello bit 写入到tmp.txt中
C.将hello bit 打印到终端显示并且写入tmp.txt文件中
D.既不打印,也没有写入到文件中

解析:
选错选了C,忘记了printf是默认向1中打印,因为dup2,标准输出已经被重定向,因此数据会被写入文件中,而不是直接打印

答案选B ,粗心大意

最近为瑞幸打call,双十一囤了很多咖啡

请添加图片描述

来源地址:https://blog.csdn.net/qq_42996461/article/details/127811244

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-人工智能
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯