(21)Linux的文件描述符输出重定向

news2024/9/25 17:21:30

一、文件描述符

1、文件描述符的底层理解

在上一章中,我们已经把 fd 的基本原理搞清楚了,知道了 fd 的值为什么是 0,1,2,3,4,5...

也知道了 fd 为什么默认从 3 开始,而不是从 0,1,2,因为其在内核中属于进程和文件的对应关系。

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

 

再看结果为3的时候,感觉不奇怪吧

接下来我们应该探索应用特征了。我们需要探索以下三个问题:

  • ① 文件描述符的分配原则
  • ② 重定向的本质
  • ③ 理解缓冲区

2、fd 的分配原则 

代码演示:默认把 0,1,2 打开,那我们直接 close(0) 关掉它们

 

 运行结果如下:

 

此时,给我的文件分配的文件描述符就是0;

现在我们再把 2 关掉,close(2) 看看:

运行结果:

所以,默认情况下 0,1,2 被打开,你新打开的文件默认分的就是 3 (因为 0,1,2 被占了) 。

如果把 0 关掉,给你的就是 0,如果把 2 关掉,给你的就是 2……

 那是不是把 1 关掉,给你的就是 1 呢?我们来看看:

int main(void)
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) {
        perror("open");
        return 1;
    } 
 
    printf("fd: %d\n", fd);
 
    close(fd);
}

结果:

发现什么都没有。

出乎意料啊,fd 居然不是 1,而是什么都没有,这是怎么回事呢?

原因很简单,1 是 stdout,printf 打印是往 stdout 打印的,你把 1 关了当然就没有显示了。

分配规则:从头遍历数组 fd_array[] ,找到一个最小的且没有被使用的下标分配给新的文件。

根据 fd 的分配规则,新的 fd 值一定是 1,所以虽然 1 不再指向对应的显示器了,但事实上已经指向了 log.txt 的底层 struct file 对象了。
但是结果没打印出来, log.txt 里也什么都没有。

二、重定向(Redirection)

1、fflush 刷新缓冲区 

实际上并不是没有,而是没有刷新,用 fflush 刷新缓冲区后,log.txt 内就有内容了。

代码演示: 

int main(void)
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
 
    fflush(stdout);
 
    close(fd);
}

运行结果:

 

我们自己的代码中调用的就是 printf,printf 本来是往显示器打印的,

现在不往显示器打了,而是写到了文件里,它的 "方向" 似乎被改变了。

这不就是重定向吗?

2、dup2 函数

上面的一堆数据,都是内核数据结构,只有 OS 有权限,所以其必须提供对应接口,比如 dup。

除了 dup,还有有一个 dup2,后者更复杂一些,我们今天主要介绍 dum2 来进行重定向操作!

man dup2

dup2 可以让 newfd 拷贝 oldfd,如果需要可以将 newfd 先关闭。

newfd 是 oldfd 的一份拷贝,将后者 (newfd) 的内容写入前者 (oldfd),最后只保留 oldfd。

                                                        \textrm{oldfd}\leftarrow\textrm{ newfd} 

 

至于参数的传递,比如我们要输出重定向 (stdout) 到文件中:

我们要重定向时,本质是将里面的内容做改变,所以是要把 fd 的内容拷贝到 1 中的:

当我们最后进行输出重定向的时候,所有的内容都和 fd 的内容是一样的了。

所以参数在传递时,oldfd 是 fd,所以应该是 dum2(fd, 1);

dum2(fd, 1);  ✅
dum2(1, fd);  ❌

因为要将显示器的内容显示到文件里,所以 oldfd 就是 fd,newfd 就是 1 了。

注意事项:dum2() 接口在设计时非常地反直觉,所以在理解上特比容易乱,搞清楚原理!

 代码演示:dup2() 

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h> 
#include <fcntl.h>
#include <unistd.h>
 
int main(void)
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) {
        perror("open");
        return 0;
    }
 
    dup2(fd, 1);   //   fd ← 1
    fprintf(stdout, "打开文件成功,fd: %d\n", fd);
 
    // 暂时不做讲解,后面再说
    fflush(stdout);
    close(fd);
 
    return 0;
}

运行结果:

 

3、 追加重定向

追加重定向只需要将我们 open 的方式改为 O_APPEND 就行了。 

int main(void)
{
    // 追加重定向只要将我们打开文件的方式改为 O_APPEND 即可
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if (fd < 0) {
        perror("open");
        return 0;
    }
 
    dup2(fd, 1);
 
    fprintf(stdout, "打开文件成功,fd: %d\n", fd);
 
    fflush(stdout);
    close(fd);
 
    return 0;
}

 运行结果如下:

4、输入重定向 

之前我们是如何读取键盘上的数据的?

int main(void)
{
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 0;
    }
    // 读数据
    char line[64];
    while (fgets(line, sizeof(line),stdin) != NULL) {
        printf("%s\n", line);
    }
 
    fflush(stdout);
    close(fd);
 
    return 0;
}

 现在我们使用输入重新的,说白了就是将 "以前从我们键盘上去读" 改为 "在文件中读"。

代码演示:所以我们将 open 改为 O_RDONLY,dup(fd, 0) : 

int main(void)
{
    // 输入重定向
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 0;
    }
    
    // 将本来从键盘上读 (0),改为从文件里读(3)
    dup2(fd, 0);
    // 读数据
    char line[64];
    while (fgets(line, sizeof(line),stdin) != NULL) {
        printf("%s\n", line);
    }
 
    fflush(stdout);
    close(fd);
 
    return 0;
}

运行结果如下:

三、缓冲区的理解(Cache) 

思考几个问题:

什么是缓冲区?为什么要有缓冲区?缓冲区在哪里?

对于缓冲区的概念,我们在 "进度条实现" 的插叙章节中有做探讨,但只是一个简单的讲解。

我们对缓冲区有一个共识,也知道它的存在,但我们还没有去深入理解它。

我们先来探讨第一个问题:

  1. 什么是缓冲区?缓冲区的本质就是一段内存。
  2. 为什么要有缓冲区?为了 解放使用缓冲区的进程时间。缓冲区的存在可以集中处理数据刷新,减少 IO 的次数,从而达到提高整机的效率的目的。
  3. 缓冲区在哪里?我们写一段代码来感受 "缓冲区" 的存在!
     

代码演示:用 printf 和 write 各自打印一段话 

  1 #include <stdio.h>  
  2 #include <sys/stat.h>  
  3 #include <sys/types.h>  
  4 #include <unistd.h>  
  5 #include<string.h>                                                                                                                                 
  6   
  7 int main(void)                         
  8 {                                               
  9     printf("Hello printf\n");   // stdout -> 1  
 10     const char* msg = "Hello write\n";          
 11     write(1, msg, strlen(msg));         
 12                                        
 13     sleep(5);    // 休眠五秒           
 14                                        
 15     return 0;                          
 16 }     

运行结果:

 

 但是,如果我们去除 \n,我们就会发现 printf 的内容没有被立马打印,而 write 立马就出来了:

 

运行结果:

为什么sleep5s之后,才打印出hello printf????

1、语言级缓冲区

首先,我们要知道:printf 内部就是封装了 write!

printf 里打印的内容 "Hello printf" 实际上是在缓冲区里的,printf 不显示的原因是没有带 \n,数据没有被立即刷新,所以 sleep 时 printf 的内容没有被显示出来。 

此时如果我们想让他刷新,可以手动加上 fflush(stdout) 刷新一下缓冲区。

至此我们说明了,printf 没有立即刷新的原因,是因为有缓冲区的存在。

可是,write 是立即刷新的!既然 printf 又封装了 write,那么缓冲区究竟在哪?

这个缓冲区一定不在 write 内部,因为如果这个缓冲区是函数内部提供的,那么直接刷新出来了。

所以这个缓冲区它只能是 C 语言提供的,该缓冲区是一个 语言级缓冲区 (语言级别的缓冲区) 。

我们再演示一次,这次选用 C 库函数 printf, fprintf 和 fputs,系统调用接口 write,观察其现象。

💬 代码演示:老样子,首先给它们都带上 \n

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    // 给它们都带上 \n
    printf("Hello printf\n");    // stdout -> 1
    fprintf(stdout, "Hello fprintf!\n");
    fputs("Hello fputs!\n", stdout);
 
    const char* msg = "Hello write\n";
    write(1, msg, strlen(msg));
 
    sleep(5);
 
    return 0;
}

运行结果:

代码演示:现在我们再把 \n 去掉:

运行结果:

此时结果是只有 write 内容先出,当退出时 printf, fprint, fputs 的东西才显示出来。

然而 write 无论带不带 \n 都会立马刷新,也就是说,只要 printf, fprint, fputs 调了 write 数据就一定显示。 我们继续往下深挖,stdout 的返回值是 FILE,FILE 内部有 struct,封装很多的成员属性,其中就包括 fd,还有该 FILE 对应的语言级缓冲区。

C 库函数 printf, fwrite, fputs... 都会自带缓冲区,但是 write 系统调用没有带缓冲区。

我们现在提及的缓冲区都是用户级别的缓冲区,为提高性能,OS 会提供相关的 内核级缓冲区

库函数在系统调用的上层,是对系统调用做的封装,但是 write 没有缓冲区,这说明了:

该缓冲区是二次加上的,由 C 语言标准库提供,我们来看下 FILE 结构体:

 放到缓冲区,当数据积累到一定程度时再刷。

2、fflush 是怎么运行的? 

如果在刷新之前关闭了 fd,会有什么问题?

int main(void)
{
    printf("Hello printf");    // stdout -> 1
    fprintf(stdout, "Hello fprintf!");
    fputs("Hello fprintf!", stdout);
 
    const char* msg = "Hello write";
    write(1, msg, strlen(msg));
    
    close(1);   // 直接把内部的文件关掉了,看你怎么刷
 
    sleep(5);
 
    return 0;
}

 运行结果:

之前的代码示例中,为了解决这个问题,我们用 fflush 冲刷缓冲区让数据 "变" 了出来:

int main(void)
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
 
    fflush(stdout);
 
    close(fd);
}

运行结果:

重定向到文件中时不用 fflush,直接调 close 文件显示不出来的原因是:数据暂存到了缓冲区。

既然缓冲区在 FILE内部,在 C 语言中,我们每一次打开一个文件,都要有一个 FILE* 会返回。

这就意味着,每一个文件都有一个 fd 和属于它自己的语言级别缓冲区。

3、缓冲区的刷新策略 

 刷新策略,即什么时候刷新,刷新策略分为常规策略 和 特殊情况。

常规策略:

  • 无缓冲 (立即刷新)   
  • 行缓冲 (逐行刷新)   
  • 全缓冲 (缓冲区打满,再刷新)

特殊情况:

  • 进程退出
  • 用户强制刷新(即调用 fflush)

下面我们运行一组代码:

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
 
int main(void)
{
    const char* str1 = "hello printf\n";
    const char* str2 = "hello fprintf\n";
    const char* str3 = "hello fputs\n";
    const char* str4 = "hello write\n";
 
    // C 库函数
    printf(str1);
    fprintf(stdout, str2);
    fputs(str3, stdout);
 
    // 系统接口
    write(1, str4, strlen(str4));
 
    // 调用完了上面的代码,才执行的 fork
    fork();
 
    return 0;
}

运行结果如下:

到目前为止还很正常,四个接口分别输出对应的字符串,打印出 4 行,没问题。

但如果我们此时重定向,比如输入 ./myfile > log.txt,怪事就发生了!log.txt 中居然有 7 条消息:

 

解读:当我们重定向后,本来要显示到显示器的内容经过重定向显示到了文件里,

  • 如果对应的是显示器文件,刷新策略就是 行刷新 
  • 如果是磁盘文件,那就是 全刷新,即写满才刷新

然而这里重定向,由显示器重定向到了文件,缓冲区的刷新策略由 "行缓冲" 转变为 "全缓冲"。 

文件中有 7 条,printf 出现 2 次,fprintf 出现 2 次,fputs 出现 2 次,但是 write 只有一次,

这和缓冲区有关,因为 write 压根不受缓冲区的影响。

fork 要创建子进程,之后父子进程无论谁先退出,它们都要面临的问题是:父子进程刷新缓冲区

 刷新的本质:把缓冲区的数据 write 到 OS 内部,清空缓冲区。

这里的 "缓冲区" 是自己的 FILE 内部维护的,属于父进程内部的数据区域。

所以当我们刷新时,代码和数据要发生写实拷贝,即父进程刷一份,子进程刷一份,

因而导致上面的现象,printf, fprintf, fputs 刷了 2 次到了 log.txt 中。
 

 还有一点点,我们下次再讲,感谢阅读!!!!!!!!!

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql SQL审核平台Yearning本地部署

文章目录 前言1. Linux 部署Yearning2. 本地访问Yearning3. Linux 安装cpolar4. 配置Yearning公网访问地址5. 公网远程访问Yearning管理界面6. 固定Yearning公网地址 前言 Yearning 简单, 高效的MYSQL 审计平台 一款MYSQL SQL语句/查询审计工具&#xff0c;为DBA与开发人员使用…

Python的基础练习题之学生管理系统

需求 使用Python基础写一个基于控制台的学生管理平台&#xff0c;里面功能分别是&#xff1a;1.录入学生信息2.查找学生信息3.删除学生信息4.修改学生信息5.排序6.统计学生总人数7.显示所有学生信息&#xff0c;要求数据存储在文件里。 代码 代码资源地址可以直接下载 效果图…

05 Ciso模拟器连接腾讯云物联网开发平台

Ciso声明&#xff1a;本篇文章基于使用腾讯云物联网平台连接自定义esp8266物联网设备(腾讯连连控制开关实现) - CSDN App改编 一、总体概览 功能描述&#xff1a; 使用腾讯连连小程序进行控制&#xff0c; Alarm&#xff08;警铃&#xff09;&#xff1a;开的时候&#xff…

【AI视野·今日Robot 机器人论文速览 第六十九期】Wed, 3 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Wed, 3 Jan 2024 Totally 5 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers NID-SLAM: Neural Implicit Representation-based RGB-D SLAM in dynamic environments Authors Ziheng Xu, Jianwei Niu, Qingf…

实现在一个文件夹中找到特定名称特点格式的文件

当你要在一个文件夹中查找特定名称和格式的文件时&#xff0c;你可以使用 Python 的 os 和 fnmatch 模块。以下是一个简单的脚本示例&#xff0c;它可以在指定目录中查找文件&#xff1a; import os import fnmatchdef find_files(directory, pattern):"""在指…

C#使用栈方法遍历二叉树

步骤一&#xff1a;定义一个二叉树的节点类 定义一个二叉树的节点类&#xff0c;其中包含节点的值、左子节点和右子节点。 // 二叉树节点定义public class TreeNode{public int Value { get; set; } // 节点的值public TreeNode Left { get; set; } // 左子节点public TreeN…

Java Base64简单介绍

1. Base64工具 工具链接 2. Base64示例代码 public class Base64Demo {// 请注意&#xff0c;在处理二进制数据时&#xff08;例如图片或文件&#xff09;&#xff0c;不需要将字节数组转换为字符串再进行编码或解码&#xff0c;// 可以直接对字节数组进行Base64操作。上述…

Python基础知识总结3-面向对象进阶知识

面向对象三大特征介绍 继承子类扩展父类语法格式关于构造函数&#xff1a;类成员的继承和重写查看类的继承层次结构 object根类dir() 查看对象属性重写 __str__() 方法 多重继承MRO方法解析顺序super()获得父类定义多态特殊方法和运算符重载特殊属性 对象的浅拷贝和深拷贝组合_…

学习笔记——C++一维数组

1&#xff0c;一维数组的定义方式 三种定义方式 1&#xff0c;数据类型 数组名[ 数组长度 ]&#xff1b; 2&#xff0c;数据类型 数组名[ 数组长度 ]{值1&#xff0c;值2&#xff0c;值3 ……}&#xff1b;//未说明的元素用0填补 3&#xff0c;数据类型 数组名[ ]{值1&…

【数据仓库与联机分析处理】数据仓库工具Hive

目录 一、Hive简介 &#xff08;一&#xff09;什么是Hive &#xff08;二&#xff09;优缺点 &#xff08;三&#xff09;Hive架构原理 &#xff08;四&#xff09;Hive 和数据库比较 二、MySQL的安装配置 三、Hive的安装配置 1、下载安装包 2、解压并改名 3、配置环…

嵌入式培训机构四个月实训课程笔记(完整版)-Linux系统编程第三天-Linux进程练习题(物联技术666)

更多配套资料CSDN地址:点赞+关注,功德无量。更多配套资料,欢迎私信。 物联技术666_嵌入式C语言开发,嵌入式硬件,嵌入式培训笔记-CSDN博客物联技术666擅长嵌入式C语言开发,嵌入式硬件,嵌入式培训笔记,等方面的知识,物联技术666关注机器学习,arm开发,物联网,嵌入式硬件,单片机…

React 中条件渲染的 N 种方法

本文作者系360奇舞团前端开发工程师 条件渲染在React开发中非常重要的功能&#xff0c;它允许开发人员根据条件控制渲染的内容&#xff0c;在创建动态和交互式用户界面方面发挥着至关重要的作用&#xff0c;本文总结了常用的的条件渲染方法。 1.If-else if-else是一种控制流程的…

rime中州韵小狼毫 敏感词脱敏滤镜

快速录入&#xff0c;是任何一个输入法&#xff0c;以及输入人员&#xff08;无论是否专业&#xff09;的追求目标之一。现实中&#xff0c;由于各种输入法在录入文本时&#xff0c;都无法完全避免重码的问题&#xff0c;所以在输入过程中都或多或少的需要进行选字/选词操作。这…

OpenSource - 基于Netty的网络扩展库HServer

文章目录 概述官网Hserver的理念特点原理图代码案例HelloWorld 概述 HServer是一个基于Netty开发网络扩展库.使用插件方式来扩展我们的业务 HServer提供 web,gateway,rpc 等插件 同时用户也可以自定义插件&#xff0c;来完成各种各样的业务场景。 官网 https://gitee.com/HSe…

认识Git

&#x1f30e;初识Git 初识Git 什么是Git Git的安装       Centos平台安装Git       Ubuntu平台安装Git Git的基本操作       创建远程仓库       配置Git 认识工作区、暂存区与版本库       添加文件到暂存区       将暂存区文件提交至本…

JVM中对象的创建

一.JVM运行流程 JVM向操作系统申请内存&#xff0c;初始化运行时数据区&#xff0c;接下来装载使用的类&#xff0c;执行类里面相应方法的时候为当前虚拟机栈压入一个栈帧&#xff0c;方法执行完成后栈帧出栈&#xff0c;进行垃圾回收。 二.JVM中对象的创建过程 符号引用&…

三维模型数据的几何坐标变换的点云重建并行计算技术方法分析

三维模型数据的几何坐标变换的点云重建并行计算技术方法分析 倾斜摄影三维模型数据的几何坐标变换与点云重建并行计算技术的探讨主要涉及以下几个方面&#xff1a; 1、坐标系定义与转换&#xff1a;在进行坐标变换前&#xff0c;需要确定各个参考系的定义并实现坐标系之间的转…

区间预测 | Matlab实现CNN-LSTM-KDE的卷积长短期神经网络结合核密度估计多变量时序区间预测

区间预测 | Matlab实现CNN-LSTM-KDE的卷积长短期神经网络结合核密度估计多变量时序区间预测 目录 区间预测 | Matlab实现CNN-LSTM-KDE的卷积长短期神经网络结合核密度估计多变量时序区间预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.CNN-LSTM-KDE多变量时间序列区…

WorkPlus AI助理为企业提供智能客服的机器人解决方案

在数字化时代&#xff0c;企业面临着客户服务的重要挑战。AI客服机器人成为了提升客户体验和提高工作效率的关键工具。作为一款优秀的AI助理&#xff0c;WorkPlus AI助理以其智能化的特点和卓越的功能&#xff0c;为企业提供了全新的客服机器人解决方案。 为什么选择WorkPlus A…

Proteus 各版本安装指南

Proteus下载链接 https://pan.baidu.com/s/1vHgg8jK9KSHdxSU9SDy4vQ?pwd0531 1.鼠标右击【Proteus8.15(64bit&#xff09;】压缩包&#xff08;win11及以上系统需先点击“显示更多选项”&#xff09;【解压到Proteus8.15(64bit&#xff09; 】。 2.打开解压后的文件夹&#…