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

简介: 【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)

【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)   https://developer.aliyun.com/article/1565663



💫 write & read

采用 man write 指令查看相关资料

#include <unistd.h>
 
ssize_t write(int fd, const void *buf, size_t count);
参数:
    buf: 用户缓冲区
    count: 期望写的字节数
返回值:实际写入的字节数

举个栗子:

#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,0644);
  if(fd < 0)
  {
    printf("open error\n");
    return 1;
  }
  const char* msg = "more then words\n";
  int cnt = 5;
  while(cnt--)
  {
    write(fd,msg,strlen(msg));
  }
  close(fd);
  return 0; 
}


运行结果:



问题拓展:

注意小细节,写入文件的过程中,不需要写入\0!因为\0是C语言层面上规定字符串的结束标志,而写入文件关心的是字符串的内容,不需要\0标定字符串结束。


采用 man read 指令查看相关资料

#include <unistd.h>
 
ssize_t read(int fd, void *buf, size_t count);
参数:
    buf: 读到的内容放在用户层缓冲区中,也就是自己定义缓冲区
    count: 期望读多少个字节
返回值:实际读多少个字节


举个栗子:

#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);
  if(fd < 0)
  {
    printf("open");
    return 1;
  }
  
  char buffer[1024];
  ssize_t s = read(fd,buffer,sizeof(buffer)-1);
  if(s > 0)
  {
    buffer[s] = 0;
    printf("%s",buffer);
  }
  close(fd);
  return 0; 
}

运行结果:



问题拓展:

我们把读到的内容当做一个长字符串处理,写入时不写\0,读也就不会读到,因此需要在末尾添加\0,以字符串打印出来。


🌙 文件描述符fd

问题提出:

open函数的返回值是所谓的文件描述符,既然类型为int,我就好奇它的值是多少呢?



再次分析:

如果我们连续打开若干文件,会发现打印3456… 我们知道打开文件失败返回-1,那么012去哪了呢?012消失的原因,要么是不让用,要么是被别人占用。

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



总结分析:

事实上,当我们的程序运行起来变成进程,默认情况下,OS会帮助我们打开三个标准输入输出,012其实分别对应的就是标准输入、标准输出、标准错误。刚刚我们还提到语言上的stdin标准输入、stdout标准输出、stderr标准错误,对应硬件设备也是键盘、显示器、显示器,冥冥之中,这一定是有关联的,不过我们暂时先不考虑语言和系统上如何对应。


这样文件描述符被分配为01234678… 这样从0开始,连续的小整数,会让我们联想到数组下标!


验证:012代表标准输入、标准输出、标准错误



💫 file descriptor

文字描述:

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


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

即磁盘文件 = 文件内容 + 文件属性。事实上,我们之前所学的所有文件操作都可以分为两类:对文件内容的操作 + 对文件属性的操作(fseek、ftell、rewind、chmod、chgrp等等).


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


操作系统中存在大量进程,进程可以打开多个文件,即进程 : 文件 = 1 : n ,系统中可能存在着更多的打开的文件(暂时不考虑一个文件被多个进程打开的特殊情况)。那么,OS要不要把打开的文件在内存中(系统中)管理起来呢?那么就要上管理的六字真言:先描述,再组织!


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


图解:



分析:

那么现在就能解释了为什么打开文件返回的是3:新打开一个文件本质是内核会为我们描述struct file结构,再把struct file地址填入到fd_array[]数组下标去,因为012已经被占用了,于是填到3号下标,因此在上层可以拿到3.


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


总结:

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


💫 理解一切皆文件

对于键盘显示器等等这些外设,一定都有比如像read、write读写方法,因为由冯诺依曼体系结构知,外设是要和内存打交道IO的。这可能有些奇怪,比如键盘能读我知道,但能写吗?难道我键盘安安静静的自己就开始动了?!注意,我们有统一的读写方法,但不代表非要每一个都实现,比如键盘就可以没有写方法,即方法为空。



因为它们的硬件结构不同,这些方法在底层实现是完全不一样的!这些方法都是在硬件的驱动层完成的。那又是如何做到一切皆文件的呢?Linux中做了软件的虚拟层vfs(虚拟文件系统),会统一维护每一个打开文件的结构体struct file.


回忆C++中的多态,我们可以编写一个父类(甚至是纯虚的,相当于定义一个接口类),子类继承父类,重写函数。我们让父类指针指向不同的子类对象,就会调用对应的方法。那么在C语言中,可以通过函数指针,做到调用同一个方法,指向不同对象时可以执行不同的方法,从而实现多态的性质。


我们在每个struct file当中包含上一大坨的函数指针,这样,在struct file上层看来所有的文件都是调用统一的接口;在底层我们通过函数指针指向不同硬件的方法。



同样在继承体系中,我甚至也不关心你到底是那个子类,比如,动物基类Animal被猫狗鸡鸭鹅都继承了,里面有一个eat方法,基类指针指向猫就调用猫的eat,基类指针指向狗就调用狗的eat… 这样看去我们就实现了“一切皆动物”,可以理解为C++的多态是漫长的软件开发摸索中实现**“一切皆…”**的高级版本/语言版本。


💫 文件描述符的分配规则

代码分析:


.


问题分析:

我把0关掉后,再打开文件是分配的文件描述符就是0,把1关掉分配的就是1


文件描述符的分配规则

每次给新文件分配的fd,是从fd_array[]中找一个最小的、未被使用的作为新的fd.这其实很好理解,打开的文件要和进程产生关联,就要线性遍历数组中找一个未被使用的下标,填入文件地址。


🌙 重定向

💫 输出重定向

问题抛出:

有没有细心的同学,上面我们唯独没有关闭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_APPEND选项。


💫 输入重定向

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

char *fgets(char *s, int size, FILE *stream); //详见1.1节


💫 dup2

分析:

如上我通过关闭文件然后再打开文件这样重定向,但是情况不会总是这样理想。

比如两个文件描述符13都已经被打开,如何实现重定向呢?我们勇敢的推测,既然在语言层调用时接口函数只认1,那么只需要把文件描述符表的3中的内容拷贝到1中 ,就实现了原本应向显示器文件写入,而现在向log.txt写入。


图解:



总结:

dup2就是用来做这个操作的。

#include <unistd.h>
 
int dup2(int oldfd, int newfd); //oldfd->newfd
 
dup2() 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.


拷贝的是fd对应内容,最终相当于全部变成old.


🌙 Linux一切皆文件

在冯诺依曼体系中,我们知道硬件有键盘、显示器、磁盘、网卡等外设,在IO过程中,外设任何的数据处理都需要把数据读到内存,处理完毕之后将内存中的数据刷新到外设当中。因为软硬件资源多,所以操作系统需要对其先描述,在组织。所以这些外设都有对应的结构体,对应着属性信息,同时,对应着自己的IO函数,具体硬件的读写方法都在应用匹配的驱动程序里。每种硬件的访问方法都是不一样的,而Linux一切皆文件是这样体现的:任何一个被打开的文件结构体对象struct file{ //各种文件的属性 }对象,不同的文件对应的读写方法不一样,struct file对象里面可以有很多的(*readp)()、(*writep)()函数指针,通过函数指针指向具体的读写方法。


站在struct file上层看来,所有的设备和文件,统一都是struct file->,就可以调用具体的设备方法了,所以在用户级看到的就是Linux下一切皆文件!


**上层调用不同的文件,底层可以调用不同的方法,在上层看来,只需要使用对应统一的文件,使用struct file,访问不同的文件,这是C语言实现多态的特征。这里struct file称为在操作系统层面上虚拟出来的文件对象vfs(虚拟文件系统)**不用关心底层差别,统一使用文件的接口方式进行文件操作


🌟结束语 

      今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。


目录
相关文章
|
2月前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
13天前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
29天前
|
Linux C语言
C语言 文件IO (系统调用)
本文介绍了Linux系统调用中的文件I/O操作,包括文件描述符、`open`、`read`、`write`、`lseek`、`close`、`dup`、`dup2`等函数,以及如何获取文件属性信息(`stat`)、用户信息(`getpwuid`)和组信息(`getgrgid`)。此外还介绍了目录操作函数如`opendir`、`readdir`、`rewinddir`和`closedir`,并提供了相关示例代码。系统调用直接与内核交互,没有缓冲机制,效率相对较低,但实时性更高。
|
2月前
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
67 3
|
2月前
|
存储 监控 Linux
性能分析之从 IO 高定位到具体文件
【8月更文挑战第21天】性能分析之从 IO 高定位到具体文件
30 0
性能分析之从 IO 高定位到具体文件
|
2月前
IO流拷贝文件的几种方式
IO流拷贝文件的几种方式
29 1
|
1月前
crash —— 获取系统的磁盘IO统计数据
crash —— 获取系统的磁盘IO统计数据
|
2月前
|
运维 Rust 监控
Linux高效运维必备:fd命令深度解析,文件描述符管理从此得心应手!
【8月更文挑战第23天】本文介绍了一款名为fd的命令行工具,该工具基于Rust语言开发,旨在以更直观的语法和更快的速度替代传统的`find`命令。通过本文,您可以了解到如何安装fd以及一些基本用法示例,比如使用正则表达式匹配文件名、排除特定目录等。此外,文章还展示了如何结合`ps`和`lsof`命令来查找特定文件并显示其文件描述符,从而帮助您更好地管理和监控Linux系统中的文件与进程。
78 0
|
2月前
|
存储 Linux 数据处理
在Linux中,管道(pipe)和重定向(redirection)的是什么?
在Linux中,管道(pipe)和重定向(redirection)的是什么?
|
2月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
25 0
下一篇
无影云桌面