前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux系统IO】二、文件描述符与重定向

【Linux系统IO】二、文件描述符与重定向

作者头像
利刃大大
发布2025-03-02 22:29:40
发布2025-03-02 22:29:40
4600
代码可运行
举报
文章被收录于专栏:csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 引入 fd

存储基础 — 文件句柄 fd 究竟是什么?

​ 还记得我们之前调用 open() 接口的时候返回值叫做什么 文件描述符(file descripter) 吗,虽然我们还没有学习它,但是因为我们每次去调用 write()read() 甚至是 close() 的时候,都离不开这个文件描述符参数,所以它肯定和文件之间有着密切关联💥 💥 💥

​ 下面看看这段代码,看看不同文件打开时候的文件描述符分别是什么:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd5 = open("log5.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    printf("fd5:%d\n", fd5);

    return 0;
}

// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file 
fd1:3
fd2:4
fd3:5
fd4:6
fd5:7
[liren@VM-8-2-centos cfile]$ 

​ 大概可以看出来,fd按照顺序排列的,那么为什么第一个文件的 fd 不是 0 或者 1 呢,而是从 3 开始 ❓❓❓

​ 还记得我们曾经学C语言的文件操作时候说过,每次我们打开一个 C 程序或者这么说,Linux 进程默认情况下会有 3 个缺省打开的文件描述符,默认会打开三个标准输入输出流,分别为 stdin(键盘)、stdout(显示器)、stderr(显示器),并且它们按照以上顺序拥有 fd,也就是说 stdin:0、stdout:1、stderr:2

​ 所以我们创建这些新文件,都是从 fd3 开始,那么我们也另外可以发现,fd 是 从 0 开始的,我们就可以推测其实这是一个数组下标!!!

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
int main()
{
    printf("stdin->fd: %d\n", stdin->_fileno);
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd: %d\n", stderr->_fileno);
    return 0;
}

// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file 
stdin->fd: 0
stdout->fd: 1
stderr->fd: 2
[liren@VM-8-2-centos cfile]$ 

​ 下面就来介绍一下文件描述符!!!

Ⅱ. 文件描述符 fd(file descripter)

一、什么是fd ❓

​ 简单地说,文件描述符 fd 的本质就是数组的下标若一个文件被打开,那么肯定会有一个相对应的文件描述符字段给到该文件,用于标识该文件!

​ 既然是数组的下标,那么是什么数组呢 ❓❓❓

​ 其实在 linux 底层中是这样子的,每个进程的进程管理块 task_struct 中包含着一个 struct files_struct* files 指针指向 files_struct 这个结构体,这个结构体也称为 文件描述符表,其中这个结构体中包含着一个指针数组 struct file* fd_array[],其中数组中每个类型都是 struct file* 类型也就是指向各自加载进内存中的文件属性和内容的指针。

不管是 stdinstdoutstderr 还是 log.txt 等等,其本质就是 struct file 结构体, 方便操作系统管理

​ 而每次 linux 进程都会默认生成三个文件描述符分别指向 stdinstdoutstderr,下标分别为 012,这也是为什么我们每次打开或者创建一个新文件的时候,fd 都是从 3 开始的原因!所以,只要拿着文件描述符,就可以找到对应的文件,找到文件后就可以对文件进行操作了!

​ 下面就是这个大概的结构:

二、fd的分配规则

​ 既然我们知道文件描述符的底层结构,那么我们就能知道其实它的分配规则很简单,就是 每次遍历一遍 fd_array[],从下标为 0 开始查找第一个还没有被分配的位置,进行分配

注意:调用 close(1) 后只是将 fd_array[]fd1 处的 file* 置空,本质上 stdout 所对应的 fd 仍然为 1,这个要注意!

​ 接下来我们写代码测试一下,值得关注的是三个标准输入输出流的 fd 是就是其C语言文件结构体 FILE 中的 _fileno

① 按照顺序分配 fd
代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("stdin->fd: %d\n", stdin->_fileno);
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd: %d\n", stderr->_fileno);
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    return 0;
}

// 调用结果:
stdin->fd: 0
stdout->fd: 1
stderr->fd: 2
fd1:3
fd2:4
② 关闭其中的 stdin 和 stderr 再分配 fd
代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    close(0); // 关闭stdin
    close(2); // 关闭stderr
    
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    return 0;
}

// 调用结果:
fd1:0
fd2:2

​ 因为我们将 fd_array[] 中两个元素也就是 stdinstderr 关掉了,那么它们就变成 null 了,所以分配 fd1fd2 的时候就会扫描空的位置进行分配!

③ 关闭其中的 stdout 再分配 fd
代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    // close(0); // 关闭stdin
    // close(2); // 关闭stderr
    close(1); // 关闭stdout
    
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    return 0;
}

// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file 
[liren@VM-8-2-centos cfile]$ 

​ 会发现结果并没有显示出来,其实是因为我们将标准输出流关闭了,而标准输出流是向屏幕打印的一个渠道,关闭了它自然我们就看不到结果,但是不代表我们就没有分配文件描述符给新文件哦!

​ 那么这个时候我们就能发现如果我们把本来应该是向 stdout 也就是打印到屏幕上面的内容,我们经过关闭 close(1) 后,我们原本向屏幕打印的内容就变成了向某个占据给位置也就是 fd1 的文件中写入内容,这就是 重定向!!!这个我们下面会详细讨论!

3、fd 和 FILE 的关系

​ 那么我们仔细想一下C语言中是不是存在一个叫做 FILE 的结构体,并且我们在使用C语言文件操作时候是这样子的: FILE* fp = ... ,可以发现并没有使用到文件描述符 fd,但是我们在系统调用中的过程是这样子的: int fd = ... ,我们就能知道虽然说 FILE 指针帮我们在创建文件的时候不需要用到 fd,但是底层还是存在 fd 的,因为 语言级别的调用是离不开系统级别的调用的

​ 所以我们就可以知道 FILE 结构体内一定有一个字段是文件描述符 fd !对于 FILE 结构体来说,这只是语言级别的包装了系统中文件结构体的关系!

​ 下面我们来看看 linux 中的 files_struct 内核源码:

​ 对于 file_operations,不同硬件是有不同的方法的,大部分情况方法是和你的硬件驱动匹配的,虽然如此,但是最终文件通过函数指针实现你要打开的是磁盘,那就让所有的方法指向磁盘的方法,你要打开的是其它硬件,那就让所有的方法指向其它硬件的方法,而这里底层的差异,在上层看来,已经被完全屏蔽了,也就是达到了低耦合的作用。

​ 所以对进程来讲,对所有的文件进行操作,统一使用一套接口(现在我们明确了它是一组函数指针)。换言之,对进程来说,你的操作和属性接口统一使用 struct file 来描述,所以在进程看来,就是 “ 一切皆文件 ”

Ⅲ. 重定向

一、重定向的解释

​ 还记得上面我们在测试代码时候 close(1) 后我们再怎么使用 printf() 函数都没有用了吗,这里我们就来解释一番:

​ 其实是因为 printf 默认就是向显示器中打印内容数据,而当我们把 stdout 所对应的在 fd_array[] 中的位置置空了之后,我们这个进程就无法找到 stdout 这个文件了,这个时候自然就没办法向屏幕打印内容,接下来我们看下面的代码:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    close(1); // 关闭stdout
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("fd:%d\n", fd);
    fprintf(stdout, "fd:%d\n", fd); // 使用fprintf打印看看效果
    fflush(stdout); // 刷新一下缓冲区
    
    close(fd);
    return 0;
}

​ 通过上述运行结果我们能看到信息没有打印在屏幕上面,反而是打印到了或者说是拷贝到了 log.txt 中,这其实不奇怪,因为我们 close(1) 之后,fd_array[1] 就空了,那么新文件 log.txt 自然就占据了这个位置,那么其实这个指向就改变了,但是 stdout 中的文件描述符还是 1,这个没变,所以当我们调用 fprintf(stdout, ....) 的时候其实调用的就不是 stdout 这个文件了,而是 stdout 位置处的新文件,如下图所示:

​ 这其实也就是重定向!具体一点叫做 输出重定向

二、重定向本质

本质:上层使用的文件描述符 fd 不变(就像 stdout->_fileno 一样),在内核中更改 fd 所对应位置的 struct file* 的地址。

​ 重定向分为以下三种:

  1. >输出重定向 (一般用于写内容到文件)
  2. <输入重定向 (一般配合 cat 指令使用)
  3. >>追加重定向 (一般用于追加内容到文件)

​ 当然像上面那种关闭 stdout 的方法有点太粗糙了,显得比较挫,下面我们会给出系统级别的接口替我们完成重定向工作!

​ 并且 不同的重定向功能其实本质就是打开文件方式的不同而产生的

三、重定向接口 dup2()

代码语言:javascript
代码运行次数:0
复制
#include <unistd.h>
int dup2(int oldfd, int newfd);
// 功能:复制一个文件描述符,将oldfd拷贝给newfd(duplicate a file descriptor)
// 返回值:成功的话返回newfd(一般用于备份),失败的话返回-1,并且设置errno为对应错误信息

// 参数:
//  1、oldfd代表要进行重定向的目标文件描述符
//  2、newfd代表即将要被覆盖的文件描述符

🚗 注意:dup2() 它是将文件标识符中的内容进行交换,而不是交换文件标识符。

​ 这个接口的参数填写的时候经常会搞错,下面举个例子来加以解释两个参数:

​ 就以我们上面那串代码为例,我们要输出重定向到 log.txt ,所以我们要覆盖的就是 stdout ,也就是说这里的 newfd 就是要被覆盖的,那么就是 stdout 的文件描述符也就是 1,而 log.txtfd 就是 oldfd ,这个记得不要搞错!下面我们就用代码来实现一下上面的例子:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

    // 调用dup2拷贝文件描述符
    int newfd = dup2(fd, stdout->_fileno);
    
    // 判断是否拷贝成功
    if(newfd != -1)
    {
        // 使用fprintf打印看看效果
        fprintf(stdout, "fd:%d\n", fd);
        
        // const char* str = "lirendada";
        // write(fd, str, strlen(str));
    }
    
    close(fd);
    return 0;
}

// 调用结果:
[liren@VM-8-2-centos cfile]$ cat log.txt
fd:3
[liren@VM-8-2-centos cfile]$

​ 如果想要实现 追加重定向,非常简单,只需要将文件的打开方式变成 O_APPEND 即可~

​ 那如果要实现 输入重定向 怎么办呢,下面我们来实现一下:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("log.txt", O_RDONLY);

    dup2(fd, stdin->_fileno);

    // 将本来要从键盘读取的过程转化为从log.txt中读取
    char line[64];
    while(1)
    {
        // 从stdin对应的文件描述符指向的文件读取
        if((fgets(line, sizeof(line), stdin)) == NULL)
            break;
        printf("> %s", line);
    }
    
    close(fd);
    return 0;
}

// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file 
> fd:3
[liren@VM-8-2-centos cfile]$ 

四、再谈myshell

​ 既然我们学习了重定向,那么我们可以试试看给之前我们写的简易版shell添加重定向的功能:

​ 比如说我们输入的指令是这样子的:“ls -a -l -i > file.txt” ,那么我们就要判断一下指令中是否存在重定向的符号:><>> ,然后各自执行相对于的重定向工作!

​ 为了方便区分新增的功能,以前实现的内容的注释我就去掉了,新增的内容才有注释:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define NUM 1024  
#define OPTION_NUM 64 
char lineCommand[NUM];  
char* myargv[OPTION_NUM]; 

int lastSig = 0;
int lastCode = 0;

#define NONE_REDIR 0 // 非重定向
#define INPUT_REDIR 1 // 输入重定向
#define OUTPUT_REDIR 2 // 输出重定向
#define APPEND_REDIR 3 // 追加重定向
int redirType = NONE_REDIR; // 重定向类型默认为非重定向
char* redirFile = NULL; // 重定向的文件,默认为空

// 检测是否重定向和分割函数
void commandCheck(char* commands)
{
    assert(commands);

    char* start = commands;
    char* end = commands + strlen(commands);
    while(start < end)
    {
        if(*start == '>')
        {
            // 比如"ls -a -l >  file.txt"或者"ls -a -l >>  file.txt"
            *start = '\0';
            start++;
            if(*start == '>') // 判断是否为追加重定向
            {
                redirType = APPEND_REDIR;
                start++;
            }
            else
            {
                redirType = OUTPUT_REDIR;
            }

            while(*start == ' ') // 跳过多余空格
                start++;

            // 填写重定向信息
            redirFile = start;
            break;
        }
        else if(*start == '<')
        {
            // 比如"cat < file.txt"
            *start = '\0';
            ++start;
            while(*start == ' ') // 跳过多余空格
                start++;

            // 填写重定向信息
            redirType = INPUT_REDIR;
            redirFile = start;
            break;
        }
        else
        {
            start++;
        }
    }
}

int main()
{
    // 因为shell是循环输入的,所以要套在死循环里面
    while(1)
    {
        // 记得更新一下重定向状态
        redirFile = NULL;
        redirType = 0;
        errno = 0;

        
        printf("[%s@%s %s]~ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
        fflush(stdout);
    
        
        char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL); 
        lineCommand[strlen(lineCommand) - 1] = '\0'; 
        (void)s; 

        // 比如"ls -a -l > file.txt"分割为"ls -a -l"和"file.txt"
        // 只需要找到重定向符号,将它们置为'\0'即可!
        commandCheck(lineCommand);

       
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;


        if(myargv[0] != NULL && strcmp("ls", myargv[0]) == 0)
            myargv[i++] = (char*)"--color=auto";

        while(myargv[i++] = strtok(NULL, " "))
        {}
    
        if(myargv[0] != NULL & strcmp("cd", myargv[0]) == 0)
        {
            if(myargv[1] != NULL)
                chdir(myargv[1]);
            continue;
        }

        if(myargv[0] && myargv[1] && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
                printf("退出状态:%d,终止信号:%d\n", lastCode, lastSig);
            else
                printf("%s\n", myargv[1]);
            continue;
        }

    #ifdef DEBUG
        for(int i = 0; myargv[i]; ++i)
            printf("myargv[%d]:%s\n", i, myargv[i]);
    #endif

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            // 因为命令是子进程执行的,真正重定向的工作一定是子进程来完成的
            // 如何重定向,是父进程要给子进程提供信息的
            // 又因为进程独立性,子进程重定向后其files_struct是不会影响父进程的
            // 并且因为被打开文件是共享的,所以就能达到子进程重定向,父进程不受影响且看到效果的目的
            if(redirType == NONE_REDIR)
            {
                // 什么都不做
            }
            else if(redirType == INPUT_REDIR)
            {
                int fd = open(redirFile, O_RDONLY);
                if(fd != -1)
                    dup2(fd, stdin->_fileno);
                else
                {
                    perror("open");
                    exit(errno);
                }
            }
            else if(redirType == OUTPUT_REDIR)
            {
                int fd = open(redirFile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
                if(fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                dup2(fd, stdout->_fileno);
            }
            else if(redirType == APPEND_REDIR)
            {
                int fd = open(redirFile, O_WRONLY | O_CREAT | O_APPEND, 0666);
                if(fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                dup2(fd, stdout->_fileno);
            }

            execvp(myargv[0], myargv);

            exit(1);
        }

        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;

        lastCode = (status >> 8) & 0xFF;
        lastSig = status & 0x7F;
    }
    return 0;
}
执行程序替换时,会不会影响曾经进程打开的重定向文件呢 ❓❓❓

​ 答案是肯定不会!程序替换是在磁盘与内存阶段,程序替换是将代码进行覆盖。重定向是在 PCBfiles_struct 改变指向的阶段,它们是在内核数据结构中,并不会与文件系统产生影响,它们是相互独立!

​ 所以 程序替换是不影响内核数据结构的,即不影响曾经进程打开的重定向文件

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 引入 fd
  • Ⅱ. 文件描述符 fd(file descripter)
    • 一、什么是fd ❓
    • 二、fd的分配规则
      • ① 按照顺序分配 fd
      • ② 关闭其中的 stdin 和 stderr 再分配 fd
      • ③ 关闭其中的 stdout 再分配 fd
    • 3、fd 和 FILE 的关系
  • Ⅲ. 重定向
    • 一、重定向的解释
    • 二、重定向本质
    • 三、重定向接口 dup2()
    • 四、再谈myshell
      • 执行程序替换时,会不会影响曾经进程打开的重定向文件呢 ❓❓❓
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档