重定向+缓冲区
- 1.重定向
- 1.1重定向本质
- 1.2重定向接口
- 1.3重定向分类
- 1.3.1>输出重定向
- 1.3.2>>追加重定向
- 1.3.3<输入重定向
- 2.理解 >, >>, <
- 3.如何理解linux下一切皆文件
- 4.缓冲区
- 4.1理解缓冲区问题
- 4.1.1为什么要有缓冲区
- 4.1.2缓冲区刷新策略的问题
- 4.1.3缓冲区在哪里,指的是什么缓冲区
- 4.1.4如何解释fork问题
- 5.缓冲区该如何理解
- 5.1myStdio.h
- 5.2myStdio.c
- 5.2.1_fopen
- 5.2.2_fwrite
- 5.2.3_fflush
- 5.2.4_fclose
- 5.3main.c
- 6.缓冲区和OS的关系
自我名言:只有努力,才能追逐梦想,只有努力,才不会欺骗自己。
喜欢的点赞,收藏,关注一下把!
1.重定向
1 #include<stdio.h>2 #include<string.h>3 #include<sys/types.h>4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #include<stdlib.h>8 9 int main()10 {11 // close(0);12 // close(2);13 close(1); 14 umask(0); 15 int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); 16 if(fd == -1) 17 { 18 perror("open"); 19 exit(1); 20 } 21 printf("fd:%d\n",fd); 22 23 close(fd); 24 25 return 0; 26 }
close(1),为什么没有打印新建文件fd呢?
printf(“%d\n”,fd); printf会把内容打印到stdout文件中。
但是close(1)关闭标准输出stdout—>显示器,int fd=open();新打开的文件fd是1。
stdout–>fd–>1,虽然我们手动关闭了stdout,但是系统并不知道,还以为fd为1的位置是stdout,但是这个位置现在已经被新打开的文件占用了,所以打印到了新打开的文件里。
1 #include<stdio.h>2 #include<string.h>3 #include<sys/types.h>4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #include<stdlib.h>8 9 int main()10 {11 // close(0);12 // close(2);13 close(1);14 umask(0);15 int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);16 if(fd == -1)17 {18 perror("open");19 exit(1);20 }21 printf("fd:%d\n",fd);22 23 //这里必须刷新一下,不然log.txt里面没有内容,这里和缓冲区有关,下面讲 24 fflush(stdout); 25 close(fd); 26 27 return 0; 28 }
本来应该打印到显示器文件中,但是却写到文件里了。------>重定向
1.1重定向本质
1.2重定向接口
这个主要介绍dup2函数。
int dup2(int oldfd, int newfd);
那怎么使用dup2来实现刚才的效果,把打印到显示器内容,写入到"log.txt"
是上面那样写的,还是下面那样写的,我们分析分析。
画图分析
因此正确写法如下
4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #include<stdlib.h>8 9 int main()10 {11 // close(0);12 // close(2);13 // close(1);14 umask(0);15 int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);16 if(fd == -1)17 {18 perror("open");19 exit(1);20 }21 //重定向 22 dup2(fd,1); 23 printf("fd:%d\n",fd); 24 25 //这里必须刷新一下,不然log.txt里面没有内容 26 fflush(stdout); 27 close(fd); 28 29 return 0; 30 }
1.3重定向分类
1.3.1>输出重定向
上面内容就是输出重定向,把新打开文件的fd重定向到fd为1(默认为显示器)的位置。
1.3.2>>追加重定向
1 #include<stdio.h>2 #include<string.h>3 #include<sys/types.h>4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #include<stdlib.h>8 9 int main()10 {11 // close(0);12 // close(2);13 // close(1);14 umask(0);15 // int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);16 int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);17 if(fd == -1)18 {19 perror("open");20 exit(1);21 }22 //重定向23 dup2(fd,1);24 printf("你好\n");25 printf("吃了吗\n"); 26 27 //这里必须刷新一下,不然log.txt里面没有内容 28 fflush(stdout); 29 close(fd); 30 31 return 0; 32 }
1.3.3<输入重定向
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>int main()
{ // close(0); // close(2); // close(1); umask(0); // int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); // int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); int fd=open("log.txt",O_RDONLY); if(fd == -1) { perror("open"); exit(1); } //输入重定向 dup2(fd,0); char outbuffer[64]; while(1) { printf("<"); if(fgets(outbuffer,sizeof(outbuffer),stdin) == NULL) break; printf("%s",outbuffer); }return 0;}
2.理解 >, >>, <
在前面实现过一个自己的shell,现在我们给这个shell增加重定向功能来理解重定向。
//增加一个分割指令和文件名的函数
commandstrtok(lineCommand);
分割的时候,我们需要知道重定向是什么类型,文件是什么名字,因此增加两个全局变量来记录。
//重定向类型 //第一个为初始重定向 #define DEFAULT_REDIR 0 #define INPUT_REDIR 1 #define OUTPUT_REDIR 2 #define ERROR_EDIRR 3 //重定向类型+文件名 int redirType=DEFAULT_REDIR; char* redirFile=NULL;
分割函数
//这里找文件名没有写成函数,而写个宏#define trimSpace(start) do{\while(isspace(*start)) ++start;\}while(0)void commandstrtok(char* cmd){assert(cmd);char* start=cmd;char* end=cmd+strlen(cmd);while(start < end){if(*start == '<'){*start=0;++start;//这里可能 ls -a -l > log.txt,trimSpace(start);redirType=INPUT_REDIR;redirFile=start;break;}else if(*start == '>') { *start=0; ++start; if(*start == '>') { redirType=APPEND_REDIR; ++start; } else { redirType=OUTPUT_REDIR; } trimSpace(start); redirFile=start; break; } else{ ++start;}}
因为命令是子进程执行的,真正重定向的功能一定是由子进程来完成的,
但是如何重定向是父进程要告知给子进程的。
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{ switch(redirType){case INPUT_REDIR:{int fd=open("log.txt",O_RDONLY);if(fd <0){perror("open");return 1;}//重定向文件已经打开了dup2(fd,0);}break;case OUTPUT_REDIR:case APPEND_REDIR:{umask(0);int flags=O_WRONLY|O_CREAT;if(redirType == OUTPUT_REDIR) flags|=O_CREAT;else flags|=O_APPEND; int fd=open("log.txt",flags,0666);if(fd < 0){perror("open");return 1;}dup2(fd,1); }break;default:printf("bug?\n");break;}//程序替换execvp(myargv[0],myargv);exit(1);
}
shell完整代码
问:子进程重定向会影响父进程吗?
不会
问:指向程序替换的时候,会不会影响曾经进程打开的重定向文件呢?
不会
3.如何理解linux下一切皆文件
Linux下一切皆文件,那键盘,显示器,磁盘,网卡等硬件在linux下都是文件吗?
可以这样说的。
站在struct file上层看来,所有的设备和文件,统一都是struct file--------->Linux下一切皆文件。
这里可能有这样一个问题,如果同一个文件被多个指针指向,但是某个进程把这个文件关了,会不会影响其他进程对这个文件的操作呢?
其实并不会,一个被打开的文件有引用计数。
表明有几个指针指向这个文件,这样做是因为一个文件可能被多个指针指向。如果某个进程关闭文件,影响到其他进程,就不能保证进程的独立性。close关闭文件,其实并不是真正关闭文件,只是把引用计数减1,当只有一个指针指向这个文件,close关闭文件才是真正关闭这个文件。
4.缓冲区
先看一种现象。
1 #include<stdio.h> 2 #include<string.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<unistd.h> 7 #include<stdlib.h> 8 9 int main() 10 { 11 printf("hello printf\n"); 12 fprintf(stdout,"%s\n","hello fprintf"); 13 14 15 const char* output="hello write\n"; 16 write(1,output,strlen(output)); 17 18 19 return 0; 20 }
1 #include<stdio.h>2 #include<string.h>3 #include<sys/types.h>4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #include<stdlib.h>8 9 int main()10 {11 printf("hello printf\n");12 fprintf(stdout,"%s\n","hello fprintf");13 14 15 const char* output="hello write\n";16 write(1,output,strlen(output));17 18 fork(); 19 return 0; 20 }
对比两张图片,第二段代码补充fork(创建子进程),重定向之后,C接口的函数,被打印了两次,系统调用的接口前面都只是打印了一次。
这是什么原因?现在只是知道肯定是fork函数有关。
其实这里也和缓冲区有关。
4.1理解缓冲区问题
缓冲区本质就是一段内存!!!
4.1.1为什么要有缓冲区
这里讲一个小故事,帮助理解。
在四川的张三要给远在北京的李四送一个键盘。
张三为了节省时间,选择2;
现实中快递行业的意义?
节省发送者的时间。
进程把数据打包给磁盘,是一个很慢的过程,如果让进程一直等着显然不太好。因此我们也需要一个快速的方式----->缓冲区
缓冲区的意义是什么呢?
节省进程进行数据IO的时间。
但是我们在用文件写的接口时,并没有专门拷贝数据啊?(以fwrite为例)
其实与其理解fwrite是写入到文件的函数,倒不如理解fwrie是拷贝函数,将数据从进程拷贝到"缓冲区"或者“外设”中。
4.1.2缓冲区刷新策略的问题
把东西给顺丰之后,那顺丰什么时候发货呢?
张三第二天在想寄一个鼠标给李四,当走到快递站点发现自己昨天的快递还没有发走,就问快递人员,快递人员说,我们的快递都是用大货车和飞机发送的,就这一个快递不可能马上就发走,等到满足发送件货才发。
缓冲区的刷新策略:
不同外设IO访问速度是不同的。
缓冲区结合具体的设备,定制自己的刷新策略。(3策略,2特殊)
3策略
a.立即刷新 ----> 无缓冲
b.行刷新 ----> 行缓存 ----> 显示器 (显示器是给人看的,如果一次给很多信息,人看着不舒服)
c.缓冲区满 ----> 全缓存 ----> 磁盘文件(显示器除外)
2特例
1:用户强制刷新 (fflush)
2:进程退出 (进程退出要刷新缓冲区)
4.1.3缓冲区在哪里,指的是什么缓冲区
C接口打印了两次,系统调用接口打印了一次,这种现象一定和缓冲区有关。
虽然现在不知道缓冲区在哪里,但是我们知道缓冲区一定不在内核中。
因为C接口底层调用的是系统调用接口,如果缓冲区在内核中,write也应该打印两次。
那缓冲区到底在哪?
我们之前谈论的所有缓冲区,都指的是用户级语言层面给我们提供的缓冲区。
还记得C程序默认给我们打开stdin,stdout,stderr都是------->FILE*------>指向FILE结构体,结构体包含-------> fd,其实还包含一个缓冲区。
因此,缓冲区在FILE结构体中。
所以当我们自己要强制刷新:fflush(文件指针),关闭:fclose(文件指针)传的都是FILE*。因为缓存区在FILE结构体中。
4.1.4如何解释fork问题
代码结束之前,创建子进程。
1.我们没有进行>输出重定向,看到了三条信息。
stdout默认使用的是行刷新,在fork之前,两条C函数已经将数据打印输出到到显示器上,你的FILE内部,进程内部不存在对应的数据了。这时创建子进程,等到子进程,父进程退出的时,都要刷新缓冲区,但是这时缓冲区已经没有内容可以刷新了。因此C函数打印两条信息。
2.>输入重定向之后,C接口打印两次
当我们进行>,写入文件就不再是显示器,而是普通文件,采用的刷新策略是全缓存,之前2条C打印函数,虽然带了\n,但是不足与将stdout缓冲区写满,数据并没有被刷新。
fork的时候,stdout属于父进程,创建子进程紧跟着就是进程退出,谁先退出,一定要进行缓冲区刷新(缓存区刷新---->就是修改)
这时就有了写时拷贝!! 因此C接口,数据最终会显示两份。
3.write为什么前后只打印一次
上面过程都是write无关,write没有FILE,而用的是fd,就没有C提供的缓冲区。
5.缓冲区该如何理解
自己写一个简易的缓冲区,来帮助理解"数据刷新策略"+“数据如何缓存”
5.1myStdio.h
1 #pragma once 2 #include<errno.h> 3 #include<stdio.h> 4 #include<string.h> 5 #include<sys/types.h> 6 #include<sys/stat.h> 7 #include<fcntl.h> 8 #include<unistd.h> 9 #include<stdlib.h> 10 11 #define SIZE 1024 12 #define SYNC_NOW 1 13 #define SYNC_LINE 2 14 #define SYNC_FULL 3 15 16 typedef struct _FILE{ 17 int flags;//刷新方式 18 int fileno; //文件描述符19 int capacity;//buffer容量 20 int size;//buffer当前使用量 21 char buffer[SIZE]; //缓冲区22 }_FILE; 23 24 25 _FILE* _fopen(const char* path_name,const char* mode); 26 void _fwrite(const void* ptr,int num,_FILE* fp); 27 void _fclose(_FILE* fp); 28 void _fflush(_FILE* fp);
5.2myStdio.c
5.2.1_fopen
_FILE* _fopen(const char* path_name,const char* mode)
{assert(path_name);int flags=0;int defaultmode=0666;if(strcmp(mode,"r") == 0){flags|=O_RDONLY;}else if(strcmp(mode,"w") == 0){flags|=(O_WRONLY|O_CREAT|O_TRUNC);}else if(strcmp(mode,"a") == 0){flags|=(O_WRONLY|O_CREAT|O_APPEND);}int fd=0;if(flags & O_RDONLY){fd=open(path_name,flags);}else{fd=open(path_name,flags,defaultmode); }if(fd < 0){const char*err=strerror(errno);write(2,err,strlen(err));return NULL;//这就是为什么创建文件失败,返回NULL}_FILE* fp=(_FILE*)malloc(sizeof(_FILE));assert(fp);fp->flags=SYNC_LINE;fp->fileno=fd;fp->capacity=SIZE;fp->size=0;memset(fp->buffer,0,SIZE);return fp;
}
5.2.2_fwrite
void _fwrite(const void* ptr,int num,_FILE* fp)
{ //写到缓冲区里 memcpy(fp->buffer+fp->size,ptr,num); //这里没有考虑缓冲区溢出问题 fp->size+=num; //判断是否要刷新 if(fp->flags & SYNC_NOW) { write(fp->fileno,fp->buffer,fp->size); fp->size=0;//清空缓冲区 } else if(fp->flags & SYNC_LINE) { if(fp->buffer[fp->size-1] == '\n') //这里也没有考虑abc\ndef这种形式,如果是这样的可以用for循环 { write(fp->fileno,fp->buffer,fp->size); fp->size=0; } } else if(fp->flags & SYNC_FULL) { if(fp->size == fp->capacity) { write(fp->fileno,fp->buffer,fp->size); fp->size=0; } }
}
5.2.3_fflush
void _fflush(_FILE* fp)
{ if(fp->size > 0) write(fp->fileno,fp->buffer,fp->size); fsync(fp->fileno); //把数据强制从内核缓冲区刷新到磁盘 fp->size=0;
}
这里引入了内核缓冲区,下面解释。
5.2.4_fclose
void _fclose(_FILE* fp)
{ _fflush(fp); close(fp->fileno);
}
5.3main.c
行刷新
1 #include"myStdio.h"2 3 4 int main()5 {6 _FILE* fp=_fopen("log.txt","w");7 if(fp == NULL)8 {9 perror("_fopen");10 return 1;11 }12 13 int cnt=10;14 const char* msg="hello linux\n";15 while(1)16 {17 _fwrite(msg,strlen(msg),fp);18 sleep(1);19 printf("count:%d\n",cnt--);20 if(cnt == 0) break;21 } 22 _fclose(fp);23 24 return 0;25 }
退出刷新
1 #include"myStdio.h"2 3 4 int main()5 {6 _FILE* fp=_fopen("log.txt","w");7 if(fp == NULL)8 {9 perror("_fopen");10 return 1;11 }12 13 int cnt=10;14 const char* msg="hello linux";15 while(1)16 {17 _fwrite(msg,strlen(msg),fp);18 sleep(1);19 printf("count:%d\n",cnt--); 20 if(cnt == 0) break;21 } 22 _fclose(fp); 23 24 return 0; 25 }
立即刷新
1 #include"myStdio.h"2 3 4 int main()5 {6 _FILE* fp=_fopen("log.txt","w");7 if(fp == NULL)8 {9 perror("_fopen");10 return 1;11 }12 13 int cnt=10;14 const char* msg="hello linux";15 while(1)16 {17 _fwrite(msg,strlen(msg),fp);18 _fflush(fp); 19 sleep(1);20 printf("count:%d\n",cnt--);21 if(cnt == 0) break;22 } 23 _fclose(fp);24 25 return 0; 26 }
6.缓冲区和OS的关系
这里是由write函数,直接把数据写到磁盘上的文件中吗?
其实并不是这样的。
write并不是直接把缓冲区里的内容刷新到文件中,在打开文件对应的struct file{}结构体中其他有一个方法指针,还有一个指向内核缓冲区的指针,系统会把FILE结构体里面缓冲区内容通过struct file{}找到内核缓冲区,再由write()拷贝到内核缓冲区,然后再由这个内核缓冲区把内容刷新到文件中区,至于如何刷新和用户毫无关系,我们所知道的行刷新等,这是由语言层面所分类的,而内核缓冲区刷新由OS自主决定。
那么我们该如何证明有这个内核缓冲区呢?
超级大佬可以证明,这里证明不了,但是我们可以看到接口。
还有一个问题,如果OS突然宕机了会发生什么情况?
数据肯定会丢失。如果还是按照OS规定的内核缓冲区刷新策略肯定是不行的。
我们希望可以在用户层就告知OS,内核缓冲区不要再给我缓存了,赶紧把数据刷新到磁盘中。
这里介绍fsync函数
强制性把该文件对应的内核缓冲区数据持久到磁盘。就是OS不要给我缓存了,赶紧把数据给我更新到磁盘上。