Linux:文件与fd(被打开的文件)

news/2025/2/1 18:13:50/

在这里插入图片描述
hello,各位小伙伴,本篇文章跟大家一起学习《Linux:文件与fd(被打开的文件)》,感谢大家对我上一篇的支持,如有什么问题,还请多多指教 !
如果本篇文章对你有帮助,还请各位点点赞!!!

话不多说,开始正题:

文章目录

    • 前置准备
    • 被打开的文件——内存
      • 文件IO基础讲解
      • 三大流
      • 系统调用接口
      • 文件描述符`fd`
      • 文件描述符表
      • 如何理解硬件也是文件
      • 文本写入 vs 二进制写入
      • `IO`的基本过程----文件内核级缓冲区----重定向
      • fd的分配
      • 完善`sehll`
      • 回归上面的问题

前置准备

问大家一个问题,空文件有没有大小?答案是有:

  • 文件 = 内容 + 属性
    即使一个文件是空文件,也只是没有内容而已,还是要有相关的属性如:名称、创建时间、权限……
  • 在访问任何一个文件之前,都必须打开它,为什么?
    想想在访问文件之前,文件在哪?没错,在磁盘上
  • 访问一个文件,是谁在访问?
    当我们写完一个有fopen的代码的时候,文件打开了吗?没有
    当我们将其转化为可执行文件的时候,文件打开了吗?没有
    当我们运行可执行文件的时候,文件打开了吗?当fopen运行结束,文件打开了!!
    所以,访问文件的是进程!!!
  • 进程是在内存当中的,本质上就是CPU在跑进程,但是文件在磁盘中,根据冯诺依曼原理,CPU只能跑内存上的数据,所以,要想访问文件,就必须把文件加载到内存当中,也就是说,打开文件,其实就是将文件加载到内存当中!!
  • 将一个文件加载到内存,无非就是加载属性或者内容嘛,一个进程能够打开一个文件,那么能打开多个文件吗?包的!那一个系统里那么多进程,岂不是非常多文件被打开?那么操作系统要不要对其进行管理呢?答案是肯定的!如何管理:先描述再组织!
  • 盲猜一手:在内核中:文件 = 内核数据结构 + 文件内容
  • 进程 = 内核数据结构 + 程序的代码和数据;和文件十分相像,所以结论:
    我们在研究打开的文件,是在研究进程和文件的关系!
  • 那没有被打开的文件呢?在哪里?在磁盘中!

以下是本文所要讲解的内容:

  1. 被打开的文件——内存
  2. 没有被打开的文件——磁盘
  3. 二者结合就是文件系统!

被打开的文件——内存

文件IO基础讲解

fopen有很多个选项:
在这里插入图片描述
首先讲解w:打开文件,若文件不存在会帮助我们创建文件;若文件存在,将会把原先的文件内容清空。
这个操作和命令行中的>输出重定向很相像,一样是清空原先文件内容,再将内容输出在文件中,但是若>右侧没有内容,相当于清空文件。

myfile.c:

#include <stdio.h>int main()
{FILE *fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}char buffer[1024];const char *message = "hello";int i = 0;while(i < 10){snprintf(buffer, sizeof(buffer),"%s: %d\n", message, i);fputs(buffer, fp);i++;}fclose(fp);return 0;
}

在这里插入图片描述
可以看到,执行了两次myfilelog.txt的内容并没有变化,那是当然!
要是把fopenw改为a
在这里插入图片描述
发现端倪了吧!其实a就是append的意思——追加,所以打印结果才会这样,追加并不会将原先的文件清空
在命令行中也有追加重定向>>
在这里插入图片描述
剩下的选项都比较简单就不讲了

三大流

一个程序在默认启动时给我们打开这三种输出输入流

  • stdin 标准输入 键盘
  • stdout 标准输出 显示器
  • stderr 标准错误 显示器

在这里插入图片描述
可以看到这三种输出流都属于FILE*,这不和fopen的返回值一模一样吗?
其实这三种输出流都是C语言给我们默认提供的

但是键盘、显示器都是硬件啊,怎么能联系在一起?
没错,我们语言层是不能直接与硬件层直接互通的,必须通过操作系统,才能实现互通
所以,我们所用的C文件接口,底层一定要封装对应的文件类系统调用!

系统调用接口

open:
在这里插入图片描述

  • 第一个参数pathname就是要打开文件的路径名称
  • 第二个参数flags就是标记位
    主要用得到标记位:在这里插入图片描述
    这些标记为其实都是些宏,对于flags看上去是一个整型int,实际上是一个32比特位的位图,而这些宏,实际上就是只有一个比特位为1的值

其实很好理解,看如下代码:

#include <stdio.h>#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)void Test(int flags)
{if(flags & ONE){printf("ONE\n");}if(flags & TWO){printf("TWO\n");}if(flags & THREE){printf("THREE\n");}if(flags & FOUR){printf("FOUR\n");}if(flags & FIVE){printf("FIVE\n");}
}int main()
{Test(ONE);printf("========================\n");Test(ONE | TWO);printf("========================\n");Test(ONE | TWO | THREE);return 0;
}

在这里插入图片描述
我们使用一下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT);return 0;
}

在这里插入图片描述
文件是创建出来了,怎么权限是乱码啊?

要记住,我们上述使用的fopen是已经被包装好的函数,我们在这使用的open是系统级的调用接口,是不会帮助我们设置权限的,也没有权限去帮我们设置默认权限(因为都是系统级别),需要我们在创建的时候设置好默认权限!

怎么设置,我们上述参数还少了一个没讲哦,没错,就是mode_t mode,所以一般我们都建议使用三参数的open,除非你只读或只写并且文件存在,看如下代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT, 0666);// 一般建议在前面加0return 0;
}

在这里插入图片描述
权限变正常了,但是为什么不是我设置的0666的权限?这是因为权限掩码的问题:
在这里插入图片描述

在设置权限的时候,你设置的权限会按位与上权限掩码,然后取反,得到的结果就是最终的权限!

当然了umask也是可以改变的:
在这里插入图片描述

umask(0);

将其设置为0之后,我们设置的权限就是最终的权限:
在这里插入图片描述
要注意的是:你在程序里设置的umask并不会影响系统的umask,也就是说每个进程都会有属于自己的umask,继承于父进程的umask

你也可以修改系统的权限掩码:

umask 0777

剩下最后就是open的返回值:
在这里插入图片描述
返回值是一个文件描述符,并且是个int,那到底是什么呢?我们看一下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");}printf("fd1: %d\n", fd1);return 0;
}

在这里插入图片描述

3是个什么鬼?获取了这个文件描述符后,就可以关闭这个文件close,也可以在关闭文件前读取文件read或者写入文件write

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");}const char *buffer = "hello\n";write(fd1, buffer, strlen(buffer));printf("fd1: %d\n", fd1);close(fd1);return 0;
}

在这里插入图片描述
写入成功!!!读取也是一样的道理就不多说了

直入主题,要是此时把hello改成##会怎样?

const char *buffer = "##";

在这里插入图片描述
噢!!!发生了覆盖,根据现象这就很好理解了吧,你并没有告诉程序你要清空原先内容,所以,程序就直接覆盖写入,好理解吧,那要是我想清空文件?O_TRUNC这不就有了么:

int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

那要是我不想覆盖,而是想追加呢?O_APPEND

int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);

其实讲这么多总结出:

  • fopen、fclose、fwrite、fread——C库函数
  • open、close、write、read——系统调用
  • C库函数就是系统调用封装的

文件描述符fd

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}

在这里插入图片描述
为什么是这样子?我们知道< 0就是打开文件失败,但是为什么没有0、1、2

还记得上文讲到在默认启动时给我们打开这三种输出输入流

  • stdin 标准输入 键盘
  • stdout 标准输出 显示器
  • stderr 标准错误 显示器

这不就刚刚好对应上我们的0、1、2了吗,test一下:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{write(1, "hello\n", 6);return 0;
}

在这里插入图片描述
测试成功!!!测试一下read

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{char buffer[128];ssize_t s = read(0, buffer, sizeof(buffer));if(s > 0){buffer[s - 1] = 0;}printf("%s\n", buffer);
}

read的返回值是:实际上读取到了多少个字符,如果返回值s大于0,说明读取成功,又因为我们再输入完成后会多输入一个\n的字符,所以将buffer最后一个字符设置为0,字符串最后以\0结尾顺带把\n去掉:
在这里插入图片描述
对于ssize_t你可以直接当作是种整数,是一种有符号的整数

再补充一个细节,能不能在写入的时候把\0也写进去:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);const char *buffer = "aaa";write(fd1, buffer, strlen(buffer) + 1);close(fd1);return 0;
}

看上去没什么问题,实际上问题大了,文件确实能够成功创建,但是当我们vim进入看一下文件时:
在这里插入图片描述
多了一个^@,兄弟萌,别被骗了啊,我们只是在C、C++语言上字符串以\0做结尾,文件上并没有说过啊!!

所以写进去会发现是一个不可读取的字符,就是乱码

  • 从文件读取要记得加\0做结尾
  • 写入文件要记得去掉\0

那请问大家,有什么时候我们会用上0、1、2、3、4……,答案是:数组下标

文件描述符表

对于操作系统来讲,打开一个文件就是将文件从磁盘中加载到内存当中,会将文件的内容和属性加载进内存

在内存里会有已经打开了的文件描述符0、1、2,他们以链表的形式串联起来,当新增一个打开的文件的时候,就会插入一个新节点struct_file,所以打开文件和关闭文件不就是相当于对链表的增删查改了吗?

但是在内存里有成千上百个进程,这个链表又不能与进程强相关,那该怎么办?

在这里插入图片描述
在进程的数据结构task_struct中有struct files_struct *files,指向一个结构体struct files_struct,这个结构体里有一个文件描述符数组struct file* fd_array[N],这些数组有自己的下标,数组元素指向着不同的文件,要记住:**Linux下一切皆文件!**所以硬件也不除外

所以在我们调用接口打开一个文件的时候,open就会遍历数组struct file* fd_array[N],发现0、1、2都被占了,找到3没有被占,于是就把文件地址填写进3这个位置,并且把3返回给应用层

在系统层面上访问文件只有一条路:文件描述符,没有其他路可以走!

好,抛出问题:什么是FILE、FILE*,这些都是stdin、stdout、stderr的类型
在这里插入图片描述

FILE、FILE*跟上面的图的内容没有任何关系!!
FILE是一个结构体struct FILE,是C语言封装好的结构体,里面必然会有很多与文件相关的属性,虽然我不知道有什么,但是肯定有文件描述符fd,为什么?因为刚刚讲过,在系统层面上访问文件只有一条路:文件描述符,没有其他路可以走!再加上fopen、fclose、fwrite的底层就是系统调用的接口,所以FILE结构体肯定有维护着的文件描述符,test一下:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{printf("stdin: %d\n", stdin->_fileno);printf("stdout: %d\n", stdout->_fileno);printf("stderr: %d\n", stderr->_fileno);FILE *fp = fopen("log1.txt", "w");printf("fp: %d\n", fp->_fileno);return 0;
}

在这里插入图片描述
测试成功!!!

如何理解硬件也是文件

硬件和操作系统之间会有一个驱动程序来关联,操作系统和语言层之间会有系统调用接口来关联
在这里插入图片描述
对于那么多的硬件,操作系统也是要对硬件进行管理的,同样是先描述,再组织,会产生一个结构体struct device来对硬件进行描述,然后就像task_struct一样用链表串起来,当需要进行工作时,就放进工作队列

在结构体struct file里,描述着键盘、屏幕、网卡……,肯定会有读read和写write的方法,由于每种硬件的读和写都是不一样的,在C语言中,结构体本身不能包含方法(函数),但是我们可以使用函数指针,要是没有读或写就直接将函数指针设置为空即可,对于每种不同的硬件,完善不同的读和写,再用函数指针指向相应的函数:
在这里插入图片描述
在系统层面做了一层软件的封装,一切皆文件!!虚拟文件系统vfs

对于上图操作,就会联想到C++的多态。

文本写入 vs 二进制写入

先回答一下为什么语言喜欢做封装?C/C++

显示器显示12345是怎么显示的?是直接一个字符串12345,还是说12345一个一个字符显示?
答案是后者!所以显示器也叫做字符设备,我们都知道ACSII码,系统会将ASCII解释成不同的字符

我们试一下用write接口来对显示器输出一个整数:

#include <stdio.h>
#include <unistd.h>int main()
{int a = 12345;write(1, &a, sizeof(a));return 0;
}

在这里插入图片描述
为什么会输出90而不是12345啊?那是因为显示器只认识字符,当12345这个数转换为二进制的后,被识别成90了而已,所以我们要自己手动将12345转换为字符串的格式,但是这样也太麻烦了,于是系统就有了自己封装好的函数:
在这里插入图片描述
在这里插入图片描述
也叫做格式化输出,在输入上也是同理,是一个一个字符输入的,叫做输入格式化
这是方便用户操作!

有没有一种可能,当平台不再是Linux,而是Windows、Macos?这个时候如果我们只能使用系统调用接口,在这些平台上不就不能跑了吗?所以C语言封装了接口之后,不管什么平台,我们都只需要对封装好的了接口进行操作,不需要考虑底层的系统调用的接口,这就提高了语言的可移植性!

C语言封装好的库为什么能够消除平台的差异性?那是因为在库里是有源代码的,里面有很多不同平台下的代码,也就是说当你是Linux,就留下Linux版本的接口,其他的全部去掉

文本写入和二进制写入在系统层面是没有区别的,所谓的格式是用户层自己决定的,要形成可执行程序、视频音频……就写二进制,要形成文本文件,就写文本。

为什么C语言喜欢封装:

  • 方便用户操作
  • 提高了语言的可移植性

IO的基本过程----文件内核级缓冲区----重定向

在文件描述符表里的元素所指向的文件struct file(就比如说标准输出、标准输入、标准错误、打开的文件……),都会有操作表文件的内核级缓冲区,所谓的操作表其实就是上述所讲的函数指针的集合,因为不同的文件,输入输出方式不一定一样,文件的内核级缓冲区是什么?看如下代码write

write(stdout, "hello", ...);

里面的hello就是文件内核级缓冲区所存放的东西,进程在调用write的时候会从task_struct找到files_struct,再从files_struct里面文件描述符表找到对应的文件,然后根据操作表执行操作,例如write,系统就会把缓冲区的数据直接刷新到对应的文件中,这个刷新的行为是由操作系统自主决定的write其实就相当于把用户里的数据拷贝到缓冲区里头,也就是从用户拷贝到内核,也就是个拷贝函数

要注意:每个文件都有自己的操作表和缓冲区

  • 所以我们写Word文档要点保存呢?就是把缓冲区的数据刷新到磁盘里头,为什么不直接写入到磁盘里呢?那是因为磁盘的IO速度太慢了
  • 同理read只不过和write反过来了,要是缓冲区没有数据,就从磁盘中把数据刷新到缓冲区,再让read把缓冲区的数据拷贝到用户层
  • 所以writeread都是拷贝函数
  • 对于修改操作,就是把磁盘中的数据加载到缓冲区(内存),然后再进行修改----先读取,再写入
  • 缓冲区的存在就是为了提高IO效率,减少IO次数

fd的分配

对于文件描述符是怎么分配的,先看如下代码:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);return 0;
}

在这里插入图片描述
因为0、1、2已经被占用,所以这是正确的,但是要是我把0、1、2其中一个给关闭了呢?

close(0);

在这里插入图片描述
发现fd1变成0了,后面的fd也紧跟其后变成3、4、5,那要是我把2给关了呢?

close(2);

在这里插入图片描述

文件描述符在分配的时候会扫描整个文件描述符表,找到最小的并且没有被使用的文件描述符,分配给新打开的文件

但是假如我们把文件描述符1给关了呢?显示器就不会显示打印结果了,那是不是应该把打印结果写入到log1.txt里面了呢,毕竟把文件描述符1分配给他了,看结果:
在这里插入图片描述

我们vim log1.txt
在这里插入图片描述

解释为什么写进去了:当我们把1给关闭了,打开新的文件,那么文件描述符1就分配给了新打开的文件,但是printf这个函数默认把数据刷新给文件描述符1,此时的1正是新打开的文件,所以打印结果就会写入新打开的文件

果然写进去了,但是要是程序里把文件关闭了,还会打印吗?

close(fd1);
close(fd2);
close(fd3);
close(fd4);

在这里插入图片描述

我们vim log1.txt
在这里插入图片描述
依然是空白一片,也就是说根本没有把内容给写入进去,凭什么啊?我明明是先printf才关闭的文件,为什么内容没写入进去

别急,我们在关闭文件前,加个fflush看看,是不是恍然大悟:

fflush(stdout);

在这里插入图片描述
运行程序时找让没有打印结果,但是log1.txt里面确实是有内容了!为什么刷新了就会写入数据了呢?

  • fflush(stdout);实际上是刷新标准输出流stdout,使得缓冲区中的内容立即写入到实际的输出设备(通常是控制台或终端)。在 C 语言中,stdout是一个标准的输出流,通常是缓冲的。
  • fflush()是一个标准库函数,其作用是清空(刷新)输出流的缓冲区。stdout 是文件描述符为 1 的标准输出流,因此 fflush(stdout) 本质上就是刷新文件描述符为 1 的缓冲区。

也就是说并非没有写入数据,只是我们把数据写入到了标准输出的缓冲区里,但是并没有刷新到磁盘中就关闭了文件,当然看不到内容

在这里插入图片描述
这就是**重定向!!!**只不过这种做法有点不怎么优雅,我们来看看更优雅的做法:
在这里插入图片描述
其实我们并不需要把原先的文件描述符关闭,然后再重新分配,我么直接把新的文件的文件描述符所指向的文件的地址覆盖原先的文件描述符:
在这里插入图片描述
然后我们只需要把一开始新的文件描述符(也就是多余的)关闭,不就完成了重定向了吗:
在这里插入图片描述
那么问题来了,oldfdnewfd是什么?先看代码:

dup2(fd1, 1);

如果要实现我们上述的重定向,将1重定向到log1.txt,就应该这么写,这里的newfd实际上就是被覆盖的那个,因为是被覆盖了,所以就是新的,很好理解,那么oldfd就是保持不变的那个,因为不变,所以就是旧的

那么被覆盖的文件描述符所指向的文件是会自动关闭的,但是新的文件描述符并不会自动关闭,也就是说会有两个文件描述符指向同一个文件,这样可以的吗?答案是可以的。

struct file里是有一个引用计数来记录有多少个文件描述符指向该文件的,这也就意味着以后可以多个进程共享一个文件

当然你也可以自己关闭,我这里建议是不用关闭

我们来玩一下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);printf("fd : %d\n", fd);fprintf(stdout, "fd : %d\n", fd);fputs("hello\n", stdout);const char * buffer = "hello\n";fwrite(buffer, 1, strlen(buffer), stdout);return 0;
}

在这里插入图片描述
好,我们使用dup2

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);dup2(fd, 1);printf("fd : %d\n", fd);fprintf(stdout, "fd : %d\n", fd);fputs("hello\n", stdout);const char * buffer = "hello\n";fwrite(buffer, 1, strlen(buffer), stdout);return 0;
}

在这里插入图片描述
大家也可以试一下用O_APPEND的方式打开文件,追加重定向!

输入重定向,由于简单,直接给代码:
在这里插入图片描述
此时我的test.txt是存在数据的

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("test.txt", O_RDONLY);dup2(fd, 0);char buffer[2048];size_t s = read(0, buffer, sizeof(buffer));if(s > 0){buffer[s] = 0;printf("stdin redir:\n%s\n", buffer);}return 0;
}

现在弄人从显示器上读取就变成了从我的文件test.txt读取:
在这里插入图片描述

完善sehll

好了,现在我们可以实现重定向了,如果再加上命令行参数,我们把上一篇文章的shell给继续完善,使它支持重定向

以下代码是为了检测到是否重定向:

// 检测是否为空格
#define TrimSpace(pos) do{\while(isspace(*pos)){\pos++;\}\
}while(0)#define NoneRedir   0
#define InputRedir  1
#define OutputRedir 2
#define AppRedir    3int redir = NoneRedir;
char *filename = nullptr;void ParseCommandLine(char buffer[], int len) // 3. 分析命令
{(void)len; // 解除报警gargc = 0;// 重定向redir = NoneRedir;filename = nullptr;int end = len - 1;while(end >= 0){if(buffer[end] == '<'){redir = InputRedir;buffer[end] = 0;filename = &buffer[end] + 1;TrimSpace(filename);break;}else if(buffer[end] == '>'){if(buffer[end - 1] == '>')// 进一步判断是不是追加重定向{redir = AppRedir;buffer[end] = 0;buffer[end - 1] = 0;filename = &buffer[end] + 1;TrimSpace(filename);break;}else{redir = OutputRedir;buffer[end] = 0;filename = &buffer[end] + 1;TrimSpace(filename);break;}}end--;        }memset(gargv, 0, sizeof((char*)buffer)); // 为了保证安全,清空历史字符串const char *sep = " "; // 作为分割符gargv[gargc++] = strtok(buffer, sep);while(gargv[gargc++] = strtok(nullptr, sep));gargc--;
}

我们知道,重定向的工作都是子进程去做的,那么程序替换会不会影响重定向?
答案是:不会

虚拟内存mm_struct通过页表映射到物理内存,程序替换只是将物理内存进行覆盖,不会产生新的进程,必要时写时拷贝,然后修改页表,而我们的文件描述符表在file_struct里面,是自己的数据结构,所以在程序替换前所做的重定向不会受到影响!!

以下代码是执行重定向命令:

bool ExecuteCommand()   // 4. 执行命令
{pid_t id = fork(); // 让子进程去执行命令if(id < 0) return false;if(id == 0){if(redir == InputRedir){if(filename){int fd = open(filename, O_RDONLY, 0666);if(fd < 0){exit(2);}dup2(fd, 0);}else{exit(1);}}else if(redir == OutputRedir){if(filename){int fd = open(filename, O_TRUNC | O_CREAT | O_WRONLY, 0666);if(fd < 0){exit(4);}dup2(fd, 1);}else{exit(3);}}else if(redir == AppRedir){if(filename){int fd = open(filename, O_CREAT | O_APPEND | O_WRONLY, 0666);if(fd < 0){exit(6);}dup2(fd, 1);}else{exit(5);}}else{// do nothing}execvpe(gargv[0], gargv, genv);exit(0);// 不让子进程去跑后面的代码}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)){lastcode = WEXITSTATUS(status);}else{lastcode = 100;}return true;}return false;
}

回归上面的问题

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);//fflush(stdout);close(fd1);return 0;
}

在这里插入图片描述

为什么我们在使用printf()的时候我们不需要fflush就可以直接将数据从缓冲区刷新到磁盘中,但是fclose(stdout)之后就需要手动刷新?

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);fflush(stdout);close(fd1);return 0;
}

在这里插入图片描述
我们使用的printf、fputs、sacnf……都是C语言,底层都是系统调用,都是把数据拷贝到另一地方,但是系统调用是有成本的(时间或者空间)

我们自己写的函数都是有成本的,更别说实现系统调用的函数,成本只会更高,我们的内联、宏就是为了减少我们函数的调用,你看我们自己写的函数都要尽可能的减少调用,更别说系统调用了

再举例,我们的STL在扩容的时候是不是直接扩一点几倍,进而形成了内存池,实际上就是为了减少扩容的调用,减少成本

所以我们就有了一个用户级的缓冲区,用户级的缓冲区在哪呢?我们怎么没见过?

在我们的struct FILE里其实有缓冲区,可以理解为char buffer[NUM],当收集到足够多的数据的时候,就会直接通过系统调用将缓冲区的数据刷新到内核级缓冲区,相比于直接将数据一个一个刷新到内核级缓冲区,这样的效率是非常高的

  • 显示器文件:行刷新
  • 普通文件:写满刷新
  • 还有个不写入用户级缓冲区:直接调用系统函数

所以你明白了为什么我们在使用printf()的时候我们不需要fflush就可以直接将数据从缓冲区刷新到磁盘中,但是fclose(stdout)之后就需要手动刷新了吗?

其实就是我们将文件描述符1改成了普通文件,也就是说我们将数据写到了fd1里面去了,此时还没刷新到内核里面去,但我们调用了close(fd1),所以我们就看不到数据

但要是我们不使用close(fd1)

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);//fflush(stdout);//close(fd1);return 0;
}

在这里插入图片描述
照样能够刷新出来,那是因为每一个进程在退出的时候,都会自动刷新自己的缓冲区

要是我们将close换成fclose(stdout)
在这里插入图片描述
一样能够看到数据,那是因为fclose是C语言函数,是封装过的,在关闭文件前会刷新缓冲区

对于内核级缓冲区什么时候刷新,一般都是操作系统决定的,但是我们也可以强制刷新fsync函数:
在这里插入图片描述

你学会了吗?
好啦,本章对于《Linux:文件与fd(被打开的文件)》的学习就先到这里,如果有什么问题,还请指教指教,希望本篇文章能够对你有所帮助,我们下一篇见!!!

如你喜欢,点点赞就是对我的支持,感谢感谢!!!

请添加图片描述


http://www.ppmy.cn/news/1568482.html

相关文章

git:恢复纯版本库

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…

实验二 数据库的附加/分离、导入/导出与备份/还原

实验二 数据库的附加/分离、导入/导出与备份/还原 一、实验目的 1、理解备份的基本概念&#xff0c;掌握各种备份数据库的方法。 2、掌握如何从备份中还原数据库。 3、掌握数据库中各种数据的导入/导出。 4、掌握数据库的附加与分离&#xff0c;理解数据库的附加与分离的作用。…

JVM01_概述、跨平台原理、分类、三大商业虚拟机

①. 什么是JVM&#xff1f; ①. JVM 是 java虚拟机&#xff0c;是用来执行java字节码(二进制的形式)的虚拟计算机 ②. jvm是运行在操作系统之上的&#xff0c;与硬件没有任何关系 ②. Java的跨平台及原理 ①. 跨平台&#xff1a;由Java编写的程序可以在不同的操作系统上运行&am…

ASP.NET Core WebAPI的异步及返回值

目录 Action方法的异步 Action方法参数 捕捉URL占位符 捕捉QueryString的值 JSON报文体 其他方式 Action方法的异步 Action方法既可以同步也可以异步。异步Action方法的名字一般不需要以Async结尾。Web API中Action方法的返回值如果是普通数据类型&#xff0c;那么返回值…

Java Web-Tomcat Servlet

Web服务器-Tomcat Web服务器简介 Web 服务器是一种软件程序&#xff0c;它主要用于在网络上接收和处理客户端&#xff08;如浏览器&#xff09;发送的 HTTP 请求&#xff0c;并返回相应的网页内容或数据。以下是关于 Web 服务器的详细介绍&#xff1a; 功能 接收请求&#…

《解锁DeepSeek本地部署:开启你的专属AI之旅》

一、DeepSeek 的魅力与本地部署的意义 在人工智能的璀璨星空中&#xff0c;DeepSeek 宛如一颗耀眼的新星&#xff0c;自问世以来便吸引了无数目光。它是由中国人工智能初创企业深度求索推出的大模型&#xff0c;凭借着一系列卓越的技术创新和强大的功能表现&#xff0c;在全球 …

解锁 Python 与 MySQL 交互密码:全方位技术解析与实战攻略

目录 一、引言 二、环境准备 2.1 安装 MySQL 2.2 安装 Python 及相关库 2.2.1 使用 mysql - connector - python 2.2.2 使用 pymysql 三、基本连接与操作 3.1 连接到 MySQL 数据库 3.2 创建游标对象 3.3 执行 SQL 查询 3.3.1 查询单条记录 3.3.2 查询多条记录 3.4…

网络爬虫学习:应用selenium获取Edge浏览器版本号,自动下载对应版本msedgedriver,确保Edge浏览器顺利打开。

一、前言 我从24年11月份开始学习网络爬虫应用开发&#xff0c;经过2个来月的努力&#xff0c;于1月下旬完成了开发一款网络爬虫软件的学习目标。这里对本次学习及应用开发进行一下回顾总结。 前几天我已经发了一篇日志&#xff08;网络爬虫学习&#xff1a;应用selenium从搜…