Linux 文件系列:深入理解文件fd,重定向,自定义shell当中重定向的模拟实现
一.预备知识
经过刚才的分析,我们可以一个很重要的结论:
一个文件要被打开,一定要先在OS中形成被打开的文件对象
下面我们来回顾一下C语言中常见的文件接口
我们会发现重定向跟它们有所联系
二.回顾C语言中常见的文件接口跟重定向建立联系
关于C语言文件操作的详细内容,大家可以看我的这篇博客:
C语言文件操作详解
1.fopen函数的介绍
2.fclose函数的介绍
3.代码演示
1.以"w"(写)的方式打开
以"w"(写)的方式打开,如果文件不存在,就会在当前进程所在的路径当中创建它
创建成功
我们用vim写一些内容,再用w打开,看看w是否会清空之前的内容
清空成功
2.跟输出重定向的联系
我们会发现,fopen的"w"选项跟输出重定向很像啊
下面我们再来看看"a"选项的方式打开跟追加重定向的关系
3.以 “a”(追加)的方式打开
"a"也是写入,不过是从文件结尾处开始写入,是追加式写入,并不会清空文件
并没有清空原有内容
4.跟追加重定向的联系
我们会发现,fopen的"a"选项跟追加重定向很像啊
三.认识并使用系统接口
下面我们来认识并使用一下系统调用接口
首先我们达成1个共识:
C语言的文件操作接口,它的底层一定封装了系统调用接口
1.open
1.open和fopen的联系(引出 FILE和struct file的联系)
这是C语言提供的库函数:fopen:
这是系统调用接口:open:
可见,这个fd跟我们之前常用的FILE*指针很像啊,
其实它们的功能是一样的,C语言的FILE是一个结构体,这个结构体里面封装了fd
而这个fd是被打开的文件的结构体(struct file内核数据结构)中的一个属性,是用来区分不同文件的
2.open的进一步介绍
刚才我们还没有介绍第2个参数呢,下面我们来看一下
至此我们也理解了fopen是如何对open进行封装的
下面我们来使用一下open函数并且看看这个fd到底是啥啊?
3.open函数的使用
1.close函数
2.开始使用并且看看这个fd到底是什么?
现在我们有了两个问题:
- 0 1 2去哪了?
- 为什么会是 3 4 5 6?
下面就让我们借助这两个问题来深入理解一下文件描述符fd
四.理解文件描述符fd
1.文件描述符fd的本质
2.标准输入,标准输出,标准错误
在C语言的学习中我们都听说过
C语言程序(也就是进程),只要运行起来,默认就打开3个流
今天我们要说明的是:
3.理解Linux下一切皆文件的设计理念
五.理解struct file内核数据结构
六.fd的分配规则
1.先抛出结论
2.代码演示
分配规则1就不言而喻了,我们来验证分配规则2
我们先关闭stdin,然后在打开log.txt
如果该进程中log.txt被分配的fd是0,那么验证成功
验证成功
3.替换标准输出时的现象
下面我们先关闭stdout,然后再打开log.txt
为什么最后的
printf("log.txt的fd是: %d\n",fd);
没有成功打印呢?
因为stdout是标准输出流,是显示器对应的流,
我们平常printf是将字符串打印到stdout当中,但是我们在printf之前已经把stdout关掉了
所以不会打印到显示器
可是当我加了一行代码
cat log.txt之后
发现刚才printf中本来要往显示器上打印的数据现在写到了log.txt里面
这说明:
1.printf只认识stdout,也就是fd为1的文件
2.上层的fd并没有改变,但是底层fd指向的内容发生改变了
本来fd值为1的这个fd应该要指向显示器这个设备文件的
但是在这个进程当中 现在指向log.txt了
3.也就是说这个过程其实就是进行了一种类似于狸猫换太子式的指向的改变
七.理解重定向
1.重定向的本质
经由刚才的print的例子之后,我们可以发现:
由此可以得出重定向的本质:
重定向的本质,其实就是修改特定文件fd的指向
2.演示一下重定向
1.输出重定向
这是log.txt之前的数据
输出重定向成功
2.追加重定向
追加重定向成功
3.输入重定向
要进行输入重定向,我们要使用fread函数
1.fread函数
2.演示
输入重定向成功
八.dup2函数:实现两个fd之间的重定向
其实库里面给我们提供了一个函数dup2
可以实现两个fd之间的重定向
下面我们使用dup2函数再来演示一下重定向
1.dup2实现输出重定向
此时log.txt的fd是3
2.dup2实现追加重定向
实现成功
3.dup2实现输入重定向
实现成功
九.自定义shell当中重定向的模拟实现
经过上面的练习之后,下面我们修改一下我们的myshell.c代码,模拟实现一下重定向
关于myshell.c代码的实现,大家可以看我的博客当中的
Linux自定义shell的编写,里面实现了自定义shell
1.原myshell.c代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#define DEBUG 1
#define SEP " "
char cwd[1024]={
'\0'};
int lastcode=0;//上一次进程退出时的退出码
char env[1024][1024]={
'\0'};
int my_index=0;
const char* getUsername()
{
const char* username=getenv("USER");
if(username==NULL) return "none";
return username;
}
const char* getHostname()
{
const char* hostname=getenv("HOSTNAME");
if(hostname==NULL) return "none";
return hostname;
}
const char* getPwd()
{
const char* pwd=getenv("PWD");
if(pwd==NULL) return "none";
return pwd;
}
//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
int i=0;
usercommand[i++]=strtok(command,SEP);
while(usercommand[i++]=strtok(NULL,SEP));
}
//解析命令行
void GetCommand(char* command,char* usercommand[])
{
command[strlen(command)-1]='\0';//清理掉最后的'\0'
CommandSplit(usercommand,command);
#ifdef DEBUG
int i=0;
while(usercommand[i]!=NULL)
{
printf("%d : %s\n",i,usercommand[i]);
i++;
}
#endif
}
//创建子进程,完成任务
void Execute(char* usercommand[])
{
pid_t id=fork();
if(id==0)
{
//子进程执行部分
execvp(usercommand[0],usercommand);
//如果子进程程序替换失败,已退出码为1的状态返回
exit(1);
}
else
{
//父进程执行部分
int status=0;
//阻塞等待