【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 ★★★ 本文前置知识: 文件系统以及相关系统调用接口 输入以及输出重定向 那么在此前的学习中,我们了解了文件的概念以及相关的系统调用接口,并…...
【互联网性能指标】QPS/TPS/PV/UV/IP/GMV/DAU/MAU/RPS
📕我是廖志伟,一名Java开发工程师、《Java项目实战——深入理解大型互联网企业通用技术》(基础篇)、(进阶篇)、(架构篇)清华大学出版社签约作家、Java领域优质创作者、CSDN博客专家、…...
Django工程获取请求参数的几种方式
在 Django 中获取请求参数的完整方法如下: 一、GET 请求参数获取 def view_func(request):# 获取单个参数(推荐方式)name request.GET.get(name, default) # 带默认值age request.GET.get(age, 0)# 获取多个同名参数(如复选框…...
【微知】如何根据内核模块ko查看所依赖其他哪些模块?(modinfo rdma_ucm |grep depends)
背景 有些情况下查看某个模块被哪些模块依赖可以用lsmod看到后面的列表,但是反向查看就要麻烦一些,比如某个模块依赖哪些其他模块?通过modinfo xxx.ko获取里面的depends相关信息 方法 modinfo rdma_ucm |grep depends实操 实操前先看依赖…...
基于大模型的结节性甲状腺肿诊疗全流程预测与方案研究报告
目录 一、引言 1.1 研究背景与目的 1.2 研究意义 1.3 国内外研究现状 二、大模型预测原理与方法 2.1 相关大模型概述 2.2 数据收集与预处理 2.3 模型训练与验证 三、术前预测与评估 3.1 结节性质预测 3.1.1 良恶性判断 3.1.2 与传统诊断方法对比 3.2 手术风险预测…...
Linux安装ComfyUI
Linux安装ComfyUI 1. ComfyUI2. 放置模型文件3. 创建python虚拟环境3.1 删除 Conda 虚拟环境 4. python虚拟环境,安装PyTorch5. 安装依赖6. 运行7. 打开8. 下载模型 移动到路径 1. ComfyUI # cat /etc/issue Ubuntu 20.04.6 LTS \n \lmkdir comfyUI cd comfyUI/git…...
订阅指南:用关键指标驱动业务增长
分析订阅业务远非看似简单。仅仅增加订阅数可能并不比维持一批忠实用户更有利可图。深入分析订阅数据及其背后的运作机制,将帮助您优化产品决策、预测收入并促进增长。本文将为您解读关键订阅指标的实际意义,并展示如何通过订阅宝这一专业工具࿰…...
2025华为OD机试真题E卷 - 螺旋数字矩阵【Java】
题目描述 疫情期间,小明隔离在家,百无聊赖,在纸上写数字玩。他发明了一种写法:给出数字个数 n (0 < n ≤ 999)和行数 m(0 < m ≤ 999),从左上角的 1 开始,按照顺时针螺旋向内写方式,依次写出2,3,…,n,最终形成一个 m 行矩阵。小明对这个矩阵有些要求: 1、…...
【开发学习】如何使用deepseek创建记录事件时间的PC应用程序
本文记录了尝试使用deepseek创建应用程序的过程,实现记录事件&时间的PC应用程序,包括创建代码、测试及调整。 目的:创建一个应用,用户输入文本提交,应用记录下时间和文本,数据留存在excel和应用程序中。…...
OSPF-单区域的配置
一、单区域概念: 单区域OSPF中,整个网络被视为一个区域,区域ID通常为0(骨干区域)。所有的路由器都在这个区域内交换链路状态信息。 补充知识点: OSPF为何需要loopback接口: 1.Loopback接口的…...
【2025力扣打卡系列】0-1背包 完全背包
坚持按题型打卡&刷&梳理力扣算法题系列,语言为python3,Day5 0-1背包【目标和】 有n个物品,第i个物品的体积为w[i], 价值为v[i]。每个物品至多选一个,求体积和不超过capacity时的最大价值和常见变形 至多装capacity&#x…...
分布式锁—Redisson的同步器组件
1.Redisson的分布式锁简单总结 Redisson分布式锁包括:可重入锁、公平锁、联锁、红锁、读写锁。 (1)可重入锁RedissonLock 非公平锁,最基础的分布式锁,最常用的锁。 (2)公平锁RedissonFairLock 各个客户端尝试获取锁时会排队,按照队…...
OpenEuler24.x下ZABBIX6/7实战1:zabbix7.2.4安装及zabbix-agent安装
兰生幽谷,不为莫服而不芳; 君子行义,不为莫知而止休。 1 安装及准备 先决条件:建议使用CentOS8以上的操作系统。 CentOS8.5.2111内核版本为 图1- 1 华为OpenEuler24(以后简称OE24)的内核为 [rootzbxsvr ~]# uname -r 5.10.0-…...
人工智能技术篇*卷(一)
了解人工智能的发展历史,会让我们心中有个大概了解,但这远远不够,我们起码还要知道大概有什么技术,怎么用它处理问题,有需要的话最好深入到算法原理。我们先从整体上看这个技术,接下来将不断细化。 我们知…...
ROS实践一构建Gazebo机器人模型文件urdf
URDF(Unified Robot Description Format)是一种基于XML的格式,用于描述机器人模型的结构、关节、连杆和传感器信息,并可以与Gazebo、RViz等仿真环境结合使用。 一、基础语法 1. urdf文件组成 URDF 主要由以下几个核心元素&#…...
C++学习——哈希表(一)
文章目录 前言一、哈希表的模板代码二、哈希计数器三、哈希表中的无序映射四、哈希表的总结 前言 本文为《C学习》的第11篇文章,今天学习最后一个数据结构哈希表(散列表)。 一、哈希表的模板代码 #include<iostream> using namespace…...
DeepSeek R1在医学领域的应用与技术分析(Discuss V1版)
DeepSeek R1作为一款高性能、低成本的国产开源大模型,正在深刻重塑医学软件工程的开发逻辑与应用场景。其技术特性,如混合专家架构(MoE)和参数高效微调(PEFT),与医疗行业的实际需求紧密结合,推动医疗AI从“技术驱动”向“场景驱动”转型。以下从具体业务领域需求出发,…...
Git 如何配置多个远程仓库和免密登录?
自我简介:4年导游,10年程序员,最近6年一直深耕低代码领域,分享低代码和AI领域见解。 通用后台管理系统 代号:虎鲸 缘由 每次开发后台界面都会有很多相同模块,尝试抽离出公共模块作为快速开发的基座。 目标…...
【Linux篇】从冯诺依曼到进程管理:计算机体系与操作系统的核心逻辑
📌 个人主页: 孙同学_ 🔧 文章专栏:Liunx 💡 关注我,分享经验,助你少走弯路! 文章目录 1.冯诺依曼体系结构存储分级理解数据流动 2. 操作系统(Operator System)2.1 概念2.2 设计OS的…...
【Linux docker】关于docker启动出错的解决方法。
无论遇到什么docker启动不了的问题 就是 查看docker状态sytemctl status docker查看docker日志sudo journalctl -u docker.service查看docker三个配置文件:/etc/docker/daemon.json(如果存在) /etc/systemd/system/docker.serviceÿ…...
数据结构:有序表的插入
本文是我编写的针对计算机专业考研复习《数据结构》所用资料内容选刊。主要目的在于向复习这门课程的同学说明,此类问题不仅仅使用顺序表,也可以使用链表。并且,在复习中,两种数据结构都要掌握。 若线性表中的数据元素相互之间可以…...
【医院内部控制专题】7.医院内部控制环境要素剖析(三):人力资源政策
医院成本核算、绩效管理、运营统计、内部控制、管理会计专题索引 一、引言 在当今医疗行业竞争日益激烈的背景下,医院内部控制的重要性愈发凸显。内部控制作为医院管理的关键组成部分,对于保障医院资产安全、提高会计信息质量、提升运营效率以及实现战略目标起着至关重要的…...
计算机网络——交换机
一、什么是交换机? 交换机(Switch)是局域网(LAN)中的核心设备,负责在 数据链路层(OSI第二层)高效转发数据帧。它像一位“智能交通警察”,根据设备的 MAC地址 精准引导数…...
CentOS7离线部署安装Dify
离线部署安装Dify 在安装 Dify 之前,请确保您的机器满足以下最低系统要求: CPU > 2 核 内存 > 4 GiB 1.安装docker和docker compose 启动 Dify 服务器最简单的方式是通过docker compose。因此现在服务器上安装好docker和docker compose…...
力扣hot100_二叉树(4)_python版本
一、199. 二叉树的右视图 思路: 直接复用层序遍历的代码,然后取每层的最后一个节点代码: class Solution:def rightSideView(self, root: Optional[TreeNode]) -> List[int]:层序遍历取每层的第一个if not root: return []res []queue …...
bug-Ant中a-select的placeholder不生效(绑定默认值为undefined)
1.问题 Ant中使用a-select下拉框时,placeholder设置输入框显示默认值提示,vue2ant null与undefined在js中明确的区别: null:一个值被定义,定义为“空值” undefined:根本不存在定义 2.解决 2.1 a-select使…...
Spring 面向切面编程 XML 配置实现
Spring 支持AOP ,并且可以通过XML配置来实现。 <beans xmlns"http://www.springframework.org/schema/beans"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xmlns:aop"http://www.springframework.org/schema/aop"xmlns:…...
【Pandas】pandas Series compare
# Pandas2.2 Series ## Computations descriptive stats |方法|描述| |-|:-------| |Series.compare(other[, align_axis, ...])|用于比较两个 Series| ### pandas.Series.compare pandas.Series.compare 方法用于比较两个 Series,并返回一个包含差异的 DataFram…...
颠覆语言认知的革命!神经概率语言模型如何突破人类思维边界?
颠覆语言认知的革命!神经概率语言模型如何突破人类思维边界? 一、传统模型的世纪困境:当n-gram遇上"月光族难题" 令人震惊的案例:2012年Google语音识别系统将 用户说:“我要还信用卡” 系统识别ÿ…...
【实战ES】实战 Elasticsearch:快速上手与深度实践-5.1.1热点分片识别与均衡策略
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 5.1.1 Filebeat Logstash ES Kibana 全链路配置实1. 架构设计与组件选型1.1 技术栈对比分析1.2 硬件配置推荐 2. Filebeat 高级配置2.1 多输入源配置2.2 性能优化参数 3.…...
