目录
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
1.内容回顾:
(1)文件=文件内容+文件属性。 文件属性也是数据,即使你创建一个空文件,也要占据磁盘。
(2)文件操作=文件内容的操作+文件属性的操作。有可能在操作文件的过程中既改变内容,也改变属性。
(3)“打开”文件是将文件的属性或内容加载到内存中,这是由冯诺依曼体系决定的。
(4)是不是所有的文件,都会处于被打开的状态? 绝对不是!
没有被打开的文件在哪里?在磁盘存储。
(5)打开的文件(内存文件)和磁盘文件
(6)通常我们打开文件,访问文件,关闭文件,是谁在进行相关操作?
fopen、fclose、fread、fwrite...这些是当我们的文件程序,运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。这些是进程在操作!
1.1 通过代码回顾C文件接口:
hello.c写文件
#include <stdio.h> #include <string.h> int main() { FILE* fp = fopen("myfile", "w"); if (!fp) { printf("fopen error!\n"); } const char* msg = "hello linux!\n"; int count = 5; while (count--) { fwrite(msg, strlen(msg), 1, fp); } fclose(fp); return 0; }
hello.c读文件
#include <stdio.h> #include <string.h> int main() { FILE* fp = fopen("myfile", "r"); if (!fp) { printf("fopen error!\n"); } char buf[1024]; const char* msg = "hello bit!\n"; while (1) { //注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明 ssize_t s = fread(buf, 1, strlen(msg), fp); if (s > 0) { buf[s] = 0; printf("%s", buf); } if (feof(fp)) { break; } } fclose(fp); return 0; }
输出信息到显示器,你有哪些方法
#include <stdio.h> #include <string.h> int main() { const char* msg = "hello fwrite\n"; fwrite(msg, strlen(msg), 1, stdout); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); return 0; }
1.2 stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
1.3 总结
打开文件的方式:
如上,是我们之前学的文件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉及学习。
2.编程演示文件操作:
这里我们在Linux系统中实现C语言版的文件操作:
#include<stdio.h> #include<unistd.h> int main() { //1.默认这个文件会在哪里形成呢? 当前路径(进程所在的路径) //2.关注一下文件清空的问题 FILE *fp=fopen("log.txt","w"); //写入 if(fp == NULL) { perror("fopen"); return 1; } printf("mypid: %d\n",getpid()); while(1) { sleep(1); } const char* msg = "Test for myfile"; int cnt =1; while(cnt<10) { fprintf(fp,"%s: %d\n",msg,cnt++); } fclose(fp); }
2.1 什么是当前工作路径?
cwd -> /home/study/lesson/lesson18
current work directory
这里我们更改一下我们这个路径:
#include<stdio.h> #include<unistd.h> int main() { chdir("/home/study/lesson"); //更改当前进程的工作路径,用于测试 //1.默认这个文件会在哪里形成呢? 当前路径(进程所在的路径) //2.关注一下文件清空的问题 FILE *fp=fopen("log.txt","w"); //写入 if(fp == NULL) { perror("fopen"); return 1; } printf("mypid: %d\n",getpid()); while(1) { sleep(1); } const char* msg = "Test for myfile"; int cnt =1; while(cnt<10) { fprintf(fp,"%s: %d\n",msg,cnt++); } fclose(fp); }
2.2 在Linux环境下,测试C语言中的文件操作
#include<stdio.h> #include<unistd.h> int main() { //chdir("/home/study/lesson"); //更改当前进程的工作路径,用于测试 //1.默认这个文件会在哪里形成呢? 当前路径(进程所在的路径) //2.关注一下文件清空的问题 //3.测试其他操作:r,w,r+,w+,a,... //FILE *fp=fopen("log.txt","w"); //写入 FILE *fp=fopen("log.txt","a"); // a:追加操作 if(fp == NULL) { perror("fopen"); return 1; } //printf("mypid: %d\n",getpid()); //while(1) //{ // sleep(1); //} const char* msg = "Test for myfile"; int cnt =1; while(cnt<=5) { fprintf(fp,"%s: %d\n",msg,cnt++); } fclose(fp); }
(1)测试C语言的文件操作“a”:
man fopen:
(2)测试C语言的文件操作“w”:
当我们以"w"方式打开文件,准备写入的时候,其实文件已经先被清空了!
“a”;追加写入,不断的往文件中新增内容—>追加重定向!
(3)测试C语言的文件操作"fgets"(文件读取):
#include<stdio.h> #include<unistd.h> int main() { //chdir("/home/study/lesson"); //更改当前进程的工作路径,用于测试 //1.默认这个文件会在哪里形成呢? 当前路径(进程所在的路径) //2.关注一下文件清空的问题 //3.测试其他操作:r,w,r+,w+,a,... //FILE *fp=fopen("log.txt","w"); //写入 //FILE *fp=fopen("log.txt","a"); // a:追加操作 FILE *fp=fopen("log.txt","r"); //读入 if(fp == NULL) { perror("fopen"); return 1; } //测试读取操作"r",fgets char buffer[64]; while(fgets(buffer,sizeof(buffer),fp) != NULL) { printf("echo: %s",buffer); } //printf("mypid: %d\n",getpid()); //while(1) //{ // sleep(1); //} //const char* msg = "Test for myfile"; //int cnt =1; //while(cnt<=5) //{ // fprintf(fp,"%s: %d\n",msg,cnt++); //} fclose(fp); }
2.3 回归理论:
1.当我们向文件写入的时候,最终是向磁盘写入!而磁盘是硬件,只有操作系统才有资格向硬件写入!
2.所有的上层访问文件的操作,都必须贯穿操作系统,而操作系统是如何被上层使用的呢?是必须使用操作系统提供的相关系统调用!
3.那么我们如何理解printf?这是封装了的系统接口。
4.所有得语言都对系统接口做了封装!
5.那为什么要封装?
(1)原生系统接口,使用成本比较高
(2)直接使用原生操作系统接口,语言不具备跨平台性!
6.那么封装是如何解决跨平台性问题的呢?
通过穷举所有的底层接口+条件编译!
7.C库提供的文件访问接口与系统调用是上下层关系!
8.不同的语言有不同的文件访问接口!这就是我们必须学习文件级别的系统接口!
9.进程和打开文件的关系:内存级的关系
3. 系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问.
先来直接以代码的形式,实现和上面一模一样的代码:
hello.c 写文件:hello.c读文件#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { umask(0); int fd = open("myfile", O_WRONLY | O_CREAT, 0644); if (fd < 0) { perror("open"); return 1; } int count = 5; const char* msg = "hello bit!\n"; int len = strlen(msg); while (count--) { write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据 } close(fd); return 0; }
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd = open("myfile", O_RDONLY); if (fd < 0) { perror("open"); return 1; } const char* msg = "hello bit!\n"; char buf[1024]; while (1) { ssize_t s = read(fd, buf, strlen(msg));//类比write if (s > 0) { printf("%s", buf); } else { break; } } close(fd); return 0; }
4. 系统接口介绍
4.1 open (man 2 open)接口
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char* pathname, int flags); int open(const char* pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags : 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数 :
O_RDONLY: 只读打开
O_WRONLY : 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND : 追加写
返回值:
成功:新打开的文件描述符
失败: - 1mode_t理解:使用man mode查看。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
#include<stdio.h> #define PRINT_A 0x1 //0000 0001 #define PRINT_B 0x2 //0000 0010 #define PRINT_C 0x4 //0000 0100 #define PRINT_D 0x8 //0000 1000 #define PRINT_DFL 0x0 //open void Show(int flags) { if(flags & PRINT_A) printf("hello, A\n"); if(flags & PRINT_B) printf("hello, B\n"); if(flags & PRINT_C) printf("hello, C\n"); if(flags & PRINT_D) printf("hello, D\n"); if(flags == PRINT_DFL) printf("hello, Defult\n"); } int main() { printf("PRINT_DFL:\n"); Show(PRINT_DFL); printf("PRINT_A:\n"); Show(PRINT_A); printf("PRINT_B:\n"); Show(PRINT_B); printf("PRINT_A 和 PRINT_B\n"); Show(PRINT_A | PRINT_B); printf("PRINT_C | PRINTF_D:\n"); Show(PRINT_C | PRINT_D); printf("PRINT All:\n"); Show(PRINT_A | PRINT_B | PRINT_C |PRINT_D); return 0; }
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main() { int fd = open("log.txt",O_WRONLY | O_CREAT); if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); return 0; }
两个参数的open没有指明新建的文件的权限问题!
umask不仅仅是命令,也是一个系统级的接口!
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main() { umask(0); int fd = open("log.txt",O_WRONLY | O_CREAT,0666); if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); return 0; }
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { umask(0); int fd = open("log.txt",O_WRONLY | O_CREAT,0666); if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); int cnt = 0; const char* str="This is a test for linux file!\n"; while(cnt < 5) { //write(fd,str,strlen(str)+1); //+1导致乱码! write(fd,str,strlen(str)); cnt++; } close(fd); return 0; }
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { umask(0); int fd = open("log.txt",O_WRONLY | O_CREAT,0666); if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); int cnt = 0; //const char* str="This is a test for linux file!\n"; const char* str="AAAAAA!\n"; while(cnt < 2) { //write(fd,str,strlen(str)+1); //+1导致乱码! write(fd,str,strlen(str)); cnt++; } close(fd); return 0; }
open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数
- 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
- 而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们学习操作系统概念时,画的一张图:
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
4.2 其他接口:
write read close lseek ,类比C文件相关接口。
4.3 C语言中的文件操作:
(1)C语言中的文件操作“w”:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { umask(0); //在之前学习的C语言中的fopen函数,其底层就是open的O_WRONLY | O_CREAT | O_TRUNC //fopen("log.txt,"w"); //O_TRUNC是用于清空! int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); int cnt = 0; //const char* str="This is a test for linux file!\n"; const char* str="AAAAAA!\n"; while(cnt < 2) { //write(fd,str,strlen(str)+1); //+1导致乱码! write(fd,str,strlen(str)); cnt++; } close(fd); return 0; }
(2)C语言中的文件操作“a”:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { umask(0); //在之前学习的C语言中的fopen函数,其底层就是open的O_WRONLY | O_CREAT | O_TRUNC //fopen("log.txt,"w"); //O_TRUNC是用于清空! //int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); //对应于C语言中的"w"方式操作! int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666); //对应于C语言中的"a"方式操作! if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); int cnt = 0; //const char* str="This is a test for linux file!\n"; const char* str="AAAAAA!\n"; while(cnt < 2) { //write(fd,str,strlen(str)+1); //+1导致乱码! write(fd,str,strlen(str)); cnt++; } close(fd); return 0; }
(3)C语言中的文件操作“r”:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { umask(0); //在之前学习的C语言中的fopen函数,其底层就是open的O_WRONLY | O_CREAT | O_TRUNC //fopen("log.txt,"w"); 底层是open,O_WRONLY | O_CREAT | O_TRUNC //fopen("log.txt,"a"); 底层是open,O_WRONLY | O_CREAT | O_APPEND //O_TRUNC是用于清空! //int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); //对应于C语言中的"w"方式操作! //int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666); //对应于C语言中的"a"方式操作! int fd = open("log.txt",O_RDONLY); //对应于C语言中的"r"方式操作! if(fd < 0) { perror("open"); return 1; } printf("fd:%d \n",fd); char buffer[128]; ssize_t s = read(fd,buffer,sizeof(buffer)-1); //sizeof(buffer)-1是为了使得读取的内容形成字符串(字符串以\0结尾) if(s > 0) { buffer[s]='\0'; //因为系统调用接口read中的接收类型为void*,因此既可以是二进制,也可以是字符等,这里将其下标最后一个字符置为\0 printf("%s",buffer); } //int cnt = 0; const char* str="This is a test for linux file!\n"; //const char* str="AAAAAA!\n"; //while(cnt < 2) //{ // //write(fd,str,strlen(str)+1); //+1导致乱码! // write(fd,str,strlen(str)); // cnt++; //} close(fd); return 0; }
open、read、close、write这4个是我们读写文件的最底层的4个调用接口!
5. 文件描述符fd
5.1 理解文件描述符fd:
通过对open函数的学习,我们知道了文件描述符就是一个小整数
(1) 0 & 1 & 2
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器 所以输入输出还可以采用如下方式:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int main() { char buf[1024]; ssize_t s = read(0, buf, sizeof(buf)); if (s > 0) { buf[s] = 0; write(1, buf, strlen(buf)); write(2, buf, strlen(buf)); } return 0; }
测试fd:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { umask(0); //在之前学习的C语言中的fopen函数,其底层就是open的O_WRONLY | O_CREAT | O_TRUNC //fopen("log.txt,"w"); 底层是open,O_WRONLY | O_CREAT | O_TRUNC //fopen("log.txt,"a"); 底层是open,O_WRONLY | O_CREAT | O_APPEND //O_TRUNC是用于清空! //int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); //对应于C语言中的"w"方式操作! //int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666); //对应于C语言中的"a"方式操作! //int fd = open("log.txt",O_RDONLY); //对应于C语言中的"r"方式操作! //测试文件描述符:fd int fda = open("log.txta",O_WRONLY | O_CREAT | O_TRUNC,0666); int fdb = open("log.txtb",O_WRONLY | O_CREAT | O_TRUNC,0666); int fdc = open("log.txtc",O_WRONLY | O_CREAT | O_TRUNC,0666); int fdd = open("log.txtd",O_WRONLY | O_CREAT | O_TRUNC,0666); int fde = open("log.txte",O_WRONLY | O_CREAT | O_TRUNC,0666); printf("fda: %d\n",fda); printf("fdb: %d\n",fdb); printf("fdc: %d\n",fdc); printf("fdd: %d\n",fdd); printf("fde: %d\n",fde); //if(fd < 0) //{ // perror("open"); // return 1; //} //printf("fd:%d \n",fd); //char buffer[128]; //ssize_t s = read(fd,buffer,sizeof(buffer)-1); //sizeof(buffer)-1是为了使得读取的内容形成字符串(字符串以\0结尾) //if(s > 0) //{ // buffer[s]='\0'; //因为系统调用接口read中的接收类型为void*,因此既可以是二进制,也可以是字符等,这里将其下标最后一个字符置为\0 // printf("%s",buffer); //} //int cnt = 0; const char* str="This is a test for linux file!\n"; //const char* str="AAAAAA!\n"; //while(cnt < 2) //{ // //write(fd,str,strlen(str)+1); //+1导致乱码! // write(fd,str,strlen(str)); // cnt++; //} //close(fd); return 0; }
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { //先验证0,1,2就是标准I/O char buffer[1024]; ssize_t s = read(0,buffer,sizeof(buffer)-1); if( s > 0 ) { buffer[s] = '\0'; printf("echo: %s",buffer); } return 0; }
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { //先验证0,1,2就是标准I/O //char buffer[1024]; //ssize_t s = read(0,buffer,sizeof(buffer)-1); //if( s > 0 ) //{ // buffer[s] = '\0'; // printf("echo: %s",buffer); //} //const char*s = "Test OS interface write\n"; //write(1,s,strlen(s)); //write(2,s,strlen(s)); //验证0、1、2和stdin、stdout、stderr的对应关系 //stdin、stdout、stderr是C语言库函数封装好的系统接口 printf("stdin: %d\n",stdin->_fileno); printf("stdout: %d\n",stdout->_fileno); printf("stderr: %d\n",stderr->_fileno); return 0; }
不同语言的系统调用底层差异是客观存在的,但通过使用软件层,屏蔽了底层差异,用的函数指针的方式!
所有的软件底层的差异都可以添加一层软件层来屏蔽掉底层软件的差异!
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*fifiles, 指向一张表fifiles_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件.
5.2 文件描述符的分配规则
通过代码分析:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("myfile", O_RDONLY); if (fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); close(fd); return 0; }
输出发现是 fd: 3
关闭0或者2,再次观察:#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(0); //close(2); int fd = open("myfile", O_RDONLY); if (fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); close(fd); return 0; }
发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在fifiles_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
6. 重定向
那如果关闭1呢?看代码:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("./log", O_CREAT | O_RDWR); if (fd < 0) { perror("open"); return 1; } close(1); dup2(fd, 1); for (;;) { char buf[1024] = { 0 }; ssize_t read_size = read(0, buf, sizeof(buf) - 1); if (read_size < 0) { perror("read"); break; } printf("%s", buf); fflush(stdout); } return 0; }
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
6.1 重定向的本质
什么是重定向?
来段代码演示:
先来复习一下文件描述符的分配规则:
本来是要向显示器打印,最终却变成了向指定文件打印——这个现象背后的原理就是重定向!
如果我们要进行重定向,上层只认识0,1,2,3,4,5等这样的fd,那么我们可以在OS内部通过一定的方式调整数组的特定下标的内容(指向),就可以完成重定向操作!
重定向实现具体操作:
由以上分析可知,上面的一堆数据,都是内核数据结构,而只有OS才有权限操作,因此OS一定提供了接口。比如dpu:man 2 dpu
6. 2 使用 dup2 系统调用
函数原型如下:#include <unistd.h> int dup2(int oldfd, int newfd);
示例代码:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("./log", O_CREAT | O_RDWR); if (fd < 0) { perror("open"); return 1; } close(1); dup2(fd, 1); for (;;) { char buf[1024] = { 0 }; ssize_t read_size = read(0, buf, sizeof(buf) - 1); if (read_size < 0) { perror("read"); break; } printf("%s", buf); fflush(stdout); } return 0; }
这里的copy不是简单的int newfd、int oldfd拷贝下标!而是在内核层面上拷贝newfd、oldfd里面的内容(指针)!拷贝就是拷贝指针的指向,既然是指针指向的拷贝,那么这两个指针最终指向是同一个地址!
newfd是oldfd的一份拷贝,最终剩下的内容是oldfd原本指向的内容(地址)!(注意这个只剩下oldfd!)
这里是指针拷贝,那么拷贝对象指针指向的地址就应该是被拷贝对象指针指向的地址!(这里不容易理解,我们举个例子:比如stdout只认准fd=1,而通过close(1),使得fd=1的指针不再指向stdout的地址了,假设这个新的fd=3(即指向的是log.txt),综合传参方式就清晰了,那么就把fd=3的指向拷贝至1,最终剩下的是原本fd 指向的内容!
传参方式:int dup2(int oldfd,int newfd)
dup2( fd , 1 )分析了以上原因,我们再来看代码:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { //close(0); //close(1); //close(2); int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); if(fd < 0) { perror("open"); return 0; } dup2(fd,1); fprintf(stdout,"打开文件成功,fd: %d\n",fd); fflush(stdout); close(fd); }
这里理解一下一个文件为什么被打开多次?
通过 *file指向与count计数,打开一次count++。关闭则file=NULL; 类似于C++ 中智能指针的shared_ptr的知识。文件的销毁由OS负责,当count==0或其他情况时销毁。
重定向有许多,比如追加重定向、输出重定向等。
6.3 常见的重定向及使用:
(1)追加重定向:
(2)输入重定向:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { //close(0); //close(1); //close(2); //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 < 0) { perror("open"); return 0; } dup2(fd,0); //dup2(fd,1); 当然这里我们也可以将成功被拷贝的关闭 //int ret = dup2(fd,1); //fd = 3 , 1 //if(ret > 0) // close(fd); //即当3指向的内容被成功拷贝至1的指向,就关闭3,因为最终使用的仍是1作为标志索引 //printf("ret: %d\n",ret); char line[64]; //举个例子 //fegts是从特定的流当中将数据读到缓冲区 while(fgets(line, sizeof line, stdin) != NULL) { printf("%s\n",line); } fprintf(stdout,"打开文件成功,fd: %d\n",fd); fflush(stdout); close(fd); }
7. 缓冲区
缓冲区的理解:
7.1 什么是缓冲区?
缓冲区的本质:就是一段内存
7.2 为什么要有缓冲区?
(1)解放使用缓冲区的进程时间
(2)缓冲区的存在可以集中处理数据刷新,减少I/O的次数,从而达到提高整机的效率的目的。
7.3缓冲区在哪里?
打印现象:先打印hello,printf,再打印hello,write,等待5s.
printf不带有‘\n’,没有立即刷新是因为有缓冲区的。
而write是立即刷新的,后面printf又打印是 C语言中的库函数printf就是封装了write。
那么这个缓冲区在哪里?
根据分析,缓冲区一定不在write内部,也就不是内核级别的!这样看来,缓冲区是C语言提供的!则缓冲区就是语言级别的缓冲区!
C语言的三个库函数printf、fprintf、fputs都有一个公共参数stdout 。使用man手册查看:
std既然是FILE类型,那么这个struct就会封装很多的属性,其中就包括fd 和该FILE对应的语言级别的缓冲区!
7.4 如果在刷新之前关闭了fd会怎样?
这里关闭文件描述符,在sleep()期间没有刷新,关闭了文件描述符,则在进程
结束刷新时,找不到fd!也就无法打印!无法利用_fileno写入OS!这也就解释了我们之前的演示中,没有fflush(stdout);执行close(fd)后,就无法打印的原因!既然缓冲区在FILE内部,在C语言中的封装的库函数,我们每一次打开一个文件,都要有一个FILE*会返回!那就意味着,每一个文件都有一个fd和属于它的语言级别的缓冲区!
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { //printf("hello,printf\n"); //stdout -- 1 printf(" hello,printf"); fprintf( stdout,"hello,fprintf"); fputs(" hello,fputs",stdout); //const char *msg = "hello,write\n"; const char *msg = "hello,write"; write(1,msg,strlen(msg)); //close(1); sleep(5); close(stdout->_fileno); return 0; }
解释:
1.如果我们只向显示器打印,且字符串带有的都是'\n',所以无论是C语言库函数还是系统接口,数据都能立即刷新出来 。因为它们面对的stdout这样的设备,我们把它称之为显示器。因为有'\n',数据立即刷新!
2.当我们将其重定向到文件里(./myfile > log.txt):重定向内部做了dup2()等操作,
将本来显示到显示器的内容经过重定向写到文件里,因为进行了重定向,并且是有各自对应的刷新策略:显示器文件(行刷新),块设备、磁盘文件(全刷新(写满刷新))3.当我们进行重定向,打印发现数据成了7条:
首先系统接口打印1条、再次C语言库函数共6条(各自打印2条)
造成这种现象一定与缓冲区有关!这也说明了write根本不受缓冲区的影响,只打印了一条!
4.所以现在的问题是为什么调用C语言库函数经过重定向以后,却在文件里打印了两次?
printf等打印两次一定与fork()有关,首先C库函数把数据打印到文件里。因为此时已经重定向到 log.txt,数据不会立即刷新了,而是变成了全缓冲!所以这几个库函数的对应的数据信息暂存在了各自对应的stdout对应的语言级别的缓冲区,没有刷新。而系统接口write不受影响,只打印一次即完成!
5.当调用fork()的时候,因为是全缓冲,数据没有立即刷新,就暂时保存在了FILE的结构体里的语言级别的缓冲区里,当进行fork()时,就要创建子进程,而fork()执行之后,父子进程就会退出!那么父子进程就要开始刷新缓冲区。
6.刷新的本质:就是把缓冲区的数据写入到到OS内部,再清空缓冲区
7.缓冲区是C语言库函数自己的FILE内部维护的,属于父进程内部的数据区域!所以当调用fork()的时候,数据没有立即刷新而暂时保存在缓冲区里,而fork执行结束,不管是父子进程谁先刷新清空,代码父子进程共享,而数据父子进程要各自以写时拷贝的方式各自形成一份,则父子进程各自刷新了一份!所以就有了C库函数打印两次的情况!
这里有个隐性的条件:当进行重定向时,之前是向显示器写入,现在变成了向磁盘文件写入!即缓冲方式发生了变化!即由行缓冲—>全缓冲 !8. FILE
来段代码再研究一下:
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd
#include <stdio.h> #include <string.h> int main() { const char* msg0 = "hello printf\n"; const char* msg1 = "hello fwrite\n"; const char* msg2 = "hello write\n"; printf("%s", msg0); fwrite(msg1, strlen(msg0), 1, stdout); write(1, msg2, strlen(msg2)); fork(); return 0; }
运行出结果:
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:hello printf hello fwrite hello write
hello write hello printf hello fwrite hello printf hello fwrite
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
如果有兴趣,可以看看FILE结构体:typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
在 / usr / include / libio.h struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags //缓冲区相关 /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char* _IO_save_base; /* Pointer to start of non-current get area. */ char* _IO_backup_base; /* Pointer to first valid character of backup area */ char* _IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker* _markers; struct _IO_FILE* _chain; int _fileno; //封装的文件描述符 #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t* _lock; #ifdef _IO_USE_OLD_IO_FILE };
8.1 模拟实现重定向(行刷新策略):
#include<stdio.h> #include<unistd.h> #include<sys/stat.h> #include<sys/types.h> #include<fcntl.h> #include<string.h> #include<assert.h> #include<stdlib.h> #define NUM 1024 #define NONE_FLUSH 0x0 //无缓冲(立即刷新) #define LINE_FLUSH 0x1 //行缓冲 #define FULL_FLUSH 0x2 //全缓冲 //类型重命名 typedef struct _MyFILE { int _fileno; char _buffer[NUM]; int _end; //_buffer的结尾,后面用到 int _flags; //fflush method }MyFILE; MyFILE* my_fopen(const char* filename, const char* method) { assert(filename); assert(method); int flags = O_RDONLY; //默认以写、读方式打开 if (strcmp(method, "r") == 0) {} else if (strcmp(method, "r+") == 0) {} else if (strcmp(method, "w") == 0) { //如果是以写方式打开,就修改为O_WRONLY。 //如果打开的时候不存在,就O_CREAT 创建 //如果打开需要将文件清空,就O_TRUNC flags = O_WRONLY | O_CREAT | O_TRUNC; } else if (strcmp(method, "w+") == 0) {} else if (strcmp(method, "a") == 0) { //追加同理 flags = O_WRONLY | O_CREAT | O_APPEND; } else if (strcmp(method, "a+") == 0) {} //打开文件 int fileno = open(filename, flags, 0666); if (fileno < 0) { return NULL; } //打开文件成功就申请空间 MyFILE* fp = (MyFILE*)malloc(sizeof(MyFILE)); if (fp == NULL) return fp; memset(fp, 0, sizeof(MyFILE)); //1.正常情况下,会在打开文件时进行判断,使用的系统接口为stat //man 2 stat。stat就是用来检测一个特定路径下,文件是否存在 //这里没有使用,只进行简单演示 //2.一般情况下,我们打开的文件是什么类型,也要在my_fopen里获得 //这里没有使用处理, //3.这里默认将刷新方式设置为行缓冲 fp->_fileno = fileno; fp->_flags |= LINE_FLUSH; fp->_end = 0; return fp; } void my_fflush(MyFILE* fp) { assert(fp); if (fp->_end > 0) { write(fp->_fileno, fp->_buffer, fp->_end); fp->_end = 0; syncfs(fp->_fileno); //man 2 sync:把buffer cache直接提交到磁盘 } } //这里由于是模拟实现,就不考虑my_fwrite的返回值 void my_fwrite(MyFILE* fp, const char* start, int len) { // start表示要写入的字符串,len表示要写多长 assert(fp); assert(start); assert(len > 0); //写入到缓冲区 strncpy(fp->_buffer + fp->_end, start, len); //将数据写到缓冲区了 fp->_end += len; //_end永远指向的是有效字符的下一个位置 if (fp->_flags & NONE_FLUSH) {} else if (fp->_flags & LINE_FLUSH) { if (fp->_end > 0 && fp->_buffer[fp->_end - 1] == '\n') { //仅仅是写到内核中 write(fp->_fileno, fp->_buffer, fp->_end); fp->_end = 0; syncfs(fp->_fileno); } } else if (fp->_flags & FULL_FLUSH) {} } void my_fclose(MyFILE* fp) { //如果在close时,还有数据,那就刷新 my_fflush(fp); close(fp->_fileno); free(fp); } int main() { MyFILE* fp = my_fopen("log.txt", "w"); if (fp == NULL) { printf("my_fopen error\n"); return 1; } const char* s1 = "this is a testAAAA\n"; my_fwrite(fp, s1, strlen(s1)); printf("消息立即刷新!\n"); sleep(3); const char* s2 = "this is a testBBBB"; my_fwrite(fp, s2, strlen(s2)); //不带'\n',如果继续写,那不会写到文件里,而是放到缓冲区里暂存 printf("写入了一个不满足刷新条件的字符串!\n"); sleep(3); const char* s3 = "this is a testCCCC"; my_fwrite(fp, s3, strlen(s3)); printf("写入了一个不满足刷新条件的字符串!\n"); sleep(3); const char* s4 = "end\n"; my_fwrite(fp, s4, strlen(s4)); printf("写入了一个满足刷新条件的字符串!\n"); sleep(3); const char* s5 = "PPPPPPPPP"; my_fwrite(fp, s5, strlen(s5)); printf("写入了一个不满足刷新条件的字符串!\n"); sleep(1); my_fflush(fp); sleep(3); my_fclose(fp); }
这里的“PPPPPPPPPPPP”没有'\n',因此fork()发生写时拷贝,my_fclose(fp)进程退出时,刷新!
缓冲区的另一个好处是:对数据进行格式化(输入/输出)