本系列主要旨在帮助初学者学习和巩固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的底层:重定向的原理和虚拟文件系统。
回顾上文的知识点:
- Linux一切皆是文件。(只是简单的理解,并没有深入)
- C语言提供的printf,fprintf,fgets,fread,fwrite,这几种接口
- C语言提供的几种打开文件的模式:只读,只写,追加等等
- open 函数的参数解析(位图)
- 什么是fd(文件描述符)
- 如何使用write/read/close
本文目标:
- fd究竟代表这什么
- 详细了解dup2函数,里面的参数究竟怎么用
- 完善shell,加入shell的重定向功能

2.fd的底层:一个数组的下标:
在上一篇文章中我们已经理解了三个已经打开的文件:stdout,stdin,stderr以及他们的文件描述符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)想象成只有一把钥匙的连号储物柜:
- 初始状态:系统默认给你占用了 0、1、2 号柜子(标准输入、输出、错误)。
- 你的操作 close(1):你把 1号 柜子退租了。
现在的状态:0号(占用),1号(空闲),2号(占用)。 - 你的操作 open("file.txt"):你向系统申请一个新的柜子。
系统管理员(内核)开始从头扫描:
"0号?有人用了。"
"1号?它是空的!给你吧。" - 结果:你的新文件就拿到了 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的重定向功能的实现。
感谢各位对本篇文章的支持。谢谢各位点个三连吧!

