目录
前言
书接上文【Linux】基础I/O----C语言文件操作与系统调用文件操作详情请点击,今天继续介绍【Linux】基础I/O----文件描述符与重定向
一、文件描述符
-
在OS系统中存在着很多被打开的文件,因此,需要对打开文件做管理(先组织、再描述),因此一定存在一个数据结构,该结构内部能够直接或间接保存文件属性和内容----
struct file,各个文件的结构用链表连接,这样对文件的管理就转变成了对链表的增删查改

-
同时文件会被不同的进程所打开,因此OS要区分不同进程打开的文件,进程PCB(task_struct)中有一个结构必须要能找到自己进程打开的文件----
struct files_struct* files -
在基础I/O----C语言文件操作与系统调用文件操作部分中,介绍了open函数,但是open函数的返回值----文件描述符 ,只知道返回值是一个整数,但是具体是什么我们并不清楚,现在我们将返回值打印,观察结果


- 从上面fd打印结果我们可以看到fd是从3开始的连续整数,因为进程需要管理打开的文件(多个),因此PCB并不会直接指向文件结构,而是先指向一个
struct files_struct结构(文件描述符表 ),该结构中有一个struct file* fd_array[]数组,保存着文件结构

- 文件描述符的本质就是:数组下标,那么为什么下标从3开始呢?因为下标0、1、2分别保存着stdin、stdout、stderr三个进程启动就默认打开的文件
- 从上述分析我们可以知道,对于一个进程而言,在操作系统角度,找到某个打开文件只能通过文件描述符(fd)来找,所以我们前面C语言接口打开文件返回值FILE* (结构体)中一定也封装了fd文件描述符
- 所以stdin、stdout、stderr中也会有fd信息

- 通过打印结果可以看到,stdin、stdout、stderr的文件描述符分别为0、1、2
-
C++中的cin、cout、cerr(类)中也一定封装了文件描述符 -
对于以上原理结论我们可通过内核源码验证

-
所以对于open函数,进程有task_struct、默认打开stdin、stdout、stderr,有files_struct,当使用open函数时,打开文件,并在files_struct指针数组中保存文件地址,file结构中能直接或间接找到文件属性和文件内核缓冲区中的文件内容。
-
对于write函数(write(3, "hello world")),首先我们会在用户定义的缓冲区中保存我们要写入的数据(char buffer[xxx]或者const char* str = "hello world"),再将内容拷贝给文件内核缓冲区中(并没有写入到磁盘文件中),具体什么时候写入到磁盘文件中,由操作系统决定进行刷新
-
读取磁盘文件内容也是读取文件内核缓冲区内容,当磁盘文件还没有加载到内核中时,则进程阻塞,进入阻塞队列,等待磁盘将文件内容和属性加载到内存中

文件描述符的分配规则:给新打开文件分配fd,从文件描述符表数组中寻找下标最小的、未被使用的下标作为该文件的fd(比如关闭0或者2下标文件,再新打开文件,我们可以发现,新文件的fd是0或者2)
二、重定向
引入
- 当我们关闭fd = 1的文件,再新打开一个文件时又会发生什么呢?

- printf函数将数据打印到显示器中,但是代码中我们关闭了fd = 1的文件(stdout),再新打开了一个文件,根据文件描述符的分配规则,该log.txt文件的fd = 1,所以printf原本写入到显示器文件的语句,写入到了log.txt文件中,这种现象叫做**(输出)重定向**
- 但是我们查看log.txt文件内容并没有任何内容

- 我们可以先屏蔽close(fd)代码,再运行,或者在关闭文件之前,
fflush(stdout):和语言缓冲区有关,发现log.txt文件中有了打印内容


dup2系统调用
- 系统调用函数dup2:实现重定向
- 比如新打开文件的fd = 3(oldfd),当我要将他重定向到1(newfd)下标时,将3的内容拷贝到1中,1则会指向该新文件,1、3指向同一个文件,最终fd和3中都是fd的内容,因此1和3(使用fd访问)都可以访问到打开的新文件
- 所以我们要进行重定向,应该是
dup2(fd, 1)
cpp
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("fd");
return 1;
}
dup2(fd, 1);
printf("hello linux\n");
return 0;
}

三、自主shell添加重定向功能
知识铺垫
- 有了重定向的认识,那么创建子进程后,子进程如何看待的父进程打开的文件?
- 子进程拷贝父进程的task_struct、文件描述符表
- 子进程的文件描述符表中保存着父进程打开的文件,并不需要再拷贝一份新的父进程打开的文件,因为只有创建一个新的文件才需要创建struct file结构
- 这样,子进程prinf在显示器上打印的时候和父进程都是在同一个显示器文件中进行打印
- 子进程并没有自己打开0、1、2,但是子进程继承了父进程的文件描述符表,默认打开标准输入、标准输出、标准错误
- 子进程如果关闭0、1、2,但是并不会影响父进程中的0、1、2,file结构中有一个引用计数 (
int ref_count)

- 如果我们做了程序替换(不会创建新进程),会影响历史打开文件吗?答案是并不会影响该进程原来打开的文件,因此程序替换只是替换数据段和代码段,并不影响文件

cpp
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("fd");
return 1;
}
dup2(fd, 1);
execl("/usr/bin/ls", "ls", "-al", NULL);
return 0;
}
通过结果我们可以看到,程序替换之后并没有向显示器上打印运行结果,而是将结果打印到了重定向之后的文件log.txt中
重定向功能实现
- 前面我们编写了一个自主shell,详情请点击查看
- 首先重定向可以是下面几种情况 ,因此我们需要识别 >、>>、<,然后获取重定向文件
ls -l -a > log.txt(输出重定向)ls -l -a >> log.txt(追加重定向)cat < log.txt(输入重定向)
cpp
//myshell.cc
//全局变量
#define NONE_REDIR 0 //无重定向
#define OUTPUT_REDIR 1 //输出重定向
#define APPEND_REDIR 2 //追加重定向
#define INPUT_REDIR 3 //输入重定向
std::string filename; //重定向文件名
int redir_type = NONE_REDIR; //初始为无重定向
- shell是一个死循环程序,因此我们需要在进行初始化,将文件清空、类型置为NONE_REDIR
cpp
void InitGlobal()
{
gargc = 0;
memset(gargv, 0, sizeof(gargv));
filename.clear();
redir_type = NONE_REDIR;
}
- 定义一个函数(
void CheckRedir(char cmd[]))检测是否是重定向,将ls -l -a > log.txt-> ls -l -a\0log.txt,filename = log.txt,redir_type = OUTPUT_REDIR,将ls -l -a交给cmd中,进行后面的普通命令行解析 - 同时如果
ls -l -a > log.txt中,如果 > 前后有空格,或者没有空格,或者有几个空格,我们也要对这样的情况进行处理
cpp
// 宏函数
#define TrimSpace(start) do\
{\
while(isspace(*start))\
{\
start++;\
}\
//重定向命令转换
void CheckRedir(char cmd[])
{
char* start = cmd;
char* end = cmd + strlen(cmd) - 1;
while(start <= end)
{
if(*start == '>')
{
// >>
if(*(start + 1) == '>')
{
//追加重定向
redir_type = APPEND_REDIR;
*start = '\0';
start += 2;
TrimSpace(start);
filename = start;
break;
}
// >
else
{
//输出重定向
redir_type = OUTPUT_REDIR;
*start = '\0';
start++;
//移除空格
TrimSpace(start);
filename = start;
break;
}
}
// <
else if(*start == '<')
{
//输入重定向
redir_type = INPUT_REDIR;
*start = '\0';
start++;
TrimSpace(start);
filename = start;
break;
}
else
{
start++;
}
}
}
- 将重定向指令经过上面解析之后,进入后面的普通命令解析,再判断是否是内建命令(否),再指向命令,在执行命令中,子进程根据redir_type的不同来执行
cpp
void ForAndExec()
{
pid_t id =fork();
if(id < 0)
{
perror("fork");
return;
}
else if(id == 0)
{
//子进程
if(redir_type == OUTPUT_REDIR)
{
int output = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(output < 0)
{
perror("output");
return;
}
dup2(output, 1);
}
else if(redir_type == APPEND_REDIR)
{
int appendfd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if(appendfd < 0)
{
perror("appendfd");
return;
}
dup2(appendfd, 1);
}
else if(redir_type == INPUT_REDIR)
{
int input = open(filename.c_str(), O_RDONLY);
if(input < 0)
{
perror("input");
return;
}
dup2(input, 0);
}
else
{
//不做处理
}
execvp(gargv[0], gargv);
exit(0);
}
else
{
//父进程:等待
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
}









