本章目标
1.重定向的原理
2.更新自主shell,支持重定向
3.理解一切皆文件,缓冲区
4.缓冲类型,验证FILE包含文件描述符
1.重定向的原理
如果要理解重定向的本质,我们要先明白文件描述符是如何进行分配的,下面我们写一段代码来进行测试
c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fda = open("loga.txt",O_WRONLY|O_CREAT|O_TRUNC);
int fdb = open("logb.txt",O_WRONLY|O_CREAT|O_TRUNC);
int fdc = open("logc.txt",O_WRONLY|O_CREAT|O_TRUNC);
printf("fda is %d\n",fda);
printf("fdb is %d\n",fdb);
printf("fdc is %d\n",fdc);
return 0;
}

我们看到,跟我上个章节所叙述的结果是一致的.
我们能够看到打开了三个文件,他们被分的文件描述符是 3,4,5
我们现在可以关闭把1号描述符关闭,我们知道这个文件指向是标准输出,我们关闭之后再看看结果

关闭文件的系统调用是这个,很简单,不做解释了,我们直接看现象
c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);
close(1);
int fda = open("loga.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fdb = open("logb.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fdc = open("logc.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fda is %d\n",fda);
printf("fdb is %d\n",fdb);
printf("fdc is %d\n",fdc);
return 0;
}

当我们重新编译之后去执行这个文件我们没有看到这个输出,这个是正常的,我们再创建文件的时候已经把一号文件描述符给关闭了
但是我们生成的文件都还在,我们查看第一个loga.txt

我们看到1号描述符的位置给了fda
因此我们可以得出一个结论就是文件描述符的本质是给新创建的文件一个空出来的,还没有被分配当中的最小的那一个位置.
这种把本应该输入文件的内容给了一个文件,我们把它叫做输出重定向
常见的还有向<输入重定向, >>追加重定向
而它的本质上就是覆盖掉0,1,2的文件描述符
我们去完成重定向的时候,肯定不是通过这种方式去实现的,直接用close去关闭
再Linux当中给我们提供了系统调用.
dup2


这个系统调用会用oldfd去覆盖newfd,也就是fd去覆盖012,它同样也支持去覆盖其他的文件描述符.它的返回值就是覆盖前的newfd
在这里我们先演示下用法
c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);
int fda = open("loga.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fda,1);
int fdb = open("logb.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fdc = open("logc.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fda is %d\n",fda);
printf("fdb is %d\n",fdb);
printf("fdc is %d\n",fdc);
return 0;
}

在这里是1的值也是fda,而不是fda在1的位置.所以fda分配的文件描述符仍然是3
2.更新自主shell,支持重定向
https://gitee.com/woodcola/linux-c/tree/master/shell
博主的shell,大家可以自行取用.

在今天我们的shell就不把它设为一个内建命令,如果我们的命令要是支持重定向的话
它一定是shell命令+>>/>/<+文件名我们至于要标记重定向信息以及拿到文件名即可
,我们可以通过给重定向信息创建全局变量,然后再创建子进程后程序替换前去进行重定向操作.
c
#define IsSpace(start) do\
{\
while(isspace(*start))\
{\
start++;\
}\
}while(0)
c
{
if(*(start+1)=='>')
{
*start='\0';
start++;
*start='\0';
start++;
IsSpace(start);
redirfilename = start;
redirection = appredirection;
break;
//追加重定向
}
//输出重定向
*start='\0';
start++;
IsSpace(start);
redirfilename=start;
redirection = outredirection;
break;
}
else if(*start=='<')
{
//输出重定向
*start='\0';
start++;
IsSpace(start);
redirfilename = start;
redirection = inredirection;
break;
}
else
{
start++;
}
}
return 0;
}
c
int execode()
{
pid_t id = fork();
if(id<0)
return -1;
if(id==0)
{
umask(0);
if(redirection==noredirection)
{
;
}
else if(redirection==inredirection)
{
int fd = open(redirfilename,O_RDONLY);
dup2(fd,0);
}
else if(redirection==outredirection)
{
int fd = open(redirfilename,O_WRONLY|O_TRUNC|O_CREAT,0666);
dup2(fd,1);
}
else if(redirection==appredirection)
{
int fd = open(redirfilename,O_WRONLY|O_APPEND|O_CREAT,0666);
dup2(fd,1);
}
else
{
;
}
printf("我是子进程,我是exec启动前: %d\n", getpid());
execvpe(argv[0],argv,myenv);
exit(1);
}
else
{
int stat = 0;
pid_t rid = waitpid(id,&stat,0);
if(rid==id)
{
lastcode = WEXITSTATUS(stat);
printf("wait child process success!->%d\n",lastcode);
}
}
return 0;
}
3.理解一切皆文件,缓冲区

在我们学习冯诺依曼体系的时候,我们知道我们外部设备有很多,键盘,磁盘,显示器,等等
它们想和cpu进行交互就一定要通过内存,那么就涉及到外设与内存进行io.他们的读写方法肯定是不一样的.在Linux当中这些外设被抽象成了文件.而这些读写方法是硬件厂商提供的.为了保持一致,Linux给他们提供一套标准.他们的可以通过函数指针的方式将自己的读写方法存到文件的操作集当中.


当我们打开硬件文件的时候使用同一套io的标准,这样就可以实现了接口上的统一.
在硬件层你怎么实现的我不管,但是在os层我用了同一个标准,当我需要你这种硬件的io操作的时候,我只需要直接调用即可.这样就实现了硬件层和软件层的解耦合
那么如何理解缓冲区呢?
在如何理解缓冲区,之前我们要先明确我们打开一个文件进行读写的一个流程
我们要先明白一件事,像我们上面那种直接使用系统调用进行读写这并不是一种常见的操作,系统调用是有成本的,os需要从用户态陷入到内核态,还有处理信号,调用特定的函数,进程的轮转调度.这些都是需要时间的.
而现代的编程语言为了提高效率,都会在语言层面开出一个缓冲区,在这个缓冲区满足特定的条件再把它刷新到特定的文件当中.
这样就减少了系统调用,提高了效率.
而再c/c++这两个语言来说,他们的缓冲区存在于FILE文件句柄,以及像iofstream这种文件流当中了.
这个缓冲区一定是被malloc出来的.
这也是就是为什么再之前我们写minnistl的时候,开空间总是1.5倍或者2倍来进行扩容,
像malloc这样的函数,它的底层封装的也是系统调用.这样减少系统调用的做法就是为了提高效率
而在c语言中,它又被分为了两个部分输入缓冲区和输出缓冲区.当满足条件了就会刷新
对于文件操作无论是何种,一定是先加载,在修改,最后再刷新.
所以我们之前谈到的缓冲区是语言层面的缓冲区.而ffush这种操作就是将语言层缓冲区的内容刷新到内核级文件的缓冲区当中.
所以像fopen这种函数,它返回值FILE文件句柄一定是在它的内部创建的
因为系统的调用的open它只会返回文件描述符
而在这个FILE文件句柄当中,它一定有的内容一定是包含以下三种东西的
文件描述符
文件缓冲区
文件的大部分信息
而fclose这种操作就是先关闭文件描述符,再刷新缓冲区,最后释放文件句柄.

4.缓冲类型,验证FILE包含文件描述符
从内核到磁盘上的刷新策略这是由os自主完成的,而从语言层缓冲区到内核缓冲区的这个过程就是由我们完成的了,我们会在特定的情况下区刷新缓冲区,这个特定的情况一般分为三种刷新策略或者叫做缓冲类型
1.向显示器文件刷新,行缓冲
顾名思义只有遇到了换行这个转义字符才会去刷新缓冲区
因为我们正常像显示器打印带上换行就会打印
我们现在测验一下
c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
// close(1);
// umask(0);
// int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
while(1)
{
printf("aaaaaaaaa\n");
sleep(1);
}
return 0;
}
每个一秒写一次,每个一秒打印一次

现象很明显,如果我们不带\0就不会刷新

验证全缓冲
所谓的全缓冲就是当我们向文件进行写入,也就是磁盘io的时候会进行全缓冲的刷新方式当缓冲区满了的情况才会刷新,因为大量的写入需要消耗时间今天就让它等10秒,只用再进程退出的时候才会进行刷新缓冲区,在这里将1关了可以让程序的内容直接写入到文件当中了.
c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
close(1);
umask(0);
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int cnt =10;
while(cnt--)
{
printf("aaaaaaaaa\n");
sleep(1);
}
return 0;
}

还有一种是直接无缓冲直接写入
这种就是用系统调用来进行的了,在前面我们已经试验过多次了,不做演示了
验证FILE包含文件描述符
有关于这个话题,再这里我们用一下3个代码来进行模拟这种现象
如果我们要证明file当中文件描述符,我们就需要通过库函数和系统调用来进行交替的使用来证明这个问题
c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
close(1);
umask(0);
int fd = open("log.txt",O_RONLY|O_CREAT|O_TRUNC,0666);
printf("dadadadadd\n");
close(fd);
return 0;
}
这段代码当我们运行的时候,在log.txt中没有任何结果,因为我们修改了1的位置的文件,当这个从标准输出变成其他文件,它的刷新策略就从,行刷新变为了全缓冲,当我们将fd关了只后,进程结束的时候会检查stdout的缓冲区,但是因为1的位置上的文件描述符已经被关闭了.我们想要进行刷新缓冲区到指定的文件就刷新不过去了.
我们就是想在这个程序里看到结果可以在close前刷新下stdout
c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
close(1);
umask(0);
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("dadadadadd\n");
fflush(stdout);
close(fd);
return 0;
}

第二份代码,我们可以顺便验证下标准错误是没有缓冲区的
在这里我们提及一下为什么会出现标准错误
如果没有标准错误,我们的日志信息和错误信息将会混在一起
标准错误的出现就是将这两种信息进行分离的
c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdlib.h>
#include<fcntl.h>
int main()
{
close(2);
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
perror("aaaaaa");
close(fd);
return 0;
}

成功跑起来了,我们没有去刷新stderr,但是也能够看到,这样就证明了它是不带缓冲区的
剩下的我们下一节再说,篇幅接下来写下去就太多了