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

这篇具有很好参考价值的文章主要介绍了【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器
🍁你好,我是 RO-BERRY
📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识
🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器



1.文件描述符fd

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

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

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

  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();

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

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

这就相当于我们使用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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器
输出是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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

发现是结果是: 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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

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

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


那重定向的本质是什么呢?
【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

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

#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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

在这里就是将下标为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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器
我们就实现了直接对文件进行写入

在这里就是将下标为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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

使用 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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器
要想使用重定向,使用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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器

我们将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;
}

【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区,linux学习,linux,运维,服务器
我们发现 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结构体文章来源地址https://www.toymoban.com/news/detail-849384.html

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
};

到了这里,关于【linux深入剖析】文件描述符 | 对比 fd 和 FILE | 缓冲区的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向

    作者:დ旧言~ 座右铭:松树千年终是朽,槿花一日自为荣。 目标:了解在Linux下的系统文件IO,知道什么是文件描述符,什么是重定向 毒鸡汤:白日莫闲过,青春不再来。 专栏选自:Linux初阶 望小伙伴们点赞👍收藏✨加关注哟💕💕 最早我们在C语言中学习关于如何用代码

    2024年04月14日
    浏览(51)
  • 【看表情包学Linux】文件描述符 | 重定向 Redirection | dup2 函数 | 缓冲区的理解 (Cache)

       🤣  爆笑 教程  👉 《看表情包学Linux》👈   猛戳订阅     🔥 💭 写在前面: 在上一章中,我们已经把 fd 的基本原理搞清楚了。本章我们将开始探索 fd 的应用特征,探索 文件描述符的分配原则。讲解重定向,上一章是如何使用 fflush 把内容变出来的,介绍 dup2 函数,

    2023年04月25日
    浏览(53)
  • 【Linux】深入理解文件缓冲区

    问题引入 首先看一段代码: 运行代码,结果如下: 如果此时将输出结果重定向一下: 会发现 printf 、 fwrite 都打印了两次。 究其原因,就要谈到缓冲区和缓冲区刷新策略的概念了。 如何理解缓冲区 假设你在青岛,你要从网上买一件商品,商家所在地是北京。你不会跑去北

    2024年02月11日
    浏览(55)
  • Linux文件系列: 深入理解缓冲区和C标准库的简单模拟实现

    至此,我们理解了缓冲区的概念和作用,下面我们来简易模拟实现一下C标准库 我们要实现的是: 1.文件结构体的定义 1.首先要有一个文件结构体: 刷新策略分别宏定义为 2.myfopen等等函数的声明 path:文件路径+文件名 mode:打开文件的方式 “r”:只读 “w”:覆盖写 “a”:追加写 strea

    2024年03月11日
    浏览(65)
  • 系统文件IO、文件描述符fd、重定向、文件系统、动态库和静态库

    C文件接口 C文件接口都是封装了系统的文件接口,学习系统的文件接口有利于更熟悉文件的操作。 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: 要打开或创建的目

    2024年02月07日
    浏览(41)
  • FD_SET设置的文件描述符超过1024引发coredump

    在开发过程中,遇到一个coredump的问题,最后排查到是FD_SET的文件描述符大于1023 2、开始执行 这种问题就更坑人了,并不是只要超过1023就会必现,到1200就快复现了 3、gdb调试 这里还好,最起码gdb报的行数是在36,在FD_SET这行,在自己的开发环境就没这么好了 3、内核里面 FD

    2024年02月13日
    浏览(52)
  • 【Linux】基础IO —— 缓冲区深度剖析

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

    2024年02月02日
    浏览(53)
  • 【Linux】模拟实现FILE以及认识缓冲区

    刷新缓冲逻辑图 自定义实现 如何强制刷新内核缓冲区 根据文件描述符进行强制刷新 例子 像我们进行scanf输入的时候,其实本身我们输入的是一串字符串,将这个字符串读入对应的缓冲区buff后,然后通过分解工作,进一步传入系统,系统,系统在通过一些指令输入输出想要

    2024年02月10日
    浏览(42)
  • 【linux深入剖析】深入理解基础外设--磁盘以及理解文件系统

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 我们所有的文件都是与进程相关的文件–进程打开的文件 系统中是不是所有的文件都被打开了呢?如果没

    2024年04月11日
    浏览(44)
  • 【Linux】深入理解缓冲区

    目录 什么是缓冲区 为什么要有缓冲区 缓冲区刷新策略 缓冲区在哪里  手动设计一个用户层缓冲区 缓冲区本质上一块内存区域,用来保存临时数据。 缓冲区在各种计算任务中都广泛应用,包括输入/输出操作、网络通信、图像处理、音频处理等。 这块内存区域是由 谁提供的

    2024年02月15日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包