【Linux学习】文件描述符重定向缓冲区

目录

九.文件描述符

        9.1 文件描述符概念

        9.2 文件描述符的分配规则

        9.3 重定向

        9.3.1 常见的重定向操作

        9.3.2 重定向的原理

        9.4 缓冲区

        9.4.1 缓冲区概念

        9.4.2 缓冲区刷新策略

        9.4.3 C语言的缓冲区在哪里?


九.文件描述符

        9.1 文件描述符概念

在上一篇讲到基础IO时,我们说到open函数在打开一个文件后有一个int类型的返回值.我们将之称为文件描述符. 那么到底什么是文件描述符?它有什么作用呢?

概念:

文件描述符(File Descriptor,简称fd),是一个用于标识和操作文件或输入/输出设备的整数。在Unix-like操作系统中,包括Linux,文件描述符是对打开文件或I/O设备的引用.每个进程都有一个文件描述符表,它是一个数组,其中包含了该进程打开的文件或I/O设备的引用

也就是说:每一个进程都会维护一个独立的文件描述符表(File Descriptor Table)用来管理自己打开的文件

这个文件描述符表存储在哪里?

这里我们可以结合之前的知识猜到,文件描述表应该存储在进程的PCB结构中.

事实上,也确实如此.如上图所示,在进程对应的的task_struct结构中会存在一个files指针指向指向一个files_struct结构体

每个进程都会有单独维护的files_struct结构体,其专门用于跟踪对应进程与文件相关的信息.

files_struct有两个地方来管理所有打开的文件结构,即有两个数组来管理所有打开的文件结构

  • files_struct结构体中会包含一个struct fdtable *fdt,其指向fdtable(文件描述符表),fdtable同样也是一个结构体,其中包含** struct file fd,这个数组存储了每个文件描述符对应的文件指针
  • files_struct结构体中同样直接包含了一个fd_array用来直接存储了每个文件描述符对应的文件指针

在这两个数组中都储存着指向file结构体的指针,也就是说进程打开的文件在内核中会被描述为struct file结构体的实例,这个结构体包含了有关打开文件的各种信息,包括文件的状态、位置、访问模式等

总的来说,通过这种方式,内核能够更灵活地管理文件,而进程则通过文件描述符引用这些文件实例,而不必直接关心底层的文件结构和操作.这提供了一种高层次的抽象,简化了用户空间程序对文件的操作

在这里我们可以通过一步一步讲解fwrite()的运行过程,自上而下的梳理一遍

当我们在调用了fwrite()时,首先需要接收一个FILE *指针,表示要写入的文件的FILE结构,而对应的FILE结构体中存有文件描述符(fd),这个时候fwrite()会底层调用write(fd,.....),来执行操作系统内部的write(),它能找到进程的task_struct,再通过里面的files指针再找到files_struct,而files_struct中的fd_array[fd]便指向打开的文件的file结构体,这时候便内存文件便找到了,也就可以后续操作.

注意:

  • 文件的fd实际上是fd_array数组的对应下标
  • C语言中的FILE结构体和内核中的file结构体是两个不同的概念,需要区分

        9.2 文件描述符的分配规则

在上文讲到基础IO时,我们讲到C语言的程序会默认打开三个流,分别是stdin,stdout,stderr.

这时候结合Linux系统的设计哲学:一切皆文件,我们可以知道,在C语言编写的程序运行后,会默认打开对应的三个文件

这时候,我们可以编写下面这个简单的程序来验证.

也就是说fd_array数组当中下标为0、1、2的位置已经被占用了,之后打开的文件都是重3之后开始

那么如果我们在进行文件操作之前将默认打开的文件关掉会怎么样呢?

这里我们将下标为1的文件先关掉进行验证,给程序先加上这样一句代码:

close(0);

这时候我们再此运行观察

这时我们可以得出 文件描述符的分配规则:

顺序分配: 一般情况下,文件描述符会按照顺序分配,即从最小的未使用的整数开始分配

        9.3 重定向

9.3.1 常见的重定向操作

重定向是指将一个文件描述符与另一个文件描述符或文件关联起来,从而改变输入或输出的方向。在 Unix-like 操作系统中,包括 Linux,重定向是一种强大的机制,允许在命令行中灵活地管理输入和输出。

以下是常见的重定向操作符:

1.>:输出重定向

        示例:command > output.txt

        描述:将命令的标准输出重定向到文件 output.txt 中。如果文件不存在,则会创建文件;如果文件已存在,将会覆盖其中的内容。

2.>>:追加输出重定向

        示例:command >> output.txt

        描述:将命令的标准输出追加到文件 output.txt 中。如果文件不存在,则会创建文件;如果文件已存在,内容将会被追加到文件末尾。

3.<:输入重定向

        示例:command < input.txt

        描述:将文件 input.txt 中的内容作为命令的标准输入。命令将读取文件的内容而不是从键盘读取。

4.|:管道

        示例:command1 | command2

        描述:将 command1 的标准输出通过管道传递给 command2 的标准输入。这使得两个命令可以协作处理数据。

5.2>:错误输出重定向

        示例:command 2> error.txt

        描述:将命令的错误输出重定向到文件 error.txt 中。类似于 >,但是针对错误输出。

6.&>&>>:合并输出和错误输出重定向

        示例:command &> output_and_error.txtcommand &>> output_and_error.txt

        描述:将命令的标准输出和错误输出合并,并重定向到文件 output_and_error.txt 中。

9.3.2 重定向的原理

重定向的本质是通过操作系统提供的文件描述符机制,动态地改变进程的输入和输出源

dup2:

 #include<unistd.h>

 int dup2(int oldfd, int newfd);

函数返的作用:dup2的作用是将 newfd 指向的文件描述符关闭,然后将 oldfd 复制到 newfd,使得它们指向同一个打开的文件、套接字或管道。如果 newfd已经打开,dup2 会首先关闭它

这个系统调用的主要应用场景之一是在重定向中,将一个文件描述符重定向到另一个文件描述符。例如,在 shell 命令中,使用 > 这样的操作符进行输出重定向时,实际上就是使用了dup2

函数返回值: dup2如果调用成功,返回newfd,否则返回-1。

使用dup2时,我们需要注意以下两点:

  • 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
  • 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。

借助dup2(),我们可以试着模拟一下重定向的过程

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
	int fd = open("log.txt", O_CREAT | O_RDWR,0666);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	close(1);
	dup2(fd, 1);
	for (;;) {
		char buf[1024] = { 0 };
		ssize_t read_size = read(0, buf, sizeof(buf) - 1);
		if (read_size < 0) {
			perror("read");
			break;
		}
		printf("%s", buf);
		fflush(stdout);
	}
	return 0;
}

通过这样一个简单的程序我们便可以将标准输出(文件描述符1)重定向到一个文件("log.txt"),并不断地从标准输入(文件描述符0)读取数据,然后将其写入到文件中.

        9.4 缓冲区

9.4.1 缓冲区概念

缓冲区(Buffer)是计算机系统中常见的概念,它是一块用于临时存储数据的内存区域。缓冲区在各种计算机应用中都起着关键作用,从文件输入输出到网络通信,都涉及到对数据的缓冲和管理

下面是缓冲区的常见作用:

  1. 提高效率: 缓冲区的存在可以减少对底层资源的频繁访问,从而提高数据传输的效率。通过按块读写数据,而不是逐个字节进行操作,可以减少读写次数,降低系统开销

  2. 平滑数据流: 缓冲区可以平滑数据流,使得数据以块的形式传输。这对于文件输入输出、网络通信等场景特别有用,有助于减少频繁的小规模数据传输,提高整体性能

  3. 优化磁盘和网络访问: 在文件系统中,文件缓冲区可以优化磁盘访问,减少磁盘I/O的次数。在网络通信中,网络缓冲区可以优化数据在网络上传输的效率

  4. 应对不同速度的设备: 缓冲区可以协调不同速度的设备之间的数据传输,确保数据能够以适当的速率流动,避免了生产者和消费者之间的速度不匹配问题

  5. 提高用户体验: 输入缓冲区允许用户按块输入数据,提高了用户体验。输出缓冲区允许程序按块输出数据,减少对屏幕或文件的频繁写入,提高了响应速度

  6. 支持流式处理: 缓冲区使得数据能够以流式的方式进行处理,而不是一次性处理整个数据集。这对于处理大数据或实时数据流非常重要

  7. 适应不同工作负载: 缓冲区的存在使得系统能够更好地适应不同的工作负载。它可以根据需要动态调整缓冲策略,以满足不同场景下的性能需求

  8. 错误处理: 缓冲区可以提供一定程度的错误处理机制。例如,在网络通信中,如果无法立即发送所有数据,缓冲区可以保存部分数据,等待合适的时机重新发送

9.4.2 缓冲区刷新策略

C语言中一般有以下几种缓冲区刷新策略:

行缓冲(Line Buffering):在行缓冲模式下,缓冲区在遇到换行符 \n 时自动刷新。也就是说,当遇到换行符时,缓冲区中的数据会被立即写入文件

全缓冲(Fully Buffered):在全缓冲模式下,缓冲区满时会触发刷新,此时缓冲区中的数据才会被写入文件

手动刷新缓冲区:使用 fflush() 函数手动刷新缓冲区。这对于确保数据在特定时刻被写入文件很有用

关闭文件时刷新:当文件关闭时,C库会自动刷新缓冲区


一般而言,行缓冲的设备文件 --- 显示器

全缓冲的设备文件 --- 磁盘文件

极端情况,你是可以自定义规则的 --- 这时候我们可以自己使用fflush()来手动刷新

但所有的设备,永远倾向于全缓冲 --> 缓冲区满了再刷新 --> 需要更少次数的IO操作 -->更少次数的外设访问(相当于提高了整机效率).

有同学可能有疑问,比如10行数据,每一行有100个字节,虽然10行最后再一起刷新,只进行了一次的外设访问,但是数据量很多啊,1000个字节,而按行刷新虽然刷新了10次,但每次数据量少啊,那为什么外设访问次数越少越好呢?

这是因为和外部设备IO的时候,数据量的大小不是主要矛盾,你和外设预备IO的过程是最耗费时间的.


9.4.3 C语言的缓冲区在哪里?

缓冲区在哪里?我们所说的这个缓冲区是由操作系统维护还是语言维护?

为了回答这个问题,我们可以先感受下面这段代码

#include<stdio.h>  
#include<fcntl.h>  
#include<sys/stat.h>  
#include<sys/types.h>  
#include<unistd.h>  
#include<string.h>
int main()
{
    //C语言提供的接口    
    printf("hello,printf\n");
    fprintf(stdout, "hello,fprintf\n");
    const char* s = "hello,fputs\n";
    fputs(s, stdout);

    //系统接口    
    const char* ss = "hello,write\n";
    write(1, ss, strlen(s));
    //创建子进程
    fork();
}

当我们把代码编译运行后:

此时当我们将程序的输出 重定向到另外的文件时

 这时候我们可以先预测一下输出的结果

最后我们会惊讶的发现一个现象:所有C语言提供的函数都输出了两次 .系统接口的只输出了一次

这时候我们改一下我们原本的代码,在write()与fork()之间加上一句fflush()

这时候再进行一次上述操作

这次我们发现结果又发生了变化,这是为什么呢?


这时候我们得回到最初的问题:缓冲区是由操作系统维护还是语言维护?

这时大家也许会猜到 我们所说的这个 缓冲区其实是由C语言自己维护的

在源代码中,你可以找到stdio.h 头文件以及其中包含的FILE结构体的定义,而FILE结构体里面不仅封装了fd,而且包含了该文件对应的语言层的缓冲区结构_IO_FILE。

_IO_FILE的内部结构大致如下:

/* In glibc, FILE is a typedef, defined in FILE struct and defined
   elsewhere.  */
struct _IO_FILE {
  int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */
  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;   /* Current read pointer */
  char *_IO_read_end;   /* End of get area. */
  char *_IO_read_base;  /* Start of putback+get area. */
  char *_IO_write_base; /* Start of put area. */
  char *_IO_write_ptr;  /* Current put pointer. */
  char *_IO_write_end;  /* End of put area. */
  char *_IO_buf_base;   /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;/* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;

  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
  __off64_t _offset;

  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

回到我们的最初的代码

我们直接运行程序是向显示器中打印,采用的是行刷新策略,而我们重定向到文件中,向文件中打印,便成了全缓冲策略.

1.如果是向显示器中打印,那么采用的是行刷新策略,那么最后执行fork的时候,所有的数据都已经刷新完成了,此时再执行fork就没有意义了.

2.如果对程序进行了重定向,即此时要向文件中打印,此时刷新策略便隐式的变成了全缓冲

遇到\n换行符便不会再触发刷新.

fork的时候,一定是函数已经执行完了,但是数据还没有刷新! 这些数据在当前进程对应的C标准库中的缓冲区里. 而这些数据是父进程的.

fork之后子进程和父进程执行return 0前,父进程和子进程实际上是共享相同的缓冲区的.但是,当子进程执行 return 0 后,由于任一方进程退出时触发了关闭文件时刷新策略,此时可能触发了对标准输出缓冲区的刷新.这时,写时复制拷贝机制会起作用,确保子进程得到自己的缓冲区副本,从而避免对父进程缓冲区的影响。这样就有了两份数据,然后分别输出到文件中。

所以就出现了C语言标准库输出的函数打印了两次,而系统接口打印了一次。

因为系统接口是直接写入到了文件中,而不用经过缓冲区。


而在我们更改之后的代码中,fflush已经强制将缓冲区的内容刷新了出来,此时缓冲区已经是空的了,然后再执行fork,父子进程缓冲区都是空的,所以也没有数据刷新了,这样才各自只打印了一条语句

此时,更加让我们确信了一个事实:缓冲区一定不是操作系统提供的!而是由C语言标准库提供的!因为如果是操作系统提供的,那么这个系统接口也应该输出两次,而不是只有一次

注意:

缓冲区的概念并不是特定于 C 语言,而是一种广泛应用于计算机科学和编程的概念。缓冲区是一块用于临时存储数据的内存区域,其目的是提高数据传输的效率。在不同的编程语言和操作系统中,都存在类似的概念

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/205024.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java项目学生管理系统二查询所有

学生管理 近年来&#xff0c;Java作为一门广泛应用于后端开发的编程语言&#xff0c;具备了广泛的应用领域和丰富的开发资源。在前几天的博客中&#xff0c;我们探讨了如何搭建前后端环境&#xff0c;为接下来的开发工作打下了坚实的基础。今天&#xff0c;我们将进一步扩展我…

10.0 输入输出 I/O

IO操作主要是指使用Java程序完成输入&#xff08;Input&#xff09;、输出&#xff08;Output&#xff09;操作。所谓输入是指将文件内容以数据流的形式读取到内存中&#xff0c;输出是指通过Java程序将内存中的数据写入到文件中&#xff0c;输入、输出操作在实际开发中应用较为…

Rust UI开发(5):iced中如何进行页面布局(pick_list的使用)?(串口调试助手)

注&#xff1a;此文适合于对rust有一些了解的朋友 iced是一个跨平台的GUI库&#xff0c;用于为rust语言程序构建UI界面。 这是一个系列博文&#xff0c;本文是第五篇&#xff0c;前四篇链接&#xff1a; 1、Rust UI开发&#xff08;一&#xff09;&#xff1a;使用iced构建UI时…

Springboot+vue的客户关系管理系统(有报告),Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的客户关系管理系统&#xff08;有报告&#xff09;&#xff0c;Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的客户关系管理系统&#xff0c;采用M&#xff08…

C++学习之继承中修改成员权限细节

看看下面的代码 这是错误的 class A { public:int x 10; }; class B :public A {using A::x;int x 100; };看看函数 class A { public:void fun(){cout << "uuuu" << endl;} }; class B :public A { public:using A::fun;void fun(){cout << …

C++基础——文件操作

文章目录 1 概述2 文本文件2.1 写文件2.1.1 写文件流程2.1.2 文件打开方式 2.2 读文件 3 二进制文件3.1 写文件3.2 读文件 1 概述 程序最基本的操作之一就是文件操作&#xff0c;程序运行时的数据都是临时数据&#xff0c;当程序结束后就不复存在了。通常都是通过文件或其他持…

2000-2021年各省人口密度数据

2000-2021年各省人口密度数据 1、时间&#xff1a;2000-2021年 2、指标&#xff1a;地区、年份、年末常住总人口(万人&#xff09;、面积&#xff08;平方千米&#xff09;、人口密度&#xff08;人/平方千米&#xff09; 3、来源&#xff1a;各省年鉴、统计年鉴、各省统计局…

File类

File 概述 File: 路径 IO流: 传输 路径 相对路径, 绝对路径 File File对象就表示一个路径&#xff0c;可以是文件的路径、也可以是文件夹的路径这个路径可以是存在的&#xff0c;也允许是不存在的 构造方法 代码示例: package FileTest1;import java.io.File;public c…

Verilog 入门(四)(门电平模型化)

文章目录 内置基本门多输入门简单示例 内置基本门 Verilog HDL 中提供下列内置基本门&#xff1a; 多输入门 and&#xff0c;nand&#xff0c;or&#xff0c;nor&#xff0c;xor&#xff0c;xnor 多输出门 buf&#xff0c;not 三态门上拉、下拉电阻MOS 开关双向开关 门级逻辑…

CAN总线学习(STM32的CAN寄存器使用)(笔记二)

CAN总线基础基础知识的文章&#xff1a;CAN总线学习&#xff08;CAN总线基础知识&#xff09;&#xff08;笔记一&#xff09;-CSDN博客 在前面CAN总线基础知识和报文中介绍的是报文内容是比较全面的&#xff0c;STM32在CAN协议的基础上做了一些简单的简化&#xff0c;例如下图…

C++ 抽象类和接口 详解

目录 0 引言1 抽象类2 接口2.1 Java与C接口的区别 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;C专栏&#x1f4a5; 标题&#xff1a;C 抽象类和接口 详解❣️ 寄语&#xff1a;书到用时方恨少&#xff0c;事非经过不知难&#xff01;&#x1f…

Java数据结构之《顺序查找》问题

一、前言&#xff1a; 这是怀化学院的&#xff1a;Java数据结构中的一道难度中等偏下的一道编程题(此方法为博主自己研究&#xff0c;问题基本解决&#xff0c;若有bug欢迎下方评论提出意见&#xff0c;我会第一时间改进代码&#xff0c;谢谢&#xff01;) 后面其他编程题只要我…

【python】当当书籍数据抓取分析与可视化(代码+报告)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

06_MySQL数据库高可用解决方案(MHA)

各位道友好&#xff0c;鼠鼠我呀校招刚通过了移动的面试 &#xff0c;但是安排的岗位是偏远县城里面的岗位&#xff0c;各位能给给建议吗&#xff1f;鼠鼠我啊真不想有时候变成销售员去卖产品&#xff01;&#xff01;&#xff01; 任务背景 一、真实案例 公司现在需要做MySQ…

什么是木马

木马 1. 定义2. 木马的特征3. 木马攻击流程4. 常见木马类型5. 如何防御木马 1. 定义 木马一名来源于古希腊特洛伊战争中著名的“木马计”&#xff0c;指可以非法控制计算机&#xff0c;或在他人计算机中从事秘密活动的恶意软件。 木马通过伪装成正常软件被下载到用户主机&…

全面探索模拟浏览器的Python爬虫工具

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com Python爬虫是获取网页信息的重要工具&#xff0c;但有时网站对爬虫有限制&#xff0c;要求模拟浏览器行为。本文将深入探讨如何使用Python模拟浏览器行为进行网络数据抓取。我们将介绍相关工具和技术&#xff0c…

docker读取字体异常

解决方法 docker容器中执行 apk add ttf-freefont 根据版本不同 apk add ttf-dejavu-fonts apk add ttf-bernoulli

使用Docker安装Jenkins,解决插件安装失败,版本太低等问题

如果已经遇到插件安装部分失败&#xff0c;Jenkins版本太低&#xff0c;又要换什么清华镜像地址&#xff0c;不要犹豫&#xff0c;直接以下步骤卸载重装就好了 开始安装 yum 更新到最新 yum update到Jenkins官网查找最新的LST版本 最后的版本号一定要带&#xff0c;指定下载具…

Java最难的语法<泛型>

时间过得很快&#xff0c;我们马上就进入了&#xff0c;Java最难语法的学习&#xff0c;加油吧&#xff01; 1.包装类 想要学好泛型就要了解包装类。 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;Java给每个…

vue使用el-select el-option失效 不显示名称 还是显示原数据

<el-form-item label"生效标记" prop"enableSign" label-width"17rem"><el-select v-model"dialog.elForm.enableSign" placeholder"请选择内容"><el-option v-for"item in enableSignList":key&q…