Linux系统编程:文件描述符和重定向

news2024/9/29 11:36:36

目录

一. 文件描述符

1.1 什么是文件描述符

1.2  OS如何通过文件描述符找到指定文件

1.3 文件描述符的分配规则

二. 重定向

2.1 重定向的现象和底层原理

2.2 通过系统接口dup2实现重定向

三. 总结 


一. 文件描述符

1.1 什么是文件描述符

Liunx操作系统为用户提供了四个用于读写的系统接口,它们分别为:

  • int open(const char* file, int flag, mode_t mode) -- 打开文件。
  • int close(int fd) -- 关闭文件。
  • ssize_t write(int fd, const void* ptr, size_t size) -- 向指定文件中写内容。
  • ssize_t read(int fd, void* ptr, size_t size) -- 从指定文件中读取内容。

上面的open函数,返回的就是新打开的文件的描述符,而close、write、read函数,都是通过文件描述符fd来找到文件,对文件进行相应的操作。

综上,凭借感性认识,我们可以认为文件描述符是Linux操作系统下,进程用于找到指定文件的一种标识,就像在语言层面上,C语言通过FILE*指针,找到指定的文件来进行操作一样,FILE的功能,也是确定进行操作的文件。

操作系统并不认识FILE*,如果想要直接通过系统接口进行IO操作,就必须要指定文件描述符。语言层面上的IO函数其底层一定封装了系统IO接口。因此,我们可以断定,C语言FILE的本质是结构体类型,里面有一个成员变量就是文件描述符fd,C语言的IO函数会拿着FILE内部的文件描述符成员去调用系统的IO接口。

代码1.1先后使用open打开了4个文件(省略检查打开是否成功),这四个文件的文件描述符分别为fd1~fd4,我们使用printf函数,打印出这四个文件描述符,他们的值为3~6。

代码1.1:open打开文件,输出文件描述符

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

提问:用户使用open打开的文件所对应的文件描述符为3/4/5...,那么,0/1/2去哪里了呢?

当一个C/C++可执行程序载入到内存,成为进程开始被执行时,会自动打开三个流,它们分别为:stdin -- 标准输入流、stdout -- 标准输出流、stderr -- 标准错误流。

一般情况下,标准输入流stdin就是键盘,标准输出流stdout和标准错误流stderr就是显示器,而在Linux下,我们认为一切能够进行IO操作的设备都是文件,那么C/C++进程默认打开这三个流,就相当于打开了三个文件。

既然是打开了文件,就要对文件进行管理,也就需要为它们分配一个文件描述符。在Linux下,文件描述符0、1、2的意义为:

  • 0 -- 标准输入流,stdin
  • 1 -- 标准输出流,stdout
  • 2 -- 标准错误流,stderr

代码1.2对上面的结论进行了证明,stdin/stdout/stderr的类型都是FILE*,既然是FILE*,其内部必然要存储文件描述符。通过stdin->_fileno操作,打印stdin/stdout/stderr对应的文件描述符,结果分别为0/1/2。同时,定义const char* s = "hello world",使用write接口,指定文件描述符1和2输出s的内容,可以看到hello world被两次输出在显示屏上。

代码1.2:验证文件描述符0、1、2的意义

#include<stdio.h>    
#include<unistd.h>    
#include<string.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<sys/fcntl.h>    
    
int main()    
{    
    printf("stdin fd:%d\n", stdin->_fileno);    
    printf("stdout fd:%d\n", stdout->_fileno);    
    printf("stderr fd:%d\n", stderr->_fileno);    
    
    const char* s = "hello world\n";    
    write(1, s, strlen(s));    
    write(2, s, strlen(s));    
    
    return 0;    
} 
图1.1 代码1.2的运行结果

1.2  OS如何通过文件描述符找到指定文件

如果一个进程要打开多个文件,那么OS就一定要将这些文件组织管理起来。同时,我们还知道,OS会为每个进程都创建一个进程控制块PCB,即task_struct,task_struct中会记录一个进程的全部属性信息,当然也包括这个进程所打开的文件。task_struct中会有一个files_struct*类型的指针成员变量fs,指向存储当前进程文件信息的结构体files_struct。

files_struct要记录进程打开的多个文件的信息,其内部有fd_array数组,数组元素的类型为file*,fd_array每个下标位置对应的元素都会指向一个文件对象file,因此,文件描述符的本质就是元素类型为指向文件对象的指针的数组的下标。

当用户传递给系统接口文件描述符fd希望操作特定文件时,系统所进行的操作的流程为:找到进程PCB -> 找到其内部的files_struct* fs成员变量 -> 通过fs找到管理进程打开的文件信息的结构体file_struct -> 通过fd_array[fd],找到指定文件进行操作。

图1.2为task_struct -> fd_array[fd]的结构示意图。

图1.2  fd管理进程文件的体系结构

通过上面的分析,我们可以分析出,C/C++的IO函数,访问文件的底层执行流程为:拿到FILE中的fd成员 -> 将拿到的fd作为参数调用系统接口 -> 通过task_struct找到file_struct -> fd_array[fd]找到文件对象file -> 执行相应操作。

1.3 文件描述符的分配规则

我们自己打开的文件,其实并不一定从3开始分配文件描述符。在代码1.3中,我们通过close(0),将标准输出流stdin关闭,然后在先后通过open函数打开两个文件,此时输出这两个文件对应的文件描述符,分别为0和3。

结论:新打开文件的文件描述符为最小的、还没有被占用的文件描述符。

代码1.3:文件描述符分配规则测试

int main()    
{    
    close(0); //关闭标准输入流                                                                                                                                                                                                                
    
    int fd1 = open("log1.txt", O_WRONLY|O_CREAT, 0666);    
    if(fd1 < 0)    
    {    
        perror("open log1.txt");    
        return 1;    
    }    
    
    
    int fd2 = open("log2.txt", O_WRONLY|O_CREAT, 0666);    
    if(fd2 < 0)    
    {    
        perror("open log2.txt");    
        return 2;    
    }    
    
    printf("fd1:%d\n", fd1);   // fd1:1    
    printf("fd2:%d\n", fd2);   // fd2:3    
    
    return 0;    
}    
图1.3 文件描述符的分配

二. 重定向

2.1 重定向的现象和底层原理

看代码2.1,我们使用printf,将标准输出流stdout关闭,然后使用open,以写文件的方式打开文件log.txt,使用printf函数,将打开文件成功后的文件描述符和const char* s = "hello world\n"输出出来。但是运行代码,我们发现,进程并没有向屏幕上处任何内容。但是,如果cat log.txt查看log.txt文件的内容,我们发现,本该输出到屏幕上的内容被输出到了log.txt文件中。

上面提到的现象就是输出重定向,将close(1)关闭了标准输出流,新打开的文件log.txt的文件描述符被赋予了1,而stdout为C/C++语言层已经定义并初始化的FILE*对象,其内部记录的文件描述符为1。

我们通过close(1)将标准输出流关闭,并不会影响stdout->_fileno的值,stdout->_fileno永远为1。然而,此时文件描述符1已经不再对应屏幕,而是被新打开的文件log.txt占用,操作系统只认识文件描述符,只能通过文件描述符来查找进行读写操作的文件,因此,printf由于在底层实现为向stdout输出内容,即向fd=1的文件输出内容,此时log.txt的fd为1,printf就向log.txt输出内容了。

代码2.1:输出重定向

#include<stdio.h>    
#include<unistd.h>    
#include<string.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<sys/fcntl.h>    
    
int main()    
{    
    close(1);   //关闭标准输出流    
    
    int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);  //打开文件,fd=1    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    printf("open success, fd:%d\n", fd);    
    
    const char* s = "hello world\n";    
    printf("%s", s);    
    printf("%s", s);    
    
    return 0;                                                                                                                                                                                                                                 
}   
图2.1 输出重定向的实现原理

同样,如果我们通过close(0)将标准输入流关闭,然后再open打开文件,这时新打开的文件的文件描述符就是0,而FILE* stdin中的_fileno成员的值为0,这样标准输入就会被重定向为新打开的文件。此时scanf、fgets( ... , stdin)等操作,都是从open打开的文件中读取数据,这样就实现了输入重定向。

代码2.1:输入重定向

#include<stdio.h>    
#include<unistd.h>    
#include<string.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<sys/fcntl.h>    
    
int main()    
{    
    close(0);    
    
    int fd = open("log.txt", O_RDONLY);   //打开文件log.txt,里面的内容为hello world    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    char buff[20];    
    memset(buff, 0, 20);    
    
    fgets(buff, sizeof(buff), stdin);    
    printf("%s", buff);   //输出hello world    
    
    close(fd);    
    
    return 0;                                                                                                                                                                                                                                 
} 

2.2 通过系统接口dup2实现重定向

  • 函数原型:int dup2(int oldfd, int newfd)
  • 功能:将文件描述符oldfd指向的文件对象,更改为指向newfd指向的文件对象,释放newfd原来指向的文件对象(fd_array[newfd] = NULL)
  • 返回值:如果重定向成功,返回新的文件描述符newfd,如果失败,返回-1。

使用dup2接口,必须包含头文件#include<unistd.h>

图2.2 使用dup2进行输出重定向的原理

有了dup2,我们在实现重定向时,就不需要再通过close(0)、close(1)来实现重定向了,只需要在open文件之后,通过dup2(fd, 1)/dup2(fd, 0)实现输出/输入重定向。

代码2.2使用dup2接口,将标准输出重定向到文件log.txt中,这样,原本该输出到显示屏上的内容,就被重定向到了log.txt中。

代码2.2:使用dup2接口进行输出重定向

#include<stdio.h>    
#include<unistd.h>    
#include<string.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<sys/fcntl.h>    
    
int main()    
{    
    int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    fd = dup2(fd, 1);   //输出重定向    
    printf("fd:%d\n", fd);    
    
    const char* s = "hello world";    
    printf("%s\n", s);    
    printf("%s\n", s);    
    printf("%s\n", s);    
    
    fflush(stdout);    
    close(fd);    
                                                                                                                                                                                                                                              
    return 0;    
} 

三. 总结 

  • 文件描述符的本质为数组下标,每个进程PCB中会有一个指向struct_files的指针,而struct_files中有存在有fd_array数组指向文件对象。系统IO接口通过拿到的文件描述符fd,根据fd_array[fd]找到指定的文件操作。
  • 一个C/C++程序运行起来,默认打开三个流:标准输入stdin、标准输出stdout、标准错误stderr,它们对应的文件描述符为0、1、2。
  • 重定向的底层原理是改变某个文件描述符fd所指向的文件,这样通过fd进行读写操作时,被操作的文件就发生了改变。

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

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

相关文章

微积分习题课

目录 例题1&#xff1a; 例题2&#xff1a; 解法2&#xff1a; 例题3&#xff1a; 例题4&#xff1a; 方法2&#xff1a; 例题5&#xff1a; 例题6&#xff1a; 例题7&#xff1a; 例题8&#xff1a; 例题9&#xff1a; ​编辑 例题1&#xff1a; 例题2&#xff1a; 解法2&…

二元函数的泰勒展开

定理如下&#xff1a; 证明如下&#xff1a; 注意&#xff1a; 证明的核心要点是转化为关于新的变量的一元函数。

【Linux】进程间通信 -- 匿名管道 | pipe系统调用

什么是通信&#xff1f;为什么要有通信&#xff1f;如何实现&#xff1f;管道通信匿名管道 pipe系统调用读写特征管道的特征: 什么是通信&#xff1f; 进程具有独立性&#xff0c;我们现在的进程间需要通信&#xff0c;那么这个成本一定不低 数据传输&#xff1a;一个进程需要将…

@FeignClient源码浅析

Spring如何识别FeignClient 从EnableFeignClients 出发&#xff0c;寻找Spring如何识别FeignClient 从源码中查看到Import(FeignClientsRegistrar.class) Retention(RetentionPolicy.RUNTIME) Target(ElementType.TYPE) Documented Import(FeignClientsRegistrar.class) pub…

PyQt结合OpenCV实现实时人流量统计

1. 废话篇&#xff08;可跳过&#xff09; 之前学的基本都是Web端的技术。前两天的面试&#xff0c;让我深入的去学习一下 Qt 技术&#xff0c;了解完概念之后&#xff0c;才知道我之前接触的类 TkInter 技术&#xff0c;有点安卓开发的味道。。。 2. 人流量统计效果图 3. 业务…

L1-027 出租(Python实现) 测试点全过

题目 下面是新浪微博上曾经很火的一张图&#xff1a; 一时间网上一片求救声&#xff0c;急问这个怎么破。其实这段代码很简单&#xff0c;index数组就是arr数组的下标&#xff0c;index[0]2 对应 arr[2]1&#xff0c;index[1]0 对应 arr[0]8&#xff0c;index[2]3 对应 arr[…

Php Jenkins phpunit配置

目录 作用 前提 安装 安装xUnit插件 win10重启Jenkins 全局环境设置 创建项目配置 描述 源码管理 构建触发器 构建步骤 插件安装 工作空间 php代码phpunit文件示例 项目根目录配置 phpunit.xml Protect/Tests/test_start.php composer.json 作用 jenkins 自动…

【docker部署安装ApiSix】

docker安装ApiSi 常见问题-提前查阅 1-端口被占用 确保所需的所有端口&#xff08;默认的 9080/9091/9443/2379/9000&#xff09;未被其他系统/进程使用 #查询端口占用情况 netstat -antp |grep 9443# 如果端口冲突可尝试修改apisix的端口配置&#xff0c; # 但不建议&#x…

0305kali linux配置运行-docker-macos aarm64

文章目录 1 下载运行2 配置2.1 配置系统环境2.2 配置SSH服务2.3 安装工具 3 问题总结结语 1 下载运行 拉取kali linux镜像 docker pull kalilinux/kali-rolling该镜像为“纯净版”系统&#xff0c;没有任何工具&#xff0c;体积小。下面当我们运行起来之后&#xff0c;到容器中…

GlusterFs 分布式复制卷(Distributed-Replicate)性能测试

目录 fio工具参数解释 Glusterfs 和NFS 性能测试 顺序写&#xff1a; 随机写&#xff1a; 顺序读&#xff1a; 随机读&#xff1a; 随机读写&#xff1a; 参数说明&#xff1a; 测试结论&#xff1a; 与NFS对比 压测对比结果 NFS和GlusterFs的优缺点 NFS的优点 NFS…

基于卷积神经网络VGG的猫狗识别

&#xff01;有需要本项目的实验源码的可以私信博主&#xff01; 摘要&#xff1a;随着大数据时代的到来&#xff0c;深度学习、数据挖掘、图像处理等已经成为了一个热门研究方向。深度学习是一个复杂的机器学习算法&#xff0c;在语音和图像识别方面取得的效果&#xff0c;远远…

综合能源系统(1)——综合能源系统基本定义与内涵

综合能源系统关键技术与典型案例  何泽家&#xff0c;李德智主编 综合能源系统基本定义 综合能源系统(Integrated Energy System&#xff0c;IES)的概念最早产生于热电联产领域&#xff0c;侧重于热电系统的协同优化&#xff0c;而后逐渐扩展丰富&#xff0c;涉及电、热、冷…

DEVICENET转MODBUS-TCP网关应用案例

远创智控YC-DNT-TCP连接到DEVICENET总线中做为从站使用&#xff0c;连接到 MODBUS-TCP 总线中做为主站或从站使用。是自主研发的一款 DEVICENET 从站功能的通讯网关。 YC-DNT-TCP常用拓展图 技术指标 网关的 MODBUS 接口可通过拨码选择做为主站&#xff08;客户端&#xff09…

QNAP威联通NAS搭建SFTP服务,并内网穿透实现公网远程访问

文章目录 前言1. 威联通NAS启用SFTP2. 测试局域网访问3. 内网穿透3.1 威联通安装cpolar内网穿透3.2 创建隧道3.3 测试公网远程访问 4. 配置固定公网TCP端口地址4.1 保留一个固定TCP端口地址4.2 配置固定TCP端口地址4.3 测试使用固定TCP端口地址远程连接威联通SFTP 转载自远程内…

【算法集训之线性表篇】Day 05

文章目录 题目思路代码实现效果 题目 将两个有序顺序表合并为一个有序顺序表&#xff0c;函数结果返回值为顺序表。 思路 我们可以利用二路归并排序算法中的Merge函数思路&#xff0c;设置两个指针i&#xff0c;j&#xff0c;分别记录在顺序表a和b中的访问位置&#xff0c;再…

【未解决】No rule to make target ‘/usr/lib/x86_64-linux-gnu/libGL.so‘

测试ros自带的PCL1.8是否能用&#xff0c;网上找个测试代码&#xff0c;编译阶段报错&#xff1a; cmake .. -- Could NOT find ensenso (missing: ENSENSO_LIBRARY ENSENSO_INCLUDE_DIR) ** WARNING ** io features related to ensenso will be disabled -- Could NOT find …

Blender环境纹理材质贴图入门教程

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D开发工具链 大家好&#xff0c;今天跟大家分享Blender材质贴图入门图文教程&#xff0c;一套blender的PBR材质包&#xff0c;和HDRI环境纹理贴图&#xff0c;在文末领取&#xff0c;希望能助到大家更高效完成场景练习。 据我了解…

Linux Deploy(一)Linux Deploy简介与软件安装

一、Linux Deploy简介 Linux Deploy可以在安卓机器上使用chroot容器技术运行arm或者x86的Linux系统&#xff0c;利用该技术可以搭建个人服务器&#xff0c;Linux Deploy可运行多种linux发行版的软件&#xff0c;不失为一个好的家庭微型服务系统&#xff0c;如果想把手机弄成微…

【Spark_BigData】期末复习考试——

复习题目 yarn框架中不包含的进程为 Yarn包括两个主要进程:资源管理器Resource-Manager,节点管理器Node-Manager。 Scheduler zookeeper spark SQL 前身 Shark 在Spark中,DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。 HiveContext继承自SQLCon…

安装最新版CMAK,处理报错java.util.NoSuchElementException: key not found: PLAINTEXT

安装最新版CMAK&#xff0c;处理报错java.util.NoSuchElementException: key not found: PLAINTEXT 一、下载CMAK二、解压CMAK三、修改配置文件四、安装jdk11五、启动CMAK六、CMAK界面设置Kafka集群信息七、完整报错八、报错原因九、解决方法 一、下载CMAK CMAK下载地址&#…