Linux中的文件描述符和重定向

  • Post author:
  • Post category:linux




文件描述符概念

我们在使用系统调用进行文件操作的时候

image.png

image.png

这里的open返回值就是一个文件描述符简称fd。

文件描述符其实就是一个从3开始的小整数,文件描述符是小整数的原因是因为文件描述符实际是文件描述符表这个数组的下标。

image.png

为什么从3开始,是因为系统默认打开了三个文件,stdin,stdout,stderr。



分配规则

进程打开文件之后给文件分配文件描述符的规则是:

从小到大,按顺序寻找最小的没有被占用的fd。


代码演示:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>



int main()
{
    close(0);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666);
    
    printf("fd:%d\n",fd);

    close(fd);
    return 0;
}

image.png

因为操作系统默认打开的三个文件,所以我们关闭一个之后再打开文件,此时我们的文件描述符就会被分配成0.同理如果关闭0 和 2 那么此时就会给fd分配最小的0.

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>



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

    close(fd);
    return 0;
}

但是当我们1关闭也就是stdout封装的文件描述符,此时运行程序会发现什么都没有输出。因为printf默认是向stdout里输出的,从系统调用的角度看就是向文件描述符为1的文件里输出的。但是此时我们打开的文件log.txt的文件描述符是1,所以理论上打印的内容应该在log.txt里面

image.png

但是实际并没有,这是因为字符串在向普通文件中写入和向显示器中写入的时候,缓冲区的刷新策略不同。所以我们需要手动刷新缓冲区。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>



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

image.png

刷新文件缓冲区之后字符串成功写入了文件中

刷新缓冲区的时候参数是stdout,因为stdout封装的文件描述符就是1,而此时1描述的文件是log.txt所以数据就被刷新到文件中了。



重定向

上面这种情况,本来要向stdout输出后来却输出到了文件log.txt中,这种特性就是输出重定向。



重定向的本质

上层用的fd不变,在内核中更改fd在文件描述符表中对应的struct file*指针。



重定向的类型

  1. 输出重定向 >
  2. 追加重定向 >>
  3. 输入重定向 <

在shell命令行中可以直接使用重定向,下面是演示:

image.png

输出重定向,将原本要输出到显示器的内容重定向到了test.txt文件之中

image.png

追加重定向

image.png

输出重定向



重定向的原理

image.png

这是不进行重定向时我们打开的文件被文件描述符3所指向。此时我们进行输出重定向,原理实际就是将内核数据结构files_struct 中指向myfile的这个指针拷贝到下标为1处,也就是标准输出的文件描述符处。使得文件描述符1指向的文件就是新打开的myfile。

image.png



重定向的实现

通过使用close关闭文件,再open打开文件的方式来实现重定向不仅麻烦而且不好控制,所以我们可以使用一个系统调用dup2()

image.png

image.png

dup2的返回值是文件描述符

image.png

由图可知dup2的功能是将oldfd文件描述符对应的文件指针拷贝到了newfd所对应的位置并覆盖了原来newfd对应的文件指针,如果有需要会先关闭newfd所对应的文件。

也就是如果我们要是实现输出重定向,需要使用我们打开的文件的文件指针去覆盖掉stdout的文件描述符所在的位置。

代码演示:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>



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

image.png

此时新打开文件的fd还是3,但是我们已经完成了重定向,将本来应该打印到显示器上的数据打印到了文件中,因为dup2实现重定向的原理就是用文件描述符表中下标为3的内容覆盖了下标为1的内容实现输出重定向。



代码实现shell支持重定向

下面我们手写一个shell让它支持重定向功能。

首先输入一般是以下几种情况:

ls -a -l > log.txt

ls -a -l >> log.txt

cat < log.txt

所以我们首先要做的就是输入检测,将输入的字符串以中间的重定向符号为分割,将重定向符号置为\0,然后去掉多余空格剩下的两部分分别是指令操作和文件名。

然后在子进程内部就可以按照重定型符号以不同的方式打开文件,然后进行重定向,最后使用程序替换执行指令即可。

#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<string.h>
#include<ctype.h>
#include<errno.h>


#define RMSPACE(start) do{ while(isspace(*start)) ++start; }while(0)

enum Redir 
{
    NONE_REDIR = 0,
    INPUT_REDIR,
    OUT_REDIR,
    APPEND_REDIR
};

char* FileName = NULL;
int RedirWay = NONE_REDIR;

char GetsChar[1024];
char* Str[64];
void StrPart(char* s)
{
    char* start = s;
    char* end = s + strlen(s);
    while(start < end)
    {
        if(*start == '>')
        {
            *start = '\0';
            start++;
            RedirWay = OUT_REDIR;
            if(*start == '>')
            {
                start++;
                RedirWay = APPEND_REDIR;
            }
            RMSPACE(start);
            FileName = start;
            break;
        }
        else if(*start == '<')
        {
            *start = '\0';
            start++;
            RMSPACE(start);
            FileName = start;
            RedirWay = INPUT_REDIR;
            break;
        }
        else 
        {
            start++;
        }
    }
}


int main()
{
    while(1)
    {
        //每次循环重置重定向方式和文件名
        RedirWay = NONE_REDIR;
        FileName = NULL;
        printf("[%s@%s 路径]$",getenv("LOGNAME"),getenv("HOSTNAME"));
        //读取整行输入的指令
        char* s = fgets(GetsChar, sizeof(GetsChar) - 1,stdin);
        assert(s);
        
        //将读入的\n置为0
        GetsChar[strlen(s) - 1] = 0;

        //将字符串按重定向符分割
        StrPart(s);


        //将读入的字符串按空格分割
        Str[0] = strtok(GetsChar," ");
        int i = 1;

        //对ls命令添加上颜色
        if(Str[0] != NULL && (strcmp(Str[0],"ls") == 0))
        {
            Str[i++] = (char*)"--color=auto";
        }
        //将字符串分割连同最后结尾NULL一同输入Str数组
        while((Str[i++] = strtok(NULL," ")));
        
        //内建命令cd
        //不用子进程使用进程替换来执行,而是使用系统调用来完成的命令就叫做内建命令
        if(Str[0] != NULL && (strcmp(Str[0],"cd") == 0))
        {
            if(Str[1] == NULL)
            {
                char* path = getenv("HOME");
                chdir(path);
            }
            else 
            {
                chdir(Str[1]);
            }
            continue;
        }
#ifdef TEST
        for(int i = 0;Str[i]; i++)
        {
            printf("%d:%s\n",i,Str[i]);
        }
#endif
        
        pid_t id = fork();
        assert(id != -1);

        //子进程
        if(id == 0)
        {
            switch(RedirWay)
            {
                case NONE_REDIR:
                    break;
                case INPUT_REDIR:
                    {
                        int fd = open(FileName,O_RDONLY);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        dup2(fd,0);
                    }
                    break;
                case OUT_REDIR:
                case APPEND_REDIR:
                    {
                        int flags = O_WRONLY | O_CREAT;
                        if(RedirWay == OUT_REDIR)
                        {
                            flags |= O_TRUNC;
                        }
                        else 
                        {
                            flags |= O_APPEND;
                        }
                        int fd = open(FileName, flags, 0666);
                        if(fd < 0) 
                        {
                            perror("open");
                            exit(errno);
                        }
                        dup2(fd,1);
                    }
                    break;
                default:
                    puts("The RedirWay is bug");
                    break;
            }
            
            execvp(Str[0],Str);
            exit(1);
        }
        
        waitpid(id,NULL,0);
    }

    return 0;
}

当我们的使用子进程进行重定向的时候,内核数据结构中,创建子进程不会再拷贝一份文件对象而是直接让子进程的文件描述符表内保存和父进程同样的地址,指向的是同一份文件对象。


所以子进程进行重定向不会影响到父进程,这也是进程独立性的体现


image.png

子进程进行重定向也是修改的子进程的files_struct 的数据结构,并且这里采用了引用计数,比如此时两个进程都打开了文件标准输出,如果子进程关闭了标准输出,此时其实标准输出子进程是没有资格关闭的,子进程关闭的意思是子进程现在不用标准输出了,此时标准输出的引用计数-1,当引用计数减为0的时候操作系统才会真正去关闭标准输出。


子进程进行程序替换是不会影响到内核数据结构的,进程替换只是将其他程序的代码和数据替换子进程的代码和数据,不会修改到内核数据结构


image.png

父子进程在没有进行重定向的时候,父子进程打开的文件时共享的,也就是父进程打开的文件在子进程里面可以继续进行读写访问。



如何理解Linux下一切皆文件

image.png

首先计算机底层是以冯诺依曼体系结构联系组成的硬件,每种硬件都有对应的驱动程序驱动程序肯定有对应硬件的读写函数,方便操作系统与硬件进行IO。而操作系统管理硬件的方式就是先描述再组织。

但是不同的硬件他们的驱动程序肯定是不同的,读写方法也是不同的,如何统一的描述硬件呢?

操作系统再描述硬件的结构体中将硬件也看做是文件,硬件的各种属性就是文件属性,然后设置了两个函数指针,他们分别指向了硬件的读写驱动函数。


所以站在struct file 的角度上来看所有的硬件设备和文件都是统一的结构体对象,所以Linux下一切皆文件


这种在操作系统层看来使用统一的方法(函数)调用,既可以调度文件也可以调度硬件设别,在上层看来调用的是同一个函数,这就是多态思想。

上面的实现是C语言形式的多态实现。


优点就是,摒弃了底层的设备差别,以统一的方式进行操作

如何证明上面的说法是正确的呢?

在Linux源码中,task_struct结构体中保存了一个结构体指针struct files_struct* files;

image.png

struct files_struct 结构体就是进程组织文件的结构体,内部包含了文件描述符表。

image.png

文件描述符表是一个数组,一般如果是虚拟机,文件描述符表最大是32,但是可以进行扩展,云服务器进行了修改应该最大是100000或者是65536.

这里的struct file结构体就是操作系统为了管理文件和硬件设备描述出来的内核结构体。

image.png

struct file里面的 struct file_operations结构体内部保存的就是操作文件(硬件设备等)所有的函数指针,操作系统只需要初始化完成这些函数指针,就不需要关注操作的到底是硬件还是文件。

image.png



版权声明:本文为qq_62745420原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。