iOS之深入解析文件内存映射MMAP

一、常规文件操作

  • 常规文件操作(read/write)有以下重要步骤:
    • 进程发起读文件请求;
    • 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的 inode;
    • inode 在 address_space 上查找要请求的文件页是否已经缓存在内核页高速缓冲中。如果存在,则直接返回这片文件页的内容;
    • 如果不存在,则通过 inode 定位到文件磁盘地址,将数据从磁盘复制到内核页高速缓冲,之后再次发起读页面过程,进而将内核页高速缓冲中的数据发给用户进程。
  • 常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。由于页缓存处在内核空间,不能被用户进程直接寻址,所以需要将页缓存中数据页再次拷贝到内存对应的用户空间中;
  • read/write 是系统调用很耗时,如下图,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,实际上完成了两次数据拷贝;

在这里插入图片描述

  • 如果两个进程都对磁盘中的一个文件内容进行访问,那么这个内容在物理内存中有三份:进程 A 的地址空间 + 进程 B 的地址空间 + 内核页高速缓冲空间;
  • 写操作也是一样,待写入的 buffer 在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

二、mmap 内存映射

① mmap 简介
  • mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:

在这里插入图片描述

  • 由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
  • 在日常开发中偶尔会遇到 mmap,它最常用到的场景是 MMKV,其次用到的是日志打印。
  • 进程是 App 运行的基本单位,进程之间相对独立。iOS 系统中 App 运行的内存空间地址是虚拟空间地址,存储数据是在各自的沙盒。当在 App 中去读写沙盒中的文件时,会使用 NSFileManager 去查找文件,然后可以使用 NSData 去加载二进制数据。文件操作的更底层实现过程,是使用 linux 的 read()、write() 函数直接操作文件句柄(也叫文件描述符 fd)。
  • 在操作系统层面,当 App 读取一个文件时,实际是有两步:
    • 将文件从磁盘读取到物理内存;
    • 从系统空间拷贝到用户空间(可以认为是复制到系统给 App 统一分配的内存)。
  • iOS 系统使用页缓存机制,通过 MMU(Memory Management Unit)将虚拟内存地址和物理地址进行映射,并且由于进程的地址空间和系统的地址空间不一样,所以还需要多一次拷贝。
  • 而 mmap 将磁盘上文件的地址信息与进程用的虚拟逻辑地址进行映射,建立映射的过程与普通的内存读取不同:正常的是将文件拷贝到内存,mmap 只是建立映射而不会将文件加载到内存中。

在这里插入图片描述

  • 在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程由系统调用 mmap() 实现,所以建立内存映射的效率很高。
  • 既然建立内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接通过内存操作访问到硬盘上的文件呢?那就要看内存映射之后的几个相关的过程。
  • mmap() 会返回一个指针 ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用 read 或 write 对文件进行读写,而只需要通过 ptr 就能够操作文件。但是 ptr 所指向的是一个逻辑地址,要操作其中的数据,必须通过 MMU 将逻辑地址转换成物理地址,如上图中过程 2 所示。这个过程与内存映射无关。
  • 前面说道,建立内存映射并没有实际拷贝数据,这时,MMU 在地址映射表中是无法找到与 ptr 相对应的物理地址的,也就是 MMU 失败,将产生一个缺页中断,缺页中断的中断响应函数会在 swap 中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过 mmap() 建立的映射关系,从硬盘上将文件读取到物理内存中,如上图中过程 3 所示。这个过程与内存映射无关。
  • 如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如图1中过程4所示。这个过程也与内存映射无关。
  • mmap 内存映射的实现过程,总的来说可以分为三个阶段:
    • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
    • 调用内核空间的系统调用函数 mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系;
    • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
② 适用场景
  • 有一个很大的文件,因为映射有额外的性能消耗,所以适用于频繁读操作的场景;(单次使用的场景不建议使用)。
  • 有一个小文件,它的内容您想要立即读入内存并经常访问。这种技术最适合那些大小不超过几个虚拟内存页的文件(页是地址空间的最小单位,虚拟页和物理页的大小是一样的,通常为 4KB)。
  • 需要在内存中缓存文件的特定部分。文件映射消除了缓存数据的需要,这使得系统磁盘缓存中的其他数据空间更大。
  • 当随机访问一个非常大的文件时,通常最好只映射文件的一小部分。映射大文件的问题是文件会消耗活动内存。如果文件足够大,系统可能会被迫将其他部分的内存分页以加载文件。将多个文件映射到内存中会使这个问题更加复杂。
③ 不适合的场景
  • 希望从开始到结束的顺序从头到尾读取一个文件;
  • 文件有几百兆字节或者更大。将大文件映射到内存中会快速地填充内存,并可能导致分页,这将抵消首先映射文件的好处。对于大型顺序读取操作,禁用磁盘缓存并将文件读入一个小内存缓冲区;
  • 该文件大于可用的连续虚拟内存地址空间。对于 64 位应用程序来说,这不是什么问题,但是对于 32 位应用程序来说,这是一个问题。32 位虚拟内存最大是 4GB,可以只映射部分;
  • 因为每次操作内存会同步到磁盘,所以不适用于移动磁盘或者网络磁盘上的文件;
  • 变长文件不适用。
④ mmap 内存映射原理
  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域:
    • 进程在用户空间调用库函数 mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
    • 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址;
    • 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化;
    • 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中。
  • 调用内核空间的系统调用函数 mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系:
    • 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息;
    • 通过该文件的文件结构体,链接到 file_operations 模块,调用内核函数 mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数;
    • 内核 mmap 函数通过虚拟文件系统 inode 模块定位到文件磁盘物理地址;
    • 通过 remap_pfn_range 函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
    • 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
    • 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
    • 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
    • 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
  • 前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
  • 修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用 msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

三、iOS 中的 mmap

  • 以官方的demo为例,其它的代码很简明直接,核心就在于 mmap 函数:
	/**
	 *  @param  start  映射开始地址,设置 NULL 则让系统决定映射开始地址
	 *  @param  length  映射区域的长度,单位是 Byte
	 *  @param  prot  映射内存的保护标志,主要是读写相关,是位运算标志;(记得与下面fd对应句柄打开的设置一致)
	 *  @param  flags  映射类型,通常是文件和共享类型
	 *  @param  fd  文件句柄
	 *  @param  off_toffset  被映射对象的起点偏移
	 */
	void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
	
	*outDataPtr = mmap(NULL,
	                   size,
	                   PROT_READ|PROT_WRITE,
	                   MAP_FILE|MAP_SHARED,
	                   fileDescriptor,
	                   0);
  • 用官网的代码做参考,实现一个读写的例子:
	#import "ViewController.h"
	#import <sys/mman.h>
	#import <sys/stat.h>
	
	int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize) {
	    int outError;
	    int fileDescriptor;
	    struct stat statInfo;
	    
	    // Return safe values on error.
	    outError = 0;
	    *outDataPtr = NULL;
	    *outDataLength = 0;
	    
	    // Open the file.
	    fileDescriptor = open( inPathName, O_RDWR, 0 );
	    if(fileDescriptor < 0) {
	        outError = errno;
	    } else {
	        // We now know the file exists. Retrieve the file size.
	        if( fstat( fileDescriptor, &statInfo ) != 0 ) {
	            outError = errno;
	        } else {
	            ftruncate(fileDescriptor, statInfo.st_size + appendSize);
	            fsync(fileDescriptor);
	            *outDataPtr = mmap(NULL,
	                               statInfo.st_size + appendSize,
	                               PROT_READ|PROT_WRITE,
	                               MAP_FILE|MAP_SHARED,
	                               fileDescriptor,
	                               0);
	            if( *outDataPtr == MAP_FAILED ) {
	                outError = errno;
	            } else {
	                // On success, return the size of the mapped file.
	                *outDataLength = statInfo.st_size;
	            }
	        }
	        
	        // Now close the file. The kernel doesn’t use our file descriptor.
	        close( fileDescriptor );
	    }
	    
	    return outError;
	}
	
	
	void ProcessFile(const char * inPathName) {
	    size_t dataLength;
	    void * dataPtr;
	    char *appendStr = " append_key";
	    int appendSize = (int)strlen(appendStr);
	    if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
	        dataPtr = dataPtr + dataLength;
	        memcpy(dataPtr, appendStr, appendSize);
	        // Unmap files
	        munmap(dataPtr, appendSize + dataLength);
	    }
	}
	
	@interface ViewController ()
	
	@end
	
	@implementation ViewController
	
	- (void)viewDidLoad {
	    [super viewDidLoad];
	    
	    NSString * path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
	    NSLog(@"path: %@", path);
	    NSString *str = @"test str";
	    [str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
	    
	    ProcessFile(path.UTF8String);
	    NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
	    NSLog(@"result:%@", result);
	}

四、MMKV 和 mmap

① MMKV 简介
  • NSUserDefault 是常见的缓存工具,但是数据有时会同步不及时,比如说在 crash 前保存的值很容易出现保存失败的情况,在 App 重新启动之后读取不到保存的值。
  • MMKV 很好的解决了 NSUserDefault 的局限,但是同样由于其独特设计,在数据量较大、操作频繁的场景下,会产生性能问题。这里的使用给出两个建议:
    • 不要全部用 defaultMMKV,根据业务大的类型做聚合,避免某一个 MMKV 数据过大,特别是对于某些只会出现一次的新手引导、红点之类的逻辑,尽可能按业务聚合,使用多个 MMKV 的对象;
    • 对于需要频繁读写的数据,可以在内存持有一份数据缓存,必要时再更新到 MMKV。
② MMKV 原理
  • 内存准备:通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由 iOS 负责将内存回写到文件,不必担心 crash 导致数据丢失。
  • 数据组织:数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。
	message KV {
		string key = 1 ;
		buffer value = 2;
	}
	- (B00L)setInt32:(int32 t)value forKey:(NSString*)key {
		auto data = PBEncode(value); 
		return [self setData:data forKey:key];
	}
	- (BO0L)setData: (NSData*)data forKey:(NSString*)key {
		auto kv = KV[key,data];
		auto buf = PBEncode(kv);
		return [self write: buf];
	}
  • 写入优化:标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。
  • 空间增长:使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下,这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
	- (B00L)append: (NSData*)data {
		if (space >= data.length) {
			append(fd, data);
		} else {
			newData = unique(m_allKV);
			if (total_space >= newData.length) {
				write(fd, newData);
			} else {
				while (total_space < newData.length) {
					total_ space *= 2;
				}
				ftruncate(fd, total . space);
				write(fd, newData);
			}
		}
	}
  • 数据有效性:考虑到文件系统、操作系统都有一定的不稳定性,另外增加了 crc 校验,对无效数据进行甄别。
  • MMKV 性能:写了个简单的测试,将 MMKV、NSUserDefaults 的性能进行对比(循环写入1w 次数据,测试环境:iPhone X 256G, iOS 11.2.6,单位:ms):

在这里插入图片描述

  • 可见 MMKV 性能远远优于 iOS 自带的 NSUserDefaults。另外,在测试中发现,NSUserDefaults 在每 2-3 次测试,就会有 1 次比较耗时的操作,怀疑是触发了数据 synchronize 重整写入。对比之下,MMKV 即使触发数据重整,也保持了性能的稳定高效。

五、NSData 与 mmap

  • NSData 有一个静态方法和 mmap 有关系:
	+ (id)dataWithContentsOfFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask error:(NSError **)errorPtr;
	
	typedef NS_OPTIONS(NSUInteger, NSDataReadingOptions) {
	
	    // Hint to map the file in if possible and safe. 在保证安全的前提下使用 mmap
	    NSDataReadingMappedIfSafe =   1UL << 0,
	    // Hint to get the file not to be cached in the kernel. 不要缓存。如果该文件只会读取一次,这个设置可以提高性能
	    NSDataReadingUncached = 1UL << 1,
	    // Hint to map the file in if possible. This takes precedence over NSDataReadingMappedIfSafe if both are given.  总使用 mmap
	    NSDataReadingMappedAlways API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)) = 1UL << 3,
	    ...
	};
  • Mapped 的意思是使用 mmap,那么 ifSafe 是什么意思呢?NSDataReadingMappedIfSafe 和 NSDataReadingMappedAlways 有什么区别?
  • 如果使用 mmap,则在 NSData 的生命周期内,都不能删除对应的文件。
  • 如果文件是在固定磁盘,非可移动磁盘、网络磁盘,则满足 NSDataReadingMappedIfSafe。对 iOS 而言,这个 NSDataReadingMappedIfSafe = NSDataReadingMappedAlways。
  • 那什么情况下应该用对应的参数?
    • 如果文件很大,直接使用 dataWithContentsOfFile 方法,会导致 load 整个文件,出现内存占用过多的情况;此时用 NSDataReadingMappedIfSafe,则会使用 mmap 建立文件映射,减少内存的占用。
    • 使用场景:视频加载。视频文件通常比较大,但是使用的过程中不会同时读取整个视频文件的内容,可以使用 mmap 优化。

六、总结

  • mmap 就是文件的内存映射。通常读取文件是将文件读取到内存,会占用真正的物理内存;而 mmap 是用进程的内存虚拟地址空间去映射实际的文件中,这个过程由操作系统处理。mmap 不会为文件分配物理内存,而是相当于将内存地址指向文件的磁盘地址,后续对这些内存进行的读写操作,会由操作系统同步到磁盘上的文件。
  • iOS 中使用 mmap 可以用 c 方法的 mmap(),也可以使用 NSData 的接口带上NSDataReadingMappedIfSafe 参数。前者自由度更大,后者用于读取数据。
  • mmap 优点:
    • 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代 I/O 读写,提高了文件读取效率。
    • 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
    • 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
    • 同时,如果进程 A 和进程 B 都映射了区域 C,当 A 第一次读取 C 时通过缺页从磁盘复制文件页到内存中;但当 B 再读 C 的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
    • 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件 I/O 操作,极大影响效率。这个问题可以通过 mmap 映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap 都可以发挥其功效。

热门文章

暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

GMX 命令分类列表

建模和计算操作命令&#xff1a; 1.1 . 创建拓扑与坐标文件 gmx editconf - 编辑模拟盒子以及写入子组(subgroups) gmx protonate - 结构质子化 gmx x2top - 根据坐标生成原始拓扑文件 gmx solvate - 体系溶剂化 gmx insert-molecules - 将分子插入已有空位 gmx genconf - 增加…
暂无图片
编程学习 ·

一文高效回顾研究生课程《数值分析》重点

数值分析这门课的本质就是用离散的已知点去估计整体&#xff0c;就是由黑盒子产生的结果去估计这个黑盒子。在数学里这个黑盒子就是一个函数嘛&#xff0c;这门课会介绍许多方法去利用离散点最大化地逼近这个函数&#xff0c;甚至它的导数、积分&#xff0c;甚至微分方程的解。…
暂无图片
编程学习 ·

在职阿里5年,一个28岁女软测工程师的心声

简单的先说一下&#xff0c;坐标杭州&#xff0c;14届本科毕业&#xff0c;算上年前在阿里巴巴的面试&#xff0c;一共有面试了有6家公司&#xff08;因为不想请假&#xff0c;因此只是每个晚上去其他公司面试&#xff0c;所以面试的公司比较少&#xff09; ​ 编辑切换为居中…
暂无图片
编程学习 ·

字符串左旋c语言

目录 题目&#xff1a; 解题思路&#xff1a; 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 总代码&#xff1a; 题目&#xff1a; 实现一个函数&#xff0c;可以左旋字符串中的k个字符。 例如&#xff1a; ABCD左旋一个字符得到BCDA ABCD左旋两个字符…
暂无图片
编程学习 ·

设计模式--观察者模式笔记

模式的定义与特点 观察者&#xff08;Observer&#xff09;模式的定义&#xff1a;指多个对象间存在一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xf…
暂无图片
编程学习 ·

睡觉突然身体动不了,什么是睡眠痽痪症

很多朋友可能有这样的体验&#xff0c;睡觉过程中突然意识清醒&#xff0c;身体却动弹不了。这时候感觉非常恐怖&#xff0c;希望旁边有一个人推自己一下。阳光以前也经常会碰到这样的情况&#xff0c;一年有一百多次&#xff0c;那时候很害怕晚上到来&#xff0c;睡觉了就会出…
暂无图片
编程学习 ·

深入理解C++智能指针——浅析MSVC源码

文章目录unique_ptrshared_ptr 与 weak_ptrstd::bad_weak_ptr 异常std::enable_shared_from_thisunique_ptr unique_ptr 是一个只移型别&#xff08;move-only type&#xff0c;只移型别还有std::mutex等&#xff09;。 结合一下工厂模式&#xff0c;看看其基本用法&#xff…
暂无图片
编程学习 ·

@TableField(exist = false)

TableField(exist false) //申明此字段不在数据库存在&#xff0c;但代码中需要用到它&#xff0c;通知Mybatis-plus在做写库操作是忽略它。,.
暂无图片
编程学习 ·

Java Web day15

第十二章文件上传和下载 一、如何实现文件上传 要实现Web开发中的文件上传功能&#xff0c;通常需要完成两步操作&#xff1a;一.是在Web页面中添加上传输入项&#xff1b;二是在Servlet中读取上传文件的数据&#xff0c;并保存到本地硬盘中。 需要使用一个Apache组织提供一个…
暂无图片
编程学习 ·

【51nod 2478】【单调栈】【前缀和】小b接水

小b接水题目解题思路Code51nod 2478 小b接水 题目 输入样例 12 0 1 0 2 1 0 1 3 2 1 2 1输出样例 6解题思路 可以发现最后能拦住水的都是向两边递减高度&#xff08;&#xff1f;&#xff09; 不管两个高积木之间的的积木是怎样乱七八糟的高度&#xff0c;最后能用来装水的…
暂无图片
编程学习 ·

花了大半天写了一个UVC扩展单元调试工具

基于DIRECTSHOW 实现的&#xff0c;用的是MFC VS2019. 详见&#xff1a;http://www.usbzh.com/article/detail-761.html 获取方法 加QQ群:952873936&#xff0c;然后在群文件\USB调试工具&测试软件\UVCXU-V1.0(UVC扩展单元调试工具-USB中文网官方版).exe USB中文网 USB中文…
暂无图片
编程学习 ·

贪心(一):区间问题、Huffman树

区间问题 例题一&#xff1a;区间选点 给定 N 个闭区间 [ai,bi]请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 N&#xff0c;表示区间数。 接下来 …
暂无图片
编程学习 ·

C语言练习实例——费氏数列

目录 题目 解法 输出结果 题目 Fibonacci为1200年代的欧洲数学家&#xff0c;在他的着作中曾经提到&#xff1a;「若有一只免子每个月生一只小免子&#xff0c;一个月后小免子也开始生产。起初只有一只免子&#xff0c;一个月后就有两只免子&#xff0c;二个月后有三只免子…
暂无图片
编程学习 ·

Android开发(2): Android 资源

个人笔记整理 Android 资源 Android中的资源&#xff0c;一般分为两类&#xff1a; 系统内置资源&#xff1a;Android SDK中所提供的已经定义好的资源&#xff0c;用户可以直接拿来使用。 用户自定义资源&#xff1a;用户自己定义或引入的&#xff0c;只适用于当前应用的资源…
暂无图片
编程学习 ·

零基础如何在短时间内拿到算法offer

​算法工程师是利用算法处理事物的职业 算法&#xff08;Algorithm&#xff09;是一系列解决问题的清晰指令&#xff0c;也就是说&#xff0c;能够对一定规范的输入&#xff0c;在有限时间内获得所要求的输出。 如果一个算法有缺陷&#xff0c;或不适合于某个问题&#xff0c;执…
暂无图片
编程学习 ·

人工智能:知识图谱实战总结

人工智能python&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习人工智能&#xff1a;知识图谱实战前言一、实体建模工具Protegepython&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习 人工智能&#xff1a;知识图…
暂无图片
编程学习 ·

【无标题】

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…