【Linux】基础 IO(文件描述符)-- 详解(上)

简介: 【Linux】基础 IO(文件描述符)-- 详解(上)

一、前言

1、文件的宏观理解

文件在哪呢?

从广义上理解,键盘、显示器、网卡、声卡、显卡、磁盘等几乎所有的外设都可以称之为文件,因为 “Linux 下,一切皆文件”。

从狭义上的理解,文件在磁盘(硬件)上放着,只有操作系统才能真正的去访问磁盘。磁盘是一种永久存储介质,不会受断电的影响,磁盘也是外设之一,所以对文件的所有操作都是对外设的输入输出,简称 IO(Input、Output)。


2、文件的操作范畴

当我们在 Windows 下新建一个文本文件,它是否占用磁盘空间?

虽然它是一个空的文本文件,并且这里显示是 0KB,但它依旧会占用磁盘空间,因为一个文件新建出来之后,它的很多数据信息都需要维护,其中包括文件名、修改日期、类型、大小、权限等。

当我们对空文本写入字符时,可以直观的看到文本的大小发生了变化。

所以说一个文件 = 文件内容 + 属性(元数据),也就是说我们要学习的所有的文件操作无外乎就是对文件的内容或内容进行操作。

  • 如果想往文件里写入 "hello world"、'a',这叫作对文件内容进行操作; fread、fwrite、fgets、fputs、fgetc、fputc 都是对文件内容进行操作。
  • 如果想更改文件权限、拥有者所属组、文件名等,这叫作对文件属性进行操作;fseek、ftell、rewind 都是对文件属性进行操作。

3、系统看待文件访问

以前写的 fread、fwrite 等相关代码访问文件的 C 程序 ➡ 经过编译形成 .exe(可执行程序) ➡ 双击或 ./ 运行程序,把程序加载到内存中。所以对文件的访问本质上就是进程对文件的访问

我们在操作文件时所使用到的接口,例如 fread、fwrite,这是 C 语言给我们提供的接口,而要操作的文件是在磁盘这个硬件上,同时我们很明确磁盘的管理者是操作系统,普通用户不可能直接去访问硬件,在计算机体系结构中用户是通过不同语言所提供的这些被封装过的接口来贯穿式的访问硬件(用户 ➡ 库函数 ➡ 系统调用接口 ➡ 操作系统 ➡ 驱动程序 ➡ 硬件)。导致了不同语言有不同的语言级别的文件访问接口,但封装的都是系统接口,所以本质上并不是 C 语言帮我们把数据写到磁盘文件中,真正干活的是操作系统所提供的与文件相关的系统调用接口


为什么要学习 OS 层面上的文件接口呢?

因为这样的接口只有一套(OS 只有一个)。


如果语言不提供对文件系统接口的封装,那是不是所有访问文件的操作都必须直接使用 OS 的接口?

是。


选择使用语言的用户要不要访问文件呢?

要。一旦使用系统接口编写文件代码,那么这份代码就无法在其它平台直接运行了,因为其不具备跨平台性。


显示器是硬件吗?printf 向显示器打印也是一种写入,为什么不觉得奇怪呢?

是的。 因为我们往显示器上打印时随即能够看到结果,而往文件里写入时并不直观,具有滞后性。其实这与磁盘写入到文件里没有本质区别。


4、如何理解 “Linux 下,一切皆文件”

默认情况下,标准输入是键盘文件,标准输出是显示器文件,标准错误是显示器文件。

而这三个本身是硬件,如何理解 “Linux 中,一切皆文件”?

  • 所有的外设硬件本质对应的核心操作无外乎是 read / write。对于键盘文件,它的读方法就是从键盘读取数据到内存;它的写方法设置为空,因为没有把数据写到键盘上这种说法。
  • 对于显示器文件,如调用 printf 函数和 cout 时,操作系统是要往显示器上写入的,本质就是一种 write。理论上来说,操作系统是不会往显示器上读数据的,所以设置为空吗?不对,我们通过键盘在命令行输入 ls 命令显示在 Xshell 上,系统要执行命令时不就是应该往显示器上读数据然后再执行吗?如果是往显示器上读,那么我们在输入密码时,密码是不显示的,系统也能往显示器上读吗?其实我们输入的命令是通过键盘输入的,所以系统应该是往键盘读数据至于用户能看到输入的命令,仅仅是为了方便用户,操作系统把从键盘输入的数据一份给了系统进行读取,一份给了显示器方便用户查看。
  • 所以不同的硬件对应的读写方式肯定是不一样的,但是它们都有自己的 read 和 write 方法。也就是说,这里的硬件可以统一看作成一种特殊的文件。比如这里设计一种结构:struct file,它包括文件的属性、文件的操作或方法等,Linux 说一切皆文件,Linux 操作系统就必须要能够保证这点。在 C 语言中,怎么让一个结构体既有属性又有方法呢?函数指针。此时每一个硬件都对应这样一个结构,硬件一旦数量很多,操作系统就需要对它们进行管理 —— 先描述,再组织。所谓的描述就是 struct file;而组织就是要把每一个硬件对应的结构体关联起来,并用 file header 指向。所以在操作系统的角度,它所看到的就是一切皆文件,也就是说所有硬件的差异经过描述就变成了同一种东西,只不过当具体访问某种设备时,使用函数指针执行不同的方法达到了不同的行为。现在就能理解为什么可以把键盘、显示器这些设备当作文件,因为本质不同,设备的读写方法是有差异的,但我们可以通过函数指针让不同的硬件包括普通文件在操作系统看来是同样的方法、同样的文件。


二、简单回顾 C 文件接口及相关文件操作

1、fopen

#include <stdio.h>
 
FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);  

(1)打开文件的方式
  r      Open text file for reading. 
         The stream is positioned at the beginning of the file.
 
  r+     Open for reading and writing.
         The stream is positioned at the beginning of the file.
 
  w      Truncate(缩短) file to zero length or create text file for writing.
         The stream is positioned at the beginning of the file.
 
  w+     Open for reading and writing.
         The file is created if it does not exist, otherwise it is truncated.
         The stream is positioned at the beginning of the file.
  a      Open for appending (writing at end of file). 
         The file is created if it does not exist. 
         The stream is positioned at the end of the file.
  a+     Open for reading and appending (writing at end of file).
         The file is created if it does not exist. The initial file position
         for reading is at the beginning of the file, 
         but output is always appended to the end of the file.

2、写文件

虽然没有 ./ 指定路径,但它还是在当前路径下新建文件了。因为每个进程都有一个内置的属性 cwd(可以在 /proc 目录下查找对应进程的属性信息),cwd 可以让进程知道自己当前所处的路径。这也就解释了在 VS 中不指明路径,它也能新建对应的文件在对应的路径的原因。所有,进程在哪个路径运行,新建的文件就在哪个路径。


什么叫做当前路径?

当一个进程运行起来时,每个进程都会记录自己当前所处的工作路径。



(1)❌错误写法:fwrite(msg, strlen(msg)+1, 1, fp);

strlen(msg)+1 会导致乱码,也就是把 '\0' 也追加造成的,因为 '\0' 结尾是 C 语言的规定,和文件无关,文件不用遵守。

         

这里 cat myfile.txt 并没有看到乱码的原因是:'\0' 是不可见的,所以在这里只有 vim myfile.txt 才会看到乱码。

size_t fwrite (const void* ptr, size_t size, size_t count, FILE* stream);

  • size 表示你要写入的基本单元是多大(以字节为单位),count 表示你要写入几个这样的基本单元。换而言之,最终往文件中写的字节数 = size*count。比如要写入 10 个字节,那么 size=1 && count=10、size=2 && count=5,不过一般建议把 size 写大一点,count 写小一点。
  • fwrite 的返回值是成功写入元素的个数,也就是期望写 count 个,每个是 size,那么实际最后返回的就是你实际写入成功了几组 size。比如你期望 size 是 10,count 是 1,那么大部分情况下都会把这 1 个单元都写入的,写入成功且返回值是 1;这里的你期望写入多少和实际写入多少就好比下面这个例子:

小明:爸,我想要 10 块钱。(这是期望的)

爸:我只有 5 块钱,给你 3 块钱吧。(这是实际的)

当然,这里是后面网络部分才会涉及到,目前在往磁盘文件中写入时,大部分情况下硬件设备是能够满足我们的要求的,所以这里不关心 fwrite 的返回值。


(2)现象

注意:fopen 以 "w" 方式打开文件,默认先将文件内容清空(在 fwrite 之前)。

tips:可以把 > 看成一个命令,向文件写入内容,直接输入命令:>log.txt 的话就相当于先打开文件清空。


3、读文件

perror 函数可以打印出错误信息。



(1)写一个 cat 命令


4、追加文件

注意:fopen 以 "a" 方式打开文件,表示直接往文件末尾追加,不断地向文件中写入新增内容。


5、输出信息到显示器的方法

#include <stdio.h>
#include <string.h>
 
int main()
{
    const char *msg = "hello fwrite\n";
    fwrite(msg, strlen(msg), 1, stdout);
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    return 0;
}

6、三个标准输入输出流:stdin & stdout & stderr

  • 这里需要理清一个概念,不是任何 C 程序运行会默认打开,而是进程在启动时会默认打开三个 “文件”,分别是 stdinstdoutstderr
  • 仔细观察发现这三个流的类型都是 FILE*与 fopen 返回值类型相同,都是文件指针

这里可以直接使用 fwrite 这样的接口,向显示器写数据的原因是因为 C 进程一运行,stdout 就默认打开了。同理 fread 能从键盘读数据的原因是 C 进程一运行,stdin 就默认打开了。

也就是说 C 接口除了对普通文件进行读写之外(需要手动打开),还可以对 stdin、stdout、stderr 进行读写(不需要手动打开)。


为什么 C 进程运行就会默认打开 stdin、stdout、stderr?

scanf -> 键盘、printf -> 显示器、perror -> 显示器

如果不默认打开,那么我们是不能直接调用这些接口的,所以默认打开的原因是便于我们直接上手,且大部分编码都会有输入输出的需求。也就是说,scanf、printf、perror 这样的库函数,底层一定使用了 stdin、stdout、stderr 文件指针来完成对应不同的功能。此外还有一些接口和 printf、scanf 很像,它们本身是把使用的过程暴露出来,比如 fprintf(stdout, “%d, %d, %c\n”, 9, 17, a)。


仅仅只是 C 这样吗?

这里可以肯定的是,不仅是 C 进程运行会打开 stdin、stdout、stderr,其它语言几乎都是这样的,C++ 是 cin、cout、cerr。

所以我们可以发现一个现象,不管是学习什么语言,第一个程序永远是 "Hello World!"。这里说几乎所有语言都这样也就意味着不仅仅是语言层提供的功能了。比如一条人山人海的路从头到尾只有个别商贩在摆摊,那么我们认为这是商贩的个人行为,当地的管理者是排斥这种行为的;但如果一整路从头到尾都有商贩在摆摊,那么我们就认为是当地的管理者支持这种行为的。同样,不同语言彼此之间并没有进行过任何商量,而最终都会默认打开,所以这不仅仅是语言支持,也一定是操作系统支持的。


三、系统文件 I/O

1、为什么要学习文件系统接口

在 C 语言中要访问硬件必须贯穿计算机体系结构,而 fopen、fclose 等系列的库函数,其底层都要调用系统接口,这里它们对应的系统接口也很好记忆 —— 去掉 "f" 即为系统接口。不同语言所提供的接口本质上是对系统接口进行封装,学习封装的接口本质上学的就是语言级别的接口。也就是要学习不同的语言,就得学会不同语言操作文件的方法,但实际上对特定的操作系统最终都会调用系统所提供的接口。


接下来学习系统接口,我们要学习它的原因主要有两点:

  1. 只要学懂了系统接口,后面再学习其它语言上的文件操作,就只是学习它的语法,底层不需要再学习了。
  2. 这些系统接口更贴近于系统,所以我们就能通过接口来学习文件的若干特性。

【Linux】基础 IO(文件描述符)-- 详解(下)https://developer.aliyun.com/article/1515565?spm=a2c6h.13148508.setting.28.11104f0e63xoTy

相关文章
|
1月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
89 0
|
1月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
75 1
Linux C/C++之IO多路复用(aio)
|
3月前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
1月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
23 0
Linux C/C++之IO多路复用(poll,epoll)
|
3月前
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
148 3
|
2月前
|
Unix Linux
linux中在进程之间传递文件描述符的实现方式
linux中在进程之间传递文件描述符的实现方式
|
3月前
|
运维 Rust 监控
Linux高效运维必备:fd命令深度解析,文件描述符管理从此得心应手!
【8月更文挑战第23天】本文介绍了一款名为fd的命令行工具,该工具基于Rust语言开发,旨在以更直观的语法和更快的速度替代传统的`find`命令。通过本文,您可以了解到如何安装fd以及一些基本用法示例,比如使用正则表达式匹配文件名、排除特定目录等。此外,文章还展示了如何结合`ps`和`lsof`命令来查找特定文件并显示其文件描述符,从而帮助您更好地管理和监控Linux系统中的文件与进程。
128 0
|
3月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
32 0
|
3月前
|
存储 IDE Linux
Linux源码阅读笔记14-IO体系结构与访问设备
Linux源码阅读笔记14-IO体系结构与访问设备
|
4月前
|
Linux 数据处理 C语言
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
76 0