小编:本文由 WebInfra 团队姚忠孝、杨文明、张地编写。意在通过深入剖析常用的内存分配器的关键实现,以理解虚拟机动态内存管理的设计哲学,并为实现虚拟机高效的内存管理提供指引。
在现代计算机体系结构中,内存是系统核心资源之一。虚拟机( VM )作为运行程序的抽象"计算机",内存管理是其不可或缺的能力,其中主要包括如内存分配、垃圾回收等,而其中内存分配器又是决定"计算机"内存模型,以及高效内存分配和回收的关键组件。
如上图所示,各种编程都提供了动态分配内存对象的能力,例如创建浏览器 Dom 对象,创建 Javascript 的内存数组对象( Array Buffer 等),以及面向系统编程的 C / C++ 中的动态分配的内存等。在应用开发者角度看,通过语言或者库提供的动态内存管理(分配,释放)的接口就是实现对象内存的分配和回收,而不需要关心底层的具体实现,例如,所分配对象的实际内存大小,对象在内存中的位置排布(对象地址),可以用于分配的内存区域,对象何时被回收,如何处理多线程情况下的内存分配等等;而对于虚拟机或者 Runtime 的开发者来说,高效的内存分配和回收管理是其核心任务之一,并且内存分配器的主要目标包括如下几方面:
1. 减少内存碎片,包括内部碎片和外部碎片
32
字节,分配了 40
字节,多余的 8
字节就是内部碎片。16
字节的内存,但是内存分配器中保存着部分 8
字节的内存,一直分配不出去。2. 提高性能
内存分配需要通过内核分配虚拟地址空间和物理内存,频繁的系统调用和多线程竞争显著的降低了内存分配的性能。内存分配器通过接管内存的分配和回收,无论在单线程还是多线程场景下都可以很好的起到提升性能的作用。
3. 提高安全性
随着系统和应用对安全性的关注度提升,默认的内存分配机制无论在内存数据布局,还是对硬件安全机制的使用上都无法最大化的满足应用诉求,因此自定义内存分配器可以针对特定的系统和应用充分的利用硬件安全机制( MTE )等,同时也能够在实际系统中引入内存安全性检测机制来进一步提升安全性。
因此,本文通过深入分析常用的内存分配器的关键实现( dlmalloc , jemalloc , scudo , partition-alloc ),来更好的理解背后的设计考量和设计哲学,以为我们实现高效,轻量的运行时和虚拟机内存分配器,内存管理模型提供指引。
实际的内存分配需要按照所申请的内存大小分别处理,如下所述:
小内存通过空闲链表管理空闲内存的使用状态;下图是某一时刻可能的内存快照情况,每个内存大小( size )都可作为空闲链表数组的下标访问对应大小的空闲链表。
基于空闲链表维护的空闲内存状态,采用如下的分配顺序和分配策略进行实际内存分配。
大内存不采用空闲链表数组进行管理(虚拟机地址空间方范围大,时间和空间效率均低),采用 trie-tree 作为大内存的状态维护结构,每次内存分配和释放在 trie-tree 上进行高效的搜索和插入。
大内存采用如下的分配顺序和分配策略进行实际内存分配。
对于超大内存,直接通过 mmap 从操作系统分配内存。
该内存管理器的主要目标是提升多线程内存使用的性能( efficiency 、 locality )和较少内存碎片(最大优势是强大的多线程分配能力)
上图展示了内存分配的主要流程及数据结构。jemalloc
通过 areans 进行内存统一内存分配和回收, 由于两个arena在地址空间上几乎不存在任何联系, 就可以在无锁的状态下完成分配。同样由于空间不连续, 落到同一个 cache-line 中的几率也很小, 保证了各自独立。理想情况下每个线程由一个 arena
负责其内存的分配和回收,由于 arena 的数量有限, 因此不能保证所有线程都能独占 arena , Android 系统中默认采用两个 arena ,因此会采用负载均衡算法( round-robin )实现线程到 arena 的均衡分配并通过内部的 lock 保持同步。
多个线程可能共享同一个 arena , jemalloc
为了进一步避免分配内存时出现多线程竞争锁,因此为每个线程创建一个 " tcache ",专门用于当前线程的内存申请。每次申请释放内存时, jemalloc
会使用对应的线程从 tcache 中进行内存分配(其实就是 Thread Local 空闲链表)。
每个线程内部的内存分配按如下3种不同大小的内存请求分别处理:
小对象( Small Object ) 根据 Small Object 对象大小,从 Run 中的指定 " region " 返回。
大对象( Large Object )
Large Object 大小比 chunk 小,比 page 大,会单独返回一个 " Run "(1或多个 page 组成)。
超大对象( Huge Object ) > 4 MB
huge object 的内存直接采用 mmap 从 system memory 中申请(独立" Chunk "),并由一棵与 arena 独立的红黑树进行管理。
Scudo 这个名字源自 Escudo ,在西班牙语和葡萄牙语中表示“盾牌”。为了安全性考虑,从 Android 11 版本开始,Scudo 会用于所有原生代码(低内存设备除外,其仍使用 jemalloc )。在运行时,所有可执行文件及其库依赖项的所有原生堆分配和取消分配操作均由 Scudo 完成;如果在堆中检测到损坏情况或可疑行为,该进程会中止。(以下内容以 Android-11 64位系统实现为分析目标)
Scudo 定义了 Primary 和 Secondary 两种类型分配器[4],当需求小于 max_class_size ( 64 k )时使用 Primary Allocator ,大于 max_class_size 时使用 Secondary Allocator 。
struct AndroidConfig {
using SizeClassMap = AndroidSizeClassMap;
#if SCUDO_CAN_USE_PRIMARY64
// 256MB regions
typedef SizeClassAllocator64<SizeClassMap, 28U, 1000, 1000,
/*MaySupportMemoryTagging=*/true>
Primary;
#else
// 256KB regions
typedef SizeClassAllocator32<SizeClassMap, 18U, 1000, 1000> Primary;
#endif
// Cache blocks up to 2MB
typedef MapAllocator<MapAllocatorCache<32U, 2UL << 20, 0, 1000>> Secondary;
template <class A>
using TSDRegistryT = TSDRegistrySharedT<A, 2U>; // Shared, max 2 TSDs.
};
*Primary Allocator
Primary Allocator [5]首先会根据 AndroidSizeClassConfig [6]中的配置申请一个完整的虚拟内存空间,并将虚拟内存空间分成不同的区域( sizeClass ),每块区域只能分配出固定空间,区域1只能分配32 bytes (包含 header ),区域2只能分配48 bytes 。
Primary Allocator 从操作系统申请" PrimaryBase "指向的虚拟内存区域后,按照如下的配置为虚拟内存区域划分为 32 个 256 M 的内存区域( SizeClass )。
每个 SizeClass 提供特定大小的内存分配,其内存区域按照 RegionInfo 结构布局,其中" randon offset "是0~16页的随机值,以使随机化 ReginBeg 地址降低定向攻击的风险。
每个 SizeClass 区域初始分配内存为 MAP_NOACCESS ,当收到内存分配请求时, ClassSize 会分配出一块可用的内存区域( MappedUser ),并以 SizeClass 指定的 block 大小切分成可用使用的 Regions 。当进行 Regions 的分配的同时,每个可用的 Region 均通过 TranferBatch 进行管理( TBatch ), 这样 TBatch 的链表就可以作为 FreeList 供内存分配使用。
为了安全性的考虑,最终返回的内存按照上图" block “所示的内存布局组织。其中” chunk-header "[7]是为了保证每一个 chunk 区域的完整性, Checksum 用的是较为简单的CRC算法,此外,内存释放过程中 Scudo 增加了 header 检测机制,一方面可以检测内存踩踏和多次释放,另一方面也阻止了野指针的访问。
struct UnpackedHeader {
uptr ClassId : 8;
u8 State : 2;
u8 Origin : 2;
uptr SizeOrUnusedBytes : 20;
uptr Offset : 16;
uptr Checksum : 16;
};
struct AndroidSizeClassConfig {
static const uptr NumBits = 7;
static const uptr MinSizeLog = 4;
static const uptr MidSizeLog = 6;
static const uptr MaxSizeLog = 16;
static const u32 MaxNumCachedHint = 14;
static const uptr MaxBytesCachedLog = 13;
static constexpr u32 Classes[] = {
0x00020, 0x00030, 0x00040, 0x00050, 0x00060, 0x00070, 0x00090, 0x000b0,
0x000c0, 0x000e0, 0x00120, 0x00160, 0x001c0, 0x00250, 0x00320, 0x00450,
0x00670, 0x00830, 0x00a10, 0x00c30, 0x01010, 0x01210, 0x01bd0, 0x02210,
0x02d90, 0x03790, 0x04010, 0x04810, 0x05a10, 0x07310, 0x08210, 0x10010,
};
static const uptr SizeDelta = 16;
};
*Thread Local Cache
Scudo Primary Allocator 使用了 Thread Local Cache 机制来加速多线程下内存的分配,如下图所示。当一个线程需要分配内存时,它会通过各自对应的 TSD ( Thead Specific Data )来发起对象的申请,每个 TSD 对象通过各自的 Allocator::Cache 及其 TransferBatch 来寻找合适的空闲内存。但 TransferBatch 的大小是有限的,如果没有可用的内存会进一步利用 Primary Allocator 进行分配。
理想情况下每个线程由一个 TSD 负责其内存的分配和回收,由于 TSD 的数量有限, 因此不能保证所有线程都能独占 TSD , Android 系统中默认采用两个 TSD ,因此会采用负载均衡算法( round-robin )实现线程到 arean 的均衡分配并通过内部的 lock 保持同步。
*Secondary Allocator
Secondary Allocator [8]主要用于大内存的分配(> max_size_class ),直接采用 mmap 分配出一块满足要求的内存并按照 LargeBlock::Header 进行组织, 并统一在 InUseBlocks 链表中统一管理,如下图所示。
为了安全性考虑, LargeBlock 采用如下图 LargeBlock Layout 进行组织,其中:
此外,为了提高效率效率, LargeBlock 在释放的时候会通过 MapAllocatorCache 将可被复用的空闲内存进行缓存,在大内存分配中优先从 Cache 中进行分配。其中 CachedBlock 和 LargeBlock::Header 基本一致,都是对 LargeBlock 内存布局信息的记录和管理。
Scudo Android R 内存分配核心流程如下所示。
NOINLINE void *allocate(uptr Size, Chunk::Origin Origin,
uptr Alignment = MinAlignment,
bool ZeroContents = false) {
initThreadMaybe(); // 初始化TSD Array 和 Primary SizeClass地址空间
// skip trivials.......
// If the requested size happens to be 0 (more common than you might think),
// allocate MinAlignment bytes on top of the header. Then add the extra
// bytes required to fulfill the alignment requirements: we allocate enough
// to be sure that there will be an address in the block that will satisfy
// the alignment.
const uptr NeededSize =
roundUpTo(Size, MinAlignment) +
((Alignment > MinAlignment) ? Alignment : Chunk::getHeaderSize());
// skip trivials.......
void *Block = nullptr;
uptr ClassId = 0;
uptr SecondaryBlockEnd;
if (LIKELY(PrimaryT::canAllocate(NeededSize))) {
// 从 Primary Allocator分配small object
ClassId = SizeClassMap::getClassIdBySize(NeededSize);
DCHECK_NE(ClassId, 0U);
bool UnlockRequired;
auto *TSD = TSDRegistry.getTSDAndLock(&UnlockRequired);
Block = TSD->Cache.allocate(ClassId);
// If the allocation failed, the most likely reason with a 32-bit primary
// is the region being full. In that event, retry in each successively
// larger class until it fits. If it fails to fit in the largest class,
// fallback to the Secondary.
if (UNLIKELY(!Block)) {
while (ClassId < SizeClassMap::LargestClassId) {
Block = TSD->Cache.allocate(++ClassId);
if (LIKELY(Block)) {
break;
}
}
if (UNLIKELY(!Block)) {
ClassId = 0;
}
}
if (UnlockRequired)
TSD->unlock();
}
// 如果分配的是大内存,或者Primary 无法分配小内存,
// 则直接在Secondary Allocator进行分配
if (UNLIKELY(ClassId == 0))
Block = Secondary.allocate(NeededSize, Alignment, &SecondaryBlockEnd,
ZeroContents);
// skip trivials.......
const uptr BlockUptr = reinterpret_cast<uptr>(Block);
const uptr UnalignedUserPtr = BlockUptr + Chunk::getHeaderSize();
const uptr UserPtr = roundUpTo(UnalignedUserPtr, Alignment);
void *Ptr = reinterpret_cast<void *>(UserPtr);
void *TaggedPtr = Ptr;
// skip trivials.......
// 根据返回内存地址,设置chunk-header对象数据
Chunk::UnpackedHeader Header = {};
if (UNLIKELY(UnalignedUserPtr != UserPtr)) {
const uptr Offset = UserPtr - UnalignedUserPtr;
DCHECK_GE(Offset, 2 * sizeof(u32));
// The BlockMarker has no security purpose, but is specifically meant for
// the chunk iteration function that can be used in debugging situations.
// It is the only situation where we have to locate the start of a chunk
// based on its block address.
reinterpret_cast<u32 *>(Block)[0] = BlockMarker;
reinterpret_cast<u32 *>(Block)[1] = static_cast<u32>(Offset);
Header.Offset = (Offset >> MinAlignmentLog) & Chunk::OffsetMask;
}
Header.ClassId = ClassId & Chunk::ClassIdMask;
Header.State = Chunk::State::Allocated;
Header.Origin = Origin & Chunk::OriginMask;
Header.SizeOrUnusedBytes =
(ClassId ? Size : SecondaryBlockEnd - (UserPtr + Size)) &
Chunk::SizeOrUnusedBytesMask;
// 设置chunk-header,CheckSum用于完整性校验
Chunk::storeHeader(Cookie, Ptr, &Header);
// skip trivials.......
return TaggedPtr;
}
以上代码包括的核心流程如下图所示:
Scudo
虽然它的分配策略更加简化,安全性上得到了很大的改进,但为了安全性引入 chunk header 等元数据管理,内存随机化和对齐引入内存碎片一定程度上降低了内存的使用效率;此外,基于 chunk header 的内存校验以及内存安全性保障也一定程度上降低了效率(性能)。
以上的分析主要参照了 Android R 中的实现,最初来源于 LLVM 中 scudo 的实现, LLVM 中有 scudo_allocator [9] 和 standalone [10] 2份实现,具体内容可以从[9][10]作为入口进行源码的分析。
PartitionAlloc 是 Chromium 的跨平台分配器,优化客户端而不是服务器工作负载的内存使用,并专注于有意义的最终用户活动,而不是在实际使用中并不重要的微基准[16]。
*Central Partition Allocator [16]
PartitionAlloc 通过分区( Partition )来隔离各内存区域。其中每个分区都使用基于 slab 的分配器来分配内存, PartitionAlloc 预先保留" Super Page “作为分区虚拟地址空间( Slab )。每个"Super Page”( Slab )按照实际可分配的最小内存单元大小( Slot )分成各自独立的" Slot Span ",每个 Slot Span 包含多个 Partition Page 并归属于特定的桶( Partiton Bucket )用于提供中小内存的分配。
PartitionAlloc 针对大内存分配(> kMaxBucketed )是通过直接内存映射( direct map )实现的(特殊的 bucket 管理),为了保证和小内存分配结构上的统一性,直接内存映射内存结构采用(伪装)和小内存分配类似的 Super Page 进行管理,并且保留了类似的内存布局布局(如下图所示)。
如上图所示, PartitionAlloc 中每个分区的内存通过一个 PartitionRoot 进行管理," PartitionRoot “中维护了一个 Bucket 数组;每个 Bucket 维护了” slot span “的集合;随着分配请求的到来, slot span 逐渐得到物理内存( Commit ),并按照所关联桶的可分配内存的大小,将物理内存切分为 slot 的连续内存区域。PartitionAlloc 除了支持多个独立的分区来提升安全性外;每个” Super Page “被分割成多个” Partition Page “,其中第一个和最后一个” Partition-Page "主要作为保护页使用( PROT_NONE )。( Super Page 选择 2 MB 是因为 PTE 在 ARM 、 ia 32 和 x 64上对应的虚拟地址是2 M )。
*Partition Layer
PartitionAlloc 虽然通过分区来隔离各区域,但在同一分区内多线程访问受单个分区锁的保护。为了缓解多线程的数据竞争和扩展性问题,采用如下三层架构来提高性能:
为了缓解多线程内存分配的数据竞争问题,每个线程的数据缓存区( TLS )保存了少量用于常用中小内存分配的 Slots 。因为这些 Slots 是按线程存储的,所以可以在没有锁的情况下分配它们,并且只需要更快的线程本地存储查找,从而提高进程中的缓存局部性。每个线程的缓存已经过定制以满足大多数请求,通过批量分配和释放第二层的内存,分期锁获取,并在不捕获过多内存的情况下进一步改善局部性。
Slot span free-lists 在每线程缓存未命中时调用。对于每个对应的 bucket size,PartitionAlloc 维护与该大小相关联的 Slot Span ,并从 Slot Span 的空闲列表中获取一个空闲 Slot 。这仍然是一条快速路径( fast path ),但比每线程缓存慢,因为它需要锁定。但是,此部分仅适用于每个线程缓存不支持的较大分配,或者作为填充每个线程缓存的批处理。
最后,如果桶中没有空闲的 slot ,那么 Slot span management 要么从 Super Page ( slab )中挖出空间用于新的slot span,要么从操作系统分配一个全新的Super Page ( slab )。这是一条慢速路径( slow path ),很慢但非常不经常操作。
以上简单介绍了 PartitionAlloc 中分区的关键结构, PartitionAlloc 在 Chromium 中主要维护了四个分区,针对每种分配器的用途采取不同的优化方案,并根据实际对象的类型在四个分区中任意一个上分配对象。
PartitionAlloc 在 Node 分区和 LayoutObject 分区上分配时不会获取锁,因为它保证 Node 和 LayoutObject 仅由主线程分配。PartitionAlloc 在 Buffer 分区和 FastMalloc 分区上分配时获取锁。PartitionAlloc 使用自旋锁以提高性能。
*安全性
安全因素的考虑也是PartitionAlloc最重要的目标之一,这里利用虚拟地址空间来达到安全加固的目的:不同的分区存在于隔离的地址空间;当某个分区内存页上所有对象都被释放掉之后,其物理内存归还于系统后,但其地址空间仍被此分区保留,这样就保证了此地址空间只能被此分区重用,从而避免了信息泄露;PartitionAlloc的内存布局,提供了以下安全属性:
传统内存分配器( jemalloc , etc.)调用 malloc 为对象分配内存时,无法指定分配器将在哪里存储什么样的数据,并且无法指定要存储它的位置。C++ 类对象可能紧挨着包含密钥的字符串,该密钥可能与函数指针的结构相邻。在所有这些数据之间是分配器用来管理堆的元数据(通常为双向链表和标志存储的指针),这为漏洞寻找要覆盖的数据目标时为他提供了更多选择。例如,当利用释放后使用漏洞时,我们希望将类型 B 的对象放置在先前分配类型 A 的对象的位置。然后我们可以触发类型 A 的过时指针的取消引用,该指针反过来使用类型 B 对象中的位。这通常是可能的,因为两个对象都分配在同一个堆中[43],而 PartitionAlloc 具有如下特性可以满足分配需求。
由于 PartitionAlloc 是面向 Chromium 特定应用场景的高效内存分配器,为特定内存使用场景的定制化能力能够提供高效的内存分配和回收(例如, layout 分区, node 分区),但面向通用的内存分配场景及高安全场景如果能够和 jemalloc, secudo 能力进行有效的融合( porting ),可能会是一个可行的路径和方向( MTE 等)。
从如上的分析可以看出,分配器的整体性能和空间效率取决于各种因素之间的权衡,例如缓存多少、内存分配/回收策略等,实际的时间和空间效率需要根据具体场景衡量并针对性的优化。如下从性能,空间效率,以及 benchmark 方面提供一些总结和参考。
内存分配器 | 说明 |
---|---|
dlmalloc | https://github.com/ARMmbed/dlmalloc · 非线程安全,多线程分配效率低 · 内存使用率较低(内存碎片多) · 分配效率低 · 安全性低 |
jemlloc | https://github.com/jemalloc/jemalloc/blob/dev/INSTALL.md **· **代码体积较大,元数据占据内存大 · 内存分配效率高(基于run(桶) + region) · 内存使用率高 · 安全性中 |
scudo | https://android.googlesource.com/platform/external/scudo/ · 更注重安全性,内存使用效率和性能上存在一定的损失 · 内存分配效率偏高(sizeclass + region, 安全性+checksum校验) · 内存使用率低(元数据,安全区,地址随机化内存占用多) · 安全性高( 安全性+checksum校验,地址随机化,保护区) |
partition-alloc | https://source.chromium.org/chromium/chromium/src/+/main:base/allocator/partition_allocator/PartitionAlloc.md · 代码体积小 · 内存使用率偏高(相比于jemalloc多保护区,基于range的分配) · 安全性偏高(相比sudo少安全性,完整性校验,地址随机化) · 内存分配效率高(基于bucket + slot,分区特定优化) · 支持"全"平台 PartitionAlloc Everywhere (BlinkOn 14) : https://www.youtube.com/watch?v=QfY-WMFjjKA |
NOTE:Benchmark alternate malloc implementations:
Allocator | QPS (higher is better) | Max RSS (lower is better) |
---|---|---|
tcmalloc (internal) | 410K | 357MB |
jemalloc | 356K | 1359MB |
dlmalloc (glibc) | 295K | 333MB |
mesh | 142K | 710MB |
portableumem | 24K | 393MB |
hardened_malloc* | 18K | 458MB |
guarder | FATALERROR** | |
freeguard | SIGSEGV*** | |
scudo (standalone) | 400K | 318MB |
[1]. A Tale of Two Mallocs: On Android libc Allocators – Part 1–dlmalloc https://blog.nsogroup.com/a-tale-of-two-mallocs-on-android-libc-allocators-part-1-dlmalloc/
[2]. jemalloc: https://github.com/jemalloc/jemalloc/blob/dev/INSTALL.md
[3]. A Tale of Two Mallocs: On Android libc Allocators – Part 2–jemalloc: https://blog.nsogroup.com/a-tale-of-two-mallocs-on-android-libc-allocators-part-2-jemalloc/
[4] AndroidConfig: https://android.googlesource.com/platform/external/scudo/+/refs/tags/android-11.0.0_r3/standalone/allocator_config.h#39
[5].SizeClassAllocator64: https://android.googlesource.com/platform/external/scudo/+/refs/tags/android-11.0.0_r3/standalone/primary64.h#46
[6]. AndroidSizeClassConfig:
https://android.googlesource.com/platform/external/scudo/+/refs/tags/android-11.0.0_r3/standalone/size_class_map.h#171
[7]. Chunk: https://android.googlesource.com/platform/external/scudo/+/refs/tags/android-11.0.0_r3/standalone/chunk.h#65
[8] MapAllocator : https://android.googlesource.com/platform/external/scudo/+/refs/tags/android-11.0.0_r3/standalone/secondary.h#225
[9] scudo_allocator :https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/scudo/scudo_allocator.h
[10] standalone: https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/scudo/standalone/combined.h
[11]. Scudo : https://source.android.com/devices/tech/debug/scudo
[12]. Scudo Hardened Allocator: https://llvm.org/docs/ScudoHardenedAllocator.html
[13]. System hardening in Android 11: https://android-developers.googleblog.com/2020/06/system-hardening-in-android-11.html
[14]. Android Native | Scudo内存分配器: https://juejin.cn/post/6914550038140026887
[15]. Scudo Allocator : https://zhuanlan.zhihu.com/p/235620563
[16]. Efficient And Safe Allocations Everywhere! : https://blog.chromium.org/2021/04/efficient-and-safe-allocations-everywhere.html
[17]. PartitionAlloc Design: https://chromium.googlesource.com/chromium/src/+/master/base/allocator/partition_allocator/PartitionAlloc.md
[18]. partition_alloc_constants: https://source.chromium.org/chromium/chromium/src/+/main:base/allocator/partition_allocator/partition_alloc_constants.h
[19]. Unified allocator shim: https://chromium.googlesource.com/chromium/src/base/+/refs/heads/main/allocator/README.md
[20]. Porting PartitionAlloc To PDFium: https://struct.github.io/pdfiu
文章浏览阅读238次。PHP的getimagesize读取远程文件,使用的方法本质上跟file_get_contents一样,所以都会出现非常耗时的情况。_php getimagesize远程图片
文章浏览阅读5.7k次,点赞7次,收藏17次。计算机应用技术比较偏向软件方向,培养掌握计算机应用专业必要的基础理论、常用计算机软件操作和编程语言,培养目标是具有较强实践技能的高级计算机应用型人才。专业课主要有:计算机软硬件技术基础、Linux操作系统、数据库系统SQL、数据结构与C程序设计、计算机网络原理、高级语言汇编、Java语言程序设计、图形图像应用处理(PhotoShop)、微机原理与接口技术、C语言、数据结构、操作系统、平面设计、VB..._计算机网络红人计算机应用区别
文章浏览阅读916次,点赞18次,收藏13次。是 Docker 官方维护的一个服务,用于存储和分发 Docker 官方镜像,包括一些常见的操作系统、编程语言运行时环境等。通过使用 Docker 官方镜像注册表,用户可以方便地访问和获取到官方维护的镜像,用于构建、运行和部署他们的容器化应用程序。kube-flannel是一个在Kubernetes集群中用于网络通信的网络解决方案。kube-flannel使用了一个虚拟的overlay网络,它允许Kubernetes节点之间的容器在不同的主机上进行通信,同时保持网络的简单性和性能。_docker容器regpositories网址出不来
文章浏览阅读805次。Two dimensional neuron modelsReduction to two dimensionsGeneral approach_phase plane analysis matlab
文章浏览阅读85次。转载地址:http://blog.csdn.net/fengkuanghun/article/details/7878862如何实现将View向上平移自身高度一半的距离?TranslateAnimation translate = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, ...
文章浏览阅读9.6k次,点赞3次,收藏9次。Unity Hub for Mac 安装_unity hub for mac
文章浏览阅读129次。该文章汇总了Maven2.0中常用的一些Property, 所以这些properties都是从Maven的官方文档和Maven的用户邮件中搜集过来的. 注意, 因为所有的pom.*属性在Maven3中已经不推荐了, 所以下面只介绍project.* Build-in Properties: 内带的属性${basedir}, pom.xml文件所在的目录${version}, ..._maven built-in
文章浏览阅读561次。EasyPlayer性能稳定、播放流畅,可支持的视频流格式有RTSP、RTMP、HLS、FLV、WebRTC等,具备较高的可用性。_easyplayer有声音无画面
文章浏览阅读900次,点赞17次,收藏14次。针对以上面试题,小编已经把面试题+答案整理好了。
文章浏览阅读1.3k次,点赞4次,收藏8次。虽然Zabbix Server管理页面提供了中文模式,但是在具体的模板监控项和触发器警告等等还是全部英文,所以这里说下这些汉化方法。Zabbix Server服务端默认使用的是Mariadb数据库,采用API修改监控项触发器的方法。如有翻译错误欢迎评论指正。_zabbix汉化
文章浏览阅读544次。一、Pentaho CDE的安装1、首先登录 Pentaho User Console2、打开MarketPlace 在这里可以看到所有可用的插件在这里你可以点击安装 Community Dashboard Editor注:这里为了方便连接数据库我安装了CDA 即Community Data Access 安装方法同上 (Pentao 5.0.1每安装一个插件后就要重启一次如果直接安装..._pentaho community edition
文章浏览阅读3.4k次,点赞13次,收藏20次。在上一篇: Android项目实战 | 从零开始写app(九)】Tablayout+ViewPager实现页面分类顶部标题页面联动切换 的基础上实现数据的填充展示由于首页会展示到推荐新闻列表,所以今天先把新闻模块的数据先请求下来,就跳着更吧,后面再继续完善首页~~这篇早早就写好了,奈何发布了几次老是说审核不通过,说内容违规???? 无可奈何~菜鸡一枚,写得不好,有问题的请指教~~文章导航一、【Android项目实战 | 从零开始写app(一)】 创建项目二、【Android项目实战 | 从零开_android listview和http实现获取新闻列表