当前位置: 首页 > news >正文

基础的IO

目录

1.前情提要

2.C语言文件操作回顾

2.1流

3.系统文件I/O

3.1传递标志位(flags)

3.2系统调用

3.2.1open

3.2.2close函数

3.2.3write

3.2.4read

3.2.5其他

3.3文件描述符

3.3.1概念

3.3.2fd分配规则

3.3.3实现重定向

3.3.4.dup2系统调用

3.3.5将重定向功能写入自己写的shell程序

4.linux下一切皆文件

5.缓冲区

5.1什么是缓冲区

5.2为什么要引入缓冲区机制

5.3缓冲类型

5.4FILE

6.简单设计一下libc库


1.前情提要

文件在磁盘里

磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的

磁盘是外设(即是输出设备也是输入设备)

磁盘上的文件 本质是对文件的所有操作,都是对外设的输入和输出简称IO

----------------------------------------------

Linux 下一切皆文件(键盘、显示器、网卡、磁盘 …… 这些都是抽象化的过程)

----------------------------------------------

对于0KB的空文件是占用磁盘空间的

文件是文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)

所有的文件操作本质是文件内容操作和文件属性操作

访问文件之前都得先打开(即文件必须被加载到内存中)。修改文件是通过执行代码的方式修改。

打开文件的,是包含有类似fopen代码的进程。且可以打开多个文件

        这些文件除了我们自己的写入写出打开关闭,像是缓冲区,刷新,状态变更这些操作都是要做的,但我们不用做,都是os在做,这意味着,这些被打开的文件也需要被os管理。怎么管理?先描述再组织。

----------------------------------------------

对文件的操作本质是进程对文件的操作

磁盘的管理者是操作系统

文件的读写本质不是通过C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而
是通过文件相关的系统调用接口来实现的

2.C语言文件操作回顾

更多的一些文件接口,可以参考C语言文件操作:从基础到高级-CSDN博客

首先,头文件是stdio.h

FILE *fp=fopen("./test.txt","w");
第一个参数是文件的路径(相对、绝对,相对是以当前进程的工作目录为基础)
第二个参数打开方式,分r,r+,w,w+,a,a+
r是只读,r+是读写,文件不存在打开失败,w是写,
w+是读写,文件不存在先创建文件,a是文件末尾追加,文件不存在先创建
a+是追加和读,读是从文件起点开始,但追加仍旧是末尾。
b是二进制,b可以跟其他组合。
以w方式打开文件,文件内容会先被自动清空。
fclose(fp)
关闭文件
为了良好习惯,文件记得关闭,免得占用内存。

   

    linux的重定向,> 文件名,是以w的形式打开的,这也是为什么我们每次重定向后,文件内容不是被追加,而是完全全新的内容,同理追加重定向>>,就是a形式打开。

        这也是linux中,当我们想清空文件内容的时候,只要>文件名即可。

       前面我们讲进程的时候,说了/proc/pid文件中,会有相应进程的信息,比如cwd和exe这两个符号链接,当可执行程序变成进程,exe指向的是可执行程序的路径,cwd指向的是可执行程序所处的目录的路径。依靠cwd,进程在创建文件之类的时候,才可以做到在当前目录下创建文件。

        而更改工作目录也就是cwd,其实我上个文章写自定义hsell的时候就有用,就是chdir函数(c语言的)。

        注意,写入的时候不要考虑\0,因为c语言规定字符串结尾是\0,但其他语言不一定,所以文件不要写入\0

2.1流

#include <stdio.h>
extern FILE *stdin;
extern FILE*stdout;
extern FILE *stderr;

c语言默认会打开这3个文件流。

        因为大多数c程序,最基础就是需要从键盘读取数据,输出结果到显示器,输出错误信息到显示器,而键盘和显示器显然也是文件,根据前面文件操作,就是要打开相应的文件,做读取和写入的工作。所以c语言默认帮忙打开了这些文件。

        形象的理解就是

extern FILE *stdin=fopen("键盘文件路径","r");
extern FILE*stdout=fopen("显示器文件路径","w");
extern FILE *stderr=fopen("显示器文件路径","w");

stdin-标准输入流,大多数环境中支持从键盘输入数据;

stdout-标准输出流,大多数环境中支持输出至显示器;

stderr-标准错误流,大多数环境中输出到显示器界面

基于上面的内容,像是打印内容到显示器,c语言就可以这样

const char *msg = "hello fwrite\n";
fwrite(msg,,strlen(msg),l,stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fputs("hello fputs\n",stdout);

输入的时候,fsacnf,scanf都可以收到键盘,stdin

3.系统文件I/O

        我们知道os才是硬件的管理者,程序不允许绕过os对硬件进行读写,也就是说,除了上层语言(比如c语言)提供的文件接口之外,os必然也要提供相应的系统调用接口供上层语言使用。

 而fopen,fwrite等c语言提供的文件接口,可以理解为对系统调用的open,write等接口的二次封装。

3.1传递标志位(flags)

#include <stdio.h>#define ONE 1  //0000 0001
#define TWO 2  //0000 0010
#define THREE 4  //0000 0100void func(int flags){if (flags & ONE) printf("flags has ONE! ");if (flags & TWO) printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE!");printf("\n");
}int main() {func(ONE);func(THREE);func(ONE | TWO);func(ONE | THREE | TWO);
return 0;
}

3.2系统调用

3.2.1open

#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:追加写
以上的参数(不止,man手册里还可以看到别的)本质都是宏,
是某个数字的宏,且这些数字在比特位上基本只有一位是1,其他都是0,且互相之间1的位置也是错位的。返回值:
成功:新打开的文件描述符
失败:-1

        open作用很简单,就是打开或创建文件。

        flags的用法,前面有参考的。

        这个mode,其实就是文件的权限。比如当我们写文件,并且给了O_CREAT,但不传mode,这时候创建的文件权限是乱码的,会有s什么的。这也是为什么linux中改变文件权限的指令是chmod。

        

        注意,每个进程都有自己的umask,默认是当前用户的,通过umask函数可以临时改变当前进程的umask(会影响后续创建的子进程)

  

关于文件描述符,后面会讲,可以理解为文件指针(暂时,具体后面讲实际关系)

3.2.2close函数

没什么好说的,就是关闭文件,close(文件描述符)

3.2.3write

第一个是文件描述符,第二个是写入数据的来源,第三个是写入数据的字节数

这样就能实现写入,但是,跟fopen的w还是有点不同的,fopen的w方式是会先清空文件内容的,对应到这里就是还有个选项要加

如果是追加,就是加O_APPEND,不要加O_TRUNC

上面的内容,可以理解为模拟c的fopen。

3.2.4read

都是差不多的内容。

3.2.5其他

剩下的可以自行了解

3.3文件描述符

3.3.1概念

        从前面的内容,我们可以发现,文件描述符fd是整数,且-1是打开失败。

        文件描述符是从0开始的,c语言默认打开的3个流,stdin,stdout,stderror,对应的就是0,1,2。而我们在程序中用open打开的文件,文件描述符是从3开始增长的。

        c语言的FILE类型,就是c标准库对fd的一个封装,是一个结构体,里面包含了fd。同理,前面的那些代码,把fd的参数直接写成0,1,2,就可以指定像键盘、显示器等输入输出。

        关于为什么c语言要特意封装fd,封装关于文件的系统调用,其实之前的文章里有说,我这里简单重复下。

        不同的系统有不同的系统调用(可能函数名一样,参数却不太一样,也可能函数名都不一样),比如我这里写的关于linux下的系统调用接口,跟windows又有些区别。如果我们的c语言代码直接调用系统接口的话,那么代码的移植性、跨平台性就不行(同样的代码在别的系统下不能运行)。所以不同系统下,同样是c语言,都有不一样的标准库,这里的不一样指的是内部封装的系统调用部分。这样使用者不需要考虑不同平台的系统调用,直接调用c语言的文件接口即可,具体要调用什么系统接口,由不同平台的c语言标准库来管理。

       不同的语言,封装出来的文件接口都不一样,用途可能相似,但函数名或参数都有些微的区别,但不管是什么语言,只要是对硬件(比如这里的磁盘中的文件)操作,那么必然都需要封装系统调用。

下面是一张网图,是展示了一部分结构

        图中的files_struct也是个结构体类型。

        其中file对象就是os用来管理被打开文件的结构体,这些file对象之间也是采用链表进行连接(这是os直接管理的方式,图中的数组是对于某个进程的pcb而言,其关系就跟我们前面讲进程的时候,同一个pcb可以被链入不同的队列,这里的file对象也是如此)。

        file对象中存储着间接或直接指向文件的属性(文件的属性)、方法集、缓冲区(文件的内容)的指针。也直接存储着部分文件属性,如权限mode,flag,pos,struct file *next等等。

       因此,比如进程1号运行open系统调用,当代码执行之后,如果这个文件没被打开(没有载入内存),那么os会从磁盘将文件载入内存,用file对象封装相关属性、内容、方法,链入链表被os管理,1号的files_struct对象中的数组会在最后有效数据的下一位填入该file对象的地址,最后再把数组下标返回给进程(也就是fd)。

        当我们执行write系统调用的时候,根据传入的fd,进程会从fd_array数组找到相应file对象的地址,访问之后,将write的另两个参数决定的,要写入的内容写入内存中该文件的缓冲区,最后由os将缓冲区的内容写入磁盘。

        read也是类似,将缓存区的内容传入参数的数组里,缓冲区没内容,就让os从磁盘重新写入缓冲区。

        对于close也是类似,就是通过fd找到对应file对象,然后free掉释放资源。

        从上面可以总结一句,那就是fd,即文件描述符,本质就是数组(文件描述符表)的下标

        

3.3.2fd分配规则

一句话:最小的没有被使用的数组下标,会分配给最新打开的文件

        比如我们打开一个新文件,那么就会是3(0,1,2被占了);

        比如我们先close(0)先关闭了stdin的流,那么这时候我们再open打开一个新文件,此时该文件对应的fd就是0;同理关2,开新,得2;关0,2,开新,得0。

        

3.3.3实现重定向

        我们要知道,printf内部系统调用write,fd默认是stdout指针的fileno值 1。

        当我们试图先close(1)之后,再open,再printf。这时候printf依旧是默认传stdout的fileno值1为fd给write,write只会对文件描述符表的下标1位置对应的文件执行写入。

        而我们前面open之后,此时fd为1对应的文件已经变成了open所打开文件,所以这时候我们可以发现printf并没有向屏幕输出,而是将内容输入到了open所打开的文件中。

        依靠上面的发现,不难实现重定向,本质上就是将默认的fd所指向的文件,通过改变地址值,从而指向另一个文件

        

        上面就是一个很简单的输入重定向,本来是从fd0指向的是键盘,也就是从键盘接受数据,此时fd0指向了test.txt文件,变成了从test.txt文件接受数据,并存入sample变量,再printf输出到屏幕。

        

        这个就是一个简单的输出重定向。

        追加重定向,就是把上面open的O_TRUNC换成O_APPEND即可。

3.3.4.dup2系统调用

上面的重定向设计是利用分配规则的。

但我们其实可以选择直接将新的fd对应的指针内容拷贝到0、1、2中(这样0,1,2就指向新打开的文件了),从而实现重定向。

因此os也提供了相应的系统调用,即dup2

注意,这个函数,是把newfd和oldfd所存的指针内容都变成oldfd所存指针内容。

所以如果我们要用这个函数实现重定向,并且假设新打开的文件fd是3,我们要实现输出重定向,那么此时,传参时oldfd就是3,newfd就是1。

注意这样操作不会导致fd3失效,我们如果又open个文件,此时这个文件被分配的fd是4

3.3.5将重定向功能写入自己写的shell程序

#include<stdio.h>
#include<stdlib.h>
#include<string.h>  
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>#define SIZE 1024       //命令最大长度
#define MAX_ARGV 64     //命令可以分割的最大长度
#define SEP " " //命令分割的依据
#define STREND '\0'   //字符串结尾
char *argv[MAX_ARGV];   //用于存放切割后的命令
char pwd[SIZE];  //用于存储当前路径
char env[SIZE];   //用于导入环境变量
int lastcode=0;    //存储最近一次子进程的退出码//重定向
#define NoneRedir -1 //表示不同的状态
#define StdinRedir 0
#define StdoutRedir 1
#define AppendRedir 2int redir_type = NoneRedir;  //重定向的类型,默认不需要重定向
char * filename=NULL;  //表示重定向的文件#define IgnSpace(buf,pos) do {while(isspace(buf[pos])) pos++; }while(0)         //注意,这里不加;是因为后面调用的时候适应语法,这是个跳过空格的宏函数//获取环境变量中需要的内容,比如主机名、用户名、当前工作目录
const char* GetEnv(const char *str){char *EnvValue=getenv(str);if(EnvValue){//有获取到return EnvValue;}else{return "None";}
}//把交互部分封装起来
int Interactive(char *out,int size){//输出提示符printf("[%s@%s %s]$ ",GetEnv("USER"),GetEnv("HOSTNAME"),GetEnv("PWD"));//获取命令字符串,如:ls -a -d,获取一行,fgets会自动加\0,也会读到回车fgets(out,size,stdin);//把回车去掉,另外fgets可以保证最少会收到一个回车符,不会是空串,可以放心访问out[strlen(out)-1]='\0';return strlen(out);
}void CheckRedir(char in[])
{redir_type=NoneRedir;filename=NULL;int pos=strlen(in)-1;//最后一个有效字符下标,检查从末尾开始往前查while(pos>=0){//这里不需要担心留有空格,导致左半部分命令不完整,下面的strtok是会抛弃空串的。//cat test >> log.txt//cat test > log.txt//cat < log.txtif(in[pos]=='>'){if(in[pos-1]=='>'){redir_type=AppendRedir; in[pos-1]=STREND;pos++;IgnSpace(in,pos);filename=in+pos;break;}else{redir_type=StdoutRedir;in[pos++]=STREND;IgnSpace(in,pos);filename=in+pos;break;}}else if(in[pos]=='<'){redir_type=StdinRedir;in[pos++]=STREND;IgnSpace(in,pos);filename=in+pos;break;}else{pos--;}}
}void Split(char in[]){CheckRedir(in);//检查是否有重定向int i=0;argv[i++]=strtok(in,SEP);//对历史字符串,其余字符要传NULL,另外最后一定要赋予NULL,该数组是用于exec的while(argv[i++]=strtok(NULL,SEP));//当不能再切了之后,就会返回NULL,NULL赋值之后,while跳出if(strcmp(argv[0],"ls")==0){argv[i-1]=(char*)"--color=auto";argv[i]=NULL;}//linux的shell,是把ls作为ls --color=auto的别名了。
}void Execute(){pid_t id=fork();if(id==0){//重定向处理int fd=-1;if(redir_type==StdinRedir){fd=open(filename,O_RDONLY);if(fd==-1){perror("open faild\n");return;}dup2(fd,0);}else if(redir_type==StdoutRedir){fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);if(fd==-1){perror("open faild\n");return;}}else if(redir_type==AppendRedir){fd=open(filename,O_CREAT|O_WRONLY|O_APPEND);dup2(fd,1);if(fd==-1){perror("open faild\n");return;}}else{//不用做任何事}//注意,进程替换只会替换程序和数据,但是对于pcb的一些信息都是继承下来的//也就是说,如文件描述符表也是会继承下来的。//子进程执行命令execvp(argv[0],argv);    exit(0);}int status=0;pid_t rid=waitpid(id,&status,0);if(rid==id)lastcode=WEXITSTATUS(status);}int BuildinCmd(){int ret=0;//如果是内键命令,返回1,否则0if(strcmp("cd",argv[0])==0){ret=1;char * target=argv[1]; //cd xxx or cd //同样对应target要么是个路径,要么就是个NULL,if(!target)target=(char*)GetEnv("HOME");chdir(target);//改变工作目录char tmp[1024];getcwd(tmp,1024);snprintf(pwd,SIZE,"PWD=%s",tmp);//sn就是把printf的内容传入n大小的字符串里,s就是不限制大小。//这里就是SIZE大小的pwd字符串。putenv(pwd);//chdir不会更改环境变量,需要我们手动更改}else if(strcmp("export",argv[0])==0){ret=1;//if(argv[1])putenv(argv[1]); 不能这样写,因为argv[1]存的是指针//,指向的commandline,而commandline每次循环都会被新的命令字符串覆盖,导致原先更改的环境变量内容被覆盖if(argv[1]){strcpy(env,argv[1]);putenv(env);}//这样写其实也有问题的,我env是一维数组,//每次都是维护一个环境变量,只要我多次export,旧的环境变量就没了//,所以,其实是要维护一整个环境变量表的,但这里为了方便,没写//其实还可以写个函数从父shell获取环境变量,写入到我们的二维env表中//但是linux的shell是从配置文件读取的,我这个想法也只是取个巧//这也是export的底层,就是更改shell维护的环境变量表}else if(strcmp("echo",argv[0])==0){ret=1;if(argv[1]==NULL){printf("\n");}else {if(argv[1][0]=='$'){if(argv[1][1]=='?'){printf("%d\n",lastcode);lastcode=0;}else{char *e=getenv(&argv[1][1]);if(e)printf("%s\n",e);}}else{printf("%s\n",argv[1]);}}}return ret;
}int main(){while(1){//交互:输出提示符,获取命令字符串char commandline[SIZE];int n=Interactive(commandline,SIZE);if(n==0)continue;//对命令字符串进行切割Split(commandline);//处理内键命令,如cd等n=BuildinCmd();if(n)continue;//执行命令Execute();}return 0; 
}

4.linux下一切皆文件

文字还是太苍白了,这是一张网络搜集的图片。

        里面的内容(稍作修改),file是包含了file_operations的一个对象指针的。

        可以发现,不管设备的读写操作差异有多大,对于os来说,只要访问file对象的读写函数指针并传参即可,具体怎么读怎么写,完全依赖于对应设备的驱动程序提供的读写方法。注意,大多数操作对于硬件来说,都可以划分到读和写两个操作之中。当然实际进入源代码会发现还有很多其他函数。        

        之所以linux可以视一切皆文件,就是依赖这个设计,从而屏蔽了硬件的差异,这一层设计也叫做vfs。这个设计也可以看作用c语言设计了对象(有成员属性,有成员方法)。再加上同样的结构体file,调用同一个成员函数(读写),却可以实现不同的读写方法,这也是多态思想的一种实现。

        

5.缓冲区

5.1什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

我们这只考虑用户级缓冲区,内核级的缓冲区暂不考虑。进程中的file结构体中的缓冲区就是内核级缓冲区。而c语言提供的,FILE类型,里面也有相应的缓冲区,这就是用户级缓冲区

用户级缓冲区基本就是语言自带的缓冲区,比如c语言。这里以c语言缓冲区为例。

5.2为什么要引入缓冲区机制

        读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

        不难发现,这个操作核心就是空间换时间,因为系统调用是非常耗时间和空间的,一次系统调用就完成100次系统调用的任务是最高效的,调用一次也是调用100次也是调用,那当然是调一次就完成所有任务就最好

        我们平时的c语言调用fwrite等非系统调用接口,实现写入操作的时候,是先把数据写入了语言自带的缓冲区(在内存),然后在合适的时间(具体看下面的缓冲类型)刷新语言缓冲区,刷新缓冲区会执行系统调用,以此来减少系统调用的次数,(内核也有自己的一套缓冲区以及刷新写入磁盘的逻辑,这里可以认为在 语言的缓冲区刷新之后就会写入内核的缓冲区  之前,对语言来说已经完成了写入磁盘的工作)

        c语言的文件接口不需要频繁调用系统接口,在没触发缓冲区刷新条件前,只需要将写入的数据拷贝到语言的缓冲区中即可。节省了调用这些文件接口的时候

5.3缓冲类型

标准I/0提供了3种类型的缓冲区。
全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/0系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行1/0系统调用操作,默认行缓冲区的大小为1024。
无缓冲区:无缓冲区是指标准l/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新
1.缓冲区满时;
2.执行flush语句(即强制刷新);
3.进程结束;

5.4FILE

前面讲了很久语言的缓冲区,实际存在哪里呢?

        我们知道file结构体内部是有缓冲区的,所以所谓的语言自带的缓冲区,就是这个缓冲区。有几个文件被打开,就有几个对应的缓冲区。

        我们的fwrite,fputs等写入数据的时候,就是将数据拷贝到这个file结构体内部维护的缓冲区里,由缓冲区自行根据条件决定是否刷新

可以简单看下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 -I0_read_ptr and _I0_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 θ
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];/*xchar*save_gptr;char*-save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

为了验证这个缓冲区,我们可以看下面的代码

我们会发现,fwrite和fprintf输出到屏幕和重定向到文件都没问题,但write却只在输出屏幕的时候出现了,没有输出到文件

        首先,我们要明白,输出到显示器的时候,刷新策略是行刷新,碰到\n会刷新,所以3个都会刷新到内核中,然后再出现在显示器中。

        其次,当我们使用重定向到文件的时候,刷新策略变成了全缓冲,而缓冲区是比较大的,“fprintf”和“fwrite”显然填不满,也就是说不会马上被刷新到内核,而是放到用户级缓冲区中,当fork之后,无论是父还是子,退出进程的时候,都要刷新缓冲区,而刷新缓冲区就是一种对数据的修改,这时候就会触发写时拷贝,所以这个缓冲区的内容就出现了2份,父子各一份,然后两边各自退出进程,这时候各自刷新自己的缓冲区,这就使得文件里被写入了2份数据“fprintf”和“fwrite”。

        通过这个实验也可以发现,write是直接将数据写入了内核中,不会放在缓冲区,自然也就不会因为写时拷贝导致写入2份数据,而fprintf和fwrite是c语言的接口,有语言自带的用户级缓冲区,会触发写时拷贝(file结构体内的缓冲区就是个数据,file又是被层层的链接,一直链接到进程,那么父子进程共享的数据自然也包括了这个缓冲区的数据)。

        

        我们平时说的printf格式化输出,其实就是将数字、字符串,以一个个字符的形式写入到输出缓冲区连接起来(比如printf("%d,%s",a,s) ),而scanf就是将键盘输入的字符先存在缓冲区,然后以格式符的要求将数据写入相应的变量中。

6.简单设计一下libc库

因为有多个文件,我这里就不黏贴文件了,我把gitee的链接贴出来。

Study-Me: 学习 - Gitee.com

http://www.dtcms.com/a/500978.html

相关文章:

  • 网站结构优化建议政务服务网站建设运行情况
  • 住建部建设厅官方网站中国空间站组合体
  • asp.net mvc 网站开发之美网站建设解决方案好处
  • 百度云服务器搭建网站步骤百度怎么做网站广告
  • USART--串口
  • 天津做网站哪个公司好上海搬家公司收费
  • vs做网站链接sql创建一个网站的最常用的方法是先建立一个文件夹
  • C++:从0开始学习链表
  • TPFanCtrl2,一款ThinkPad风扇控制工具
  • 辽宁地矿建设集团有限公司网站办办网官网
  • 网站建设协议书 印花税阿里巴巴网站域名注册
  • Redis的过期策略与内存淘汰机制
  • 从架构到体验:友猫社区平台的全栈技术解析与功能体系详解
  • 鸿蒙开发环境安装以及桌面应用开发
  • 网站建设博客作业蚌埠铁路建设监理公司网站
  • 计算机网络(tcp_socket )(一)
  • 湖北省建设厅网站上岗证查询立创商城
  • python学习之路(四)
  • 电商网站开发的职责建筑企业查询
  • 国外网站配色个人论坛类网站
  • 【Java 基础】核心知识点梳理
  • 做网站图片视频加载慢做网站的公司都有哪些岗位
  • 中药饮片采购平台的定义与作用是什么?
  • 【AI 学习日记】 深入解析MCP —— 从基础配置到高级应用指南
  • 网站买卖交易平台做网站需要编程?
  • 公司网站建设内容wordpress 识别二维码
  • 在淘宝上的毕设网站代做wordpress上传的图片不显示
  • 织梦网站数据库备份文件夹wordpress安装的要求
  • 医疗网站建设哪个好用郑州市网络设计公司
  • 用dz做网站怎么设置数据库远程wordpress数据库