【C++与Linux基础】文件篇(3)-fd的本质和minishell的重定向功能

本系列主要旨在帮助初学者学习和巩固Linux系统。也是笔者自己学习Linux的心得体会。


个人主页: 爱装代码的小瓶子
文章系列: Linux
2. C++


文章目录

  • [1. 前情提要:回顾知识,无缝衔接:](#1. 前情提要:回顾知识,无缝衔接:)
  • 2.fd的底层:一个数组的下标:
    • [2-1 fd的特性,总是分配最小的:](#2-1 fd的特性,总是分配最小的:)
  • [3. 重写minishell与dup2的用法:](#3. 重写minishell与dup2的用法:)
    • [3-1 dup2到底怎么用,搞清楚,不迷糊:](#3-1 dup2到底怎么用,搞清楚,不迷糊:)
    • [3-2 MiniShell 的继续完善:](#3-2 MiniShell 的继续完善:)
  • 总结:

1. 前情提要:回顾知识,无缝衔接:

在上一篇文章中:【C++与Linux】文件篇(2)- 文件操作的系统接口详解已经谈论了系统的函数接口。今天我们更要深入理解Linux的底层:重定向的原理和虚拟文件系统。

回顾上文的知识点:

  1. Linux一切皆是文件。(只是简单的理解,并没有深入)
  2. C语言提供的printf,fprintf,fgets,fread,fwrite,这几种接口
  3. C语言提供的几种打开文件的模式:只读,只写,追加等等
  4. open 函数的参数解析(位图)
  5. 什么是fd(文件描述符)
  6. 如何使用write/read/close

本文目标:

  1. fd究竟代表这什么
  2. 详细了解dup2函数,里面的参数究竟怎么用
  3. 完善shell,加入shell的重定向功能

2.fd的底层:一个数组的下标:

在上一篇文章中我们已经理解了三个已经打开的文件:stdoutstdinstderr以及他们的文件描述符0,1,2;是不是感到一阵熟悉感。每次就是数组的下标。

那么这个数组在哪里呢?

我们的Linux的pcb(struct_task):它内部包含一个指针 *files,指向 struct files_struct。这表示"这个进程打开了哪些文件"。而其中 struct files_struct里面包含一个指针数组fd_array.里面存放了不同的打开的文件。已经打开的就放在前面的位置。按顺序排放。而FD就是这个数组的下标。

是不是很抽象。没事,我们来看看一个图片:

是不是很详细,没错,其实我在上面说的文件,并不是一个真实的文件,而是指向一个一个结构体,里面放着它的文件读取位置的下标和文件的操作。

2-1 fd的特性,总是分配最小的:

我们先来看一个程序:

cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>



int main()
{
    close(1);
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    if(fd == -1){
        perror("open error");
    }
    
    printf("fd = %d\n",fd);
    char* msg = "hello world\n";
    write(fd,msg,strlen(msg));
    write(1,msg,strlen(msg));
    return 0;
}

你可能会觉得,我们会在屏幕上打印 fd = 1;实则不然。我们可以来看看结果:

我们可以看到但我们运行的时候,我们在shell是一点都看不到的。但是我们再次打开log.txt,结果全写进去了log里面了。这里就不得不提到它的一个特性:

"当进程打开一个新的文件时,内核总是分配当前【最小的、未被使用的】非负整数作为文件描述符。" (The kernel always allocates the lowest-numbered unused file descriptor.)

如果不好理解的话:

我们可以把文件描述符(fd)想象成只有一把钥匙的连号储物柜:

  1. 初始状态:系统默认给你占用了 0、1、2 号柜子(标准输入、输出、错误)。
  2. 你的操作 close(1):你把 1号 柜子退租了。
    现在的状态:0号(占用),1号(空闲),2号(占用)。
  3. 你的操作 open("file.txt"):你向系统申请一个新的柜子。
    系统管理员(内核)开始从头扫描:
    "0号?有人用了。"
    "1号?它是空的!给你吧。"
  4. 结果:你的新文件就拿到了 fd = 1。

你看看这个操作,是不是很像一个东西,没错就是重定向。我们再来一段代码来加深这个实验:

c 复制代码
    close(2);
    close(0);
    int fd = open("log.txt",O_RDONLY);
    if(fd == -1){
        perror("open error");
        exit(1);
    }
    printf("fd = %d\n",fd);
    close(fd);
    return 0;

我们看到我们关闭了 0和2,最终他选择0.这就是上面我说的原则或者特性。

3. 重写minishell与dup2的用法:

这里我们分成两个模块来完成讲解,这两个是息息相关。当我们结束了dup2的用法之后,我们就可以来看看我们自己shell,使他支持重定向的功能。

3-1 dup2到底怎么用,搞清楚,不迷糊:

先看图,我们需要哪些头文件:

cpp 复制代码
#include <unistd.h>

函数样式:

cpp 复制代码
 int dup2(int oldfd, int newfd);

在这里我们newfd 指向 1,stdout(显示器),oldfd 指向log.txt.我们打算利用这个来完成重定向,那么就有以下代码:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    if(fd == -1) {
        perror("open error");
        exit(1);
    }
    
    int ret1 = dup2(fd,1);
    close(fd);
    char* msg = "hello world\n";
    ssize_t ret2 = write(1,msg,strlen(msg));
    printf("%s",msg);
    return 0;
}

我们再这里可以观察到我们再复制之后,就立马关闭了fd,现在无论是printf,还是像1里面写入,都是像log.txt里面写入了。

我们可以看到,我们已经向log.txt,写入了我们的字符。这就是后面我们的shell的重定向功能的基础。

其实这里还是比较难以理解,容易搞混的。在这里,你这样记住oldfd是要被保留的,而newlfd是要被替换的。这里很想C语言里面的赋值逻辑, 比如 a = b,就是把b 的值赋给 a。这里大致也是这个逻辑

3-2 MiniShell 的继续完善:

再我们前两篇文章中:【C++与Linux基础】进程篇 - 改进Shell,完成内建命令里面已经有了一个稍微完整的shell,但是可惜的是我们并没有实现重定向功能,在今天,我们来完善这个功能。

稍后我会绑定两个资源,一个是没有重定向的minishell,一个是具有完整的minishell。大家可以根据自己的需要,来下载对应的资源。

我们需要新增/引入

  • 新增常量与状态NONE_REDIR、INPUT_REDIR、OUTPUT_REDIR、APPEND_REDIR.这个分别对应不同的状态
  • 全局变量 redir、filename。这两个是分辨文件和怎么重定向的。

我们知道重定向有: > >> <这三个种类。所以我们对应了后面三种宏。还有重定向的后面可以带很多个空格,我们也需要剔除这写空格。关于这些符号,我们应该在切割字符串之前开始进行:

cpp 复制代码
 #define NONE_REDIR 0
 #define INPUT_REDIR 1
 #define OUTPUT_REDIR 2
 #define APPEND_REDIR 3  

 std::string filename;
 int redir = NONE_REDIR; 

先确定不重定向和3中重定向方式: > >> < 和准备用一个string变量去接受文件名。

cpp 复制代码
void  RedirCheck(char cmd[]){
    filename.clear();//先把filename的文件给清空
    redir = NONE_REDIR;
    int start = 0;
    int end = strlen(cmd) - 1;//防止指向\0,还要再减去1

    while(end > start){
        if(cmd [end] == '<'){
            cmd[end++] = 0;//开始切割> 处理文件名:
            TrimSpace(cmd,end);
            redir = INPUT_REDIR;
            filename = cmd + end;
            break;//说明不需要在去检验之前的
        }
        else if(cmd[end] == '>'){
            //这里我们注意,这里是后面的一个 >
            if(cmd[end - 1] == '>'){
                redir = APPEND_REDIR;
                cmd[end - 1] = 0; 
            }
            else redir = OUTPUT_REDIR;
            cmd[end++] = 0;//以> 开始切割,然他变成0
            TrimSpace(cmd,end);
            filename = cmd + end;
            break;//
        }
        else  end--;
    }

}

这个函数主要是分割前面的命令和后定位这个句子到底是> >> 或者 < 。随后改变redir这个变量:为后面在子进程选择重定向方式做准备。

cpp 复制代码
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        if(redir == INPUT_REDIR){
            int fd = open(filename.c_str(),O_RDONLY);
            if(fd == -1) 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 ==  -1) 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 == -1) exit(1);
            dup2(fd,1);//原本像标准输出打印,现在像文件中打印。
            close(fd);
        }
        //child
        //由于将cmd[> 或者 << <] 变成了 0.后面在进行命令行分析就和原来一样了
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

在这里我们可以看到,我们在子进程进行分类,如果是不同的方式,我们就以不同的方式去打开这个文件,完成重定向。

可以看到以及完成了重定向,这个还是不错的。

总结:

今天简单的介绍了fd的底层和minishell的重定向功能的实现。

感谢各位对本篇文章的支持。谢谢各位点个三连吧!



相关推荐
s_daqing5 小时前
arm的ubuntu启动node
linux·arm开发·ubuntu
m0_663234015 小时前
Python代码示例:数字求和实现
linux·服务器·前端
2301_822382765 小时前
嵌入式C++实时内核
开发语言·c++·算法
Max_uuc5 小时前
【C++ 硬核】拒绝单位混淆:利用 Phantom Types (幻影类型) 实现零开销的物理量安全计算
开发语言·c++
济6175 小时前
linux 系统移植(第二十八期)---- 运用MfgTool 工具烧写自制的烧写自制的系统系统---- Ubuntu20.04
linux·运维·服务器
2301_790300965 小时前
C++与物联网开发
开发语言·c++·算法
太理摆烂哥5 小时前
Linux基础指令
linux·运维·服务器
Doro再努力5 小时前
【Linux04】 Linux基础指令完结与Linux权限初识(一)
linux·运维·服务器
江畔何人初5 小时前
k8s中namespace与容器cgroup区别
linux·运维·云原生