欢迎光临
我们一直在努力

Re:Linux系统篇(二十九)文件篇·二:深度解析Linux文件描述符、dup2指针覆盖与内建命令重定向完全解析

在这里插入图片描述

◆ 博主名称: 小此方-CSDN博客

大家好,欢迎来到小此方的博客。

⭐️Linux系列个人专栏:
【主题曲】Linux

⭐️此方的GitHub:
github_此方

⭐️
Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录

  • 概要&序論
  • 一、文件描述符的概念
    • 1.1文件描述符的基础认识
    • 1.2基于文件描述符的向上讨论
  • 二、文件描述符表
    • 2.1文件描述符表的概念
    • 2.2结合文件描述符底层重新理解I/O
    • Tips:struct file里面有什么
  • 三、重定向的原理
    • 3.1 手动模拟重定向
      • 3.1.1修改后产生的现象
      • 3.1.2重定向的原理
    • 3.2 自动接口:dup2 系统调用
      • 3.2.1接口介绍
      • 3.2.2实际应用
  • 四、升级自定义Shell:实现重定向功能
    • 4.1 核心设计思路与宏定义
    • 4.2 文本解析:重定向检测
    • 4.3 进程替换与重定向的完美结合
      • Tips:为什么一定要在子进程里做 dup2?
    • 4.4 完整的代码
  • 五、加餐补充内容
    • 5.1自定义Shell如何重定向内建命令
      • 5.1.1 认识 dup 系统调用
      • 5.2 代码层面的完美适配
    • 5.2 文件与引用计数
      • 5.1.1引用计数的增减规则

概要&序論

  Hello大家好,我是此方。本文深刻探讨 Linux 文件描述符底层结构与重定向机制。

  • 文件描述符(fd)整型值本质与标准输入、输出、错误的绑定关系;
  • 内核 task_struct、文件描述符表 files_struct 与 struct file 结构体的指针层级映射;
  • 详解文件描述符的“最小分配规则”及手动闭合标准流实现重定向的底层原理; dup 与 dup2 系统调用对内核文件对象指针的覆盖与备份操作;
  • 完成自定义 Shell 的“超级升级”,重定向。内核 struct file 的引用计数 好的,我们直接开始。

一、文件描述符的概念

1.1文件描述符的基础认识

  我们上篇文章故意回避了一个问题:open系统调用的返回值是什么?答案是“文件描述符(new file descriptor)”。 在这里插入图片描述

  文件描述符是什么?我们打印它看一看:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
umask(0);
int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd1 < 0||fd2 < 0||fd3 < 0||fd4 < 0)exit(1);
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);
}

在这里插入图片描述   我们发现,文件描述符是一个整型值。但是有人会发现:为什么文件描述符是从3开始打印而不是从0开始打印?   因为,我们上一篇文章也说了:“程序启动的时候为我们自动打开了三个流(文件),标准输入(0)、标准输出(1)、标准错误(2)。以C为例,stdin,stdout,和stderr”

1.2基于文件描述符的向上讨论

  回到我们C语言的文件操作,我想告诉大家:上篇文章我们讲到fopen函数底层封装了open函数,那么FILE返回值这个结构体里面一定也封装了一个fd文件描述符。   使用指令打开:

view /usr/include/bits/types/struct_FILE.h

在这里插入图片描述   这里的fileno就是我们要找的文件描述符。

在操作系统看来,它只认识文件描述符。 任何语言,不论它的顶层怎么设计,底层都封装了文件描述符。

以下全部都是基于文件描述符的向下讨论

  我刚才说文件描述符是一个数字,它是从0开始计数的,有没有想到什么?对,数组下标。文件描述符就是数下标。 从哪里开始讲呢?

二、文件描述符表

2.1文件描述符表的概念

  我们的系统在为我们创建进程的时候,除了task_struct、页表、虚拟地址空间等等。还创建了一个文件描述符表结构体,这个结构体里面有一个数组:文件描述符数组(这个数组是一个结构体指针数组)   我们找一个task_struct的源代码看看,嗯,确实有一个:

在这里插入图片描述   文件描述符数组在源代码中:

在这里插入图片描述   那么画一个简单的草图,就是这个结构:

在这里插入图片描述

  这个文件描述符数组里面存储的结构体是什么?是结构体struct file,我们找到它的源代码: (待会儿回来看它的东西有什么)

在这里插入图片描述

  当我们打开一个文件,操作系统会为我们创建一个文件描述结构体struct file,这个结构体中存放文件的属性,然后里面有一个指针指向一块文件缓冲区,这个文件缓冲区里面存放了这个文件的内容。

在这里插入图片描述

  将我们上面画的两张图拼在一起,结论出来了:

  文件描述符本质是文件描述符表这个结构体(struct file_struct)中的文件描述符数组的下标,这些下标指向一个个文件描述结构体(struct file),于是,拿到文件描述符就可以获取文件的描述结构体,就可以获得某个被打开的文件的全部信息。

在这里插入图片描述

2.2结合文件描述符底层重新理解I/O

对文件内容做任何的操作,都必须将文件内容加载(从磁盘到内存的拷贝)到内核中对应的文件缓冲区中。

在这里插入图片描述 read函数的本质是:从内核到用户空间的拷贝函数。

在这里插入图片描述 写也是一样,我不想画图了,就是将应用层的内容拷贝到缓冲区,然后操作系统再定期把内容刷新到磁盘里。

Tips:struct file里面有什么

  来看看这个struct file结构体里面有什么吧:什么着啊那的,先看看就行,不用管,后续的文章中再详细讲解。其中主要包含的内容比如:

struct file
{
//属性集合
//int mode
//读写位置
//读写选项
//缓冲区 — TODO
//操作方法
//struct list_head list;
}

  文件描述结构体里面也有一个union来构建file与file之间的连接,方便管理(“先描述再组织”无处不在)

union {
struct list_headfu_list; // 内核文件链表指针★
struct rcu_head fu_rcuhead; // RCU锁释放时的内存头
} f_u;

在这里插入图片描述

struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/

union {
struct list_headfu_list; // 内核文件链表指针★
struct rcu_head fu_rcuhead; // RCU锁释放时的内存头
} f_u;

struct dentry*f_dentry; // 【属性】指向目录项对象的指针(通过它能找到文件名、inode等)
struct vfsmount *f_vfsmnt; // 【属性】指向文件系统挂载点的指针

const struct file_operations*f_op; // 【操作方法】极其重要!指向该文件操作函数指针集合(如 read, write 等底层实现)

atomic_tf_count; // 【属性】文件对象的引用计数(有多少个fd指向它,为0时文件才真正关闭)
unsigned int f_flags; // 【读写选项】打开文件时的标志(如 O_RDONLY, O_WRONLY, O_NONBLOCK)
mode_tf_mode; // 【属性/int mode】文件的访问权限(如可读、可写)
loff_tf_pos; // 【读写位置】当前文件的读写偏移量(即下一次 read/write 开始的字节位置)

struct fown_structf_owner; // 【属性】异步I/O时接收信号的进程属主信息
unsigned intf_uid, f_gid; // 【属性】文件所有者的 User ID 和 Group ID
struct file_ra_statef_ra; // 【属性】文件预读(Read-Ahead)状态流,用来优化磁盘读取性能

unsigned longf_version; // 【属性】版本号,每次使用后会自动更迭
void*f_security; // 【属性】安全模块(如 SELinux)使用的安全上下文指针

/* needed for tty driver, and maybe others */
void*private_data; // 【属性】私有数据指针(系统调用或驱动程序常用来挂载自定义的特殊结构体)

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_headf_ep_links; // 【属性】被 epoll 监听时,挂载到 epoll 事件等待队列的链表节点
spinlock_tf_ep_lock; // 【属性】保护 epoll 链表的自旋锁
#endif /* #ifdef CONFIG_EPOLL */

struct address_space*f_mapping; // 【缓冲区 – TODO】指向页高速缓存(Page Cache)映射的指针!
// 这就是你上一张图说到的“内核开辟的文件缓冲区”在内核里的核心代理人。
};

三、重定向的原理

3.1 手动模拟重定向

3.1.1修改后产生的现象

  在理解了文件描述符的底层逻辑(也就是数组下标)之后,我们再来看操作系统分配文件描述符的规则:

最小的,没有被使用的,作为新的 fd 给用户。

  既然程序启动时默认打开了 0 (stdin)、1 (stdout)、2 (stderr),那如果我们故意把 1 给关闭了,然后再去打开一个新的文件,会发生什么?   根据规则,操作系统一看,0 占着,1 空着,好,那就把 1 分配给新打开的文件!   我们来看下面这段代码,手动模拟一下这个过程:

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

int main() {
close(1); // 故意关闭标准输出

// 打开一个新文件,由于 1 被释放,根据最小分配原则,fd 必然是 1
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

// 此时往标准输出打印,看看会发生什么?
printf("fd: %d\\n", fd);

// 注意:由于缓冲区问题,最后记得刷新或关闭
close(fd);
return 0;
}

  按道理说,printf 是往标准输出里面打印东西的。但是当我们编译运行后,屏幕上什么都没有输出。   我们通过 ll 指令查看当前目录,发现多了一个 log.txt。打开这个文件一看,本该打印在屏幕上的 fd: 1 居然被写入到了 log.txt 里面!

3.1.2重定向的原理

  为什么会这样?

在这里插入图片描述

  因为 printf 只认 stdout,而 stdout 结构体内部封装的 fileno 就是 1。对应用层来说,“往 1 号下标对应的文件写数据”这一逻辑从未改变。   但是在内核层,我们通过 close(1) 断开了 1 号下标指向标准显示器的连接,并让它重新指向了 log.txt 的 struct file。   上层不变底层变,上层还是原来的文件描述符,底层变成了别的地址,这——就是重定向的底层原理!

3.2 自动接口:dup2 系统调用

3.2.1接口介绍

  虽然通过先 close 再 open 可以实现重定向,但这未免有点太笨拙了,而且不够优雅。为此,Linux 系统为我们提供了一个高效率的系统调用接口:dup2。   我们先来看一下它的系统调用手册:

# int dup2(int oldfd, int newfd);

RETURN VALUE: On success, these system calls return the new descriptor. On error, -1 is returned, and errno is set appropriately.

  关于 dup2 的两个参数 oldfd 和 newfd,很多人第一次看文档极其容易搞混。官方文档有一句非常核心的解释:

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary.

  这句话翻译过来就是:让 newfd 成为 oldfd 的一份拷贝。 如果 newfd 已经打开了,会先把它关闭。   这里的“拷贝”,拷贝的绝对不是数组下标的数字,而是文件描述符表数组项中的内容(即 struct file 的指针)。

在这里插入图片描述

  简而言之,就是把 oldfd 指向的文件对象指针,覆盖到 newfd 的位置上。最终的结果是:newfd 和 oldfd 都指向了原来 oldfd 所指向的文件。

3.2.2实际应用

   如果要进行输出重定向(比如把本该输出到屏幕的内容重定向到文件中): 我们希望原本往 1 号(newfd)打的内容走到新打开的文件 fd(oldfd)中去。所以代码应该写成:dup2(fd, 1);   下面我们用 dup2 来重写一下上面的功能:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
// 1. 正常打开文件,拿到一个普通的 fd(比如 3)
int fd = open("myfile.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0) {
perror("open");
exit(1);
}
// 2. 使用 dup2 进行输出重定向
// 凡是往 1 号文件描述符写入的内容,都写到 myfile 当中,而不再写到标准输出!
dup2(fd, 1);
// 3. 测试输出
printf("凡是往1号文件描述符写的内容,都写到了myfile当中,而不再写到标准输出!\\n");
printf("fd: %d\\n", fd);
// 4. 关闭文件 //dup2它不回去主动关闭原来的文件
close(fd);
return 0;
}

四、升级自定义Shell:实现重定向功能

   我们在自定义Shell编写的那篇中讲到过,第四阶段的代码放在《文件篇》中讲解,它来了。我们要实现的是自定义Shell的“重定向功能”。

4.1 核心设计思路与宏定义

   要想让 Shell 支持重定向,我们需要把整个过程拆解为两步:

  • 宏观检测: 在解析用户输入的命令行字符串时,检查其中是否包含 <、>、>> 等重定向符号。如果存在,必须将这些符号以及后面的文件名从原命令行中“拆离”出来。
  • 底层替换: 在 fork() 出子进程之后、程序替换 execvp() 之前,根据重定向的类型,调用 open() 系统调用打开对应的文件,并使用 dup2() 改变子进程的标准输入或标准输出。
  •    首先,我们在代码顶层定义代表重定向类型的 4 个宏状态,以及用于保存全局重定向状态和目标文件名的全局变量:

    #define NONE_REDIR 0 // 无重定向
    #define INPUT_REDIR 1 // 输入重定向 <
    #define OUTPUT_REDIR 2 // 输出重定向 >
    #define APPEND_REDIR 3 // 追加重定向 >>

    int redir = NONE_REDIR; // 记录当前重定向的类型
    string filename; // 记录重定向的目标文件名

    4.2 文本解析:重定向检测

       由于用户的输入可能长这样:ls -a -l > log.txt。我们需要从字符串的末尾开始向前扫描。因为重定向符号及其文件名一般都在命令行的最右侧。    一旦扫描到符号,我们需要做两件事:

    • 将符号位置置为 \\0,从而把前面的有效命令和后面的文件名阶段分隔开。
    • 跳过符号与文件名之间的空格,提取出纯净的文件名给 filename。

       检测函数的核心实现如下:

    void RedirCheck(char* cmd)
    {
    redir = NONE_REDIR;
    filename.clear();
    int end = strlen(cmd) 1;

    // 从后往前扫描
    while (end >= 0)
    {
    if (cmd[end] == '<')
    {
    cmd[end] = '\\0'; // 截断前半部分命令
    end++;
    while (isspace(cmd[end])) end++; // 跳过可能存在的空格
    redir = INPUT_REDIR;
    filename = cmd + end; // 拿到文件名
    break;
    }
    else if (cmd[end] == '>')
    {
    if (end > 0 && cmd[end 1] == '>') // 检测到连续的 >>
    {
    cmd[end 1] = '\\0';
    end++;
    while (isspace(cmd[end])) end++;
    redir = APPEND_REDIR;
    filename = cmd + end;
    }
    else // 检测到单个 >
    {
    cmd[end] = '\\0';
    end++;
    while (isspace(cmd[end])) end++;
    redir = OUTPUT_REDIR;
    filename = cmd + end;
    }
    break;
    }
    end;
    }
    }

       很多人在写这段字符串解析时,会先把 cmd[end] = 0,紧接着在循环条件里写 while(isspace(cmd[end])) end++;。此时 cmd[end] 已经是 \\0(字符串结束符)了,isspace('\\0') 永远为假,导致循环直接跳出,提取的文件名里会夹杂着大量的非法空格或者错位指针,导致后续 open 失败,正确的做法是先将位置置空,然后将指针 end++ 移向后方,再去跳过空格。

    4.3 进程替换与重定向的完美结合

       文本解析好了,状态也保存了。接下来到了最核心的命令执行函数 Execute()。

    Tips:为什么一定要在子进程里做 dup2?

       这也是自定义 Shell 升级时最容易犯的原则性错误:绝对不能在父进程中执行重定向。    如果我们在父进程(Shell 本身)里直接调用 dup2(fd, 1),那么完蛋了,后续你的 Shell 打印命令提示符 [user@host…]$ 时,提示符不会显示在屏幕上,而是全部被写进了你的重定向文件里!    升级后的执行逻辑:

    void Execute()
    {
    // 无论是普通命令还是重定向命令,统一先 fork
    pid_t id = fork();
    if (id == 0)
    {
    if (redir == INPUT_REDIR)
    {
    // 输入重定向:只读打开
    int fd = open(filename.c_str(), O_RDONLY);
    if(fd < 0) { perror("open"); exit(1); }
    dup2(fd, 0); // 覆盖标准输入
    close(fd);
    }
    else if (redir == OUTPUT_REDIR)
    {
    // 输出重定向:只写、无则创建、覆盖式刷新
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0) { perror("open"); exit(1); }
    dup2(fd, 1); // 覆盖标准输出
    close(fd);
    }
    else if (redir == APPEND_REDIR)
    {
    // 追加重定向:只写、无则创建、追加式写入
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0) { perror("open"); exit(1); }
    dup2(fd, 1); // 覆盖标准输出
    close(fd);
    }
    execvp(g_argv[0], g_argv);
    perror("execvp"); // 如果 execvp 失败,说明命令输入错误
    exit(1);
    }

    // 父进程只负责等待子进程,回收僵尸进程,获取退出码
    int status = 0;
    waitpid(id, &status, 0);
    if (WIFEXITED(status))
    {
    exitcode = WEXITSTATUS(status);
    }
    }

    4.4 完整的代码

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    #include<string>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <cctype>

    using namespace std;

    const int COMMAND_SIZE = 128;
    const int LINE_SIZE = 1024;
    #define PROMPT "[%s@%s %s]%s"

    const int ENV_SIZE = 100;
    int g_envs= 0;
    char* g_env[ENV_SIZE] = {0};

    #define NONE_REDIR 0
    #define INPUT_REDIR 1
    #define OUTPUT_REDIR 2
    #define APPEND_REDIR 3

    int redir = NONE_REDIR;
    string filename ;

    int g_argc = 0;
    char* g_argv[LINE_SIZE] = {0};

    int exitcode = 0;

    const char* GetHostName()
    {
    const char* host = getenv("HOSTNAME");
    return host == NULL? "None" : host ;
    }

    const char* GetUserName()
    {
    const char* user = getenv("USER");
    return user == NULL? "None" : user ;
    }

    string GetDirectory(char* _cwd )
    {
    if(_cwd == NULL) return " ";
    string cwd = _cwd;
    int pos = cwd.rfind("/");
    if(pos == string :: npos )
    return " ";
    return cwd.substr(pos +1);
    }

    string GetCwd()
    {
    char* cwd = getenv("PWD");
    return GetDirectory(cwd);
    }

    const char* GetSign()
    {
    if(strcmp(GetUserName(), "root") == 0)
    return "#";
    else return "$";
    }

    const char* GetHome()
    {
    const char* home = getenv("HOME");
    return home ==NULL? "None" :home;
    }

    void MakeCommandPrompt(char* det , int size)
    {
    snprintf(det,size, PROMPT ,GetUserName(),GetHostName(),GetCwd(),GetSign());
    }

    void PrintCommandPrompt()
    {
    char Prompt[COMMAND_SIZE] = {0};
    MakeCommandPrompt(Prompt,sizeof(Prompt));
    printf("%s",Prompt);
    fflush(stdout);
    }

    bool GetCommandLine(char* cmd,int size )
    {
    char* buf = fgets(cmd , size , stdin);
    if(buf==NULL) return false;
    cmd[(size_t)(strlen(cmd)1)] = 0;
    if(strlen(cmd)==0) return false;
    return true;
    }

    void Initenv()
    {
    extern char** environ;
    for(int i = 0; environ[i] ; i++)
    {
    g_env[i] = (char*)malloc(strlen(environ[i])+1);
    if(g_env[i]==NULL)
    {
    string enverror("环境变量初始化异常");
    throw enverror;
    }
    strcpy(g_env[i],environ[i]);
    g_envs++;
    }
    g_env[g_envs]=NULL;
    for(int i =0 ;g_env[i];i++)
    putenv(g_env[i]);
    }

    void RedirCheck(char* cmd)
    {
    redir = NONE_REDIR;
    filename.clear();
    int start = 0;
    int end = strlen(cmd) 1;
    while(end > start)
    {
    if(cmd[end]=='<')
    {
    cmd[end] = 0;
    end++; // 修复:先向后移动一位再跳过空格
    while(isspace(cmd[end]))
    end++;
    redir = INPUT_REDIR;
    filename = cmd + end;
    break;
    }
    else if(cmd[end]=='>')
    {
    if(cmd[end1]=='>')//>>
    {
    cmd[end1] = 0;
    cmd[end] = 0;
    end++;
    while(isspace(cmd[end]))
    end++;
    redir = APPEND_REDIR;
    filename = cmd + end;
    }
    else
    {
    cmd[end] = 0;
    end++;
    while(isspace(cmd[end]))
    end++;
    redir = OUTPUT_REDIR;
    filename = cmd + end;
    }
    break;
    }
    else end;
    }
    }

    bool AnalyseCommandLine(char* cmd)
    {
    #define EXC " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(cmd,EXC);
    while((bool)(g_argv[g_argc++]=strtok(NULL,EXC)));
    g_argv[g_argc] = NULL;
    g_argc;
    if(g_argc==0)
    return false;
    return true;
    }

    void Cd()
    {
    if(g_argc == 1)
    chdir(GetHome());
    else if (g_argc == 2){
    if(strcmp(g_argv[1] ,"~") == 0)
    chdir(GetHome());
    else if(strcmp(g_argv[1],"-")==0)
    {
    const char* oldpwd = getenv("OLDPWD");
    if(oldpwd) chdir(oldpwd);
    }
    else chdir(g_argv[1]);
    }
    else{
    string cderror("cd命令执行错误");
    throw cderror;
    }

    // 维护 PWD 环境变量,防止 cd 后提示符不更新路径
    char cwd_buf[LINE_SIZE];
    if (getcwd(cwd_buf, sizeof(cwd_buf))) {
    setenv("PWD", cwd_buf, 1);
    }
    }

    void Echo()
    {
    #define MAX_SIZE_ENV 1000
    if(g_argv[1] == NULL)
    {
    cout << endl;
    }
    else if(strcmp(g_argv[1], "$?") == 0)
    cout<<exitcode<<endl;
    else if(g_argv[1][0] == '$')
    {
    const char* tmp = getenv(g_argv[1] + 1);
    if(tmp) cout<< tmp << endl;
    else cout << endl;
    }
    else
    printf("%s\\n",g_argv[1]);
    }

    bool BuildinCommandCheck()
    {
    if(strcmp(g_argv[0] ,"cd") == 0)
    {
    Cd();
    return true;
    }
    else if(strcmp(g_argv[0] , "echo") == 0)
    {
    Echo();
    return true;
    }
    return false;
    }

    void Execute()
    {
    pid_t id = fork();
    if(id == 0)
    {
    if(redir == INPUT_REDIR)
    {
    int fd = open(filename.c_str(), O_RDONLY);
    dup2(fd,0);
    close(fd);
    }
    else if(redir == OUTPUT_REDIR)
    {
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    dup2(fd,1);
    close(fd);
    }
    else if(redir == APPEND_REDIR)
    {
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
    dup2(fd,1);
    close(fd);
    }

    execvp(g_argv[0], g_argv);
    exit(1); // 如果 execvp 失败,子进程安全退出
    }

    int statue = 0;
    waitpid(id, &statue, 0);
    if (WIFEXITED(statue)) {
    exitcode = WEXITSTATUS(statue);
    }
    }

    int main()
    {
    try
    {
    Initenv();
    }
    catch(string error)
    {
    cout<<error<<endl;
    }
    try
    {
    while(true)
    {
    //打印命令行提示符
    PrintCommandPrompt();
    //获取命令行参数
    char commandline[LINE_SIZE] = {0};
    if(!GetCommandLine(commandline ,sizeof(commandline)))
    continue;
    //检测重定向类型
    RedirCheck(commandline);
    //分析命令行参数
    if(!AnalyseCommandLine(commandline))
    continue;
    //检测是否是内建命令
    if(BuildinCommandCheck())
    continue;
    //执行命令
    Execute();
    }
    }
    catch (string error)
    {
    cout<<error<<endl;
    }
    catch(...)
    {
    cout<<"未知异常"<<endl;
    }
    return 0;
    }

    五、加餐补充内容

    5.1自定义Shell如何重定向内建命令

      在完成了自定义 Shell 的重定向升级后,你可能会发现一个隐藏的致命问题:如果用户输入的是内建命令,应该如何重定向?   因此,内建命令的重定向,核心关键在于:重定向操作完成后,如何将底层指针恢复如初?   为了解决这个问题,我们需要引入另一个系统调用:dup。

    5.1.1 认识 dup 系统调用

      与 dup2 强制覆盖指定下标不同,dup 的作用是复制一份标准指针到当前最小的空闲位置。

    #include <unistd.h>
    int dup(int oldfd);

    返回值: 成功时返回新分配的文件描述符(该描述符指向与 oldfd 相同的文件对象);失败返回 -1。

      利用 dup 恢复指针:

  • 备份: 在对 1 号(stdout)进行重定向覆盖之前,先调用 int save_stdout = dup(1);。此时,系统会分配一个新 fd(比如 3),让 3 号也指向标准显示器。这样我们就把显示器的指针备份到了 3 号位置。
  • 重定向: 调用 dup2(fd, 1);,放心地让 1 号指向目标文件,供内建命令使用。
  • 执行: 调用内建命令的执行函数(如 Echo())。
  • 恢复: 内建命令执行完毕后,调用 dup2(save_stdout, 1);。把刚才备份在 3 号的显示器指针重新覆盖回 1 号位置!
  • 善后: 调用 close(save_stdout); 和 close(fd);,释放不需要的临时描述符。
  • 5.2 代码层面的完美适配

      修改后的内建命令处理逻辑如下:

    bool BuildinCommandCheck(){
    // 如果没有内建命令命中,直接返回 false
    if (strcmp(g_argv[0], "cd") != 0 && strcmp(g_argv[0], "echo") != 0){
    return false;
    }
    int save_stdin = 1;
    int save_stdout = 1;
    if (redir == INPUT_REDIR){
    save_stdin = dup(0); // 备份原标准输入
    int fd = open(filename.c_str(), O_RDONLY);
    if(fd >= 0) {
    dup2(fd, 0); // 重定向标准输入
    close(fd);
    }
    }
    else if (redir == OUTPUT_REDIR){
    save_stdout = dup(1); // 备份原标准输出
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd >= 0) {
    dup2(fd, 1); // 重定向标准输出
    close(fd);
    }
    }
    else if (redir == APPEND_REDIR){
    save_stdout = dup(1); // 备份原标准输出
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd >= 0) {
    dup2(fd, 1); // 重定向标准输出
    close(fd);
    }
    }
    if (strcmp(g_argv[0], "cd") == 0)
    Cd();
    else if (strcmp(g_argv[0], "echo") == 0)
    Echo();
    //关键:恢复
    if (save_stdin != 1){
    dup2(save_stdin, 0); // 恢复标准输入
    close(save_stdin); // 释放备份
    }
    if (save_stdout != 1){
    dup2(save_stdout, 1); // 恢复标准输出
    close(save_stdout); // 释放备份
    }
    return true;
    }

    5.2 文件与引用计数

      我们在前面的草图中提到了系统的核心原则:一个文件可以被多个进程同时打开。这就带来了一个问题:当某个进程关闭一个文件时,操作系统怎么知道该不该把这个文件从内核中真正释放掉?

    5.1.1引用计数的增减规则

      在内核的 struct file 结构体内部,有一个名为 f_count 的成员(即引用计数)。它的工作机制:

    在这里插入图片描述

    • 增加: 每当有一个新的文件描述符(fd)指针指向该文件(如通过 open、fork 进程继承,或调用 dup系列接口拷贝指针),该文件的引用计数就会 ++。
    • 减少: 每当用户层调用 close(fd),或者进程退出导致描述符表被销毁时,操作系统并不会直接抹除该文件,而是先让对应的引用计数 –。
    • 关闭: 当文件的引用计数为0的时候,文件才会被正式关闭。

    在这里插入图片描述   有人会问:为什么struct file_struct里面也有一个引用计数?这个没法讲,得等到线程章节再说,你就记住一句话:”这个引用计数决定文件描述符表何时真正被销毁“ 在这里插入图片描述


    好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

    赞(0)
    未经允许不得转载:171主机测评 » Re:Linux系统篇(二十九)文件篇·二:深度解析Linux文件描述符、dup2指针覆盖与内建命令重定向完全解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址