【Linux】文件描述符及重定向

目录

文件描述符的引入

什么是文件描述符

文件描述符的分配规则

重定向

输出重定向

输入重定向

追加重定向

dup2()


文件描述符的引入

上一章,我们讲解了系统接口,了解了open()函数的返回值是一个整数.那么这个整数究竟是什么呢?我们可以用以下代码来看一下这种现象:

  int main()    
  {    
    umask(0);    
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND,0666);    
    printf("open success, fd: %d\n",fd1);                                                                                               
    int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND,0666);    
    printf("open success, fd: %d\n",fd2);    
    int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_APPEND,0666);    
    printf("open success, fd: %d\n",fd3);    
    int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_APPEND,0666);    
    printf("open success, fd: %d\n",fd4);  

    close(fd1);    
    close(fd2);    
    close(fd3);    
    close(fd4);    
  }  

我们分别打开了4个文件.然后退出make编译然后运行.

 我们发现:这些的fd是连续,并且fd是从3开始的,那么0,1,2去哪里了呢?

我们上一章提到了,执行程序时,会默认打开3个输入输出流,分别为:stdin,stdout,stderr.

这三个分别占据了0,1,2的位置.


我们可以使用一个例子来验证它们之间的关联.

  int main()    
  {    
    umask(0);    
    //向stdout(显示器)中输出
    fprintf(stdout,"hello,stdout\n");    
     
    //向fd为1的文件写入                                                                                                                                   
    const char* s = "hello,1\n";    
    write(1,s,strlen(s)); 
  }

我们可以发现,这两条语句都成功的在显示屏中输出了.

这就说明fd为1的文件 确实是对应着显示器.


我们在前一章讲了fopen()这个接口,我们再来看一下

 我们别的不再说,我们看它的返回值是FILE*.我们知道这是个指针.

但是这个FILE是什么呢?FILE是一个struct结构体,它的内部有很多和文件相关的成员,它是由C标准库提供的.

C语言 库函数内部一定要调用 系统接口的!在系统角度,系统只认fd,像FILE这些其它变量,系统是不会识别的.所以这个FILE结构体里面一定封装了fd.

话都说到这了,那么stdin,stdout,stderr的类型也是FILE*,它的内部也一定封装了fd.我们可以打印输出来证明一下.

int main()      
{    
  printf("stdin: %d\n",stdin->_fileno);    
  printf("stdout: %d\n",stdout->_fileno);    
  printf("stderr: %d\n",stderr->_fileno);

  return 0; 
}

运行结果图:

结果确实如我们所想的那样.便成功验证了我们刚才所说的.


所以我们最终的结论是:

1.C语言的文件调用接口 和 系统接口是有一定关系的.

2.在用户层面上是stdin,stdout,stderr,在系统层面上只能用数字0,1,2,来表示。这些数字便是文件描述符。

什么是文件描述符

上面一直在说fd,知道它是一个整数,那么它到底是什么呢?

进程要访问文件,首先必须打开文件。

一个进程可以打开多个文件,所以一般而言,进程:文件 = 1:n

文件要被访问,前提是加载到内存中,才能被直接访问.

既然一个进程可以打开多个文件,那么如果多个进程都打开自己的文件呢?

这个时候系统中便会存在大量被打开的文件,所以OS需要把这么多的文件管理起来.

管理方式:先描述 + 再组织。

在内核中,如何看待打开的文件? OS为了管理每一个被打开的文件,需要先将其抽象成struct结构体,然后再描述,这个结构体叫做struct file,大致内容如下:

 有很多这样的文件需要组织管理起来,那么采用了双链表进行管理.

但是进程是如何知道哪个是我打开的文件呢,所以现在唯一需要处理的就是进程和文件之间的对应关系.

这里先提前说一句:

操作系统负责处理这些进程文件操作请求,管理文件描述符和维护文件状态,但文件的具体内容和处理是由进程来控制和管理的。

所以不要因为一会操作系统管理,一会进程管理而混淆了.

我们知道文件描述符fd是从0开始递增的,在我们所学到的容器里面,vector容器的下标也是从0开始向后递增的。

所以这个对应关系其实本质上是一个数组,然后这个数组的每个下标指向相应的file_struct,这个数组里的每个下标便叫做文件描述符(fd).这个数组叫做文件描述符表.

它作为一个成员在进程task_struct结构体中.

 在task_struct结构体中,我们通过内核源代码可以看到:

 内部有一个结构体指针,这个指针指向files_struct这个结构体,然后我们来看一下这个结构体里面有什么内容.

 重点看最后一个结构体fd_array,它的类型是一个指针数组类型,它就是文件描述符表,每个元素都是一个指针,指向file结构体,那我们接下来看看这个file里面有什么内容.

这个里面就包含了一个文件所有内容,包括属性和内容相关的,我们现在知道通过fd_array便可以找到所有的文件即可.


总结一下以上的关系:

进程task_struct结构体中,有一个结构体指针指向了files_struct,file_struct里又有一个结构体数组fd_array,fd_array数组里每个下标便是文件描述符,下标对应的内容便是fd为该下标文件的内容.

然后我们再整体理顺一下之间的关系。以及是如何一步步推导过来的.

文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。

而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的分配规则

我们就先创建一个新的文件,然后看看它的文件描述符是多少,代码如下:

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

 然后我们退出vim,然后make编译并运行.

可以看到,fd现在是3,这也没有问题,因为0,1,2已经默认被stdin,stdout,stderr占用了.


此时如果我们把fd为0的文件关掉,此时再看一下文件的fd是多少.

close(0);

 然后此时运行结果显示,fd为0.

同样地,如果我们把fd为2的文件关掉.

close(2);

 

此时的文件fd又成了2.

我们大概知道了,新打开文件的fd默认为第一个没有被占用的fd.

所以这里fd的分配规则是:给新文件分配最小的,且没有被占用的文件描述符.

重定向

输出重定向

我们前面只说了关闭fd为0和2的,但是如果关闭fd为1的文件呢?

我们输入以下代码

int main()
{
    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); //stdout->FILE[fileno=1]->log.txt                                                                          
    printf("fd: %d\n",fd);
    printf("fd: %d\n",fd);

    //close(fd); //后面解释

    return 0;
}

然后我们再运行这段代码.

我们发现什么都没有输出,这不应该啊,按理来说,应该输出三个1才对.

但是我们如果输出一下log.txt里面的内容:

我们发现,本应该输出在显示器上的内容输出到了log.txt这个文件中.

当我们不关闭fd=1的文件时,我们再次编译并运行.

 此时我们发现正常输出到显示器上了.

以上是因为:printf默认是向stdout(标准输出)里面打印的,即fd为1的文件。但是此时fd=1的文件被关掉,然后被新建立的文件占用了,所以此时printf再输出,便会直接输出到这个文件当中了.

这种原本向显示器中打印的,但是最后却写入(显示)到了别的文件中,这个行为便叫做输出重定向.

下面是一张重定向原理图:

原来fd=1指向的文件是显示器,然后被更改指向myfile,所以之前写的向显示器上输出的内容全部输出到了myfile这个文件中.

所以重定向的本质是:在OS内部,更改fd对应内容的指向. 

输入重定向

我们知道了重定向的原理,输入重定向也很好理解.无非是从键读取的内容变成了从别的文件中读取.

先来看这代码:

int main()    
{    
    int fd = open("log.txt",O_RDONLY,0666);    
    if(fd < 0)    
    {    
      perror("open");    
      return 0;    
    }    
    printf("打开的文件fd为:%d\n",fd);    
    
    char buffer[64];    
    fgets(buffer,sizeof(buffer),stdin);    
    
    printf("%s\n",buffer);  
}

我们先打开了一个文件,然后输出它的fd,接着,我们写了一个fgets,第三个参数为stdin,表示从键盘中读取数据,并将读取的数据输出到buffer中,并输出buffer.

 我们可以看到它完整地将我的输入输出了出来.

但是此时如果我们在前面加上close(0),同时为了效果明显,我们在log.txt文件中写入以下内容:

然后我们再运行 代码

 我们发现它此时没有卡着等待我的键盘输入,而是直接从文件中读取了。

相当于是把fd=0所指向的键盘文件 变成了log.txt文件.

这个过程便叫做输入重定向.

追加重定向

这个和输出重定向类似,只不过是把open函数中O_TRUNC换成了O_APPEND.

代码如下:

int main()    
{    
    close(1);    
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);    
    if(fd < 0)    
    {    
      perror("open");    
      return 0;    
    }    
    fprintf(stdout,"you can see me\n"); 
}

这段程序本来每次是向显示器中输出“you can see me”,然后经过重定向,最后追加到了log.txt中.

 这便是追加重定向.

dup2()

以上的代码只是为了演示重定向的原理,真正使用重定向时,我们并不会那样使用,而且每次使用时都得先关闭文件再打开文件,这样写出来的代码可读性并不高.

所以为了解决这个问题,我们可以使用dup2一个系统调用来解决.

我们先看一下dup2的用法:

 然后再看一下解释:

 它的意思是newfd是oldfd的一份拷贝,即将oldfd拷贝给newfd. 最终要和oldfd一致.

如果oldfd不是有效的文件描述符,那么调用失败,newfd不会被关闭.

如果oldfd是有效的文件描述符,那么将会把oldfd的值拷贝给newfd。


回到刚开始说的输出重定向.

我们最终改变了fd=1的所指向的内容,即将原来的fd=3的文件指针 拷贝给了 fd=1的文件指针.

最终结果和fd=3一致,正如上面这句话所说“ 最终要和oldfd一致” ,所以fd=3是oldfd,fd=1则是newfd.这样便理清了关系.


知道了这些,我们赶紧来用用dup2.

int main(int argc, char* argv[])  
{  
    if(argc != 2)  
    {  
      return 2;  
    }  
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);  
    if(fd < 0)  
    {  
      perror("open");  
      return 1;  
    }  
    dup2(fd,1);//将fd(3)的内容拷贝到fd=1当中  
    fprintf(stdout,"%s\n",argv[1]); 
}

 argc和argv是命令行参数,具体可以参照我之前的文章:传送门,里面有详细的解释.

然后此时我们运行程序:

这样便把我们的本该输出到显示器上的内容输出到文件中了.

 这样dup2的大致用法也就说完了.

本文章到这里就结束了,如有疑问或者错误的地方,欢迎评论区指正或私信哦.

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/131876970