【Linux内核系列】:深入理解缓冲区
🔥 本文专栏:Linux
🌸作者主页:努力努力再努力wz



★★★ 本文前置知识:
文件系统以及相关系统调用接口
输入以及输出重定向
那么在此前的学习中,我们了解了文件的概念以及相关的系统调用接口,并且我们也知道了输入以及输出重定向的一个原理以及实现,那么今天这篇文章的内容将会着重讲解以及解析用户缓冲区以及有了用户缓冲区这个概念之后,我们可以结合之前所学的系统接口来自己实现一个诸如fopen以及fwrite这样的c库函数,那么废话不多说,进入我们正文的学习
1.引入
那么在正式介绍我们用户缓冲区的概念之前,我们先来看一些场景来引入我们的用户缓冲区:
那么我用c语言写了一段简单的代码,那么代码的逻辑也是十分简单,那么也就是我们用三个c式的库函数也就是printf以及fprintf和fwrite分别向显示器文件当中写入一段字符串,然后最后再调用我们的系统接口write向显示器文件写入一段字符串,然后运行这段代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{char* str="hello fwrite\n";char* str1="hello fprintf\n";printf("hello printf\n");fwrite(str,strlen(str),1,stdout);fprintf(stdout,str1);char* str2="hello write\n";write(1,str2,strlen(str2));return 0;
}
那么我们可以在终端看到打印了4次字符串,那么分别对应我们调用的3个库函数以及一个系统调用接口往显示器写入的字符串,那么非常符合我们的预期,是很正常的一个现象,但是我们的场景还没完,这是我们的第一个场景
接下来我们不往显示器做打印,而是输出重定向到一个long.txt的文件当中,那么我们再来打印我们此时重定向的目标文件的文件内容
发现我们往long.txt文件写入了四个字符串和我们之前在显示器打印的内容是一模一样的,其实这个结果也是符合我们的预期,没问题,那么接下来在引入我们的第三个场景
那么在此前的代码的基础上,我们在代码的结尾调用一个fork系统调用接口,那么此时我们在来运行这段修改过的代码,那么根据我们对于fork系统调用的理解,我们调用fork接口,那么会创建一个子进程,那么此时父子进程会共同执行fork调用之后的代码段的内容,但是由于我们在代码结尾调用的fork接口,而我们的写入操作是在fork调用之前就写入完毕,所以我们即使创建了子进程,那么它不会往显示器做任何内容的写入,只能父进程做写入,所以按照预期来说执行这个代码的结果终端还是会打印4个字符串,那么我们接下来就执行这段代码,来验证我们之前的推导
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{char* str="hello fwrite\n";char* str1="hello fprintf\n";printf("hello printf\n");fwrite(str,strlen(str),1,stdout);fprintf(stdout,str1);char* str2="hello 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(filename&&flag);int flag_mode;if(strcmp(flag,"w")==0){flag_mode=O_CREAT|O_WRONLY|O_TRUNC;}else if(strcmp(flag,"a")==0){flag_mode=O_CREAT|O_APPEND;}else if(strcmp(flag,"r")==0){flag_mode=O_RDONLY;}else{return NULL;}int fd=open(filename,flag_mode,0666);if(fd<0){perror("open");return NULL;}_FILE* f=(_FILE*)malloc(sizeof(_FILE));if(f==NULL){close(fd);return f;}f->_fd=fd;f->inbuff=NULL;f->outbuff=NULL;f->in_size=0;f->out_size=0;f->_flags=flag_mode;f->_mode=FLUSH_LINE;if((flag_mode&O_WRONLY)!=0){f->inbuff=(char*)malloc(N*sizeof(char));f->in_size=N;if(f->inbuff==NULL){close(fd);free(f);return NULL;}} if((flag_mode&O_RDONLY)!=0){f->outbuff=(char*)malloc(N*sizeof(char));f->out_size=N;if(f->outbuff==NULL){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->inbuff&&stream->in_size!=0){int q=write(stream->_fd,stream->inbuff,stream->in_size);if(q<0){perror("write");return -1;}}close(stream->_fd);free(stream->inbuff);free(stream->outbuff);free(stream);return 0;
}
完整实现
mystdio.h文件:
#pragma once#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#include<stdio.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文件:
#include"mystdio.h"
_FILE* _fopen(char* filename,char* flag ){assert(filename&&flag);int flag_mode;if(strcmp(flag,"w")==0){flag_mode=O_CREAT|O_WRONLY|O_TRUNC;}else if(strcmp(flag,"a")==0){flag_mode=O_CREAT|O_APPEND;}else if(strcmp(flag,"r")==0){flag_mode=O_RDONLY;}else{return NULL;}int fd=open(filename,flag_mode,0666);if(fd<0){perror("open");return NULL;}_FILE* f=(_FILE*)malloc(sizeof(_FILE));if(f==NULL){close(fd);return f;}f->_fd=fd;f->inbuff=NULL;f->outbuff=NULL;f->in_size=0;f->out_size=0;f->_flags=flag_mode;f->_mode=FLUSH_LINE;if((flag_mode&O_WRONLY)!=0){f->inbuff=(char*)malloc(N*sizeof(char));f->in_size=N;if(f->inbuff==NULL){close(fd);free(f);return NULL;}} if((flag_mode&O_RDONLY)!=0){f->outbuff=(char*)malloc(N*sizeof(char));f->out_size=N;if(f->outbuff==NULL){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->inbuff&&stream->in_size!=0){int q=write(stream->_fd,stream->inbuff,stream->in_size);if(q<0){perror("write");return -1;}}close(stream->_fd);free(stream->inbuff);free(stream->outbuff);free(stream);return 0;
}
main.c文件:
#include"mystdio.c”
int main()
{
char* tt="hello Linux\n";_FILE* fp=_fopen("log.txt","w");_fwrite(tt,strlen(tt),1,fp);_fclose(fp);return 0;
}
Linux上运行截图:
结语
那么这就是本篇关于用户缓冲区的所有知识啦,那么下来也推荐大家可以自己去实现这三个c库函数甚至还可以去实现fprintf函数等,那么我的下一期文章将是文件系统的讲解,请大家多多期待,那么我会持续更新,那么本篇博客创作不易,还请大家多多三连加关注,你的支持,就是我创作的最大动力!
相关文章:

【Linux内核系列】:深入理解缓冲区
🔥 本文专栏:Linux 🌸作者主页:努力努力再努力wz ★★★ 本文前置知识: 文件系统以及相关系统调用接口 输入以及输出重定向 那么在此前的学习中,我们了解了文件的概念以及相关的系统调用接口,并…...

Python开发Scikit-learn面试题及参考答案
目录 如何用 SimpleImputer 处理数据集中的缺失值? 使用 StandardScaler 对数据进行标准化的原理是什么?与 MinMaxScaler 有何区别? 如何用 OneHotEncoder 对类别型特征进行编码? 解释特征选择中 SelectKBest 与 VarianceThreshold 的应用场景。 如何通过 PolynomialFe…...

~(取反)在算法竞赛中的常见用法和注意事项
在算法竞赛中,取反符号 ~ 主要用于按位取反操作,其功能是对整数的二进制表示逐位取反(0 变 1,1 变 0)。以下是 ~ 在算法竞赛中的常见用法和注意事项: 1. 按位取反的基本用法 ~ 对整数的二进制表示进行取反…...

C++ MySQL 常用接口(基于 MySQL Connector/C++)
C MySQL 常用接口(基于 MySQL Connector/C) 1. 数据库连接 接口: sql::mysql::MySQL_Driver *driver; sql::Connection *con;作用: 用于创建 MySQL 连接对象。 示例: driver sql::mysql::get_mysql_driver_insta…...

本地部署 OpenManus 保姆级教程(Windows 版)
一、环境搭建 我的电脑是Windows 10版本,其他的没尝试,如果大家系统和我的不一致,请自行判断,基本上没什么大的出入啊。 openManus的Git地址:https://github.com/mannaandpoem/OpenManus 根据官网的两种安装推荐方式如…...

【Pandas】pandas Series compare
# Pandas2.2 Series ## Computations descriptive stats |方法|描述| |-|:-------| |Series.compare(other[, align_axis, ...])|用于比较两个 Series| ### pandas.Series.compare pandas.Series.compare 方法用于比较两个 Series,并返回一个包含差异的 DataFram…...

基于DeepSeek的智慧医药系统(源码+部署教程)
运行环境 智慧医药系统运行环境如下: 前端: HTMLCSS后端:Java AIGCDeepseekIDE工具:IDEA技术栈:Springboot HTMLCSS MySQL 主要角色 智慧医药系统主要分为两个角色。 游客 尚未进行注册和登录。具备登录注册、…...

如何为服务设置合理的线程数
1. 首先,要确定最大线程数的限制因素。通常,线程数量受限于内存、CPU和操作系统限制。比如,每个线程都需要一定的栈内存,默认情况下Java线程的栈大小是1MB(64位系统可能更大),所以如果内存不足&…...

Unity--Cubism Live2D模型使用
了解LIVE2D在unity的使用--前提记录 了解各个组件的作用 Live2D Manuals & Tutorials 这些文件都是重要的控制动画参数的 Cubism Editor是编辑Live2D的工具,而导出的数据的类型,需要满足以上的条件 SDK中包含的Cubism的Importer会自动生成一个Pref…...

Vue.js 3 的设计思路:从声明式UI到高效渲染机制
目录 一、声明式UI与虚拟DOM的灵活性 二、渲染器:虚拟DOM到真实DOM的桥梁 三、组件的本质与实现 四、编译与运行时的协同优化 五、性能与可维护性的权衡 总结 Vue.js 3 作为新一代前端框架,其设计理念在声明式UI描述、虚拟DOM优化、组件化架构…...

部署前后端项目
部署项目 liunx 软件安装 软件安装方式 在Linux系统中,安装软件的方式主要有四种,这四种安装方式的特点如下: 建议nginx、MySQL、Redis等等使用docker安装,会很便捷,这里只演示JDK、ngxin手动的安装 安装JDK 上述我…...

Vue Diff算法原理深度解析:如何高效更新虚拟DOM?
文章目录 1. 为什么需要Diff算法?2. Diff算法核心原则3. 核心流程图解4. 核心代码实现(简化版)5. Key的重要性示例6. 算法优化策略7. 时间复杂度优化8. 与其他框架的对比9. 总结 1. 为什么需要Diff算法? 在Vue的响应式系统中&…...

Dify平台部署记录
安装dify项目 官网地址:http://difyai.com/ github地址:https://github.com/langgenius/dify 下载项目: git clone https://github.com/langgenius/dify.git下载过慢,直接访问网页下载zip压缩包: 解压,…...

ArcGIS Pro中字段的新建方法与应用
一、引言 在地理信息系统(GIS)的数据管理和分析过程中,字段操作起着至关重要的作用。 无论是进行地图制作、空间分析还是数据统计,字段都是承载属性信息的基本单元。 ArcGIS Pro作为一款功能强大的GIS软件,为用户提…...

Git 的基本概念和使用方式。
Git 是一种分布式版本控制系统,用于跟踪文件和目录的变化。Git 的基本概念和使用方式如下: 仓库(Repository):Git 仓库是用来存储项目文件和历史记录的地方。一个 Git 仓库包含项目的文件、版本记录和配置信息。 提交…...

贪心算法--
1.柠檬水找零 link:860. 柠檬水找零 - 力扣(LeetCode) code class Solution { public:bool lemonadeChange(vector<int>& bills) {// 贪心算法, 优先花出大面额bill, 尽可能保护小面额billint five 0, ten 0;// 不…...

mysql下载与安装、关系数据库和表的创建
一、mysql下载: MySQL获取: 官网:www.mysql.com 也可以从Oracle官方进入:https://www.oracle.com/ 下载地址:https://downloads.mysql.com/archives/community/ 选择对应的版本和对应的操作系统&a…...

万字技术指南STM32F103C8T6 + ESP8266-01 连接 OneNet 平台 MQTT/HTTP
此博客为一份详细的指南,涵盖 STM32F103C8T6 通过 ESP8266-01 连接 OneNet 平台,并使用 MQTT/HTTP 进行数据通信的完整流程。这份文档包括: OneNet 平台的介绍与功能概览在 OneNet 上创建和配置设备的方法STM32CubeIDE 的开发环境搭建ESP826…...

MWC 2025 | 紫光展锐联合移远通信推出全面支持R16特性的5G模组RG620UA-EU
2025年世界移动通信大会(MWC 2025)期间,紫光展锐联合移远通信,正式发布了全面支持5G R16特性的模组RG620UA-EU,以强大的灵活性和便捷性赋能产业。 展锐芯加持,关键性能优异 RG620UA-EU模组基于紫光展锐V62…...

PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程(通用)!
PyCharm 接入 DeepSeek、OpenAI、Gemini、Mistral等大模型完整版教程(通用)! 当我们成功接入大模型时,可以选中任意代码区域进行解答,共分为三个区域,分别是选中区域、提问区域以及回答区域,我…...

小智智能体语言大模型硬件软件开发
硬件可以参考ESP32-AI语音助手 - 立创开源硬件平台 单片机使用esp32s3,可以直接替换,但是引脚IO有变化,而且esp32s3 io35 36 37不能用,所以得飞一条线,原先接在io35的飞到io4上。如果不飞线的话系统一直重启 软件使用…...

网络tcp协议设置,网络tcp协议设置不了
网络TCP协议的设置通常涉及到多个方面,包括IP地址、子网掩码、默认网关、DNS服务器等参数的配置,以及TCP/IP协议栈本身的配置。如果遇到网络TCP协议设置不了的问题,可能是由多种原因导致的。以下是一些可能的原因及解决方法: 一、…...

配置Hadoop集群
Hadoop的运行模式 本地运行:在一台单机上运行,没有分布式文件系统,直接读写本地操作系统的文件系统。特点:不对配置文件进行修改,Hadoop 不会启动 伪分布式:也是在一台单机上运行,但用不同的 …...

模型微调-基于LLaMA-Factory进行微调的一个简单案例
模型微调-基于LLaMA-Factory进行微调的一个简单案例 1. 租用云计算资源2. 拉取 LLaMa-Factory3. 安装依赖环境4. 启动 LLaMa-Factory 界面5. 从 Huggingface 下载模型6. 模型验证7. 模型微调 1. 租用云计算资源 以下示例基于 AutoDL 云计算资源。 在云计算平台选择可用的云计…...

设置重定向不缓存
response.setHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); response.setHeader(“Pragma”, “no-cache”);response.setHeader(“Expires”, “0”);response.sendRedirect(newURL); response.setContentType(“text/html;charsetUTF-8”); PrintWr…...

java-算法基础优化
一、ACM风格输入输出(高效,替换原有的输入输出流) 1.推荐原因:(内存托管) 对于原本的Scanner读取流,只能根据行来读取数据,而BufferredReader读取信息可以直接读取整个文件…...

⚡ 回声谷即时通讯系统
基于SpringBootVue3的实时通信解决方案 🌟 核心特性 #mermaid-svg-uxEwEcjlUVI6Tjjf {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-uxEwEcjlUVI6Tjjf .error-icon{fill:#552222;}#mermaid-svg-uxEwEcjl…...

《 PyQt5》—— 创建 Python GUI(图形用户界面)
文章目录 PyQt5安装基本概念进行配置配置QtDesigner配置PyUIC配置Pyrcc 使用PyQt5使用如何使用ui文件 PyQt5 PyQt5 是一个用于创建 Python GUI(图形用户界面)应用程序的强大工具包,它是 Qt 应用程序框架的 Python 绑定。Qt 是一个跨平台的 C…...

Python图形编程之EasyGUI: indexbox的用法
目录<<上一章:ynbox用法详解 下一章:boolbox用法详解 >> # 1 Python图形编程之EasyGUI: indexbox的用法 1.1 基本用法 indexbox提供用户一个选择不同选项的功能,不同的选项由按钮来表示,提供类似功能的还有choicebox…...

vue+dhtmlx-gantt 实现甘特图-快速入门【甘特图】
文章目录 一、前言二、使用说明2.1 引入依赖2.2 引入组件2.3 引入dhtmlx-gantt2.4 甘特图数据配置2.5 初始化配置 三、代码示例3.1 Vue2完整示例3.2 Vue3 完整示例 四、效果图 一、前言 dhtmlxGantt 是一款功能强大的甘特图组件,支持 Vue 3 集成。它提供了丰富的功…...