当前位置: 首页 > news >正文

Linux操作系统之文件(二):重定向

目录

前言:

一、理解一切皆文件

二、为什么语言喜欢封装

1、方便用户使用

2、可移植性

三、重定向

1、缓冲区

2、重定向

3、shell里面的重定向

总结:


前言:

我们上篇文章点此跳转末尾提到了文件描述符,本篇文章将会为大家补充文件描述符的有关内容,更加深刻的理解"Linux下一切皆文件"。最后,将会为大家介绍内核缓冲区与重定向的有关内容

一、理解一切皆文件

⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东
西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了文件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是文件。

我们可能很难抽象的去想象,为什么键盘,显示器这些外部设备,能被理解为文件。

但是这样做,有个最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例子,Linux 中几乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进行;几乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。

我们需要知道,操作系统要描述这些硬件,也是需要先描述再组织的。所以,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体:
 

struct file{

        ...

        struct inode  *f_inode;

        const struct file_operations  *f_op;

        ...

        atomic_long_t  f_count;//引用计数

        usigned int f_flags;

        fmode_t f_mode;

        ...

}__attribute__((aligned(4)));

而在每一个struct file结构体对象里,都存在一个操作表。这个操作表,实际上就是一个函数指针集合,里面蕴含着这个结构体对象的各种封装的函数调用。

比如前面说的显示器,键盘这些外设,我们说他是文件,就是因为他们都具有一个struct file结构体。这个结构体就类比于人类,键盘和显示器,就类比为人类中的小明和小红个体。比如我说键盘,他就会具有一个独属于键盘的struct file结构体,然后在这个结构体对象里,存在一个操作表(函数指针集合),这个函数指针集合里面通过指针存放着各种的已经被封装好的函数,这些函数,底层又调用的时read,write,open这样的系统调用接口。使得我们从struct file的角度上看,我们是统一调度的读和写。

struct file 做了一层软件的封装,使得所以一切皆文件:

我(进程)只要通过PCB找到文件表示符表,通过一个一个的文件标识符,找到struct file对象,我就能调用读写,完成对底层硬件的访问。这叫做操作系统的VFS虚拟文件系统。

于是操作系统,就这样,可以通过封装好的同名函数,对不同的外设进行操作。上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过对操作表的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。

这个放在C++里,就是多态的应用。


二、为什么语言喜欢封装

1、方便用户使用

首先我们需要知道文本写入与二进制写入的区别。

计算机系统中,所有数据的本质都是二进制形式存储和传输的,但人类与计算机交互时需要更直观的表达方式,这就产生了文本和二进制两种数据表示形式的差异。

操作系统提供的系统调用(如Linux的read与wirte)本质上只能处理原始的二进制数据流。例如

// Linux系统调用:只能读写二进制字节流
ssize_t write(int fd, const void *buf, size_t count);

#include<stdio.h>

#include<unistd.h>

int main()

{

    int a= 12345;

    write(1,&a,sizeof(a));

    return 0;

}

这个代码的运行结果为:90!

不管90代表什么含义,我们只想知道,为什么它打印出来的不是12345呢? 

根本原因在于数据表示方式的错位write()系统调用直接输出了整数的原始二进制字节,而终端却将这些字节误解为ASCII字符在显示屏上进行显示。

显示屏是一个字符设备,所以会默认把这些当做ASCII码值,因此会按字节解析:

  • 0x39 → ASCII字符 '9'

  • 0x30 → ASCII字符 '0'

  • 0x00 → ASCII空字符(终端通常忽略或显示为空格)

  • 0x00 → 同上

因此终端会依次显示:'9''0',然后遇到空字符可能停止输出。

但是我们的printf("%d",a);却可以直接打印出12345,这又是为什么呢?

试看这样的打印结果:

int main()

{

    int a= 12345;

    // write(1,&a,sizeof(a));

    printf("%c\n%d\n",a,a);

    return 0;

}

输出结果为:

%c打印就与write的打印结果有什么区别呢)?

因为它们都涉及直接解释内存中的二进制数据而不像 %d 那样进行格式化(所谓格式化,就是:内存级别的二进制数据转化为字符串风格)转换。 

printf 做了以下操作:

  1. 截断 int 为 char

    • %c 只取 a 的最低 1 字节(因为 char 是 1 字节)。

    • 如果 a = 123450x00003039),取最低字节 0x39(即 ASCII '9')。

  2. 直接输出该字节对应的 ASCII 字符,不会进行数字到字符串的转换。

无论如何,系统调用都不能直接打印整数 。

而是需要做一定的变换:

int main()

{

    int a= 12345;

    // write(1,&a,sizeof(a));

    char buffer[20];

    int len = sprintf(buffer, "%d", a);  // 整数转字符串

    write(1, buffer, len);               // 输出字符串

    return 0;

}

所以才会有封装:我们需要一个格式化的过程:把内存级别的二进制数据转化为字符串风格在打印在显示器上。只有这样做,才能方便用户的使用。

同样的道理,我们在键盘中输入1234,是输入的整数1234呢?还是一个一个的字符。

肯定是字符!不是我们想要的整数,所以有scanf格式化输入(“%d”,&a)

为什么要&a呢?

scanf的底层是read,printf只需要 a 的值来显示,不需要修改它,但是scanf会修改,所以需要a的地址。

char buffer[100];
read(0, buffer, sizeof(buffer));  // 从标准输入(文件描述符0)读取原始字节

从buffer转换到->%d的过程我们就叫做输入格式化。

所以大家记住了,在操作系统角度上,没有文本的概念只有二进制。文本的概念是我们自己在语言层转化的

2、可移植性

Linux 的 write 和 Windows 的 WriteFile 功能相同,但接口完全不同。所以直接调用系统调用的代码无法跨平台运行。

于是语言需要统一接口,隐藏底层:

// 标准库的 printf 内部实现(伪代码)
int printf(const char* format, ...) {#ifdef LINUXwrite(1, formatted_str, len);#elif WINDOWSWriteConsoleA(1, formatted_str, len);#endif
}

 用户只需调用 printf,无需关心底层是 write 还是 WriteConsoleA

 


三、重定向

1、缓冲区

我们上文提到过,每一个文件都有自己的操作表,于此同时,也有自己的内核级缓冲区。

当我们在执行一个write接口时,write会根据PCB找到files_struct,随后通过文件描述符下标,找到对应地址,随后找到当前文件,随后会划分为两个步骤:

1、write会把输入的字符拷贝到文件的内核缓冲区

2、然后再去系统调用中调用file所指向的ops操作集合中所指向的函数方法,把缓冲区数据刷新到外设中

数据从内核缓冲区到外设的刷新 由操作系统控制,而非 write 直接完成

所以我们的write本质上是一个拷贝函数,作用把数据从用户层拷贝到内核缓冲区。

也是因为这样,word文档要求保存,你写只是写到了缓冲区,你点击保存,才是真正从内核刷新到外设。

这是write写入的过程,那么读取呢?修改呢??

本质是一样的。

读取由操作系统决定将数据从外设写入缓冲区,read负责拷贝。(所以容易出现堵塞,例如scanf())。

修改的话,进程肯定是无法在磁盘里修改的

一定是把当前文件的内容加载到内核缓冲区里,读取到用户空间,修改完在写回缓冲区,操作系统再自动刷新到外设,所以修改的本质也是先读取在写入。

为什么要存在这个缓冲区?

因为内存操作太快,而外设速度太慢。

1、合并多次小 I/O 操作

假如我要进行100次IO操作,他们分别进行刷新的话,效率就太慢了。所以我们存在这个缓冲区,如果前99次都在缓冲区缓存住,最后统一做一次刷新,就节省了99次的IO时间。

2、预读优化

缓冲区不仅缓存写入,还预读可能访问的数据(如顺序读取文件时提前加载下一块),当然前提是内存有空闲。


2、重定向

有以下代码:

#include<stdio.h>

#include<unistd.h>

#include <fcntl.h>

int main()

{

    int fd1=open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

    int fd2=open("log2.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

    int fd3=open("log3.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

    int fd4=open("log4.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

    printf("%d\n",fd1);

    printf("%d\n",fd2);

    printf("%d\n",fd3);

    printf("%d\n",fd4);

    close(fd1);

    close(fd2);

    close(fd3);

    close(fd4);

   

    return 0;

}

运行结果:

如果我们在代码最前面增加一个close(0)呢?

打印结果改变了,如果改成2呢?

 

我们知道进程打开一个文件,需要给文件分配新的fd。从上面我们可以推测,fd的分配规则为:分配最小的且没有被使用的下标位置。

如果我们关闭1呢?

怎么什么都没有打印呢?

按照上面的规则,此时1应该变成了log1.txt的文件描述符,我们关闭了标准输出流,也就是显示屏他打印不出来很正常。

 

但是为什么也看不了文件里面的内容呢?

再加一个fflush试试:

int main()

{

    //close(0);

    //close(2);

    close(1);

    int fd1=open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC , 0666);

    int fd2=open("log2.txt",O_WRONLY | O_CREAT | O_TRUNC , 0666);

    int fd3=open("log3.txt",O_WRONLY | O_CREAT | O_TRUNC , 0666);

    int fd4=open("log4.txt",O_WRONLY | O_CREAT | O_TRUNC , 0666);

    printf("%d\n",fd1);

    printf("%d\n",fd2);

    printf("%d\n",fd3);

    printf("%d\n",fd4);

    fflush(stdout);

    close(fd1);

    close(fd2);

    close(fd3);

    close(fd4);

   

    return 0;

}

这样就显示出来了。 

为什么没有fflush时,log1.txt中看不到内容?

根本原因是 printf 的输出被缓冲在内存中,尚未写入文件,而程序结束时没有触发缓冲区的自动刷新。

本来应该像显示器文件中写入数据,结果却写入到了文件中?

这,就是重定向!

但是实际上的重定向,应该是把myfile的地址给了1,再把3指向的消除,就完成了重定向。

这个过程由一个函数:dup2完成:dup(3,1)

int main()

{

    int fd=open("log.txt",O_WRONLY | O_CREAT | O_TRUNC , 0666);

    dup2(3,1);

    printf("hello fd:%d\n",fd);

    fprintf(stdout,"hello fd:%d\n",fd);

    fputs("hello fputs\n",stdout);

    const char *message="hello fd\n";

    fwrite(message,strlen(message),1,stdout);

    return 0;

}

  

我们把3的文件给了1,所以1是newfd,而3是oldfd。

一个文件可以被多个文件描述符指定,而这,正是为什么结构中含有引用计数的原因:

3、shell里面的重定向

我们知道,在shell里面可以通过<,>,>>完成输出重定向,输入重定向,追加重定向。那么在SHell中的重定向又是如何完成的呢?

首先,我们需要识别并提取重定向相关的信息。

比如从后面一个一个开始循环判断是否出现字符'<'、'>'、'>>'。

若出现,就开始把这行命令划分为两部分,后面的就是我们的文件名。

随后,通过通过 fork() 创建子进程,并在子进程中完成文件描述符的重定向。

  • 使用 open() 打开目标文件。

  • 用 dup2() 将标准输入/输出(0/1)重定向到文件。

由于进程切换是不会影响重定向的结果,所以子进程哪怕执行其他命令,其他的代码,重定向了当前文件,就还是会输出(输入于)到这个文件 。


总结:

"一切皆文件"。这可不是一句空话,而是贯穿整个操作系统设计的核心理念。

想象一下,你在Linux里操作键盘输入、显示器输出、磁盘读写,甚至操作进程,用的都是同一套API:open、read、write、close。这不是很神奇吗?背后的秘密就在于VFS(虚拟文件系统)这个精妙的设计。每个硬件设备在内核里都被抽象成了一个struct file结构体,就像给每个硬件设备都发了一张"身份证"。

这个struct file也不简单,它里面藏着一个操作表,相当于这个设备的"技能树"。比如键盘有键盘的read方法,显示器有显示器的write方法,但对外都统一叫做read和write。这不就是面向对象里的多态吗?C++程序员看到这里应该会心一笑。

说到系统调用,这里有个很有意思的现象。我们直接调用write输出整数时,终端显示的却是乱码。为啥?因为write是个老实人,它真的就是把内存里的二进制原封不动地扔给终端。而终端这个"死脑筋"又把每个字节当成ASCII码来显示。这时候printf这个"聪明人"就出场了,它会把整数格式化成字符串再交给write,这就是语言层封装的价值。

再说说重定向这个魔术。当我们把标准输出(文件描述符1)指向一个文件时,后续所有printf的输出就都跑到文件里去了。这背后的魔法就是dup2函数,它能把一个文件描述符"变成"另一个。比如dup2(fd,1)之后,1就不再指向显示器,而是指向fd对应的文件了。Shell里的>、<这些符号玩的也是这个把戏。

不得不提的还有内核缓冲区这个"中间商"。write操作其实很懒,它只是把数据从用户空间拷贝到内核缓冲区就完事了,真正的写入操作是由操作系统在后台悄悄完成的。这就好比你在Word里打字,没点保存之前内容其实还在内存里。缓冲区的存在让多次小写入可以合并成一次大写入,效率直接起飞。

最后说说这个设计的精妙之处。通过"一切皆文件"的抽象,Linux把复杂的硬件操作统一成了简单的文件操作。开发者再也不用为每个硬件写不同的代码,一套read/write走天下。这种设计不仅优雅,还极具扩展性——新硬件只要实现那几个标准的文件操作接口,就能无缝融入系统。

如果本文对你有所帮助,感谢你一键三连支持!

如果有疑问或者指正欢迎私信或者评论区留言 !!!

http://www.dtcms.com/a/265732.html

相关文章:

  • Android 系统默认的Launcher3,Android 系统的导航栏(通常是屏幕底部)显示的 4 个快捷应用图标,如何替换这4个应用图标为客户想要的。
  • Fiddler中文版抓包工具在后端API调试与Mock中的巧用
  • Treap树
  • thinkphp8接管异常处理类
  • linux系统 weblogic10.3.6(jar) 下载及安装
  • 后端 Maven打包 JAR 文件、前端打包dist文件、通过后端服务访问前端页面、Nginx安装与部署
  • Josn模块的使用
  • MVC 架构设计模式
  • Docker 安装 Redis 哨兵模式
  • 【数据结构】C++的unordered_map/set模拟实现(开散列(哈希桶)作底层)
  • 机器人“触摸”水果成熟度突破:SwishFormer模型与DIGIT视触觉传感器在HelloRobot上的水果检测应用
  • TDSQL如何查出某一列中的逗号数量
  • 从 TCP/IP 协议栈角度深入分析网络文件系统 (NFS)
  • (1)手摸手-学习 Vue3 之 Vite 创建项目
  • grpc 和限流Sentinel
  • STC8G 8051内核单片机开发(GPIO)
  • 2025年6月微短剧备案分析:都市题材占四成,20-29集成主流体量
  • OS15.【Linux】gdb调试器的简单使用
  • 修改文件属主
  • 活体检测api集成方案-炫彩活体检测助力身份核验
  • 马斯克脑机接口(Neuralink)技术进展,已经实现瘫痪患者通过BCI控制电脑、玩视频游戏、学习编程,未来盲人也能恢复视力了
  • [极客时间]LangChain 实战课 -----|(10) 链(下):想学“育花”还是“插花”?用RouterChain确定客户意图
  • 预警:病毒 “黑吃黑”,GitHub 开源远控项目暗藏后门
  • 2024年INS SCI2区,强化搜索自适应大邻域搜索算法RSALNS+无人机扩展型协作多任务分配,深度解析+性能实测
  • 实现如何利用 Kafka 延时删除 用户邮箱的验证码(如何发送邮箱+源码) - 第一期
  • 前缀和算法详解
  • FASTAPI+VUE3平价商贸管理系统
  • React自学 基础一
  • 基于大语言模型进行Prompt优化
  • 深入解析 AAC AudioSpecificConfig 在 RTSP/RTMP 播放器中的核心作用