网站商城怎么做app,广东网站设计的公司,超市小程序怎么做,学建筑设计出来能干嘛#x1f525; 本文专栏#xff1a;Linux #x1f338;作者主页#xff1a;努力努力再努力wz ★★★ 本文前置知识#xff1a;
文件系统以及相关系统调用接口 输入以及输出重定向 那么在此前的学习中#xff0c;我们了解了文件的概念以及相关的系统调用接口#xff0c;并… 本文专栏Linux 作者主页努力努力再努力wz ★★★ 本文前置知识
文件系统以及相关系统调用接口 输入以及输出重定向 那么在此前的学习中我们了解了文件的概念以及相关的系统调用接口并且我们也知道了输入以及输出重定向的一个原理以及实现那么今天这篇文章的内容将会着重讲解以及解析用户缓冲区以及有了用户缓冲区这个概念之后我们可以结合之前所学的系统接口来自己实现一个诸如fopen以及fwrite这样的c库函数那么废话不多说进入我们正文的学习 1.引入
那么在正式介绍我们用户缓冲区的概念之前我们先来看一些场景来引入我们的用户缓冲区
那么我用c语言写了一段简单的代码那么代码的逻辑也是十分简单那么也就是我们用三个c式的库函数也就是printf以及fprintf和fwrite分别向显示器文件当中写入一段字符串然后最后再调用我们的系统接口write向显示器文件写入一段字符串然后运行这段代码
#includestdio.h
#includeunistd.h
#includestring.h
int main()
{char* strhello fwrite\n;char* str1hello fprintf\n;printf(hello printf\n);fwrite(str,strlen(str),1,stdout);fprintf(stdout,str1);char* str2hello write\n;write(1,str2,strlen(str2));return 0;
}那么我们可以在终端看到打印了4次字符串那么分别对应我们调用的3个库函数以及一个系统调用接口往显示器写入的字符串那么非常符合我们的预期是很正常的一个现象但是我们的场景还没完这是我们的第一个场景
接下来我们不往显示器做打印而是输出重定向到一个long.txt的文件当中那么我们再来打印我们此时重定向的目标文件的文件内容
发现我们往long.txt文件写入了四个字符串和我们之前在显示器打印的内容是一模一样的其实这个结果也是符合我们的预期没问题那么接下来在引入我们的第三个场景
那么在此前的代码的基础上我们在代码的结尾调用一个fork系统调用接口那么此时我们在来运行这段修改过的代码那么根据我们对于fork系统调用的理解我们调用fork接口那么会创建一个子进程那么此时父子进程会共同执行fork调用之后的代码段的内容但是由于我们在代码结尾调用的fork接口而我们的写入操作是在fork调用之前就写入完毕所以我们即使创建了子进程那么它不会往显示器做任何内容的写入只能父进程做写入所以按照预期来说执行这个代码的结果终端还是会打印4个字符串那么我们接下来就执行这段代码来验证我们之前的推导
#includestdio.h
#includeunistd.h
#includestring.h
int main()
{char* strhello fwrite\n;char* str1hello fprintf\n;printf(hello printf\n);fwrite(str,strlen(str),1,stdout);fprintf(stdout,str1);char* str2hello write\n;write(1,str2,strlen(str2));fork();return 0;
}那么我们发现结果还是符合我们的预期那么接下来就来引入的第四个场景了那么此时我们在我们修改的代码的基础上再来输出重定向到我们的long.txt文件当中那么打印long.txt的文件内容那么这里我们前三个场景都是符合我们的预期那么这第四个场景我们的预期也许就是long.txt文件的内容还是会输出我们之前向显示器文件写入的那4个字符串那么我们来看一下结果是否是如我们所料
那么我们发现我们此时long.txt中打印出来的结果不是打印了之前的那四个字符串而是七个字符串并且我们发现write写入的字符串只被打印了一次而其他三个c库函数也就是printf和fwrite以及fprintf写入的字符串分别各自打印了两次而之所以会出现场景四这样的现象那么就和我们这篇文章要讲的用户缓冲区有关
那么一旦理解了用户缓冲区的概念那么第四个场景的解释就顺理成章了
2.缓冲区
那么观察上文的第四个场景我们知道我们的c库函数所写入的字符串被打印了2次而系统接口write则是被正常打印一次而之所以c库函数会出现这种情况那么是因为在语言层面上c语言定义了一个缓冲区那么我们诸如fprintf以及fwrite的c库函数我们知道他们在实现的时候底层一定封装了我们的系统调用接口也就是write系统调用接口但是我们fprintf以及fwrite库函数在获取到向显示器等文件写入的数据时它不会立马就交给write接口来做写入而是先保存到它所定义的一个缓冲区当中
那么现在我们无非就有两点疑问
缓冲区是什么为什么要有缓冲区
那么首先解释第一个问题缓冲区是什么那么我们得先从我们的fopen函数的原理以及实现来说起那么fopen函数的功能就是用来打开一个文件而我们知道fopen函数实现会封装open系统调用接口那么open系统接口想必我们一定很熟悉了那么它会首先为打开的文件定义一个file结构体然后扫描打开该文件的进程的文件描述表也就是指针数组然后找到一个位置没有指向任何一个file结构体然后将其指向该创建的file结构体对象然后返回该位置的数组下标也就是文件描述符
而fopen函数内部调用了open接口那么它必然是内部先调用open接口使其为该文件创建一个操作系统层面上的内核的一个file结构体然后获取到该文件的文件描述符然后接着会为该文件定义一个数组而没错该数组就是我们保存输入或者输出内容的缓冲区那么它会申请开辟一个固定大小的一个动态数组获取到指向该数组的指针也就是数组首元素的地址那么此时fopen函数内部会定义一个struct FILE结构体那么该结构体会封装该文件的文件描述符以及将指向该数组首元素的指针以及当前数组保存的有效内容的长度和缓冲策略等等那么它会在堆上申请该FILE结构体并且进行相应属性的初始化其中就包括文件描述符以及缓冲区的大小然后返回该FILE结构体的地址
所以fopen返回的结构体是我们语言层面上定义的一个结构体其中封装了该文件的文件描述符以及缓冲区的地址以及相关属性等那么不要与操作系统内核的file结构体给混淆了所以缓冲区的本质就是一个动态数组
那么根据fopen函数的原理那么每次调用一个fopen函数来打开一个文件那么此时fopen打开的每一个文件都会有对应的FILE结构体同理也会有各自对应的缓冲区 而对于第二个问题这里我们知道文件是保存在磁盘当中而我们要向文件当中写入那么必定要与外部设备也就是磁盘进行交互而对于磁盘来说其中访问以及读写磁盘的数据的效率是很慢的所以这里设计写入数据的时候采取的方式是多级缓冲也就是我们语言层面上获取到用户向文件中写入的数据那么它先保存到用户层面上的缓冲区然后根据特定的缓冲策略在来决定什么时候调用write接口来刷新到我们操作系统内核层面上的缓冲区
所以我们将我们用户层面上的缓冲区复制交给write写入也不是直接向磁盘中写入而是交给内核的缓冲区那么最后在将内核的缓冲区的数据在刷新写入到磁盘中那么理解这个过程就可以来类比我们生活中发快递的例子我们假设从成都发一个快递送到新疆我们肯定不会自己亲自去跑到新疆去递交快递而是交给我们楼下的菜鸟驿站那么菜鸟驿站此时获取到我们的快递后那么它不会一获取到顾客的快递那么就派一架飞机或者火车来送该顾客的快递那样效率太低下了而是它有自己的不同的发送策略比如我们要么该驿站的每个货架的都装满了快递那么就清空所有的货架的快递然后发送或者说我的袋子装满了其中的快递那么就先发送这个袋子里面的所有快递然后交给航班或者火车那么航班公司或者火车公司采取的发送的策略肯定就是我们飞机或者火车装满了那么我们才进行发送
所以在这个例子中菜鸟驿站就是用户层面上的缓冲区而航班公司就是系统层面上的缓冲区那么有了用户层面上的缓冲区我们就不用频繁的调用write函数从而优化提高效率那么这就是缓冲区存在的意义
而刚才在上面的例子我们还提及过菜鸟驿站有自己的发送策略那么这里就对应我们的缓冲区的缓冲策略
而我们的缓冲区有三个缓冲策略分别是 行缓冲那么我们将我们的数据保存到缓冲区直到遇到换行符然后刷新此时包括换行符之前所所有保存的数据全缓冲那么全缓冲则是无视换行符那么直到保存的数据到达了我们该缓冲区的容量的上限那么我们就刷新该缓冲区的所有内容然后写入到内核的缓冲区中无缓冲那么不保存到用户层面的缓冲区直接写入到内核层面的缓冲区中 而至于选择什么样的缓冲策略那么则和我们写入的目标文件的类型有关如果我们是要往显示器文件当中写入那么我们由于显示器是给我们人阅读的那么人的阅读习惯是一行一行来阅读的所以我们向显示器文件采取的一个缓冲策略则是行缓冲而对于普通文件来写入的则是采取的是全缓冲的策略而对于向保存错误信息的显示器文件那么一旦进程遇到异常那么就得立马输出错误信息所以采取的是无缓冲策略 注我们将一个打开文件的关闭其中就要调用close接口那么它的工作不仅是将当前文件描述符的file结构体的引用计数减一如果引用计数为0的话回清理其内核的file结构体以及内存中的数据而它在进行该清理工作之前会刷新当前文件对应的内核缓冲区而fclose内部则会封装了close那么在调用close之前那么它会刷新用户缓冲区的所有内容然后写入到内核的缓冲区中最后在close该打开的文件并且进程结束也会自动刷新用户以及内核缓冲区 那么有了用户缓冲区以及缓冲策略的概念那么我们就能解释第四个场景出现的原因了那么我们调用fork函数然后创建了一个子进程那么创建子进程本质就是就是子进程会拷贝父进程的task_struct结构体然后修改其中的部分属性比如PID以及PPID得到自己独立的一份task_struct结构体而拷贝父进程的task_struct结构体那么意味着子进程也有自己独立的一份文件描述表并且由于父子进程共享物理内存页那么意味着子进程也同样有与父进程相同内容的用户缓冲区而由于我们此时是向普通文件做写入那么我们的fprintf以及printf等c库函数所写入的字符串都会保存到缓冲区并且此时缓冲区未被写满那么此时他们是不会刷新到内核的缓冲区中而进程一旦结束那么操作系统会刷新用户以及内核缓冲区的内容所以我们就能够看到c库函数写入的字符串各自写入了两次因为父子进程各种刷新了对应的缓冲区而由于write接口是不经过用户缓冲区而它是在fork调用之前就写入到了内核缓冲区中所以write写入的字符换只打印了一次
3.模拟实现一个fopen函数以及fwrite以及fclose函数
那么在知道了缓冲区的概念之后那么我们就可以自己简单实现这三个库函数来加深我们对于缓冲区的理解
1.fopen函数的实现
那么fopen函数的参数列表就是我们要打开的目标文件的文件名那么它是一个字符指针以及我们打开该文件的模式也就是一个字符指针指向一个字符串所以这里我们第一步首先是判断这两个指针是否为空那么不为空的话我们就调用open函数来打开该目标文件不过打开的时候还有根据我们fopen函数的第二个参数也就是打开的模式来确定我们open打开的模式也就是宏如果是w模式的话那么则是从清空目标文件的内容从文本起始位置处写入那么对应open接口的宏就是O_CREAT|O_WRONLY|O_TRUNC而如果是a也就是追加模式那么则是从文本末尾处接着写入那么对应open接口的宏是O_CREAT|O_APPEND那么这部分的代码逻辑我们就用if else逻辑其中用strcmp的匹配来判断
然后open调用成功后我们会获得该文件的描述符失败则返回NULL那么接下来就是定义一个FILE结构体然后进行初始化那么其中FILE结构体就应该包括文件描述符以及保存输入以及输出的动态数组也就是缓冲区以及两个记录该缓冲区有效内容的长度的变量和当前的打开标志位以及缓冲策略而之所以记录打开的标志位是因为我们open接口会对打开的模式的行为进行检查而如果之后我们进行不对应该标志位的行为比如标志位为读而你却进行写操作那么写操作没有进行相应的权限检查就会引发错误那么标志位我们就是用一个int类型的变量来记录它的值就是open接口的第二参数也就是宏定义
那么我们malloc申请完FILE结构体以及动态数组并且进行部分属性的初始化之后那么接下来就判断目标文件的类型确定缓冲策略但是这里我由于对一些判断文件的系统接口的知识的缺失所以我这里没有实现这个功能我统一都是将其设置为行缓冲其中不同缓冲策略我分别用整形0,1,2来表示并且有对应的宏定义那么这里读者想要实现完整的fopen函数可以去查阅相关的判断文件类型的接口完善这个环节
最终创建成功并且完成初始化就返回该FILE结构体的指针
代码实现
#define N 1024
#define FLUSH_NOW 0
#define FLUSH_LINE 1
#define FLUSH_ALL 2typedef struct IO_FILE{int _fd;char* inbuff;int in_size;char* outbuff;int out_size;int _flags;int _mode;}_FILE;_FILE* fopen(char* filename,char* flag ){assert(filenameflag);int flag_mode;if(strcmp(flag,w)0){flag_modeO_CREAT|O_WRONLY|O_TRUNC;}else if(strcmp(flag,a)0){flag_modeO_CREAT|O_APPEND;}else if(strcmp(flag,r)0){flag_modeO_RDONLY;}else{return NULL;}int fdopen(filename,flag_mode,0666);if(fd0){perror(open);return NULL;}_FILE* f(_FILE*)malloc(sizeof(_FILE));if(fNULL){close(fd);return f;}f-_fdfd;f-inbuffNULL;f-outbuffNULL;f-in_size0;f-out_size0;f-_flagsflag_mode;f-_modeFLUSH_LINE;if((flag_modeO_WRONLY)!0){f-inbuff(char*)malloc(N*sizeof(char));f-in_sizeN;if(f-inbuffNULL){close(fd);free(f);return NULL;}} if((flag_modeO_RDONLY)!0){f-outbuff(char*)malloc(N*sizeof(char));f-out_sizeN;if(f-outbuffNULL){close(fd);free(f);return NULL;}}return f;}2.fwrite的实现
那么fwrite的参数分别是你要写入的内容的数组以及写入的长度size单位是字节以及要写入几组该长度的块nmemb和写入的目标文件的FILE结构体的指针返回值则是成功写入的块
那么第一步则是判断两个指针是否为空以及写入的长度是否为空或者块的数量是否为空如果满足那么返回0那么第二步则是检查我们该文件的标志位是否有写权限没有则返回为空然后我们下一个环节再是判断缓冲的策略在判断之前我们首先先计算了要写入的总长度total那么它的大小则是nmemb*size然后接着在定义一个变量written记录当前已经写入了多少字节的内容初始化written为0然后判断缓冲策略
如果是无缓冲那么我们就直接调用write函数那么它会返回成功写入的字节长度我们得到返回值再除以size得到成功写入的块并返回
而对于全缓冲以及行缓冲我们则是做一个while循环那么这里退出的条件就是我们写完total字节数退出也就是written等于total那么在循环内部判断这两个缓冲策略并且每次判断之前都要判断我们当前缓冲区的剩余容量是否够保存当前剩余字节如果够直接保存当前剩余字节不够则是先写满当前缓冲区剩余字节数然后再判断是否为行缓冲还是全缓冲而其中对于行缓冲来说我们会调用memchr得到第一个换行符的位置然后将之后的位置移动到前面那么此时计算当前缓冲区有效长度然后written加上该换行符之前内容的所有字节数
而全缓冲则是判断当前有效长度是否写满缓冲区如果写满直接调用write将缓冲区的内容写入到内核缓冲区
那么这三个函数其中最难实现的就是fwrite那么在当时我自己去实现的时候那是非常的坐牢那么相信你看完fwrite的实现原理后能够轻松掌握并且实现
代码实现 int fwrite(const void* ptr, int size, int nmemb, _FILE* stream) {if (!ptr || !stream || size 0 || nmemb 0) return 0;if ((stream-_flags (O_WRONLY | O_RDWR)) 0) {return 0;}const char* data (const char*)ptr;int total_bytes size * nmemb;int written 0;if (stream-_mode FLUSH_NOW) {int res write(stream-_fd, data, total_bytes);return (res 0) ? res / size : 0; }while (written total_bytes) {int avail N - stream-in_size;int to_copy (total_bytes - written avail) ? (total_bytes - written) : avail;memcpy(stream-inbuff stream-in_size, data written, to_copy);stream-in_size to_copy;written to_copy;if (stream-_mode FLUSH_LINE) {char* start stream-inbuff;char* end start stream-in_size;while (start end) {char* newline memchr(start, \n, end - start);if (!newline) break;int line_length newline - start 1;int res write(stream-_fd, start, line_length);if (res ! line_length) {return written / size; }int remaining end - (newline 1);memmove(stream-inbuff, newline 1, remaining);stream-in_size remaining;start stream-inbuff;end start remaining;}}if (stream-in_size N) {int res write(stream-_fd, stream-inbuff, N);if (res ! N) {return written / size;}stream-in_size 0;}}return nmemb;
}3.fclose函数
那么fclose函数实现非常简单了那么它会接受关闭的FILE结构体的指针
那么我们首先第一步还是判断指针是否为空接下来则是先将用户缓冲区的内容给刷新那么就需要判断两个缓冲区是否有有效内容有就调用write接口写入由于FILE结构体以及缓冲区都是在堆上申请开辟所以先释放掉缓冲区然后再释放整个FILE结构体最后调用close接口释放操作系统的内核的file结构体成功返回0失败返回-1
int fclose(_FILE* stream)
{assert(f);if(stream-inbuffstream-in_size!0){int qwrite(stream-_fd,stream-inbuff,stream-in_size);if(q0){perror(write);return -1;}}close(stream-_fd);free(stream-inbuff);free(stream-outbuff);free(stream);return 0;
}完整实现
mystdio.h文件
#pragma once#includeunistd.h
#includefcntl.h
#includestring.h
#includeassert.h
#includestdlib.h
#includestdio.h
#define N 1024
#define FLUSH_NOW 0
#define FLUSH_LINE 1
#define FLUSH_ALL 2typedef struct IO_FILE{int _fd;char* inbuff;int in_size;char* outbuff;int out_size;int _flags;int _mode;}_FILE;
int _fclose(_FILE* stream);
_FILE* _fopen(char* filename,char* flag );int _fwrite( void* ptr, int size, int nmemb, _FILE* stream);
mystdio.c文件
#includemystdio.h
_FILE* _fopen(char* filename,char* flag ){assert(filenameflag);int flag_mode;if(strcmp(flag,w)0){flag_modeO_CREAT|O_WRONLY|O_TRUNC;}else if(strcmp(flag,a)0){flag_modeO_CREAT|O_APPEND;}else if(strcmp(flag,r)0){flag_modeO_RDONLY;}else{return NULL;}int fdopen(filename,flag_mode,0666);if(fd0){perror(open);return NULL;}_FILE* f(_FILE*)malloc(sizeof(_FILE));if(fNULL){close(fd);return f;}f-_fdfd;f-inbuffNULL;f-outbuffNULL;f-in_size0;f-out_size0;f-_flagsflag_mode;f-_modeFLUSH_LINE;if((flag_modeO_WRONLY)!0){f-inbuff(char*)malloc(N*sizeof(char));f-in_sizeN;if(f-inbuffNULL){close(fd);free(f);return NULL;}} if((flag_modeO_RDONLY)!0){f-outbuff(char*)malloc(N*sizeof(char));f-out_sizeN;if(f-outbuffNULL){close(fd);free(f);return NULL;}}return f;}int _fwrite(const void* ptr, int size, int nmemb, _FILE* stream) {if (!ptr || !stream || size 0 || nmemb 0) return 0;if ((stream-_flags (O_WRONLY | O_RDWR)) 0) {return 0;}const char* data (const char*)ptr;int total_bytes size * nmemb;int written 0;if (stream-_mode FLUSH_NOW) {int res write(stream-_fd, data, total_bytes);return (res 0) ? res / size : 0; }while (written total_bytes) {int avail N - stream-in_size;int to_copy (total_bytes - written avail) ? (total_bytes - written) : avail;memcpy(stream-inbuff stream-in_size, data written, to_copy);stream-in_size to_copy;written to_copy;if (stream-_mode FLUSH_LINE) {char* start stream-inbuff;char* end start stream-in_size;while (start end) {char* newline memchr(start, \n, end - start);if (!newline) break;int line_length newline - start 1;int res write(stream-_fd, start, line_length);if (res ! line_length) {return written / size; }int remaining end - (newline 1);memmove(stream-inbuff, newline 1, remaining);stream-in_size remaining;start stream-inbuff;end start remaining;}}if (stream-in_size N) {int res write(stream-_fd, stream-inbuff, N);if (res ! N) {return written / size;}stream-in_size 0;}}return nmemb;
}
int _fclose(_FILE* stream)
{assert(f);if(stream-inbuffstream-in_size!0){int qwrite(stream-_fd,stream-inbuff,stream-in_size);if(q0){perror(write);return -1;}}close(stream-_fd);free(stream-inbuff);free(stream-outbuff);free(stream);return 0;
}main.c文件
#includemystdio.c”
int main()
{
char* tthello Linux\n;_FILE* fp_fopen(log.txt,w);_fwrite(tt,strlen(tt),1,fp);_fclose(fp);return 0;
}
Linux上运行截图
结语
那么这就是本篇关于用户缓冲区的所有知识啦那么下来也推荐大家可以自己去实现这三个c库函数甚至还可以去实现fprintf函数等那么我的下一期文章将是文件系统的讲解请大家多多期待那么我会持续更新那么本篇博客创作不易还请大家多多三连加关注你的支持就是我创作的最大动力