1. 前情提要:回顾知识,无缝衔接:
在上一篇文章中已经谈论了系统的函数接口。今天我们更要深入理解 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 的特性,总是分配最小的:
我们先来看一个程序:
#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。
你看看这个操作,是不是很像一个东西,没错就是重定向。我们再来一段代码来加深这个实验:
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 到底怎么用,搞清楚,不迷糊:
先看图,我们需要哪些头文件:
#include <unistd.h>
函数样式:
int dup2(int oldfd, int newfd);
在这里我们 newfd 指向 1,stdout(显示器),oldfd 指向 log.txt.我们打算利用这个来完成重定向,那么就有以下代码:
#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 的继续完善:
再我们前两篇文章中已经有了一个稍微完整的 shell,但是可惜的是我们并没有实现重定向功能,在今天,我们来完善这个功能。
我们需要新增/引入
- 新增常量与状态 NONE_REDIR、INPUT_REDIR、OUTPUT_REDIR、APPEND_REDIR.这个分别对应不同的状态
- 全局变量 redir、filename。这两个是分辨文件和怎么重定向的。
我们知道重定向有: >>><这三个种类。所以我们对应了后面三种宏。还有重定向的后面可以带很多个空格,我们也需要剔除这写空格。关于这些符号,我们应该在切割字符串之前开始进行:
#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 变量去接受文件名。
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 这个变量:为后面在子进程选择重定向方式做准备。
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 的重定向功能的实现。


