【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 ★★★ 本文前置知识: 文件系统以及相关系统调用接口 输入以及输出重定向 那么在此前的学习中,我们了解了文件的概念以及相关的系统调用接口,并…...
【Qt】成员函数指针
一、成员函数指针的本质 与普通函数指针的区别: // 普通函数指针 void (*funcPtr)() &普通函数;// 成员函数指针 void (MyClass::*memberFuncPtr)() &MyClass::成员函数;• 绑定对象:成员函数指针必须与类的实例对象结合使用 • 隐含 this 指…...
关于 Can Utils 的详细介绍、使用方法及配置指南
Can Utils:开源CAN总线工具集合 一、Can Utils简介 Can Utils 是一组开源的CAN总线工具链,专为嵌入式开发者和网络诊断工程师设计,支持Linux、Windows和macOS系统。它包含一系列轻量级命令行工具(如 cantoolz、candump、canbus …...
【Academy】OAuth 2.0 身份验证漏洞 ------ OAuth 2.0 authentication vulnerabilities
OAuth 2.0 身份验证漏洞 ------ OAuth 2.0 authentication vulnerabilities 1. 什么是 OAuth?2. OAuth 2.0 是如何工作的?3. OAuth 授权类型3.1 OAuth 范围3.2 授权代码授权类型3.3 隐式授权类型 4. OAuth 身份验证4.1 识别 OAuth 身份验证4.2 侦察OAuth…...
dify中使用NL2SQL
在 Dify 工作流中融入 NL2SQL(自然语言转 SQL)之能力,可依循如下步骤达成,借由 Dify 的模块化设计以及模型编排之功能,优化数据库查询之智能化交互: 一、环境准备与 Dify 部署 安装 Docker 与 Dify 务须确…...
android viewmodel如何使用
嗯,我现在要学习如何在Android中使用ViewModel。我之前听说过ViewModel是用来管理UI相关数据的,这样在配置变化比如屏幕旋转时数据不会丢失。但具体怎么用呢?我需要先回忆一下相关的知识。 首先,ViewModel应该是Android Architec…...
蓝桥杯备赛-基础训练(四)字符串 day17
好久不见,今天开始继续更新,或许拿不了奖,但是希望记录自己学习的过程,如果感觉有收获的同学在下面多多评论说说我代码的缺陷,感谢大家! 1、反转字符串 编写一个函数,其作用是将输入的字符串反…...
AI Agent开发框架分析:前端视角
1. Mastra (https://mastra.ai/docs) 优点: 提供直观的界面构建器,适合无代码/低代码开发支持JavaScript/TypeScript,可直接集成到前端项目可视化工作流设计,降低入门门槛内置多种UI组件,加速前端开发 缺点…...
第3节:IP地址分类与子网划分实战指南
IP地址分类与子网划分实战指南:从小白到入门 在网络通信中,IP地址是设备之间相互识别和通信的基础。无论是家庭网络还是企业网络,IP地址的分配和管理都是网络运维的核心任务之一。然而,对于初学者来说,IP地址的分类、子网掩码、CIDR(无类别域间路由)和VLSM(可变长子网…...
贪心算法三
> 作者:დ旧言~ > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:了解什么是贪心算法,并且掌握贪心算法。 > 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安! >…...
pytest基础知识
pytest知识了解 pytest的基础知识了解:Python测试框架之pytest详解_lovedingd的博客-CSDN博客_pytest框架 (包含设置断点,pdb,获取最慢的10个用例的执行耗时) pytest-pytest.main()运行测试用例,pytest参数: pytest-…...
JavaWeb后端基础(7)AOP
AOP是Spring框架的核心之一,那什么是AOP?AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。AOP是一种思想,而在Spring框…...
[AI]从零开始的ComflyUI安装教程
一、前言 AI画图如今已经进化到了让人难以想象的地步。早在2022年各大视频网站上就出现了许多真人使用AI绘制二次元形象的视频。在那个时期,也有人凭借AI画图狠狠的吃到了一波AI红利。在现在,对于普通人来说,AI画图仍然是非常值得探索的。不管…...
文本对抗样本系列的论文阅读笔记(整理合订)
文本对抗样本系列的论文阅读笔记 以前调研文本对抗样本时的论文笔记梳理,论文都很经典,有现成的框架(TextAttack)可以直接用,论文中部分内容直接是截取自论文,所以存在中英混合笔记的情况。 BERT-Attack …...
鸿基智启:东土科技为具身智能时代构建确定性底座
人类文明的每一次跨越都伴随着工具的革新。从蒸汽机的齿轮到计算机的代码,生产力的进化始终与技术的“具身化”紧密相连。当大语言模型掀起认知革命,具身智能正以“物理实体自主决策”的双重属性重新定义工业、医疗、服务等领域的运行逻辑。在这场革命中…...
javascript-es6 (六)
编程思想 面向过程 面向过程 就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次 调用就可以了 就是按照我们分析好了的步骤,按照步骤解决问题 面向对象 面向对象 是把事务分解成为一个个对象&am…...
【leetcode hot 100 19】删除链表的第N个节点
解法一:将ListNode放入ArrayList中,要删除的元素为num list.size()-n。如果num 0则将头节点删除;否则利用num-1个元素的next删除第num个元素。 /*** Definition for singly-linked list.* public class ListNode {* int val;* Lis…...
微信小程序将markdown内容转为pdf并下载
要在微信小程序中将Markdown内容转换为PDF并下载,您可以使用以下方法: 方法一:使用第三方API服务 选择第三方API服务: 可以选择像 Pandoc、Markdown-PDF 或 PDFShift 这样的服务,将Markdown转换为PDF。例如,PDFShift 提供了一个API接口,可以将Markdown内容转换为PDF格式…...
【贪心算法】将数组和减半的最小操作数
1.题目解析 2208. 将数组和减半的最少操作次数 - 力扣(LeetCode) 2.讲解算法原理 使用当前数组中最大的数将它减半,,直到数组和减小到一半为止,从而快速达到目的 重点是找到最大数,可以采用大根堆快速达到…...
【面试】Kafka
Kafka 1、为什么要使用 kafka2、Kafka 的架构是怎么样的3、什么是 Kafka 的重平衡机制4、Kafka 几种选举过程5、Kafka 高水位了解过吗6、Kafka 如何保证消息不丢失7、Kafka 如何保证消息不重复消费8、Kafka 为什么这么快 1、为什么要使用 kafka 1. 解耦:在一个复杂…...
PHP MySQL 创建数据库
PHP MySQL 创建数据库 引言 在网站开发中,数据库是存储和管理数据的核心部分。PHP 和 MySQL 是最常用的网页开发语言和数据库管理系统之一。本文将详细介绍如何在 PHP 中使用 MySQL 创建数据库,并对其操作进行详细讲解。 前提条件 在开始创建数据库之…...
通义万相 2.1 × 蓝耘智算:AIGC 界的「黄金搭档」如何重塑创作未来?
我的个人主页 我的专栏: 人工智能领域、java-数据结构、Javase、C语言,希望能帮助到大家!!! 点赞👍收藏❤ 引言 在当今数字化浪潮席卷的时代,AIGC(生成式人工智能)领域正…...
【面试题系列】:使用消息队列怎么防止消息重复?从原理到实战……
一、消息队列的核心价值与挑战 消息队列(MQ)作为现代分布式系统的基础设施,其核心价值在于解耦、削峰填谷和异步通信。但在追求高可靠性的过程中,消息重复成为必须攻克的技术难题。根据调研数据,在生产环境中消息重复…...
Damage与Injury
### “Damage”和“Injury”的区别 “Damage”和“Injury”都有“损害”或“伤害”的意思,但它们的用法、语境和侧重点有所不同。以下是从词性、适用对象、语义侧重和具体场景四个方面详细对比两者的区别: --- #### 1. **词性** - **Damage**…...
18 HarmonyOS NEXT UVList组件开发指南(五)
温馨提示:本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦! 第五篇:UVList组件最佳实践与实际应用案例 文章目录 第五篇:UVList组件最佳实践与实际应用案例1. 最佳实践总结1.1 组件设计…...
vue3组合式API怎么获取全局变量globalProperties
设置全局变量 main.ts app.config.globalProperties.$category { index: 0 } 获取全局变量 const { appContext } getCurrentInstance() as ComponentInternalInstance console.log(appContext.config.globalProperties.$category) 或是 const { proxy } getCurrentInstance…...
华为机试牛客刷题之HJ14 字符串排序
HJ14 字符串排序 描述 对于给定的由大小写字母混合构成的 n 个单词,输出按字典序从小到大排序后的结果。 从字符串的第一个字符开始逐个比较,直到找到第一个不同的位置,通过比较这个位置字符对应的(A<⋯<Z<a<⋯<…...
CPU 负载 和 CPU利用率 的区别
简单记录下 top 命令中,CPU利用率核CPU负载的概念, (1)CPU利用率:指在一段时间内 表示 CPU 实际工作时间占总时间的百分比。表示正在执行进程的时间比例,包括用户空间和内核空间程序的执行时间。通常包含以…...
SSM框架
SSM 框架是 Java Web 开发中广泛使用的经典组合,由 Spring、Spring MVC 和 MyBatis 三个开源框架整合而成,适用于构建中大型企业级应用。 1. SSM框架组成 框架作用核心特性Spring管理业务层(Service)和持久层(DAO&am…...
maven无法解析插件 org.apache.maven.plugins:maven-jar-plugin:3.4.1
解决流程 1.修改maven仓库库地址 2.删除本地的maven仓库 maven插件一直加载有问题: 无法解析插件 org.apache.maven.plugins:maven-jar-plugin:3.4.1 开始以为maven版本有问题,重装了maven,重装了idea工具。结果问题还是没解决。研究之后发现…...
如何修复“RPC 服务器不可用”错误
远程过程调用(Remote Procedure Call, RPC)是允许客户端在不同计算机上执行进程的众多可用网络进程之一。本文将深入探讨RPC如何在不同的软件系统之间实现无缝消息交换,同时重点介绍与RPC相关的常见错误的一些原因。 什么是远程过…...
晋升系列4:学习方法
每一个成功的人,都是从底层开始打怪,不断的总结经验,一步一步打上来的。在这个过程中需要坚持、总结方法论。 对一件事情长久坚持的人其实比较少,在坚持的人中,不断的总结优化的更少,所以最终达到高级别的…...
单链表-代码精简版
单链表核心知识详解 单链表是一种动态存储的线性数据结构,其特点是逻辑上连续,物理上非连续,每个节点包含数据域和指向下一个节点的指针域。以下是核心知识点与完整实现代码: 一、单链表的结构定义 单链表节点通过结构体自引用…...
关于前后端整合和打包成exe文件的个人的总结和思考
前言 感觉有很多东西,不知道写什么,随便写点吧。 正文 前后端合并 就不说怎么开发的,就说点个人感觉重要的东西。 前端用ReactViteaxios随便写一个demo,用于CRUD。 后端用Django REST Framework。 设置前端打包 import { …...
基于muduo+mysql+jsoncpp的简易HTTPWebServer
一、项目介绍 本项目基于C语言、陈硕老师的muduo网络库、mysql数据库以及jsoncpp,服务器监听两个端口,一个端口用于处理http请求,另一个端口用于处理发送来的json数据。 此项目在实现时,识别出车牌后打包为json数据发送给后端服务…...
Java/Kotlin逆向基础与Smali语法精解
1. 法律警示与道德边界 1.1 司法判例深度剖析 案例一:2021年某游戏外挂团伙刑事案 犯罪手法:逆向《王者荣耀》通信协议,修改战斗数据包 技术细节:Hook libil2cpp.so的SendPacket函数 量刑依据:非法经营罪ÿ…...
C++:入门详解(关于C与C++基本差别)
目录 一.C的第一个程序 二.命名空间(namespace) 1.命名空间的定义与使用: (1)命名空间里可以定义变量,函数,结构体等多种类型 (2)命名空间调用(…...
CI/CD—GitLab钩子触发Jenkins自动构建项目
GitLab钩子简介: 项目钩子 项目钩子是针对单个项目的钩子,会在项目级别的特定事件发生时触发。这些事件包括代码推送、合并请求创建、问题创建等。项目钩子由项目管理员或具有相应权限的用户进行配置,仅对特定项目生效。 使用场景:…...
RPA 职业前景:个人职场发展的 “新机遇”
1. RPA职业定义与范畴 1.1 RPA核心概念 机器人流程自动化(RPA)是一种通过软件机器人模拟人类操作,自动执行重复性、规则性任务的技术。RPA的核心在于其能够高效、准确地处理大量数据和流程,减少人工干预,从而提高工作…...
【CSS3】金丹篇
目录 标准流概念元素类型及排列规则块级元素行内元素行内块元素 标准流的特点打破标准流 浮动基本使用清除浮动额外标签法单伪元素法双伪元素法(推荐)overflow 法 Flex 布局Flex 组成主轴对齐方式侧轴对齐方式修改主轴方向弹性盒子伸缩比弹性盒子换行行对…...
Git(一)
一、介绍 二、Git代码托管服务 三、Git常用命令 全局设置: 获取Git仓库: 工作区、暂存区、版本库概念: Git工作区文件的状态: 本地仓库操作: 远程仓库操作: 分支操作: 标签操作: 四…...
Python大数据可视化:基于spark的短视频推荐系统的设计与实现_django+spider
开发语言:Python框架:djangoPython版本:python3.7.7数据库:mysql 5.7数据库工具:Navicat11开发软件:PyCharm 系统展示 管理员登录 管理员功能界面 热门视频界面 用户界面 用户反馈界面 论坛交流界面 系统…...
面试题之react useMemo和uesCallback
在面试中,关于 React 中的 useMemo 和 useCallback 的区别 是一个常见的问题。 useMemo 和 useCallback 的区别 1. 功能定义 useMemo: 用于缓存计算结果,避免在每次组件渲染时重新计算复杂的值。它接受一个计算函数和一个依赖数组࿰…...
K8S学习之基础十九:k8s的四层代理Service
K8S四层代理Service 四层负载均衡Service 在k8s中,访问pod可以通过ip端口的方式,但是pod是由生命 周期的,pod在重启的时候ip地址往往会发生变化,访问pod就需要新的ip地址,这样就会很麻烦,每次pod地址改变就…...
C++:string容器(下篇)
1.string浅拷贝的问题 // 为了和标准库区分,此处使用String class String { public :/*String():_str(new char[1]){*_str \0;}*///String(const char* str "\0") // 错误示范//String(const char* str nullptr) // 错误示范String(const char* str …...
sudo systemctl restart docker 重启docker失败
一般会使用如下命令,进行docker重启。 sudo systemctl daemon-reload sudo systemctl restart docker 重启失败时,会提示:Job for docker.service failed because the control process exited with error code. See "systemctl status…...
Linux基本操作指令3
1、wget: 这是一个用于从网络上下载文件的命令行工具。它支持 HTTP、HTTPS 和 FTP 协议。 wget http://download.qt.io/archive/qt/5.12/5.12.9/qt-opensource-linux-x64-5.12.9.run 2、下载完成后,你可以通过以下命令使文件可执行并运行安装程序: ch…...
React:类组件(上)
kerwin老师我来了 类组件的创建 class组件,js里的类命名首字符大写,类里面包括构造函数,方法 组件类要继承React.Component才有效 必须包含render方法 import React from react class App extends React.Component{render() {return <…...
sqli-lab靶场学习(七)——Less23-25(关键字被过滤、二次注入)
前言 之前的每一关,我们都是在末尾加上注释符,屏蔽后面的语句,这样我们只要闭合了区间之后,差不多就是为所欲为的状态。但如果注释符不生效的情况下,又该如何呢? Less23(注释符被过滤ÿ…...
虚函数和虚表的原理是什么?
虚函数是一个使用virtual关键字声明的成员函数,在基类中声明虚函数,在子类中可以使用override重写该函数。虚函数根据指针或引用指向的实际对象调用,实现运行时的多态。 虚函数表(虚表)是一个用于存储虚函数地址的数组…...