
 送给大家一句话:
人真正的名字是:欲望。所以你得知道,消灭恐惧最有效的办法,就是消灭欲望。 – 史铁生 《我与地坛》
 
开始了解重定向
- 1 前言
- 2 重定向与缓冲区
- 2.1 文件描述符分配规则
- 2.2 重定向的现象
- 2.3 重定向的理解
- 2.4 缓冲区的理解
 
- 3 进程与重定向
- Thanks♪(・ω・)ノ谢谢阅读!!!
- 下一篇文章见!!!
1 前言
上一篇文章我们复习了C文件IO相关操作,了解了linux下的文件系统调用(open write read ),认识了文件描述符fd值,今天我们来学习重定向和缓冲区,这个缓冲区之前遇到过很多次,比如进度条项目的刷新缓冲区操作。然后我们可以来尝试封装一下系统调用,模拟C语言的文件库。
2 重定向与缓冲区
2.1 文件描述符分配规则
接下来我们来了解重定向!
 首先我们来看fd文件描述符的分配规则,我们写一段代码来看:
    1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 #include<unistd.h>6 #include<string.h>7 #include<stdlib.h>8 9 const char* filename = "log.txt";10 11 12 int main()13 {14 	15   int fd = open("myfile", O_RDONLY);16   if(fd < 0){17   perror("open");18     return 1;19   }20   printf("fd: %d\n", fd);21   close(fd);22   return 0;    23   }
我们运行来看:
 
 这和我们的预期是一样的,我们文件操作那篇文章讲解了fd 的 0 1 2 分别代表了标准输入,标准输出,标准错误。那么在创建的文件描述符很自然的就使用了3! 那么加入我们关闭012中的文件呢,那么新打开的文件描述符会是3吗???
    1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 #include<unistd.h>6 #include<string.h>7 #include<stdlib.h>8 9 const char* filename = "log.txt";10 11 12 int main()13 {14 	close(0); 15   int fd = open("myfile", O_RDONLY);16   if(fd < 0){17   perror("open");18     return 1;19   }20   printf("fd: %d\n", fd);21   close(fd);22   return 0;    23 }
来看:
 
 我们新创建的文件的文件描述符就成了 0 !
 再来试试:
- 关闭 2 close(2)-->新创建的文件的文件描述符就成了 2
- 关闭 1 close(1)-->就什么也打印不出来(标准输出被关闭自然打印不出来)
- 关闭 0 2 close(2)close(0)--> 新创建的文件的文件描述符就成了 0
这样我们大致可以总结出来一个结论:
 文件描述符的分配规则:进程会查自己的文件描述符表,分配最小的并且没有被使用过的 fd
2.2 重定向的现象
刚才我们看到了文件描述符的分配规则,也发现关闭1 (标准输出)就我们打印出来,我们再来探究一下:如果我们关闭了 标准输出,并打开了一个文件,那么该文件就成为了1 ,来看看会发生什么现象:
  1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 #include<unistd.h>6 #include<string.h>7 #include<stdlib.h>8 9 const char* filename = "log.txt";10 11 12 int main()13 {14 15   close(1);16 17   int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC , 0666);18   if(fd < 0){19   perror("open");20     return 1;21   }22   printf("fd: %d\n", fd);23   fprintf(stdout,"fprintf fd :%d\n",fd);24 25   fflush(stdout);    26   close(fd);27   return 0 ;
来看效果:
 
 我们发现并没有在显示器打印出来,而是在新文件log.txt中打印出来了!!!
- 因为我们关闭了1号文件 (标准输出
- 然后又打开了一个文件,那么1号下标就成了该新文件的文件描述符。
- 又因为stdout是对系统的封装,里面封装了 1 号文件
- 那么stdout 的指向没有发生改变(还是1 号文件),所以自然就打印到了log.txt中去了!.
这种技术就叫做 重定向,也就是把本应该打印到显示器的内容打印到了一个其他文件中。
 其本质就是在内核中改变文件描述符表特定下标的内容,和上层无关!
 
可是如果不加入fflush 呢???结果是log.txt文件里也什么都没有?!这就涉及缓冲区的内容了。
 首先 一个文件都有一个方法表和内核文件缓冲区。同样在C语言中 (stdin stdout stderr都是struct FILE* 的指针,)文件结构体里面一定封装了fd描述符,而且也封装了语言级的缓冲区。以往的 printf fprintf都是先讲内容写到语言级的缓冲区里在写到文件内核缓冲区了,所以fflush作为一个系统调用,就是刷新文件内核缓冲区,使其输出到文件中!!!
 而为什么不加入fflush 呢结果是log.txt文件里也什么都没有呢??? 就是因为内容写入到文件内核缓冲区里还没有刷新就被close关闭了,所以还没刷新就文件被关闭了,还怎么打印到文件中。而且我们不写fflush 不写close 就可以成功打印到文件中!!!
2.3 重定向的理解
完成重定向的操作肯定不是像我们上面做的那样简单粗暴(又要删除,又要创建新文件),我们有一个系统调用dup2
NAMEdup, dup2, dup3 - duplicate a file descriptorSYNOPSIS#include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);#define _GNU_SOURCE             /* See feature_test_macros(7) */#include <fcntl.h>              /* Obtain O_* constant definitions */#include <unistd.h>int dup3(int oldfd, int newfd, int flags);每次我们使用dup2 就可以实现重定向 ,来看其功能描述。
  dup2() makes newfd be the copy of oldfd, closing newfd first if necessary
 通过描述可以知道:
- 首先文件描述符的拷贝不是对数字的拷贝,而是下标所对应内容(文件结构体指针)的拷贝
- 然后是实现了将oldfd的内容拷贝到newfd(多个下标指向一个文件),dup2( fd , 1 )就是将fd指向的文件拷贝到1 (标准输出)里。
这样通过dup2既可以完成重定向:
    1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 #include<unistd.h>6 #include<string.h>7 #include<stdlib.h>8 9 const char* filename = "log.txt";10 11 12 int main()13 {14 15   int fd = open(filename , O_CREAT | O_WRONLY | O_TRUNC);16 17   dup2(fd,1);18 19   printf("Hello world!\n");20   fprintf(stdout,"Hello world!\n");                                              21   close(fd);22   return 0;23 }                                          
来看效果:
 
 这样也实现了重定向的功能!!!比简单粗暴的关闭stdout 再打开新文件好多了!!!
 我们也可以将O_TRUNC 换成O_APPEND,这样每次都是追加内容,所以我们的命令也有了对应:
- >相当于- O_TRUNC覆盖
- >>相当于- O_APPEND追加
就这么简单!!!
2.4 缓冲区的理解
缓冲区分为:用户级缓冲区 和 内核缓冲区。缓冲区的作用是:解耦和提高使用者效率。
类比生活中,缓冲区就是类似一个超市,我们不需要去工厂进行采购,这样十分麻烦,而直接去超市就解决了问题。也可以比作顺丰快递,我们想要寄东西,只需要交给快递站就可以,我们不需要考虑快递怎么到达目的地!
 所以我们操作系统与语言层中,我们的printf 和 fprintf就不需要考虑我们如何将内容写入到文件中,这不是他们需要关心的事情!!!
那为什么会拷贝两次呢???为什么会有两个缓冲区, **因为系统调用是有成本的!**操作系统可能正在执行其他任务,所以为了注重用户体验,就需要缓冲区(也就提高printf fprintf 的效率,因为我们实际上还没有将内容打印到文件,只是打印到了缓冲区,可能调用10次pringtf ,但是只需要刷新一次,是不是刷新IO的效率就高了)
- 缓冲区可以理解为一段内存空间
- 缓冲区是为了给上层通过良好的IO体验(语言 --> 操作系统 --> 磁盘)
- 缓冲区的刷新策略是什么呢? - 立即刷新 语言层:fflush() , 系统调用:fysnc(int fd)相当于无缓冲
- 行刷新 :显示器(配合人的阅读习惯)
- 全缓冲,缓冲区写满才刷新:普通文件
- 特殊情况 :进程退出会自动刷新缓冲区
 
- 立即刷新 
截图内核的刷新策略我们不关心,就针对用户层面来研究。
3 进程与重定向
我们再来与先前的进程控制结合一下,来看:
  1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 #include<unistd.h>6 #include<string.h>7 #include<stdlib.h>8 9 const char* filename = "log.txt";10 11 12 int main()13 {14 15   int fd = open(filename , O_CREAT | O_WRONLY | O_TRUNC);16 17   18 19   printf("Hello printf!\n");20   fprintf(stdout,"Hello fprintf!\n");21 22   const char *msg = "hello write!\n";23   write(1,msg,strlen(msg));                                                                                                                                                   24                                                                                                                                            25   fork();      26   close(fd);27	  return 0;28 }
我们运行一下来看效果:
 
啊???这是什么现象???
- 显示器与文件的打印顺序不一样
- 打印次数不一样?!
- 现象 1: 是因为显示器采用行刷新所以每次换行就会打印出来,普通文件采用全缓冲,最后才会打印出来,打印顺序类似入栈出栈。
- 现象 2 : 按理说我们fork()之后,创建了子进程,子进程会继承父进程的代码与数据。那么为什么显示器只打印了一遍呢???因为显示器采用行刷新,fork的时候,语言缓冲区和内核缓冲区里没有内容,所以子进程不会打印出来。而向文件打印时,fork之前语言缓冲区有内容(printf fprintf ),内核缓冲区有内容(write)。fork后 ,子进程会拷贝一份数据也就语言层的缓冲区被打印了两次,内核缓冲区不会拷贝给子进程(不是用户级,所有用户共享)
缓冲区就在struct file 内部!每个文件都有自己的缓冲区。