目录
前言
在之前我们学习了文件标识符,直到close可以使用文件标识符进行关闭,但是当我们关闭1号(stdout)时,无法往显示器中打印数据,这都是文件的重定向在起作用,今天我们来一探究竟。
一、重定向
大家看如下代码,关闭了fd:1(stdout),根据之前的学习,后面打开的 log1.txt 的fd就会设为1。fflush(stdout)我们跟缓冲区有关,我们暂时忽略。那么无法往显示器上打印数据,数据去哪里了呢?
看到结果,竟然往log1.txt上面打印了
这是因为printf()函数,只认文件描述符1,他只会往文件描述符1号上面打印,之前 1 为显示器,于是往显示器上打印,现在显示器被关闭,1号变成了文件,那么printf也会往1号进行打印,也就打印到了文件中。
而这就是重定向,这里是输出重定向,重定向的本质就是修改特定文件fd的下表里的内容。也就是以前指向原文件,后面指向新文件。
其中有一句代码fflush(stdout);很重要,如果我们不进行刷新缓冲区,那么也不会往log1.txt里面写入数据。这是C语言给我们提供的缓冲区,他会将我们打印的内容刷新到对应的fd文件中。因为我们后续会close(fd)文件,后续从C语言提供的缓冲区中把数据刷新到fd的时候,fd已经关闭,也就无法刷新,因此这里需要fflush进行刷新再关闭。
二、重定向的运用
那么根据输出重定向,追加重定向很很简单了,O_TRUNC换成O_APPEND就行。
那我们再看看输入重定向,我们使用 fread+重定向去读数据。
fread第一个参数为读取的数据放到哪里,第二个参数为读取单个单元的大小,第三个参数为读取多少单元,第四个参数是从哪里读。
我们使用如下代码,关闭0号文件,那么打开的log1.txt的fd就会被设为0,后面fread读取的时候,不再会从键盘中读取,而是直接冲log1.txt中读取,依然是老样子,fread认的是文件标识符。
那么结果也是能够预料到的,fread从文件中读取了数据。这就是输入重定向
重定向的基本原理:上层fd不变,底层fd指向的内容发生改变,是文件描述表级别的数组里的内容的拷贝。
比如log.txt文件发现 1 号文件描述符无内容,他就直接拷贝到1号文件描述符。
三、dup2
但是每次都要用close关闭某个文件这确实有点戳,我们可以使用dup2来进行文件描述符表级别的数组里内容的拷贝。
第一个参数为oldfd,第二个参数为newfd,这局代码会让oldfd进行覆盖newfd(oldfd被保留下来)。这样文件描述符表数组就会有两个指针指向同一文件了。
当我们使用某个fd进行关闭文件时,并不会影响另一个fd,这涉及到引用计数,也就是有几个指针指向,count就为几,某个指针取消指向,使用close进行释放,只会让count-1,直到count为0才会释放。
如下代码,不再close,而是dup2,依然可以达到效果
四、命令行中的重定向
我们知道命令行中>为输入重定向,>>追加重定向,<输出重定向
我们拿出之前写的简易版myshell再添加上重定向
简易版myshell代码如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define NUM 1024
#define SIZE 64
char* getUsername()
{
char* env = getenv("USER");
if(env) return env;
return NULL;
}
char* getHostname()
{
char* env = getenv("HOSTNAME");
if(env) return env;
return NULL;
}
char* getPwd()
{
char* env = getenv("PWD");
if(env) return env;
return NULL;
}
int main()
{
while(1)
{
char command[NUM];
char* argv[SIZE];
int argc = 0;
printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());//打印
fgets(command,NUM,stdin); //输入完成后还会输入回车,导致换行
command[strlen(command)-1] = '\0';
argv[argc++] = strtok(command," ");
while(argv[argc++] = strtok(NULL, " "));
pid_t id = fork();
if(id == 0)
{
//child
execvp(argv[0],argv);
exit(1);
}
else
{
pid_t rid = waitpid(id,NULL,0);
if(rid>0) printf("等待成功\n");
}
}
}
首先宏定义一下代表各种重定向,定义一个全局变量,文件名也设置一下
在我们显示基本消息并输入命令后,command获取到了命令字符串,此时我们就需要判断是否发生重定向
总代码如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define NUM 1024
#define SIZE 64
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#define NUM 1024
#define SIZE 64
#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir;
char* filename = NULL;
char* getUsername()
{
char* env = getenv("USER");
if(env) return env;
return NULL;
}
char* getHostname()
{
char* env = getenv("HOSTNAME");
if(env) return env;
return NULL;
}
char* getPwd()
{
char* env = getenv("PWD");
if(env) return env;
return NULL;
}
#define SkipSpace(filename) do{ while(isspace(*filename)) filename++; }while(0)
void checkRedir(char command[],int len)
{
char* end = command + len -1;
char* start = command;
while(end>=start)
{
// ls -al > log.txt
if(*end == '>')
{
//ls -al >> log.txt
if((*end-1)=='>')
{
*(end-1) = '\0';
filename = end+1;
SkipSpace(filename);
redir = AppendRedir;
break;
}
else
{
*end = '\0';
filename = end + 1;
SkipSpace(filename);
redir = OutputRedir;
break;
}
}
else if(*end == '<')
{
*end = '\0';
filename = end+1;
SkipSpace(filename);
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
int main()
{
while(1)
{
redir = NoneRedir;
filename = NULL;
//显示消息并输入
char command[NUM];
char* argv[SIZE];
int argc = 0;
printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());//打印
fgets(command,NUM,stdin); //输入完成后还会输入回车,导致换行
command[strlen(command)-1] = '\0';
//判断是否发生重定向
checkRedir(command,strlen(command));
//分割字符串
argv[argc++] = strtok(command," ");
while(argv[argc++] = strtok(NULL, " "));
pid_t id = fork();
if(id == 0)
{
//child
int fd = 0;
if(redir == InputRedir)
{
fd = open(filename,O_RDONLY);
dup2(fd,0);
}
else if(redir == OutputRedir)
{
fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd,1);
}
else if(redir == AppendRedir)
{
fd = open(filename,O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd,1);
}
execvp(argv[0],argv);
exit(1);
}
else
{
pid_t rid = waitpid(id,NULL,0);
if(rid>0) {};
}
}
}
我们的代码中,先进行的重定向,再进行了程序替换,这并不会影响,因为是进程的PCB中的指针指向的文件结构体,文件结构体中有文件标识符表,文件标识符表进行了重定向。
而程序替换也是进程PCB中的一个指针指向的虚拟内存空间,通过页表映射到物理内存,程序替换是物理内存代码与数据的覆盖,并不会改变进程(pid也不会变,依然是原来的进程)。那就也不必提PCB里面的内容了。
五、为什么要有标准错误
我们之前的重定向,一直在重定向0号(stdin)和1号(stdout),为什么还要有2号(stderr)呢?
大家看如下代码,打印结果也符合预期,都是往显示器上打印。
可是当我们将执行内容重定向到log.txt时,发现hello stderr没有被重定向,而是仍然输出在屏幕上
这是因为我们在进行输出重定向的时候,将1号文件标识符区域进行了覆盖,不管2号文件描述符什么事情,因此stderr老样子,维持不动。
如果我们非要将标准输出和标准错误的内容都放到log.txt里,则需要在后面添加 2>&1 。这句代码的意思是将&1里面的内容放到2号文件描述符里,由于1号文件描述符已经被 log.txt 覆盖了,因此2好描述符相当于也被 log.txt 覆盖,也就都打印到了log.txt里面
为什么这这样写可以呢?因为 ./myfile > log.txt 其实上是 ./myfile 1 > log.txt 。代表将log.txt里的内容放到1号文件描述符中。2>&1 也是同理,1号地址的内容放到2号
这样设计的目的,是让一个程序在运行时,将标准输出和标注错误放到不同的文件中,方便我们排查错误,如下就打印到了分别的两个文件中。
下一章:缓冲区