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

目录

一. 文件描述符

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:输入重定向

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

  • 函数原型:int dup2(int oldfd, int newfd)
  • 功能:将newfd文件描述符重定向到oldfd文件描述符。
  • 返回值:如果重定向成功,返回新的文件描述符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;    
} 

2.3 标准输出流stdout和标准错误流stderr的区别 

一个C/C++进程运行起来,会默认打开3个流,分别为标准输入、标准输出和标准错误。一般情况下,标准输出流stdout和标准错误流stderr都是显示屏,那么,它们有什么不同吗?

为了验证stdout和stderr的不同,编写了代码2.3进行测试,运行代码,如果不进行输出重定向,我们发现,输出到stdout和stderr的信息都被输出到了显示屏上。如果通过./test.exe>log.txt,将输出重定向到log.txt文件中,我们发现,只有输出到stdout的信息被重定向到了log.txt文件中,而输出到stderr的信息依旧被输出到显示屏上,对于追加重定向,默认也只是重定向输出到stdout的内容。

结论:命令行中的输出重定向> 、追加重定向>>,默认都只是重定向输出到标准输出流stdout的内容,不影响标准错误流。

如果我们希望将标准输出和标准错误分别重定向到特定文件,或者希望标准输出和标准错误重定向到同一个文件,那么指令为:

  •  ./test.exe>log.txt 2>error.txt  --  将可执行文件test.exe运行结果中,输出到stdout中的内容重定向到log.txt文件,将输出到stderr中的内容重定向到error.txt文件中。
  • ./test.exe>log.txt 2>&1 -- 将可执行文件test.exe运行结果中输出到stdout和stderr中的内容都重定向到log.txt文件。 

代码2.3:验证stdout和stderr的区别 

#include<stdio.h>    
#include<string.h>    
#include<unistd.h>    
    
int main()    
{    
    const char* s1 = "2.fupts -- stdout\n";    
    const char* s2 = "3.write -- stdout\n";    
    
    //向标准输出流中打印数据    
    fprintf(stdout, "1.printf -- stdout\n");    
    fputs(s1, stdout);    
    write(1, s2, strlen(s2));    
    
    const char* s3 = "2.fupts -- stderr\n";    
    const char* s4 = "3.write -- stderr\n";
                                                                                                                                                                                                                                              
    //向标准错误流中输出数据                
    fprintf(stderr, "1.printf -- stderr\n");    
    fputs(s3, stderr);       
    write(2, s4, strlen(s4));    
             
    return 0;    
}
图2.3 代码2.3的运行结果
图2.4 对stdout和stderr进行输出重定向

结合上面的现象,以及Linux下一切皆文件的结论 ,我们可以认为fd_array[1]和fd_array[2]都是显示器文件,只不过它们是不同的文件,相当于OS为显示器打开了两个文件,也可以理解为同一个显示器文件被打开了两次。 

图2.5 标准输出流和标准错误流

三. 总结 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值