Linux 基础IO 二
1.文件描述符的分配规则
#include<stdio.h>
#include<string.h>
//#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{close(1);//fd分配原则,从最小的开始,没有被占用的文件描述符给新打开的文件;int fd = open("log.txt", O_CREAT | O_TRUNC | O_RDONLY, 0666);if (fd < 0){perror("open");return 1;}//按道理,都应该是往显示器(标准输出)打印的;//但是下面的都写入到了log.txt中;//这叫什么呢?输出重定向;printf("fd:%d\n", fd);//stdout在FILE中的fd=1;而printf只向1中打印,他不管1到底是谁;1原本指向的是stdout,现在成了log.txt;fprintf(stdout, "hello printf\n");const char* s = "hello fwrite\n";fwrite(s, strlen(s), 1, stdout);return 0;
}
1.1输出重定向原理:文件操作前,进程会默认关联打开三个文件标准流文件;可是已经关闭了标准输出流,相当于把files_struct arrary指针数组中的'1'位置的数据置为了NULL;可是上层面已经把stdout 设置成了‘1’;根据文件描述符分配规则;所以就写入了log.txt;
重定向的本质,其实就是在OS内部,更改fd对应的内容的指向;
1.2输入重定向
#include<stdio.h>
#include<string.h>
//#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{close(0);int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}printf("fd:%d\n");char buffer[64];fgets(buffer, sizeof(buffer), stdin);printf("%s\n", buffer);return 0;
}
1.3追加重定向
#include<stdio.h>
#include<string.h>
//#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{close(1);int fd = open("log.txt", O_APPEND |O_WRONLY |O_CREAT);if (fd < 0){perror("open");return 1;}fprintf(stdout,"you can see me sucessly\n");return 0;
}
2.以上全是野路子,是为了理解基本原理来写的;具体的看以下:
函数int dup(int oldfd,int newfd) :1:newfd 3:oldfd
#include<stdio.h>
#include<string.h>
//#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main(int argc,char* argv[])
{if (argc != 2) return 2;int fd = open("log.txt", O_CREAT |O_WRONLY |O_CREAT);if (fd < 0){perror("open");return 1;}fprintf(stdout,"%s\n",argv[1]);//stdout ->1->显示器dup2(fd, 1); //重定向fprintf(stdout, "%s\n", argv[1]);close(fd)return 0;
}
3. 底层不同的硬件,一定对应着不同的操作方法。
但是不同的硬件都是外设,每一个设备的核心的访问函数都可以是 read 、write (I/O)
所以:所有的设备,都有自己的read、write 但是代码的实现都不一样,就形成了类似C++的多态,调的函数名相同,但调用的不同——这样就是Linux下的一切皆文件——>VFS (virtual file system)虚拟文件系统
4.缓冲区问题
4.1什么是缓冲区? 答:就是一段内存空间
4.2为什么要有缓冲区?答:为了提高效率。如果cpu想向外设发送数据,肯定会等待多条数据一同发送到同一个外设中,而不是多次发送,所以提高了效率;主要是为了提高用户的响应速度;(写回模式)cpu太快了,相比之下磁盘太慢了,cpu等不起磁盘写入;
4.3缓冲区在哪里?谁提供? 答:
4.4缓冲区的发送数据的策率?答: 1.立即刷新 2.行刷新 (行缓冲,一般以\r,\n)3.满刷新(全缓冲:必须把缓冲区写满了,就刷新) 4.特殊情况:1.用户强制刷新(fflush) 2.进程退出,也会强制刷新;
4.5关于缓冲区的认知
一般而言,行缓冲的设备文件——是显示器
全缓冲的设备文件——磁盘文件
所有的设备,永远都倾向于全缓冲!因为缓冲区满了才刷新,意味着需要更少次的I/O操作——>更少的外设访问成本(提高效率)——cpu、内存和外设I/O的时候,数据量的大小不是主要矛盾,和外设预备I/O的过程才是最耗费时间的;其他的刷新策略,都是结合实际情况做的妥协;
显示器:是直接给用户看的,一方面要照顾效率,一方面要照顾用户体验;
极端情况下:是可以自定义规则的;
5.同样一个程序,向显示器打印输出4行文字,向普通文件(磁盘文件)打印输出7行,其中C的I/O接口打印了两次,系统接口打印了1次,和向显示器打印一样;
#include<stdio.h>
#include<string.h>
//#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{//C语言提供的fprintf(stdout, "hello fprintf\n");printf("hello printf\n");const char* s = "hello fputs\n";fputs(s, stdout);//OS提供的const char* ss = "hello write\n";write(1, ss, strlen(ss));//注意我是在最后才调用的fork,上面的函数已经跑完了fork();//创建子进程;return 0;
}
问题1:上面的测试,并不能影响系统接口!
问题迁移:上述代码用到了几个知识? 答:写时拷贝技术,将缓冲区的数据考了一份给子进程;显示器和磁盘的刷新策略不同;文件输出重定向;————对程序进行了重定向,隐形的刷新策略变成了全缓冲,\n无意义了,fork的时候,函数已经执行完了,但是数据还未刷新,父进程里的缓冲区的数据,会被子进程接收,fork之后,父子分流,之后return,父子各自退出;进程退出,强行刷新;刷新是将缓冲区数据刷给内存,也是写时拷贝(不仅仅是改变变量数据)给子进程,所以打印了两份
如果有所谓的缓冲区,我们之前所谈的缓冲区,绝对不是由操作系统提供的,应该是由C标准库维护的,也就所谓的语言层面维护的;
5.1缓冲区在哪?答:进程启动连接了C标准库,缓冲区就是C标准库中开辟的一段内存空间,fputs数据,就是把数据写入到了这段内存空间中,在由标准库调用系统接口,压入磁盘;
5.2以上说的缓冲区是C标准库给我们提供的用户级缓冲区;也就是还存在内核级缓冲区;file结构体中也是存在内核缓冲区的,一旦使用write写入内核缓冲区,就代表和进程无关了;
5.3结构体file对象中,不止由fd,还有与缓冲区相关的类型指针
6.实战:自己设计用户缓冲区,minishell支持重定向;
#include<stdio.h>
#include<string.h>
//#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<stdlib.h>struct my_file
{int fd;int end; //当前缓冲区的结尾;char buffer[1024];
};
typedef struct my_file my_file;my_file* fopen_(const char* pathname, const char* mode)
{assert(pathname);assert(mode);my_file* fp = NULL;if (strcmp(mode, "r") == 0)//C语言用的穷举,C++直接map,里面放lamda就行;{}else if (strcmp(mode, "w") == 0){int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT,0666);if (fd >= 0){fp = (my_file*)malloc(sizeof(my_file));memset(fp, 0, sizeof(my_file));fp->fd = fd;}}return fp;
}
void fputs_(const char* message, my_file* fp)//这个接口是由C标准库来执行
{assert(message);assert(fp);strcpy(fp->buffer + fp->end, message);fp->end += strlen(message);//刷新策略;是用户通过执行C标准库中的代码逻辑,来完成刷新;//这里效率提高,体现在哪里?因为C提供了缓冲区,那么我们减少了IO的执行次数if (fp->fd == 0){//标准输入}else if (fp->fd == 1){//标准输出if (fp->buffer[fp->end - 1] == '\n'){write(fp->fd, fp->buffer, fp->end);fp->end = 0;}}else if (fp->fd == 2){//标准错误}else{//其他文件}
}
void fclose_(my_file* fp)
{assert(fp);fflush_(fp);close(fp->fd);free(fp);
}
void fflush_(my_file* fp)
{assert(fp);if (fp->end != 0){//暂且认为刷新——其实是把数据写到了内核write(fp->fd, fp->buffer, fp->end);syncfs(fp->fd);//将数据写入磁盘fp->end = 0;}}int main()
{my_file* fp = fopen_("./log.txt,", "w");if (fp == NULL){printf("open file error");return 1;}fputs_("hello world", fp);fclose_(fp);return 0;
}