【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区


🍁你好,我是 RO-BERRY
📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识
🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油

在这里插入图片描述


目录

  • 1.文件描述符fd
  • 2.文件描述符的分配规则
  • 3.重定向
    • 模拟实现<(输入重定向)
    • 模拟实现>(将命令的输出结果重定向到一个文件中)
    • 模拟实现>>(将命令的输出结果追加到一个文件中)
    • 使用 dup2 系统调用
      • 重定向stdout
      • 重定向stdin
  • 4.缓冲区
  • 5. FILE


1.文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
在这里插入图片描述
现在我们再来理解,什么才叫是一切皆文件呢?

我们在底层当中一定会有许多硬件,如:键盘、显示器、磁盘、网卡等。在我们眼里,这每一个硬件都是一个单独的个体,这些设备对应的操作方法一定是不一样的

  1. 对键盘:
    读:read_keyboard();
    写:write_keyboard();

  2. 对显示器:
    读:read_screen(); (空的)
    写:write_screen();
    我们是无法从显示器上读到数据的,所以对于显示器读为空

  3. 对磁盘:
    读:read_disk();
    写:write_disk();

我们每一个文件都会对应一个文件结构体便于存储

struct file
{
	int type;
	int mode;
	int pos;
	int flag;
	........
	//函数指针--方法集
	size_t(*read)(xxxx);    //读方法
	size_t(*write)(xxx);    //写方法
	struct file *next;      //下一个文件的指针
	....
}

文件对每一个键盘都会开一个文件结构体与其对应的硬件相链接

1. 文件对键盘:
读:size_t(*read)(xxxx); ---指向---> read_keyboard();
写:size_t(*write)(xxx); ---指向---> write_keyboard();

2. 文件对显示器:
读:size_t(*read)(xxxx); ---指向---> read_screen();  (空的)
写:size_t(*write)(xxx); ---指向---> write_screen();

3. 文件对磁盘:
读:size_t(*read)(xxxx); ---指向---> read_disk();
写:size_t(*write)(xxx); ---指向---> write_disk();

在这里插入图片描述

我们读键盘,就会调用键盘对应的函数接口,其他的同理,在上层我们只需要对文件函数指针进行调用就可以,因为有函数指针的存在,对于上层用户就可以认为一切皆指针,对我们来说硬件的差异已经被文件结构体屏蔽掉了。

这就相当于我们使用C语言实现了面向对象,对于不同的对象实现不同的功能,其函数指针也就相当于我们C++的多态调用!


2.文件描述符的分配规则

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

  return 0;
}

在这里插入图片描述
输出是fd:3,原因上一节也讲过,这是因为012默认打开了

  • 我们将0标准输入关掉会发生什么?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
  close(0);   //将0标准输入关掉
  int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
  if(fd == -1)
  {
    perror("open");
    return 1;
  }
  printf("fd: %d\n",fd);

  return 0;
}

在这里插入图片描述

发现是结果是: fd: 0

可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

3.重定向

我们关掉标准输出1

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

在这里插入图片描述

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log.txt(myfile)当中,其中,fd=1。

这种现象叫做输出重定向。常见的重定向有:>, >>, <


那重定向的本质是什么呢?
在这里插入图片描述

模拟实现<(输入重定向)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
  close(0);
  open("log.txt",O_RDONLY);   //0
  int a = 0;
  scanf("%d",&a);    //scanf认定的是标准输入stdin -> _fileno = 0
  printf("%d\n",a);

  return 0;
}

在这里插入图片描述

在这里就是将下标为0的文件描述符指针指向了我们的文件log.txt,这样就实现了我们输入重定向,直接从文件里面读的数据,因为scanf并没有从我们的键盘读入数据

模拟实现>(将命令的输出结果重定向到一个文件中)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
  close(1);
  open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);   //0
  printf("hello printf\n");
  fprintf(stdout,"hello fprintf\n");
  
  return 0;
}

在这里插入图片描述
我们就实现了直接对文件进行写入

在这里就是将下标为1的文件描述符指针指向了我们的文件log.txt,这样就实现了我们输出重定向

模拟实现>>(将命令的输出结果追加到一个文件中)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
  close(1);
  open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); 
  printf("hello printf\n");
  fprintf(stdout,"hello fprintf\n");
  
  return 0;
}

在这里插入图片描述

使用 dup2 系统调用

我们上面几个方式还是太复杂了,接下来我们来感受一个新的函数–dup2函数

在Linux中,dup2函数是用于复制文件描述符的函数。它的原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);
  • 该函数的作用是将oldfd指向的文件描述符复制到newfd指向的文件描述符。如果newfd已经打开,则会先关闭newfd指向的文件描述符,然后再复制oldfd。

  • dup2函数的返回值为新的文件描述符,如果复制成功,则返回newfd;如果出错,则返回-1,并设置errno来指示错误类型。

  • 使用dup2函数可以实现重定向标准输入、输出和错误输出。例如,可以将标准输出重定向到一个文件中,或者将标准错误输出重定向到一个套接字中。

重定向stdout

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); 
  dup2(fd, 1);
  printf("hello hahahaha\n");
  return 0;
}

在这里插入图片描述
要想使用重定向,使用dup2函数就可以了

其实我们使用的printf/scanf/fprintf/fscanf/sscanf/sprintf....这些是只认stdin/stdout的,也就是说只认文件描述符为0/1

重定向stdin

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd = open("log.txt",O_RDONLY); 
  dup2(fd, 0);
  char buffer[1024];
  while(1)
  {
    char* s =fgets(buffer, sizeof(buffer), stdin);
    if(s == NULL) break;
    printf("file content: %s", buffer);
  }
  return 0;
}

在这里插入图片描述

我们将stdin指向了文件log.txt,于是我们从log.txt中读取数据


4.缓冲区

缓冲区它就是一块内存区

为什么要有缓冲区?

比如说这里有你的宿舍和你朋友的宿舍,你们不在同一个城市,相隔较远,但是你朋友今天过生日,你给他买了一个键盘,你就抱着键盘,坐火车去你朋友的城市,再去到他的学校门口,最后喊他出来拿生日礼物。这个方式是不是效率太低了?

  • 我们其实可以通过寄快递的方式,将你的礼物送给你的朋友,你也只需要出宿舍进到快递站,这个快递站就相当于我们的缓冲区,缓冲区的存在意义就在于提高我们使用者的效率。因为有快递站的存在,我们就可以把东西给他就可以实现目的,同样的,我们只需要将数据拷贝到缓冲区就可以了;我们将东西送到快递站之后,快递站也不是一收到你的东西他就会立马给你送,它会收到很多东西后,一起进行发送,同样的,我们的缓冲区也会聚集数据,一次拷贝,提供整体效率,有了缓冲区,就可以减少我们拷贝的次数,缓冲区的主要目的就是提高效率

从技术角度来说,缓冲区的本质就是一块内存区域,其提高效率的本质就是使用空间换时间。

  • 我们平时所用的缓冲区,和操作系统内核本身没有任何关系(尽管他有),我们这个缓冲区是语言层面的缓冲区,对于我们遇到的就可以解释为C语言会自带缓冲区
  • 进程的pcb指向自己的文件描述表,文件描述表指向我们的文件结构体,文件结构体里有指针指向我们的文件缓冲区,再由文件缓冲区会将我们的数据刷新到磁盘里。
  • 调用系统调用是有成本的,时间&&空间,例如:创建一个进程是需要fork的,在系统里要对其申请一大堆东西,这是需要大量时间空间的,所以系统会提前申请好一大堆空间,我们需要用的时候直接用。
  • C语言在它自己的语言层定义了一层缓冲区,我们写数据是将数据写到C语言的缓冲区里,再由它调用系统调用帮我们写入内核。
  • 为什么C语言要维护这么一个缓冲区?
    提供C语言的缓冲区可以让我们在调用fwrite的函数调用系统调用的过程中减少我们对系统调用的次数,我们将一次调用的结果拷贝到缓冲区,之后每次调用就可以直接调用,不用再重复进行系统调用了,系统调用是需要成本的,通过缓冲区可以整体提高我们的拷贝效率,直接提高C接口的使用效率!

缓冲区是如何刷新数据的?

应用层缓冲区刷新策略

  1. 无刷新,无缓冲
  2. 行刷新 — 显示器 — xxx\nyyy 将\n之前的数据xxx给你刷新出去
  3. 全刷新,全部刷新 — 普通文件,我们访问普通文件会将缓冲区写满再刷新
  4. 用户强制刷新
  5. 进程退出时自动刷新

内核缓冲区是由操作系统自主决定的

5. FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd。

缓冲区具体在哪里?

我们的stdin,stdout,stderr,fp都是FILE*的文件,每一个文件都对应一个缓冲区,缓冲区是在FILE结构体中维护的。所以我们平时使用的fwrite和fputs,的参数都有FILE*的参数,我们输入的字符串都会在FILE*内部的缓冲区进行拷贝,我们调用十次百次都会在其中进行调用,就不需要重复调用系统调用,提高效率

size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fputs(const char *s, FILE *stream);

来段代码研究一下:

#include <stdio.h>
#include <string.h>
int main()
{
 const char *msg0="hello printf\n";
 const char *msg1="hello fwrite\n";
 const char *msg2="hello write\n";
 printf("%s", msg0);
 fwrite(msg1, strlen(msg0), 1, stdout);
 write(1, msg2, strlen(msg2));
 fork();
 return 0;
}

在这里插入图片描述
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
在这里插入图片描述
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲

综上:

  1. printf fwrite库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
  2. 那这个缓冲区谁提供呢?printf fwrite是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供

如果有兴趣,可以看看FILE结构体

typedef struct _IO_FILE FILE;/usr/include/stdio.h

struct _IO_FILE {
	int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
	//缓冲区相关
	/* The following pointers correspond to the C++ streambuf protocol. */
	/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
	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; //封装的文件描述符
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
	/* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];
	/* char* _save_gptr; char* _save_egptr; */
	_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

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

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

相关文章

【网络安全技术】——密码技术基础与身份认证技术(学习笔记)

&#x1f4d6; 前言&#xff1a;加密技术是目前网络安全的基础。数据加密技术是指对在网络中所发送的明文消息用加密密钥加密成密文进行传送&#xff0c;接收方用解密密钥进行解密再现明文消息&#xff0c;从而保证传输过程中密文信息即使被泄漏&#xff0c;在无密钥的情况下仍…

基于单片机三路信号故障诊断仿真设计

单片机设计介绍&#xff0c;基于单片机三路信号故障诊断仿真设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机三路信号故障诊断仿真设计概要主要涵盖了系统设计的整体框架、关键模块功能、仿真方法以及预期实现的目…

2D与动画

2D转换 1.移动 translate 1. 语法 transform: translate(x,y); 或者分开写 transform: translateX(n); transform: translateY(n); 2.重点 定义 2D 转换中的移动&#xff0c;沿着 X 和 Y 轴移动元素 translate最大的优点&#xff1a;不会影响到其他元素的位置 translat…

HarmonyOS 应用开发之UIAbility组件间交互(设备内)

UIAbility是系统调度的最小单元。在设备内的功能模块之间跳转时&#xff0c;会涉及到启动特定的UIAbility&#xff0c;该UIAbility可以是应用内的其他UIAbility&#xff0c;也可以是其他应用的UIAbility&#xff08;例如启动三方支付UIAbility&#xff09;。 本文将从如下场景…

HarmonyOS实战开发-实现Ability内页面间的跳转和数据传递。

介绍 本篇Codelab基于Stage模型下的Ability开发&#xff0c;实现Ability内页面间的跳转和数据传递。 最终效果图如下&#xff1a; 相关概念 页面路由&#xff1a;提供通过不同的url访问不同的页面&#xff0c;包括跳转到应用内的指定页面、用应用内的某个页面替换当前页面、…

数据分析之Power BI

POWER QUERY 获取清洗 POWER PIVOT建模分析 如何加载power pivot 文件-选项-加载项-com加载项-转到 POWER VIEW 可视呈现 如何加载power view 文件-选项-自定义功能区-不在功能区中的命令-新建组-power view-添加-确定 POWER MAP可视地图

HTTP(1)

目录 一、认识HTTP协议 理解 应用层协议 二、fiddler的安装以及介绍 1、fiddler的安装 2、fiddler的介绍 三、HTTP 报文格式 1、http的请求 2、http的响应 五、认识URL 六、关于URL encode 一、认识HTTP协议 HTTP 全称为&#xff1a;“超文本传输协议”&#xff0c;是…

【01-20】计算机网络基础知识(非常详细)从零基础入门到精通,看完这一篇就够了

【01-20】计算机网络基础知识&#xff08;非常详细&#xff09;从零基础入门到精通&#xff0c;看完这一篇就够了 以下是本文参考的资料 欢迎大家查收原版 本版本仅作个人笔记使用1、OSI 的七层模型分别是&#xff1f;各自的功能是什么&#xff1f;2、说一下一次完整的HTTP请求…

Mybatis-plus + 通用mapper(tk.mybatis)

推荐课程&#xff1a;MyBatisPlus实战教程02-课程介绍与案例演示_哔哩哔哩_bilibili 官网&#xff1a;MyBatis-Plus (baomidou.com) 目录 01 引言 1&#xff09;MyBatis与MyBatis-Plus区别 2&#xff09;Mybatis-plus入门案例 案例一&#xff1a;spring容器版本的案例 案例…

pip永久修改镜像地址

修改命令&#xff1a; pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ 效果&#xff1a; 会在C:\Users\PC(用户名)\AppData\Roaming\pip目录下新增或修改文件pip.ini 文件内容&#xff1a; [global] index-url https://pypi.tuna.tsinghua.e…

通过keil MDK生成静态库以减少编译时间

当我们的程序复杂度提高,代码量增大,每次编译都会花费更多的时间,虽然相比较Linux动则好几个小时的编译时间,单片机编译的时间已经算很短了,但是一个200多KB的程序编译也得需要好几分钟。如果将一些成熟固定,几乎不会再去修改的程序编译成静态库,那么可以减少一部分编译…

HarmonyOS实战开发-实现UIAbility内和UIAbility间页面的跳转

介绍 本篇Codelab基于Stage模型下的UIAbility开发&#xff0c;实现UIAbility内和UIAbility间页面的跳转。包含如下功能&#xff1a; UIAbility内页面的跳转。跳转到指定UIAbility的首页。跳转到指定UIAbility的指定页面&#xff08;非首页&#xff09;。 最终效果图如下&…

新数据显示,寻求博士后职位的美国科学家越来越少

戈罗登科夫/盖蒂图片社 美国国家科学基金会&#xff08;NSF&#xff09;发布的新数据显示&#xff0c;美国公民从事博士后工作的人数急剧下降&#xff0c;特别是在生物和生物医学科学领域。这一趋势凸显了人们对学术界正面临博士后短缺的担忧&#xff0c;以及早期职业科学家越来…

PPP、RRE、MGRE综合实验

一、实验拓扑图 二、实验要求 1.R5为ISP&#xff0c;只能进行IP地址配置&#xff0c;其所有地址均配为公有IP地址&#xff1b; 2.R1和R5间使用PPP的PAP认证&#xff0c;R5为主认证方: R2与R5之间使用ppp的CHAP认证&#xff0c; R5为主认证方;R3与R5之间使用HDLC封装; 3.R1、R2、…

Python:执行py命令,提示: Can‘t find a default Python.

1.Python运行环境罢工 今天&#xff0c;要运行一个前年用python编写的爬虫程序&#xff0c;先检测python运行环境是否正常&#xff1a; D:\Python38-32\works>c:\windows\py.exe Cant find a default Python. 再试&#xff1a; D:\Python38-32\works>py --list Installe…

Keepalived+MySQL简单搭建实现数据库高可用

需求&#xff1a;想要实现当MySQL服务挂了之后&#xff0c;能够自动切换到另一台&#xff0c;不对当前服务造成过多影响。查找了很多实现数据库高可用方案&#xff0c;比较常见的有MHA&#xff08;至少三台&#xff0c;一主多从&#xff09;、开源数据库中间件&#xff08;Myca…

浅试Kimi

最近KIMI大模型挺火的&#xff0c;擅长处理中文文本&#xff0c;咱也来试试吧&#xff01; 测试问题&#xff1a; 写一篇800字以上的短片小说&#xff1a;主要故事是以一位上进但其他方面表现平平的大男孩小贱&#xff0c;刚到公司不久&#xff0c;就被一位名叫大弟的女同事看…

量化交易入门(二十八)什么是布林带,量化中怎么使用

什么叫布林带 布林带&#xff08;Bollinger Bands&#xff09;是一种常用的技术分析指标&#xff0c;由约翰布林&#xff08;John Bollinger&#xff09;于20世纪80年代开发。它由三条线组成&#xff1a;中轨&#xff08;通常为20日移动平均线&#xff09;、上轨&#xff08;中…

python编程软件有什么

Python开发软件可根据其用途不同分为两种&#xff0c;一种是Python代码编辑器&#xff0c;一种是Python集成开发工具&#xff0c;两者的配合使用可以极大的提高Python开发人员的编程效率&#xff0c;以下是常用的几款Python代码编辑器和Python集成开发工具。 一、Python代码编…

嵌入式linux学习之交叉编译器安装

交叉编译器介绍 ARM 裸机、Uboot 移植、Linux 移植这些都需要在 Ubuntu 下进行编译&#xff0c;编译就需要编译器&#xff0c;在 Liux 进行 C 语言开发里面使用 GCC 编译器进行代码编译&#xff0c;但是 Ubuntu 自带的 gcc 编译器是针对 X86 架构的&#xff01;而我们现在要编…
最新文章