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

从物理扇区到路径访问:Linux文件抽象的全景解析

一、理解“一切皆文件”

1. 理解

我们已经知道OS是一款软硬件资源管理的软件那么它是怎么管理的呢先描述,在组织

对于进程,文件,都有其对应的PCB,file那么对于硬件设备呢OS通过驱动程序与硬件进行交互计算机有各种各样的硬件设备,这些设备也是需要被管理的

那么,在OS内一定也有对于硬件设备管理的数据结构。以键盘,显示器,网卡,鼠标,磁盘为例,这些硬件设备可以进行读取,也可以进行写入,其中有些硬件设备只需要读取,不需要写入(键盘),有些需要写入不需要读取(显示器),有些既需要读取又需要写入(磁盘)。

所以,计算机外设,有的只读,有的只写,有的读写。但是不同的设备,它们的访问方式一定是不同的。访问设备,都是谁在访问呢?我们在编写代码中,使用 scanf 函数获取键盘数据,printf 函数向显示器打印数据本质都是进程通过OS访问硬件设备的。所以,接下来,我们就要聊聊进程与硬件之间的关系了

我们已经知道,进程PCB里有一个 struct file-struct * files指针,该指针指向一个 struct file_struct 的结构体,该结构体里有一个 struct file* fd_array[]结构体指针数组,该数组里存放的就是一个个 struct file 结构体的地址

OS大部分都是由C语言编写的,在C语言结构体中,无法定义函数的实现,但是可以有函数指针呀。

每个设备都有对应的 struct file,所以,对于每个硬件设备的读写,只需要有两个函数指针就可以解决了。而这两个函数指针都是在 struct file 文件结构体里的

所以,在 linux 上怎么看待一切皆文件呢是站在进程的视角,struct file 结构体之上,看待文件的视角

当然了,管理硬件肯定不是只有这么一点,只需要两个指针就完成了。这里只是以读写为例。在底层肯定有许多函数指针管理硬件的使用

在 linux 中,打开文件,要创建 struct file,三个核心

1.文件属性

2.文件内核缓冲区

3.底层设备文件的操作表(struct file_operations)

2. 缓冲区

缓冲区的本质其实就是一段内存空间

举个例子理解一下缓冲区。

你是张三,家在云南,有一个好朋友叫李四,在北京。李四即将过生日,你买了一个机械键盘作为礼物给李四,难道你要亲自从云南跑到北京去吗?当然不可能了。你直接跑到楼下面,将礼物给菜鸟驿站,让菜鸟驿站给你送过去就可以了。对于你而言,这样可以节省大量的人力,物力。但是菜鸟驿站拿到你的东西,会立即发送吗?当然也不会了。

所以,内存空间允许数据在缓冲区中积压,一次就可以发送多次数据,变相的减少IO的次数

缓存的最大意义是提高使用缓存的进程的效率

老板什么时候发送快递就相当于使用什么样的刷新策略?

1.无缓冲,立即刷新

2.有缓冲,行刷新。(显示器使用)

3.有缓冲,写满再刷新。(普通文件采用这种方式)

两种情况

1.进程退出时,主动刷新缓存

2.进程强制刷新(fflush)

简单验证一下。

在这里插入图片描述
在这里插入图片描述

经过验证,可以看到在2秒内,显示器上是没有数据的。那么,期间数据在哪里呢

是在语言级缓冲区上的。那么,这个缓冲区它在哪里呢?我们怎么从来没见过。

还记得 FILE 结构体吗?C语言访问文件都是通过 FILE访问的,包括 stdin, stdout, stderr。FILE 本质就是一个结构体。我们可以在 /usr/include/libio.h (每个系统可能会不一样)文件里查找,在这个结构体里就有缓冲区

在这里插入图片描述

现在,我们重新理解 printf , scanf 的格式化过程。它们将数据进行格式化,那么格式化到哪里了呢格式化结果写入 FILE 缓冲区中。所以,数据并没有写入内核缓冲区中,它还需要检测是否需要刷新

在这里插入图片描述

所以,这段代码,log.txt 为什么没有数据呢? 这是因为,printf 在格式化数据的时候,结果是被写入到了语言级缓冲区,并没有刷新到内核缓冲区,普通文件是全缓冲。而此时我们已经关闭了 log.txt 文件,OS会释放该文件的资源

在这里插入图片描述

调用 fflush 会强制刷新数据到内核缓冲区,然后再关闭该文件,此时数据已经刷新到了内核缓冲区中,文件里自然就有内容了。

或者进程退出自动刷新缓冲区,经过验证,在前两秒内,数据并没有写入到 log.txt ,两秒之后,进程退出,数据写入到该文件里。

在这里插入图片描述
在这里插入图片描述

相信大家已经理解了,是不是觉得自己又行了。那当我祭出如下代码的时候,阁下又要如何应对呢?

在这里插入图片描述
在这里插入图片描述

这是为什么呢?我想有不少小伙伴看懵了吧。

write 是系统调用,它会把数据直接拷贝到内核缓冲区中,所以 hello systemcall只有一份,而 printf, fprintf, fwrite 是C语言库函数,它们会把数据拷贝到用户缓冲区中,由于是重定向文件,所以是全缓冲刷新策略,数据会被积压在用户缓冲区中,fork 之后,子进程以父进程为模板,子进程用户缓冲区中也会有相同的数据积压在缓冲区里,进程退出时,父子进程各自刷新缓冲区,所以有两份数据

而向显示器上打印数据,是行缓冲策略,所以,数据会按照代码的顺序依次打印在终端上

在C,C++里除了标准输入输出流,还有标准错误流。我们来看下面的一段程序是怎么回事呢?

在这里插入图片描述
在这里插入图片描述

这是为什么呢?标准输出流,标准错误流都是向显示器打印的。而我们做了输出重定向,就默认向 1 号文件描述符指向的文件进行写入,而 2 号文件描述符不受影响,就打印在了显示器上

同理,我们指定2号文件描述符输出重定向,perror, std::error 就打印到了2号文件描述符指定的文件里,而 printf, std::cout 就打印在了显示器上

当然了,也是有办法可以将它们打印在一个文件里的。

在这里插入图片描述

二、简单设计libc库

. mystdio.h

#ifndef __MY__STDIO__H
#define __MY_STDIO__H#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>#define SIZE 4096#define FLUSH_NONE 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4#define NORMAL 1
#define FORCE 2#define UMASK 0666typedef struct MY__IO__FILE
{int _fileno;int flag;//刷新策略char outbuffer[SIZE];//缓冲区int curr;//缓冲区当前的位置int cap;//缓冲区容量
}MYFILE;MYFILE* my_fopen(const char* filename, const char* mode);
void my_fclose(MYFILE* fp);
int my_fwrite(const char* s, int size, MYFILE* fp);
void my_fflush(MYFILE* fp);
#endif

. mystdio.c

#include"mystdio.h"MYFILE* my_fopen(const char* filename, const char* mode)
{int fd = -1;//文件描述符if(strcmp(mode, "w") == 0){fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, UMASK);}else if(strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if(strcmp(mode, "a") == 0){fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, UMASK);}else if(strcmp(mode, "a+") == 0){fd = open(filename, O_CREAT | O_RDWR | O_APPEND, UMASK);}else{}MYFILE* fp = (MYFILE*)malloc(sizeof(MYFILE));if(!fp)return NULL;fp->_fileno = fd;fp->flag = FLUSH_LINE;fp->outbuffer[0] = 0;fp->curr = 0;fp->cap = SIZE;return fp;}static void my_fflush_core(MYFILE* fp, int force)
{if(fp->curr <= 0)return;if(force == FORCE){write(fp->_fileno, fp->outbuffer, fp->curr);fp->curr = 0;}else{if((fp->flag & FLUSH_LINE) && fp->outbuffer[fp->curr - 1] == '\n'){write(fp->_fileno, fp->outbuffer, fp->curr);fp->curr = 0;}else if((fp->flag & FLUSH_FULL) && fp->curr == fp->cap){write(fp->_fileno, fp->outbuffer, fp->curr);fp->curr = 0;}else{}}
}int my_fwrite(const char* s, int size, MYFILE* fp)
{memcpy(fp->outbuffer+fp->curr, s, size);fp->curr += size;my_fflush_core(fp, NORMAL);return size;}void my_fflush(MYFILE* fp)
{my_fflush_core(fp, FORCE);
}void my_fclose(MYFILE* fp)
{if(fp->_fileno >= 0){my_fflush(fp);//用户->内核fsync(fp->_fileno);//内核->外设close(fp->_fileno);free(fp);}
}

. test.c

#include"mystdio.h"int main()
{MYFILE* fp = my_fopen("log.txt", "w");if(fp == NULL)return 1;const char* str = "hello linux";int cnt = 20;char data[128];while(cnt--){snprintf(data, sizeof(data), "%s:%d", str, cnt);//printf("%s\n",data);my_fwrite(data, strlen(data), fp);sleep(1);}my_fclose(fp);return 0;
}

. Makefile

mystdio:mystdio.c test.cgcc -o $@ $^
.PHONY:clean
clean:rm -f mystdio

三、理解硬件(磁盘)

1. 认识磁盘

我们用一张图来简单的认识一下磁盘。

在这里插入图片描述

主轴马达高速转动会带动盘片,磁头会上下摆动读取盘片上的数据。盘片分正反两面,一片两面,两片四面…。

我们把盘片的示意图看一下。

在这里插入图片描述

我们可以把盘片看成半径不同的同心圆每一个同心圆就是一个个磁道同心圆可以看成由一个个扇区组成中间空白的部分就是没有数据,数据都在扇区上

扇区是OS访问磁盘设备的基本单位:一般是512字节

一个盘片两面,从面0开始进行编号,而磁头和盘面是一对一的,一面一磁头

磁头是共进退的。也就是说,在同一时间,磁头在不同的盘面上指向的是同一个磁道,同一个扇区(这里我们认为每个面上的每个磁道的扇区数是相同的,所有盘面同一磁道的扇区严格对齐)。

所有盘面半径相同的磁道构成一个柱面

在这里插入图片描述

磁头左右摆动本质是在定位哪一个磁道(柱面)

盘片旋转的本质是确定哪一个磁道(柱面),定位该磁道(柱面)上的哪一个扇区

磁头和盘片转动的本质是对磁盘特定位置进行寻址

2. 寻址方式

磁头,磁道编号都是从0开始的,扇区的编号是从1开始的。那么,我们怎么在一个磁盘里定位任意一个扇区呢?

1.选择磁道(cylinder)

2.选择磁头(header)

3.选择扇区(sector)

我们把这种寻址方式叫做CHS地址定位法。但这种技术太老了,现在已经不怎么用了。

大家小的时候应该都见过磁带吧。

在这里插入图片描述
通过轴转动,磁带可以从一边转到另一边。我们可以把磁带全部抽出来,它就变成了一条很长的磁带,而将它卷起来,不就类似于磁盘的示意图吗。

所以,我们可以将磁盘的存储结构抽象成一个一维数组

盘片的每一面就类似于这样:

在这里插入图片描述

所以,现在如何定位一个扇区呢?一个数组下标:定位任意一个扇区,我们把这种地址定位法叫做LBA(logic block address)地址

但是磁盘只认识CHS,所以,我们要如何把LBA转成CHS呢?

其实很简单,假设,一个磁盘有5个盘面,每个盘面100个磁道,每个磁道10个扇区,当LBA为124时,是哪一个盘面,那个磁道,那个扇区呢?

124 / 100 = 1(header),124 % 100 = 24,24 / 10 = 2(cylinder),24 % 10 = 4(sector)。

上面是我们基于磁盘物理层面的认识,我们以盘面为例进行展开。但是,磁盘实际并不是以盘面展开的,而是以柱面展开的

某一盘面的某个磁道展开:

在这里插入图片描述

这不就是一维数组

整个磁盘所有盘面的同一个磁道展开,即柱面展开:

在这里插入图片描述

这不就是二维数组

再将整个磁盘展开:

在这里插入图片描述

这不就是将每个柱面作为数组中的一个元素嘛。所以,整个磁盘其实就可以看成一个三维数组。寻址的时候,先找柱面,再找磁道,最后是扇区,不就刚好和CHS的顺序对应嘛

在C/C++中,二维数组是不是也可以看做是一维数组呀,三维数组也是一样的

在这里插入图片描述

所以,磁盘的存储结构可以被看做是一个线性结构线性结构的地址就是LBA,它可以被转化为三维数组的三个下标

LBA转化为CHS,本质就是一维数组的下标,转换成三个数字

OS只需要使用LBA就可以了。LBA怎么转换CHS,CHS怎么转换成LBA?这是由磁盘来完成的。

现在,大家应该对磁盘有了一个比较深刻的理解了。磁盘就是一个以sector为单位的一维数组

3. 管理磁盘

磁盘是一个典型的块设备,它是以512字节为单位的,但是OS读取磁盘数据的时候,并不是以磁盘的基本单位来读取的,而是以4KB为单位读取,也就是8个扇区,一个块,以块为单位读取的

“块”是文件存取的最小单位

总结OS看待磁盘,就认为磁盘是一个块设备,每个块都有下标

文件系统的角度磁盘当做 block array[N]---->块设备

假设有一个磁盘800GB,那么它有多少个块呢?应该有209715200个块,那么要怎么对这些块进行管理呢?

这就像我们国家有如此广阔的土地,要管理这些土地,要怎么管理呢?国家会设置各个省份,每个省内设置市,市下面设置县…,每个地方都是如此,省份集中归于中央管理。磁盘也是类似的。

磁盘也会进行划分,每个区划分多大空间,一共划分几个区,这个就叫做分区

理解一下,OS是怎么把数据写给磁盘的。

寄存器,大家都听过吧。不止CPU内部有寄存器,外设内部也有寄存器

举个例子:磁盘内部有各种各样的寄存器,以dir, addr, data三个寄存器为例,OS向磁盘写数据,就会通过IO总线向dir寄存器发送w,addr寄存器写入LBA地址,data寄存器写入数据,然后磁盘将LBA地址转化为CHS地址,寻址之后,磁盘中的控制电路会根据w指令将数据写入到CHS地址

如果你要读数据,也是类似的。OS通过IO总线,向dir寄存器发送r,addr寄存器写入LBA地址,磁盘将LBA地址转化为CHS地址,控制电路根据 r 指令,在CHS地址处读取数据到data寄存器中,然后再被OS获得

上面我们谈论到了,磁盘的空间很大,会被划分成很多的块,为了管理这么多的块,所以有了分区,但是分区的空间还是很大,也不方便管理,所以对分区进行了进一步的划分,只要管理好了组,每个组按照同样的方式去管理,就管好了分区,管理好了分区就管好了磁盘。每个分区的信息(分区的开始位置,结束位置)都被写在了磁盘的第一个扇区中,分区1【start, end】,分区2【start,end】,分区3【start,end】,我们把它叫做分区表

所以现在,我们着重就要理解怎么管理好一个组了。

在这里插入图片描述

文件 = 文件内容 + 文件属性这些都是文件的数据,都要被存储

1.在linux中,文件的内容和属性是分开存储的

2.OS和磁盘文件进行IO,是以4KB为单位的

Data Blocks中只保存文件的内容,以4KB为单位,绝大部分的磁盘分组空间,都被DataBlocks占据

DataBlocks是有空间大小的,一个数据块占4KB,那么DataBlocks中一定有大量的数据块,如果一个文件大小是16KB,它只需要4个数据块。

每一个数据块都是有编号的。那么,DataBlocks中也会有不同的文件内容占据数据块,那么如何判断哪些数据块是被哪些文件拥有的呢?这个问题稍后回答。

现在,我们还有一个问题,有些数据块是被使用了的,有些没有被使用。那么,要如何判断那些数据块是被使用了的呢?要根据数据块里面的内容吗?当然不是了,数据块里面都是二进制,我们也不认识。

不知道大家注意到了Block Bitmap,没错,就是根据它来判断的。

Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪些数据块没有被占用

Block Bitmap是一个块位图,它是根据 bit 的位置和内容标记数据块的

bit 的位置:表示对应哪一个数据块

bit 的内容[0,1]:表示对应的数据块是否被占用

假设,我们有100万个数据块,那么就需要31个数据块(Block Bitmap),100万个 bit 位来表示Data Blocks中数据块的占用情况。

现在,已经有了一个共识了。文件 = 文件内容 + 文件属性。文件内容聊的差不多了,现在,就来聊聊文件属性。

Linux中,表达文件属性,用结构体 struct inode表示文件的属性既然是结构体,那么它的大小就是固定的。这个结构体大小一般是128字节或者256字节

在磁盘上,文件不止有一个,那么文件的属性内容也会有很多,但是它们的属性内容虽然不一样,文件的属性是一样的。这就像街上有许多人,有很多人性别是男,有很多人性别是女。他们虽然性别不一样,但是性别这个属性是一样的,只是向这个属性中填写了不同的内容。

OS读取文件,读取 inode ,一次依旧4KB读取如果按照 inode大小为128字节计算,那么一次可以读取32个文件的 inode,这也意味着,文件加载时,并不是一个一个文件加载到内存的,而是一次性读取多个文件的 inode

一般而言,一个文件一个 inode,一个文件可能对应0个或者多个 data block

注意两个问题

1.磁盘当中有许多文件,为了标示文件的唯一性,struct inode中会存在一个 int inode_number

2.Linux中,文件名不能也不再 inode 中保存

struct inode是存储在 inode Table中的,inode Table是当前分组所有 inode属性的集合,inode编号以分区为单位,整体划分,不可跨分区。我们把 inode Table叫做 i 节点表

inode Table也是以4KB为单位,假设一共16KB,那么 inode Table一共有128个 inode。那么,这么多的 inode,那些是被使用了的,哪些是未被使用的,怎么判断呢?

inode Bitmap是不是和 Block Bitmap很像呢?inode Bitmap就是用来判断 inode是否被使用了的。它也是用位图的方式来判断

bit的位置:表示第几个 inode

bit的内容[1/0]:表示对应的 inode是否被占用

那么,现在有一个新的问题。文件 = 文件内容 + 文件属性,但是文件内容和文件属性是分开的呀,将来我们可以先拿到文件的 inode号,可是该文件的内容怎么找呢?

struct inode中是存在和 block 的映射关系表的,其实就是一个数组,这个数组里存着该文件的文件块块号

在这里插入图片描述

只要通过 inode编号,就能找到文件对应的 inode属性,内部具有和数据块的对应关系,就能进一步找到文件的内容了

现在,文件的内容和属性都已经了解了,还剩下两个GDT(Group Descriptor Table)和Super Block。

GDT,块组描述符表,描述块组属性信息,整个分区分成多少块组就对应多少个块组描述符,每个块组描述符存储一个块组的描述信息。比如:这个块组中从哪里开始是 inode Table,从哪里开始是Data Blocks,空闲的 inode和数据块还有多少个等等。

Super Block:存放文件系统本身的结构信息,描述整个分区的文件系统信息。那么,就有问题了?

问题1:Super Block既然是描述一个分区的所有分组的整体情况,为什么会在一个块组中,不应该在 File System中吗

Super Block不仅仅在一个组里,可能会同时存在多个 block group中。不一定所有的组都有 super block,但是几乎多个组会同时存在同样的 super block

如果 super block 放在了 file system中,那么一旦file system数据刮花了,整个分区都崩了,但是放在多个块组里,即便块组也坏了,那影响也非常小,而且多个块组里都有 super block,即便其中一个坏了,还可以用其它块组里的 super block来修复。这就是对数据的备份

问题2:新建一个分区,super block 和 gdt 一定是有效数据

磁盘上新建分区,OS就要给特定分区,写入管理信息,即写入文件系统和分区分组相关的管理数据,文件数据可以暂时不要。

问题3:访问一个文件,在分区内,标示该文件的唯一性:inode编号

那么,描述一下,1.新建文件 2.删除文件 3.修改文件 4.查看文件。

新建文件在 inode bitmap 中找 bit 为0,将它改为1,代表申请一个 inode,然后根据 bit的位置在 inode table相对应位置处的 inode中填写数据。文件有内容的话,就在 block bitmap中查找 bit 为0,改为1,申请数据块,然后再 data blocks中写入数据

删除文件拿着 inode编号,在inode中找到与数据块的映射表,在block bitmap中将数据块块号对应的 bit 改为0,再将 inode bitmap中对应的 bit 改为0

所以,删除文件其实只是改了位图,并没有改动数据有时候误删了数据,通过一些工具还能找回来。但是,不要做编辑操作,否则可能会覆盖数据

剩下的也不用说了,也是可以理解的。

问题4:关于 inode编号和 datablock编号,是全分区统一分配的,不是只在分组内有效

inode不能跨区域,一个分区,一个文件系统,互相独立

在这里插入图片描述

问题5:上面所说的都是用 inode编号来查找文件内容和属性的,可是我们从来没使用过 inode,一直用的都是文件名啊

Linux下一切皆文件,那么目录是文件吗?当然是了

什么是目录呢?目录 = 目录的内容 + 目录的属性

目录的属性就是 struct inode,只不过和普通文件(inode)存储中存储的内容不一样

那目录的内容存的是什么呢?它也要有 data block,只不过它里面存的是文件名与 inode的映射关系。通过文件名就可以找到对应的 inode

在这里插入图片描述

所以,同一个目录下,文件名不能重复。毕竟每个文件的 inode编号是唯一的

指定目录下新建文件的本质是将文件名与 inode的映射关系,写入到当前目录的 data block里面

这就是为什么在当前目录下新建文件,需要该目录具有 w 权限

读取一个文件属性的话,需要当前目录具有 r 权限

对目录设置 rw 权限本质是约束用户,访问目录的 data block

至于 x 权限,是允许是否打开目录的,就有点抽象了。

那么,大家有没有疑问呢?比如说,我们要打开一个文件就要打开该文件的目录,那么打开该目录也要打开它的上一级目录,以此类推,直到打开根目录

而根目录是确定的。也就是说,要打开一个特定路径下的文件,我们需要对该文件所在的路径进行解析

这就是 open文件的时候,必须要有路径的原因

这就是Linux下访问任意文件,都需要路径访问

这就是为什么进程PCB,要有 cwd 的根本原因

4. 路径解析

但是,每一次访问文件都需要解析路径,难道不慢吗?

现在来看,目录和文件存储方式是没有区别的。但是,我们是可以看到有目录树的啊!并且,它这样不慢吗?

Linux中,当用户访问指定路径下的文件(目录,普通文件),在路径解析的过程中,在内核中形成目录树和路径缓存

在这里插入图片描述
在这里插入图片描述

所以,路径解析的时候,只有第一次是慢的,第2次,第n次的时候路径解析会优先从 dentry 树结构中进行解析

5. inode table 与 data block的映射关系表

现在,我们把重点放在 inode table中的 inode 与 data block的映射关系表上

在这里插入图片描述
在这里插入图片描述

可以看到,数组大小是15,那么一个块4KB,那也才60KB大小呀。可是,一个文件的大小有时候可是很大的,这怎么能够存的下呢?

其实,数组的前12个下标(0~11)直接用来存储块地址。剩下的三个,依次是一级间接块索引表指针,二级间接块索引表指针,三级间接块索引表指针一个指针的大小也就是4/8字节。我们按照4字节来计算,一级索引表指针指向的块能够存储1024个块地址,也就是1024个块

那如果是二级索引表指针呢?那不就是1024 * 1024个块了吗。三级索引表指针呢?就是1000000000个块了吗(大致算了一下)。这不就足够了吗。

在这里插入图片描述

6. 挂载分区

我们已经能够根据 inode 号在指定分区内找文件了,也能根据目录文件内容,找指定的 inode 了可是 inode 不是不能跨分区吗,Linux下有很多分区啊,我怎么直到我在哪一个分区

这是因为每一个分区都会被挂在到一个指定的目录下,所以查找文件时,根据 inode 才能知道自己在哪一个分区下查找

在这里插入图片描述

/dev/vda1就被挂在到了根目录下。这就是为什么要有路径的原因,根据路径就可以进行字符串的匹配,切换挂载分区,就可以根据 inode 找到对应的文件了

今天的文章分享到此结束,觉得不错的小伙伴给个一键三连吧。

http://www.xdnf.cn/news/1241947.html

相关文章:

  • 深入剖析RT-Thread串口驱动:基于STM32H750的FinSH Shell全链路Trace分析与实战解密(上)
  • 深度学习TR3周:Pytorch复现Transformer
  • OpenCV轻松入门_面向python(第二章图像处理基础)
  • JS--获取事件的子元素与父元素
  • 思途Spring学习 0804
  • 【Keras学习笔记】手写识别
  • C++-异常
  • Linux84 SHELL编程:流程控制 前瞻(1)
  • 贯穿全生命周期,生成式AI正在重塑游戏行业
  • Coze Loop:开源智能体自动化流程编排平台原理与实践
  • k8s集群
  • 案件线索展示与交付项目
  • 数据结构:如何判断一个链表中是否存在环(Check for LOOP in Linked List)
  • 深度学习图像处理篇之AlexNet模型详解
  • 【PHP】对比两张图片的相似度
  • WPF 按钮背景色渐变
  • 服务器的Mysql 集群技术
  • linux下docker安装ollama
  • Petalinux快捷下载
  • 部署 Kibana 8.2.2 可视化管理 Elasticsearch 8.2.2 集群
  • RabbitMQ--介绍
  • 【深度学习新浪潮】近三年零样本图像分类研发进展调研
  • 文件与目录操作命令
  • MySQL 基本操作入门指南
  • Apache IoTDB(3):时序数据库 IoTDB Docker部署实战
  • [GYCTF2020]FlaskApp
  • Nginx vs Spring Cloud Gateway:限流功能深度对比与实践指南
  • 直角坐标系里的四象限对NLP中的深层语义分析的积极影响和启示
  • spring boot开发中的资源处理等问题
  • 怎样推动AI技术在人机协同中的发展?